Skip to content

Commit 8906ed2

Browse files
aaronsteersoctavia-squidington-iii
andauthored
tests: handle case of acceptance-test-config.yml with no valid test cases (#531)
Co-authored-by: octavia-squidington-iii <contact@airbyte.com>
1 parent d09c2ec commit 8906ed2

File tree

6 files changed

+79
-26
lines changed

6 files changed

+79
-26
lines changed

airbyte_cdk/test/entrypoint_wrapper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def records(self) -> List[AirbyteMessage]:
8282
def state_messages(self) -> List[AirbyteMessage]:
8383
return self._get_message_by_types([Type.STATE])
8484

85+
@property
86+
def spec_messages(self) -> List[AirbyteMessage]:
87+
return self._get_message_by_types([Type.SPEC])
88+
8589
@property
8690
def connection_status_messages(self) -> List[AirbyteMessage]:
8791
return self._get_message_by_types([Type.CONNECTION_STATUS])

airbyte_cdk/test/standard_tests/_job_runner.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ def spec(self, logger: logging.Logger) -> Any:
5656

5757
def run_test_job(
5858
connector: IConnector | type[IConnector] | Callable[[], IConnector],
59-
verb: Literal["read", "check", "discover"],
60-
test_scenario: ConnectorTestScenario,
59+
verb: Literal["spec", "read", "check", "discover"],
6160
*,
61+
test_scenario: ConnectorTestScenario | None = None,
6262
catalog: ConfiguredAirbyteCatalog | dict[str, Any] | None = None,
6363
) -> entrypoint_wrapper.EntrypointOutput:
6464
"""Run a test scenario from provided CLI args and return the result."""
65+
# Use default (empty) scenario if not provided:
66+
test_scenario = test_scenario or ConnectorTestScenario()
67+
6568
if not connector:
6669
raise ValueError("Connector is required")
6770

@@ -81,14 +84,14 @@ def run_test_job(
8184
)
8285

8386
args: list[str] = [verb]
84-
if test_scenario.config_path:
85-
args += ["--config", str(test_scenario.config_path)]
86-
elif test_scenario.config_dict:
87+
config_dict = test_scenario.get_config_dict(empty_if_missing=True)
88+
if config_dict and verb != "spec":
89+
# Write the config to a temp json file and pass the path to the file as an argument.
8790
config_path = (
8891
Path(tempfile.gettempdir()) / "airbyte-test" / f"temp_config_{uuid.uuid4().hex}.json"
8992
)
9093
config_path.parent.mkdir(parents=True, exist_ok=True)
91-
config_path.write_text(orjson.dumps(test_scenario.config_dict).decode())
94+
config_path.write_text(orjson.dumps(config_dict).decode())
9295
args += ["--config", str(config_path)]
9396

9497
catalog_path: Path | None = None

airbyte_cdk/test/standard_tests/connector_base.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def get_test_class_dir(cls) -> Path:
8989
@classmethod
9090
def create_connector(
9191
cls,
92-
scenario: ConnectorTestScenario,
92+
scenario: ConnectorTestScenario | None,
9393
) -> IConnector:
9494
"""Instantiate the connector class."""
9595
connector = cls.connector # type: ignore
@@ -147,28 +147,35 @@ def get_scenarios(
147147
This has to be a separate function because pytest does not allow
148148
parametrization of fixtures with arguments from the test class itself.
149149
"""
150-
category = "connection"
150+
categories = ["connection", "spec"]
151151
all_tests_config = yaml.safe_load(cls.acceptance_test_config_path.read_text())
152152
if "acceptance_tests" not in all_tests_config:
153153
raise ValueError(
154154
f"Acceptance tests config not found in {cls.acceptance_test_config_path}."
155155
f" Found only: {str(all_tests_config)}."
156156
)
157-
if category not in all_tests_config["acceptance_tests"]:
158-
return []
159-
if "tests" not in all_tests_config["acceptance_tests"][category]:
160-
raise ValueError(f"No tests found for category {category}")
161-
162-
tests_scenarios = [
163-
ConnectorTestScenario.model_validate(test)
164-
for test in all_tests_config["acceptance_tests"][category]["tests"]
165-
if "iam_role" not in test["config_path"]
166-
]
157+
158+
test_scenarios: list[ConnectorTestScenario] = []
159+
for category in categories:
160+
if (
161+
category not in all_tests_config["acceptance_tests"]
162+
or "tests" not in all_tests_config["acceptance_tests"][category]
163+
):
164+
continue
165+
166+
test_scenarios.extend(
167+
[
168+
ConnectorTestScenario.model_validate(test)
169+
for test in all_tests_config["acceptance_tests"][category]["tests"]
170+
if "config_path" in test and "iam_role" not in test["config_path"]
171+
]
172+
)
173+
167174
connector_root = cls.get_connector_root_dir().absolute()
168-
for test in tests_scenarios:
175+
for test in test_scenarios:
169176
if test.config_path:
170177
test.config_path = connector_root / test.config_path
171178
if test.configured_catalog_path:
172179
test.configured_catalog_path = connector_root / test.configured_catalog_path
173180

174-
return tests_scenarios
181+
return test_scenarios

airbyte_cdk/test/standard_tests/declarative_sources.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def components_py_path(cls) -> Path | None:
6464
@classmethod
6565
def create_connector(
6666
cls,
67-
scenario: ConnectorTestScenario,
67+
scenario: ConnectorTestScenario | None,
6868
) -> IConnector:
6969
"""Create a connector scenario for the test suite.
7070
@@ -73,9 +73,13 @@ def create_connector(
7373
7474
Subclasses should not need to override this method.
7575
"""
76-
config: dict[str, Any] = scenario.get_config_dict()
77-
76+
scenario = scenario or ConnectorTestScenario() # Use default (empty) scenario if None
7877
manifest_dict = yaml.safe_load(cls.manifest_yaml_path.read_text())
78+
config = {
79+
"__injected_manifest": manifest_dict,
80+
}
81+
config.update(scenario.get_config_dict(empty_if_missing=True))
82+
7983
if cls.components_py_path and cls.components_py_path.exists():
8084
os.environ["AIRBYTE_ENABLE_UNSAFE_CODE"] = "true"
8185
config["__injected_components_py"] = cls.components_py_path.read_text()

airbyte_cdk/test/standard_tests/models/scenario.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,29 @@ class AcceptanceTestFileTypes(BaseModel):
4343
file_types: AcceptanceTestFileTypes | None = None
4444
status: Literal["succeed", "failed"] | None = None
4545

46-
def get_config_dict(self) -> dict[str, Any]:
46+
def get_config_dict(
47+
self,
48+
*,
49+
empty_if_missing: bool,
50+
) -> dict[str, Any]:
4751
"""Return the config dictionary.
4852
4953
If a config dictionary has already been loaded, return it. Otherwise, load
5054
the config file and return the dictionary.
55+
56+
If `self.config_dict` and `self.config_path` are both `None`:
57+
- return an empty dictionary if `empty_if_missing` is True
58+
- raise a ValueError if `empty_if_missing` is False
5159
"""
52-
if self.config_dict:
60+
if self.config_dict is not None:
5361
return self.config_dict
5462

55-
if self.config_path:
63+
if self.config_path is not None:
5664
return cast(dict[str, Any], yaml.safe_load(self.config_path.read_text()))
5765

66+
if empty_if_missing:
67+
return {}
68+
5869
raise ValueError("No config dictionary or path provided.")
5970

6071
@property

airbyte_cdk/test/standard_tests/source_base.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ def test_discover(
6464
test_scenario=scenario,
6565
)
6666

67+
def test_spec(self) -> None:
68+
"""Standard test for `spec`.
69+
70+
This test does not require a `scenario` input, since `spec`
71+
does not require any inputs.
72+
73+
We assume `spec` should always succeed and it should always generate
74+
a valid `SPEC` message.
75+
76+
Note: the parsing of messages by type also implicitly validates that
77+
the generated `SPEC` message is valid JSON.
78+
"""
79+
result = run_test_job(
80+
verb="spec",
81+
test_scenario=None,
82+
connector=self.create_connector(scenario=None),
83+
)
84+
# If an error occurs, it will be raised above.
85+
86+
assert len(result.spec_messages) == 1, (
87+
"Expected exactly 1 spec message but got {len(result.spec_messages)}",
88+
result.errors,
89+
)
90+
6791
def test_basic_read(
6892
self,
6993
scenario: ConnectorTestScenario,

0 commit comments

Comments
 (0)