Skip to content

Commit 2e6abc1

Browse files
committed
refactor into docker base test suite for java connectors
1 parent cd0ea9e commit 2e6abc1

File tree

3 files changed

+193
-175
lines changed

3 files changed

+193
-175
lines changed

airbyte_cdk/test/standard_tests/connector_base.py

Lines changed: 12 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,31 @@
33

44
from __future__ import annotations
55

6-
import abc
76
import importlib
8-
import inspect
97
import os
10-
import shutil
11-
import sys
12-
from collections.abc import Callable
138
from pathlib import Path
14-
from typing import cast
9+
from typing import TYPE_CHECKING, cast
1510

16-
import pytest
17-
import yaml
1811
from boltons.typeutils import classproperty
1912

2013
from airbyte_cdk.models import (
2114
AirbyteMessage,
2215
Type,
2316
)
24-
from airbyte_cdk.models.connector_metadata import MetadataFile
25-
from airbyte_cdk.test import entrypoint_wrapper
2617
from airbyte_cdk.test.standard_tests._job_runner import IConnector, run_test_job
27-
from airbyte_cdk.test.standard_tests.models import (
28-
ConnectorTestScenario,
29-
)
30-
from airbyte_cdk.utils.connector_paths import (
31-
ACCEPTANCE_TEST_CONFIG,
32-
find_connector_root,
33-
)
34-
from airbyte_cdk.utils.docker import build_connector_image, run_docker_command
18+
from airbyte_cdk.test.standard_tests.docker_base import DockerConnectorTestSuite
19+
20+
if TYPE_CHECKING:
21+
from collections.abc import Callable
22+
23+
from airbyte_cdk.test import entrypoint_wrapper
24+
from airbyte_cdk.test.standard_tests.models import (
25+
ConnectorTestScenario,
26+
)
3527

3628

37-
class ConnectorTestSuiteBase(abc.ABC):
38-
"""Base class for connector test suites."""
29+
class ConnectorTestSuiteBase(DockerConnectorTestSuite):
30+
"""Base class for Python connector test suites."""
3931

4032
connector: type[IConnector] | Callable[[], IConnector] | None # type: ignore [reportRedeclaration]
4133
"""The connector class or a factory function that returns an scenario of IConnector."""
@@ -83,13 +75,6 @@ def connector(cls) -> type[IConnector] | Callable[[], IConnector] | None:
8375
) from e
8476
return cast(type[IConnector], getattr(module, matching_class_name))
8577

86-
@classmethod
87-
def get_test_class_dir(cls) -> Path:
88-
"""Get the file path that contains the class."""
89-
module = sys.modules[cls.__module__]
90-
# Get the directory containing the test file
91-
return Path(inspect.getfile(module)).parent
92-
9378
@classmethod
9479
def create_connector(
9580
cls,
@@ -127,148 +112,3 @@ def test_check(
127112
assert len(conn_status_messages) == 1, (
128113
f"Expected exactly one CONNECTION_STATUS message. Got: {result._messages}"
129114
)
130-
131-
@classmethod
132-
def get_connector_root_dir(cls) -> Path:
133-
"""Get the root directory of the connector."""
134-
return find_connector_root([cls.get_test_class_dir(), Path.cwd()])
135-
136-
@classproperty
137-
def acceptance_test_config_path(cls) -> Path:
138-
"""Get the path to the acceptance test config file."""
139-
result = cls.get_connector_root_dir() / ACCEPTANCE_TEST_CONFIG
140-
if result.exists():
141-
return result
142-
143-
raise FileNotFoundError(f"Acceptance test config file not found at: {str(result)}")
144-
145-
@classmethod
146-
def get_scenarios(
147-
cls,
148-
) -> list[ConnectorTestScenario]:
149-
"""Get acceptance tests for a given category.
150-
151-
This has to be a separate function because pytest does not allow
152-
parametrization of fixtures with arguments from the test class itself.
153-
"""
154-
categories = ["connection", "spec"]
155-
all_tests_config = yaml.safe_load(cls.acceptance_test_config_path.read_text())
156-
if "acceptance_tests" not in all_tests_config:
157-
raise ValueError(
158-
f"Acceptance tests config not found in {cls.acceptance_test_config_path}."
159-
f" Found only: {str(all_tests_config)}."
160-
)
161-
162-
test_scenarios: list[ConnectorTestScenario] = []
163-
for category in categories:
164-
if (
165-
category not in all_tests_config["acceptance_tests"]
166-
or "tests" not in all_tests_config["acceptance_tests"][category]
167-
):
168-
continue
169-
170-
test_scenarios.extend(
171-
[
172-
ConnectorTestScenario.model_validate(test)
173-
for test in all_tests_config["acceptance_tests"][category]["tests"]
174-
if "config_path" in test and "iam_role" not in test["config_path"]
175-
]
176-
)
177-
178-
connector_root = cls.get_connector_root_dir().absolute()
179-
for test in test_scenarios:
180-
if test.config_path:
181-
test.config_path = connector_root / test.config_path
182-
if test.configured_catalog_path:
183-
test.configured_catalog_path = connector_root / test.configured_catalog_path
184-
185-
return test_scenarios
186-
187-
@pytest.mark.skipif(
188-
shutil.which("docker") is None,
189-
reason="docker CLI not found in PATH, skipping docker image tests",
190-
)
191-
@pytest.mark.image_tests
192-
def test_docker_image_build_and_spec(
193-
self,
194-
connector_image_override: str | None,
195-
) -> None:
196-
"""Run `docker_image` acceptance tests."""
197-
connector_dir = self.get_connector_root_dir()
198-
metadata = MetadataFile.from_file(connector_dir / "metadata.yaml")
199-
200-
connector_image: str | None = connector_image_override
201-
if not connector_image:
202-
tag = "dev-latest"
203-
connector_image = build_connector_image(
204-
connector_name=connector_dir.name,
205-
connector_directory=connector_dir,
206-
metadata=metadata,
207-
tag=tag,
208-
no_verify=False,
209-
)
210-
211-
_ = run_docker_command(
212-
[
213-
"docker",
214-
"run",
215-
"--rm",
216-
connector_image,
217-
"spec",
218-
],
219-
check=True, # Raise an error if the command fails
220-
capture_output=False,
221-
)
222-
223-
@pytest.mark.skipif(
224-
shutil.which("docker") is None,
225-
reason="docker CLI not found in PATH, skipping docker image tests",
226-
)
227-
@pytest.mark.image_tests
228-
def test_docker_image_build_and_check(
229-
self,
230-
scenario: ConnectorTestScenario,
231-
connector_image_override: str | None,
232-
) -> None:
233-
"""Run `docker_image` acceptance tests.
234-
235-
This test builds the connector image and runs the `check` command inside the container.
236-
237-
Note:
238-
- It is expected for docker image caches to be reused between test runs.
239-
- In the rare case that image caches need to be cleared, please clear
240-
the local docker image cache using `docker image prune -a` command.
241-
"""
242-
if scenario.expect_exception:
243-
pytest.skip("Skipping test_docker_image_build_and_check (expected to fail).")
244-
245-
tag = "dev-latest"
246-
connector_dir = self.get_connector_root_dir()
247-
metadata = MetadataFile.from_file(connector_dir / "metadata.yaml")
248-
connector_image: str | None = connector_image_override
249-
if not connector_image:
250-
tag = "dev-latest"
251-
connector_image = build_connector_image(
252-
connector_name=connector_dir.name,
253-
connector_directory=connector_dir,
254-
metadata=metadata,
255-
tag=tag,
256-
no_verify=False,
257-
)
258-
259-
container_config_path = "/secrets/config.json"
260-
with scenario.with_temp_config_file() as temp_config_file:
261-
_ = run_docker_command(
262-
[
263-
"docker",
264-
"run",
265-
"--rm",
266-
"-v",
267-
f"{temp_config_file}:{container_config_path}",
268-
connector_image,
269-
"check",
270-
f"--config={container_config_path}",
271-
],
272-
check=True, # Raise an error if the command fails
273-
capture_output=False,
274-
)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2+
"""Base class for connector test suites."""
3+
4+
from __future__ import annotations
5+
6+
import inspect
7+
import shutil
8+
import sys
9+
from pathlib import Path
10+
11+
import pytest
12+
import yaml
13+
from boltons.typeutils import classproperty
14+
15+
from airbyte_cdk.models.connector_metadata import MetadataFile
16+
from airbyte_cdk.test.standard_tests.models import (
17+
ConnectorTestScenario,
18+
)
19+
from airbyte_cdk.utils.connector_paths import (
20+
ACCEPTANCE_TEST_CONFIG,
21+
find_connector_root,
22+
)
23+
from airbyte_cdk.utils.docker import build_connector_image, run_docker_command
24+
25+
26+
class DockerConnectorTestSuite:
27+
"""Base class for connector test suites."""
28+
29+
@classmethod
30+
def get_test_class_dir(cls) -> Path:
31+
"""Get the file path that contains the class."""
32+
module = sys.modules[cls.__module__]
33+
# Get the directory containing the test file
34+
return Path(inspect.getfile(module)).parent
35+
36+
@classmethod
37+
def get_connector_root_dir(cls) -> Path:
38+
"""Get the root directory of the connector."""
39+
return find_connector_root([cls.get_test_class_dir(), Path.cwd()])
40+
41+
@classproperty
42+
def acceptance_test_config_path(cls) -> Path:
43+
"""Get the path to the acceptance test config file."""
44+
result = cls.get_connector_root_dir() / ACCEPTANCE_TEST_CONFIG
45+
if result.exists():
46+
return result
47+
48+
raise FileNotFoundError(f"Acceptance test config file not found at: {str(result)}")
49+
50+
@classmethod
51+
def get_scenarios(
52+
cls,
53+
) -> list[ConnectorTestScenario]:
54+
"""Get acceptance tests for a given category.
55+
56+
This has to be a separate function because pytest does not allow
57+
parametrization of fixtures with arguments from the test class itself.
58+
"""
59+
categories = ["connection", "spec"]
60+
all_tests_config = yaml.safe_load(cls.acceptance_test_config_path.read_text())
61+
if "acceptance_tests" not in all_tests_config:
62+
raise ValueError(
63+
f"Acceptance tests config not found in {cls.acceptance_test_config_path}."
64+
f" Found only: {str(all_tests_config)}."
65+
)
66+
67+
test_scenarios: list[ConnectorTestScenario] = []
68+
for category in categories:
69+
if (
70+
category not in all_tests_config["acceptance_tests"]
71+
or "tests" not in all_tests_config["acceptance_tests"][category]
72+
):
73+
continue
74+
75+
test_scenarios.extend([
76+
ConnectorTestScenario.model_validate(test)
77+
for test in all_tests_config["acceptance_tests"][category]["tests"]
78+
if "config_path" in test and "iam_role" not in test["config_path"]
79+
])
80+
81+
connector_root = cls.get_connector_root_dir().absolute()
82+
for test in test_scenarios:
83+
if test.config_path:
84+
test.config_path = connector_root / test.config_path
85+
if test.configured_catalog_path:
86+
test.configured_catalog_path = connector_root / test.configured_catalog_path
87+
88+
return test_scenarios
89+
90+
@pytest.mark.skipif(
91+
shutil.which("docker") is None,
92+
reason="docker CLI not found in PATH, skipping docker image tests",
93+
)
94+
@pytest.mark.image_tests
95+
def test_docker_image_build_and_spec(
96+
self,
97+
connector_image_override: str | None,
98+
) -> None:
99+
"""Run `docker_image` acceptance tests."""
100+
connector_dir = self.get_connector_root_dir()
101+
metadata = MetadataFile.from_file(connector_dir / "metadata.yaml")
102+
103+
connector_image: str | None = connector_image_override
104+
if not connector_image:
105+
tag = "dev-latest"
106+
connector_image = build_connector_image(
107+
connector_name=connector_dir.name,
108+
connector_directory=connector_dir,
109+
metadata=metadata,
110+
tag=tag,
111+
no_verify=False,
112+
)
113+
114+
_ = run_docker_command(
115+
[
116+
"docker",
117+
"run",
118+
"--rm",
119+
connector_image,
120+
"spec",
121+
],
122+
check=True, # Raise an error if the command fails
123+
capture_output=False,
124+
)
125+
126+
@pytest.mark.skipif(
127+
shutil.which("docker") is None,
128+
reason="docker CLI not found in PATH, skipping docker image tests",
129+
)
130+
@pytest.mark.image_tests
131+
def test_docker_image_build_and_check(
132+
self,
133+
scenario: ConnectorTestScenario,
134+
connector_image_override: str | None,
135+
) -> None:
136+
"""Run `docker_image` acceptance tests.
137+
138+
This test builds the connector image and runs the `check` command inside the container.
139+
140+
Note:
141+
- It is expected for docker image caches to be reused between test runs.
142+
- In the rare case that image caches need to be cleared, please clear
143+
the local docker image cache using `docker image prune -a` command.
144+
"""
145+
if scenario.expect_exception:
146+
pytest.skip("Skipping test_docker_image_build_and_check (expected to fail).")
147+
148+
tag = "dev-latest"
149+
connector_dir = self.get_connector_root_dir()
150+
metadata = MetadataFile.from_file(connector_dir / "metadata.yaml")
151+
connector_image: str | None = connector_image_override
152+
if not connector_image:
153+
tag = "dev-latest"
154+
connector_image = build_connector_image(
155+
connector_name=connector_dir.name,
156+
connector_directory=connector_dir,
157+
metadata=metadata,
158+
tag=tag,
159+
no_verify=False,
160+
)
161+
162+
container_config_path = "/secrets/config.json"
163+
with scenario.with_temp_config_file() as temp_config_file:
164+
_ = run_docker_command(
165+
[
166+
"docker",
167+
"run",
168+
"--rm",
169+
"-v",
170+
f"{temp_config_file}:{container_config_path}",
171+
connector_image,
172+
"check",
173+
f"--config={container_config_path}",
174+
],
175+
check=True, # Raise an error if the command fails
176+
capture_output=False,
177+
)

0 commit comments

Comments
 (0)