mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-21 15:38:11 +00:00
* feat: added entityReference field in testSuite to link testSuite to an entity when the testSuite is executable. * feat: added `executableEntityReference` as an entity reference for executable test suite to their entity * feat: add status object to test case results * feat: ran python linting
This commit is contained in:
parent
236141d9df
commit
06735fe8db
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"tests": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"testSuiteName": "sample_data.ecommerce_db.shopify.dim_address",
|
"testSuiteName": "sample_data.ecommerce_db.shopify.dim_address.TestSuite",
|
||||||
|
"executableEntityReference": "sample_data.ecommerce_db.shopify.dim_address",
|
||||||
"testSuiteDescription": "This is an executable test suite linked to an entity",
|
"testSuiteDescription": "This is an executable test suite linked to an entity",
|
||||||
"scheduleInterval": "0 0 * * MON",
|
"scheduleInterval": "0 0 * * MON",
|
||||||
"testCases": [
|
"testCases": [
|
||||||
|
@ -50,6 +50,7 @@ from metadata.generated.schema.metadataIngestion.workflow import (
|
|||||||
OpenMetadataWorkflowConfig,
|
OpenMetadataWorkflowConfig,
|
||||||
)
|
)
|
||||||
from metadata.generated.schema.tests.testCase import TestCase
|
from metadata.generated.schema.tests.testCase import TestCase
|
||||||
|
from metadata.generated.schema.tests.testDefinition import TestDefinition, TestPlatform
|
||||||
from metadata.generated.schema.tests.testSuite import TestSuite
|
from metadata.generated.schema.tests.testSuite import TestSuite
|
||||||
from metadata.generated.schema.type.basic import EntityLink, FullyQualifiedEntityName
|
from metadata.generated.schema.type.basic import EntityLink, FullyQualifiedEntityName
|
||||||
from metadata.ingestion.api.parser import parse_workflow_config_gracefully
|
from metadata.ingestion.api.parser import parse_workflow_config_gracefully
|
||||||
@ -115,6 +116,10 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
|
|
||||||
self.status = ProcessorStatus()
|
self.status = ProcessorStatus()
|
||||||
|
|
||||||
|
self.table_entity: Optional[Table] = self._get_table_entity(
|
||||||
|
self.source_config.entityFullyQualifiedName.__root__
|
||||||
|
)
|
||||||
|
|
||||||
if self.config.sink:
|
if self.config.sink:
|
||||||
self.sink = get_sink(
|
self.sink = get_sink(
|
||||||
sink_type=self.config.sink.type,
|
sink_type=self.config.sink.type,
|
||||||
@ -142,7 +147,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
)
|
)
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
def _get_table_entity(self, entity_fqn: str):
|
def _get_table_entity(self, entity_fqn: str) -> Optional[Table]:
|
||||||
"""given an entity fqn return the table entity
|
"""given an entity fqn return the table entity
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -151,20 +156,17 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
return self.metadata.get_by_name(
|
return self.metadata.get_by_name(
|
||||||
entity=Table,
|
entity=Table,
|
||||||
fqn=entity_fqn,
|
fqn=entity_fqn,
|
||||||
fields=["tableProfilerConfig"],
|
fields=["tableProfilerConfig", "testSuite"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_test_suite_entity(self) -> Optional[TestSuite]:
|
def create_or_return_test_suite_entity(self) -> Optional[TestSuite]:
|
||||||
"""
|
"""
|
||||||
try to get test suite name from source.servicName.
|
try to get test suite name from source.servicName.
|
||||||
In the UI workflow we'll write the entity name (i.e. the test suite)
|
In the UI workflow we'll write the entity name (i.e. the test suite)
|
||||||
to source.serviceName.
|
to source.serviceName.
|
||||||
"""
|
"""
|
||||||
test_suite = self.metadata.get_by_name(
|
self.table_entity = cast(Table, self.table_entity) # satisfy type checker
|
||||||
entity=TestSuite,
|
test_suite = self.table_entity.testSuite
|
||||||
fqn=self.source_config.entityFullyQualifiedName.__root__,
|
|
||||||
)
|
|
||||||
|
|
||||||
if test_suite and not test_suite.executable:
|
if test_suite and not test_suite.executable:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Test suite {test_suite.fullyQualifiedName.__root__} is not executable."
|
f"Test suite {test_suite.fullyQualifiedName.__root__} is not executable."
|
||||||
@ -180,10 +182,11 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
)
|
)
|
||||||
test_suite = self.metadata.create_or_update_executable_test_suite(
|
test_suite = self.metadata.create_or_update_executable_test_suite(
|
||||||
CreateTestSuiteRequest(
|
CreateTestSuiteRequest(
|
||||||
name=self.source_config.entityFullyQualifiedName.__root__,
|
name=f"{self.source_config.entityFullyQualifiedName.__root__}.TestSuite",
|
||||||
displayName=self.source_config.entityFullyQualifiedName.__root__,
|
displayName=f"{self.source_config.entityFullyQualifiedName.__root__} Test Suite",
|
||||||
description="Test Suite created from YAML processor config file",
|
description="Test Suite created from YAML processor config file",
|
||||||
owner=None,
|
owner=None,
|
||||||
|
executableEntityReference=self.source_config.entityFullyQualifiedName.__root__,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -209,10 +212,33 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
cli_test_cases = cast(
|
cli_test_cases = cast(
|
||||||
List[TestCaseDefinition], cli_test_cases
|
List[TestCaseDefinition], cli_test_cases
|
||||||
) # satisfy type checker
|
) # satisfy type checker
|
||||||
test_cases = self.compare_and_create_test_cases(cli_test_cases, test_cases)
|
test_cases = self.compare_and_create_test_cases(
|
||||||
|
cli_test_cases, test_cases, test_suite
|
||||||
|
)
|
||||||
|
|
||||||
return test_cases
|
return test_cases
|
||||||
|
|
||||||
|
def filter_for_om_test_cases(self, test_cases: List[TestCase]) -> List[TestCase]:
|
||||||
|
"""
|
||||||
|
Filter test cases for OM test cases only. This will prevent us from running non OM test cases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test_cases: list of test cases
|
||||||
|
"""
|
||||||
|
om_test_cases: List[TestCase] = []
|
||||||
|
for test_case in test_cases:
|
||||||
|
test_definition: TestDefinition = self.metadata.get_by_id(
|
||||||
|
TestDefinition, test_case.testDefinition.id
|
||||||
|
)
|
||||||
|
if TestPlatform.OpenMetadata not in test_definition.testPlatforms:
|
||||||
|
logger.debug(
|
||||||
|
f"Test case {test_case.name.__root__} is not an OpenMetadata test case."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
om_test_cases.append(test_case)
|
||||||
|
|
||||||
|
return om_test_cases
|
||||||
|
|
||||||
def get_test_case_from_cli_config(
|
def get_test_case_from_cli_config(
|
||||||
self,
|
self,
|
||||||
) -> Optional[List[TestCaseDefinition]]:
|
) -> Optional[List[TestCaseDefinition]]:
|
||||||
@ -257,6 +283,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
self,
|
self,
|
||||||
cli_test_cases_definitions: Optional[List[TestCaseDefinition]],
|
cli_test_cases_definitions: Optional[List[TestCaseDefinition]],
|
||||||
test_cases: List[TestCase],
|
test_cases: List[TestCase],
|
||||||
|
test_suite: TestSuite,
|
||||||
) -> Optional[List[TestCase]]:
|
) -> Optional[List[TestCase]]:
|
||||||
"""
|
"""
|
||||||
compare test cases defined in CLI config workflow with test cases
|
compare test cases defined in CLI config workflow with test cases
|
||||||
@ -306,7 +333,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
test_case_to_create.columnName,
|
test_case_to_create.columnName,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
testSuite=self.source_config.entityFullyQualifiedName,
|
testSuite=test_suite.fullyQualifiedName.__root__,
|
||||||
parameterValues=list(test_case_to_create.parameterValues)
|
parameterValues=list(test_case_to_create.parameterValues)
|
||||||
if test_case_to_create.parameterValues
|
if test_case_to_create.parameterValues
|
||||||
else None,
|
else None,
|
||||||
@ -330,17 +357,14 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
|
|
||||||
def run_test_suite(self):
|
def run_test_suite(self):
|
||||||
"""Main logic to run the tests"""
|
"""Main logic to run the tests"""
|
||||||
table_entity: Table = self._get_table_entity(
|
if not self.table_entity:
|
||||||
self.source_config.entityFullyQualifiedName.__root__
|
|
||||||
)
|
|
||||||
if not table_entity:
|
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Could not retrieve table entity for {self.source_config.entityFullyQualifiedName.__root__}"
|
f"Could not retrieve table entity for {self.source_config.entityFullyQualifiedName.__root__}. "
|
||||||
"Make sure the table exists in OpenMetadata and/or the JWT Token provided is valid."
|
"Make sure the table exists in OpenMetadata and/or the JWT Token provided is valid."
|
||||||
)
|
)
|
||||||
|
|
||||||
test_suite = self.get_test_suite_entity()
|
test_suite = self.create_or_return_test_suite_entity()
|
||||||
if not test_suite:
|
if not test_suite:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"No test suite found for table {self.source_config.entityFullyQualifiedName.__root__} "
|
f"No test suite found for table {self.source_config.entityFullyQualifiedName.__root__} "
|
||||||
@ -356,14 +380,16 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
openmetadata_test_cases = self.filter_for_om_test_cases(test_cases)
|
||||||
|
|
||||||
test_suite_runner = test_suite_source_factory.create(
|
test_suite_runner = test_suite_source_factory.create(
|
||||||
self.config.source.type.lower(),
|
self.config.source.type.lower(),
|
||||||
self.config,
|
self.config,
|
||||||
self.metadata,
|
self.metadata,
|
||||||
table_entity,
|
self.table_entity,
|
||||||
).get_data_quality_runner()
|
).get_data_quality_runner()
|
||||||
|
|
||||||
for test_case in test_cases:
|
for test_case in openmetadata_test_cases:
|
||||||
try:
|
try:
|
||||||
test_result = test_suite_runner.run_and_handle(test_case)
|
test_result = test_suite_runner.run_and_handle(test_case)
|
||||||
if not test_result:
|
if not test_result:
|
||||||
|
@ -21,11 +21,14 @@ from datetime import datetime
|
|||||||
from typing import Callable, List, Optional, TypeVar, Union
|
from typing import Callable, List, Optional, TypeVar, Union
|
||||||
|
|
||||||
from metadata.generated.schema.tests.basic import (
|
from metadata.generated.schema.tests.basic import (
|
||||||
|
TestCaseFailureStatus,
|
||||||
|
TestCaseFailureStatusType,
|
||||||
TestCaseResult,
|
TestCaseResult,
|
||||||
TestCaseStatus,
|
TestCaseStatus,
|
||||||
TestResultValue,
|
TestResultValue,
|
||||||
)
|
)
|
||||||
from metadata.generated.schema.tests.testCase import TestCase, TestCaseParameterValue
|
from metadata.generated.schema.tests.testCase import TestCase, TestCaseParameterValue
|
||||||
|
from metadata.generated.schema.type.basic import Timestamp
|
||||||
from metadata.profiler.processor.runner import QueryRunner
|
from metadata.profiler.processor.runner import QueryRunner
|
||||||
|
|
||||||
T = TypeVar("T", bound=Callable)
|
T = TypeVar("T", bound=Callable)
|
||||||
@ -102,12 +105,24 @@ class BaseTestValidator(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
TestCaseResult:
|
TestCaseResult:
|
||||||
"""
|
"""
|
||||||
|
if status == TestCaseStatus.Failed:
|
||||||
|
test_case_failure_status = TestCaseFailureStatus(
|
||||||
|
testCaseFailureStatusType=TestCaseFailureStatusType.New,
|
||||||
|
testCaseFailureReason=None,
|
||||||
|
testCaseFailureComment=None,
|
||||||
|
updatedAt=Timestamp(__root__=int(datetime.utcnow().timestamp() * 1000)),
|
||||||
|
updatedBy=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
test_case_failure_status = None
|
||||||
|
|
||||||
return TestCaseResult(
|
return TestCaseResult(
|
||||||
timestamp=execution_date, # type: ignore
|
timestamp=execution_date, # type: ignore
|
||||||
testCaseStatus=status,
|
testCaseStatus=status,
|
||||||
result=result,
|
result=result,
|
||||||
testResultValue=test_result_value,
|
testResultValue=test_result_value,
|
||||||
sampleData=None,
|
sampleData=None,
|
||||||
|
testCaseFailureStatus=test_case_failure_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
def format_column_list(self, status: TestCaseStatus, cols: List):
|
def format_column_list(self, status: TestCaseStatus, cols: List):
|
||||||
|
@ -1098,6 +1098,7 @@ class SampleDataSource(
|
|||||||
test_suite=CreateTestSuiteRequest(
|
test_suite=CreateTestSuiteRequest(
|
||||||
name=test_suite["testSuiteName"],
|
name=test_suite["testSuiteName"],
|
||||||
description=test_suite["testSuiteDescription"],
|
description=test_suite["testSuiteDescription"],
|
||||||
|
executableEntityReference=test_suite["executableEntityReference"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,8 +82,9 @@ class OMetaTestSuiteTest(TestCase):
|
|||||||
|
|
||||||
cls.test_suite: TestSuite = cls.metadata.create_or_update_executable_test_suite(
|
cls.test_suite: TestSuite = cls.metadata.create_or_update_executable_test_suite(
|
||||||
CreateTestSuiteRequest(
|
CreateTestSuiteRequest(
|
||||||
name="sample_data.ecommerce_db.shopify.dim_address",
|
name="sample_data.ecommerce_db.shopify.dim_address.TestSuite",
|
||||||
description="This is a test suite for the integration tests",
|
description="This is a test suite for the integration tests",
|
||||||
|
executableEntityReference="sample_data.ecommerce_db.shopify.dim_address",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -111,10 +112,11 @@ class OMetaTestSuiteTest(TestCase):
|
|||||||
def test_get_or_create_test_suite(self):
|
def test_get_or_create_test_suite(self):
|
||||||
"""test we get a test suite object"""
|
"""test we get a test suite object"""
|
||||||
test_suite = self.metadata.get_or_create_test_suite(
|
test_suite = self.metadata.get_or_create_test_suite(
|
||||||
"sample_data.ecommerce_db.shopify.dim_address"
|
"sample_data.ecommerce_db.shopify.dim_address.TestSuite"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
test_suite.name.__root__ == "sample_data.ecommerce_db.shopify.dim_address"
|
test_suite.name.__root__
|
||||||
|
== "sample_data.ecommerce_db.shopify.dim_address.TestSuite"
|
||||||
)
|
)
|
||||||
assert isinstance(test_suite, TestSuite)
|
assert isinstance(test_suite, TestSuite)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from metadata.generated.schema.entity.services.connections.metadata.openMetadata
|
|||||||
OpenMetadataConnection,
|
OpenMetadataConnection,
|
||||||
)
|
)
|
||||||
from metadata.generated.schema.tests.testCase import TestCase
|
from metadata.generated.schema.tests.testCase import TestCase
|
||||||
|
from metadata.generated.schema.tests.testDefinition import TestDefinition
|
||||||
from metadata.generated.schema.tests.testSuite import TestSuite
|
from metadata.generated.schema.tests.testSuite import TestSuite
|
||||||
from metadata.ingestion.ometa.ometa_api import OpenMetadata
|
from metadata.ingestion.ometa.ometa_api import OpenMetadata
|
||||||
|
|
||||||
@ -115,21 +116,22 @@ class TestSuiteWorkflowTests(unittest.TestCase):
|
|||||||
_test_suite_config.update(processor)
|
_test_suite_config.update(processor)
|
||||||
|
|
||||||
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
||||||
workflow_test_suite = workflow.get_test_suite_entity()
|
workflow_test_suite = workflow.create_or_return_test_suite_entity()
|
||||||
|
|
||||||
test_suite = self.metadata.get_by_name(
|
test_suite = self.metadata.get_by_name(
|
||||||
entity=TestSuite, fqn="sample_data.ecommerce_db.shopify.dim_address"
|
entity=TestSuite,
|
||||||
|
fqn="sample_data.ecommerce_db.shopify.dim_address.TestSuite",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert workflow_test_suite.id == test_suite.id
|
assert workflow_test_suite.id == test_suite.id
|
||||||
self.test_suite_ids = [test_suite.id]
|
self.test_suite_ids = [test_suite.id]
|
||||||
|
|
||||||
def test_get_test_suite_entity(self):
|
def test_create_or_return_test_suite_entity(self):
|
||||||
"""test we can correctly retrieve a test suite"""
|
"""test we can correctly retrieve a test suite"""
|
||||||
_test_suite_config = deepcopy(test_suite_config)
|
_test_suite_config = deepcopy(test_suite_config)
|
||||||
|
|
||||||
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
||||||
test_suite = workflow.get_test_suite_entity()
|
test_suite = workflow.create_or_return_test_suite_entity()
|
||||||
|
|
||||||
expected_test_suite = self.metadata.get_by_name(
|
expected_test_suite = self.metadata.get_by_name(
|
||||||
entity=TestSuite, fqn="critical_metrics_suite"
|
entity=TestSuite, fqn="critical_metrics_suite"
|
||||||
@ -162,7 +164,7 @@ class TestSuiteWorkflowTests(unittest.TestCase):
|
|||||||
_test_suite_config.update(processor)
|
_test_suite_config.update(processor)
|
||||||
|
|
||||||
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
workflow = TestSuiteWorkflow.create(_test_suite_config)
|
||||||
test_suite = workflow.get_test_suite_entity()
|
test_suite = workflow.create_or_return_test_suite_entity()
|
||||||
test_cases = workflow.get_test_cases_from_test_suite(test_suite)
|
test_cases = workflow.get_test_cases_from_test_suite(test_suite)
|
||||||
|
|
||||||
assert isinstance(test_cases, MutableSequence)
|
assert isinstance(test_cases, MutableSequence)
|
||||||
@ -258,7 +260,7 @@ class TestSuiteWorkflowTests(unittest.TestCase):
|
|||||||
fqn="sample_data.ecommerce_db.shopify.dim_address.address_id.my_test_case_two",
|
fqn="sample_data.ecommerce_db.shopify.dim_address.address_id.my_test_case_two",
|
||||||
)
|
)
|
||||||
|
|
||||||
test_suite = workflow.get_test_suite_entity()
|
test_suite = workflow.create_or_return_test_suite_entity()
|
||||||
test_cases = self.metadata.list_entities(
|
test_cases = self.metadata.list_entities(
|
||||||
entity=TestCase,
|
entity=TestCase,
|
||||||
fields=["testSuite", "entityLink", "testDefinition"],
|
fields=["testSuite", "entityLink", "testDefinition"],
|
||||||
@ -266,7 +268,7 @@ class TestSuiteWorkflowTests(unittest.TestCase):
|
|||||||
).entities
|
).entities
|
||||||
config_test_cases_def = workflow.get_test_case_from_cli_config()
|
config_test_cases_def = workflow.get_test_case_from_cli_config()
|
||||||
created_test_case = workflow.compare_and_create_test_cases(
|
created_test_case = workflow.compare_and_create_test_cases(
|
||||||
config_test_cases_def, test_cases
|
config_test_cases_def, test_cases, test_suite
|
||||||
)
|
)
|
||||||
|
|
||||||
# clean up test
|
# clean up test
|
||||||
@ -297,3 +299,15 @@ class TestSuiteWorkflowTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(service_connection, Table)
|
assert isinstance(service_connection, Table)
|
||||||
|
|
||||||
|
# def test_filter_for_om_test_cases(self):
|
||||||
|
# """test filter for OM test cases method"""
|
||||||
|
# om_test_case_1 = TestCase(
|
||||||
|
# name="om_test_case_1",
|
||||||
|
# testDefinition=self.metadata.get_entity_reference(
|
||||||
|
# TestDefinition,
|
||||||
|
# "columnValuesToMatchRegex"
|
||||||
|
# ),
|
||||||
|
# entityLink="<entityLink>",
|
||||||
|
# testSuite=self.metadata.get_entity_reference("sample_data.ecommerce_db.shopify.dim_address.TestSuite"),
|
||||||
|
# )
|
||||||
|
@ -20,7 +20,11 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from metadata.data_quality.validations.validator import Validator
|
from metadata.data_quality.validations.validator import Validator
|
||||||
from metadata.generated.schema.tests.basic import TestCaseResult, TestCaseStatus
|
from metadata.generated.schema.tests.basic import (
|
||||||
|
TestCaseFailureStatusType,
|
||||||
|
TestCaseResult,
|
||||||
|
TestCaseStatus,
|
||||||
|
)
|
||||||
from metadata.utils.importer import import_test_case_class
|
from metadata.utils.importer import import_test_case_class
|
||||||
|
|
||||||
EXECUTION_DATE = datetime.strptime("2021-07-03", "%Y-%m-%d")
|
EXECUTION_DATE = datetime.strptime("2021-07-03", "%Y-%m-%d")
|
||||||
@ -335,3 +339,9 @@ def test_suite_validation_database(
|
|||||||
if val_2:
|
if val_2:
|
||||||
assert res.testResultValue[1].value == val_2
|
assert res.testResultValue[1].value == val_2
|
||||||
assert res.testCaseStatus == status
|
assert res.testCaseStatus == status
|
||||||
|
if res.testCaseStatus == TestCaseStatus.Failed:
|
||||||
|
assert (
|
||||||
|
res.testCaseFailureStatus.testCaseFailureStatusType
|
||||||
|
== TestCaseFailureStatusType.New
|
||||||
|
)
|
||||||
|
assert res.testCaseFailureStatus.updatedAt is not None
|
||||||
|
@ -21,7 +21,11 @@ import pytest
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from metadata.data_quality.validations.validator import Validator
|
from metadata.data_quality.validations.validator import Validator
|
||||||
from metadata.generated.schema.tests.basic import TestCaseResult, TestCaseStatus
|
from metadata.generated.schema.tests.basic import (
|
||||||
|
TestCaseFailureStatusType,
|
||||||
|
TestCaseResult,
|
||||||
|
TestCaseStatus,
|
||||||
|
)
|
||||||
from metadata.utils.importer import import_test_case_class
|
from metadata.utils.importer import import_test_case_class
|
||||||
|
|
||||||
EXECUTION_DATE = datetime.strptime("2021-07-03", "%Y-%m-%d")
|
EXECUTION_DATE = datetime.strptime("2021-07-03", "%Y-%m-%d")
|
||||||
@ -148,7 +152,7 @@ DATALAKE_DATA_FRAME = lambda times_increase_sample_data: DataFrame(
|
|||||||
"test_case_column_values_missing_count_to_be_equal",
|
"test_case_column_values_missing_count_to_be_equal",
|
||||||
"columnValuesMissingCount",
|
"columnValuesMissingCount",
|
||||||
"COLUMN",
|
"COLUMN",
|
||||||
(TestCaseResult, "2000", None, TestCaseStatus.Success),
|
(TestCaseResult, "2000", None, TestCaseStatus.Failed),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"test_case_column_values_missing_count_to_be_equal_missing_values",
|
"test_case_column_values_missing_count_to_be_equal_missing_values",
|
||||||
@ -249,7 +253,7 @@ DATALAKE_DATA_FRAME = lambda times_increase_sample_data: DataFrame(
|
|||||||
"test_case_table_row_count_to_be_between",
|
"test_case_table_row_count_to_be_between",
|
||||||
"tableRowCountToBeBetween",
|
"tableRowCountToBeBetween",
|
||||||
"TABLE",
|
"TABLE",
|
||||||
(TestCaseResult, "6000", None, TestCaseStatus.Success),
|
(TestCaseResult, "6000", None, TestCaseStatus.Failed),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"test_case_table_row_count_to_be_equal",
|
"test_case_table_row_count_to_be_equal",
|
||||||
@ -298,4 +302,12 @@ def test_suite_validation_datalake(
|
|||||||
assert res.testResultValue[0].value == val_1
|
assert res.testResultValue[0].value == val_1
|
||||||
if val_2:
|
if val_2:
|
||||||
assert res.testResultValue[1].value == val_2
|
assert res.testResultValue[1].value == val_2
|
||||||
|
|
||||||
assert res.testCaseStatus == status
|
assert res.testCaseStatus == status
|
||||||
|
|
||||||
|
if res.testCaseStatus == TestCaseStatus.Failed:
|
||||||
|
assert (
|
||||||
|
res.testCaseFailureStatus.testCaseFailureStatusType
|
||||||
|
== TestCaseFailureStatusType.New
|
||||||
|
)
|
||||||
|
assert res.testCaseFailureStatus.updatedAt is not None
|
||||||
|
@ -3,12 +3,16 @@ package org.openmetadata.service.jdbi3;
|
|||||||
import static org.openmetadata.service.Entity.TEST_CASE;
|
import static org.openmetadata.service.Entity.TEST_CASE;
|
||||||
import static org.openmetadata.service.Entity.TEST_DEFINITION;
|
import static org.openmetadata.service.Entity.TEST_DEFINITION;
|
||||||
import static org.openmetadata.service.Entity.TEST_SUITE;
|
import static org.openmetadata.service.Entity.TEST_SUITE;
|
||||||
|
import static org.openmetadata.service.util.RestUtil.ENTITY_NO_CHANGE;
|
||||||
|
import static org.openmetadata.service.util.RestUtil.ENTITY_UPDATED;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import javax.json.JsonPatch;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import org.jdbi.v3.sqlobject.transaction.Transaction;
|
import org.jdbi.v3.sqlobject.transaction.Transaction;
|
||||||
@ -55,6 +59,29 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
|
|||||||
return test.withTestCaseResult(fields.contains(TEST_CASE_RESULT_FIELD) ? getTestCaseResult(test) : null);
|
return test.withTestCaseResult(fields.contains(TEST_CASE_RESULT_FIELD) ? getTestCaseResult(test) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RestUtil.PatchResponse<TestCaseResult> patchTestCaseResults(
|
||||||
|
String fqn, Long timestamp, UriInfo uriInfo, String user, JsonPatch patch) throws IOException {
|
||||||
|
String change = ENTITY_NO_CHANGE;
|
||||||
|
TestCaseResult original =
|
||||||
|
JsonUtils.readValue(
|
||||||
|
daoCollection
|
||||||
|
.entityExtensionTimeSeriesDao()
|
||||||
|
.getExtensionAtTimestamp(fqn, TESTCASE_RESULT_EXTENSION, timestamp),
|
||||||
|
TestCaseResult.class);
|
||||||
|
|
||||||
|
TestCaseResult updated = JsonUtils.applyPatch(original, patch, TestCaseResult.class);
|
||||||
|
|
||||||
|
if (!Objects.equals(original.getTestCaseFailureStatus(), updated.getTestCaseFailureStatus())) {
|
||||||
|
updated.getTestCaseFailureStatus().setUpdatedBy(user);
|
||||||
|
updated.getTestCaseFailureStatus().setUpdatedAt(System.currentTimeMillis());
|
||||||
|
daoCollection
|
||||||
|
.entityExtensionTimeSeriesDao()
|
||||||
|
.update(fqn, TESTCASE_RESULT_EXTENSION, JsonUtils.pojoToJson(updated), timestamp);
|
||||||
|
change = ENTITY_UPDATED;
|
||||||
|
}
|
||||||
|
return new RestUtil.PatchResponse<>(Response.Status.OK, updated, change);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFullyQualifiedName(TestCase test) {
|
public void setFullyQualifiedName(TestCase test) {
|
||||||
EntityLink entityLink = EntityLink.parse(test.getEntityLink());
|
EntityLink entityLink = EntityLink.parse(test.getEntityLink());
|
||||||
|
@ -67,7 +67,9 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void storeExecutableRelationship(TestSuite testSuite) throws IOException {
|
public void storeExecutableRelationship(TestSuite testSuite) throws IOException {
|
||||||
Table table = Entity.getEntityByName(Entity.TABLE, testSuite.getName(), null, null);
|
Table table =
|
||||||
|
Entity.getEntityByName(
|
||||||
|
Entity.TABLE, testSuite.getExecutableEntityReference().getFullyQualifiedName(), null, null);
|
||||||
addRelationship(table.getId(), testSuite.getId(), Entity.TABLE, TEST_SUITE, Relationship.CONTAINS);
|
addRelationship(table.getId(), testSuite.getId(), Entity.TABLE, TEST_SUITE, Relationship.CONTAINS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,6 +361,40 @@ public class TestCaseResource extends EntityResource<TestCase, TestCaseRepositor
|
|||||||
return response.toResponse();
|
return response.toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PATCH
|
||||||
|
@Path("/{fqn}/testCaseResult/{timestamp}")
|
||||||
|
@Operation(
|
||||||
|
operationId = "patchTestCaseResult",
|
||||||
|
summary = "Update a test case result",
|
||||||
|
description = "Update an existing test case using JsonPatch.",
|
||||||
|
externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902"))
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
|
||||||
|
public Response patchTestCaseResult(
|
||||||
|
@Context UriInfo uriInfo,
|
||||||
|
@Context SecurityContext securityContext,
|
||||||
|
@Parameter(description = "fqn of the test case", schema = @Schema(type = "string")) @PathParam("fqn") String fqn,
|
||||||
|
@Parameter(description = "Timestamp of the testCase result", schema = @Schema(type = "long"))
|
||||||
|
@PathParam("timestamp")
|
||||||
|
Long timestamp,
|
||||||
|
@RequestBody(
|
||||||
|
description = "JsonPatch with array of operations",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
|
||||||
|
examples = {
|
||||||
|
@ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]")
|
||||||
|
}))
|
||||||
|
JsonPatch patch)
|
||||||
|
throws IOException {
|
||||||
|
// Override OperationContext to change the entity to table and operation from UPDATE to EDIT_TESTS
|
||||||
|
ResourceContextInterface resourceContext = TestCaseResourceContext.builder().name(fqn).build();
|
||||||
|
OperationContext operationContext = new OperationContext(Entity.TABLE, MetadataOperation.EDIT_TESTS);
|
||||||
|
authorizer.authorize(securityContext, operationContext, resourceContext);
|
||||||
|
PatchResponse<TestCaseResult> patchResponse =
|
||||||
|
dao.patchTestCaseResults(fqn, timestamp, uriInfo, securityContext.getUserPrincipal().getName(), patch);
|
||||||
|
return patchResponse.toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Operation(
|
@Operation(
|
||||||
operationId = "createOrUpdateTest",
|
operationId = "createOrUpdateTest",
|
||||||
|
@ -34,8 +34,10 @@ import javax.ws.rs.core.UriInfo;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.openmetadata.schema.api.data.RestoreEntity;
|
import org.openmetadata.schema.api.data.RestoreEntity;
|
||||||
import org.openmetadata.schema.api.tests.CreateTestSuite;
|
import org.openmetadata.schema.api.tests.CreateTestSuite;
|
||||||
|
import org.openmetadata.schema.entity.data.Table;
|
||||||
import org.openmetadata.schema.tests.TestSuite;
|
import org.openmetadata.schema.tests.TestSuite;
|
||||||
import org.openmetadata.schema.type.EntityHistory;
|
import org.openmetadata.schema.type.EntityHistory;
|
||||||
|
import org.openmetadata.schema.type.EntityReference;
|
||||||
import org.openmetadata.schema.type.Include;
|
import org.openmetadata.schema.type.Include;
|
||||||
import org.openmetadata.service.Entity;
|
import org.openmetadata.service.Entity;
|
||||||
import org.openmetadata.service.jdbi3.CollectionDAO;
|
import org.openmetadata.service.jdbi3.CollectionDAO;
|
||||||
@ -248,6 +250,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
|
|||||||
public Response create(
|
public Response create(
|
||||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
create = create.withExecutableEntityReference(null); // entity reference is not applicable for logical test suites
|
||||||
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
||||||
testSuite.setExecutable(false);
|
testSuite.setExecutable(false);
|
||||||
return create(uriInfo, securityContext, testSuite);
|
return create(uriInfo, securityContext, testSuite);
|
||||||
@ -269,8 +272,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
|
|||||||
public Response createExecutable(
|
public Response createExecutable(
|
||||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
// We'll check if we have a corresponding table entity
|
Entity.getEntityByName(Entity.TABLE, create.getExecutableEntityReference(), null, null); // check if entity exists
|
||||||
Entity.getEntityByName(Entity.TABLE, create.getName(), null, null);
|
|
||||||
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
||||||
testSuite.setExecutable(true);
|
testSuite.setExecutable(true);
|
||||||
return create(uriInfo, securityContext, testSuite);
|
return create(uriInfo, securityContext, testSuite);
|
||||||
@ -315,6 +317,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
|
|||||||
public Response createOrUpdate(
|
public Response createOrUpdate(
|
||||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
create = create.withExecutableEntityReference(null); // entity reference is not applicable for logical test suites
|
||||||
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
||||||
testSuite.setExecutable(false);
|
testSuite.setExecutable(false);
|
||||||
return createOrUpdate(uriInfo, securityContext, testSuite);
|
return createOrUpdate(uriInfo, securityContext, testSuite);
|
||||||
@ -335,7 +338,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
|
|||||||
public Response createOrUpdateExecutable(
|
public Response createOrUpdateExecutable(
|
||||||
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Entity.getEntityByName(Entity.TABLE, create.getName(), null, null);
|
Entity.getEntityByName(Entity.TABLE, create.getExecutableEntityReference(), null, null); // Check if table exists
|
||||||
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
|
||||||
testSuite.setExecutable(true);
|
testSuite.setExecutable(true);
|
||||||
return createOrUpdate(uriInfo, securityContext, testSuite);
|
return createOrUpdate(uriInfo, securityContext, testSuite);
|
||||||
@ -409,9 +412,21 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TestSuite getTestSuite(CreateTestSuite create, String user) throws IOException {
|
private TestSuite getTestSuite(CreateTestSuite create, String user) throws IOException {
|
||||||
return copy(new TestSuite(), create, user)
|
TestSuite testSuite =
|
||||||
|
copy(new TestSuite(), create, user)
|
||||||
.withDescription(create.getDescription())
|
.withDescription(create.getDescription())
|
||||||
.withDisplayName(create.getDisplayName())
|
.withDisplayName(create.getDisplayName())
|
||||||
.withName(create.getName());
|
.withName(create.getName());
|
||||||
|
if (create.getExecutableEntityReference() != null) {
|
||||||
|
Table table = Entity.getEntityByName(Entity.TABLE, create.getExecutableEntityReference(), null, null);
|
||||||
|
EntityReference entityReference =
|
||||||
|
new EntityReference()
|
||||||
|
.withId(table.getId())
|
||||||
|
.withFullyQualifiedName(table.getFullyQualifiedName())
|
||||||
|
.withName(table.getName())
|
||||||
|
.withType(Entity.TABLE);
|
||||||
|
testSuite.setExecutableEntityReference(entityReference);
|
||||||
|
}
|
||||||
|
return testSuite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,13 @@ import static org.openmetadata.service.util.TestUtils.assertListNotNull;
|
|||||||
import static org.openmetadata.service.util.TestUtils.assertListNull;
|
import static org.openmetadata.service.util.TestUtils.assertListNull;
|
||||||
import static org.openmetadata.service.util.TestUtils.assertResponse;
|
import static org.openmetadata.service.util.TestUtils.assertResponse;
|
||||||
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
|
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
|
||||||
|
import static org.openmetadata.service.util.TestUtils.dateToTimestamp;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import javax.json.JsonPatch;
|
||||||
import javax.ws.rs.client.WebTarget;
|
import javax.ws.rs.client.WebTarget;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.http.client.HttpResponseException;
|
import org.apache.http.client.HttpResponseException;
|
||||||
@ -39,6 +41,9 @@ import org.openmetadata.schema.api.tests.CreateTestSuite;
|
|||||||
import org.openmetadata.schema.tests.TestCase;
|
import org.openmetadata.schema.tests.TestCase;
|
||||||
import org.openmetadata.schema.tests.TestCaseParameterValue;
|
import org.openmetadata.schema.tests.TestCaseParameterValue;
|
||||||
import org.openmetadata.schema.tests.TestSuite;
|
import org.openmetadata.schema.tests.TestSuite;
|
||||||
|
import org.openmetadata.schema.tests.type.TestCaseFailureReason;
|
||||||
|
import org.openmetadata.schema.tests.type.TestCaseFailureStatus;
|
||||||
|
import org.openmetadata.schema.tests.type.TestCaseFailureStatusType;
|
||||||
import org.openmetadata.schema.tests.type.TestCaseResult;
|
import org.openmetadata.schema.tests.type.TestCaseResult;
|
||||||
import org.openmetadata.schema.tests.type.TestCaseStatus;
|
import org.openmetadata.schema.tests.type.TestCaseStatus;
|
||||||
import org.openmetadata.schema.type.ChangeDescription;
|
import org.openmetadata.schema.type.ChangeDescription;
|
||||||
@ -438,6 +443,97 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
|||||||
deleteAndCheckEntity(testCase, ownerAuthHeaders);
|
deleteAndCheckEntity(testCase, ownerAuthHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patch_testCaseResultsFailureStatus_change(TestInfo test) throws IOException, ParseException {
|
||||||
|
CreateTestCase create =
|
||||||
|
createRequest(test)
|
||||||
|
.withEntityLink(TABLE_LINK_2)
|
||||||
|
.withTestSuite(TEST_SUITE1.getFullyQualifiedName())
|
||||||
|
.withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName())
|
||||||
|
.withParameterValues(List.of(new TestCaseParameterValue().withValue("100").withName("missingCountValue")));
|
||||||
|
TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
TestCaseResult testCaseResult =
|
||||||
|
new TestCaseResult()
|
||||||
|
.withResult("tested")
|
||||||
|
.withTestCaseStatus(TestCaseStatus.Failed)
|
||||||
|
.withTimestamp(TestUtils.dateToTimestamp("2021-09-09"));
|
||||||
|
TestCaseFailureStatus testCaseFailureStatus =
|
||||||
|
new TestCaseFailureStatus().withTestCaseFailureStatusType(TestCaseFailureStatusType.New);
|
||||||
|
testCaseResult.setTestCaseFailureStatus(testCaseFailureStatus);
|
||||||
|
putTestCaseResult(testCase.getFullyQualifiedName(), testCaseResult, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
ResultList<TestCaseResult> testCaseResultResultList =
|
||||||
|
getTestCaseResults(
|
||||||
|
testCase.getFullyQualifiedName(),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
testCaseResultResultList.getData().get(0).getTestCaseFailureStatus().getTestCaseFailureStatusType(),
|
||||||
|
TestCaseFailureStatusType.New);
|
||||||
|
|
||||||
|
String original = JsonUtils.pojoToJson(testCaseResult);
|
||||||
|
testCaseResult
|
||||||
|
.getTestCaseFailureStatus()
|
||||||
|
.withTestCaseFailureStatusType(TestCaseFailureStatusType.Resolved)
|
||||||
|
.withTestCaseFailureReason(TestCaseFailureReason.FalsePositive)
|
||||||
|
.withTestCaseFailureComment("Test failure was a false positive");
|
||||||
|
|
||||||
|
JsonPatch patch = JsonUtils.getJsonPatch(original, JsonUtils.pojoToJson(testCaseResult));
|
||||||
|
|
||||||
|
patchTestCaseResult(testCase.getFullyQualifiedName(), dateToTimestamp("2021-09-09"), patch, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
ResultList<TestCaseResult> testCaseResultResultListUpdated =
|
||||||
|
getTestCaseResults(
|
||||||
|
testCase.getFullyQualifiedName(),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
testCaseResultResultListUpdated.getData().get(0).getTestCaseFailureStatus().getTestCaseFailureStatusType(),
|
||||||
|
TestCaseFailureStatusType.Resolved);
|
||||||
|
assertEquals(
|
||||||
|
testCaseResultResultListUpdated.getData().get(0).getTestCaseFailureStatus().getTestCaseFailureReason(),
|
||||||
|
TestCaseFailureReason.FalsePositive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patch_testCaseResults_noChange(TestInfo test) throws IOException, ParseException {
|
||||||
|
CreateTestCase create =
|
||||||
|
createRequest(test)
|
||||||
|
.withEntityLink(TABLE_LINK_2)
|
||||||
|
.withTestSuite(TEST_SUITE1.getFullyQualifiedName())
|
||||||
|
.withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName())
|
||||||
|
.withParameterValues(List.of(new TestCaseParameterValue().withValue("100").withName("missingCountValue")));
|
||||||
|
TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
TestCaseResult testCaseResult =
|
||||||
|
new TestCaseResult()
|
||||||
|
.withResult("tested")
|
||||||
|
.withTestCaseStatus(TestCaseStatus.Success)
|
||||||
|
.withTimestamp(TestUtils.dateToTimestamp("2021-09-09"));
|
||||||
|
putTestCaseResult(testCase.getFullyQualifiedName(), testCaseResult, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
String original = JsonUtils.pojoToJson(testCaseResult);
|
||||||
|
testCaseResult.setTestCaseStatus(TestCaseStatus.Failed);
|
||||||
|
JsonPatch patch = JsonUtils.getJsonPatch(original, JsonUtils.pojoToJson(testCaseResult));
|
||||||
|
|
||||||
|
patchTestCaseResult(testCase.getFullyQualifiedName(), dateToTimestamp("2021-09-09"), patch, ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
ResultList<TestCaseResult> testCaseResultResultListUpdated =
|
||||||
|
getTestCaseResults(
|
||||||
|
testCase.getFullyQualifiedName(),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
TestUtils.dateToTimestamp("2021-09-09"),
|
||||||
|
ADMIN_AUTH_HEADERS);
|
||||||
|
|
||||||
|
// patching anything else than the test case failure status should not change anything
|
||||||
|
assertEquals(testCaseResultResultListUpdated.getData().get(0).getTestCaseStatus(), TestCaseStatus.Success);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Override
|
@Override
|
||||||
public void delete_entity_as_non_admin_401(TestInfo test) throws HttpResponseException {
|
public void delete_entity_as_non_admin_401(TestInfo test) throws HttpResponseException {
|
||||||
@ -572,6 +668,13 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
|
|||||||
return TestUtils.get(target, TestCaseResource.TestCaseList.class, authHeaders);
|
return TestUtils.get(target, TestCaseResource.TestCaseList.class, authHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TestCaseResult patchTestCaseResult(
|
||||||
|
String testCaseFqn, Long timestamp, JsonPatch patch, Map<String, String> authHeaders)
|
||||||
|
throws HttpResponseException {
|
||||||
|
WebTarget target = getCollection().path("/" + testCaseFqn + "/testCaseResult/" + timestamp);
|
||||||
|
return TestUtils.patch(target, patch, TestCaseResult.class, authHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyTestCaseResults(
|
private void verifyTestCaseResults(
|
||||||
ResultList<TestCaseResult> actualTestCaseResults,
|
ResultList<TestCaseResult> actualTestCaseResults,
|
||||||
List<TestCaseResult> expectedTestCaseResults,
|
List<TestCaseResult> expectedTestCaseResults,
|
||||||
|
@ -336,6 +336,7 @@ public class TestSuiteResourceTest extends EntityResourceTest<TestSuite, CreateT
|
|||||||
public TestSuite createExecutableTestSuite(CreateTestSuite createTestSuite, Map<String, String> authHeaders)
|
public TestSuite createExecutableTestSuite(CreateTestSuite createTestSuite, Map<String, String> authHeaders)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
WebTarget target = getResource("dataQuality/testSuites/executable");
|
WebTarget target = getResource("dataQuality/testSuites/executable");
|
||||||
|
createTestSuite.setExecutableEntityReference(createTestSuite.getName());
|
||||||
return TestUtils.post(target, createTestSuite, TestSuite.class, authHeaders);
|
return TestUtils.post(target, createTestSuite, TestSuite.class, authHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"description": "Name that identifies this test suite. For executable testSuite, this should match the an entity FQN in the platform.",
|
"description": "Name that identifies this test suite.",
|
||||||
"$ref": "#/definitions/testSuiteEntityName"
|
"$ref": "#/definitions/testSuiteEntityName"
|
||||||
},
|
},
|
||||||
"displayName": {
|
"displayName": {
|
||||||
@ -30,6 +30,10 @@
|
|||||||
"owner": {
|
"owner": {
|
||||||
"description": "Owner of this test suite",
|
"description": "Owner of this test suite",
|
||||||
"$ref": "../../type/entityReference.json"
|
"$ref": "../../type/entityReference.json"
|
||||||
|
},
|
||||||
|
"executableEntityReference": {
|
||||||
|
"description": "FQN of the entity the test suite is executed against.. Only applicable for executable test suites.",
|
||||||
|
"$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"default": "TestSuite"
|
"default": "TestSuite"
|
||||||
},
|
},
|
||||||
"entityFullyQualifiedName": {
|
"entityFullyQualifiedName": {
|
||||||
"description": "Name of the test suite. For executable test suite it should match a fully qualified name of the table",
|
"description": "Fully qualified name of the entity to be tested.",
|
||||||
"$ref": "../type/basic.json#/definitions/fullyQualifiedEntityName"
|
"$ref": "../type/basic.json#/definitions/fullyQualifiedEntityName"
|
||||||
},
|
},
|
||||||
"profileSample": {
|
"profileSample": {
|
||||||
|
@ -62,6 +62,66 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/testResultValue"
|
"$ref": "#/definitions/testResultValue"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"testCaseFailureStatus": {
|
||||||
|
"description": "Schema to capture test case result.",
|
||||||
|
"javaType": "org.openmetadata.schema.tests.type.TestCaseFailureStatus",
|
||||||
|
"type": "object",
|
||||||
|
"properties":
|
||||||
|
{
|
||||||
|
"testCaseFailureStatusType": {
|
||||||
|
"description": "Status of Test Case Acknowledgement.",
|
||||||
|
"javaType": "org.openmetadata.schema.tests.type.TestCaseFailureStatusType",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["Ack", "New", "Resolved"],
|
||||||
|
"javaEnums": [
|
||||||
|
{
|
||||||
|
"name": "Ack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "New"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Resolved"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"testCaseFailureReason": {
|
||||||
|
"description": "Reason of Test Case resolution.",
|
||||||
|
"javaType": "org.openmetadata.schema.tests.type.TestCaseFailureReason",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["FalsePositive", "MissingData", "Duplicates", "OutOfBounds", "Other"],
|
||||||
|
"javaEnums": [
|
||||||
|
{
|
||||||
|
"name": "FalsePositive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MissingData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Duplicates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OutOfBounds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"testCaseFailureComment": {
|
||||||
|
"description": "Test case failure resolution comment.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedBy": {
|
||||||
|
"description": "User who updated the test case failure status.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"description": "Time when test case failure status was updated.",
|
||||||
|
"$ref": "../type/basic.json#/definitions/timestamp"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -101,6 +101,10 @@
|
|||||||
"description": "Indicates if the test suite is executable. Set on the backend.",
|
"description": "Indicates if the test suite is executable. Set on the backend.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"executableEntityReference": {
|
||||||
|
"description": "Entity reference the test suite is executed against. Only applicable if the test suite is executable.",
|
||||||
|
"$ref": "../type/entityReference.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "description"],
|
"required": ["name", "description"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user