MINOR - Create Test Case Resolution ts entry & delete resolution when… (#14541)

* MINOR - Create Test Case Resolution ts entry & delete resolution when Test Case is deleted
This commit is contained in:
Pere Miquel Brull 2024-01-05 09:15:49 +01:00 committed by GitHub
parent a07ef89cbb
commit 0255171218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 471 additions and 305 deletions

View File

@ -19,10 +19,6 @@
], ],
"resolutions": { "resolutions": {
"sequenceOne": [ "sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{ {
"testCaseResolutionStatusType": "Ack", "testCaseResolutionStatusType": "Ack",
"severity": "Severity1" "severity": "Severity1"
@ -108,10 +104,6 @@
], ],
"resolutions": { "resolutions": {
"sequenceOne": [ "sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{ {
"testCaseResolutionStatusType": "Ack", "testCaseResolutionStatusType": "Ack",
"severity": "Severity1" "severity": "Severity1"
@ -186,10 +178,6 @@
], ],
"resolutions": { "resolutions": {
"sequenceOne": [ "sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{ {
"testCaseResolutionStatusType": "Ack", "testCaseResolutionStatusType": "Ack",
"severity": "Severity1" "severity": "Severity1"
@ -264,10 +252,6 @@
], ],
"resolutions": { "resolutions": {
"sequenceOne": [ "sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{ {
"testCaseResolutionStatusType": "Ack", "testCaseResolutionStatusType": "Ack",
"severity": "Severity1" "severity": "Severity1"

View File

@ -17,6 +17,9 @@ from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from metadata.generated.schema.api.tests.createTestCase import CreateTestCaseRequest from metadata.generated.schema.api.tests.createTestCase import CreateTestCaseRequest
from metadata.generated.schema.api.tests.createTestCaseResolutionStatus import (
CreateTestCaseResolutionStatus,
)
from metadata.generated.schema.api.tests.createTestSuite import CreateTestSuiteRequest from metadata.generated.schema.api.tests.createTestSuite import CreateTestSuiteRequest
from metadata.generated.schema.tests.basic import TestCaseResult from metadata.generated.schema.tests.basic import TestCaseResult
from metadata.generated.schema.tests.testCase import TestCase from metadata.generated.schema.tests.testCase import TestCase
@ -38,3 +41,9 @@ class OMetaTestCaseSample(BaseModel):
class OMetaTestCaseResultsSample(BaseModel): class OMetaTestCaseResultsSample(BaseModel):
test_case_results: TestCaseResult test_case_results: TestCaseResult
test_case_name: str test_case_name: str
class OMetaTestCaseResolutionStatus(BaseModel):
"""For sample data"""
test_case_resolution: CreateTestCaseResolutionStatus

View File

@ -47,6 +47,9 @@ from metadata.generated.schema.entity.teams.team import Team
from metadata.generated.schema.entity.teams.user import User from metadata.generated.schema.entity.teams.user import User
from metadata.generated.schema.tests.basic import TestCaseResult from metadata.generated.schema.tests.basic import TestCaseResult
from metadata.generated.schema.tests.testCase import TestCase from metadata.generated.schema.tests.testCase import TestCase
from metadata.generated.schema.tests.testCaseResolutionStatus import (
TestCaseResolutionStatus,
)
from metadata.generated.schema.tests.testSuite import TestSuite from metadata.generated.schema.tests.testSuite import TestSuite
from metadata.generated.schema.type.schema import Topic from metadata.generated.schema.type.schema import Topic
from metadata.ingestion.api.models import Either, Entity, StackTraceError from metadata.ingestion.api.models import Either, Entity, StackTraceError
@ -67,6 +70,7 @@ from metadata.ingestion.models.profile_data import OMetaTableProfileSampleData
from metadata.ingestion.models.search_index_data import OMetaIndexSampleData from metadata.ingestion.models.search_index_data import OMetaIndexSampleData
from metadata.ingestion.models.tests_data import ( from metadata.ingestion.models.tests_data import (
OMetaLogicalTestSuiteSample, OMetaLogicalTestSuiteSample,
OMetaTestCaseResolutionStatus,
OMetaTestCaseResultsSample, OMetaTestCaseResultsSample,
OMetaTestCaseSample, OMetaTestCaseSample,
OMetaTestSuiteSample, OMetaTestSuiteSample,
@ -411,6 +415,15 @@ class MetadataRestSink(Sink): # pylint: disable=too-many-public-methods
) )
return Either(right=res) return Either(right=res)
@_run_dispatch.register
def write_test_case_resolution_status(
self, record: OMetaTestCaseResolutionStatus
) -> TestCaseResolutionStatus:
"""For sample data"""
res = self.metadata.create_test_case_resolution(record.test_case_resolution)
return Either(right=res)
@_run_dispatch.register @_run_dispatch.register
def write_data_insight_sample( def write_data_insight_sample(
self, record: OMetaDataInsightSample self, record: OMetaDataInsightSample

View File

@ -118,6 +118,7 @@ from metadata.ingestion.models.pipeline_status import OMetaPipelineStatus
from metadata.ingestion.models.profile_data import OMetaTableProfileSampleData from metadata.ingestion.models.profile_data import OMetaTableProfileSampleData
from metadata.ingestion.models.tests_data import ( from metadata.ingestion.models.tests_data import (
OMetaLogicalTestSuiteSample, OMetaLogicalTestSuiteSample,
OMetaTestCaseResolutionStatus,
OMetaTestCaseResultsSample, OMetaTestCaseResultsSample,
OMetaTestCaseSample, OMetaTestCaseSample,
OMetaTestSuiteSample, OMetaTestSuiteSample,
@ -566,6 +567,7 @@ class SampleDataSource(
yield from self.ingest_test_suite() yield from self.ingest_test_suite()
yield from self.ingest_test_case() yield from self.ingest_test_case()
yield from self.ingest_test_case_results() yield from self.ingest_test_case_results()
yield from self.ingest_incidents()
yield from self.ingest_logical_test_suite() yield from self.ingest_logical_test_suite()
yield from self.ingest_data_insights() yield from self.ingest_data_insights()
yield from self.ingest_life_cycle() yield from self.ingest_life_cycle()
@ -1408,6 +1410,15 @@ class SampleDataSource(
) )
yield Either(right=test_case_req) yield Either(right=test_case_req)
def ingest_incidents(self) -> Iterable[Either[OMetaTestCaseResolutionStatus]]:
"""
Ingest incidents after the first test failures have been added.
The test failure already creates the incident with NEW, so we
start always from ACK in the sample flows.
"""
for test_suite in self.tests_suites["tests"]:
for test_case in test_suite["testCases"]:
test_case_fqn = f"{entity_link.get_table_or_column_fqn(test_case['entityLink'])}.{test_case['name']}" test_case_fqn = f"{entity_link.get_table_or_column_fqn(test_case['entityLink'])}.{test_case['name']}"
for _, resolutions in test_case["resolutions"].items(): for _, resolutions in test_case["resolutions"].items():
@ -1449,8 +1460,10 @@ class SampleDataSource(
testCaseFailureComment="Resolution comment", testCaseFailureComment="Resolution comment",
) )
self.metadata.create_test_case_resolution( yield Either(
create_test_case_resolution right=OMetaTestCaseResolutionStatus(
test_case_resolution=create_test_case_resolution
)
) )
def ingest_test_case_results(self) -> Iterable[Either[OMetaTestCaseResultsSample]]: def ingest_test_case_results(self) -> Iterable[Either[OMetaTestCaseResultsSample]]:

View File

@ -0,0 +1,23 @@
package org.openmetadata.service.exception;
import javax.ws.rs.core.Response;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.sdk.exception.WebServiceException;
public class IncidentManagerException extends WebServiceException {
protected IncidentManagerException(Response.Status status, String message) {
super(status.getStatusCode(), message);
}
public IncidentManagerException(String message) {
super(Response.Status.INTERNAL_SERVER_ERROR, message);
}
public static IncidentManagerException invalidStatus(
TestCaseResolutionStatusTypes lastStatus, TestCaseResolutionStatusTypes newStatus) {
return new IncidentManagerException(
Response.Status.BAD_REQUEST,
String.format("Incident with status [%s] cannot be moved to [%s]", lastStatus, newStatus));
}
}

View File

@ -3592,6 +3592,32 @@ public interface CollectionDAO {
default String getTimeSeriesTableName() { default String getTimeSeriesTableName() {
return "data_quality_data_time_series"; return "data_quality_data_time_series";
} }
@ConnectionAwareSqlQuery(
value =
"SELECT json FROM data_quality_data_time_series where entityFQNHash = :testCaseFQNHash "
+ "AND JSON_EXTRACT(json, '$.incidentId') IS NOT NULL",
connectionType = MYSQL)
@ConnectionAwareSqlQuery(
value =
"SELECT json FROM data_quality_data_time_series where entityFQNHash = :testCaseFQNHash "
+ "AND json ->> 'incidentId' IS NOT NULL",
connectionType = POSTGRES)
List<String> getResultsWithIncidents(@Bind("testCaseFQNHash") String testCaseFQNHash);
@ConnectionAwareSqlUpdate(
value =
"SELECT json FROM data_quality_data_time_series where entityFQNHash = :entityFQNHash "
+ "AND JSON_EXTRACT(json, '$.incidentId') IS NOT NULL",
connectionType = MYSQL)
@ConnectionAwareSqlUpdate(
value =
"SELECT json FROM data_quality_data_time_series where entityFQNHash = :entityFQNHash "
+ "AND json ->> 'incidentId' IS NOT NULL",
connectionType = POSTGRES)
// TODO: need to find the right way to get this cleaned
void cleanTestCaseIncidents(
@Bind("entityFQNHash") String entityFQNHash, @Bind("stateId") String stateId);
} }
interface TestCaseResolutionStatusTimeSeriesDAO extends EntityTimeSeriesDAO { interface TestCaseResolutionStatusTimeSeriesDAO extends EntityTimeSeriesDAO {
@ -3605,6 +3631,10 @@ public interface CollectionDAO {
"SELECT json FROM test_case_resolution_status_time_series " "SELECT json FROM test_case_resolution_status_time_series "
+ "WHERE stateId = :stateId ORDER BY timestamp DESC") + "WHERE stateId = :stateId ORDER BY timestamp DESC")
List<String> listTestCaseResolutionStatusesForStateId(@Bind("stateId") String stateId); List<String> listTestCaseResolutionStatusesForStateId(@Bind("stateId") String stateId);
@SqlUpdate(
"DELETE FROM test_case_resolution_status_time_series WHERE entityFQNHash = :entityFQNHash")
void delete(@BindFQN("entityFQNHash") String entityFQNHash);
} }
class EntitiesCountRowMapper implements RowMapper<EntitiesCount> { class EntitiesCountRowMapper implements RowMapper<EntitiesCount> {

View File

@ -19,6 +19,7 @@ public abstract class EntityTimeSeriesRepository<T extends EntityTimeSeriesInter
@Getter protected final SearchRepository searchRepository; @Getter protected final SearchRepository searchRepository;
@Getter protected final String entityType; @Getter protected final String entityType;
@Getter protected final Class<T> entityClass; @Getter protected final Class<T> entityClass;
@Getter protected final CollectionDAO daoCollection;
protected EntityTimeSeriesRepository( protected EntityTimeSeriesRepository(
String collectionPath, String collectionPath,
@ -30,6 +31,7 @@ public abstract class EntityTimeSeriesRepository<T extends EntityTimeSeriesInter
this.entityClass = entityClass; this.entityClass = entityClass;
this.entityType = entityType; this.entityType = entityType;
this.searchRepository = Entity.getSearchRepository(); this.searchRepository = Entity.getSearchRepository();
this.daoCollection = Entity.getCollectionDAO();
Entity.registerEntity(entityClass, entityType, this); Entity.registerEntity(entityClass, entityType, this);
} }

View File

@ -725,7 +725,7 @@ public class FeedRepository {
restorePatchAttributes(original, updated); restorePatchAttributes(original, updated);
if (!updated.getReactions().isEmpty()) { if (!nullOrEmpty(updated.getReactions())) {
populateUserReactions(updated.getReactions()); populateUserReactions(updated.getReactions());
updated updated
.getReactions() .getReactions()
@ -885,8 +885,10 @@ public class FeedRepository {
&& !Collections.isEmpty(updated.getReactions())) && !Collections.isEmpty(updated.getReactions()))
|| (!Collections.isEmpty(original.getReactions()) || (!Collections.isEmpty(original.getReactions())
&& Collections.isEmpty(updated.getReactions())) && Collections.isEmpty(updated.getReactions()))
|| original.getReactions().size() != updated.getReactions().size() || (original.getReactions() != null
|| !original.getReactions().containsAll(updated.getReactions()) && updated.getReactions() != null
&& (original.getReactions().size() != updated.getReactions().size()
|| !original.getReactions().containsAll(updated.getReactions())))
|| (original.getAnnouncement() != null || (original.getAnnouncement() != null
&& (!original && (!original
.getAnnouncement() .getAnnouncement()

View File

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import javax.json.JsonPatch; 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;
@ -29,8 +30,8 @@ import org.openmetadata.schema.tests.TestCaseParameter;
import org.openmetadata.schema.tests.TestCaseParameterValue; import org.openmetadata.schema.tests.TestCaseParameterValue;
import org.openmetadata.schema.tests.TestDefinition; import org.openmetadata.schema.tests.TestDefinition;
import org.openmetadata.schema.tests.TestSuite; import org.openmetadata.schema.tests.TestSuite;
import org.openmetadata.schema.tests.type.Assigned;
import org.openmetadata.schema.tests.type.Resolved; import org.openmetadata.schema.tests.type.Resolved;
import org.openmetadata.schema.tests.type.TestCaseFailureReasonType;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatus; import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes; import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.tests.type.TestCaseResult; import org.openmetadata.schema.tests.type.TestCaseResult;
@ -57,6 +58,7 @@ import org.openmetadata.service.util.ResultList;
public class TestCaseRepository extends EntityRepository<TestCase> { public class TestCaseRepository extends EntityRepository<TestCase> {
private static final String TEST_SUITE_FIELD = "testSuite"; private static final String TEST_SUITE_FIELD = "testSuite";
private static final String TEST_CASE_RESULT_FIELD = "testCaseResult"; private static final String TEST_CASE_RESULT_FIELD = "testCaseResult";
private static final String INCIDENTS_FIELD = "incidents";
public static final String COLLECTION_PATH = "/v1/dataQuality/testCases"; public static final String COLLECTION_PATH = "/v1/dataQuality/testCases";
private static final String UPDATE_FIELDS = "owner,entityLink,testSuite,testDefinition"; private static final String UPDATE_FIELDS = "owner,entityLink,testSuite,testDefinition";
private static final String PATCH_FIELDS = "owner,entityLink,testSuite,testDefinition"; private static final String PATCH_FIELDS = "owner,entityLink,testSuite,testDefinition";
@ -79,10 +81,12 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
test.setTestSuite(fields.contains(TEST_SUITE_FIELD) ? getTestSuite(test) : test.getTestSuite()); test.setTestSuite(fields.contains(TEST_SUITE_FIELD) ? getTestSuite(test) : test.getTestSuite());
test.setTestDefinition( test.setTestDefinition(
fields.contains(TEST_DEFINITION) ? getTestDefinition(test) : test.getTestDefinition()); fields.contains(TEST_DEFINITION) ? getTestDefinition(test) : test.getTestDefinition());
test.withTestCaseResult( test.setTestCaseResult(
fields.contains(TEST_CASE_RESULT_FIELD) fields.contains(TEST_CASE_RESULT_FIELD)
? getTestCaseResult(test) ? getTestCaseResult(test)
: test.getTestCaseResult()); : test.getTestCaseResult());
test.setIncidents(
fields.contains(INCIDENTS_FIELD) ? getIncidentIds(test) : test.getIncidents());
} }
@Override @Override
@ -90,7 +94,7 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
test.setTestSuites(fields.contains("testSuites") ? test.getTestSuites() : null); test.setTestSuites(fields.contains("testSuites") ? test.getTestSuites() : null);
test.setTestSuite(fields.contains(TEST_SUITE) ? test.getTestSuite() : null); test.setTestSuite(fields.contains(TEST_SUITE) ? test.getTestSuite() : null);
test.setTestDefinition(fields.contains(TEST_DEFINITION) ? test.getTestDefinition() : null); test.setTestDefinition(fields.contains(TEST_DEFINITION) ? test.getTestDefinition() : null);
test.withTestCaseResult( test.setTestCaseResult(
fields.contains(TEST_CASE_RESULT_FIELD) ? test.getTestCaseResult() : null); fields.contains(TEST_CASE_RESULT_FIELD) ? test.getTestCaseResult() : null);
} }
@ -228,16 +232,22 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Relationship.APPLIED_TO); Relationship.APPLIED_TO);
} }
@Override
protected void postDelete(TestCase test) {
// If we delete the test case, we need to clean up the resolution ts
daoCollection.testCaseResolutionStatusTimeSeriesDao().delete(test.getFullyQualifiedName());
}
public RestUtil.PutResponse<TestCaseResult> addTestCaseResult( public RestUtil.PutResponse<TestCaseResult> addTestCaseResult(
String updatedBy, UriInfo uriInfo, String fqn, TestCaseResult testCaseResult) { String updatedBy, UriInfo uriInfo, String fqn, TestCaseResult testCaseResult) {
// Validate the request content // Validate the request content
TestCase testCase = findByName(fqn, Include.NON_DELETED); TestCase testCase = findByName(fqn, Include.NON_DELETED);
// set the test case resolution status reference if test failed // set the test case resolution status reference if test failed
testCaseResult.setTestCaseResolutionStatusReference( testCaseResult.setIncidentId(
testCaseResult.getTestCaseStatus() != TestCaseStatus.Failed testCaseResult.getTestCaseStatus() == TestCaseStatus.Failed
? null ? createIncidentOnFailure(testCase, updatedBy)
: setTestCaseResolutionStatus(testCase, updatedBy)); : null);
daoCollection daoCollection
.dataQualityDataTimeSeriesDao() .dataQualityDataTimeSeriesDao()
@ -260,8 +270,12 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Response.Status.CREATED, changeEvent, RestUtil.ENTITY_FIELDS_CHANGED); Response.Status.CREATED, changeEvent, RestUtil.ENTITY_FIELDS_CHANGED);
} }
private TestCaseResolutionStatus setTestCaseResolutionStatus( private UUID createIncidentOnFailure(TestCase testCase, String updatedBy) {
TestCase testCase, String updatedBy) {
TestCaseResolutionStatusRepository testCaseResolutionStatusRepository =
(TestCaseResolutionStatusRepository)
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
String json = String json =
daoCollection daoCollection
.testCaseResolutionStatusTimeSeriesDao() .testCaseResolutionStatusTimeSeriesDao()
@ -270,21 +284,27 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
TestCaseResolutionStatus storedTestCaseResolutionStatus = TestCaseResolutionStatus storedTestCaseResolutionStatus =
json != null ? JsonUtils.readValue(json, TestCaseResolutionStatus.class) : null; json != null ? JsonUtils.readValue(json, TestCaseResolutionStatus.class) : null;
if ((storedTestCaseResolutionStatus != null)
&& (storedTestCaseResolutionStatus.getTestCaseResolutionStatusType()
!= TestCaseResolutionStatusTypes.Resolved)) {
// if we already have a non resolve status then we'll simply return it // if we already have a non resolve status then we'll simply return it
return storedTestCaseResolutionStatus; if (Boolean.TRUE.equals(
testCaseResolutionStatusRepository.unresolvedIncident(storedTestCaseResolutionStatus))) {
return storedTestCaseResolutionStatus.getStateId();
} }
// if the test case resolution is null or resolved then we'll create a new one // if the test case resolution is null or resolved then we'll create a new one
return new TestCaseResolutionStatus() TestCaseResolutionStatus status =
new TestCaseResolutionStatus()
.withStateId(UUID.randomUUID()) .withStateId(UUID.randomUUID())
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New)
.withUpdatedBy(getEntityReferenceByName(Entity.USER, updatedBy, Include.ALL)) .withUpdatedBy(getEntityReferenceByName(Entity.USER, updatedBy, Include.ALL))
.withUpdatedAt(System.currentTimeMillis()) .withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(testCase.getEntityReference()); .withTestCaseReference(testCase.getEntityReference());
TestCaseResolutionStatus incident =
testCaseResolutionStatusRepository.createNewRecord(
status, null, testCase.getFullyQualifiedName());
return incident.getStateId();
} }
public RestUtil.PutResponse<TestCaseResult> deleteTestCaseResult( public RestUtil.PutResponse<TestCaseResult> deleteTestCaseResult(
@ -487,6 +507,26 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
testCaseResults, String.valueOf(startTs), String.valueOf(endTs), testCaseResults.size()); testCaseResults, String.valueOf(startTs), String.valueOf(endTs), testCaseResults.size());
} }
/**
* Check all the test case results that have an ongoing incident and get the stateId of the incident
*/
private List<UUID> getIncidentIds(TestCase test) {
List<TestCaseResult> testCaseResults;
testCaseResults =
JsonUtils.readObjects(
daoCollection
.dataQualityDataTimeSeriesDao()
.getResultsWithIncidents(
FullyQualifiedName.buildHash(test.getFullyQualifiedName())),
TestCaseResult.class);
return testCaseResults.stream()
.map(TestCaseResult::getIncidentId)
.collect(Collectors.toSet())
.stream()
.toList();
}
public int getTestCaseCount(List<UUID> testCaseIds) { public int getTestCaseCount(List<UUID> testCaseIds) {
return daoCollection.testCaseDAO().countOfTestCases(testCaseIds); return daoCollection.testCaseDAO().countOfTestCases(testCaseIds);
} }
@ -683,6 +723,9 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS); Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
} }
/**
* If the task is resolved, we'll resolve the Incident with the given reason
*/
@Override @Override
@Transaction @Transaction
public TestCase performTask(String userName, ResolveTask resolveTask) { public TestCase performTask(String userName, ResolveTask resolveTask) {
@ -719,14 +762,20 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Entity.TEST_CASE_RESOLUTION_STATUS, Entity.TEST_CASE_RESOLUTION_STATUS,
JsonUtils.pojoToJson(testCaseResolutionStatus)); JsonUtils.pojoToJson(testCaseResolutionStatus));
testCaseResolutionStatusRepository.postCreate(testCaseResolutionStatus); testCaseResolutionStatusRepository.postCreate(testCaseResolutionStatus);
// TODO: remove incident ID from test case result
return Entity.getEntity(testCaseResolutionStatus.getTestCaseReference(), "", Include.ALL); return Entity.getEntity(testCaseResolutionStatus.getTestCaseReference(), "", Include.ALL);
} }
/**
* If we close the task, we'll flag the incident as Resolved as a False Positive, if
* it is not resolved yet.
* Closing the task means that the incident is not applicable.
*/
@Override @Override
@Transaction @Transaction
public void closeTask(String userName, CloseTask closeTask) { public void closeTask(String userName, CloseTask closeTask) {
// closing task in the context of test case resolution status means that the resolution task
// has been reassigned to someone else
TestCaseResolutionStatus latestTestCaseResolutionStatus = TestCaseResolutionStatus latestTestCaseResolutionStatus =
testCaseResolutionStatusRepository.getLatestRecord(closeTask.getTestCaseFQN()); testCaseResolutionStatusRepository.getLatestRecord(closeTask.getTestCaseFQN());
if (latestTestCaseResolutionStatus == null) { if (latestTestCaseResolutionStatus == null) {
@ -740,19 +789,23 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
return; return;
} }
User user = Entity.getEntityByName(Entity.USER, userName, "", Include.ALL); User user = getEntityByName(Entity.USER, userName, "", Include.ALL);
User assignee = Entity.getEntityByName(Entity.USER, closeTask.getComment(), "", Include.ALL);
TestCaseResolutionStatus testCaseResolutionStatus = TestCaseResolutionStatus testCaseResolutionStatus =
new TestCaseResolutionStatus() new TestCaseResolutionStatus()
.withId(UUID.randomUUID()) .withId(UUID.randomUUID())
.withStateId(latestTestCaseResolutionStatus.getStateId()) .withStateId(latestTestCaseResolutionStatus.getStateId())
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.withTestCaseResolutionStatusDetails( .withTestCaseResolutionStatusDetails(
new Assigned().withAssignee(assignee.getEntityReference())) new Resolved()
.withTestCaseFailureComment(closeTask.getComment())
// If we close the task directly we won't know the reason
.withTestCaseFailureReason(TestCaseFailureReasonType.FalsePositive)
.withResolvedBy(user.getEntityReference()))
.withUpdatedAt(System.currentTimeMillis()) .withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(latestTestCaseResolutionStatus.getTestCaseReference()) .withTestCaseReference(latestTestCaseResolutionStatus.getTestCaseReference())
.withUpdatedBy(user.getEntityReference()); .withUpdatedBy(user.getEntityReference());
Entity.getCollectionDAO() Entity.getCollectionDAO()
.testCaseResolutionStatusTimeSeriesDao() .testCaseResolutionStatusTimeSeriesDao()
.insert( .insert(

View File

@ -11,7 +11,6 @@ import java.util.UUID;
import javax.json.JsonPatch; import javax.json.JsonPatch;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.jdbi.v3.sqlobject.transaction.Transaction; import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.api.feed.CloseTask;
import org.openmetadata.schema.api.feed.ResolveTask; import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.entity.feed.Thread; import org.openmetadata.schema.entity.feed.Thread;
import org.openmetadata.schema.entity.teams.User; import org.openmetadata.schema.entity.teams.User;
@ -22,12 +21,14 @@ import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes; import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TaskDetails; import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.schema.type.TaskStatus; import org.openmetadata.schema.type.TaskStatus;
import org.openmetadata.schema.type.TaskType; import org.openmetadata.schema.type.TaskType;
import org.openmetadata.schema.type.ThreadType; import org.openmetadata.schema.type.ThreadType;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.exception.IncidentManagerException;
import org.openmetadata.service.resources.feeds.MessageParser; import org.openmetadata.service.resources.feeds.MessageParser;
import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.JsonUtils;
@ -96,11 +97,47 @@ public class TestCaseResolutionStatusRepository
} }
} }
@Override public Boolean unresolvedIncident(TestCaseResolutionStatus incident) {
protected void postCreate(TestCaseResolutionStatus entity) { return incident != null
super.postCreate(entity); && !incident
if (entity.getTestCaseResolutionStatusType() == TestCaseResolutionStatusTypes.Assigned) { .getTestCaseResolutionStatusType()
createAssignedTask(entity); .equals(TestCaseResolutionStatusTypes.Resolved);
}
private Thread getIncidentTask(TestCaseResolutionStatus incident) {
// Fetch the latest task (which comes from the NEW state) and close it
String jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(incident.getStateId());
return JsonUtils.readValue(jsonThread, Thread.class);
}
/**
* Ensure we are following the correct status flow
*/
private void validateStatus(
TestCaseResolutionStatusTypes lastStatus, TestCaseResolutionStatusTypes newStatus) {
switch (lastStatus) {
case New -> {
/* New can go to any status */
}
case Ack -> {
if (newStatus.equals(TestCaseResolutionStatusTypes.New)) {
throw IncidentManagerException.invalidStatus(lastStatus, newStatus);
}
}
case Assigned -> {
if (List.of(TestCaseResolutionStatusTypes.New, TestCaseResolutionStatusTypes.Ack)
.contains(newStatus)) {
throw IncidentManagerException.invalidStatus(lastStatus, newStatus);
}
}
case Resolved -> {
if (!newStatus.equals(TestCaseResolutionStatusTypes.Resolved)) {
throw IncidentManagerException.invalidStatus(lastStatus, newStatus);
}
}
} }
} }
@ -108,62 +145,107 @@ public class TestCaseResolutionStatusRepository
@Transaction @Transaction
public TestCaseResolutionStatus createNewRecord( public TestCaseResolutionStatus createNewRecord(
TestCaseResolutionStatus recordEntity, String extension, String recordFQN) { TestCaseResolutionStatus recordEntity, String extension, String recordFQN) {
TestCaseResolutionStatus latestTestCaseFailure =
TestCaseResolutionStatus lastIncident =
getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName()); getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName());
recordEntity.setStateId( if (recordEntity.getStateId() == null) {
((latestTestCaseFailure != null) recordEntity.setStateId(UUID.randomUUID());
&& (latestTestCaseFailure.getTestCaseResolutionStatusType() }
!= TestCaseResolutionStatusTypes.Resolved))
? latestTestCaseFailure.getStateId()
: UUID.randomUUID());
if (latestTestCaseFailure != null // if we have an ongoing incident, set the stateId if the new record to be created
&& latestTestCaseFailure // and validate the flow
.getTestCaseResolutionStatusType() if (Boolean.TRUE.equals(unresolvedIncident(lastIncident))) {
.equals(TestCaseResolutionStatusTypes.Assigned)) { recordEntity.setStateId(lastIncident.getStateId());
String jsonThread = validateStatus(
Entity.getCollectionDAO() lastIncident.getTestCaseResolutionStatusType(),
.feedDAO() recordEntity.getTestCaseResolutionStatusType());
.fetchThreadByTestCaseResolutionStatusId(latestTestCaseFailure.getId()); }
Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
if (recordEntity switch (recordEntity.getTestCaseResolutionStatusType()) {
.getTestCaseResolutionStatusType() // When we create a NEW incident, we need to open a task with the test case owner as the
.equals(TestCaseResolutionStatusTypes.Assigned)) { // assignee. We don't need to check any past history
// We have an open task and we are passing an assigned status type case New -> {
// (i.e. we are re-assigning). This scenario is when the test case resolution status is // If there is already an unresolved incident, return it without doing any
// being sent through the API (and not resolving an open task) // further logic.
// we'll get the associated thread with the latest test case failure if (Boolean.TRUE.equals(unresolvedIncident(lastIncident))) {
return getLatestRecord(lastIncident.getTestCaseReference().getFullyQualifiedName());
}
openNewTask(recordEntity);
}
case Ack -> {
/* nothing to do for ACK. The Owner already has the task open. It will close it when reassigning it */
}
case Assigned -> assignTask(recordEntity, lastIncident);
// When the incident is Resolved, we will close the Assigned task.
case Resolved -> {
resolveTask(recordEntity, lastIncident);
// We don't create a new record. The new status will be added via the
// TestCaseFailureResolutionTaskWorkflow
// implemented in the TestCaseRepository.
return getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName());
}
default -> throw new IllegalArgumentException(
String.format("Invalid status %s", recordEntity.getTestCaseResolutionStatusType()));
}
return super.createNewRecord(recordEntity, extension, recordFQN);
}
private void openNewTask(TestCaseResolutionStatus incidentStatus) {
List<EntityReference> owners =
EntityUtil.getEntityReferences(
daoCollection
.relationshipDAO()
.findFrom(
incidentStatus.getTestCaseReference().getId(),
Entity.TEST_CASE,
Relationship.OWNS.ordinal(),
Entity.USER));
createTask(incidentStatus, owners, "New Incident");
}
private void assignTask(
TestCaseResolutionStatus newIncidentStatus, TestCaseResolutionStatus lastIncidentStatus) {
if (lastIncidentStatus == null) {
throw new IncidentManagerException(
String.format(
"Cannot find the last incident status for stateId %s",
newIncidentStatus.getStateId()));
}
Thread thread = getIncidentTask(lastIncidentStatus);
// we'll close the task (the flow will also create a new assigned test case resolution
// status and open a new task)
Assigned assigned = Assigned assigned =
JsonUtils.convertValue( JsonUtils.convertValue(
recordEntity.getTestCaseResolutionStatusDetails(), Assigned.class); newIncidentStatus.getTestCaseResolutionStatusDetails(), Assigned.class);
User assignee =
Entity.getEntity(Entity.USER, assigned.getAssignee().getId(), "", Include.ALL);
User updatedBy = User updatedBy =
Entity.getEntity(Entity.USER, recordEntity.getUpdatedBy().getId(), "", Include.ALL); Entity.getEntity(Entity.USER, newIncidentStatus.getUpdatedBy().getId(), "", Include.ALL);
CloseTask closeTask =
new CloseTask() patchTaskAssignee(thread, assigned.getAssignee(), updatedBy.getName());
.withComment(assignee.getFullyQualifiedName()) }
.withTestCaseFQN(recordEntity.getTestCaseReference().getFullyQualifiedName());
Entity.getFeedRepository().closeTask(thread, updatedBy.getFullyQualifiedName(), closeTask); private void resolveTask(
return getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName()); TestCaseResolutionStatus newIncidentStatus, TestCaseResolutionStatus lastIncidentStatus) {
} else if (recordEntity
.getTestCaseResolutionStatusType() if (lastIncidentStatus == null) {
.equals(TestCaseResolutionStatusTypes.Resolved)) { throw new IncidentManagerException(
// We have an open task and we are passing a resolved status type (i.e. we are marking it as String.format(
// resolved). This scenario is when the test case resolution status is being sent through "Cannot find the last incident status for stateId %s",
// the API (and not resolving an open task) newIncidentStatus.getStateId()));
}
Thread thread = getIncidentTask(lastIncidentStatus);
Resolved resolved = Resolved resolved =
JsonUtils.convertValue( JsonUtils.convertValue(
recordEntity.getTestCaseResolutionStatusDetails(), Resolved.class); newIncidentStatus.getTestCaseResolutionStatusDetails(), Resolved.class);
TestCase testCase = TestCase testCase =
Entity.getEntity( Entity.getEntity(
Entity.TEST_CASE, recordEntity.getTestCaseReference().getId(), "", Include.ALL); Entity.TEST_CASE, newIncidentStatus.getTestCaseReference().getId(), "", Include.ALL);
User updatedBy = User updatedBy =
Entity.getEntity(Entity.USER, recordEntity.getUpdatedBy().getId(), "", Include.ALL); Entity.getEntity(Entity.USER, newIncidentStatus.getUpdatedBy().getId(), "", Include.ALL);
ResolveTask resolveTask = ResolveTask resolveTask =
new ResolveTask() new ResolveTask()
.withTestCaseFQN(testCase.getFullyQualifiedName()) .withTestCaseFQN(testCase.getFullyQualifiedName())
@ -174,43 +256,45 @@ public class TestCaseResolutionStatusRepository
new FeedRepository.ThreadContext(thread), new FeedRepository.ThreadContext(thread),
updatedBy.getFullyQualifiedName(), updatedBy.getFullyQualifiedName(),
resolveTask); resolveTask);
return getLatestRecord(testCase.getFullyQualifiedName());
} }
throw new IllegalArgumentException( private void createTask(
String.format( TestCaseResolutionStatus incidentStatus, List<EntityReference> assignees, String message) {
"Test Case Resolution status %s with type `Assigned` cannot be moved to `New` or `Ack`. You can `Assign` or `Resolve` the test case failure. ",
latestTestCaseFailure.getId().toString()));
}
return super.createNewRecord(recordEntity, extension, recordFQN);
}
private void createAssignedTask(TestCaseResolutionStatus entity) {
Assigned assigned =
JsonUtils.convertValue(entity.getTestCaseResolutionStatusDetails(), Assigned.class);
List<EntityReference> assignees = Collections.singletonList(assigned.getAssignee());
TaskDetails taskDetails = TaskDetails taskDetails =
new TaskDetails() new TaskDetails()
.withAssignees(assignees) .withAssignees(assignees)
.withType(TaskType.RequestTestCaseFailureResolution) .withType(TaskType.RequestTestCaseFailureResolution)
.withStatus(TaskStatus.Open) .withStatus(TaskStatus.Open)
.withTestCaseResolutionStatusId(entity.getId()); // Each incident flow - flagged by its State ID - will have a single unique Task
.withTestCaseResolutionStatusId(incidentStatus.getStateId());
MessageParser.EntityLink entityLink = MessageParser.EntityLink entityLink =
new MessageParser.EntityLink( new MessageParser.EntityLink(
Entity.TEST_CASE, entity.getTestCaseReference().getFullyQualifiedName()); Entity.TEST_CASE, incidentStatus.getTestCaseReference().getFullyQualifiedName());
Thread thread = Thread thread =
new Thread() new Thread()
.withId(UUID.randomUUID()) .withId(UUID.randomUUID())
.withThreadTs(System.currentTimeMillis()) .withThreadTs(System.currentTimeMillis())
.withMessage("Test Case Failure Resolution requested for ") .withMessage(message)
.withCreatedBy(entity.getUpdatedBy().getName()) .withCreatedBy(incidentStatus.getUpdatedBy().getName())
.withAbout(entityLink.getLinkString()) .withAbout(entityLink.getLinkString())
.withType(ThreadType.Task) .withType(ThreadType.Task)
.withTask(taskDetails) .withTask(taskDetails)
.withUpdatedBy(entity.getUpdatedBy().getName()) .withUpdatedBy(incidentStatus.getUpdatedBy().getName())
.withUpdatedAt(System.currentTimeMillis()); .withUpdatedAt(System.currentTimeMillis());
FeedRepository feedRepository = Entity.getFeedRepository(); FeedRepository feedRepository = Entity.getFeedRepository();
feedRepository.create(thread); feedRepository.create(thread);
} }
private void patchTaskAssignee(Thread originalTask, EntityReference newAssignee, String user) {
Thread updatedTask = JsonUtils.deepCopy(originalTask, Thread.class);
updatedTask.setTask(
updatedTask.getTask().withAssignees(Collections.singletonList(newAssignee)));
JsonPatch patch = JsonUtils.getJsonPatch(originalTask, updatedTask);
FeedRepository feedRepository = Entity.getFeedRepository();
feedRepository.patchThread(null, originalTask.getId(), user, patch);
}
} }

View File

@ -73,7 +73,7 @@ import org.openmetadata.service.util.ResultList;
public class TestCaseResource extends EntityResource<TestCase, TestCaseRepository> { public class TestCaseResource extends EntityResource<TestCase, TestCaseRepository> {
public static final String COLLECTION_PATH = "/v1/dataQuality/testCases"; public static final String COLLECTION_PATH = "/v1/dataQuality/testCases";
static final String FIELDS = "owner,testSuite,testDefinition,testSuites"; static final String FIELDS = "owner,testSuite,testDefinition,testSuites,incidents";
@Override @Override
public TestCase addHref(UriInfo uriInfo, TestCase test) { public TestCase addHref(UriInfo uriInfo, TestCase test) {

View File

@ -1089,54 +1089,41 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
paginate(maxEntities, allEntities, logicalTestSuite); paginate(maxEntities, allEntities, logicalTestSuite);
} }
// Test Case Failure Status Tests
@Test @Test
void post_createTestCaseResultFailure(TestInfo test) throws HttpResponseException { void post_createTestCaseResultFailure(TestInfo test)
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); throws HttpResponseException, ParseException {
// We're going to check how each test only has a single open stateID
// Create a test case failure status for each status type // and 2 tests have their own flow
List<CreateTestCaseResolutionStatus> testCaseFailureStatuses = new ArrayList<>();
List<CreateTestCaseResolutionStatus> resolvedTestCaseFailureStatus = new ArrayList<>();
for (TestCaseResolutionStatusTypes statusType : TestCaseResolutionStatusTypes.values()) {
CreateTestCaseResolutionStatus createTestCaseFailureStatus =
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(statusType)
.withTestCaseResolutionStatusDetails(null);
if (statusType.equals(TestCaseResolutionStatusTypes.Assigned)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Assigned().withAssignee(USER1_REF));
}
if (statusType.equals(TestCaseResolutionStatusTypes.Resolved)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Resolved()
.withTestCaseFailureComment("resolved")
.withTestCaseFailureReason(TestCaseFailureReasonType.MissingData)
.withResolvedBy(USER1_REF));
resolvedTestCaseFailureStatus.add(createTestCaseFailureStatus);
continue;
}
testCaseFailureStatuses.add(createTestCaseFailureStatus);
}
// Create 2 the test case failure statuses with all stages
// this should generate 2 sequence IDs
Long startTs = System.currentTimeMillis(); Long startTs = System.currentTimeMillis();
createTestCaseResolutionStatus(testCaseFailureStatuses); TestCase testCaseEntity1 = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
// create resolved test case failure status last TestCase testCaseEntity2 =
createTestCaseResolutionStatus(resolvedTestCaseFailureStatus); createEntity(createRequest(getEntityName(test) + "2"), ADMIN_AUTH_HEADERS);
// Start a new sequence ID // Add a failed result, which will create a NEW incident and add a new status
createTestCaseResolutionStatus(testCaseFailureStatuses); for (TestCase testCase : List.of(testCaseEntity1, testCaseEntity2)) {
// create resolved test case failure status last putTestCaseResult(
createTestCaseResolutionStatus(resolvedTestCaseFailureStatus); testCase.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
CreateTestCaseResolutionStatus createAckIncident =
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCase.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Ack)
.withTestCaseResolutionStatusDetails(null);
createTestCaseFailureStatus(createAckIncident);
}
Long endTs = System.currentTimeMillis(); Long endTs = System.currentTimeMillis();
// Get the test case failure statuses // Get the test case failure statuses
ResultList<TestCaseResolutionStatus> testCaseFailureStatusResultList = ResultList<TestCaseResolutionStatus> testCaseFailureStatusResultList =
getTestCaseFailureStatus(startTs, endTs, null, null); getTestCaseFailureStatus(startTs, endTs, null, null);
assertEquals(8, testCaseFailureStatusResultList.getData().size()); assertEquals(4, testCaseFailureStatusResultList.getData().size());
// check we have only 2 distinct sequence IDs // check we have only 2 distinct sequence IDs, one for each test case
List<UUID> stateIds = List<UUID> stateIds =
testCaseFailureStatusResultList.getData().stream() testCaseFailureStatusResultList.getData().stream()
.map(TestCaseResolutionStatus::getStateId) .map(TestCaseResolutionStatus::getStateId)
@ -1156,58 +1143,38 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
// Get the test case failure statuses by sequence ID // Get the test case failure statuses by sequence ID
ResultList<TestCaseResolutionStatus> storedTestCaseResolutions = ResultList<TestCaseResolutionStatus> storedTestCaseResolutions =
getTestCaseFailureStatusByStateId(stateId); getTestCaseFailureStatusByStateId(stateId);
assertEquals(4, storedTestCaseResolutions.getData().size()); assertEquals(2, storedTestCaseResolutions.getData().size());
assertEquals(stateId, storedTestCaseResolutions.getData().get(0).getStateId()); assertEquals(stateId, storedTestCaseResolutions.getData().get(0).getStateId());
// Get the test case resolution statuses by status type // Get the test case resolution statuses by status type
storedTestCaseResolutions = storedTestCaseResolutions =
getTestCaseFailureStatus(startTs, endTs, null, TestCaseResolutionStatusTypes.Assigned); getTestCaseFailureStatus(startTs, endTs, null, TestCaseResolutionStatusTypes.Ack);
assertEquals(2, storedTestCaseResolutions.getData().size()); assertEquals(2, storedTestCaseResolutions.getData().size());
assertEquals( assertEquals(
TestCaseResolutionStatusTypes.Assigned, TestCaseResolutionStatusTypes.Ack,
storedTestCaseResolutions.getData().get(0).getTestCaseResolutionStatusType()); storedTestCaseResolutions.getData().get(0).getTestCaseResolutionStatusType());
// Get test case resolution statuses by assignee name
storedTestCaseResolutions = getTestCaseFailureStatus(startTs, endTs, USER1.getName(), null);
assertEquals(2, storedTestCaseResolutions.getData().size());
} }
@Test @Test
void test_listTestCaseFailureStatusPagination(TestInfo test) throws IOException { void test_listTestCaseFailureStatusPagination(TestInfo test) throws IOException, ParseException {
// Create a number of entities between 5 and 20 inclusive // Create a number of entities between 5 and 20 inclusive
Random rand = new Random(); Random rand = new Random();
int maxEntities = rand.nextInt(16) + 5; int maxEntities = rand.nextInt(16) + 5;
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
TestCaseResolutionStatusTypes[] testCaseFailureStatusTypes =
TestCaseResolutionStatusTypes.values();
List<CreateTestCaseResolutionStatus> testCaseFailureStatuses = new ArrayList<>();
for (int i = 0; i < maxEntities; i++) {
// randomly pick a status type
TestCaseResolutionStatusTypes testCaseFailureStatusType =
testCaseFailureStatusTypes[i % testCaseFailureStatusTypes.length];
CreateTestCaseResolutionStatus createTestCaseFailureStatus =
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(testCaseFailureStatusType)
.withTestCaseResolutionStatusDetails(null);
if (testCaseFailureStatusType.equals(TestCaseResolutionStatusTypes.Assigned)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Assigned().withAssignee(USER1_REF));
}
if (testCaseFailureStatusType.equals(TestCaseResolutionStatusTypes.Resolved)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Resolved()
.withTestCaseFailureComment("resolved")
.withTestCaseFailureReason(TestCaseFailureReasonType.MissingData)
.withResolvedBy(USER1_REF));
}
testCaseFailureStatuses.add(createTestCaseFailureStatus);
}
Long startTs = System.currentTimeMillis() - 1000; Long startTs = System.currentTimeMillis() - 1000;
createTestCaseResolutionStatus(testCaseFailureStatuses); for (int i = 0; i < maxEntities; i++) {
// We'll create random test cases
TestCase testCaseEntity =
createEntity(createRequest(getEntityName(test) + i), ADMIN_AUTH_HEADERS);
// Adding failed test case, which will create a NEW incident
putTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
}
Long endTs = System.currentTimeMillis() + 1000; Long endTs = System.currentTimeMillis() + 1000;
// List all entities and use it for checking pagination // List all entities and use it for checking pagination
@ -1217,57 +1184,6 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
paginateTestCaseFailureStatus(maxEntities, allEntities, null, startTs, endTs); paginateTestCaseFailureStatus(maxEntities, allEntities, null, startTs, endTs);
} }
@Test
void test_listTestCaseFailureStatusLatestPagination(TestInfo test) throws IOException {
// Create a number of entities between 5 and 20 inclusive
Random rand = new Random();
TestCase testCaseEntity;
int maxEntities = rand.nextInt(16) + 5;
TestCaseResolutionStatusTypes[] testCaseFailureStatusTypes =
TestCaseResolutionStatusTypes.values();
List<CreateTestCaseResolutionStatus> testCaseFailureStatuses = new ArrayList<>();
for (int i = 0; i < maxEntities; i++) {
// create `maxEntities` number of test cases
testCaseEntity = createEntity(createRequest(getEntityName(test) + i), ADMIN_AUTH_HEADERS);
for (int j = 0; j < 5; j++) {
// create 5 test case failure statuses for each test case
// randomly pick a status type
TestCaseResolutionStatusTypes testCaseFailureStatusType =
testCaseFailureStatusTypes[j % TestCaseResolutionStatusTypes.values().length];
CreateTestCaseResolutionStatus createTestCaseFailureStatus =
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(testCaseFailureStatusType)
.withTestCaseResolutionStatusDetails(null);
if (testCaseFailureStatusType.equals(TestCaseResolutionStatusTypes.Assigned)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Assigned().withAssignee(USER1_REF));
}
if (testCaseFailureStatusType.equals(TestCaseResolutionStatusTypes.Resolved)) {
createTestCaseFailureStatus.setTestCaseResolutionStatusDetails(
new Resolved()
.withTestCaseFailureComment("resolved")
.withTestCaseFailureReason(TestCaseFailureReasonType.MissingData)
.withResolvedBy(USER1_REF));
}
testCaseFailureStatuses.add(createTestCaseFailureStatus);
}
}
Long startTs = System.currentTimeMillis() - 1000;
createTestCaseResolutionStatus(testCaseFailureStatuses);
Long endTs = System.currentTimeMillis() + 1000;
// List all entities and use it for checking pagination
ResultList<TestCaseResolutionStatus> allEntities =
getTestCaseFailureStatus(1000000, null, true, startTs, endTs, null);
paginateTestCaseFailureStatus(maxEntities, allEntities, true, startTs, endTs);
}
@Test @Test
void patch_TestCaseResultFailure(TestInfo test) throws HttpResponseException { void patch_TestCaseResultFailure(TestInfo test) throws HttpResponseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
@ -1326,24 +1242,34 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
@Test @Test
public void test_testCaseResolutionTaskResolveWorkflowThruFeed(TestInfo test) public void test_testCaseResolutionTaskResolveWorkflowThruFeed(TestInfo test)
throws HttpResponseException { throws HttpResponseException, ParseException {
Long startTs = System.currentTimeMillis(); Long startTs = System.currentTimeMillis();
FeedResourceTest feedResourceTest = new FeedResourceTest(); FeedResourceTest feedResourceTest = new FeedResourceTest();
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
CreateTestCaseResolutionStatus createTestCaseFailureStatus =
// Add failed test case, which will create a NEW incident
putTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
// Now, we should be good to create an ASSIGNED status
CreateTestCaseResolutionStatus createAssignedIncident =
new CreateTestCaseResolutionStatus() new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName()) .withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)); .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
TestCaseResolutionStatus testCaseFailureStatus = TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
createTestCaseFailureStatus(createTestCaseFailureStatus);
String jsonThread = String jsonThread =
Entity.getCollectionDAO() Entity.getCollectionDAO()
.feedDAO() .feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId()); .fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
Thread thread = JsonUtils.readValue(jsonThread, Thread.class); Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(testCaseFailureStatus.getId(), thread.getTask().getTestCaseResolutionStatusId()); assertEquals(assignedIncident.getStateId(), thread.getTask().getTestCaseResolutionStatusId());
assertEquals(TaskStatus.Open, thread.getTask().getStatus()); assertEquals(TaskStatus.Open, thread.getTask().getStatus());
// resolve the task. The old task should be closed and the latest test case resolution status // resolve the task. The old task should be closed and the latest test case resolution status
@ -1358,7 +1284,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
jsonThread = jsonThread =
Entity.getCollectionDAO() Entity.getCollectionDAO()
.feedDAO() .feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId()); .fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
thread = JsonUtils.readValue(jsonThread, Thread.class); thread = JsonUtils.readValue(jsonThread, Thread.class);
// Confirm that the task is closed // Confirm that the task is closed
assertEquals(TaskStatus.Closed, thread.getTask().getStatus()); assertEquals(TaskStatus.Closed, thread.getTask().getStatus());
@ -1380,7 +1306,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
TestCaseResolutionStatusTypes.Resolved, TestCaseResolutionStatusTypes.Resolved,
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType()); mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType());
assertEquals( assertEquals(
testCaseFailureStatus.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId()); assignedIncident.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
Resolved resolved = Resolved resolved =
JsonUtils.convertValue( JsonUtils.convertValue(
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusDetails(), mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusDetails(),
@ -1391,31 +1317,40 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
@Test @Test
public void test_testCaseResolutionTaskCloseWorkflowThruFeed(TestInfo test) public void test_testCaseResolutionTaskCloseWorkflowThruFeed(TestInfo test)
throws HttpResponseException { throws HttpResponseException, ParseException {
Long startTs = System.currentTimeMillis(); Long startTs = System.currentTimeMillis();
FeedResourceTest feedResourceTest = new FeedResourceTest(); FeedResourceTest feedResourceTest = new FeedResourceTest();
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
CreateTestCaseResolutionStatus createTestCaseFailureStatus =
// Add failed test case, which will create a NEW incident
putTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
// Now, we should be good to create an ASSIGNED status
CreateTestCaseResolutionStatus createAssignedIncident =
new CreateTestCaseResolutionStatus() new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName()) .withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)); .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
TestCaseResolutionStatus testCaseFailureStatus = TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
createTestCaseFailureStatus(createTestCaseFailureStatus);
// Assert that the task is open // Assert that the task is open
String jsonThread = String jsonThread =
Entity.getCollectionDAO() Entity.getCollectionDAO()
.feedDAO() .feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId()); .fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
Thread thread = JsonUtils.readValue(jsonThread, Thread.class); Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(testCaseFailureStatus.getId(), thread.getTask().getTestCaseResolutionStatusId()); assertEquals(assignedIncident.getStateId(), thread.getTask().getTestCaseResolutionStatusId());
assertEquals(TaskStatus.Open, thread.getTask().getStatus()); assertEquals(TaskStatus.Open, thread.getTask().getStatus());
// close the task. The old task should be closed and the latest test case resolution status // close the task. The old task should be closed and the latest test case resolution status
// should be updated (assigned) with the same state ID and a new task should be opened // should be updated (resolved) with the same state ID.
CloseTask closeTask = CloseTask closeTask =
new CloseTask() new CloseTask()
.withComment(USER1.getFullyQualifiedName()) .withComment(USER1.getFullyQualifiedName())
@ -1424,7 +1359,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
jsonThread = jsonThread =
Entity.getCollectionDAO() Entity.getCollectionDAO()
.feedDAO() .feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId()); .fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
thread = JsonUtils.readValue(jsonThread, Thread.class); thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(TaskStatus.Closed, thread.getTask().getStatus()); assertEquals(TaskStatus.Closed, thread.getTask().getStatus());
@ -1442,49 +1377,48 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
TestCaseResolutionStatus mostRecentTestCaseResolutionStatusData = TestCaseResolutionStatus mostRecentTestCaseResolutionStatusData =
mostRecentTestCaseResolutionStatus.getData().get(0); mostRecentTestCaseResolutionStatus.getData().get(0);
assertEquals( assertEquals(
TestCaseResolutionStatusTypes.Assigned, TestCaseResolutionStatusTypes.Resolved,
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType()); mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType());
assertEquals( assertEquals(
testCaseFailureStatus.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId()); assignedIncident.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
Assigned assigned =
JsonUtils.convertValue(
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusDetails(),
Assigned.class);
assertEquals(USER1.getFullyQualifiedName(), assigned.getAssignee().getFullyQualifiedName());
} }
@Test @Test
public void test_testCaseResolutionTaskWorkflowThruAPI(TestInfo test) public void test_testCaseResolutionTaskWorkflowThruAPI(TestInfo test)
throws HttpResponseException { throws HttpResponseException, ParseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
CreateTestCaseResolutionStatus createTestCaseFailureStatus = // Add failed test case, which will create a NEW incident
putTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
// Now, we should be good to create an ASSIGNED status
CreateTestCaseResolutionStatus createAssignedIncident =
new CreateTestCaseResolutionStatus() new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName()) .withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New)
.withTestCaseResolutionStatusDetails(null);
createTestCaseFailureStatus(createTestCaseFailureStatus);
TestCaseResolutionStatus testCaseFailureStatusAssigned =
createTestCaseFailureStatus(
createTestCaseFailureStatus
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF))); .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
// Confirm that the task is open // Confirm that the task is open
String jsonThread = String jsonThread =
Entity.getCollectionDAO() Entity.getCollectionDAO()
.feedDAO() .feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatusAssigned.getId()); .fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
Thread thread = JsonUtils.readValue(jsonThread, Thread.class); Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(TaskStatus.Open, thread.getTask().getStatus()); assertEquals(TaskStatus.Open, thread.getTask().getStatus());
assertEquals( assertEquals(assignedIncident.getStateId(), thread.getTask().getTestCaseResolutionStatusId());
testCaseFailureStatusAssigned.getId(), thread.getTask().getTestCaseResolutionStatusId());
// Create a new test case resolution status with type Resolved // Create a new test case resolution status with type Resolved
// and confirm the task is closed // and confirm the task is closed
CreateTestCaseResolutionStatus createTestCaseFailureStatusResolved = CreateTestCaseResolutionStatus createTestCaseFailureStatusResolved =
createTestCaseFailureStatus createAssignedIncident
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.withTestCaseResolutionStatusDetails( .withTestCaseResolutionStatusDetails(
new Resolved() new Resolved()
@ -1499,22 +1433,33 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
} }
@Test @Test
public void unauthorizedTestCaseResolutionFlow(TestInfo test) throws HttpResponseException { public void unauthorizedTestCaseResolutionFlow(TestInfo test)
throws HttpResponseException, ParseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS); TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
CreateTestCaseResolutionStatus createTestCaseFailureStatus = // Add failed test case, which will create a NEW incident
putTestCaseResult(
testCaseEntity.getFullyQualifiedName(),
new TestCaseResult()
.withResult("result")
.withTestCaseStatus(TestCaseStatus.Failed)
.withTimestamp(TestUtils.dateToTimestamp("2024-01-01")),
ADMIN_AUTH_HEADERS);
// Now, we should be good to create an ASSIGNED status
CreateTestCaseResolutionStatus createAssignedIncident =
new CreateTestCaseResolutionStatus() new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName()) .withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned) .withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)); .withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
createTestCaseFailureStatus(createTestCaseFailureStatus); createTestCaseFailureStatus(createAssignedIncident);
assertResponseContains( assertResponseContains(
() -> () ->
createTestCaseFailureStatus( createTestCaseFailureStatus(
createTestCaseFailureStatus.withTestCaseResolutionStatusType( createAssignedIncident.withTestCaseResolutionStatusType(
TestCaseResolutionStatusTypes.Ack)), TestCaseResolutionStatusTypes.Ack)),
BAD_REQUEST, BAD_REQUEST,
"with type `Assigned` cannot be moved to `New` or `Ack`. You can `Assign` or `Resolve` the test case failure."); "Incident with status [Assigned] cannot be moved to [Ack]");
} }
public void deleteTestCaseResult(String fqn, Long timestamp, Map<String, String> authHeaders) public void deleteTestCaseResult(String fqn, Long timestamp, Map<String, String> authHeaders)
@ -1626,7 +1571,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
assertEquals(expectedTestCaseResults.size(), actualTestCaseResults.getData().size()); assertEquals(expectedTestCaseResults.size(), actualTestCaseResults.getData().size());
Map<Long, TestCaseResult> testCaseResultMap = new HashMap<>(); Map<Long, TestCaseResult> testCaseResultMap = new HashMap<>();
for (TestCaseResult result : actualTestCaseResults.getData()) { for (TestCaseResult result : actualTestCaseResults.getData()) {
result.setTestCaseResolutionStatusReference(null); result.setIncidentId(null);
testCaseResultMap.put(result.getTimestamp(), result); testCaseResultMap.put(result.getTimestamp(), result);
} }
for (TestCaseResult result : expectedTestCaseResults) { for (TestCaseResult result : expectedTestCaseResults) {

View File

@ -86,9 +86,9 @@
"$ref": "#/definitions/testResultValue" "$ref": "#/definitions/testResultValue"
} }
}, },
"testCaseResolutionStatusReference": { "incidentId": {
"description": "Reference to the failure status object for the test case result.", "description": "Reference to an ongoing Incident ID (stateId) for this result.",
"$ref": "./testCaseResolutionStatus.json" "$ref": "../type/basic.json#/definitions/uuid"
}, },
"passedRows": { "passedRows": {
"description": "Number of rows that passed.", "description": "Number of rows that passed.",

View File

@ -69,6 +69,7 @@
} }
}, },
"testCaseResult": { "testCaseResult": {
"description": "Latest test case result obtained for this test case.",
"$ref": "./basic.json#/definitions/testCaseResult" "$ref": "./basic.json#/definitions/testCaseResult"
}, },
"version": { "version": {
@ -105,6 +106,13 @@
"description": "Compute the passed and failed row count for the test case.", "description": "Compute the passed and failed row count for the test case.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"incidents": {
"description": "List of incident IDs (stateId) for any testCaseResult of a given test case.",
"type": "array",
"items": {
"$ref": "../type/basic.json#/definitions/uuid"
}
} }
}, },
"required": ["name", "testDefinition", "entityLink", "testSuite"], "required": ["name", "testDefinition", "entityLink", "testSuite"],