1
0
mirror of synced 2025-12-19 18:14:56 -05:00
Files
airbyte/airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/conftest.py
Eugene Kulak cef8b80c6b SAT: DX improvements, better error handling and more (#4260)
small fixes for SAT for better DX:

- better stack trace in case of error inside the connector, print only relevant information with proper formatting (multiline stack trace instead of single string)
- better logging - print message about image pulling only when it actually happens, stop tests if image not found
- using discovery command for json_schema, when configured_catalog will be loaded we populate `json_schema` from a schema that we get from discovery command, the result is cached for all session duration.
- better record comparison, takes care of lists inside dicts - because lists are unordered we will have false negatives when compare serialized records.
- copied pytest config to airbyte root folder, so when pytest runs tests locally it can find it, this will affect all local execution of pytest
- add IPython as a standard debugger

Co-authored-by: Eugene Kulak <kulak.eugene@gmail.com>
2021-06-22 03:42:10 -04:00

169 lines
6.6 KiB
Python

#
# MIT License
#
# Copyright (c) 2020 Airbyte
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import copy
import json
from pathlib import Path
from typing import Any, List, MutableMapping, Optional
import pytest
from airbyte_cdk.models import AirbyteCatalog, AirbyteRecordMessage, ConfiguredAirbyteCatalog, ConnectorSpecification, Type
from docker import errors
from source_acceptance_test.config import Config
from source_acceptance_test.utils import ConnectorRunner, SecretDict, load_config
@pytest.fixture(name="base_path")
def base_path_fixture(pytestconfig, acceptance_test_config) -> Path:
"""Fixture to define base path for every path-like fixture"""
if acceptance_test_config.base_path:
return Path(acceptance_test_config.base_path).absolute()
return Path(pytestconfig.getoption("--acceptance-test-config")).absolute()
@pytest.fixture(name="acceptance_test_config", scope="session")
def acceptance_test_config_fixture(pytestconfig) -> Config:
"""Fixture with test's config"""
return load_config(pytestconfig.getoption("--acceptance-test-config", skip=True))
@pytest.fixture(name="connector_config_path")
def connector_config_path_fixture(inputs, base_path) -> Path:
"""Fixture with connector's config path"""
return Path(base_path) / getattr(inputs, "config_path")
@pytest.fixture(name="invalid_connector_config_path")
def invalid_connector_config_path_fixture(inputs, base_path) -> Path:
"""Fixture with connector's config path"""
return Path(base_path) / getattr(inputs, "invalid_config_path")
@pytest.fixture(name="connector_spec_path")
def connector_spec_path_fixture(inputs, base_path) -> Path:
"""Fixture with connector's specification path"""
return Path(base_path) / getattr(inputs, "spec_path")
@pytest.fixture(name="configured_catalog_path")
def configured_catalog_path_fixture(inputs, base_path) -> Optional[str]:
"""Fixture with connector's configured_catalog path"""
if getattr(inputs, "configured_catalog_path"):
return Path(base_path) / getattr(inputs, "configured_catalog_path")
return None
@pytest.fixture(name="configured_catalog")
def configured_catalog_fixture(configured_catalog_path, catalog_schemas) -> Optional[ConfiguredAirbyteCatalog]:
if configured_catalog_path:
catalog = ConfiguredAirbyteCatalog.parse_file(configured_catalog_path)
for configured_stream in catalog.streams:
configured_stream.stream.json_schema = catalog_schemas.get(configured_stream.stream.name, {})
return catalog
return None
@pytest.fixture(name="catalog")
def catalog_fixture(configured_catalog: ConfiguredAirbyteCatalog) -> Optional[AirbyteCatalog]:
if configured_catalog:
return AirbyteCatalog(streams=[stream.stream for stream in configured_catalog.streams])
return None
@pytest.fixture(name="image_tag")
def image_tag_fixture(acceptance_test_config) -> str:
return acceptance_test_config.connector_image
@pytest.fixture(name="connector_config")
def connector_config_fixture(base_path, connector_config_path) -> SecretDict:
with open(str(connector_config_path), "r") as file:
contents = file.read()
return SecretDict(json.loads(contents))
@pytest.fixture(name="invalid_connector_config")
def invalid_connector_config_fixture(base_path, invalid_connector_config_path) -> MutableMapping[str, Any]:
"""TODO: implement default value - generate from valid config"""
with open(str(invalid_connector_config_path), "r") as file:
contents = file.read()
return json.loads(contents)
@pytest.fixture(name="malformed_connector_config")
def malformed_connector_config_fixture(connector_config) -> MutableMapping[str, Any]:
"""TODO: drop required field, add extra"""
malformed_config = copy.deepcopy(connector_config)
return malformed_config
@pytest.fixture(name="connector_spec")
def connector_spec_fixture(connector_spec_path) -> ConnectorSpecification:
return ConnectorSpecification.parse_file(connector_spec_path)
@pytest.fixture(name="docker_runner")
def docker_runner_fixture(image_tag, tmp_path) -> ConnectorRunner:
return ConnectorRunner(image_tag, volume=tmp_path)
@pytest.fixture(scope="session", autouse=True)
def pull_docker_image(acceptance_test_config) -> None:
"""Startup fixture to pull docker image"""
image_name = acceptance_test_config.connector_image
config_filename = "acceptance-test-config.yml"
try:
ConnectorRunner(image_name=image_name, volume=Path("."))
except errors.ImageNotFound:
pytest.exit(f"Docker image `{image_name}` not found, please check your {config_filename} file", returncode=1)
@pytest.fixture(name="expected_records")
def expected_records_fixture(inputs, base_path) -> List[AirbyteRecordMessage]:
expect_records = getattr(inputs, "expect_records")
if not expect_records:
return []
with open(str(base_path / getattr(expect_records, "path"))) as f:
return [AirbyteRecordMessage.parse_raw(line) for line in f]
@pytest.fixture(name="cached_schemas", scope="session")
def cached_schemas_fixture() -> MutableMapping[str, Any]:
"""Simple cache for discovered catalog: stream_name -> json_schema"""
return {}
@pytest.fixture(name="catalog_schemas")
def catalog_schemas_fixture(connector_config, docker_runner: ConnectorRunner, cached_schemas) -> MutableMapping[str, Any]:
"""JSON schemas for each stream"""
if not cached_schemas:
output = docker_runner.call_discover(config=connector_config)
catalogs = [message.catalog for message in output if message.type == Type.CATALOG]
for stream in catalogs[-1].streams:
cached_schemas[stream.name] = stream.json_schema
return cached_schemas