Files
wehub-resource-sync bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:35:45 +08:00

395 lines
13 KiB
Python

"""
Integration tests for failed row sampling.
Runs data quality tests against a PostgreSQL database and asserts that
failed row samples are published for failing tests and not for passing tests.
"""
from typing import List, Optional # noqa: UP035
import pandas as pd
import pytest
from _openmetadata_testutils.helpers.assumption import Assumption, assume
from metadata.data_quality.api.models import (
TestCaseDefinition,
TestSuiteProcessorConfig,
)
from metadata.generated.schema.entity.data.table import Table
from metadata.generated.schema.entity.services.databaseService import DatabaseService
from metadata.generated.schema.metadataIngestion.testSuitePipeline import (
TestSuiteConfigType,
)
from metadata.generated.schema.tests.basic import TestCaseStatus
from metadata.generated.schema.tests.testCase import TestCase, TestCaseParameterValue
from metadata.ingestion.models.custom_pydantic import BaseModel
from metadata.ingestion.ometa.ometa_api import OpenMetadata
from metadata.utils.constants import SAMPLE_DATA_DEFAULT_COUNT
from metadata.workflow.data_quality import TestSuiteWorkflow
from metadata.workflow.metadata import MetadataWorkflow
class SampleDataParameters(BaseModel):
class Config:
arbitrary_types_allowed = True
test_case_definition: TestCaseDefinition
assumptions: List[Assumption] # noqa: UP006
table: str = "customer"
expected_query: Optional[str] = None # noqa: UP045
def __init__(self, *args, **kwargs):
if args:
field_names = list(self.__annotations__.keys())
kwargs.update(dict(zip(field_names, args))) # noqa: B905
super().__init__(**kwargs)
class HasColumn(Assumption):
def __init__(self, column_name: str):
super().__init__()
self.column_name = column_name
def assume_positive(self, df):
assert self.column_name in df.columns
class HasExactlyColumns(Assumption):
def __init__(self, num_columns: int):
super().__init__()
self.num_columns = num_columns
def assume_positive(self, df):
assert len(df.columns) == self.num_columns
DEFAULT_COLUMNS = [
"customer_id",
"store_id",
"first_name",
"last_name",
"email",
"address_id",
"activebool",
"create_date",
"last_update",
"active",
"json_field",
]
DEFAULT_COLUMN_ASSUMPTIONS = [HasColumn(column) for column in DEFAULT_COLUMNS] + [
HasExactlyColumns(len(DEFAULT_COLUMNS))
]
FAILING_TEST_PARAMS = [
pytest.param(
SampleDataParameters(*t),
id=t[0].name,
)
for t in [
(
TestCaseDefinition(
name="email_is_not_null",
testDefinitionName="columnValuesToBeNotNull",
columnName="email",
computePassedFailedRowCount=True,
),
[
~assume.notnull("email"),
assume.arbitrary("customer_id", lambda x: x % 10 == 0),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
"bad_data_customer",
),
(
TestCaseDefinition(
name="customer_id_between_0_100",
testDefinitionName="columnValuesToBeBetween",
columnName="customer_id",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="minValue", value="0"),
TestCaseParameterValue(name="maxValue", value="100"),
],
),
[~assume.between("customer_id", 0, 100), *DEFAULT_COLUMN_ASSUMPTIONS],
),
(
TestCaseDefinition(
name="there_is_a_jonnny",
testDefinitionName="columnValuesToBeInSet",
columnName="first_name",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="allowedValues", value=str(["Jonnny"])),
],
),
[~assume.column_values_in("first_name", ["Jonnny"])],
),
(
TestCaseDefinition(
name="assume_there_is_no_tim",
testDefinitionName="columnValuesToBeNotInSet",
columnName="first_name",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="forbiddenValues", value=str(["Tim"])),
],
),
[
assume.column_values_in("first_name", ["Tim"]),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
),
(
TestCaseDefinition(
name="names_up_to_three_chars",
testDefinitionName="columnValueLengthsToBeBetween",
columnName="first_name",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="minLength", value="0"),
TestCaseParameterValue(name="maxLength", value="3"),
],
),
[
~assume.length_between("first_name", 0, 3),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
),
(
TestCaseDefinition(
name="name_starts_with_j",
testDefinitionName="columnValuesToMatchRegex",
columnName="first_name",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="regex", value="^J"),
],
),
[
~assume.match_regex("first_name", "^J"),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
),
(
TestCaseDefinition(
name="name_does_not_start_with_j",
testDefinitionName="columnValuesToNotMatchRegex",
columnName="first_name",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(name="forbiddenRegex", value="^J"),
],
),
[
assume.match_regex("first_name", "^J"),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
),
(
TestCaseDefinition(
name="unique_value_test",
testDefinitionName="columnValuesToBeUnique",
columnName="first_name",
computePassedFailedRowCount=True,
),
[
~assume.unique("first_name"),
*DEFAULT_COLUMN_ASSUMPTIONS,
],
),
(
TestCaseDefinition(
name="custom_sql_test",
testDefinitionName="tableCustomSQLQuery",
computePassedFailedRowCount=True,
parameterValues=[
TestCaseParameterValue(
name="sqlExpression",
value="SELECT *, 'extra_value' as extra_column FROM customer",
),
TestCaseParameterValue(name="strategy", value="ROWS"),
],
),
[
*[HasColumn(column) for column in DEFAULT_COLUMNS],
HasExactlyColumns(len(DEFAULT_COLUMNS) + 1),
HasColumn("extra_column"),
],
),
]
]
@pytest.fixture(scope="module")
def extra_sql_commands():
return []
@pytest.fixture(scope="module")
def sql_commands(extra_sql_commands):
return [
"CREATE TABLE IF NOT EXISTS bad_data_customer AS SELECT * FROM customer;",
"UPDATE public.bad_data_customer SET email = NULL WHERE MOD(customer_id, 10) = 0;",
"UPDATE public.bad_data_customer SET first_name = 'Steveo' WHERE first_name = 'Steve';",
] + extra_sql_commands
@pytest.fixture(scope="module")
def prepare_postgres(postgres_container, sql_commands):
"""Execute SQL commands to set up test data in the dvdrental database."""
from sqlalchemy import create_engine, text
from sqlalchemy.engine.url import make_url
engine = create_engine(make_url(postgres_container.get_connection_url()).set(database="dvdrental"))
with engine.begin() as conn:
for command in sql_commands:
conn.execute(text(command))
@pytest.fixture(scope="module")
def ingest_postgres_metadata(
prepare_postgres,
postgres_service,
metadata: OpenMetadata,
sink_config,
workflow_config,
run_workflow,
):
"""Ingest metadata after preparing the database with test data."""
wf_config = {
"source": {
"type": postgres_service.connection.config.type.value.lower(),
"serviceName": postgres_service.fullyQualifiedName.root,
"serviceConnection": postgres_service.connection.model_copy(
update={"config": postgres_service.connection.config.model_copy(update={"ingestAllDatabases": True})}
),
"sourceConfig": {
"config": {
"schemaFilterPattern": {"excludes": ["information_schema"]},
}
},
},
"sink": sink_config,
"workflowConfig": workflow_config,
}
run_workflow(MetadataWorkflow, wf_config)
def _run_test_suite(
metadata: OpenMetadata,
db_service: DatabaseService,
table: Table,
test_case_definitions: list,
sink_config: dict,
workflow_config: dict,
run_workflow,
):
config = {
"source": {
"type": db_service.connection.config.type.value.lower(),
"serviceName": f"MyTestSuite_{db_service.name.root}",
"sourceConfig": {
"config": {
"type": TestSuiteConfigType.TestSuite.value,
"entityFullyQualifiedName": table.fullyQualifiedName.root,
}
},
},
"processor": {
"type": "orm-test-runner",
"config": TestSuiteProcessorConfig(testCases=test_case_definitions).model_dump(),
},
"sink": sink_config,
"workflowConfig": workflow_config,
}
run_workflow(TestSuiteWorkflow, config)
@pytest.mark.parametrize("parameters", FAILING_TEST_PARAMS)
def test_failing_tests_publish_failed_samples(
postgres_service: DatabaseService,
ingest_postgres_metadata,
patch_passwords_for_db_services,
metadata: OpenMetadata,
parameters: SampleDataParameters,
cleanup_fqns,
run_workflow,
sink_config,
workflow_config,
):
table: Table = metadata.get_by_name(
Table,
f"{postgres_service.fullyQualifiedName.root}.dvdrental.public.{parameters.table}",
nullable=False,
)
_run_test_suite(
metadata,
postgres_service,
table,
[parameters.test_case_definition],
sink_config,
workflow_config,
run_workflow,
)
test_fqn_parts = [table.fullyQualifiedName.root]
if parameters.test_case_definition.columnName:
test_fqn_parts.append(parameters.test_case_definition.columnName)
test_fqn_parts.append(parameters.test_case_definition.name)
test_case_entity: TestCase = metadata.get_by_name(
entity=TestCase,
fqn=".".join(test_fqn_parts),
fields=["*"],
nullable=False,
)
cleanup_fqns(TestCase, test_case_entity.fullyQualifiedName.root)
assert test_case_entity.testCaseResult.testCaseStatus == TestCaseStatus.Failed
failed_sample = metadata.get_failed_rows_sample(test_case_entity)
assert failed_sample is not None
df = pd.DataFrame(failed_sample.rows, columns=[c.root for c in failed_sample.columns])
assert len(df) <= SAMPLE_DATA_DEFAULT_COUNT
assert len(df) > 0
for assumption in parameters.assumptions:
assumption.validate(df)
assert test_case_entity.inspectionQuery is not None
def test_passing_tests_dont_publish(
postgres_service: DatabaseService,
ingest_postgres_metadata,
patch_passwords_for_db_services,
metadata: OpenMetadata,
cleanup_fqns,
run_workflow,
sink_config,
workflow_config,
):
table: Table = metadata.get_by_name(
Table,
f"{postgres_service.fullyQualifiedName.root}.dvdrental.public.customer",
nullable=False,
)
test_case_definition = TestCaseDefinition(
name="name_is_not_null",
testDefinitionName="columnValuesToBeNotNull",
columnName="first_name",
computePassedFailedRowCount=True,
)
_run_test_suite(
metadata,
postgres_service,
table,
[test_case_definition],
sink_config,
workflow_config,
run_workflow,
)
test_case_entity: TestCase = metadata.get_or_create_test_case(
f"{table.fullyQualifiedName.root}.{test_case_definition.columnName}.{test_case_definition.name}"
)
cleanup_fqns(TestCase, test_case_entity.fullyQualifiedName.root)
assert test_case_entity.testCaseResult.testCaseStatus == TestCaseStatus.Success
failed_sample = metadata.get_failed_rows_sample(test_case_entity)
assert failed_sample is None