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": {
"sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{
"testCaseResolutionStatusType": "Ack",
"severity": "Severity1"
@ -108,10 +104,6 @@
],
"resolutions": {
"sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{
"testCaseResolutionStatusType": "Ack",
"severity": "Severity1"
@ -186,10 +178,6 @@
],
"resolutions": {
"sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{
"testCaseResolutionStatusType": "Ack",
"severity": "Severity1"
@ -264,10 +252,6 @@
],
"resolutions": {
"sequenceOne": [
{
"testCaseResolutionStatusType": "New",
"severity": "Severity1"
},
{
"testCaseResolutionStatusType": "Ack",
"severity": "Severity1"

View File

@ -17,6 +17,9 @@ from typing import List
from pydantic import BaseModel
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.tests.basic import TestCaseResult
from metadata.generated.schema.tests.testCase import TestCase
@ -38,3 +41,9 @@ class OMetaTestCaseSample(BaseModel):
class OMetaTestCaseResultsSample(BaseModel):
test_case_results: TestCaseResult
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.tests.basic import TestCaseResult
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.type.schema import Topic
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.tests_data import (
OMetaLogicalTestSuiteSample,
OMetaTestCaseResolutionStatus,
OMetaTestCaseResultsSample,
OMetaTestCaseSample,
OMetaTestSuiteSample,
@ -411,6 +415,15 @@ class MetadataRestSink(Sink): # pylint: disable=too-many-public-methods
)
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
def write_data_insight_sample(
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.tests_data import (
OMetaLogicalTestSuiteSample,
OMetaTestCaseResolutionStatus,
OMetaTestCaseResultsSample,
OMetaTestCaseSample,
OMetaTestSuiteSample,
@ -566,6 +567,7 @@ class SampleDataSource(
yield from self.ingest_test_suite()
yield from self.ingest_test_case()
yield from self.ingest_test_case_results()
yield from self.ingest_incidents()
yield from self.ingest_logical_test_suite()
yield from self.ingest_data_insights()
yield from self.ingest_life_cycle()
@ -1408,6 +1410,15 @@ class SampleDataSource(
)
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']}"
for _, resolutions in test_case["resolutions"].items():
@ -1449,8 +1460,10 @@ class SampleDataSource(
testCaseFailureComment="Resolution comment",
)
self.metadata.create_test_case_resolution(
create_test_case_resolution
yield Either(
right=OMetaTestCaseResolutionStatus(
test_case_resolution=create_test_case_resolution
)
)
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() {
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 {
@ -3605,6 +3631,10 @@ public interface CollectionDAO {
"SELECT json FROM test_case_resolution_status_time_series "
+ "WHERE stateId = :stateId ORDER BY timestamp DESC")
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> {

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response;
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.TestDefinition;
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.TestCaseFailureReasonType;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.tests.type.TestCaseResult;
@ -57,6 +58,7 @@ import org.openmetadata.service.util.ResultList;
public class TestCaseRepository extends EntityRepository<TestCase> {
private static final String TEST_SUITE_FIELD = "testSuite";
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";
private static final String UPDATE_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.setTestDefinition(
fields.contains(TEST_DEFINITION) ? getTestDefinition(test) : test.getTestDefinition());
test.withTestCaseResult(
test.setTestCaseResult(
fields.contains(TEST_CASE_RESULT_FIELD)
? getTestCaseResult(test)
: test.getTestCaseResult());
test.setIncidents(
fields.contains(INCIDENTS_FIELD) ? getIncidentIds(test) : test.getIncidents());
}
@Override
@ -90,7 +94,7 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
test.setTestSuites(fields.contains("testSuites") ? test.getTestSuites() : null);
test.setTestSuite(fields.contains(TEST_SUITE) ? test.getTestSuite() : null);
test.setTestDefinition(fields.contains(TEST_DEFINITION) ? test.getTestDefinition() : null);
test.withTestCaseResult(
test.setTestCaseResult(
fields.contains(TEST_CASE_RESULT_FIELD) ? test.getTestCaseResult() : null);
}
@ -228,16 +232,22 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
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(
String updatedBy, UriInfo uriInfo, String fqn, TestCaseResult testCaseResult) {
// Validate the request content
TestCase testCase = findByName(fqn, Include.NON_DELETED);
// set the test case resolution status reference if test failed
testCaseResult.setTestCaseResolutionStatusReference(
testCaseResult.getTestCaseStatus() != TestCaseStatus.Failed
? null
: setTestCaseResolutionStatus(testCase, updatedBy));
testCaseResult.setIncidentId(
testCaseResult.getTestCaseStatus() == TestCaseStatus.Failed
? createIncidentOnFailure(testCase, updatedBy)
: null);
daoCollection
.dataQualityDataTimeSeriesDao()
@ -260,8 +270,12 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Response.Status.CREATED, changeEvent, RestUtil.ENTITY_FIELDS_CHANGED);
}
private TestCaseResolutionStatus setTestCaseResolutionStatus(
TestCase testCase, String updatedBy) {
private UUID createIncidentOnFailure(TestCase testCase, String updatedBy) {
TestCaseResolutionStatusRepository testCaseResolutionStatusRepository =
(TestCaseResolutionStatusRepository)
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
String json =
daoCollection
.testCaseResolutionStatusTimeSeriesDao()
@ -270,21 +284,27 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
TestCaseResolutionStatus storedTestCaseResolutionStatus =
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
return storedTestCaseResolutionStatus;
// if we already have a non resolve status then we'll simply return it
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
return new TestCaseResolutionStatus()
.withStateId(UUID.randomUUID())
.withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New)
.withUpdatedBy(getEntityReferenceByName(Entity.USER, updatedBy, Include.ALL))
.withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(testCase.getEntityReference());
TestCaseResolutionStatus status =
new TestCaseResolutionStatus()
.withStateId(UUID.randomUUID())
.withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New)
.withUpdatedBy(getEntityReferenceByName(Entity.USER, updatedBy, Include.ALL))
.withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(testCase.getEntityReference());
TestCaseResolutionStatus incident =
testCaseResolutionStatusRepository.createNewRecord(
status, null, testCase.getFullyQualifiedName());
return incident.getStateId();
}
public RestUtil.PutResponse<TestCaseResult> deleteTestCaseResult(
@ -487,6 +507,26 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
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) {
return daoCollection.testCaseDAO().countOfTestCases(testCaseIds);
}
@ -683,6 +723,9 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
}
/**
* If the task is resolved, we'll resolve the Incident with the given reason
*/
@Override
@Transaction
public TestCase performTask(String userName, ResolveTask resolveTask) {
@ -719,14 +762,20 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
Entity.TEST_CASE_RESOLUTION_STATUS,
JsonUtils.pojoToJson(testCaseResolutionStatus));
testCaseResolutionStatusRepository.postCreate(testCaseResolutionStatus);
// TODO: remove incident ID from test case result
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
@Transaction
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 =
testCaseResolutionStatusRepository.getLatestRecord(closeTask.getTestCaseFQN());
if (latestTestCaseResolutionStatus == null) {
@ -740,19 +789,23 @@ public class TestCaseRepository extends EntityRepository<TestCase> {
return;
}
User user = Entity.getEntityByName(Entity.USER, userName, "", Include.ALL);
User assignee = Entity.getEntityByName(Entity.USER, closeTask.getComment(), "", Include.ALL);
User user = getEntityByName(Entity.USER, userName, "", Include.ALL);
TestCaseResolutionStatus testCaseResolutionStatus =
new TestCaseResolutionStatus()
.withId(UUID.randomUUID())
.withStateId(latestTestCaseResolutionStatus.getStateId())
.withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.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())
.withTestCaseReference(latestTestCaseResolutionStatus.getTestCaseReference())
.withUpdatedBy(user.getEntityReference());
Entity.getCollectionDAO()
.testCaseResolutionStatusTimeSeriesDao()
.insert(

View File

@ -11,7 +11,6 @@ import java.util.UUID;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response;
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.entity.feed.Thread;
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.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.schema.type.TaskStatus;
import org.openmetadata.schema.type.TaskType;
import org.openmetadata.schema.type.ThreadType;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.exception.IncidentManagerException;
import org.openmetadata.service.resources.feeds.MessageParser;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.JsonUtils;
@ -96,11 +97,47 @@ public class TestCaseResolutionStatusRepository
}
}
@Override
protected void postCreate(TestCaseResolutionStatus entity) {
super.postCreate(entity);
if (entity.getTestCaseResolutionStatusType() == TestCaseResolutionStatusTypes.Assigned) {
createAssignedTask(entity);
public Boolean unresolvedIncident(TestCaseResolutionStatus incident) {
return incident != null
&& !incident
.getTestCaseResolutionStatusType()
.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,109 +145,156 @@ public class TestCaseResolutionStatusRepository
@Transaction
public TestCaseResolutionStatus createNewRecord(
TestCaseResolutionStatus recordEntity, String extension, String recordFQN) {
TestCaseResolutionStatus latestTestCaseFailure =
TestCaseResolutionStatus lastIncident =
getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName());
recordEntity.setStateId(
((latestTestCaseFailure != null)
&& (latestTestCaseFailure.getTestCaseResolutionStatusType()
!= TestCaseResolutionStatusTypes.Resolved))
? latestTestCaseFailure.getStateId()
: UUID.randomUUID());
if (recordEntity.getStateId() == null) {
recordEntity.setStateId(UUID.randomUUID());
}
if (latestTestCaseFailure != null
&& latestTestCaseFailure
.getTestCaseResolutionStatusType()
.equals(TestCaseResolutionStatusTypes.Assigned)) {
String jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(latestTestCaseFailure.getId());
Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
if (recordEntity
.getTestCaseResolutionStatusType()
.equals(TestCaseResolutionStatusTypes.Assigned)) {
// We have an open task and we are passing an assigned status type
// (i.e. we are re-assigning). This scenario is when the test case resolution status is
// being sent through the API (and not resolving an open task)
// we'll get the associated thread with the latest test case failure
// if we have an ongoing incident, set the stateId if the new record to be created
// and validate the flow
if (Boolean.TRUE.equals(unresolvedIncident(lastIncident))) {
recordEntity.setStateId(lastIncident.getStateId());
validateStatus(
lastIncident.getTestCaseResolutionStatusType(),
recordEntity.getTestCaseResolutionStatusType());
}
// we'll close the task (the flow will also create a new assigned test case resolution
// status and open a new task)
Assigned assigned =
JsonUtils.convertValue(
recordEntity.getTestCaseResolutionStatusDetails(), Assigned.class);
User assignee =
Entity.getEntity(Entity.USER, assigned.getAssignee().getId(), "", Include.ALL);
User updatedBy =
Entity.getEntity(Entity.USER, recordEntity.getUpdatedBy().getId(), "", Include.ALL);
CloseTask closeTask =
new CloseTask()
.withComment(assignee.getFullyQualifiedName())
.withTestCaseFQN(recordEntity.getTestCaseReference().getFullyQualifiedName());
Entity.getFeedRepository().closeTask(thread, updatedBy.getFullyQualifiedName(), closeTask);
return getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName());
} else if (recordEntity
.getTestCaseResolutionStatusType()
.equals(TestCaseResolutionStatusTypes.Resolved)) {
// We have an open task and we are passing a resolved status type (i.e. we are marking it as
// resolved). This scenario is when the test case resolution status is being sent through
// the API (and not resolving an open task)
Resolved resolved =
JsonUtils.convertValue(
recordEntity.getTestCaseResolutionStatusDetails(), Resolved.class);
TestCase testCase =
Entity.getEntity(
Entity.TEST_CASE, recordEntity.getTestCaseReference().getId(), "", Include.ALL);
User updatedBy =
Entity.getEntity(Entity.USER, recordEntity.getUpdatedBy().getId(), "", Include.ALL);
ResolveTask resolveTask =
new ResolveTask()
.withTestCaseFQN(testCase.getFullyQualifiedName())
.withTestCaseFailureReason(resolved.getTestCaseFailureReason())
.withNewValue(resolved.getTestCaseFailureComment());
Entity.getFeedRepository()
.resolveTask(
new FeedRepository.ThreadContext(thread),
updatedBy.getFullyQualifiedName(),
resolveTask);
return getLatestRecord(testCase.getFullyQualifiedName());
switch (recordEntity.getTestCaseResolutionStatusType()) {
// When we create a NEW incident, we need to open a task with the test case owner as the
// assignee. We don't need to check any past history
case New -> {
// If there is already an unresolved incident, return it without doing any
// further logic.
if (Boolean.TRUE.equals(unresolvedIncident(lastIncident))) {
return getLatestRecord(lastIncident.getTestCaseReference().getFullyQualifiedName());
}
openNewTask(recordEntity);
}
throw new IllegalArgumentException(
String.format(
"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()));
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 createAssignedTask(TestCaseResolutionStatus entity) {
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);
Assigned assigned =
JsonUtils.convertValue(entity.getTestCaseResolutionStatusDetails(), Assigned.class);
List<EntityReference> assignees = Collections.singletonList(assigned.getAssignee());
JsonUtils.convertValue(
newIncidentStatus.getTestCaseResolutionStatusDetails(), Assigned.class);
User updatedBy =
Entity.getEntity(Entity.USER, newIncidentStatus.getUpdatedBy().getId(), "", Include.ALL);
patchTaskAssignee(thread, assigned.getAssignee(), updatedBy.getName());
}
private void resolveTask(
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);
Resolved resolved =
JsonUtils.convertValue(
newIncidentStatus.getTestCaseResolutionStatusDetails(), Resolved.class);
TestCase testCase =
Entity.getEntity(
Entity.TEST_CASE, newIncidentStatus.getTestCaseReference().getId(), "", Include.ALL);
User updatedBy =
Entity.getEntity(Entity.USER, newIncidentStatus.getUpdatedBy().getId(), "", Include.ALL);
ResolveTask resolveTask =
new ResolveTask()
.withTestCaseFQN(testCase.getFullyQualifiedName())
.withTestCaseFailureReason(resolved.getTestCaseFailureReason())
.withNewValue(resolved.getTestCaseFailureComment());
Entity.getFeedRepository()
.resolveTask(
new FeedRepository.ThreadContext(thread),
updatedBy.getFullyQualifiedName(),
resolveTask);
}
private void createTask(
TestCaseResolutionStatus incidentStatus, List<EntityReference> assignees, String message) {
TaskDetails taskDetails =
new TaskDetails()
.withAssignees(assignees)
.withType(TaskType.RequestTestCaseFailureResolution)
.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 =
new MessageParser.EntityLink(
Entity.TEST_CASE, entity.getTestCaseReference().getFullyQualifiedName());
Entity.TEST_CASE, incidentStatus.getTestCaseReference().getFullyQualifiedName());
Thread thread =
new Thread()
.withId(UUID.randomUUID())
.withThreadTs(System.currentTimeMillis())
.withMessage("Test Case Failure Resolution requested for ")
.withCreatedBy(entity.getUpdatedBy().getName())
.withMessage(message)
.withCreatedBy(incidentStatus.getUpdatedBy().getName())
.withAbout(entityLink.getLinkString())
.withType(ThreadType.Task)
.withTask(taskDetails)
.withUpdatedBy(entity.getUpdatedBy().getName())
.withUpdatedBy(incidentStatus.getUpdatedBy().getName())
.withUpdatedAt(System.currentTimeMillis());
FeedRepository feedRepository = Entity.getFeedRepository();
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 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
public TestCase addHref(UriInfo uriInfo, TestCase test) {

View File

@ -1089,54 +1089,41 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
paginate(maxEntities, allEntities, logicalTestSuite);
}
// Test Case Failure Status Tests
@Test
void post_createTestCaseResultFailure(TestInfo test) throws HttpResponseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
// Create a test case failure status for each status type
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
void post_createTestCaseResultFailure(TestInfo test)
throws HttpResponseException, ParseException {
// We're going to check how each test only has a single open stateID
// and 2 tests have their own flow
Long startTs = System.currentTimeMillis();
createTestCaseResolutionStatus(testCaseFailureStatuses);
// create resolved test case failure status last
createTestCaseResolutionStatus(resolvedTestCaseFailureStatus);
TestCase testCaseEntity1 = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
TestCase testCaseEntity2 =
createEntity(createRequest(getEntityName(test) + "2"), ADMIN_AUTH_HEADERS);
// Start a new sequence ID
createTestCaseResolutionStatus(testCaseFailureStatuses);
// create resolved test case failure status last
createTestCaseResolutionStatus(resolvedTestCaseFailureStatus);
// Add a failed result, which will create a NEW incident and add a new status
for (TestCase testCase : List.of(testCaseEntity1, testCaseEntity2)) {
putTestCaseResult(
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();
// Get the test case failure statuses
ResultList<TestCaseResolutionStatus> testCaseFailureStatusResultList =
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 =
testCaseFailureStatusResultList.getData().stream()
.map(TestCaseResolutionStatus::getStateId)
@ -1156,58 +1143,38 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
// Get the test case failure statuses by sequence ID
ResultList<TestCaseResolutionStatus> storedTestCaseResolutions =
getTestCaseFailureStatusByStateId(stateId);
assertEquals(4, storedTestCaseResolutions.getData().size());
assertEquals(2, storedTestCaseResolutions.getData().size());
assertEquals(stateId, storedTestCaseResolutions.getData().get(0).getStateId());
// Get the test case resolution statuses by status type
storedTestCaseResolutions =
getTestCaseFailureStatus(startTs, endTs, null, TestCaseResolutionStatusTypes.Assigned);
getTestCaseFailureStatus(startTs, endTs, null, TestCaseResolutionStatusTypes.Ack);
assertEquals(2, storedTestCaseResolutions.getData().size());
assertEquals(
TestCaseResolutionStatusTypes.Assigned,
TestCaseResolutionStatusTypes.Ack,
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
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
Random rand = new Random();
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;
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;
// 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);
}
@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
void patch_TestCaseResultFailure(TestInfo test) throws HttpResponseException {
TestCase testCaseEntity = createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
@ -1326,24 +1242,34 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
@Test
public void test_testCaseResolutionTaskResolveWorkflowThruFeed(TestInfo test)
throws HttpResponseException {
throws HttpResponseException, ParseException {
Long startTs = System.currentTimeMillis();
FeedResourceTest feedResourceTest = new FeedResourceTest();
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()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
TestCaseResolutionStatus testCaseFailureStatus =
createTestCaseFailureStatus(createTestCaseFailureStatus);
TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
String jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId());
.fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
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());
// 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 =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId());
.fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
thread = JsonUtils.readValue(jsonThread, Thread.class);
// Confirm that the task is closed
assertEquals(TaskStatus.Closed, thread.getTask().getStatus());
@ -1380,7 +1306,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
TestCaseResolutionStatusTypes.Resolved,
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType());
assertEquals(
testCaseFailureStatus.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
assignedIncident.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
Resolved resolved =
JsonUtils.convertValue(
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusDetails(),
@ -1391,31 +1317,40 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
@Test
public void test_testCaseResolutionTaskCloseWorkflowThruFeed(TestInfo test)
throws HttpResponseException {
throws HttpResponseException, ParseException {
Long startTs = System.currentTimeMillis();
FeedResourceTest feedResourceTest = new FeedResourceTest();
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()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
TestCaseResolutionStatus testCaseFailureStatus =
createTestCaseFailureStatus(createTestCaseFailureStatus);
TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
// Assert that the task is open
String jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId());
.fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
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());
// 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 =
new CloseTask()
.withComment(USER1.getFullyQualifiedName())
@ -1424,7 +1359,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatus.getId());
.fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(TaskStatus.Closed, thread.getTask().getStatus());
@ -1442,49 +1377,48 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
TestCaseResolutionStatus mostRecentTestCaseResolutionStatusData =
mostRecentTestCaseResolutionStatus.getData().get(0);
assertEquals(
TestCaseResolutionStatusTypes.Assigned,
TestCaseResolutionStatusTypes.Resolved,
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusType());
assertEquals(
testCaseFailureStatus.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
Assigned assigned =
JsonUtils.convertValue(
mostRecentTestCaseResolutionStatusData.getTestCaseResolutionStatusDetails(),
Assigned.class);
assertEquals(USER1.getFullyQualifiedName(), assigned.getAssignee().getFullyQualifiedName());
assignedIncident.getStateId(), mostRecentTestCaseResolutionStatusData.getStateId());
}
@Test
public void test_testCaseResolutionTaskWorkflowThruAPI(TestInfo test)
throws HttpResponseException {
throws HttpResponseException, ParseException {
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()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.New)
.withTestCaseResolutionStatusDetails(null);
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
createTestCaseFailureStatus(createTestCaseFailureStatus);
TestCaseResolutionStatus testCaseFailureStatusAssigned =
createTestCaseFailureStatus(
createTestCaseFailureStatus
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF)));
TestCaseResolutionStatus assignedIncident = createTestCaseFailureStatus(createAssignedIncident);
// Confirm that the task is open
String jsonThread =
Entity.getCollectionDAO()
.feedDAO()
.fetchThreadByTestCaseResolutionStatusId(testCaseFailureStatusAssigned.getId());
.fetchThreadByTestCaseResolutionStatusId(assignedIncident.getStateId());
Thread thread = JsonUtils.readValue(jsonThread, Thread.class);
assertEquals(TaskStatus.Open, thread.getTask().getStatus());
assertEquals(
testCaseFailureStatusAssigned.getId(), thread.getTask().getTestCaseResolutionStatusId());
assertEquals(assignedIncident.getStateId(), thread.getTask().getTestCaseResolutionStatusId());
// Create a new test case resolution status with type Resolved
// and confirm the task is closed
CreateTestCaseResolutionStatus createTestCaseFailureStatusResolved =
createTestCaseFailureStatus
createAssignedIncident
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.withTestCaseResolutionStatusDetails(
new Resolved()
@ -1499,22 +1433,33 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
}
@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);
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()
.withTestCaseReference(testCaseEntity.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(USER1_REF));
createTestCaseFailureStatus(createTestCaseFailureStatus);
createTestCaseFailureStatus(createAssignedIncident);
assertResponseContains(
() ->
createTestCaseFailureStatus(
createTestCaseFailureStatus.withTestCaseResolutionStatusType(
createAssignedIncident.withTestCaseResolutionStatusType(
TestCaseResolutionStatusTypes.Ack)),
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)
@ -1626,7 +1571,7 @@ public class TestCaseResourceTest extends EntityResourceTest<TestCase, CreateTes
assertEquals(expectedTestCaseResults.size(), actualTestCaseResults.getData().size());
Map<Long, TestCaseResult> testCaseResultMap = new HashMap<>();
for (TestCaseResult result : actualTestCaseResults.getData()) {
result.setTestCaseResolutionStatusReference(null);
result.setIncidentId(null);
testCaseResultMap.put(result.getTimestamp(), result);
}
for (TestCaseResult result : expectedTestCaseResults) {

View File

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

View File

@ -69,6 +69,7 @@
}
},
"testCaseResult": {
"description": "Latest test case result obtained for this test case.",
"$ref": "./basic.json#/definitions/testCaseResult"
},
"version": {
@ -105,6 +106,13 @@
"description": "Compute the passed and failed row count for the test case.",
"type": "boolean",
"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"],