Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX #14248 - Implement test case resolution task workflow #14323

Merged
merged 5 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,10 @@ def ingest_test_case(self) -> Iterable[Either[OMetaTestCaseSample]]:
)
create_test_case_resolution.testCaseResolutionStatusDetails = Assigned(
assignee=EntityReference(
id=user.id.__root__, type="user"
id=user.id.__root__,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming entity can be team or user?
also lets make sure we are validating on the backend side

Copy link
Contributor Author

@TeddyCr TeddyCr Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually just added support for user as far as task flow creation goes. I'll merge it for now to unblock UI, and add the user validation in a seperate PR.

type="user",
name=user.name.__root__,
fullyQualifiedName=user.fullyQualifiedName.__root__,
)
)
if resolution["testCaseResolutionStatusType"] == "Resolved":
Expand All @@ -1435,7 +1438,10 @@ def ingest_test_case(self) -> Iterable[Either[OMetaTestCaseSample]]:
)
create_test_case_resolution.testCaseResolutionStatusDetails = Resolved(
resolvedBy=EntityReference(
id=user.id.__root__, type="user"
id=user.id.__root__,
type="user",
name=user.name.__root__,
fullyQualifiedName=user.fullyQualifiedName.__root__,
),
testCaseFailureReason=random.choice(
list(TestCaseFailureReasonType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,19 @@ int listCountThreadsByOwner(
@BindList("teamIds") List<String> teamIds,
@Define("condition") String condition);

@ConnectionAwareSqlQuery(
value =
"SELECT json FROM thread_entity "
+ "WHERE (json -> '$.task.testCaseResolutionStatusId') = :testCaseResolutionStatusId; ",
connectionType = MYSQL)
@ConnectionAwareSqlQuery(
value =
"SELECT json FROM thread_entity "
+ "WHERE (json#>'{task}'->>'testCaseResolutionStatusId') = :testCaseResolutionStatusId; ",
connectionType = POSTGRES)
String fetchThreadByTestCaseResolutionStatusId(
@BindUUID("testCaseResolutionStatusId") UUID testCaseResolutionStatusId);

default List<String> listThreadsByEntityLink(
FeedFilter filter, EntityLink entityLink, int limit, int relation, String userName, List<String> teamNames) {
int filterRelation = -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static org.openmetadata.service.exception.CatalogExceptionMessage.ANNOUNCEMENT_INVALID_START_TIME;
import static org.openmetadata.service.exception.CatalogExceptionMessage.ANNOUNCEMENT_OVERLAP;
import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound;
import static org.openmetadata.service.jdbi3.UserRepository.TEAMS_FIELD;
import static org.openmetadata.service.util.EntityUtil.compareEntityReference;
import static org.openmetadata.service.util.RestUtil.DELETED_TEAM_DISPLAY;
import static org.openmetadata.service.util.RestUtil.DELETED_TEAM_NAME;
Expand Down Expand Up @@ -319,7 +320,7 @@ public PatchResponse<Thread> resolveTask(UriInfo uriInfo, Thread thread, String
return new PatchResponse<>(Status.OK, updatedHref, RestUtil.ENTITY_UPDATED);
}

private void resolveTask(ThreadContext threadContext, String user, ResolveTask resolveTask) {
protected void resolveTask(ThreadContext threadContext, String user, ResolveTask resolveTask) {
TaskWorkflow taskWorkflow = threadContext.getTaskWorkflow();
EntityInterface aboutEntity = threadContext.getAboutEntity();
String origJson = JsonUtils.pojoToJson(aboutEntity);
Expand Down Expand Up @@ -568,7 +569,7 @@ public ResultList<Thread> list(FeedFilter filter, String link, int limitPosts, U
total = filteredThreads.getTotalCount();
} else {
// Only data assets are added as about
User user = userId != null ? Entity.getEntity(USER, userId, "teams", NON_DELETED) : null;
User user = userId != null ? Entity.getEntity(USER, userId, TEAMS_FIELD, NON_DELETED) : null;
List<String> teamNameHash = getTeamNames(user);
String userName = user == null ? null : user.getFullyQualifiedName();
List<String> jsons =
Expand Down Expand Up @@ -710,7 +711,7 @@ public final PatchResponse<Thread> patchThread(UriInfo uriInfo, UUID id, String

public void checkPermissionsForResolveTask(Thread thread, boolean closeTask, SecurityContext securityContext) {
String userName = securityContext.getUserPrincipal().getName();
User user = Entity.getEntityByName(USER, userName, "teams", NON_DELETED);
User user = Entity.getEntityByName(USER, userName, TEAMS_FIELD, NON_DELETED);
EntityLink about = EntityLink.parse(thread.getAbout());
EntityReference aboutRef = EntityUtil.validateEntityLink(about);
if (Boolean.TRUE.equals(user.getIsAdmin())) {
Expand Down Expand Up @@ -976,7 +977,7 @@ private FilteredThreads getThreadsByOwner(FeedFilter filter, UUID userId, int li

/** Returns the threads where the user or the team they belong to were mentioned by other users with @mention. */
private FilteredThreads getThreadsByMentions(FeedFilter filter, UUID userId, int limit) {
User user = Entity.getEntity(Entity.USER, userId, "teams", NON_DELETED);
User user = Entity.getEntity(Entity.USER, userId, TEAMS_FIELD, NON_DELETED);
String userNameHash = getUserNameHash(user);
// Return the threads where the user or team was mentioned
List<String> teamNamesHash = getTeamNames(user);
Expand All @@ -998,7 +999,7 @@ private FilteredThreads getThreadsByMentions(FeedFilter filter, UUID userId, int
private List<String> getTeamIds(UUID userId) {
List<String> teamIds = null;
if (userId != null) {
User user = Entity.getEntity(Entity.USER, userId, "teams", NON_DELETED);
User user = Entity.getEntity(Entity.USER, userId, TEAMS_FIELD, NON_DELETED);
teamIds = listOrEmpty(user.getTeams()).stream().map(ref -> ref.getId().toString()).collect(Collectors.toList());
}
return nullOrEmpty(teamIds) ? List.of(StringUtils.EMPTY) : teamIds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.openmetadata.service.Entity.TEST_CASE;
import static org.openmetadata.service.Entity.TEST_DEFINITION;
import static org.openmetadata.service.Entity.TEST_SUITE;
import static org.openmetadata.service.Entity.getEntityByName;
import static org.openmetadata.service.Entity.getEntityReferenceByName;
import static org.openmetadata.service.util.RestUtil.ENTITY_NO_CHANGE;
import static org.openmetadata.service.util.RestUtil.LOGICAL_TEST_CASES_ADDED;
Expand All @@ -19,12 +20,17 @@
import javax.ws.rs.core.UriInfo;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.api.feed.CloseTask;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.tests.ResultSummary;
import org.openmetadata.schema.tests.TestCase;
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.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.tests.type.TestCaseResult;
Expand All @@ -36,6 +42,7 @@
import org.openmetadata.schema.type.FieldChange;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TaskType;
import org.openmetadata.schema.utils.EntityInterfaceUtil;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.EntityNotFoundException;
Expand Down Expand Up @@ -588,6 +595,103 @@ public ResultList<TestCase> listBefore(
return getResultList(testCases, beforeCursor, afterCursor, total);
}

@Override
public FeedRepository.TaskWorkflow getTaskWorkflow(FeedRepository.ThreadContext threadContext) {
validateTaskThread(threadContext);
TaskType taskType = threadContext.getThread().getTask().getType();
if (EntityUtil.isTestCaseFailureResolutionTask(taskType)) {
return new TestCaseRepository.TestCaseFailureResolutionTaskWorkflow(threadContext);
}
return super.getTaskWorkflow(threadContext);
}

public static class TestCaseFailureResolutionTaskWorkflow extends FeedRepository.TaskWorkflow {
TestCaseResolutionStatusRepository testCaseResolutionStatusRepository;

TestCaseFailureResolutionTaskWorkflow(FeedRepository.ThreadContext threadContext) {
super(threadContext);
this.testCaseResolutionStatusRepository =
(TestCaseResolutionStatusRepository) Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
}

@Override
@Transaction
public TestCase performTask(String userName, ResolveTask resolveTask) {

// We need to get the latest test case resolution status to get the state id
TestCaseResolutionStatus latestTestCaseResolutionStatus =
testCaseResolutionStatusRepository.getLatestRecord(resolveTask.getTestCaseFQN());

if (latestTestCaseResolutionStatus == null) {
throw new EntityNotFoundException(
String.format("Failed to find test case resolution status for %s", resolveTask.getTestCaseFQN()));
}
User user = getEntityByName(Entity.USER, userName, "", Include.ALL);
TestCaseResolutionStatus testCaseResolutionStatus =
new TestCaseResolutionStatus()
.withId(UUID.randomUUID())
.withStateId(latestTestCaseResolutionStatus.getStateId())
.withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.withTestCaseResolutionStatusDetails(
new Resolved()
.withTestCaseFailureComment(resolveTask.getNewValue())
.withTestCaseFailureReason(resolveTask.getTestCaseFailureReason())
.withResolvedBy(user.getEntityReference()))
.withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(latestTestCaseResolutionStatus.getTestCaseReference())
.withUpdatedBy(user.getEntityReference());

Entity.getCollectionDAO()
.testCaseResolutionStatusTimeSeriesDao()
.insert(
testCaseResolutionStatus.getTestCaseReference().getFullyQualifiedName(),
Entity.TEST_CASE_RESOLUTION_STATUS,
JsonUtils.pojoToJson(testCaseResolutionStatus));
testCaseResolutionStatusRepository.postCreate(testCaseResolutionStatus);
return Entity.getEntity(testCaseResolutionStatus.getTestCaseReference(), "", Include.ALL);
}

@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) {
return;
}

if (latestTestCaseResolutionStatus
.getTestCaseResolutionStatusType()
.equals(TestCaseResolutionStatusTypes.Resolved)) {
// if the test case is already resolved then we'll return. We don't need to update the state
return;
}

User user = Entity.getEntityByName(Entity.USER, userName, "", Include.ALL);
User assignee = Entity.getEntityByName(Entity.USER, closeTask.getComment(), "", Include.ALL);
TestCaseResolutionStatus testCaseResolutionStatus =
new TestCaseResolutionStatus()
.withId(UUID.randomUUID())
.withStateId(latestTestCaseResolutionStatus.getStateId())
.withTimestamp(System.currentTimeMillis())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Assigned)
.withTestCaseResolutionStatusDetails(new Assigned().withAssignee(assignee.getEntityReference()))
.withUpdatedAt(System.currentTimeMillis())
.withTestCaseReference(latestTestCaseResolutionStatus.getTestCaseReference())
.withUpdatedBy(user.getEntityReference());
Entity.getCollectionDAO()
.testCaseResolutionStatusTimeSeriesDao()
.insert(
testCaseResolutionStatus.getTestCaseReference().getFullyQualifiedName(),
Entity.TEST_CASE_RESOLUTION_STATUS,
JsonUtils.pojoToJson(testCaseResolutionStatus));
testCaseResolutionStatusRepository.postCreate(testCaseResolutionStatus);
}
}

public class TestUpdater extends EntityUpdater {
public TestUpdater(TestCase original, TestCase updated, Operation operation) {
super(original, updated, operation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,30 @@
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.List;
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;
import org.openmetadata.schema.tests.TestCase;
import org.openmetadata.schema.tests.type.Assigned;
import org.openmetadata.schema.tests.type.Resolved;
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.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.resources.feeds.MessageParser;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.RestUtil;
Expand Down Expand Up @@ -73,4 +90,99 @@ private void validatePatchFields(TestCaseResolutionStatus updated, TestCaseResol
}
}
}

@Override
protected void postCreate(TestCaseResolutionStatus entity) {
super.postCreate(entity);
if (entity.getTestCaseResolutionStatusType() == TestCaseResolutionStatusTypes.Assigned) {
createAssignedTask(entity);
}
}

@Override
@Transaction
public TestCaseResolutionStatus createNewRecord(
TestCaseResolutionStatus recordEntity, String extension, String recordFQN) {
TestCaseResolutionStatus latestTestCaseFailure =
getLatestRecord(recordEntity.getTestCaseReference().getFullyQualifiedName());
recordEntity.setStateId(
((latestTestCaseFailure != null)
&& (latestTestCaseFailure.getTestCaseResolutionStatusType() != TestCaseResolutionStatusTypes.Resolved))
? latestTestCaseFailure.getStateId()
: 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

// 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());
}

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()));
}
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 =
new TaskDetails()
.withAssignees(assignees)
.withType(TaskType.RequestTestCaseFailureResolution)
.withStatus(TaskStatus.Open)
.withTestCaseResolutionStatusId(entity.getId());

MessageParser.EntityLink entityLink =
new MessageParser.EntityLink(Entity.TEST_CASE, entity.getTestCaseReference().getFullyQualifiedName());
Thread thread =
new Thread()
.withId(UUID.randomUUID())
.withThreadTs(System.currentTimeMillis())
.withMessage("Test Case Failure Resolution requested for ")
.withCreatedBy(entity.getUpdatedBy().getName())
.withAbout(entityLink.getLinkString())
.withType(ThreadType.Task)
.withTask(taskDetails)
.withUpdatedBy(entity.getUpdatedBy().getName())
.withUpdatedAt(System.currentTimeMillis());
FeedRepository feedRepository = Entity.getFeedRepository();
feedRepository.create(thread);
}
}
Loading
Loading