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>
169 lines
6.6 KiB
Python
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
|