From 76dcaf266ad7840caa5ca04165b13ea4380cfc0a Mon Sep 17 00:00:00 2001
From: Jarek Potiuk <jarek@potiuk.com>
Date: Thu, 17 Nov 2022 21:28:39 +0100
Subject: [PATCH] Limit the full tests to representative items in
 Python/Backend matrix

This chnage limts the matrix of tests run on CI when full tests are run
for all our tests - in the way that each of the items is for sure
present but not repeated when unnecessary.
---
 .../utils/exclude_from_matrix.py              | 47 +++++++++++++++++++
 .../airflow_breeze/utils/selective_checks.py  | 36 ++++++++++++--
 dev/breeze/tests/test_exclude_from_matrix.py  | 46 ++++++++++++++++++
 3 files changed, 125 insertions(+), 4 deletions(-)
 create mode 100644 dev/breeze/src/airflow_breeze/utils/exclude_from_matrix.py
 create mode 100644 dev/breeze/tests/test_exclude_from_matrix.py

diff --git a/dev/breeze/src/airflow_breeze/utils/exclude_from_matrix.py b/dev/breeze/src/airflow_breeze/utils/exclude_from_matrix.py
new file mode 100644
index 0000000000000..62793ebadbb32
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/exclude_from_matrix.py
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+
+def representative_combos(list_1: list[str], list_2: list[str]) -> list[tuple[str, str]]:
+    """
+    Include only representative combos from the matrix of the two lists - making sure that each of the
+    elements contributing is present at least once.
+    :param list_1: first list
+    :param list_2: second list
+    :return: list of combinations with guaranteed at least one element from each of the list
+    """
+    all_selected_combinations: list[tuple[str, str]] = []
+    for i in range(max(len(list_1), len(list_2))):
+        all_selected_combinations.append((list_1[i % len(list_1)], list_2[i % len(list_2)]))
+    return all_selected_combinations
+
+
+def excluded_combos(list_1: list[str], list_2: list[str]) -> list[tuple[str, str]]:
+    """
+    Return exclusion lists of elements that should be excluded from the matrix of the two list of items
+    if what's left should be representative list of combos (i.e. each item from both lists,
+    has to be present at least once in the combos).
+    :param list_1: first list
+    :param list_2: second list
+    :return: list of exclusions = list 1 x list 2 - representative_combos
+    """
+    all_combos: list[tuple[str, str]] = []
+    for item_1 in list_1:
+        for item_2 in list_2:
+            all_combos.append((item_1, item_2))
+    return [item for item in all_combos if item not in set(representative_combos(list_1, list_2))]
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 5147f0d7b1651..985a822f4dd30 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -21,6 +21,7 @@
 import sys
 from enum import Enum
 
+from airflow_breeze.utils.exclude_from_matrix import excluded_combos
 from airflow_breeze.utils.github_actions import get_ga_output
 from airflow_breeze.utils.kubernetes_utils import get_kubernetes_python_combos
 from airflow_breeze.utils.path_utils import AIRFLOW_SOURCES_ROOT
@@ -353,19 +354,46 @@ def providers_package_format_exclude(self) -> list[dict[str, str]]:
 
     @cached_property
     def postgres_exclude(self) -> list[dict[str, str]]:
-        return [{"python-version": "3.7"}] if self.full_tests_needed else []
+        if not self.full_tests_needed:
+            # Only basic combination so we do not need to exclude anything
+            return []
+        return [
+            # Exclude all combinations that are repeating python/postgres versions
+            {"python-version": python_version, "postgres-version": postgres_version}
+            for python_version, postgres_version in excluded_combos(
+                CURRENT_PYTHON_MAJOR_MINOR_VERSIONS, CURRENT_POSTGRES_VERSIONS
+            )
+        ]
 
     @cached_property
     def mssql_exclude(self) -> list[dict[str, str]]:
-        return [{"python-version": "3.8"}] if self.full_tests_needed else []
+        if not self.full_tests_needed:
+            # Only basic combination so we do not need to exclude anything
+            return []
+        return [
+            # Exclude all combinations that are repeating python/mssql versions
+            {"python-version": python_version, "mssql-version": mssql_version}
+            for python_version, mssql_version in excluded_combos(
+                CURRENT_PYTHON_MAJOR_MINOR_VERSIONS, CURRENT_MSSQL_VERSIONS
+            )
+        ]
 
     @cached_property
     def mysql_exclude(self) -> list[dict[str, str]]:
-        return [{"python-version": "3.10"}] if self.full_tests_needed else []
+        if not self.full_tests_needed:
+            # Only basic combination so we do not need to exclude anything
+            return []
+        return [
+            # Exclude all combinations that are repeating python/mysql versions
+            {"python-version": python_version, "mysql-version": mysql_version}
+            for python_version, mysql_version in excluded_combos(
+                CURRENT_PYTHON_MAJOR_MINOR_VERSIONS, CURRENT_MYSQL_VERSIONS
+            )
+        ]
 
     @cached_property
     def sqlite_exclude(self) -> list[dict[str, str]]:
-        return [{"python-version": "3.9"}] if self.full_tests_needed else []
+        return []
 
     @cached_property
     def kubernetes_versions(self) -> list[str]:
diff --git a/dev/breeze/tests/test_exclude_from_matrix.py b/dev/breeze/tests/test_exclude_from_matrix.py
new file mode 100644
index 0000000000000..49bb0e57345d9
--- /dev/null
+++ b/dev/breeze/tests/test_exclude_from_matrix.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+
+from airflow_breeze.utils.exclude_from_matrix import excluded_combos, representative_combos
+
+
+@pytest.mark.parametrize(
+    "list_1, list_2, expected_representative_list",
+    [
+        (["3.6", "3.7"], ["1", "2"], [("3.6", "1"), ("3.7", "2")]),
+        (["3.6", "3.7"], ["1", "2", "3"], [("3.6", "1"), ("3.7", "2"), ("3.6", "3")]),
+        (["3.6", "3.7"], ["1", "2", "3", "4"], [("3.6", "1"), ("3.7", "2"), ("3.6", "3"), ("3.7", "4")]),
+        (
+            ["3.6", "3.7", "3.8"],
+            ["1", "2", "3", "4"],
+            [("3.6", "1"), ("3.7", "2"), ("3.8", "3"), ("3.6", "4")],
+        ),
+    ],
+)
+def test_exclude_from_matrix(
+    list_1: list[str],
+    list_2: list[str],
+    expected_representative_list: dict[str, str],
+):
+    representative_list = representative_combos(list_1, list_2)
+    exclusion_list = excluded_combos(list_1, list_2)
+    assert representative_list == expected_representative_list
+    assert len(representative_list) == len(list_1) * len(list_2) - len(exclusion_list)
+    assert len(set(representative_list) & set(exclusion_list)) == 0
