diff --git a/.github/workflows/test-command.yml b/.github/workflows/test-command.yml index 8a6f36d5d775..08a7250fc694 100644 --- a/.github/workflows/test-command.yml +++ b/.github/workflows/test-command.yml @@ -146,7 +146,6 @@ jobs: **/normalization_test_output/**/build/compiled/airbyte_utils/** **/normalization_test_output/**/build/run/airbyte_utils/** **/normalization_test_output/**/models/generated/** - - name: Test coverage reports artifacts if: github.event.inputs.comment-id && success() uses: actions/upload-artifact@v3 @@ -155,10 +154,14 @@ jobs: path: | **/${{ github.event.inputs.connector }}/htmlcov/** retention-days: 3 - + - name: Run QA checks for ${{ github.event.inputs.connector }} + id: qa_checks + if: always() + run: | + run-qa-checks ${{ github.event.inputs.connector }} - name: Report Status if: github.ref == 'refs/heads/master' && always() - run: ./tools/status/report.sh ${{ github.event.inputs.connector }} ${{github.repository}} ${{github.run_id}} ${{steps.test.outcome}} + run: ./tools/status/report.sh ${{ github.event.inputs.connector }} ${{github.repository}} ${{github.run_id}} ${{steps.test.outcome}} ${{steps.qa_checks.outcome}} env: AWS_ACCESS_KEY_ID: ${{ secrets.STATUS_API_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.STATUS_API_AWS_SECRET_ACCESS_KEY }} diff --git a/tools/bin/ci_integration_test.sh b/tools/bin/ci_integration_test.sh index 3fbd1a65970d..8fb208fdf83a 100755 --- a/tools/bin/ci_integration_test.sh +++ b/tools/bin/ci_integration_test.sh @@ -32,7 +32,6 @@ else export SUB_BUILD="CONNECTORS_BASE" elif [[ "$connector" == *"connectors"* ]]; then connector_name=$(echo $connector | cut -d / -f 2) - run-qa-checks $connector_name selected_integration_test=$(echo "$all_integration_tests" | grep "^$connector_name$" || echo "") integrationTestCommand="$(_to_gradle_path "airbyte-integrations/$connector" integrationTest)" else diff --git a/tools/ci_connector_ops/ci_connector_ops/qa_checks.py b/tools/ci_connector_ops/ci_connector_ops/qa_checks.py index dc384641c454..7b61664c90ca 100644 --- a/tools/ci_connector_ops/ci_connector_ops/qa_checks.py +++ b/tools/ci_connector_ops/ci_connector_ops/qa_checks.py @@ -2,52 +2,93 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # + import sys -def check_documentation_markdown_file(connector_name: str) -> bool: +from ci_connector_ops.utils import Connector + + +def check_documentation_file_exists(connector: Connector) -> bool: """Check if a markdown file with connector documentation is available - in docs/integrations//.md + in docs/integrations/s/.md Args: - connector_name (str): The connector name + connector (Connector): a Connector dataclass instance. Returns: bool: Wether a documentation file was found. """ - # TODO implement - return True -def check_changelog_entry_is_updated(connector_name: str) -> bool: + return connector.documentation_file_path.exists() + +def check_documentation_follows_guidelines(connector: Connector) -> bool: + """Documentation guidelines are defined here https://hackmd.io/Bz75cgATSbm7DjrAqgl4rw""" + follows_guidelines = True + with open(connector.documentation_file_path) as f: + doc_lines = [l.lower() for l in f.read().splitlines()] + if not doc_lines[0].startswith("# "): + print("The connector name is not used as the main header in the documentation.") + follows_guidelines = False + if connector.definition: # We usually don't have a definition if the connector is not published. + if doc_lines[0].strip() != f"# {connector.definition['name'].lower()}": + print("The connector name is not used as the main header in the documentation.") + follows_guidelines = False + elif not doc_lines[0].startswith("# "): + print("The connector name is not used as the main header in the documentation.") + follows_guidelines = False + + expected_sections = [ + "## Prerequisites", + "## Setup guide", + "## Supported sync modes", + "## Supported streams", + "## Changelog" + ] + + for expected_section in expected_sections: + if expected_section.lower() not in doc_lines: + print(f"Connector documentation is missing a '{expected_section.replace('#', '').strip()}' section.") + follows_guidelines = False + return follows_guidelines + +def check_changelog_entry_is_updated(connector: Connector) -> bool: """Check that the changelog entry is updated for the latest connector version in docs/integrations//.md Args: - connector_name (str): The connector name + connector (Connector): a Connector dataclass instance. Returns: bool: Wether a the changelog is up to date. """ - # TODO implement - return True - -def check_connector_icon_is_available(connector_name: str) -> bool: + if not check_documentation_file_exists(connector): + return False + with open(connector.documentation_file_path) as f: + after_changelog = False + for line in f: + if "# changelog" in line.lower(): + after_changelog = True + if after_changelog and connector.version in line: + return True + return False + +def check_connector_icon_is_available(connector: Connector) -> bool: """Check an SVG icon exists for a connector in in airbyte-config/init/src/main/resources/icons/.svg Args: - connector_name (str): The connector name + connector (Connector): a Connector dataclass instance. Returns: bool: Wether an icon exists for this connector. """ - # TODO implement - return True + return connector.icon_path.exists() -def check_connector_https_url_only(connector_name: str) -> bool: +def check_connector_https_url_only(connector: Connector) -> bool: """Check a connector code contains only https url. Args: - connector_name (str): The connector name + connector (Connector): a Connector dataclass instance. Returns: bool: Wether the connector code contains only https url. @@ -55,12 +96,12 @@ def check_connector_https_url_only(connector_name: str) -> bool: # TODO implement return True -def check_connector_has_no_critical_vulnerabilities(connector_name: str) -> bool: +def check_connector_has_no_critical_vulnerabilities(connector: Connector) -> bool: """Check if the connector image is free of critical Snyk vulnerabilities. Runs a docker scan command. Args: - connector_name (str): The connector name + connector (Connector): a Connector dataclass instance. Returns: bool: Wether the connector is free of critical vulnerabilities. @@ -69,7 +110,9 @@ def check_connector_has_no_critical_vulnerabilities(connector_name: str) -> bool return True QA_CHECKS = [ - check_documentation_markdown_file, + check_documentation_file_exists, + # Disabling the following check because it's likely to not pass on a lot of connectors. + # check_documentation_follows_guidelines, check_changelog_entry_is_updated, check_connector_icon_is_available, check_connector_https_url_only, @@ -77,15 +120,19 @@ def check_connector_has_no_critical_vulnerabilities(connector_name: str) -> bool ] def run_qa_checks(): - connector_name = sys.argv[1] - print(f"Running QA checks for {connector_name}") - qa_check_results = {qa_check.__name__: qa_check(connector_name) for qa_check in QA_CHECKS} + connector_technical_name = sys.argv[1].split("/")[-1] + if not connector_technical_name.startswith("source-") and not connector_technical_name.startswith("destination-"): + print("No QA check to run as this is not a connector.") + sys.exit(0) + connector = Connector(connector_technical_name) + print(f"Running QA checks for {connector_technical_name}:{connector.version}") + qa_check_results = {qa_check.__name__: qa_check(connector) for qa_check in QA_CHECKS} if not all(qa_check_results.values()): - print(f"QA checks failed for {connector_name}") + print(f"QA checks failed for {connector_technical_name}:{connector.version}:") for check_name, check_result in qa_check_results.items(): check_result_prefix = "✅" if check_result else "❌" print(f"{check_result_prefix} - {check_name}") sys.exit(1) else: - print(f"All QA checks succeeded for {connector_name}") + print(f"All QA checks succeeded for {connector_technical_name}:{connector.version}") sys.exit(0) diff --git a/tools/ci_connector_ops/ci_connector_ops/sat_config_checks.py b/tools/ci_connector_ops/ci_connector_ops/sat_config_checks.py index 6efc6172d183..901a3a8f378e 100644 --- a/tools/ci_connector_ops/ci_connector_ops/sat_config_checks.py +++ b/tools/ci_connector_ops/ci_connector_ops/sat_config_checks.py @@ -15,7 +15,7 @@ GA_CONNECTOR_REVIEWERS = {"gl-python"} REVIEW_REQUIREMENTS_FILE_PATH = ".github/connector_org_review_requirements.yaml" -def find_connectors_with_bad_strictness_level() -> List[str]: +def find_connectors_with_bad_strictness_level() -> List[utils.Connector]: """Check if changed connectors have the expected SAT test strictness level according to their release stage. 1. Identify changed connectors 2. Retrieve their release stage from the catalog @@ -23,32 +23,30 @@ def find_connectors_with_bad_strictness_level() -> List[str]: 4. Check if the test strictness level matches the strictness level expected for their release stage. Returns: - List[str]: List of changed connector names that are not matching test strictness level expectations. + List[utils.Connector]: List of changed connector that are not matching test strictness level expectations. """ connectors_with_bad_strictness_level = [] - changed_connector_names = utils.get_changed_connector_names() - for connector_name in changed_connector_names: - connector_release_stage = utils.get_connector_release_stage(connector_name) - expected_test_strictness_level = RELEASE_STAGE_TO_STRICTNESS_LEVEL_MAPPING.get(connector_release_stage) - _, acceptance_test_config = utils.get_acceptance_test_config(connector_name) + changed_connector = utils.get_changed_connectors() + for connector in changed_connector: + expected_test_strictness_level = RELEASE_STAGE_TO_STRICTNESS_LEVEL_MAPPING.get(connector.release_stage) can_check_strictness_level = all( - [item is not None for item in [connector_release_stage, expected_test_strictness_level, acceptance_test_config]] + [item is not None for item in [connector.release_stage, expected_test_strictness_level, connector.acceptance_test_config]] ) if can_check_strictness_level: try: - assert acceptance_test_config.get("test_strictness_level") == expected_test_strictness_level + assert connector.acceptance_test_config.get("test_strictness_level") == expected_test_strictness_level except AssertionError: - connectors_with_bad_strictness_level.append(connector_name) + connectors_with_bad_strictness_level.append(connector) return connectors_with_bad_strictness_level -def find_changed_ga_connectors() -> List[str]: +def find_changed_ga_connectors() -> List[utils.Connector]: """Find GA connectors modified on the current branch. Returns: - List[str]: The list of GA connector that were modified on the current branch. + List[utils.Connector]: The list of GA connectors that were modified on the current branch. """ - changed_connector_names = utils.get_changed_connector_names() - return [connector_name for connector_name in changed_connector_names if utils.get_connector_release_stage(connector_name) == "generally_available"] + changed_connectors = utils.get_changed_connectors() + return [connector for connector in changed_connectors if connector.release_stage == "generally_available"] def find_mandatory_reviewers() -> List[Union[str, Dict[str, List]]]: ga_connector_changes = find_changed_ga_connectors() diff --git a/tools/ci_connector_ops/ci_connector_ops/utils.py b/tools/ci_connector_ops/ci_connector_ops/utils.py index 161e20e2eaad..f6b3f438afd3 100644 --- a/tools/ci_connector_ops/ci_connector_ops/utils.py +++ b/tools/ci_connector_ops/ci_connector_ops/utils.py @@ -1,7 +1,11 @@ # # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # + + +from dataclasses import dataclass import logging +from pathlib import Path from typing import Dict, Optional, Set, Tuple import git @@ -9,6 +13,7 @@ import yaml AIRBYTE_REPO = git.Repo(".") +DIFFED_BRANCH = "origin/master" OSS_CATALOG_URL = "https://storage.googleapis.com/prod-airbyte-cloud-connector-metadata-service/oss_catalog.json" CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors" SOURCE_CONNECTOR_PATH_PREFIX = CONNECTOR_PATH_PREFIX + "/source-" @@ -27,9 +32,14 @@ def download_catalog(catalog_url): OSS_CATALOG = download_catalog(OSS_CATALOG_URL) -class ConnectorNotFoundError(Exception): + + + +class ConnectorInvalidNameError(Exception): pass +class ConnectorVersionNotFound(Exception): + pass def read_definitions(definitions_file_path: str) -> Dict: with open(definitions_file_path) as definitions_file: @@ -38,19 +48,6 @@ def read_definitions(definitions_file_path: str) -> Dict: def get_connector_name_from_path(path): return path.split("/")[2] -def get_changed_connector_names() -> Set[str]: - """Retrieve a list of connector names that were changed in the current branch (compared to master). - - Returns: - Set[str]: Set of connector names e.g ["source-pokeapi"] - """ - changed_source_connector_files = { - file_path - for file_path in AIRBYTE_REPO.git.diff("--name-only", "origin/master").split("\n") - if file_path.startswith(SOURCE_CONNECTOR_PATH_PREFIX) - } - - return {get_connector_name_from_path(changed_file) for changed_file in changed_source_connector_files} def get_changed_acceptance_test_config(diff_regex: Optional[str]=None) -> Set[str]: """Retrieve a list of connector names for which the acceptance_test_config file was changed in the current branch (compared to master). @@ -62,9 +59,9 @@ def get_changed_acceptance_test_config(diff_regex: Optional[str]=None) -> Set[st Set[str]: Set of connector names e.g {"source-pokeapi"} """ if diff_regex is None: - diff_command_args = ("--name-only", "origin/master") + diff_command_args = ("--name-only", DIFFED_BRANCH) else: - diff_command_args = ("--name-only", f'-G{diff_regex}', "origin/master") + diff_command_args = ("--name-only", f'-G{diff_regex}', DIFFED_BRANCH) changed_acceptance_test_config_paths = { file_path @@ -73,60 +70,94 @@ def get_changed_acceptance_test_config(diff_regex: Optional[str]=None) -> Set[st } return {get_connector_name_from_path(changed_file) for changed_file in changed_acceptance_test_config_paths} -def get_connector_definition(connector_name: str) -> Optional[Dict]: - """Find a connector definition from the catalog. - Args: - connector_name (str): The connector name. E.G. 'source-pokeapi' - - Raises: - Exception: Raised if the definition type (source/destination) could not be determined from connector name. - - Returns: - Optional[Dict]: The definition if the connector was found in the catalo. Returns None otherwise. +@dataclass(frozen=True) +class Connector: + """Utility class to gather metadata about a connector.""" + technical_name: str + + def _get_type_and_name_from_technical_name(self) -> Tuple[str, str]: + if "-" not in self.technical_name: + raise ConnectorInvalidNameError(f"Connector type and name could not be inferred from {self.technical_name}") + _type = self.technical_name.split("-")[0] + name = self.technical_name[len(_type) + 1 :] + return _type, name + + @property + def name(self): + return self._get_type_and_name_from_technical_name()[1] + + @property + def connector_type(self) -> str: + return self._get_type_and_name_from_technical_name()[0] + + @property + def documentation_file_path(self) -> Path: + return Path(f"./docs/integrations/{self.connector_type}s/{self.name}.md") + + @property + def icon_path(self) -> Path: + if self.definition and self.definition.get("icon"): + return Path(f"./airbyte-config/init/src/main/resources/icons/{self.definition['icon']}") + return Path(f"./airbyte-config/init/src/main/resources/icons/{self.name}.svg") + + @property + def code_directory(self) -> Path: + return Path(f"./airbyte-integrations/connectors/{self.technical_name}") + + @property + def version(self) -> str: + with open(self.code_directory / "Dockerfile") as f: + for line in f: + if "io.airbyte.version" in line: + return line.split("=")[1].strip() + raise ConnectorVersionNotFound(""" + Could not find the connector version from its Dockerfile. + The io.airbyte.version tag is missing. + """) + + @property + def definition(self) -> Optional[dict]: + """Find a connector definition from the catalog. + Returns: + Optional[Dict]: The definition if the connector was found in the catalog. Returns None otherwise. + """ + try: + definition_type = self.technical_name.split("-")[0] + assert definition_type in ["source", "destination"] + except AssertionError: + raise Exception(f"Could not determine the definition type for {self.technical_name}.") + definitions = read_definitions(DEFINITIONS_FILE_PATH[definition_type]) + for definition in definitions: + if definition["dockerRepository"].replace(f"{AIRBYTE_DOCKER_REPO}/", "") == self.technical_name: + return definition + + @property + def release_stage(self) -> Optional[str]: + return self.definition["releaseStage"] if self.definition else None + + @property + def acceptance_test_config_path(self) -> Path: + return self.code_directory / ACCEPTANCE_TEST_CONFIG_FILE_NAME + + @property + def acceptance_test_config(self) -> Optional[dict]: + try: + with open(self.acceptance_test_config_path) as acceptance_test_config_file: + return yaml.safe_load(acceptance_test_config_file) + except FileNotFoundError: + logging.warning(f"No {ACCEPTANCE_TEST_CONFIG_FILE_NAME} file found for {self.technical_name}") + return None + + def __repr__(self) -> str: + return self.technical_name + +def get_changed_connectors() -> Set[Connector]: + """Retrieve a list of Connectors that were changed in the current branch (compared to master). """ - try: - definition_type = connector_name.split("-")[0] - assert definition_type in ["source", "destination"] - except AssertionError: - raise Exception(f"Could not determine the definition type for {connector_name}.") - definitions = read_definitions(DEFINITIONS_FILE_PATH[definition_type]) - for definition in definitions: - if definition["dockerRepository"].replace(f"{AIRBYTE_DOCKER_REPO}/", "") == connector_name: - return definition - raise ConnectorNotFoundError(f"{connector_name} was not found in {DEFINITIONS_FILE_PATH[definition_type]}") - - -def get_connector_release_stage(connector_name: str) -> Optional[str]: - """Retrieve the connector release stage (E.G. alpha/beta/generally_available). - - Args: - connector_name (str): The connector name. E.G. 'source-pokeapi' - - Returns: - Optional[str]: The connector release stage if it was defined. Returns None otherwise. - """ - try: - definition = get_connector_definition(connector_name) - return definition.get("releaseStage") - except ConnectorNotFoundError as e: - logging.warning(str(e)) - - -def get_acceptance_test_config(connector_name: str) -> Tuple[str, Dict]: - """Retrieve the acceptance test config file path and its content as dict. - - Args: - connector_name (str): The connector name. E.G. 'source-pokeapi' - - - Returns: - Tuple(str, Dict): The acceptance test config file path and its content as dict. - """ - acceptance_test_config_path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/{ACCEPTANCE_TEST_CONFIG_FILE_NAME}" - try: - with open(acceptance_test_config_path) as acceptance_test_config_file: - return acceptance_test_config_path, yaml.safe_load(acceptance_test_config_file) - except FileNotFoundError: - logging.warning(f"No {ACCEPTANCE_TEST_CONFIG_FILE_NAME} file found for {connector_name}") - return None, None + changed_source_connector_files = { + file_path + for file_path in AIRBYTE_REPO.git.diff("--name-only", DIFFED_BRANCH).split("\n") + if file_path.startswith(SOURCE_CONNECTOR_PATH_PREFIX) + } + return {Connector(get_connector_name_from_path(changed_file)) for changed_file in changed_source_connector_files} diff --git a/tools/ci_connector_ops/setup.py b/tools/ci_connector_ops/setup.py index b419dbbbbf73..1b85d559a3be 100644 --- a/tools/ci_connector_ops/setup.py +++ b/tools/ci_connector_ops/setup.py @@ -23,7 +23,7 @@ setup( - version="0.1.5", + version="0.1.6", name="ci_connector_ops", description="Packaged maintained by the connector operations team to perform CI for connectors", author="Airbyte", diff --git a/tools/ci_connector_ops/tests/test_qa_checks.py b/tools/ci_connector_ops/tests/test_qa_checks.py new file mode 100644 index 000000000000..b544d2f4b78e --- /dev/null +++ b/tools/ci_connector_ops/tests/test_qa_checks.py @@ -0,0 +1,122 @@ +# +# Copyright (c) 2022 Airbyte, Inc., all rights reserved. +# + + +from pathlib import Path + +import pytest + +from ci_connector_ops import qa_checks, utils + + +@pytest.mark.parametrize("connector, expect_exists", [ + (utils.Connector("source-faker"), True), + (utils.Connector("source-foobar"), False), +]) +def test_check_documentation_file_exists(connector, expect_exists): + assert qa_checks.check_documentation_file_exists(connector) == expect_exists + +def test_check_changelog_entry_is_updated_missing_doc(mocker): + mocker.patch.object( + qa_checks, + "check_documentation_file_exists", + mocker.Mock(return_value=False) + ) + assert qa_checks.check_changelog_entry_is_updated(qa_checks.Connector("source-foobar")) is False + +def test_check_changelog_entry_is_updated_no_changelog_section(mocker, tmp_path): + mock_documentation_file_path = Path(tmp_path / "doc.md") + mock_documentation_file_path.touch() + + mocker.patch.object( + qa_checks.Connector, + "documentation_file_path", + mock_documentation_file_path + ) + assert qa_checks.check_changelog_entry_is_updated(qa_checks.Connector("source-foobar")) is False + +def test_check_changelog_entry_is_updated_version_not_in_changelog(mocker, tmp_path): + mock_documentation_file_path = Path(tmp_path / "doc.md") + with open(mock_documentation_file_path, "w") as f: + f.write("# Changelog") + + mocker.patch.object( + qa_checks.Connector, + "documentation_file_path", + mock_documentation_file_path + ) + + mocker.patch.object( + qa_checks.Connector, + "version", + "0.0.0" + ) + + assert qa_checks.check_changelog_entry_is_updated(qa_checks.Connector("source-foobar")) is False + +def test_check_changelog_entry_is_updated_version_in_changelog(mocker, tmp_path): + mock_documentation_file_path = Path(tmp_path / "doc.md") + with open(mock_documentation_file_path, "w") as f: + f.write("# Changelog\n0.0.0") + + mocker.patch.object( + qa_checks.Connector, + "documentation_file_path", + mock_documentation_file_path + ) + + mocker.patch.object( + qa_checks.Connector, + "version", + "0.0.0" + ) + assert qa_checks.check_changelog_entry_is_updated(qa_checks.Connector("source-foobar")) + + +@pytest.mark.parametrize("connector, expect_exists", [ + (utils.Connector("source-faker"), True), + (utils.Connector("source-foobar"), False), +]) +def test_check_connector_icon_is_available(connector, expect_exists): + assert qa_checks.check_connector_icon_is_available(connector) == expect_exists + +@pytest.mark.parametrize("user_input, expect_qa_checks_to_run", +[ + ("not-a-connector", False), + ("connectors/source-faker", True), + ("source-faker", True), +]) +def test_run_qa_checks_success(capsys, mocker, user_input, expect_qa_checks_to_run): + mocker.patch.object(qa_checks.sys, "argv", ["", user_input]) + mocker.patch.object(qa_checks, "Connector") + mock_qa_check = mocker.Mock(return_value=True, __name__="mock_qa_check") + if expect_qa_checks_to_run: + mocker.patch.object(qa_checks, "QA_CHECKS", [mock_qa_check]) + with pytest.raises(SystemExit) as wrapped_error: + qa_checks.run_qa_checks() + assert wrapped_error.value.code == 0 + if not expect_qa_checks_to_run: + qa_checks.Connector.assert_not_called() + stdout, _ = capsys.readouterr() + assert "No QA check to run" in stdout + else: + expected_connector_technical_name = user_input.split("/")[-1] + qa_checks.Connector.assert_called_with(expected_connector_technical_name) + mock_qa_check.assert_called_with(qa_checks.Connector.return_value) + stdout, _ = capsys.readouterr() + assert f"Running QA checks for {expected_connector_technical_name}" in stdout + assert f"All QA checks succeeded for {expected_connector_technical_name}" in stdout + + +def test_run_qa_checks_error(capsys, mocker): + mocker.patch.object(qa_checks.sys, "argv", ["", "source-faker"]) + mocker.patch.object(qa_checks, "Connector") + mock_qa_check = mocker.Mock(return_value=False, __name__="mock_qa_check") + mocker.patch.object(qa_checks, "QA_CHECKS", [mock_qa_check]) + with pytest.raises(SystemExit) as wrapped_error: + qa_checks.run_qa_checks() + assert wrapped_error.value.code == 1 + stdout, _ = capsys.readouterr() + assert "QA checks failed for source-faker" in stdout + assert "❌ - mock_qa_check" in stdout diff --git a/tools/ci_connector_ops/tests/test_sat_config_checks.py b/tools/ci_connector_ops/tests/test_sat_config_checks.py index 13ef5f51484a..adcb096efa1c 100644 --- a/tools/ci_connector_ops/tests/test_sat_config_checks.py +++ b/tools/ci_connector_ops/tests/test_sat_config_checks.py @@ -8,7 +8,13 @@ import pytest -from ci_connector_ops import sat_config_checks +from ci_connector_ops import sat_config_checks, utils + + +@pytest.fixture +def mock_diffed_branched(mocker): + mocker.patch.object(sat_config_checks.utils, "DIFFED_BRANCH", utils.AIRBYTE_REPO.active_branch) + return utils.AIRBYTE_REPO.active_branch @pytest.fixture def pokeapi_acceptance_test_config_path(): @@ -88,24 +94,24 @@ def check_review_requirements_file_contains_expected_teams(capsys, expected_team requirements = yaml.safe_load(requirements_file) assert requirements[0]["teams"] == expected_teams -def test_find_mandatory_reviewers_backward_compatibility(capsys, not_ga_backward_compatibility_change_expected_team): +def test_find_mandatory_reviewers_backward_compatibility(mock_diffed_branched, capsys, not_ga_backward_compatibility_change_expected_team): check_review_requirements_file_contains_expected_teams(capsys, not_ga_backward_compatibility_change_expected_team) -def test_find_mandatory_reviewers_test_strictness_level(capsys, not_ga_test_strictness_level_change_expected_team): +def test_find_mandatory_reviewers_test_strictness_level(mock_diffed_branched, capsys, not_ga_test_strictness_level_change_expected_team): check_review_requirements_file_contains_expected_teams(capsys, not_ga_test_strictness_level_change_expected_team) -def test_find_mandatory_reviewers_ga(capsys, ga_connector_file_change_expected_team): +def test_find_mandatory_reviewers_ga(mock_diffed_branched, capsys, ga_connector_file_change_expected_team): check_review_requirements_file_contains_expected_teams(capsys, ga_connector_file_change_expected_team) -def test_find_mandatory_reviewers_ga_backward_compatibility(capsys, ga_connector_backward_compatibility_file_change): +def test_find_mandatory_reviewers_ga_backward_compatibility(mock_diffed_branched, capsys, ga_connector_backward_compatibility_file_change): check_review_requirements_file_contains_expected_teams(capsys, ga_connector_backward_compatibility_file_change) -def test_find_mandatory_reviewers_ga_test_strictness_level(capsys, ga_connector_test_strictness_level_file_change): +def test_find_mandatory_reviewers_ga_test_strictness_level(mock_diffed_branched, capsys, ga_connector_test_strictness_level_file_change): check_review_requirements_file_contains_expected_teams(capsys, ga_connector_test_strictness_level_file_change) -def test_find_mandatory_reviewers_no_tracked_changed(capsys, not_ga_not_tracked_change_expected_team): +def test_find_mandatory_reviewers_no_tracked_changed(mock_diffed_branched, capsys, not_ga_not_tracked_change_expected_team): sat_config_checks.write_review_requirements_file() captured = capsys.readouterr() assert captured.out.split("\n")[0].split("=")[-1] == "false" diff --git a/tools/ci_connector_ops/tests/test_utils.py b/tools/ci_connector_ops/tests/test_utils.py index b3f85a13717b..cfa0f07c3168 100644 --- a/tools/ci_connector_ops/tests/test_utils.py +++ b/tools/ci_connector_ops/tests/test_utils.py @@ -2,15 +2,57 @@ # Copyright (c) 2022 Airbyte, Inc., all rights reserved. # +from contextlib import nullcontext as does_not_raise +from pathlib import Path + +import pytest + from ci_connector_ops import utils -def test_get_connector_definition(): - assert utils.get_connector_definition("source-dynamodb") == { - "name": "DynamoDB", - "sourceDefinitionId": "50401137-8871-4c5a-abb7-1f5fda35545a", - "dockerRepository": "airbyte/source-dynamodb", - "dockerImageTag": "0.1.0", - "documentationUrl": "https://docs.airbyte.com/integrations/sources/dynamodb", - "sourceType": "api", - "releaseStage": "alpha" - } +class TestConnector: + + @pytest.mark.parametrize( + "technical_name, expected_type, expected_name, expected_error", + [ + ("source-faker", "source", "faker", does_not_raise()), + ("source-facebook-marketing", "source", "facebook-marketing", does_not_raise()), + ("destination-postgres", "destination", "postgres", does_not_raise()), + ("foo", None, None, pytest.raises(utils.ConnectorInvalidNameError)), + ]) + def test__get_type_and_name_from_technical_name(self, technical_name, expected_type, expected_name, expected_error): + connector = utils.Connector(technical_name) + with expected_error: + assert connector._get_type_and_name_from_technical_name() == (expected_type, expected_name) + assert connector.name == expected_name + assert connector.connector_type == expected_type + + @pytest.mark.parametrize( + "connector, exists", + [ + (utils.Connector("source-faker"), True), + (utils.Connector("source-notpublished"), False), + ]) + def test_init(self, connector, exists, mocker, tmp_path): + assert str(connector) == connector.technical_name + assert connector.connector_type, connector.name == connector._get_type_and_name_from_technical_name() + assert connector.code_directory == Path(f"./airbyte-integrations/connectors/{connector.technical_name}") + assert connector.acceptance_test_config_path == connector.code_directory / utils.ACCEPTANCE_TEST_CONFIG_FILE_NAME + assert connector.documentation_file_path == Path(f"./docs/integrations/{connector.connector_type}s/{connector.name}.md") + + if exists: + assert isinstance(connector.definition, dict) + assert isinstance(connector.release_stage, str) + assert isinstance(connector.acceptance_test_config, dict) + assert connector.icon_path == Path(f"./airbyte-config/init/src/main/resources/icons/{connector.definition['icon']}") + assert len(connector.version.split(".")) == 3 + else: + assert connector.definition is None + assert connector.release_stage is None + assert connector.acceptance_test_config is None + assert connector.icon_path == Path(f"./airbyte-config/init/src/main/resources/icons/{connector.name}.svg") + with pytest.raises(FileNotFoundError): + connector.version + with pytest.raises(utils.ConnectorVersionNotFound): + Path(tmp_path / "Dockerfile").touch() + mocker.patch.object(utils.Connector, "code_directory", tmp_path) + utils.Connector(connector.technical_name).version diff --git a/tools/status/report.sh b/tools/status/report.sh index be9c83ed62ba..67c0a2983468 100755 --- a/tools/status/report.sh +++ b/tools/status/report.sh @@ -7,7 +7,8 @@ BUCKET=airbyte-connector-build-status CONNECTOR=$1 REPOSITORY=$2 RUN_ID=$3 -OUTCOME=$4 +TEST_OUTCOME=$4 +QA_CHECKS_OUTCOME=$5 BUCKET_WRITE_ROOT=/tmp/bucket_write_root LAST_TEN_ROOT=/tmp/last_ten_root @@ -22,6 +23,10 @@ function write_job_log() { mkdir -p tests/history/"$CONNECTOR" LINK=https://github.com/$REPOSITORY/actions/runs/$RUN_ID TIMESTAMP="$(date +%s)" + OUTCOME=failure + if [ "$TEST_OUTCOME" = "success" ] && [ "$QA_CHECKS_OUTCOME" = "success" ]; then + OUTCOME=success + fi echo "{ \"link\": \"$LINK\", \"outcome\": \"$OUTCOME\" }" > tests/history/"$CONNECTOR"/"$TIMESTAMP".json aws s3 sync "$BUCKET_WRITE_ROOT"/tests/history/"$CONNECTOR"/ s3://"$BUCKET"/tests/history/"$CONNECTOR"/ }