diff --git a/gitops/snapshot_integration_tests_status.go b/gitops/snapshot_integration_tests_status.go index caa5938f9..a3585da34 100644 --- a/gitops/snapshot_integration_tests_status.go +++ b/gitops/snapshot_integration_tests_status.go @@ -46,7 +46,13 @@ const integrationTestStatusesSchema = `{ }, "details": { "type": "string" - } + }, + "startTime": { + "type": "string" + }, + "completionTime": { + "type": "string" + } }, "required": ["scenario", "status", "lastUpdateTime"] } @@ -62,6 +68,10 @@ type IntegrationTestStatusDetail struct { LastUpdateTime time.Time `json:"lastUpdateTime"` // The details of reported status Details string `json:"details"` + // Startime when we moved to inProgress + StartTime *time.Time `json:"startTime,omitempty"` // pointer to make omitempty work + // Completion time when test failed or passed + CompletionTime *time.Time `json:"completionTime,omitempty"` // pointer to make omitempty work } // SnapshotIntegrationTestStatuses type handles details about snapshot tests @@ -86,25 +96,49 @@ func (sits *SnapshotIntegrationTestStatuses) ResetDirty() { // UpdateTestStatusIfChanged updates status of scenario test when status or details changed func (sits *SnapshotIntegrationTestStatuses) UpdateTestStatusIfChanged(scenarioName string, status IntegrationTestStatus, details string) { + var detail *IntegrationTestStatusDetail detail, ok := sits.statuses[scenarioName] timestamp := time.Now().UTC() - if ok { - // update only when status or details changed, otherwise it's a no-op - // to preserve timestamps - if detail.Status != status || detail.Details != details { - detail.Status = status - detail.Details = details - detail.LastUpdateTime = timestamp - sits.dirty = true - } - } else { + if !ok { newDetail := IntegrationTestStatusDetail{ ScenarioName: scenarioName, - Status: status, + Status: -1, // undefined, must be udpated within function Details: details, LastUpdateTime: timestamp, } - sits.statuses[scenarioName] = &newDetail + detail = &newDetail + sits.statuses[scenarioName] = detail + sits.dirty = true + } + + // update only when status or details changed, otherwise it's a no-op + // to preserve timestamps + if detail.Status != status { + detail.Status = status + detail.LastUpdateTime = timestamp + sits.dirty = true + + // update start and completion time if needed, only when status changed + switch status { + case IntegrationTestStatusInProgress: + detail.StartTime = ×tamp + // null CompletionTime because testing started again + detail.CompletionTime = nil + case IntegrationTestStatusPending: + // null all timestamps as test is not inProgress neither in final state + detail.StartTime = nil + detail.CompletionTime = nil + case IntegrationTestStatusDeploymentError, + IntegrationTestStatusEnvironmentProvisionError, + IntegrationTestStatusTestFail, + IntegrationTestStatusTestPassed: + detail.CompletionTime = ×tamp + } + } + + if detail.Details != details { + detail.Details = details + detail.LastUpdateTime = timestamp sits.dirty = true } @@ -166,17 +200,17 @@ func (sits *SnapshotIntegrationTestStatuses) GetScenarioStatus(scenarioName stri // MarshalJSON converts data to JSON // Please note that internal representation of data differs from marshalled output // Example: -// [ // -// { -// "scenario": "scenario-1", -// "status": "EnvironmentProvisionError", -// "lastUpdateTime": "2023-07-26T16:57:49+02:00", -// "details": "Failed ..." -// } -// -// ] - +// [ +// { +// "scenario": "scenario-1", +// "status": "EnvironmentProvisionError", +// "lastUpdateTime": "2023-07-26T16:57:49+02:00", +// "details": "Failed ...", +// "startTime": "2023-07-26T14:57:49+02:00", +// "completionTime": "2023-07-26T16:57:49+02:00" +// } +// ] func (sits *SnapshotIntegrationTestStatuses) MarshalJSON() ([]byte, error) { result := sits.GetStatuses() return json.Marshal(result) diff --git a/gitops/snapshot_integration_tests_status_test.go b/gitops/snapshot_integration_tests_status_test.go index 7fdd477c8..09ac3c291 100644 --- a/gitops/snapshot_integration_tests_status_test.go +++ b/gitops/snapshot_integration_tests_status_test.go @@ -151,7 +151,76 @@ var _ = Describe("Snapshot integration test statuses", func() { Expect(detail.Status).To(Equal(gitops.IntegrationTestStatusPending)) }) - It("Can export valid JSON", func() { + DescribeTable("Test expected additons of startTime", + func(st gitops.IntegrationTestStatus, shouldAdd bool) { + sits.UpdateTestStatusIfChanged(testScenarioName, st, testDetails) + detail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + if shouldAdd { + Expect(detail.StartTime).NotTo(BeNil()) + } else { + Expect(detail.StartTime).To(BeNil()) + } + }, + Entry("When status is Pending", gitops.IntegrationTestStatusPending, false), + Entry("When status is InProgress", gitops.IntegrationTestStatusInProgress, true), + Entry("When status is EnvironmentProvisionError", gitops.IntegrationTestStatusEnvironmentProvisionError, false), + Entry("When status is DeploymentError", gitops.IntegrationTestStatusDeploymentError, false), + Entry("When status is TestFail", gitops.IntegrationTestStatusTestFail, false), + Entry("When status is TestPass", gitops.IntegrationTestStatusTestPassed, false), + ) + + DescribeTable("Test expected additons of completionTime", + func(st gitops.IntegrationTestStatus, shouldAdd bool) { + sits.UpdateTestStatusIfChanged(testScenarioName, st, testDetails) + detail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + if shouldAdd { + Expect(detail.CompletionTime).NotTo(BeNil()) + } else { + Expect(detail.CompletionTime).To(BeNil()) + } + }, + Entry("When status is Pending", gitops.IntegrationTestStatusPending, false), + Entry("When status is InProgress", gitops.IntegrationTestStatusInProgress, false), + Entry("When status is EnvironmentProvisionError", gitops.IntegrationTestStatusEnvironmentProvisionError, true), + Entry("When status is DeploymentError", gitops.IntegrationTestStatusDeploymentError, true), + Entry("When status is TestFail", gitops.IntegrationTestStatusTestFail, true), + Entry("When status is TestPass", gitops.IntegrationTestStatusTestPassed, true), + ) + + It("Change back to InProgress updates timestamps accordingly", func() { + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusTestPassed, testDetails) + originalDetail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + Expect(originalDetail.CompletionTime).ToNot(BeNil()) + originalStartTime := originalDetail.StartTime // copy time, it's all in pointers + + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusInProgress, testDetails) + newDetail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + Expect(originalDetail.CompletionTime).To(BeNil()) + Expect(newDetail.StartTime).NotTo(Equal(originalStartTime)) + }) + + It("not changing status keeps starting time the same", func() { + newDetails := "something important" + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusInProgress, testDetails) + originalDetail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + originalStartTime := originalDetail.StartTime // copy time, it's all in pointers + originalLastUpdateTime := originalDetail.LastUpdateTime + + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusInProgress, newDetails) + newDetail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + Expect(newDetail.StartTime).To(Equal(originalStartTime)) + // but details and lastUpdateTimestamp must changed + Expect(newDetail.Details).To(Equal(newDetails)) + Expect(newDetail.LastUpdateTime).NotTo(Equal(originalLastUpdateTime)) + }) + + It("Can export valid JSON without start and completion time (Pending)", func() { sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusPending, testDetails) detail, ok := sits.GetScenarioStatus(testScenarioName) Expect(ok).To(BeTrue()) @@ -172,6 +241,85 @@ var _ = Describe("Snapshot integration test statuses", func() { Expect(json.Marshal(sits)).To(MatchJSON(expected)) }) + It("Can export valid JSON with start time (InProgress)", func() { + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusInProgress, testDetails) + + detail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + + expectedFormatStr := `[ + { + "scenario": "%s", + "status": "InProgress", + "lastUpdateTime": "%s", + "details": "%s", + "startTime": "%s" + } + ]` + marshaledTime, err := detail.LastUpdateTime.MarshalText() + Expect(err).To(BeNil()) + marshaledStartTime, err := detail.StartTime.MarshalText() + Expect(err).To(BeNil()) + expectedStr := fmt.Sprintf(expectedFormatStr, testScenarioName, marshaledTime, testDetails, marshaledStartTime) + expected := []byte(expectedStr) + + Expect(json.Marshal(sits)).To(MatchJSON(expected)) + }) + + It("Can export valid JSON with completion time (TestFailed)", func() { + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusTestFail, testDetails) + + detail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + + expectedFormatStr := `[ + { + "scenario": "%s", + "status": "TestFail", + "lastUpdateTime": "%s", + "details": "%s", + "completionTime": "%s" + } + ]` + marshaledTime, err := detail.LastUpdateTime.MarshalText() + Expect(err).To(BeNil()) + marshaledCompletionTime, err := detail.CompletionTime.MarshalText() + Expect(err).To(BeNil()) + expectedStr := fmt.Sprintf(expectedFormatStr, testScenarioName, marshaledTime, testDetails, marshaledCompletionTime) + expected := []byte(expectedStr) + + Expect(json.Marshal(sits)).To(MatchJSON(expected)) + }) + + It("Can export valid JSON with startTime and completion time", func() { + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusInProgress, "yolo") + sits.UpdateTestStatusIfChanged(testScenarioName, gitops.IntegrationTestStatusTestFail, testDetails) + + detail, ok := sits.GetScenarioStatus(testScenarioName) + Expect(ok).To(BeTrue()) + + expectedFormatStr := `[ + { + "scenario": "%s", + "status": "TestFail", + "lastUpdateTime": "%s", + "details": "%s", + "startTime": "%s", + "completionTime": "%s" + } + ]` + marshaledTime, err := detail.LastUpdateTime.MarshalText() + Expect(err).To(BeNil()) + marshaledStartTime, err := detail.StartTime.MarshalText() + Expect(err).To(BeNil()) + marshaledCompletionTime, err := detail.CompletionTime.MarshalText() + Expect(err).To(BeNil()) + expectedStr := fmt.Sprintf(expectedFormatStr, testScenarioName, marshaledTime, testDetails, marshaledStartTime, marshaledCompletionTime) + expected := []byte(expectedStr) + + Expect(json.Marshal(sits)).To(MatchJSON(expected)) + }) + When("Contains updates to status", func() { BeforeEach(func() { @@ -373,11 +521,18 @@ var _ = Describe("Snapshot integration test statuses", func() { // fetch updated snapshot Eventually(func() error { - err := k8sClient.Get(ctx, types.NamespacedName{ + if err := k8sClient.Get(ctx, types.NamespacedName{ Name: snapshot.Name, Namespace: namespace, - }, snapshot) - return err + }, snapshot); err != nil { + return err + } + // race condition, sometimes it fetched old object + annotations := snapshot.GetAnnotations() + if _, ok := annotations[gitops.SnapshotTestsStatusAnnotation]; ok != true { + return fmt.Errorf("Snapshot doesn't contain the expected annotation") + } + return nil }, time.Second*10).ShouldNot(HaveOccurred()) statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(snapshot)