Skip to content

Commit

Permalink
feat(STONEINTG-999): report test status for group snapshot
Browse files Browse the repository at this point in the history
* Update EnsureSnapshotTestStatusReportedToGitProvider to support
  group snapshot

Signed-off-by: Hongwei Liu <hongliu@redhat.com>
  • Loading branch information
hongweiliu17 committed Sep 18, 2024
1 parent 197c47b commit ca8daa1
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 208 deletions.
1 change: 1 addition & 0 deletions gitops/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@ func IsComponentSnapshot(snapshot *applicationapiv1alpha1.Snapshot) bool {
return metadata.HasLabelWithValue(snapshot, SnapshotTypeLabel, SnapshotComponentType)
}

// IsGroupSnapshot returns true if snapshot label 'test.appstudio.openshift.io/type' is 'group'
func IsGroupSnapshot(snapshot *applicationapiv1alpha1.Snapshot) bool {
return metadata.HasLabelWithValue(snapshot, SnapshotTypeLabel, SnapshotGroupType)
}
Expand Down
120 changes: 111 additions & 9 deletions internal/controller/statusreport/statusreport_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package statusreport

import (
"context"
e "errors"
"fmt"
"time"

applicationapiv1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1"
"github.com/konflux-ci/operator-toolkit/controller"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/konflux-ci/integration-service/api/v1beta2"
Expand Down Expand Up @@ -66,19 +68,14 @@ func NewAdapter(context context.Context, snapshot *applicationapiv1alpha1.Snapsh

// EnsureSnapshotTestStatusReportedToGitProvider will ensure that integration test status is reported to the git provider
// which (indirectly) triggered its execution.
// The status is reported to git provider if it is a component snapshot
// Or reported to git providers which trigger component snapshots included in group snapshot if it is a group snapshot
func (a *Adapter) EnsureSnapshotTestStatusReportedToGitProvider() (controller.OperationResult, error) {
if gitops.IsSnapshotCreatedByPACPushEvent(a.snapshot) {
if gitops.IsSnapshotCreatedByPACPushEvent(a.snapshot) && !gitops.IsGroupSnapshot(a.snapshot) {
return controller.ContinueProcessing()
}

reporter := a.status.GetReporter(a.snapshot)
if reporter == nil {
a.logger.Info("No suitable reporter found, skipping report")
return controller.ContinueProcessing()
}
a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName()))

err := a.status.ReportSnapshotStatus(a.context, reporter, a.snapshot)
err := a.ReportSnapshotStatus(a.context, a.snapshot)
if err != nil {
a.logger.Error(err, "failed to report test status to git provider for snapshot",
"snapshot.Namespace", a.snapshot.Namespace, "snapshot.Name", a.snapshot.Name)
Expand Down Expand Up @@ -238,3 +235,108 @@ func (a *Adapter) findUntriggeredIntegrationTestFromStatus(integrationTestScenar
}
return ""
}

// ReportSnapshotStatus reports status of all integration tests into Pull Requests from component snapshot or group snapshot
func (a *Adapter) ReportSnapshotStatus(ctx context.Context, testedSnapshot *applicationapiv1alpha1.Snapshot) error {

statuses, err := gitops.NewSnapshotIntegrationTestStatusesFromSnapshot(testedSnapshot)
if err != nil {
a.logger.Error(err, "failed to get test status annotations from snapshot",
"snapshot.Namespace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name)
return err
}

integrationTestStatusDetails := statuses.GetStatuses()
if len(integrationTestStatusDetails) == 0 {
// no tests to report, skip
a.logger.Info("No test result to report to GitHub, skipping",
"snapshot.Namespace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name)
return nil
}

// get the component snapshot list that include the git provider info the report will be reported to
destinationSnapshots := make([]*applicationapiv1alpha1.Snapshot, 0)
if gitops.IsComponentSnapshot(testedSnapshot) {
destinationSnapshots = append(destinationSnapshots, testedSnapshot)
} else if gitops.IsGroupSnapshot(testedSnapshot) {
// get component snapshots from group snapshot annotation GroupSnapshotInfoAnnotation
destinationSnapshots, err = status.GetComponentSnapshotsFromGroupSnapshot(ctx, a.client, testedSnapshot)
if err != nil {
a.logger.Error(err, "failed to get component snapshots included in group snapshot",
"snapshot.NameSpace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name)
return fmt.Errorf("failed to get component snapshots included in group snapshot %s/%s", testedSnapshot.Namespace, testedSnapshot.Name)
}
}

status.MigrateSnapshotToReportStatus(testedSnapshot, integrationTestStatusDetails)

srs, err := status.NewSnapshotReportStatusFromSnapshot(testedSnapshot)
if err != nil {
a.logger.Error(err, "failed to get latest snapshot write metadata annotation for snapshot",
"snapshot.NameSpace", testedSnapshot.Namespace, "snapshot.Name", testedSnapshot.Name)
srs, _ = status.NewSnapshotReportStatus("")
}

// Report the integration test status to pr/commit included in the tested component snapshot
// or the component snapshot included in group snapshot
for _, destinationComponentSnapshot := range destinationSnapshots {
destinationComponentSnapshot := destinationComponentSnapshot
reporter := a.status.GetReporter(destinationComponentSnapshot)
if reporter == nil {
a.logger.Info("No suitable reporter found, skipping report")
continue
}
a.logger.Info(fmt.Sprintf("Detected reporter: %s", reporter.GetReporterName()))

if err := reporter.Initialize(ctx, destinationComponentSnapshot); err != nil {
a.logger.Error(err, "Failed to initialize reporter", "reporter", reporter.GetReporterName())
return fmt.Errorf("failed to initialize reporter: %w", err)
}
a.logger.Info("Reporter initialized", "reporter", reporter.GetReporterName())

// set componentName to component name of component snapshot or pr group name of group snapshot when reporting status to git provider
componentName := ""
if gitops.IsGroupSnapshot(testedSnapshot) {
componentName = "pr group " + testedSnapshot.Annotations[gitops.PRGroupAnnotation]
} else if gitops.IsComponentSnapshot(testedSnapshot) {
componentName = testedSnapshot.Labels[gitops.SnapshotComponentLabel]
}

err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
for _, integrationTestStatusDetail := range integrationTestStatusDetails {
if srs.IsNewer(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime) {
a.logger.Info("Integration Test contains new status updates", "scenario.Name", integrationTestStatusDetail.ScenarioName)
} else {
//integration test contains no changes
continue
}
testReport, reportErr := status.GenerateTestReport(ctx, a.client, *integrationTestStatusDetail, testedSnapshot, componentName)
if reportErr != nil {
if writeErr := status.WriteSnapshotReportStatus(ctx, a.client, testedSnapshot, srs); writeErr != nil { // try to write what was already written
return fmt.Errorf("failed to generate test report AND write snapshot report status metadata: %w", e.Join(reportErr, writeErr))
}
return fmt.Errorf("failed to generate test report: %w", reportErr)
}
if reportStatusErr := reporter.ReportStatus(ctx, *testReport); reportStatusErr != nil {
if writeErr := status.WriteSnapshotReportStatus(ctx, a.client, testedSnapshot, srs); writeErr != nil { // try to write what was already written
return fmt.Errorf("failed to report status AND write snapshot report status metadata: %w", e.Join(reportStatusErr, writeErr))
}
return fmt.Errorf("failed to update status: %w", reportStatusErr)
}
srs.SetLastUpdateTime(integrationTestStatusDetail.ScenarioName, integrationTestStatusDetail.LastUpdateTime)
}
if err := status.WriteSnapshotReportStatus(ctx, a.client, testedSnapshot, srs); err != nil {
return fmt.Errorf("failed to write snapshot report status metadata: %w", err)
}
return err
})
}

if err != nil {
return fmt.Errorf("issue occured during generating or updating report status: %w", err)
}

a.logger.Info(fmt.Sprintf("Successfully updated the %s annotation", gitops.SnapshotStatusReportAnnotation), "snapshot.Name", testedSnapshot.Name)

return nil
}
138 changes: 128 additions & 10 deletions internal/controller/statusreport/statusreport_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ package statusreport

import (
"bytes"
"context"
"fmt"
"os"
"reflect"
"time"

"github.com/konflux-ci/integration-service/api/v1beta2"
"github.com/tonglil/buflogr"
Expand All @@ -44,15 +47,18 @@ import (

var _ = Describe("Snapshot Adapter", Ordered, func() {
var (
adapter *Adapter
logger helpers.IntegrationLogger
buf bytes.Buffer
adapter *Adapter
logger helpers.IntegrationLogger
buf bytes.Buffer
mockReporter *status.MockReporterInterface
mockStatus *status.MockStatusInterface

hasComp *applicationapiv1alpha1.Component
hasComp2 *applicationapiv1alpha1.Component
hasApp *applicationapiv1alpha1.Application
hasSnapshot *applicationapiv1alpha1.Snapshot
hasPRSnapshot *applicationapiv1alpha1.Snapshot
githubSnapshot *applicationapiv1alpha1.Snapshot
integrationTestScenario *v1beta2.IntegrationTestScenario
)
const (
Expand Down Expand Up @@ -154,7 +160,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() {

hasPRSnapshot = &applicationapiv1alpha1.Snapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "snapshot-PR-sample",
Name: "snapshot-pr-sample",
Namespace: "default",
Labels: map[string]string{
gitops.SnapshotTypeLabel: "component",
Expand All @@ -172,6 +178,7 @@ var _ = Describe("Snapshot Adapter", Ordered, func() {
"build.appstudio.redhat.com/commit_sha": "6c65b2fcaea3e1a0a92476c8b5dc89e92a85f025",
"appstudio.redhat.com/updateComponentOnSuccess": "false",
gitops.SnapshotTestsStatusAnnotation: "[{\"scenario\":\"scenario-1\",\"status\":\"EnvironmentProvisionError\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"completionTime\":\"2023-07-26T17:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\",\"details\":\"Failed to find deploymentTargetClass with right provisioner for copy of existingEnvironment\"}]",
gitops.PipelineAsCodeGitProviderAnnotation: gitops.PipelineAsCodeGitHubProviderType,
},
},
Spec: applicationapiv1alpha1.SnapshotSpec{
Expand Down Expand Up @@ -223,6 +230,8 @@ var _ = Describe("Snapshot Adapter", Ordered, func() {
},
},
}

Expect(k8sClient.Create(ctx, hasPRSnapshot)).Should(Succeed())
Expect(k8sClient.Create(ctx, integrationTestScenario)).Should(Succeed())
})

Expand All @@ -243,14 +252,14 @@ var _ = Describe("Snapshot Adapter", Ordered, func() {
It("ensures the statusReport is called", func() {

ctrl := gomock.NewController(GinkgoT())
mockReporter := status.NewMockReporterInterface(ctrl)
mockReporter = status.NewMockReporterInterface(ctrl)
mockStatus := status.NewMockStatusInterface(ctrl)

mockReporter.EXPECT().GetReporterName().Return("mocked_reporter")

mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes()
mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter)
// ReportSnapshotStatus must be called once
mockStatus.EXPECT().ReportSnapshotStatus(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes()
mockReporter.EXPECT().GetReporterName().AnyTimes()
mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1)
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1)

mockScenarios := []v1beta2.IntegrationTestScenario{}
adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient)
Expand Down Expand Up @@ -535,4 +544,113 @@ var _ = Describe("Snapshot Adapter", Ordered, func() {
})

})

When("", func() {
BeforeEach(func() {
buf = bytes.Buffer{}

githubSnapshot = &applicationapiv1alpha1.Snapshot{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"pac.test.appstudio.openshift.io/git-provider": "github",
},
},
}

ctrl := gomock.NewController(GinkgoT())
mockReporter = status.NewMockReporterInterface(ctrl)
mockReporter.EXPECT().GetReporterName().Return("mocked-reporter").AnyTimes()
mockStatus = status.NewMockStatusInterface(ctrl)
})
It("doesn't report anything when there are not test results", func() {
mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(0) // without test results reporter shouldn't be initialized
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // without test results reported shouldn't report status

adapter = NewAdapter(ctx, githubSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient)
adapter.status = mockStatus
err := adapter.ReportSnapshotStatus(adapter.context, githubSnapshot)
Expect(err).NotTo(HaveOccurred())
})

It("doesn't report anything when data are older", func() {
mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter)
mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes()
mockReporter.EXPECT().GetReporterName().AnyTimes()

mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1)
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(0) // data are older, status shouldn't be reported

hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]"
hasPRSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\"}}}"
adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient)
adapter.status = mockStatus
err := adapter.ReportSnapshotStatus(context.Background(), adapter.snapshot)
Expect(err).NotTo(HaveOccurred())
})

It("Report new status if it was updated", func() {
mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter)
mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes()
mockReporter.EXPECT().GetReporterName().AnyTimes()

mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1)
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1)

hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]"
hasPRSnapshot.Annotations["test.appstudio.openshift.io/git-reporter-status"] = "{\"scenarios\":{\"scenario1\":{\"lastUpdateTime\":\"2023-08-26T17:57:49+02:00\"}}}"
hasPRSnapshot.Annotations["test.appstudio.openshift.io/group-test-info"] = "[{\"namespace\":\"default\",\"component\":\"devfile-sample-java-springboot-basic-8969\",\"buildPipelineRun\":\"build-plr-java-qjfxz\",\"snapshot\":\"app-8969-bbn7d\"},{\"namespace\":\"default\",\"component\":\"devfile-sample-go-basic-8969\",\"buildPipelineRun\":\"build-plr-go-jmsjq\",\"snapshot\":\"app-8969-kzq2l\"}]"
adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient)
adapter.status = mockStatus
err := adapter.ReportSnapshotStatus(context.Background(), adapter.snapshot)
Expect(err).NotTo(HaveOccurred())
})

It("Report new status if it was updated (old way - migration test)", func() {
mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter)
mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes()
mockReporter.EXPECT().GetReporterName().AnyTimes()

mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1)
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Any()).Times(1)

hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]"
hasPRSnapshot.Annotations["test.appstudio.openshift.io/pr-last-update"] = "2023-08-26T17:57:49+02:00"
adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, logger, loader.NewMockLoader(), k8sClient)
adapter.status = mockStatus
err := adapter.ReportSnapshotStatus(context.Background(), adapter.snapshot)
fmt.Fprintf(GinkgoWriter, "-------test: %v\n", "")
Expect(err).NotTo(HaveOccurred())
})

It("report expected textual data for InProgress test scenario", func() {
os.Setenv("CONSOLE_NAME", "Konflux Staging")
log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)}

mockStatus.EXPECT().GetReporter(gomock.Any()).Return(mockReporter)
mockStatus.EXPECT().GetReporter(gomock.Any()).AnyTimes()
mockReporter.EXPECT().GetReporterName().AnyTimes()

hasPRSnapshot.Annotations["test.appstudio.openshift.io/status"] = "[{\"scenario\":\"scenario1\",\"status\":\"InProgress\",\"testPipelineRunName\":\"test-pipelinerun\",\"startTime\":\"2023-07-26T16:57:49+02:00\",\"lastUpdateTime\":\"2023-08-26T17:57:50+02:00\",\"details\":\"Test in progress\"}]"
t, err := time.Parse(time.RFC3339, "2023-07-26T16:57:49+02:00")
Expect(err).NotTo(HaveOccurred())
expectedTestReport := status.TestReport{
FullName: "Konflux Staging / scenario1 / component-sample",
ScenarioName: "scenario1",
SnapshotName: "snapshot-pr-sample",
ComponentName: "component-sample",
Text: "Test in progress",
Summary: "Integration test for snapshot snapshot-pr-sample and scenario scenario1 is in progress",
Status: intgteststat.IntegrationTestStatusInProgress,
StartTime: &t,
TestPipelineRunName: "test-pipelinerun",
}
mockReporter.EXPECT().Initialize(gomock.Any(), gomock.Any()).Times(1)
mockReporter.EXPECT().ReportStatus(gomock.Any(), gomock.Eq(expectedTestReport)).Times(1)
adapter = NewAdapter(ctx, hasPRSnapshot, hasApp, log, loader.NewMockLoader(), k8sClient)
adapter.status = mockStatus
err = adapter.ReportSnapshotStatus(context.Background(), adapter.snapshot)
fmt.Fprintf(GinkgoWriter, "-------test: %v\n", buf.String())
Expect(err).NotTo(HaveOccurred())
})
})
})
26 changes: 13 additions & 13 deletions status/mock_status.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ca8daa1

Please sign in to comment.