Skip to content

Commit cdbe49c

Browse files
committed
add support for 'spec' and tests without a scenario
1 parent d3ae744 commit cdbe49c

File tree

5 files changed

+64
-27
lines changed

5 files changed

+64
-27
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: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ 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."""
@@ -81,9 +81,9 @@ def run_test_job(
8181
)
8282

8383
args: list[str] = [verb]
84-
if test_scenario.config_path:
84+
if test_scenario and test_scenario.config_path:
8585
args += ["--config", str(test_scenario.config_path)]
86-
elif test_scenario.config_dict:
86+
elif test_scenario and test_scenario.config_dict:
8787
config_path = (
8888
Path(tempfile.gettempdir()) / "airbyte-test" / f"temp_config_{uuid.uuid4().hex}.json"
8989
)
@@ -103,7 +103,7 @@ def run_test_job(
103103
)
104104
catalog_path.parent.mkdir(parents=True, exist_ok=True)
105105
catalog_path.write_text(orjson.dumps(catalog).decode())
106-
elif test_scenario.configured_catalog_path:
106+
elif test_scenario and test_scenario.configured_catalog_path:
107107
catalog_path = Path(test_scenario.configured_catalog_path)
108108

109109
if catalog_path:
@@ -112,12 +112,18 @@ def run_test_job(
112112
# This is a bit of a hack because the source needs the catalog early.
113113
# Because it *also* can fail, we have to redundantly wrap it in a try/except block.
114114

115+
expect_exception = False
116+
if test_scenario and test_scenario.expect_exception:
117+
# If the test scenario expects an exception, we need to set the
118+
# `expect_exception` flag to True.
119+
expect_exception = True
120+
115121
result: entrypoint_wrapper.EntrypointOutput = entrypoint_wrapper._run_command( # noqa: SLF001 # Non-public API
116122
source=connector_obj, # type: ignore [arg-type]
117123
args=args,
118-
expecting_exception=test_scenario.expect_exception,
124+
expecting_exception=False if not test_scenario else expect_exception,
119125
)
120-
if result.errors and not test_scenario.expect_exception:
126+
if result.errors and not expect_exception:
121127
raise AssertionError(
122128
f"Expected no errors but got {len(result.errors)}: \n" + _errors_to_str(result)
123129
)
@@ -132,7 +138,7 @@ def run_test_job(
132138
+ "\n".join([str(msg) for msg in result.connection_status_messages])
133139
+ _errors_to_str(result)
134140
)
135-
if test_scenario.expect_exception:
141+
if expect_exception:
136142
conn_status = result.connection_status_messages[0].connectionStatus
137143
assert conn_status, (
138144
"Expected CONNECTION_STATUS message to be present. Got: \n"
@@ -146,7 +152,7 @@ def run_test_job(
146152
return result
147153

148154
# For all other verbs, we assert check that an exception is raised (or not).
149-
if test_scenario.expect_exception:
155+
if expect_exception:
150156
if not result.errors:
151157
raise AssertionError("Expected exception but got none.")
152158

airbyte_cdk/test/standard_tests/connector_base.py

Lines changed: 18 additions & 15 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,30 +147,33 @@ 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
)
157157

158-
if (
159-
category not in all_tests_config["acceptance_tests"]
160-
or "tests" not in all_tests_config["acceptance_tests"][category]
161-
):
162-
return []
163-
164-
tests_scenarios = [
165-
ConnectorTestScenario.model_validate(test)
166-
for test in all_tests_config["acceptance_tests"][category]["tests"]
167-
if "iam_role" not in test["config_path"]
168-
]
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+
ConnectorTestScenario.model_validate(test)
168+
for test in all_tests_config["acceptance_tests"][category]["tests"]
169+
if "config_path" in test and "iam_role" not in test["config_path"]
170+
])
171+
169172
connector_root = cls.get_connector_root_dir().absolute()
170-
for test in tests_scenarios:
173+
for test in test_scenarios:
171174
if test.config_path:
172175
test.config_path = connector_root / test.config_path
173176
if test.configured_catalog_path:
174177
test.configured_catalog_path = connector_root / test.configured_catalog_path
175178

176-
return tests_scenarios
179+
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-
7876
manifest_dict = yaml.safe_load(cls.manifest_yaml_path.read_text())
77+
config = {
78+
"__injected_manifest": manifest_dict,
79+
}
80+
if scenario:
81+
config.update(scenario.get_config_dict())
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/source_base.py

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

67+
def test_spec(
68+
self,
69+
# scenario: ConnectorTestScenario, # No inputs needed for spec test
70+
) -> None:
71+
"""Standard test for `spec`."""
72+
result = run_test_job(
73+
verb="spec",
74+
test_scenario=None,
75+
connector=self.create_connector(scenario=None),
76+
)
77+
if result.errors:
78+
raise AssertionError(
79+
f"Expected no errors but got {len(result.errors)}: \n"
80+
+ "\n".join([str(e) for e in result.errors])
81+
)
82+
assert len(result.spec_messages) == 1, (
83+
"Expected exactly 1 spec message but got {len(result.spec_messages)}",
84+
result.errors,
85+
)
86+
6787
def test_basic_read(
6888
self,
6989
scenario: ConnectorTestScenario,

0 commit comments

Comments
 (0)