Fixe Issue #11863 - Add Status logic for test case results (#11881)

* 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:
Teddy 2023-06-06 09:45:49 +02:00 committed by GitHub
parent 236141d9df
commit 06735fe8db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 377 additions and 46 deletions

View File

@ -1,7 +1,8 @@
{
"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",
"scheduleInterval": "0 0 * * MON",
"testCases": [

View File

@ -50,6 +50,7 @@ from metadata.generated.schema.metadataIngestion.workflow import (
OpenMetadataWorkflowConfig,
)
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.type.basic import EntityLink, FullyQualifiedEntityName
from metadata.ingestion.api.parser import parse_workflow_config_gracefully
@ -115,6 +116,10 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
self.status = ProcessorStatus()
self.table_entity: Optional[Table] = self._get_table_entity(
self.source_config.entityFullyQualifiedName.__root__
)
if self.config.sink:
self.sink = get_sink(
sink_type=self.config.sink.type,
@ -142,7 +147,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
)
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
Args:
@ -151,20 +156,17 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
return self.metadata.get_by_name(
entity=Table,
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.
In the UI workflow we'll write the entity name (i.e. the test suite)
to source.serviceName.
"""
test_suite = self.metadata.get_by_name(
entity=TestSuite,
fqn=self.source_config.entityFullyQualifiedName.__root__,
)
self.table_entity = cast(Table, self.table_entity) # satisfy type checker
test_suite = self.table_entity.testSuite
if test_suite and not test_suite.executable:
logger.debug(
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(
CreateTestSuiteRequest(
name=self.source_config.entityFullyQualifiedName.__root__,
displayName=self.source_config.entityFullyQualifiedName.__root__,
name=f"{self.source_config.entityFullyQualifiedName.__root__}.TestSuite",
displayName=f"{self.source_config.entityFullyQualifiedName.__root__} Test Suite",
description="Test Suite created from YAML processor config file",
owner=None,
executableEntityReference=self.source_config.entityFullyQualifiedName.__root__,
)
)
@ -209,10 +212,33 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
cli_test_cases = cast(
List[TestCaseDefinition], cli_test_cases
) # 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
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(
self,
) -> Optional[List[TestCaseDefinition]]:
@ -257,6 +283,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
self,
cli_test_cases_definitions: Optional[List[TestCaseDefinition]],
test_cases: List[TestCase],
test_suite: TestSuite,
) -> Optional[List[TestCase]]:
"""
compare test cases defined in CLI config workflow with test cases
@ -306,7 +333,7 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
test_case_to_create.columnName,
)
),
testSuite=self.source_config.entityFullyQualifiedName,
testSuite=test_suite.fullyQualifiedName.__root__,
parameterValues=list(test_case_to_create.parameterValues)
if test_case_to_create.parameterValues
else None,
@ -330,17 +357,14 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
def run_test_suite(self):
"""Main logic to run the tests"""
table_entity: Table = self._get_table_entity(
self.source_config.entityFullyQualifiedName.__root__
)
if not table_entity:
if not self.table_entity:
logger.debug(traceback.format_exc())
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."
)
test_suite = self.get_test_suite_entity()
test_suite = self.create_or_return_test_suite_entity()
if not test_suite:
logger.debug(
f"No test suite found for table {self.source_config.entityFullyQualifiedName.__root__} "
@ -356,14 +380,16 @@ class TestSuiteWorkflow(WorkflowStatusMixin):
)
return
openmetadata_test_cases = self.filter_for_om_test_cases(test_cases)
test_suite_runner = test_suite_source_factory.create(
self.config.source.type.lower(),
self.config,
self.metadata,
table_entity,
self.table_entity,
).get_data_quality_runner()
for test_case in test_cases:
for test_case in openmetadata_test_cases:
try:
test_result = test_suite_runner.run_and_handle(test_case)
if not test_result:

View File

@ -21,11 +21,14 @@ from datetime import datetime
from typing import Callable, List, Optional, TypeVar, Union
from metadata.generated.schema.tests.basic import (
TestCaseFailureStatus,
TestCaseFailureStatusType,
TestCaseResult,
TestCaseStatus,
TestResultValue,
)
from metadata.generated.schema.tests.testCase import TestCase, TestCaseParameterValue
from metadata.generated.schema.type.basic import Timestamp
from metadata.profiler.processor.runner import QueryRunner
T = TypeVar("T", bound=Callable)
@ -102,12 +105,24 @@ class BaseTestValidator(ABC):
Returns:
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(
timestamp=execution_date, # type: ignore
testCaseStatus=status,
result=result,
testResultValue=test_result_value,
sampleData=None,
testCaseFailureStatus=test_case_failure_status,
)
def format_column_list(self, status: TestCaseStatus, cols: List):

View File

@ -1098,6 +1098,7 @@ class SampleDataSource(
test_suite=CreateTestSuiteRequest(
name=test_suite["testSuiteName"],
description=test_suite["testSuiteDescription"],
executableEntityReference=test_suite["executableEntityReference"],
)
)

View File

@ -82,8 +82,9 @@ class OMetaTestSuiteTest(TestCase):
cls.test_suite: TestSuite = cls.metadata.create_or_update_executable_test_suite(
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",
executableEntityReference="sample_data.ecommerce_db.shopify.dim_address",
)
)
@ -111,10 +112,11 @@ class OMetaTestSuiteTest(TestCase):
def test_get_or_create_test_suite(self):
"""test we get a test suite object"""
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 (
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)

View File

@ -23,6 +23,7 @@ from metadata.generated.schema.entity.services.connections.metadata.openMetadata
OpenMetadataConnection,
)
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.ingestion.ometa.ometa_api import OpenMetadata
@ -115,21 +116,22 @@ class TestSuiteWorkflowTests(unittest.TestCase):
_test_suite_config.update(processor)
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(
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
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_suite_config = deepcopy(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(
entity=TestSuite, fqn="critical_metrics_suite"
@ -162,7 +164,7 @@ class TestSuiteWorkflowTests(unittest.TestCase):
_test_suite_config.update(processor)
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)
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",
)
test_suite = workflow.get_test_suite_entity()
test_suite = workflow.create_or_return_test_suite_entity()
test_cases = self.metadata.list_entities(
entity=TestCase,
fields=["testSuite", "entityLink", "testDefinition"],
@ -266,7 +268,7 @@ class TestSuiteWorkflowTests(unittest.TestCase):
).entities
config_test_cases_def = workflow.get_test_case_from_cli_config()
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
@ -297,3 +299,15 @@ class TestSuiteWorkflowTests(unittest.TestCase):
)
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"),
# )

View File

@ -20,7 +20,11 @@ from unittest.mock import patch
import pytest
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
EXECUTION_DATE = datetime.strptime("2021-07-03", "%Y-%m-%d")
@ -335,3 +339,9 @@ def test_suite_validation_database(
if val_2:
assert res.testResultValue[1].value == val_2
assert res.testCaseStatus == status
if res.testCaseStatus == TestCaseStatus.Failed:
assert (
res.testCaseFailureStatus.testCaseFailureStatusType
== TestCaseFailureStatusType.New
)
assert res.testCaseFailureStatus.updatedAt is not None

View File

@ -21,7 +21,11 @@ import pytest
from pandas import DataFrame
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
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",
"columnValuesMissingCount",
"COLUMN",
(TestCaseResult, "2000", None, TestCaseStatus.Success),
(TestCaseResult, "2000", None, TestCaseStatus.Failed),
),
(
"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",
"tableRowCountToBeBetween",
"TABLE",
(TestCaseResult, "6000", None, TestCaseStatus.Success),
(TestCaseResult, "6000", None, TestCaseStatus.Failed),
),
(
"test_case_table_row_count_to_be_equal",
@ -298,4 +302,12 @@ def test_suite_validation_datalake(
assert res.testResultValue[0].value == val_1
if 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

View File

@ -3,12 +3,16 @@ package org.openmetadata.service.jdbi3;
import static org.openmetadata.service.Entity.TEST_CASE;
import static org.openmetadata.service.Entity.TEST_DEFINITION;
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.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
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);
}
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
public void setFullyQualifiedName(TestCase test) {
EntityLink entityLink = EntityLink.parse(test.getEntityLink());

View File

@ -67,7 +67,9 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
}
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);
}

View File

@ -361,6 +361,40 @@ public class TestCaseResource extends EntityResource<TestCase, TestCaseRepositor
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
@Operation(
operationId = "createOrUpdateTest",

View File

@ -34,8 +34,10 @@ import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.api.data.RestoreEntity;
import org.openmetadata.schema.api.tests.CreateTestSuite;
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.tests.TestSuite;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.CollectionDAO;
@ -248,6 +250,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
public Response create(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
throws IOException {
create = create.withExecutableEntityReference(null); // entity reference is not applicable for logical test suites
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
testSuite.setExecutable(false);
return create(uriInfo, securityContext, testSuite);
@ -269,8 +272,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
public Response createExecutable(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
throws IOException {
// We'll check if we have a corresponding table entity
Entity.getEntityByName(Entity.TABLE, create.getName(), null, null);
Entity.getEntityByName(Entity.TABLE, create.getExecutableEntityReference(), null, null); // check if entity exists
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
testSuite.setExecutable(true);
return create(uriInfo, securityContext, testSuite);
@ -315,6 +317,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
public Response createOrUpdate(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
throws IOException {
create = create.withExecutableEntityReference(null); // entity reference is not applicable for logical test suites
TestSuite testSuite = getTestSuite(create, securityContext.getUserPrincipal().getName());
testSuite.setExecutable(false);
return createOrUpdate(uriInfo, securityContext, testSuite);
@ -335,7 +338,7 @@ public class TestSuiteResource extends EntityResource<TestSuite, TestSuiteReposi
public Response createOrUpdateExecutable(
@Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateTestSuite create)
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.setExecutable(true);
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 {
return copy(new TestSuite(), create, user)
.withDescription(create.getDescription())
.withDisplayName(create.getDisplayName())
.withName(create.getName());
TestSuite testSuite =
copy(new TestSuite(), create, user)
.withDescription(create.getDescription())
.withDisplayName(create.getDisplayName())
.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;
}
}

View File

@ -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.assertResponse;
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
import static org.openmetadata.service.util.TestUtils.dateToTimestamp;
import java.io.IOException;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
import javax.json.JsonPatch;
import javax.ws.rs.client.WebTarget;
import lombok.extern.slf4j.Slf4j;
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.TestCaseParameterValue;
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.TestCaseStatus;
import org.openmetadata.schema.type.ChangeDescription;
@ -438,6 +443,97 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
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
@Override
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);
}
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(
ResultList<TestCaseResult> actualTestCaseResults,
List<TestCaseResult> expectedTestCaseResults,

View File

@ -336,6 +336,7 @@ public class TestSuiteResourceTest extends EntityResourceTest<TestSuite, CreateT
public TestSuite createExecutableTestSuite(CreateTestSuite createTestSuite, Map<String, String> authHeaders)
throws IOException {
WebTarget target = getResource("dataQuality/testSuites/executable");
createTestSuite.setExecutableEntityReference(createTestSuite.getName());
return TestUtils.post(target, createTestSuite, TestSuite.class, authHeaders);
}

View File

@ -16,7 +16,7 @@
},
"properties": {
"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"
},
"displayName": {
@ -30,6 +30,10 @@
"owner": {
"description": "Owner of this test suite",
"$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"],

View File

@ -18,7 +18,7 @@
"default": "TestSuite"
},
"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"
},
"profileSample": {

View File

@ -62,6 +62,66 @@
"items": {
"$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

View File

@ -101,6 +101,10 @@
"description": "Indicates if the test suite is executable. Set on the backend.",
"type": "boolean",
"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"],