Skip to content

Commit

Permalink
feat(test): report api output and service logs on failure (#697)
Browse files Browse the repository at this point in the history
* feat(test): report api output in case of failure

* feat(test): report service logs in case of failure

* feat(test): add report failed method and add method descriptions

* feat(test): configure failure reports per test

* feat(test): create new test report file

* feat(test): simplify failure report configuration

* feat(test): use indent in api data dump

* feat(test): use since timestamp instead of tail in service logs
  • Loading branch information
paralta committed Sep 20, 2023
1 parent 913f83c commit 21329e2
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 4 deletions.
9 changes: 8 additions & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ To add a new test, create a new `<test_name>_test.go` file in the current direct

```go
var _ = ginkgo.Describe("<add a brief test case description>", func() {
ginkgo.Context("<describe conditions or inputs>", func() {
reportFailedConfig := ReportFailedConfig{}
ginkgo.Context("<describe conditions or inputs>", func() {
ginkgo.It("<describe the behaviour or feature being tested>", func(ctx ginkgo.SpecContext) {
<implement test code>
})
})
ginkgo.AfterEach(func(ctx ginkgo.SpecContext) {
if ginkgo.CurrentSpecReport().Failed() {
reportFailedConfig.startTime = ginkgo.CurrentSpecReport().StartTime
ReportFailed(ctx, testEnv, client, &reportFailedConfig)
}
})
})
```

Expand Down
18 changes: 17 additions & 1 deletion e2e/abort_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
)

var _ = ginkgo.Describe("Aborting a scan", func() {
reportFailedConfig := ReportFailedConfig{}

ginkgo.Context("which is running", func() {
ginkgo.It("should stop successfully", func(ctx ginkgo.SpecContext) {
ginkgo.By("applying a scan configuration")
Expand All @@ -51,7 +53,14 @@ var _ = ginkgo.Describe("Aborting a scan", func() {
gomega.Eventually(func() bool {
scans, err = client.GetScans(ctx, params)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
return len(*scans.Items) == 1
if len(*scans.Items) == 1 {
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scan", fmt.Sprintf("id eq '%s'", *(*scans.Items)[0].Id)},
)
return true
}
return false
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())

ginkgo.By("aborting a scan")
Expand All @@ -76,4 +85,11 @@ var _ = ginkgo.Describe("Aborting a scan", func() {
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())
})
})

ginkgo.AfterEach(func(ctx ginkgo.SpecContext) {
if ginkgo.CurrentSpecReport().Failed() {
reportFailedConfig.startTime = ginkgo.CurrentSpecReport().StartTime
ReportFailed(ctx, testEnv, client, &reportFailedConfig)
}
})
})
27 changes: 26 additions & 1 deletion e2e/basic_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ import (
)

var _ = ginkgo.Describe("Running a basic scan (only SBOM)", func() {
reportFailedConfig := ReportFailedConfig{}

ginkgo.Context("which scans a docker container", func() {
ginkgo.It("should finish successfully", func(ctx ginkgo.SpecContext) {
ginkgo.By("waiting until test asset is found")
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"asset", DefaultScope},
)
assetsParams := models.GetAssetsParams{
Filter: utils.PointerTo(DefaultScope),
}
Expand All @@ -53,6 +59,11 @@ var _ = ginkgo.Describe("Running a basic scan (only SBOM)", func() {
))
gomega.Expect(err).NotTo(gomega.HaveOccurred())

reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scanConfig", fmt.Sprintf("id eq '%s'", *apiScanConfig.Id)},
)

ginkgo.By("updating scan configuration to run now")
updateScanConfig := UpdateScanConfigToStartNow(apiScanConfig)
err = client.PatchScanConfig(ctx, *apiScanConfig.Id, updateScanConfig)
Expand All @@ -71,7 +82,14 @@ var _ = ginkgo.Describe("Running a basic scan (only SBOM)", func() {
gomega.Eventually(func() bool {
scans, err = client.GetScans(ctx, scanParams)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
return len(*scans.Items) == 1
if len(*scans.Items) == 1 {
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scan", fmt.Sprintf("id eq '%s'", *(*scans.Items)[0].Id)},
)
return true
}
return false
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())

ginkgo.By("waiting until scan state changes to done")
Expand All @@ -89,4 +107,11 @@ var _ = ginkgo.Describe("Running a basic scan (only SBOM)", func() {
}, time.Second*120, time.Second).Should(gomega.BeTrue())
})
})

ginkgo.AfterEach(func(ctx ginkgo.SpecContext) {
if ginkgo.CurrentSpecReport().Failed() {
reportFailedConfig.startTime = ginkgo.CurrentSpecReport().StartTime
ReportFailed(ctx, testEnv, client, &reportFailedConfig)
}
})
})
18 changes: 17 additions & 1 deletion e2e/default_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
)

var _ = ginkgo.Describe("Running a default scan (SBOM, vulnerabilities and exploits)", func() {
reportFailedConfig := ReportFailedConfig{}

ginkgo.Context("which scans a docker container", func() {
ginkgo.It("should finish successfully", func(ctx ginkgo.SpecContext) {
ginkgo.By("applying a scan configuration")
Expand All @@ -51,7 +53,14 @@ var _ = ginkgo.Describe("Running a default scan (SBOM, vulnerabilities and explo
gomega.Eventually(func() bool {
scans, err = client.GetScans(ctx, scanParams)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
return len(*scans.Items) == 1
if len(*scans.Items) == 1 {
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scan", fmt.Sprintf("id eq '%s'", *(*scans.Items)[0].Id)},
)
return true
}
return false
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())

ginkgo.By("waiting until scan state changes to done")
Expand All @@ -69,4 +78,11 @@ var _ = ginkgo.Describe("Running a default scan (SBOM, vulnerabilities and explo
}, time.Second*120, time.Second).Should(gomega.BeTrue())
})
})

ginkgo.AfterEach(func(ctx ginkgo.SpecContext) {
if ginkgo.CurrentSpecReport().Failed() {
reportFailedConfig.startTime = ginkgo.CurrentSpecReport().StartTime
ReportFailed(ctx, testEnv, client, &reportFailedConfig)
}
})
})
49 changes: 49 additions & 0 deletions e2e/fail_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
)

var _ = ginkgo.Describe("Detecting scan failures", func() {
reportFailedConfig := ReportFailedConfig{}

ginkgo.Context("when a scan stops without assets to scan", func() {
ginkgo.It("should detect failure reason successfully", func(ctx ginkgo.SpecContext) {
ginkgo.By("applying a scan configuration with not existing label")
Expand All @@ -44,6 +46,26 @@ var _ = ginkgo.Describe("Detecting scan failures", func() {
err = client.PatchScanConfig(ctx, *apiScanConfig.Id, updateScanConfig)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("waiting until scan starts")
scanParams := models.GetScansParams{
Filter: utils.PointerTo(fmt.Sprintf(
"scanConfig/id eq '%s'",
*apiScanConfig.Id,
)),
}
gomega.Eventually(func() bool {
scans, err := client.GetScans(ctx, scanParams)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
if len(*scans.Items) == 1 {
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scan", fmt.Sprintf("id eq '%s'", *(*scans.Items)[0].Id)},
)
return true
}
return false
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())

ginkgo.By("waiting until scan state changes to failed with nothing to scan as state reason")
params := models.GetScansParams{
Filter: utils.PointerTo(fmt.Sprintf(
Expand Down Expand Up @@ -79,6 +101,26 @@ var _ = ginkgo.Describe("Detecting scan failures", func() {
err = client.PatchScanConfig(ctx, *apiScanConfig.Id, updateScanConfig)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

ginkgo.By("waiting until scan starts")
scanParams := models.GetScansParams{
Filter: utils.PointerTo(fmt.Sprintf(
"scanConfig/id eq '%s'",
*apiScanConfig.Id,
)),
}
gomega.Eventually(func() bool {
scans, err := client.GetScans(ctx, scanParams)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
if len(*scans.Items) == 1 {
reportFailedConfig.objects = append(
reportFailedConfig.objects,
APIObject{"scan", fmt.Sprintf("id eq '%s'", *(*scans.Items)[0].Id)},
)
return true
}
return false
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())

ginkgo.By("waiting until scan state changes to failed with timed out as state reason")
params := models.GetScansParams{
Filter: utils.PointerTo(fmt.Sprintf(
Expand All @@ -96,4 +138,11 @@ var _ = ginkgo.Describe("Detecting scan failures", func() {
}, DefaultTimeout, time.Second).Should(gomega.BeTrue())
})
})

ginkgo.AfterEach(func(ctx ginkgo.SpecContext) {
if ginkgo.CurrentSpecReport().Failed() {
reportFailedConfig.startTime = ginkgo.CurrentSpecReport().StartTime
ReportFailed(ctx, testEnv, client, &reportFailedConfig)
}
})
})
124 changes: 124 additions & 0 deletions e2e/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package e2e

import (
"encoding/json"
"time"

"github.com/onsi/ginkgo/v2"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/gomega"

"github.com/openclarity/vmclarity/api/models"
"github.com/openclarity/vmclarity/e2e/testenv"
"github.com/openclarity/vmclarity/pkg/shared/backendclient"
"github.com/openclarity/vmclarity/pkg/shared/utils"
)

type APIObject struct {
objectType string
filter string
}

type ReportFailedConfig struct {
startTime time.Time
// if not empty, print logs for services in slice. if empty, print logs for all services.
services []string
// if true, print all API objects.
allAPIObjects bool
// if not empty, print objects in slice.
objects []APIObject
}

// ReportFailed gathers relevant API data and docker service logs for debugging purposes.
func ReportFailed(ctx ginkgo.SpecContext, testEnv *testenv.Environment, client *backendclient.BackendClient, config *ReportFailedConfig) {
ginkgo.GinkgoWriter.Println("------------------------------")

DumpAPIData(ctx, client, config)
DumpServiceLogs(ctx, testEnv, config)

ginkgo.GinkgoWriter.Println("------------------------------")
}

// nolint:cyclop
// DumpAPIData prints API objects filtered using test parameters (e.g. assets filtered by scope, scan configs filtered by id).
// If filter not provided, no objects are printed.
func DumpAPIData(ctx ginkgo.SpecContext, client *backendclient.BackendClient, config *ReportFailedConfig) {
ginkgo.GinkgoWriter.Println(formatter.F("{{red}}[FAILED] Report API Data:{{/}}"))

if config.allAPIObjects {
config.objects = append(config.objects, APIObject{"asset", ""}, APIObject{"scanConfigs", ""}, APIObject{"scans", ""})
}

for _, object := range config.objects {
switch object.objectType {
case "asset":
var params models.GetAssetsParams
if object.filter == "" {
params = models.GetAssetsParams{}
} else {
params = models.GetAssetsParams{Filter: utils.PointerTo(object.filter)}
}
assets, err := client.GetAssets(ctx, params)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

buf, err := json.MarshalIndent(*assets.Items, "", "\t")
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.GinkgoWriter.Printf("Asset: %s\n", string(buf))

case "scanConfig":
var params models.GetScanConfigsParams
if object.filter == "" {
params = models.GetScanConfigsParams{}
} else {
params = models.GetScanConfigsParams{Filter: utils.PointerTo(object.filter)}
}
scanConfigs, err := client.GetScanConfigs(ctx, params)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

buf, err := json.MarshalIndent(*scanConfigs.Items, "", "\t")
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.GinkgoWriter.Printf("Scan Config: %s\n", string(buf))

case "scan":
var params models.GetScansParams
if object.filter == "" {
params = models.GetScansParams{}
} else {
params = models.GetScansParams{Filter: utils.PointerTo(object.filter)}
}
scans, err := client.GetScans(ctx, params)
gomega.Expect(err).NotTo(gomega.HaveOccurred())

buf, err := json.MarshalIndent(*scans.Items, "", "\t")
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.GinkgoWriter.Printf("Scan: %s\n", string(buf))
}
}
}

// DumpServiceLogs prints service logs since the test started until it failed.
func DumpServiceLogs(ctx ginkgo.SpecContext, testEnv *testenv.Environment, config *ReportFailedConfig) {
ginkgo.GinkgoWriter.Println(formatter.F("{{red}}[FAILED] Report Service Logs:{{/}}"))

if len(config.services) == 0 {
config.services = testEnv.Services()
}

err := testEnv.ServiceLogs(ctx, config.services, config.startTime, formatter.ColorableStdOut, formatter.ColorableStdErr)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
13 changes: 13 additions & 0 deletions e2e/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ package testenv
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"time"

"github.com/docker/compose/v2/cmd/formatter"

"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/command"
Expand Down Expand Up @@ -149,6 +152,16 @@ func (e *Environment) ServicesReady(ctx context.Context) (bool, error) {
return true, nil
}

// nolint:wrapcheck
func (e *Environment) ServiceLogs(ctx context.Context, services []string, startTime time.Time, stdout, stderr io.Writer) error {
consumer := formatter.NewLogConsumer(ctx, stdout, stderr, true, true, false)
return e.composer.Logs(ctx, e.project.Name, consumer, api.LogOptions{
Project: e.project,
Services: services,
Since: startTime.Format(time.RFC3339Nano),
})
}

func (e *Environment) Services() []string {
services := make([]string, len(e.project.Services))
for i, srv := range e.project.Services {
Expand Down

0 comments on commit 21329e2

Please sign in to comment.