From fdf7d2581b64871915109e05c60a988190f21de8 Mon Sep 17 00:00:00 2001 From: Zackary Santana <64446617+ZackarySantana@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:53:12 -0400 Subject: [PATCH] DEVPROD-4608 Add stepback to the metadata of tasks (#2299) --- src/components/MetadataCard.tsx | 11 +- src/hooks/useBreakingTask/index.ts | 67 ++++ .../useBreakingTask/useBreakingTask.test.tsx | 240 +++++++++++++ src/hooks/useLastExecutedTask/index.ts | 61 ++++ .../useLastExecutedTask.test.tsx | 207 ++++++++++++ src/hooks/useLastPassingTask/index.ts | 59 ++++ .../useLastPassingTask.test.tsx | 187 ++++++++++ src/hooks/useParentTask/index.ts | 53 +++ .../useParentTask/useParentTask.test.tsx | 83 +++++ .../ConfigurePatchCore.stories.storyshot | 6 +- src/pages/task/ActionButtons.tsx | 2 +- .../RelevantCommits/RelevantCommits.test.tsx | 319 ++++++++++-------- .../actionButtons/RelevantCommits/index.tsx | 136 ++------ src/pages/task/metadata/Stepback.tsx | 69 ++++ .../__snapshots__/Metadata.stories.storyshot | 82 ++--- src/pages/task/metadata/index.tsx | 64 +--- .../ParametersModal.stories.storyshot | 2 +- src/utils/getTaskFromMainlineCommitsQuery.ts | 26 ++ 18 files changed, 1310 insertions(+), 364 deletions(-) create mode 100644 src/hooks/useBreakingTask/index.ts create mode 100644 src/hooks/useBreakingTask/useBreakingTask.test.tsx create mode 100644 src/hooks/useLastExecutedTask/index.ts create mode 100644 src/hooks/useLastExecutedTask/useLastExecutedTask.test.tsx create mode 100644 src/hooks/useLastPassingTask/index.ts create mode 100644 src/hooks/useLastPassingTask/useLastPassingTask.test.tsx create mode 100644 src/hooks/useParentTask/index.ts create mode 100644 src/hooks/useParentTask/useParentTask.test.tsx create mode 100644 src/pages/task/metadata/Stepback.tsx create mode 100644 src/utils/getTaskFromMainlineCommitsQuery.ts diff --git a/src/components/MetadataCard.tsx b/src/components/MetadataCard.tsx index c6bbc9031c..0c31f2f6b9 100644 --- a/src/components/MetadataCard.tsx +++ b/src/components/MetadataCard.tsx @@ -1,5 +1,6 @@ import { ApolloError } from "@apollo/client"; import styled from "@emotion/styled"; +import { PolymorphicAs } from "@leafygreen-ui/polymorphic"; import { Body, BodyProps } from "@leafygreen-ui/typography"; import { Skeleton } from "antd"; import { ErrorWrapper } from "components/ErrorWrapper"; @@ -39,14 +40,20 @@ export const MetadataTitle: React.FC<{ children: React.ReactNode }> = ({ ); interface ItemProps { + as?: PolymorphicAs; children: React.ReactNode; "data-cy"?: string; } export const MetadataItem: React.FC = ({ + as = "p", children, "data-cy": dataCy, -}) => {children}; +}) => ( + + {children} + +); const Title = styled(Body)` font-size: 15px; @@ -63,7 +70,7 @@ const Item = styled(Body)` line-height: 14px; } - :not(:last-of-type) { + :not(:last-child) { margin-bottom: 12px; } `; diff --git a/src/hooks/useBreakingTask/index.ts b/src/hooks/useBreakingTask/index.ts new file mode 100644 index 0000000000..535b69d1b2 --- /dev/null +++ b/src/hooks/useBreakingTask/index.ts @@ -0,0 +1,67 @@ +import { useQuery } from "@apollo/client"; +import { useToastContext } from "context/toast"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { useLastPassingTask } from "hooks/useLastPassingTask"; +import { useParentTask } from "hooks/useParentTask"; +import { TaskStatus } from "types/task"; +import { string } from "utils"; +import { getTaskFromMainlineCommitsQuery } from "utils/getTaskFromMainlineCommitsQuery"; +import { isFailedTaskStatus } from "utils/statuses"; + +export const useBreakingTask = (taskId: string) => { + const dispatchToast = useToastContext(); + + const { data: taskData } = useQuery< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables + >(BASE_VERSION_AND_TASK, { + variables: { taskId }, + }); + + const { buildVariant, displayName, projectIdentifier, status } = + taskData?.task ?? {}; + + const bvOptionsBase = { + tasks: [string.applyStrictRegex(displayName)], + variants: [string.applyStrictRegex(buildVariant)], + }; + + const { task: parentTask } = useParentTask(taskId); + + const { task: lastPassingTask } = useLastPassingTask(taskId); + const passingOrderNumber = lastPassingTask?.order; + + // The breaking commit is the first failing commit after the last passing commit. + // The skip order number should be the last passing commit's order number + 1. + // We use + 2 because internally the query does a less than comparison. + // https://github.com/evergreen-ci/evergreen/blob/f6751ac3194452d457c0a6fe1a9f9b30dd674c60/model/version.go#L518 + const { data: breakingTaskData, loading } = useQuery< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables + >(LAST_MAINLINE_COMMIT, { + skip: !parentTask || !lastPassingTask || !isFailedTaskStatus(status), + variables: { + projectIdentifier, + skipOrderNumber: passingOrderNumber + 2, + buildVariantOptions: { + ...bvOptionsBase, + statuses: [TaskStatus.Failed], + }, + }, + onError: (err) => { + dispatchToast.error(`Breaking commit unavailable: '${err.message}'`); + }, + }); + const task = getTaskFromMainlineCommitsQuery(breakingTaskData); + + return { + task, + loading, + }; +}; diff --git a/src/hooks/useBreakingTask/useBreakingTask.test.tsx b/src/hooks/useBreakingTask/useBreakingTask.test.tsx new file mode 100644 index 0000000000..6c5b29e610 --- /dev/null +++ b/src/hooks/useBreakingTask/useBreakingTask.test.tsx @@ -0,0 +1,240 @@ +import { MockedProvider, MockedProviderProps } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { renderHook, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; +import { useBreakingTask } from "."; + +interface ProviderProps { + mocks?: MockedProviderProps["mocks"]; + children: React.ReactNode; +} +const ProviderWrapper: React.FC = ({ children, mocks = [] }) => ( + {children} +); + +describe("useBreakingTask", () => { + it("no breaking task is found when task is not found", () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useBreakingTask("t1"), { + wrapper: ({ children }) => ProviderWrapper({ children }), + }); + + expect(result.current.task).toBeUndefined(); + + // No error is dispatched when the task is not found. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a breaking task is found when there is a previous failing task", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useBreakingTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [ + getPatchTaskWithFailingBaseTask, + getLastPassingVersion, + getBreakingCommit, + ], + }), + }); + + await waitFor(() => { + expect(result.current.task).toBeDefined(); + }); + + expect(result.current.task.id).toBe("breaking_commit"); + + // No error is dispatched for success scenarios. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a breaking task is not found due to an error in the query and a toast is dispatched", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useBreakingTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [ + getPatchTaskWithFailingBaseTask, + getLastPassingVersion, + getBreakingCommitWithError, + ], + }), + }); + + await waitFor(() => { + // An error is dispatched when the query fails. + expect(dispatchToast.error).toHaveBeenCalledTimes(1); + }); + + expect(result.current.task).toBeUndefined(); + }); +}); + +const baseTaskId = + "evergreen_lint_lint_agent_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_21_11_29_17_55_27"; + +const getPatchTaskWithFailingBaseTask: ApolloMock< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables +> = { + request: { + query: BASE_VERSION_AND_TASK, + variables: { + taskId: "t1", + }, + }, + result: { + data: { + task: { + id: "evergreen_lint_lint_agent_patch_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_61a8edf132f41750ab47bc72_21_12_02_16_01_54", + execution: 0, + displayName: "lint-agent", + buildVariant: "lint", + projectIdentifier: "evergreen", + status: "failed", + versionMetadata: { + baseVersion: { + id: "baseVersion", + order: 3676, + __typename: "Version", + }, + isPatch: true, + id: "versionMetadataId", + __typename: "Version", + }, + baseTask: { + id: baseTaskId, + execution: 0, + status: "failed", + __typename: "Task", + }, + __typename: "Task", + }, + }, + }, +}; + +const getLastPassingVersion: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: ["success"], + }, + }, + }, + result: { + data: { + mainlineCommits: { + versions: [ + { + version: { + id: "evergreen_44110b57c6977bf3557009193628c9389772163f", + buildVariants: [ + { + tasks: [ + { + id: "last_passing_task", + execution: 0, + order: 3674, + status: "success", + __typename: "Task", + }, + ], + __typename: "GroupedBuildVariant", + }, + ], + __typename: "Version", + }, + __typename: "MainlineCommitVersion", + }, + ], + __typename: "MainlineCommits", + }, + }, + }, +}; + +const getBreakingCommit: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: ["failed"], + }, + }, + }, + result: { + data: { + mainlineCommits: { + versions: [ + { + version: { + id: "evergreen_44110b57c6977bf3557009193628c9389772163f2", + buildVariants: [ + { + tasks: [ + { + id: "breaking_commit", + execution: 0, + order: 3676, + status: "failed", + __typename: "Task", + }, + ], + __typename: "GroupedBuildVariant", + }, + ], + __typename: "Version", + }, + __typename: "MainlineCommitVersion", + }, + ], + __typename: "MainlineCommits", + }, + }, + }, +}; + +const getBreakingCommitWithError: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: ["failed"], + }, + }, + }, + error: new Error("Matching task not found!"), +}; diff --git a/src/hooks/useLastExecutedTask/index.ts b/src/hooks/useLastExecutedTask/index.ts new file mode 100644 index 0000000000..471c5719d7 --- /dev/null +++ b/src/hooks/useLastExecutedTask/index.ts @@ -0,0 +1,61 @@ +import { useQuery } from "@apollo/client"; +import { finishedTaskStatuses } from "constants/task"; +import { useToastContext } from "context/toast"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { useParentTask } from "hooks/useParentTask"; +import { string } from "utils"; +import { getTaskFromMainlineCommitsQuery } from "utils/getTaskFromMainlineCommitsQuery"; +import { isFinishedTaskStatus } from "utils/statuses"; + +export const useLastExecutedTask = (taskId: string) => { + const dispatchToast = useToastContext(); + const { data: taskData } = useQuery< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables + >(BASE_VERSION_AND_TASK, { + variables: { taskId }, + }); + + const { buildVariant, displayName, projectIdentifier, versionMetadata } = + taskData?.task ?? {}; + const { order: skipOrderNumber } = versionMetadata?.baseVersion ?? {}; + + const bvOptionsBase = { + tasks: [string.applyStrictRegex(displayName)], + variants: [string.applyStrictRegex(buildVariant)], + }; + + const { task: parentTask } = useParentTask(taskId); + + const { data: lastExecutedTaskData, loading } = useQuery< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables + >(LAST_MAINLINE_COMMIT, { + skip: !parentTask || isFinishedTaskStatus(parentTask.status), + variables: { + projectIdentifier, + skipOrderNumber, + buildVariantOptions: { + ...bvOptionsBase, + statuses: finishedTaskStatuses, + }, + }, + onError: (err) => { + dispatchToast.error( + `Could not fetch last task execution: '${err.message}'`, + ); + }, + }); + const task = getTaskFromMainlineCommitsQuery(lastExecutedTaskData); + + return { + task, + loading, + }; +}; diff --git a/src/hooks/useLastExecutedTask/useLastExecutedTask.test.tsx b/src/hooks/useLastExecutedTask/useLastExecutedTask.test.tsx new file mode 100644 index 0000000000..df63abfc0d --- /dev/null +++ b/src/hooks/useLastExecutedTask/useLastExecutedTask.test.tsx @@ -0,0 +1,207 @@ +import { MockedProvider, MockedProviderProps } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { renderHook, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; +import { useLastExecutedTask } from "."; + +interface ProviderProps { + mocks?: MockedProviderProps["mocks"]; + children: React.ReactNode; +} +const ProviderWrapper: React.FC = ({ children, mocks = [] }) => ( + {children} +); + +describe("useLastExecutedTask", () => { + it("no last executed task is found when task is not found", () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastExecutedTask("t1"), { + wrapper: ({ children }) => ProviderWrapper({ children }), + }); + + expect(result.current.task).toBeUndefined(); + + // No error is dispatched when the task is not found. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a last executed task is found", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastExecutedTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [getPatchTaskWithRunningBaseTask, getLastExecutedVersion], + }), + }); + + await waitFor(() => { + expect(result.current.task).toBeDefined(); + }); + + expect(result.current.task.id).toBe("last_executed_task"); + + // No error is dispatched for success scenarios. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a last executed task is not found due to an error in the query and a toast is dispatched", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastExecutedTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [ + getPatchTaskWithRunningBaseTask, + getLastExecutedVersionWithError, + ], + }), + }); + + await waitFor(() => { + // An error is dispatched when the query fails. + expect(dispatchToast.error).toHaveBeenCalledTimes(1); + }); + + expect(result.current.task).toBeUndefined(); + }); +}); + +const baseTaskId = + "evergreen_lint_lint_agent_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_21_11_29_17_55_27"; + +const getPatchTaskWithRunningBaseTask: ApolloMock< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables +> = { + request: { + query: BASE_VERSION_AND_TASK, + variables: { + taskId: "t1", + }, + }, + result: { + data: { + task: { + id: "evergreen_lint_lint_agent_patch_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_61a8edf132f41750ab47bc72_21_12_02_16_01_54", + execution: 0, + displayName: "lint-agent", + buildVariant: "lint", + projectIdentifier: "evergreen", + status: "started", + versionMetadata: { + baseVersion: { + id: "baseVersion", + order: 3676, + __typename: "Version", + }, + isPatch: true, + id: "versionMetadataId", + __typename: "Version", + }, + baseTask: { + id: baseTaskId, + execution: 0, + status: "started", + __typename: "Task", + }, + __typename: "Task", + }, + }, + }, +}; + +const getLastExecutedVersion: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: [ + "failed", + "setup-failed", + "system-failed", + "task-timed-out", + "test-timed-out", + "known-issue", + "system-unresponsive", + "system-timed-out", + "success", + ], + }, + }, + }, + result: { + data: { + mainlineCommits: { + versions: [ + { + version: { + id: "evergreen_44110b57c6977bf3557009193628c9389772163f", + buildVariants: [ + { + tasks: [ + { + id: "last_executed_task", + execution: 0, + order: 3676, + status: "failed", + __typename: "Task", + }, + ], + __typename: "GroupedBuildVariant", + }, + ], + __typename: "Version", + }, + __typename: "MainlineCommitVersion", + }, + ], + __typename: "MainlineCommits", + }, + }, + }, +}; + +const getLastExecutedVersionWithError: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: [ + "failed", + "setup-failed", + "system-failed", + "task-timed-out", + "test-timed-out", + "known-issue", + "system-unresponsive", + "system-timed-out", + "success", + ], + }, + }, + }, + error: new Error("Matching task not found!"), +}; diff --git a/src/hooks/useLastPassingTask/index.ts b/src/hooks/useLastPassingTask/index.ts new file mode 100644 index 0000000000..da62d64df2 --- /dev/null +++ b/src/hooks/useLastPassingTask/index.ts @@ -0,0 +1,59 @@ +import { useQuery } from "@apollo/client"; +import { useToastContext } from "context/toast"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { useParentTask } from "hooks/useParentTask"; +import { TaskStatus } from "types/task"; +import { string } from "utils"; +import { getTaskFromMainlineCommitsQuery } from "utils/getTaskFromMainlineCommitsQuery"; + +export const useLastPassingTask = (taskId: string) => { + const dispatchToast = useToastContext(); + + const { data: taskData } = useQuery< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables + >(BASE_VERSION_AND_TASK, { + variables: { taskId }, + }); + + const { buildVariant, displayName, projectIdentifier, versionMetadata } = + taskData?.task ?? {}; + const { order: skipOrderNumber } = versionMetadata?.baseVersion ?? {}; + + const bvOptionsBase = { + tasks: [string.applyStrictRegex(displayName)], + variants: [string.applyStrictRegex(buildVariant)], + }; + + const { task: parentTask } = useParentTask(taskId); + + const { data: lastPassingTaskData, loading } = useQuery< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables + >(LAST_MAINLINE_COMMIT, { + skip: !parentTask || parentTask.status === TaskStatus.Succeeded, + variables: { + projectIdentifier, + skipOrderNumber, + buildVariantOptions: { + ...bvOptionsBase, + statuses: [TaskStatus.Succeeded], + }, + }, + onError: (err) => { + dispatchToast.error(`Last passing version unavailable: '${err.message}'`); + }, + }); + const task = getTaskFromMainlineCommitsQuery(lastPassingTaskData); + + return { + task, + loading, + }; +}; diff --git a/src/hooks/useLastPassingTask/useLastPassingTask.test.tsx b/src/hooks/useLastPassingTask/useLastPassingTask.test.tsx new file mode 100644 index 0000000000..d1cd86f59b --- /dev/null +++ b/src/hooks/useLastPassingTask/useLastPassingTask.test.tsx @@ -0,0 +1,187 @@ +import { MockedProvider, MockedProviderProps } from "@apollo/client/testing"; +import { RenderFakeToastContext } from "context/toast/__mocks__"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { renderHook, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; +import { useLastPassingTask } from "."; + +interface ProviderProps { + mocks?: MockedProviderProps["mocks"]; + children: React.ReactNode; +} +const ProviderWrapper: React.FC = ({ children, mocks = [] }) => ( + {children} +); + +describe("useLastPassingTask", () => { + it("no last passing task is found when task is not found", () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastPassingTask("t1"), { + wrapper: ({ children }) => ProviderWrapper({ children }), + }); + + expect(result.current.task).toBeUndefined(); + + // No error is dispatched when the task is not found. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a last passing task is found", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastPassingTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [getPatchTaskWithFailingBaseTask, getLastPassingVersion], + }), + }); + + await waitFor(() => { + expect(result.current.task).toBeDefined(); + }); + + expect(result.current.task.id).toBe("last_passing_task"); + + // No error is dispatched for success scenarios. + expect(dispatchToast.error).toHaveBeenCalledTimes(0); + }); + it("a last passing task is not found due to an error in the query and a toast is dispatched", async () => { + const { dispatchToast } = RenderFakeToastContext(); + + const { result } = renderHook(() => useLastPassingTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [ + getPatchTaskWithFailingBaseTask, + getLastPassingVersionWithError, + ], + }), + }); + + await waitFor(() => { + // An error is dispatched when the query fails. + expect(dispatchToast.error).toHaveBeenCalledTimes(1); + }); + + expect(result.current.task).toBeUndefined(); + }); +}); + +const baseTaskId = + "evergreen_lint_lint_agent_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_21_11_29_17_55_27"; + +const getPatchTaskWithFailingBaseTask: ApolloMock< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables +> = { + request: { + query: BASE_VERSION_AND_TASK, + variables: { + taskId: "t1", + }, + }, + result: { + data: { + task: { + id: "evergreen_lint_lint_agent_patch_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_61a8edf132f41750ab47bc72_21_12_02_16_01_54", + execution: 0, + displayName: "lint-agent", + buildVariant: "lint", + projectIdentifier: "evergreen", + status: "failed", + versionMetadata: { + baseVersion: { + id: "baseVersion", + order: 3676, + __typename: "Version", + }, + isPatch: true, + id: "versionMetadataId", + __typename: "Version", + }, + baseTask: { + id: baseTaskId, + execution: 0, + status: "failed", + __typename: "Task", + }, + __typename: "Task", + }, + }, + }, +}; + +const getLastPassingVersion: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: ["success"], + }, + }, + }, + result: { + data: { + mainlineCommits: { + versions: [ + { + version: { + id: "evergreen_44110b57c6977bf3557009193628c9389772163f", + buildVariants: [ + { + tasks: [ + { + id: "last_passing_task", + execution: 0, + order: 3674, + status: "success", + __typename: "Task", + }, + ], + __typename: "GroupedBuildVariant", + }, + ], + __typename: "Version", + }, + __typename: "MainlineCommitVersion", + }, + ], + __typename: "MainlineCommits", + }, + }, + }, +}; + +const getLastPassingVersionWithError: ApolloMock< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables +> = { + request: { + query: LAST_MAINLINE_COMMIT, + variables: { + projectIdentifier: "evergreen", + skipOrderNumber: 3676, + buildVariantOptions: { + tasks: ["^lint-agent$"], + variants: ["^lint$"], + statuses: ["success"], + }, + }, + }, + error: new Error("Matching task not found!"), +}; diff --git a/src/hooks/useParentTask/index.ts b/src/hooks/useParentTask/index.ts new file mode 100644 index 0000000000..dc36b7ee8e --- /dev/null +++ b/src/hooks/useParentTask/index.ts @@ -0,0 +1,53 @@ +import { useQuery } from "@apollo/client"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK, LAST_MAINLINE_COMMIT } from "gql/queries"; +import { string } from "utils"; +import { getTaskFromMainlineCommitsQuery } from "utils/getTaskFromMainlineCommitsQuery"; + +export const useParentTask = (taskId: string) => { + const { data: taskData } = useQuery< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables + >(BASE_VERSION_AND_TASK, { + variables: { taskId }, + }); + + const { + baseTask, + buildVariant, + displayName, + projectIdentifier, + versionMetadata, + } = taskData?.task ?? {}; + const { order: skipOrderNumber } = versionMetadata?.baseVersion ?? {}; + + const bvOptionsBase = { + tasks: [string.applyStrictRegex(displayName)], + variants: [string.applyStrictRegex(buildVariant)], + }; + + const { data: parentTaskData, loading } = useQuery< + LastMainlineCommitQuery, + LastMainlineCommitQueryVariables + >(LAST_MAINLINE_COMMIT, { + skip: !versionMetadata || versionMetadata.isPatch, + variables: { + projectIdentifier, + skipOrderNumber, + buildVariantOptions: { + ...bvOptionsBase, + }, + }, + }); + const task = getTaskFromMainlineCommitsQuery(parentTaskData); + + return { + task: task ?? baseTask, + loading, + }; +}; diff --git a/src/hooks/useParentTask/useParentTask.test.tsx b/src/hooks/useParentTask/useParentTask.test.tsx new file mode 100644 index 0000000000..b6a7416356 --- /dev/null +++ b/src/hooks/useParentTask/useParentTask.test.tsx @@ -0,0 +1,83 @@ +import { MockedProvider, MockedProviderProps } from "@apollo/client/testing"; +import { + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables, +} from "gql/generated/types"; +import { BASE_VERSION_AND_TASK } from "gql/queries"; +import { renderHook, waitFor } from "test_utils"; +import { ApolloMock } from "types/gql"; +import { useParentTask } from "."; + +interface ProviderProps { + mocks?: MockedProviderProps["mocks"]; + children: React.ReactNode; +} +const ProviderWrapper: React.FC = ({ children, mocks = [] }) => ( + {children} +); + +describe("useParentTask", () => { + it("no parent task is found when task is not found", () => { + const { result } = renderHook(() => useParentTask("t1"), { + wrapper: ({ children }) => ProviderWrapper({ children }), + }); + + expect(result.current.task).toBeUndefined(); + }); + it("a parent task is found", async () => { + const { result } = renderHook(() => useParentTask("t1"), { + wrapper: ({ children }) => + ProviderWrapper({ + children, + mocks: [getPatchTaskWithFailingBaseTask], + }), + }); + + await waitFor(() => { + expect(result.current.task).toBeDefined(); + }); + + expect(result.current.task.id).toBe("task"); + }); +}); + +const getPatchTaskWithFailingBaseTask: ApolloMock< + BaseVersionAndTaskQuery, + BaseVersionAndTaskQueryVariables +> = { + request: { + query: BASE_VERSION_AND_TASK, + variables: { + taskId: "t1", + }, + }, + result: { + data: { + task: { + id: "evergreen_lint_lint_agent_patch_f4fe4814088e13b8ef423a73d65a6e0a5579cf93_61a8edf132f41750ab47bc72_21_12_02_16_01_54", + execution: 0, + displayName: "lint-agent", + buildVariant: "lint", + projectIdentifier: "evergreen", + status: "failed", + versionMetadata: { + baseVersion: { + id: "baseVersion", + order: 3676, + __typename: "Version", + }, + isPatch: true, + id: "versionMetadataId", + __typename: "Version", + }, + baseTask: { + id: "task", + execution: 0, + status: "failed", + __typename: "Task", + }, + __typename: "Task", + }, + }, + }, +}; diff --git a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot index a7ba0b691b..fba98a871a 100644 --- a/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot +++ b/src/pages/configurePatch/configurePatchCore/__snapshots__/ConfigurePatchCore.stories.storyshot @@ -80,19 +80,19 @@ exports[`Snapshot Tests pages/configurePatch/configurePatchCore ConfigureTasksDe />

Submitted by: mohamed.khelif

Submitted at: 2020-08-28T15:00:17Z

Project: diff --git a/src/pages/task/ActionButtons.tsx b/src/pages/task/ActionButtons.tsx index 48b7592925..0e190b8122 100644 --- a/src/pages/task/ActionButtons.tsx +++ b/src/pages/task/ActionButtons.tsx @@ -254,7 +254,7 @@ export const ActionButtons: React.FC = ({ {!isExecutionTask && ( <> - +