1010import warnings
1111from dataclasses import asdict
1212from pathlib import Path
13- from subprocess import CompletedProcess , SubprocessError
14- from typing import Literal , cast
13+ from typing import Any , Literal , cast
1514
1615import orjson
1716import pytest
3534from airbyte_cdk .utils .docker import (
3635 build_connector_image ,
3736 run_docker_airbyte_command ,
38- run_docker_command ,
3937)
4038
4139
@@ -66,13 +64,57 @@ def is_destination_connector(cls) -> bool:
6664 return cast (str , cls .connector_name ).startswith ("destination-" )
6765
6866 @classproperty
69- def acceptance_test_config_path (cls ) -> Path :
70- """Get the path to the acceptance test config file."""
71- result = cls .get_connector_root_dir () / ACCEPTANCE_TEST_CONFIG
72- if result .exists ():
73- return result
67+ def acceptance_test_config (cls ) -> Any :
68+ """Get the contents of acceptance test config file.
7469
75- raise FileNotFoundError (f"Acceptance test config file not found at: { str (result )} " )
70+ Also perform some basic validation that the file has the expected structure.
71+ """
72+ acceptance_test_config_path = cls .get_connector_root_dir () / ACCEPTANCE_TEST_CONFIG
73+ if not acceptance_test_config_path .exists ():
74+ raise FileNotFoundError (
75+ f"Acceptance test config file not found at: { str (acceptance_test_config_path )} "
76+ )
77+
78+ tests_config = yaml .safe_load (acceptance_test_config_path .read_text ())
79+
80+ if "acceptance_tests" not in tests_config :
81+ raise ValueError (
82+ f"Acceptance tests config not found in { acceptance_test_config_path } ."
83+ f" Found only: { str (tests_config )} ."
84+ )
85+ return tests_config
86+
87+ @staticmethod
88+ def _dedup_scenarios (scenarios : list [ConnectorTestScenario ]) -> list [ConnectorTestScenario ]:
89+ """
90+ For FAST tests, we treat each config as a separate test scenario to run against, whereas CATs defined
91+ a series of more granular scenarios specifying a config_path and empty_streams among other things.
92+
93+ This method deduplicates the CATs scenarios based on their config_path. In doing so, we choose to
94+ take the union of any defined empty_streams, to have high confidence that runnning a read with the
95+ config will not error on the lack of data in the empty streams or lack of permissions to read them.
96+
97+ """
98+ deduped_scenarios : list [ConnectorTestScenario ] = []
99+
100+ for scenario in scenarios :
101+ for existing_scenario in deduped_scenarios :
102+ if scenario .config_path == existing_scenario .config_path :
103+ # If a scenario with the same config_path already exists, we merge the empty streams.
104+ # scenarios are immutable, so we create a new one.
105+ all_empty_streams = (existing_scenario .empty_streams or []) + (
106+ scenario .empty_streams or []
107+ )
108+ merged_scenario = existing_scenario .model_copy (
109+ update = {"empty_streams" : list (set (all_empty_streams ))}
110+ )
111+ deduped_scenarios .remove (existing_scenario )
112+ deduped_scenarios .append (merged_scenario )
113+ break
114+ else :
115+ # If a scenario does not exist with the config, add the new scenario to the list.
116+ deduped_scenarios .append (scenario )
117+ return deduped_scenarios
76118
77119 @classmethod
78120 def get_scenarios (
@@ -83,9 +125,8 @@ def get_scenarios(
83125 This has to be a separate function because pytest does not allow
84126 parametrization of fixtures with arguments from the test class itself.
85127 """
86- categories = ["connection" , "spec" ]
87128 try :
88- acceptance_test_config_path = cls .acceptance_test_config_path
129+ all_tests_config = cls .acceptance_test_config
89130 except FileNotFoundError as e :
90131 # Destinations sometimes do not have an acceptance tests file.
91132 warnings .warn (
@@ -95,15 +136,9 @@ def get_scenarios(
95136 )
96137 return []
97138
98- all_tests_config = yaml .safe_load (cls .acceptance_test_config_path .read_text ())
99- if "acceptance_tests" not in all_tests_config :
100- raise ValueError (
101- f"Acceptance tests config not found in { cls .acceptance_test_config_path } ."
102- f" Found only: { str (all_tests_config )} ."
103- )
104-
105139 test_scenarios : list [ConnectorTestScenario ] = []
106- for category in categories :
140+ # we look in the basic_read section to find any empty streams
141+ for category in ["spec" , "connection" , "basic_read" ]:
107142 if (
108143 category not in all_tests_config ["acceptance_tests" ]
109144 or "tests" not in all_tests_config ["acceptance_tests" ][category ]
@@ -121,15 +156,11 @@ def get_scenarios(
121156
122157 scenario = ConnectorTestScenario .model_validate (test )
123158
124- if scenario .config_path and scenario .config_path in [
125- s .config_path for s in test_scenarios
126- ]:
127- # Skip duplicate scenarios based on config_path
128- continue
129-
130159 test_scenarios .append (scenario )
131160
132- return test_scenarios
161+ deduped_test_scenarios = cls ._dedup_scenarios (test_scenarios )
162+
163+ return deduped_test_scenarios
133164
134165 @pytest .mark .skipif (
135166 shutil .which ("docker" ) is None ,
@@ -332,6 +363,11 @@ def test_docker_image_build_and_read(
332363 # If `read_from_streams` is a list, we filter the discovered streams.
333364 streams_list = list (set (streams_list ) & set (read_from_streams ))
334365
366+ if scenario .empty_streams :
367+ # Filter out streams marked as empty in the scenario.
368+ empty_stream_names = [stream .name for stream in scenario .empty_streams ]
369+ streams_list = [s for s in streams_list if s .name not in empty_stream_names ]
370+
335371 configured_catalog : ConfiguredAirbyteCatalog = ConfiguredAirbyteCatalog (
336372 streams = [
337373 ConfiguredAirbyteStream (
0 commit comments