Skip to content

Commit a6032b0

Browse files
committed
[airbyte-ci] test connector inside their built container
1 parent b387477 commit a6032b0

File tree

4 files changed

+96
-85
lines changed

4 files changed

+96
-85
lines changed

airbyte-ci/connectors/pipelines/pipelines/bases.py

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from dataclasses import dataclass, field
1414
from datetime import datetime, timedelta
1515
from enum import Enum
16-
from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Set
16+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, List, Optional, Set
1717

1818
import anyio
1919
import asyncer
@@ -22,9 +22,9 @@
2222
from dagger import Container, DaggerError
2323
from jinja2 import Environment, PackageLoader, select_autoescape
2424
from pipelines import sentry_utils
25-
from pipelines.actions import remote_storage
25+
from pipelines.actions import environments, remote_storage
2626
from pipelines.consts import GCS_PUBLIC_DOMAIN, LOCAL_REPORTS_PATH_ROOT, PYPROJECT_TOML_FILE_PATH
27-
from pipelines.utils import METADATA_FILE_NAME, check_path_in_workdir, format_duration, get_exec_result
27+
from pipelines.utils import METADATA_FILE_NAME, format_duration, get_exec_result
2828
from rich.console import Group
2929
from rich.panel import Panel
3030
from rich.style import Style
@@ -279,37 +279,99 @@ def _get_timed_out_step_result(self) -> StepResult:
279279
class PytestStep(Step, ABC):
280280
"""An abstract class to run pytest tests and evaluate success or failure according to pytest logs."""
281281

282+
PYTEST_INI_FILE_NAME = "pytest.ini"
283+
PYPROJECT_FILE_NAME = "pyproject.toml"
284+
extra_dependencies_names = ("dev", "tests")
282285
skipped_exit_code = 5
283286

284-
async def _run_tests_in_directory(self, connector_under_test: Container, test_directory: str) -> StepResult:
285-
"""Run the pytest tests in the test_directory that was passed.
287+
@property
288+
@abstractmethod
289+
def test_directory_name(self) -> str:
290+
raise NotImplementedError("test_directory_name must be implemented in the child class.")
286291

287-
A StepStatus.SKIPPED is returned if no tests were discovered.
292+
async def _run(self, connector_under_test: Container) -> StepResult:
293+
"""Run all pytest tests declared in the test directory of the connector code.
288294
289295
Args:
290296
connector_under_test (Container): The connector under test container.
291-
test_directory (str): The directory in which the python test modules are declared
292297
293298
Returns:
294-
Tuple[StepStatus, Optional[str], Optional[str]]: Tuple of StepStatus, stderr and stdout.
299+
StepResult: Failure or success of the unit tests with stdout and stdout.
295300
"""
296-
test_config = "pytest.ini" if await check_path_in_workdir(connector_under_test, "pytest.ini") else "/" + PYPROJECT_TOML_FILE_PATH
297-
if await check_path_in_workdir(connector_under_test, test_directory):
298-
tester = connector_under_test.with_exec(
299-
[
300-
"python",
301-
"-m",
302-
"pytest",
303-
"-s",
304-
test_directory,
305-
"-c",
306-
test_config,
307-
]
308-
)
309-
return await self.get_step_result(tester)
301+
if not await self.check_if_tests_are_available(self.test_directory_name):
302+
return self.skip(f"No {self.test_directory_name} directory found in the connector.")
303+
304+
connector_under_test = connector_under_test.with_(await self.testing_environment(self.extra_dependencies_names))
305+
306+
return await self.get_step_result(connector_under_test)
307+
308+
async def check_if_tests_are_available(self, test_directory_name: str) -> bool:
309+
"""Check if the tests are available in the connector directory.
310+
311+
Returns:
312+
bool: True if the tests are available.
313+
"""
314+
connector_dir = await self.context.get_connector_dir()
315+
connector_dir_entries = await connector_dir.entries()
316+
return test_directory_name in connector_dir_entries
317+
318+
async def testing_environment(self, extra_dependencies_names: Iterable[str]) -> Callable:
319+
"""Install all extra dependencies of a connector.
320+
321+
Args:
322+
extra_dependencies_names (Iterable[str]): Extra dependencies to install.
310323
324+
Returns:
325+
Callable: The decorator to use with the with_ method of a container.
326+
"""
327+
secret_mounting_function = await environments.mounted_connector_secrets(self.context, "secrets")
328+
connector_dir = await self.context.get_connector_dir()
329+
connector_dir_entries = await connector_dir.entries()
330+
331+
if self.PYTEST_INI_FILE_NAME in connector_dir_entries:
332+
config_file_name = self.PYTEST_INI_FILE_NAME
333+
test_config = await self.context.get_connector_dir(include=[self.PYTEST_INI_FILE_NAME]).file(self.PYTEST_INI_FILE_NAME)
334+
self.logger.info(f"Found {self.PYTEST_INI_FILE_NAME}, using it for testing.")
335+
elif self.PYPROJECT_FILE_NAME in connector_dir_entries:
336+
config_file_name = self.PYPROJECT_FILE_NAME
337+
test_config = await self.context.get_connector_dir(include=[self.PYTEST_INI_FILE_NAME]).file(self.PYTEST_INI_FILE_NAME)
338+
self.logger.info(f"Found {PYPROJECT_TOML_FILE_PATH} at connector level, using it for testing.")
311339
else:
312-
return StepResult(self, StepStatus.SKIPPED)
340+
config_file_name = f"global_{self.PYPROJECT_FILE_NAME}"
341+
test_config = await self.context.get_repo_dir(include=[self.PYPROJECT_FILE_NAME]).file(self.PYPROJECT_FILE_NAME)
342+
self.logger.info(f"Found {PYPROJECT_TOML_FILE_PATH} at repo level, using it for testing.")
343+
344+
def prepare_for_testing(built_connector_container: Container) -> Container:
345+
return (
346+
built_connector_container
347+
# Reset the entrypoint
348+
.with_entrypoint([])
349+
# Mount the connector directory in /test_environment
350+
# For build optimization the full directory is not mounted by default
351+
# We need the setup.py/pyproject.toml and the tests code to be available
352+
# Install the extra dependencies
353+
.with_mounted_directory("/test_environment", connector_dir)
354+
# Jump in the /test_environment directory
355+
.with_workdir("/test_environment").with_mounted_file(config_file_name, test_config)
356+
# Mount the secrets
357+
.with_(secret_mounting_function)
358+
# Install the extra dependencies
359+
.with_exec(["pip", "install", f".[{','.join(extra_dependencies_names)}]"], skip_entrypoint=True)
360+
# Execute pytest on the test directory
361+
.with_exec(
362+
[
363+
"python",
364+
"-m",
365+
"pytest",
366+
"-s",
367+
self.test_directory_name,
368+
"-c",
369+
config_file_name,
370+
]
371+
)
372+
)
373+
374+
return prepare_for_testing
313375

314376

315377
class NoOpStep(Step):

airbyte-ci/connectors/pipelines/pipelines/tests/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from dagger import Container, Directory, File
1818
from pipelines import hacks
1919
from pipelines.actions import environments
20-
from pipelines.bases import CIContext, PytestStep, Step, StepResult, StepStatus
20+
from pipelines.bases import CIContext, Step, StepResult, StepStatus
2121
from pipelines.utils import METADATA_FILE_NAME
2222

2323

@@ -174,7 +174,7 @@ async def _run(self) -> StepResult:
174174
return await self.get_step_result(qa_checks)
175175

176176

177-
class AcceptanceTests(PytestStep):
177+
class AcceptanceTests(Step):
178178
"""A step to run acceptance tests for a connector if it has an acceptance test config file."""
179179

180180
title = "Acceptance tests"

airbyte-ci/connectors/pipelines/pipelines/tests/python_connectors.py

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44

55
"""This module groups steps made to run tests for a specific Python connector given a test context."""
66

7-
from datetime import timedelta
87
from typing import List
98

109
import asyncer
11-
from dagger import Container
1210
from pipelines.actions import environments, secrets
1311
from pipelines.bases import Step, StepResult, StepStatus
1412
from pipelines.builds import LOCAL_BUILD_PLATFORM
1513
from pipelines.builds.python_connectors import BuildConnectorImage
1614
from pipelines.contexts import ConnectorContext
17-
from pipelines.helpers.steps import run_steps
1815
from pipelines.tests.common import AcceptanceTests, PytestStep
1916
from pipelines.utils import export_container_to_tarball
2017

@@ -55,62 +52,18 @@ async def _run(self) -> StepResult:
5552
return await self.get_step_result(formatter)
5653

5754

58-
class ConnectorPackageInstall(Step):
59-
"""A step to install the Python connector package in a container."""
60-
61-
title = "Connector package install"
62-
max_duration = timedelta(minutes=20)
63-
max_retries = 3
64-
65-
async def _run(self) -> StepResult:
66-
"""Install the connector under test package in a Python container.
67-
68-
Returns:
69-
StepResult: Failure or success of the package installation and the connector under test container (with the connector package installed).
70-
"""
71-
connector_under_test = await environments.with_python_connector_installed(self.context)
72-
return await self.get_step_result(connector_under_test)
73-
74-
7555
class UnitTests(PytestStep):
7656
"""A step to run the connector unit tests with Pytest."""
7757

7858
title = "Unit tests"
79-
80-
async def _run(self, connector_under_test: Container) -> StepResult:
81-
"""Run all pytest tests declared in the unit_tests directory of the connector code.
82-
83-
Args:
84-
connector_under_test (Container): The connector under test container.
85-
86-
Returns:
87-
StepResult: Failure or success of the unit tests with stdout and stdout.
88-
"""
89-
connector_under_test_with_secrets = connector_under_test.with_(
90-
await environments.mounted_connector_secrets(self.context, "secrets")
91-
)
92-
return await self._run_tests_in_directory(connector_under_test_with_secrets, "unit_tests")
59+
test_directory_name = "unit_tests"
9360

9461

9562
class IntegrationTests(PytestStep):
9663
"""A step to run the connector integration tests with Pytest."""
9764

9865
title = "Integration tests"
99-
100-
async def _run(self, connector_under_test: Container) -> StepResult:
101-
"""Run all pytest tests declared in the integration_tests directory of the connector code.
102-
103-
Args:
104-
connector_under_test (Container): The connector under test container.
105-
106-
Returns:
107-
StepResult: Failure or success of the integration tests with stdout and stdout.
108-
"""
109-
110-
connector_under_test = connector_under_test.with_(environments.bound_docker_host(self.context)).with_(
111-
await environments.mounted_connector_secrets(self.context, "secrets")
112-
)
113-
return await self._run_tests_in_directory(connector_under_test, "integration_tests")
66+
test_directory_name = "integration_tests"
11467

11568

11669
async def run_all_tests(context: ConnectorContext) -> List[StepResult]:
@@ -122,18 +75,14 @@ async def run_all_tests(context: ConnectorContext) -> List[StepResult]:
12275
Returns:
12376
List[StepResult]: The results of all the steps that ran or were skipped.
12477
"""
78+
step_results = []
79+
build_connector_image_results = await BuildConnectorImage(context, LOCAL_BUILD_PLATFORM).run()
80+
if build_connector_image_results.status is StepStatus.FAILURE:
81+
return [build_connector_image_results]
82+
step_results.append(build_connector_image_results)
12583

126-
step_results = await run_steps(
127-
[
128-
ConnectorPackageInstall(context),
129-
BuildConnectorImage(context, LOCAL_BUILD_PLATFORM),
130-
]
131-
)
132-
if any([step_result.status is StepStatus.FAILURE for step_result in step_results]):
133-
return step_results
134-
connector_package_install_results, build_connector_image_results = step_results[0], step_results[1]
13584
connector_image_tar_file, _ = await export_container_to_tarball(context, build_connector_image_results.output_artifact)
136-
connector_container = connector_package_install_results.output_artifact
85+
connector_container = build_connector_image_results.output_artifact
13786

13887
context.connector_secrets = await secrets.get_connector_secrets(context)
13988

airbyte-ci/connectors/pipelines/pipelines/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async def check_path_in_workdir(container: Container, path: str) -> bool:
5454
Returns:
5555
bool: Whether the path exists in the container working directory.
5656
"""
57-
workdir = (await container.with_exec(["pwd"]).stdout()).strip()
57+
workdir = (await container.with_exec(["pwd"], skip_entrypoint=True).stdout()).strip()
5858
mounts = await container.mounts()
5959
if workdir in mounts:
6060
expected_file_path = Path(workdir[1:]) / path

0 commit comments

Comments
 (0)