diff --git a/ingestion/src/metadata/data_quality/processor/test_case_runner.py b/ingestion/src/metadata/data_quality/processor/test_case_runner.py index 0cb1191f0cb..48ea2948a70 100644 --- a/ingestion/src/metadata/data_quality/processor/test_case_runner.py +++ b/ingestion/src/metadata/data_quality/processor/test_case_runner.py @@ -41,14 +41,13 @@ 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.models import Either from metadata.ingestion.api.parser import parse_workflow_config_gracefully from metadata.ingestion.api.step import Step from metadata.ingestion.api.steps import Processor from metadata.ingestion.ometa.ometa_api import OpenMetadata -from metadata.utils import entity_link, fqn +from metadata.utils import entity_link from metadata.utils.logger import test_suite_logger logger = test_suite_logger() @@ -83,11 +82,6 @@ class TestCaseRunner(Processor): # Add the test cases from the YAML file, if any test_cases = self.get_test_cases( test_cases=record.test_cases, - test_suite_fqn=fqn.build( - None, - TestSuite, - table_fqn=record.table.fullyQualifiedName.root, - ), table_fqn=record.table.fullyQualifiedName.root, ) openmetadata_test_cases = self.filter_for_om_test_cases(test_cases) @@ -113,7 +107,7 @@ class TestCaseRunner(Processor): return Either(right=TestCaseResults(test_results=test_results)) def get_test_cases( - self, test_cases: List[TestCase], test_suite_fqn: str, table_fqn: str + self, test_cases: List[TestCase], table_fqn: str ) -> List[TestCase]: """ Based on the test suite test cases that we already know, pick up @@ -124,7 +118,6 @@ class TestCaseRunner(Processor): return self.compare_and_create_test_cases( cli_test_cases_definitions=cli_test_cases, test_cases=test_cases, - test_suite_fqn=test_suite_fqn, table_fqn=table_fqn, ) @@ -141,7 +134,6 @@ class TestCaseRunner(Processor): cli_test_cases_definitions: List[TestCaseDefinition], test_cases: List[TestCase], table_fqn: str, - test_suite_fqn: str, ) -> List[TestCase]: """ compare test cases defined in CLI config workflow with test cases @@ -198,7 +190,6 @@ class TestCaseRunner(Processor): column_name=test_case_to_create.columnName, ) ), - testSuite=test_suite_fqn, parameterValues=( list(test_case_to_create.parameterValues) if test_case_to_create.parameterValues diff --git a/ingestion/src/metadata/great_expectations/action.py b/ingestion/src/metadata/great_expectations/action.py index 5ff71cef05c..56ebdd76123 100644 --- a/ingestion/src/metadata/great_expectations/action.py +++ b/ingestion/src/metadata/great_expectations/action.py @@ -153,9 +153,8 @@ class OpenMetadataValidationAction(ValidationAction): ) if table_entity: - test_suite = self._check_or_create_test_suite(table_entity) for result in validation_result_suite.results: - self._handle_test_case(result, table_entity, test_suite) + self._handle_test_case(result, table_entity) @staticmethod def _get_checkpoint_batch_spec( @@ -376,9 +375,7 @@ class OpenMetadataValidationAction(ValidationAction): return [test_result_value] - def _handle_test_case( - self, result: Dict, table_entity: Table, test_suite: TestSuite - ): + def _handle_test_case(self, result: Dict, table_entity: Table): """Handle adding test to table entity based on the test case. Test Definitions will be created on the fly from the results of the great expectations run. We will then write the test case results to the @@ -387,7 +384,6 @@ class OpenMetadataValidationAction(ValidationAction): Args: result: GE test result table_entity: table entity object - test_suite: test suite object """ try: @@ -417,7 +413,6 @@ class OpenMetadataValidationAction(ValidationAction): fqn=table_entity.fullyQualifiedName.root, column_name=fqn.split_test_case_fqn(test_case_fqn).column, ), - test_suite_fqn=test_suite.fullyQualifiedName.root, test_definition_fqn=test_definition.fullyQualifiedName.root, test_case_parameter_values=self._get_test_case_params_value(result), ) diff --git a/ingestion/src/metadata/great_expectations/action1xx.py b/ingestion/src/metadata/great_expectations/action1xx.py index de5bd7f526c..fb322dccdf6 100644 --- a/ingestion/src/metadata/great_expectations/action1xx.py +++ b/ingestion/src/metadata/great_expectations/action1xx.py @@ -119,9 +119,8 @@ class OpenMetadataValidationAction1xx(ValidationAction): ) if table_entity: - test_suite = self._check_or_create_test_suite(table_entity) for result in v.results: - self._handle_test_case(result, table_entity, test_suite) + self._handle_test_case(result, table_entity) @staticmethod def _get_checkpoint_batch_spec( @@ -334,9 +333,7 @@ class OpenMetadataValidationAction1xx(ValidationAction): return [test_result_value] - def _handle_test_case( - self, result: Dict, table_entity: Table, test_suite: TestSuite - ): + def _handle_test_case(self, result: Dict, table_entity: Table): """Handle adding test to table entity based on the test case. Test Definitions will be created on the fly from the results of the great expectations run. We will then write the test case results to the @@ -345,7 +342,6 @@ class OpenMetadataValidationAction1xx(ValidationAction): Args: result: GE test result table_entity: table entity object - test_suite: test suite object """ try: @@ -375,7 +371,6 @@ class OpenMetadataValidationAction1xx(ValidationAction): fqn=table_entity.fullyQualifiedName.root, column_name=fqn.split_test_case_fqn(test_case_fqn).column, ), - test_suite_fqn=test_suite.fullyQualifiedName.root, test_definition_fqn=test_definition.fullyQualifiedName.root, test_case_parameter_values=self._get_test_case_params_value(result), ) diff --git a/ingestion/src/metadata/ingestion/ometa/mixins/tests_mixin.py b/ingestion/src/metadata/ingestion/ometa/mixins/tests_mixin.py index 655d3b7f41e..414c07fa140 100644 --- a/ingestion/src/metadata/ingestion/ometa/mixins/tests_mixin.py +++ b/ingestion/src/metadata/ingestion/ometa/mixins/tests_mixin.py @@ -167,7 +167,6 @@ class OMetaTestsMixin: self, test_case_fqn: str, entity_link: Optional[str] = None, - test_suite_fqn: Optional[str] = None, test_definition_fqn: Optional[str] = None, test_case_parameter_values: Optional[List[TestCaseParameterValue]] = None, ): @@ -196,7 +195,6 @@ class OMetaTestsMixin: CreateTestCaseRequest( name=test_case_fqn.split(".")[-1], entityLink=entity_link, - testSuite=test_suite_fqn, testDefinition=test_definition_fqn, parameterValues=test_case_parameter_values, ) # type: ignore diff --git a/ingestion/src/metadata/ingestion/source/database/dbt/metadata.py b/ingestion/src/metadata/ingestion/source/database/dbt/metadata.py index 468790465d7..7a534fff250 100644 --- a/ingestion/src/metadata/ingestion/source/database/dbt/metadata.py +++ b/ingestion/src/metadata/ingestion/source/database/dbt/metadata.py @@ -83,7 +83,6 @@ from metadata.ingestion.source.database.dbt.dbt_service import ( ) from metadata.ingestion.source.database.dbt.dbt_utils import ( check_ephemeral_node, - check_or_create_test_suite, create_test_case_parameter_definitions, create_test_case_parameter_values, generate_entity_link, @@ -1024,9 +1023,6 @@ class DbtSource(DbtServiceSource): logger.debug(f"Processing DBT Test Case for node: {manifest_node.name}") entity_link_list = generate_entity_link(dbt_test) for entity_link_str in entity_link_list: - test_suite = check_or_create_test_suite( - self.metadata, entity_link_str - ) table_fqn = get_table_fqn(entity_link_str) logger.debug(f"Table fqn found: {table_fqn}") source_elements = table_fqn.split(fqn.FQN_SEPARATOR) @@ -1056,7 +1052,6 @@ class DbtSource(DbtServiceSource): manifest_node.name ), entityLink=entity_link_str, - testSuite=test_suite.fullyQualifiedName, parameterValues=create_test_case_parameter_values( dbt_test ), diff --git a/ingestion/src/metadata/ingestion/source/database/sample_data.py b/ingestion/src/metadata/ingestion/source/database/sample_data.py index 2ac57bf8b2b..79a6ee0c409 100644 --- a/ingestion/src/metadata/ingestion/source/database/sample_data.py +++ b/ingestion/src/metadata/ingestion/source/database/sample_data.py @@ -1603,7 +1603,6 @@ class SampleDataSource( description=test_case["description"], testDefinition=test_case["testDefinitionName"], entityLink=test_case["entityLink"], - testSuite=suite.fullyQualifiedName.root, parameterValues=[ TestCaseParameterValue(**param_values) for param_values in test_case["parameterValues"] diff --git a/ingestion/tests/integration/integration_base.py b/ingestion/tests/integration/integration_base.py index a9cbb03396e..5f81dc19c3e 100644 --- a/ingestion/tests/integration/integration_base.py +++ b/ingestion/tests/integration/integration_base.py @@ -377,7 +377,6 @@ def get_create_test_suite( def get_create_test_case( entity_link: str, - test_suite: FullyQualifiedEntityName, test_definition: FullyQualifiedEntityName, parameter_values: List[TestCaseParameterValue], name: Optional[EntityName] = None, @@ -387,7 +386,6 @@ def get_create_test_case( return CreateTestCaseRequest( name=TestCaseEntityName(name), entityLink=EntityLink(entity_link), - testSuite=test_suite, testDefinition=test_definition, parameterValues=parameter_values, ) diff --git a/ingestion/tests/integration/ometa/test_ometa_patch.py b/ingestion/tests/integration/ometa/test_ometa_patch.py index 98c1882b391..8a0e1263a99 100644 --- a/ingestion/tests/integration/ometa/test_ometa_patch.py +++ b/ingestion/tests/integration/ometa/test_ometa_patch.py @@ -168,7 +168,6 @@ class OMetaTableTest(TestCase): cls.test_case = cls.metadata.create_or_update( get_create_test_case( entity_link=f"<#E::table::{cls.table.fullyQualifiedName.root}>", - test_suite=cls.test_suite.fullyQualifiedName, test_definition=cls.test_definition.fullyQualifiedName, parameter_values=[TestCaseParameterValue(name="foo", value="10")], ) diff --git a/ingestion/tests/integration/ometa/test_ometa_test_suite.py b/ingestion/tests/integration/ometa/test_ometa_test_suite.py index ccdc2be68a4..135ada0136a 100644 --- a/ingestion/tests/integration/ometa/test_ometa_test_suite.py +++ b/ingestion/tests/integration/ometa/test_ometa_test_suite.py @@ -112,7 +112,6 @@ class OMetaTestSuiteTest(TestCase): entityLink=EntityLink( "<#E::table::sample_data.ecommerce_db.shopify.dim_address>" ), - testSuite=cls.test_suite.fullyQualifiedName, testDefinition=cls.test_definition.fullyQualifiedName, parameterValues=[TestCaseParameterValue(name="foo", value="10")], ) @@ -169,7 +168,6 @@ class OMetaTestSuiteTest(TestCase): test_case = self.metadata.get_or_create_test_case( test_case_fqn, - test_suite_fqn=self.test_suite.fullyQualifiedName.root, test_definition_fqn="columnValuesToMatchRegex", entity_link="<#E::table::sample_data.ecommerce_db.shopify.dim_address::columns::last_name>", test_case_parameter_values=[ diff --git a/ingestion/tests/integration/test_suite/test_workflow.py b/ingestion/tests/integration/test_suite/test_workflow.py index 7710d0025ce..20ed27caecd 100644 --- a/ingestion/tests/integration/test_suite/test_workflow.py +++ b/ingestion/tests/integration/test_suite/test_workflow.py @@ -142,7 +142,6 @@ class TestSuiteWorkflowTests(unittest.TestCase): CreateTestCaseRequest( name="testCaseForIntegration", entityLink=f"<#E::table::{cls.table_with_suite.fullyQualifiedName.root}>", - testSuite=cls.test_suite.fullyQualifiedName, testDefinition="tableRowCountToEqual", parameterValues=[TestCaseParameterValue(name="value", value="10")], ) @@ -266,7 +265,6 @@ class TestSuiteWorkflowTests(unittest.TestCase): test_cases: List[TestCase] = workflow.steps[0].get_test_cases( test_cases=table_and_tests.right.test_cases, - test_suite_fqn=self.table_with_suite.fullyQualifiedName.root + ".testSuite", table_fqn=self.table_with_suite.fullyQualifiedName.root, ) @@ -385,7 +383,6 @@ class TestSuiteWorkflowTests(unittest.TestCase): created_test_case = workflow.steps[0].compare_and_create_test_cases( cli_test_cases_definitions=config_test_cases_def, test_cases=table_and_tests.right.test_cases, - test_suite_fqn=f"{self.table_with_suite.fullyQualifiedName.root}.testSuite", table_fqn=self.table_with_suite.fullyQualifiedName.root, ) diff --git a/openmetadata-docs/content/v1.8.x-SNAPSHOT/sdk/python/api-reference/tests_mixin.md b/openmetadata-docs/content/v1.8.x-SNAPSHOT/sdk/python/api-reference/tests_mixin.md index a21fd26a799..48f5f0c94f6 100644 --- a/openmetadata-docs/content/v1.8.x-SNAPSHOT/sdk/python/api-reference/tests_mixin.md +++ b/openmetadata-docs/content/v1.8.x-SNAPSHOT/sdk/python/api-reference/tests_mixin.md @@ -153,7 +153,6 @@ Given an entity fqn, retrieve the link test suite if it exists or create a new o get_or_create_test_case( test_case_fqn: str, entity_link: Optional[str] = None, - test_suite_fqn: Optional[str] = None, test_definition_fqn: Optional[str] = None, test_case_parameter_values: Optional[List[TestCaseParameterValue]] = None ) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java index b2fd9cddffa..1bb1b9f4b6e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java @@ -6,6 +6,8 @@ import static org.openmetadata.schema.type.EventType.LOGICAL_TEST_CASE_ADDED; import static org.openmetadata.schema.type.Include.ALL; import static org.openmetadata.service.Entity.FIELD_OWNERS; import static org.openmetadata.service.Entity.FIELD_TAGS; +import static org.openmetadata.service.Entity.INGESTION_BOT_NAME; +import static org.openmetadata.service.Entity.TABLE; import static org.openmetadata.service.Entity.TEST_CASE; import static org.openmetadata.service.Entity.TEST_CASE_RESULT; import static org.openmetadata.service.Entity.TEST_DEFINITION; @@ -34,6 +36,7 @@ import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.EntityTimeSeriesInterface; import org.openmetadata.schema.api.feed.CloseTask; import org.openmetadata.schema.api.feed.ResolveTask; +import org.openmetadata.schema.api.tests.CreateTestSuite; import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.entity.teams.User; import org.openmetadata.schema.tests.TestCase; @@ -60,6 +63,7 @@ import org.openmetadata.schema.type.change.ChangeSource; import org.openmetadata.schema.utils.EntityInterfaceUtil; import org.openmetadata.service.Entity; import org.openmetadata.service.exception.EntityNotFoundException; +import org.openmetadata.service.resources.dqtests.TestSuiteMapper; import org.openmetadata.service.resources.feeds.MessageParser.EntityLink; import org.openmetadata.service.search.SearchListFilter; import org.openmetadata.service.util.EntityUtil; @@ -99,7 +103,10 @@ public class TestCaseRepository extends EntityRepository { public void setFields(TestCase test, Fields fields) { test.setTestSuites( fields.contains(Entity.FIELD_TEST_SUITES) ? getTestSuites(test) : test.getTestSuites()); - test.setTestSuite(fields.contains(TEST_SUITE_FIELD) ? getTestSuite(test) : test.getTestSuite()); + test.setTestSuite( + fields.contains(TEST_SUITE_FIELD) + ? getTestSuite(test.getId(), entityType, TEST_SUITE, Direction.FROM) + : test.getTestSuite()); test.setTestDefinition( fields.contains(TEST_DEFINITION) ? getTestDefinition(test) : test.getTestDefinition()); test.setTestCaseResult( @@ -135,7 +142,13 @@ public class TestCaseRepository extends EntityRepository { @Override public EntityInterface getParentEntity(TestCase entity, String fields) { - return Entity.getEntity(entity.getTestSuite(), fields, ALL); + EntityReference testSuite = entity.getTestSuite(); + if (testSuite == null) { + EntityLink entityLink = EntityLink.parse(entity.getEntityLink()); + return Entity.getEntity(entityLink, fields, ALL); + } else { + return Entity.getEntity(testSuite, fields, ALL); + } } @Override @@ -161,10 +174,11 @@ public class TestCaseRepository extends EntityRepository { EntityLink entityLink = EntityLink.parse(test.getEntityLink()); EntityUtil.validateEntityLink(entityLink); - // validate test definition and test suite - TestSuite testSuite = Entity.getEntity(test.getTestSuite(), "", Include.NON_DELETED); - test.setTestSuite(testSuite.getEntityReference()); + // Get existing basic test suite or create a new one if it doesn't exist + EntityReference testSuite = getOrCreateTestSuite(test); + test.setTestSuite(testSuite); + // validate test definition TestDefinition testDefinition = Entity.getEntity(test.getTestDefinition(), "", Include.NON_DELETED); test.setTestDefinition(testDefinition.getEntityReference()); @@ -172,10 +186,42 @@ public class TestCaseRepository extends EntityRepository { validateTestParameters(test.getParameterValues(), testDefinition.getParameterDefinition()); } - private EntityReference getTestSuite(TestCase test) throws EntityNotFoundException { + /* + * Get the test suite for a test case. We'll use the entity linked to the test case + * to find the basic test suite. If it doesn't exist, create a new one. + */ + private EntityReference getOrCreateTestSuite(TestCase test) { + EntityReference entityReference = null; + try { + EntityLink entityLink = EntityLink.parse(test.getEntityLink()); + EntityInterface entity = Entity.getEntity(entityLink, "", ALL); + return getTestSuite(entity.getId(), TEST_SUITE, TABLE, Direction.TO); + } catch (EntityNotFoundException e) { + // If the test suite is not found, we'll create a new one + EntityLink entityLink = EntityLink.parse(test.getEntityLink()); + TestSuiteRepository testSuiteRepository = + (TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE); + TestSuiteMapper mapper = new TestSuiteMapper(); + CreateTestSuite createTestSuite = + new CreateTestSuite() + .withName(entityLink.getEntityFQN() + ".testSuite") + .withBasicEntityReference(entityLink.getEntityFQN()); + TestSuite testSuite = mapper.createToEntity(createTestSuite, INGESTION_BOT_NAME); + testSuite.setBasic(true); + testSuiteRepository.create(null, testSuite); + entityReference = testSuite.getEntityReference(); + } + return entityReference; + } + + private EntityReference getTestSuite(UUID id, String to, String from, Direction direction) + throws EntityNotFoundException { // `testSuite` field returns the executable `testSuite` linked to that testCase - List records = - findFromRecords(test.getId(), entityType, Relationship.CONTAINS, TEST_SUITE); + List records = new ArrayList<>(); + switch (direction) { + case FROM -> records = findFromRecords(id, to, Relationship.CONTAINS, from); + case TO -> records = findToRecords(id, from, Relationship.CONTAINS, to); + } for (CollectionDAO.EntityRelationshipRecord testSuiteId : records) { TestSuite testSuite = Entity.getEntity(TEST_SUITE, testSuiteId.getId(), "", Include.ALL); if (Boolean.TRUE.equals(testSuite.getBasic())) { @@ -185,7 +231,7 @@ public class TestCaseRepository extends EntityRepository { throw new EntityNotFoundException( String.format( "Error occurred when retrieving executable test suite for testCase %s. ", - test.getName()) + id.toString()) + "No executable test suite was found."); } @@ -431,6 +477,11 @@ public class TestCaseRepository extends EntityRepository { } public void isTestSuiteBasic(String testSuiteFqn) { + if (testSuiteFqn == null) { + // If the test suite FQN is not provided, we'll assume it's a basic test suite + return; + } + TestSuite testSuite = Entity.getEntityByName(Entity.TEST_SUITE, testSuiteFqn, null, null); if (Boolean.FALSE.equals(testSuite.getBasic())) { throw new IllegalArgumentException( @@ -740,8 +791,7 @@ public class TestCaseRepository extends EntityRepository { } // Set the column tags. Will be used to mask the sample data if (!authorizePII) { - populateEntityFieldTags( - Entity.TABLE, table.getColumns(), table.getFullyQualifiedName(), true); + populateEntityFieldTags(TABLE, table.getColumns(), table.getFullyQualifiedName(), true); List tags = daoCollection.tagUsageDAO().getTags(table.getFullyQualifiedName()); table.setTags(tags); return maskSampleData(sampleData, table, table.getColumns()); @@ -831,4 +881,9 @@ public class TestCaseRepository extends EntityRepository { } } } + + private enum Direction { + TO, + FROM + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseMapper.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseMapper.java index 71ae804bbf4..ef011484bb1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseMapper.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseMapper.java @@ -21,8 +21,8 @@ public class TestCaseMapper implements EntityMapper { .withComputePassedFailedRowCount(create.getComputePassedFailedRowCount()) .withUseDynamicAssertion(create.getUseDynamicAssertion()) .withEntityFQN(entityLink.getFullyQualifiedFieldValue()) - .withTestSuite(getEntityReference(Entity.TEST_SUITE, create.getTestSuite())) .withTestDefinition(getEntityReference(Entity.TEST_DEFINITION, create.getTestDefinition())) + .withTags(create.getTags()) .withCreatedBy(user); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java index 2cefe27b897..56519e7debd 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java @@ -687,7 +687,6 @@ public class TestCaseResource extends EntityResource testCases = new ArrayList<>(); Set entityLinks = createTestCases.stream().map(CreateTestCase::getEntityLink).collect(Collectors.toSet()); - Set testSuites = - createTestCases.stream().map(CreateTestCase::getTestSuite).collect(Collectors.toSet()); OperationContext operationContext = new OperationContext(entityType, MetadataOperation.CREATE); @@ -731,7 +728,6 @@ public class TestCaseResource extends EntityResource response = repository.createOrUpdate(uriInfo, test, securityContext.getUserPrincipal().getName()); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java index cc0f836f3d3..53b0e78327c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java @@ -550,41 +550,6 @@ public class TestSuiteResource extends EntityResource authRequests = getAuthRequestsForPost(testSuite); - return create(uriInfo, securityContext, authRequests, AuthorizationLogic.ANY, testSuite); - } - @POST @Path("/basic") @Operation( diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java index 8e9f9ef59ce..5c259fe1ba1 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java @@ -2396,7 +2396,6 @@ public class TableResourceTest extends EntityResourceTest { testCaseResourceTest .createRequest(test) .withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName())) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION2.getFullyQualifiedName()); TestCase testCase = testCaseResourceTest.assertOwnerInheritance(createTestCase, USER1_REF); @@ -2451,7 +2450,6 @@ public class TableResourceTest extends EntityResourceTest { testCaseResourceTest .createRequest(test) .withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName())) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION2.getFullyQualifiedName()); TestCase testCase = testCaseResourceTest.createEntity(createTestCase, ADMIN_AUTH_HEADERS); @@ -2504,7 +2502,6 @@ public class TableResourceTest extends EntityResourceTest { testCaseResourceTest .createRequest(test) .withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName())) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION2.getFullyQualifiedName()); TestCase testCase = testCaseResourceTest.assertDomainInheritance(createTestCase, DOMAIN.getEntityReference()); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java index a39bf17a07f..85e0a5efd6f 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java @@ -121,6 +121,7 @@ import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.TableData; import org.openmetadata.schema.type.TagLabel; import org.openmetadata.schema.type.TaskStatus; +import org.openmetadata.schema.type.TestDefinitionEntityType; import org.openmetadata.service.Entity; import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest; @@ -474,35 +475,21 @@ public class TestCaseResourceTest extends EntityResourceTest createAndCheckEntity(create, ADMIN_AUTH_HEADERS), BAD_REQUEST, ENTITY_LINK_MATCH_ERROR); - create.withEntityLink(INVALID_LINK2).withTestSuite(testSuite.getFullyQualifiedName()); + create.withEntityLink(INVALID_LINK2); assertResponseContains( () -> createAndCheckEntity(create, ADMIN_AUTH_HEADERS), NOT_FOUND, "table instance for non-existent not found"); - CreateTestCase create1 = createRequest(test); - create1.withTestSuite(TEST_DEFINITION1.getFullyQualifiedName()); - assertResponseContains( - () -> createAndCheckEntity(create1, ADMIN_AUTH_HEADERS), - NOT_FOUND, - "testSuite instance for " + TEST_DEFINITION1.getFullyQualifiedName() + " not found"); - CreateTestCase create2 = createRequest(test); - create2 - .withEntityLink(TABLE_LINK) - .withTestSuite(testSuite.getFullyQualifiedName()) - .withTestDefinition(TEST_SUITE1.getFullyQualifiedName()); + create2.withEntityLink(TABLE_LINK).withTestDefinition(TEST_SUITE1.getFullyQualifiedName()); assertResponseContains( () -> createAndCheckEntity(create2, ADMIN_AUTH_HEADERS), NOT_FOUND, @@ -514,7 +501,6 @@ public class TestCaseResourceTest extends EntityResourceTest createAndCheckEntity(create1, ADMIN_AUTH_HEADERS), BAD_REQUEST, @@ -539,7 +522,6 @@ public class TestCaseResourceTest extends EntityResourceTest", tableFQN)) - .withTestSuite(testSuiteFQN) .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) .withParameterValues( List.of( @@ -924,7 +899,6 @@ public class TestCaseResourceTest extends EntityResourceTest", tableFQN)) - .withTestSuite(testSuiteFQN) .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) .withParameterValues( List.of( @@ -1157,7 +1131,6 @@ public class TestCaseResourceTest extends EntityResourceTest", table.getFullyQualifiedName())) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION2.getFullyQualifiedName()); createEntity(create, ADMIN_AUTH_HEADERS); create = @@ -1165,7 +1138,6 @@ public class TestCaseResourceTest extends EntityResourceTest", table.getFullyQualifiedName(), columnName)) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) .withParameterValues( List.of( @@ -1295,16 +1267,12 @@ public class TestCaseResourceTest extends EntityResourceTest testCases = new ArrayList<>(); // Create the test cases (need to be created against an executable test suite) for (int i = 0; i < 5; i++) { - CreateTestCase create = - createRequest("test_testSuite__" + i) - .withTestSuite(executableTestSuite.getFullyQualifiedName()); + CreateTestCase create = createRequest("test_testSuite__" + i); TestCase testCase = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); testCases.add(testCase); } @@ -1328,10 +1296,15 @@ public class TestCaseResourceTest extends EntityResourceTest executableTestSuiteTestCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(testCases.size(), executableTestSuiteTestCases.getData().size()); + Integer initialSize = executableTestSuiteTestCases.getData().size(); + assertTrue( + executableTestSuiteTestCases.getData().stream() + .allMatch(t -> t.getTestSuite().getId().toString().equals(testSuiteID))); // Soft Delete a test case from the executable test suite and check that it is deleted from the // executable test suite and from the logical test suite @@ -1347,18 +1320,30 @@ public class TestCaseResourceTest extends EntityResourceTest { + List testSuites = t.getTestSuites(); + return testSuites.stream() + .anyMatch( + ts -> ts.getId().toString().equals(logicalTestSuite.getId().toString())); + })); assertTrue(assertTestCaseIdNotInList(logicalTestSuiteTestCases, executableTestCaseIdToDelete)); - queryParams.put("testSuiteId", executableTestSuite.getId().toString()); + queryParams.put("testSuiteId", testSuiteID); executableTestSuiteTestCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(4, executableTestSuiteTestCases.getData().size()); + assertTrue( + executableTestSuiteTestCases.getData().stream() + .allMatch( + t -> { + List testSuites = t.getTestSuites(); + return testSuites.stream() + .anyMatch(ts -> ts.getId().toString().equals(testSuiteID)); + })); assertTrue( assertTestCaseIdNotInList(executableTestSuiteTestCases, executableTestCaseIdToDelete)); } @@ -1383,16 +1383,9 @@ public class TestCaseResourceTest extends EntityResourceTest testCaseIds = listOf(testCase.getId()); @@ -1402,14 +1395,15 @@ public class TestCaseResourceTest extends EntityResourceTest testSuiteFQNs = new HashMap<>(); - testSuiteFQNs.put(logicalTestSuite.getFullyQualifiedName(), logicalTestSuite); - testSuiteFQNs.put(executableTestSuite.getFullyQualifiedName(), executableTestSuite); + Map testSuiteFQNs = new HashMap<>(); + testSuiteFQNs.put( + logicalTestSuite.getFullyQualifiedName(), logicalTestSuite.getEntityReference()); + testSuiteFQNs.put(testCase.getTestSuite().getFullyQualifiedName(), testCase.getTestSuite()); for (TestSuite testSuite : testCaseWithSuites.getTestSuites()) { assertNotNull(testSuiteFQNs.get(testSuite.getFullyQualifiedName())); @@ -1903,7 +1897,6 @@ public class TestCaseResourceTest extends EntityResourceTest queryParams = new HashMap<>(); queryParams.put("limit", 100); - queryParams.put("testSuiteId", testSuite.getId().toString()); + queryParams.put("testSuiteId", testSuiteId); + queryParams.put("fields", "testSuite"); // Assert we get all 15 test cases ResultList testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(testCaseEntries, testCases.getData().size()); + assertTrue( + testCases.getData().stream() + .allMatch(t -> t.getTestSuite().getId().toString().equals(testSuiteId))); // Assert we get 8 failed test cases queryParams.put("testCaseStatus", TestCaseStatus.Failed); testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(8, testCases.getData().size()); + assertTrue( + testCases.getData().stream() + .allMatch(t -> t.getTestCaseStatus().equals(TestCaseStatus.Failed))); // Assert we get 7 success test cases queryParams.put("testCaseStatus", TestCaseStatus.Success); testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(6, testCases.getData().size()); + assertTrue( + testCases.getData().stream() + .allMatch(t -> t.getTestCaseStatus().equals(TestCaseStatus.Success))); // Assert we get 1 aborted test cases queryParams.put("testCaseStatus", TestCaseStatus.Aborted); testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(1, testCases.getData().size()); + assertTrue( + testCases.getData().stream() + .allMatch(t -> t.getTestCaseStatus().equals(TestCaseStatus.Aborted))); queryParams.remove("testCaseStatus"); // Assert we get 7 column level test cases queryParams.put("testCaseType", "column"); + queryParams.put("fields", "testDefinition"); testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(8, testCases.getData().size()); + assertTrue( + testCases.getData().stream() + .allMatch( + t -> { + TestDefinition testDefinition = + Entity.getEntity(t.getTestDefinition(), "", Include.ALL); + return testDefinition.getEntityType().equals(TestDefinitionEntityType.COLUMN); + })); // Assert we get 8 table level test cases queryParams.put("testCaseType", "table"); + queryParams.put("fields", "testDefinition"); testCases = getTestCases(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(7, testCases.getData().size()); + testCases.getData().stream() + .filter( + t -> { + TestDefinition testDefinition = + Entity.getEntity(t.getTestDefinition(), "", Include.ALL); + return testDefinition.getEntityType().equals(TestDefinitionEntityType.TABLE); + }); + assertTrue( + testCases.getData().stream() + .allMatch( + t -> { + MessageParser.EntityLink entityLink = + MessageParser.EntityLink.parse(t.getEntityLink()); + return entityLink.getFieldName() == null; // should be empty for table test cases + })); } @Test @@ -2053,7 +2074,6 @@ public class TestCaseResourceTest extends EntityResourceTest", testSuite.getBasicEntityReference().getFullyQualifiedName())) - .withTestSuite(testSuite.getFullyQualifiedName()) .withTestDefinition(TEST_DEFINITION1.getFullyQualifiedName()); TestCase testCase = createAndCheckEntity(createTestCase, ADMIN_AUTH_HEADERS); UUID testSuiteId = testCase.getTestSuite().getId(); @@ -2555,9 +2569,7 @@ public class TestCaseResourceTest extends EntityResourceTest Allowed TestCase testCase1 = createEntity(createReq, authHeaders("user-table-edit-tests")); @@ -2875,8 +2885,7 @@ public class TestCaseResourceTest extends EntityResourceTest createEntity(createReq, authHeaders("user-table-owner")), @@ -2963,8 +2970,7 @@ public class TestCaseResourceTest extends EntityResourceTest authHeaders) { validateCommonEntityFields(createdEntity, request, getPrincipalName(authHeaders)); assertEquals(request.getEntityLink(), createdEntity.getEntityLink()); - assertReference(request.getTestSuite(), createdEntity.getTestSuite()); assertReference(request.getTestDefinition(), createdEntity.getTestDefinition()); assertEquals(request.getParameterValues(), createdEntity.getParameterValues()); } @@ -3583,7 +3594,6 @@ public class TestCaseResourceTest extends EntityResourceTest { { name: 'allowedValues', value: '["gmail","yahoo","collate"]' }, ], testDefinition: 'columnValuesToBeInSet', - testSuite: testSuiteData?.['fullyQualifiedName'], }); await afterAction(); }); @@ -724,7 +723,6 @@ test('TestCase filters', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => { const testCase2 = await filterTable1.createTestCase(apiContext, { name: smilerNameTestCase[i], entityLink: `<#E::table::${filterTable2Response?.['fullyQualifiedName']}>`, - testSuite: testSuite2Response?.['fullyQualifiedName'], }); await filterTable1.addTestCaseResult( apiContext, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index c993a5adeeb..e4af79d85a3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -324,7 +324,6 @@ export class TableClass extends EntityClass { name: `pw-test-case-${uuid()}`, entityLink: `<#E::table::${this.entityResponseData?.['fullyQualifiedName']}>`, testDefinition: 'tableRowCountToBeBetween', - testSuite: this.testSuiteResponseData?.['fullyQualifiedName'], parameterValues: [ { name: 'minValue', value: 12 }, { name: 'maxValue', value: 34 }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTestV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTestV1.tsx index a9d5ae0629d..f9dd1e97044 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTestV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/AddDataQualityTestV1.tsx @@ -36,16 +36,11 @@ import { FormSubmitType } from '../../../enums/form.enum'; import { ProfilerDashboardType } from '../../../enums/table.enum'; import { OwnerType } from '../../../enums/user.enum'; import { CreateTestCase } from '../../../generated/api/tests/createTestCase'; -import { CreateTestSuite } from '../../../generated/api/tests/createTestSuite'; import { TestCase } from '../../../generated/tests/testCase'; import { TestSuite } from '../../../generated/tests/testSuite'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; -import { - createExecutableTestSuite, - createTestCase, - getTestSuiteByName, -} from '../../../rest/testAPI'; +import { createTestCase, getTestSuiteByName } from '../../../rest/testAPI'; import { getEntityBreadcrumbs, getEntityName, @@ -127,18 +122,6 @@ const AddDataQualityTestV1: React.FC = ({ }); }; - const createTestSuite = async () => { - const testSuite: CreateTestSuite = { - name: `${table.fullyQualifiedName}.testSuite`, - basicEntityReference: table.fullyQualifiedName, - owners, - }; - const response = await createExecutableTestSuite(testSuite); - setTestSuiteData(response); - - return response; - }; - const fetchTestSuiteByFqn = async (fqn: string) => { try { const response = await getTestSuiteByName(fqn); @@ -158,17 +141,21 @@ const AddDataQualityTestV1: React.FC = ({ setTestCaseData(data); try { - const testSuite = isUndefined(testSuiteData) - ? await createTestSuite() - : table.testSuite; - const testCasePayload: CreateTestCase = { ...data, owners, - testSuite: testSuite?.fullyQualifiedName ?? '', }; const testCaseResponse = await createTestCase(testCasePayload); + if ( + testCaseResponse.testSuite.fullyQualifiedName && + isUndefined(table.testSuite) + ) { + await fetchTestSuiteByFqn( + testCaseResponse.testSuite.fullyQualifiedName + ); + } + // Update current count when Create / Delete operation performed await getResourceLimit('dataQuality', true, true); setActiveServiceStep(2); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.test.tsx index 885a94c4a8c..8931684a43f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.test.tsx @@ -184,7 +184,6 @@ describe('TestCaseForm', () => { name: 'dim_address_column_value_lengths_to_be_between_4B3B', parameterValues: [], testDefinition: 'columnValueLengthsToBeBetween', - testSuite: '', }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.tsx index 2d17c377f7a..421738a0701 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseForm.tsx @@ -143,14 +143,14 @@ const TestCaseForm: React.FC = ({ }; const getSelectedTestDefinition = useCallback(() => { - const testType = isEmpty(initialValue?.testSuite) + const testType = isEmpty(initialValue?.testDefinition) ? selectedTestType - : initialValue?.testSuite; + : initialValue?.testDefinition; return testDefinitions.find( (definition) => definition.fullyQualifiedName === testType ); - }, [initialValue?.testSuite, selectedTestType, testDefinitions]); + }, [initialValue?.testDefinition, selectedTestType, testDefinitions]); const isComputeRowCountFieldVisible = useMemo(() => { const selectedDefinition = getSelectedTestDefinition(); @@ -188,7 +188,6 @@ const TestCaseForm: React.FC = ({ ), testDefinition: value.testTypeId, description: isEmpty(value.description) ? undefined : value.description, - testSuite: '', ...testCaseClassBase.getCreateTestCaseObject(value, selectedDefinition), }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCase.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCase.ts index 7a394f0a295..a972af053d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/tests/createTestCase.ts @@ -36,14 +36,15 @@ export interface CreateTestCase { */ owners?: EntityReference[]; parameterValues?: TestCaseParameterValue[]; + /** + * Tags for this test case. This is an inherited field from the parent entity and is not set + * directly on the test case. + */ + tags?: TagLabel[]; /** * Fully qualified name of the test definition. */ testDefinition: string; - /** - * Fully qualified name of the testSuite - */ - testSuite: string; /** * If the test definition supports it, use dynamic assertion to evaluate the test case. */ @@ -121,3 +122,91 @@ export interface TestCaseParameterValue { value?: string; [property: string]: any; } + +/** + * This schema defines the type for labeling an entity with a Tag. + */ +export interface TagLabel { + /** + * Description for the tag label. + */ + description?: string; + /** + * Display Name that identifies this tag. + */ + displayName?: string; + /** + * Link to the tag resource. + */ + href?: string; + /** + * Label type describes how a tag label was applied. 'Manual' indicates the tag label was + * applied by a person. 'Derived' indicates a tag label was derived using the associated tag + * relationship (see Classification.json for more details). 'Propagated` indicates a tag + * label was propagated from upstream based on lineage. 'Automated' is used when a tool was + * used to determine the tag label. + */ + labelType: LabelType; + /** + * Name of the tag or glossary term. + */ + name?: string; + /** + * Label is from Tags or Glossary. + */ + source: TagSource; + /** + * 'Suggested' state is used when a tag label is suggested by users or tools. Owner of the + * entity must confirm the suggested labels before it is marked as 'Confirmed'. + */ + state: State; + style?: Style; + tagFQN: string; +} + +/** + * Label type describes how a tag label was applied. 'Manual' indicates the tag label was + * applied by a person. 'Derived' indicates a tag label was derived using the associated tag + * relationship (see Classification.json for more details). 'Propagated` indicates a tag + * label was propagated from upstream based on lineage. 'Automated' is used when a tool was + * used to determine the tag label. + */ +export enum LabelType { + Automated = "Automated", + Derived = "Derived", + Generated = "Generated", + Manual = "Manual", + Propagated = "Propagated", +} + +/** + * Label is from Tags or Glossary. + */ +export enum TagSource { + Classification = "Classification", + Glossary = "Glossary", +} + +/** + * 'Suggested' state is used when a tag label is suggested by users or tools. Owner of the + * entity must confirm the suggested labels before it is marked as 'Confirmed'. + */ +export enum State { + Confirmed = "Confirmed", + Suggested = "Suggested", +} + +/** + * UI Style is used to associate a color code and/or icon to entity to customize the look of + * that entity in UI. + */ +export interface Style { + /** + * Hex Color Code to mark an entity such as GlossaryTerm, Tag, Domain or Data Product. + */ + color?: string; + /** + * An icon to associate with GlossaryTerm, Tag, Domain or Data Product. + */ + iconURL?: string; +}