From da627920d75be9976300cdb25379b268c5ad6cf6 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 30 Jul 2025 21:49:23 +0200 Subject: [PATCH 1/7] Check image labels - currently only listing them --- test/acceptance/releases_images_test.go | 45 ++++++++++- test/support/common.go | 22 +++++- test/support/image_utils.go | 101 +++++++++++++++++++----- 3 files changed, 146 insertions(+), 22 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index dd37732..8811b71 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -2,7 +2,6 @@ package acceptance import ( "fmt" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/securesign/structural-tests/test/support" @@ -40,4 +39,48 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { Expect(mapped).To(HaveEach(1)) Expect(len(snapshotData.Images)).To(BeNumerically("==", len(mapped))) }) + + It("operator and operator bundle have both the same git reference", func() { + operatorReference, err := support.GetImageLabel(snapshotData.Images[support.OperatorImageKey], "vcs-ref") + Expect(err).NotTo(HaveOccurred()) + Expect(operatorReference).NotTo(BeEmpty()) + operatorBundleReference, err := support.GetImageLabel(snapshotData.Images[support.OperatorBundleImageKey], "vcs-ref") + Expect(err).NotTo(HaveOccurred()) + Expect(operatorBundleReference).NotTo(BeEmpty()) + Expect(operatorReference).To(Equal(operatorBundleReference)) + }) + + It("snapshot.json images have correct labels", func() { + var imagesData []support.ImageData + allLabelKeys := make(map[string]int) + for _, image := range snapshotData.Images { + labels, err := support.InspectImageForLabels(image) + Expect(err).NotTo(HaveOccurred()) + Expect(labels).NotTo(BeEmpty()) + var iData support.ImageData + iData.Image = image + iData.Labels = labels + + // calculate counts of labels + imagesData = append(imagesData, iData) + for key := range labels { + _, exist := allLabelKeys[key] + if exist { + allLabelKeys[key]++ + } else { + allLabelKeys[key] = 1 + } + } + } + + sortedKeys := support.GetMapKeysSorted(allLabelKeys) + support.LogMapByProvidedKeys(fmt.Sprintf("Labels counts out of max %d", len(imagesData)), allLabelKeys, sortedKeys) + + for _, key := range sortedKeys { + fmt.Printf("%s:\n", key) + for _, img := range imagesData { + fmt.Printf(" [%-120s] %s\n", img.Image, img.Labels[key]) + } + } + }) }) diff --git a/test/support/common.go b/test/support/common.go index 9bf58fe..cdc48b9 100644 --- a/test/support/common.go +++ b/test/support/common.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "slices" ) func GetEnv(key string) string { @@ -26,7 +27,7 @@ func getEnv(key string, isSecret bool) string { return envValue } -func GetMapKeys(m map[string]string) []string { +func GetMapKeys[V any](m map[string]V) []string { result := make([]string, 0, len(m)) for k := range m { result = append(result, k) @@ -42,6 +43,12 @@ func GetMapValues(m map[string]string) []string { return result } +func GetMapKeysSorted[V any](m map[string]V) []string { + keys := GetMapKeys(m) + slices.Sort(keys) + return keys +} + func SplitMap(original map[string]string, keysToKeep []string) (map[string]string, map[string]string) { remaining := make(map[string]string) moved := make(map[string]string) @@ -73,10 +80,19 @@ func LogArray(message string, data []string) { log.Print(result) } -func LogMap(message string, data map[string]string) { +func LogMap[V any](message string, data map[string]V) { result := message + "\n" for key, value := range data { - result += fmt.Sprintf(" [%-41s] %s\n", key, value) + result += fmt.Sprintf(" [%-41v] %v\n", key, value) + } + log.Print(result) +} + +func LogMapByProvidedKeys[V any](message string, data map[string]V, keysToLog []string) { + result := message + "\n" + for _, key := range keysToLog { + result += fmt.Sprintf(" [%-53v] %v\n", key, data[key]) } log.Print(result) + } diff --git a/test/support/image_utils.go b/test/support/image_utils.go index 617e97a..dad1f9d 100644 --- a/test/support/image_utils.go +++ b/test/support/image_utils.go @@ -16,21 +16,54 @@ import ( "github.com/docker/docker/client" ) +type ImageData struct { + Image string + Labels map[string]string +} + +func PullImageIfNotPresentLocally(ctx context.Context, imageDefinition string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("could not create docker client: %w", err) + } + defer cli.Close() + + // Inspect the image to check if it exists locally. + _, _, err = cli.ImageInspectWithRaw(ctx, imageDefinition) + if err == nil { + return nil // image is present already + } + + if client.IsErrNotFound(err) { + log.Printf("Image '%s' not found locally, pulling...\n", imageDefinition) + pullResp, pullErr := cli.ImagePull(ctx, imageDefinition, image.PullOptions{ + Platform: "linux/amd64", + }) + if pullErr != nil { + return fmt.Errorf("failed to pull image: %w", pullErr) + } + defer pullResp.Close() + // ensure the pull operation completes. + io.Copy(io.Discard, pullResp) + return nil + } + + // another type of error, return it. + return fmt.Errorf("failed to inspect image: %w", err) +} + func RunImage(imageDefinition string, commands []string) (string, error) { ctx := context.TODO() + err := PullImageIfNotPresentLocally(ctx, imageDefinition) + if err != nil { + return "", err + } + log.Printf("Running image %s with commands %v\n", imageDefinition, commands) cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return "", fmt.Errorf("error while initializing docker client: %w", err) } - reader, err := cli.ImagePull(ctx, imageDefinition, image.PullOptions{ - Platform: "linux/amd64", - }) - if err != nil { - return "", fmt.Errorf("cannot pull image %s: %w", imageDefinition, err) - } - _, _ = io.Copy(os.Stdout, reader) - _ = reader.Close() resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: imageDefinition, @@ -69,21 +102,53 @@ func RunImage(imageDefinition string, commands []string) (string, error) { return buf.String(), nil } -func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) error { - // Initialize the Docker client +func InspectImageForLabels(imageDefinition string) (map[string]string, error) { + ctx := context.TODO() + err := PullImageIfNotPresentLocally(ctx, imageDefinition) + if err != nil { + return nil, err + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - panic(err) + return nil, fmt.Errorf("error while initializing docker client: %w", err) + } + defer cli.Close() + + inspectData, _, err := cli.ImageInspectWithRaw(ctx, imageDefinition) + if err != nil { + return nil, fmt.Errorf("cannot inspect image %s: %w", imageDefinition, err) + } + if inspectData.Config == nil || len(inspectData.Config.Labels) == 0 { + log.Printf("Image [%s] does not have any labels\n", imageDefinition) + return make(map[string]string), nil + } + + return inspectData.Config.Labels, nil +} + +func GetImageLabel(imageDefinition, labelName string) (string, error) { + labels, err := InspectImageForLabels(imageDefinition) + if err != nil { + return "", err + } + if labelValue, ok := labels[labelName]; ok { + return labelValue, nil } + return "", fmt.Errorf("label [%s] not found in image %s", labelName, imageDefinition) +} - reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{ - Platform: "linux/amd64", - }) +func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) error { + err := PullImageIfNotPresentLocally(ctx, imageName) if err != nil { - return fmt.Errorf("cannot pull image %s: %w", imageName, err) + return err + } + + // Initialize the Docker client + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) } - _, _ = io.Copy(io.Discard, reader) - _ = reader.Close() // Create a container from the image resp, err := cli.ContainerCreate(ctx, &container.Config{ @@ -102,7 +167,7 @@ func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) }() // Use Docker's API to copy the file from the container's filesystem - reader, _, err = cli.CopyFromContainer(ctx, resp.ID, filePath) + reader, _, err := cli.CopyFromContainer(ctx, resp.ID, filePath) if err != nil { return fmt.Errorf("failed to copy file from container: %w", err) } From 509a46f69f790cfd3f5ef3ff67986fd46790c1b9 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 27 Aug 2025 16:10:43 +0200 Subject: [PATCH 2/7] Checking few labels for value or not to be empty --- test/acceptance/releases_images_test.go | 58 ++++++++++++++----------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index 8811b71..bf8f938 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -2,6 +2,7 @@ package acceptance import ( "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/securesign/structural-tests/test/support" @@ -51,36 +52,43 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { }) It("snapshot.json images have correct labels", func() { - var imagesData []support.ImageData - allLabelKeys := make(map[string]int) - for _, image := range snapshotData.Images { - labels, err := support.InspectImageForLabels(image) - Expect(err).NotTo(HaveOccurred()) - Expect(labels).NotTo(BeEmpty()) - var iData support.ImageData - iData.Image = image - iData.Labels = labels + var imageDataList []support.ImageData + + // Collect all images and their labels + for imageName, imageDefinition := range snapshotData.Images { + labels, err := support.InspectImageForLabels(imageDefinition) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to inspect labels for image %s (%s)", imageName, imageDefinition)) - // calculate counts of labels - imagesData = append(imagesData, iData) - for key := range labels { - _, exist := allLabelKeys[key] - if exist { - allLabelKeys[key]++ - } else { - allLabelKeys[key] = 1 - } + imageData := support.ImageData{ + Image: imageDefinition, + Labels: labels, } + imageDataList = append(imageDataList, imageData) + } + + // Check that all images have 'architecture' label with value 'x86_64' + for _, imageData := range imageDataList { + Expect(imageData.Labels).To(HaveKey("architecture"), + fmt.Sprintf("Image %s is missing 'architecture' label", imageData.Image)) + Expect(imageData.Labels["architecture"]).To(Equal("x86_64"), + fmt.Sprintf("Image %s has incorrect architecture label: expected 'x86_64', got '%s'", + imageData.Image, imageData.Labels["architecture"])) } - sortedKeys := support.GetMapKeysSorted(allLabelKeys) - support.LogMapByProvidedKeys(fmt.Sprintf("Labels counts out of max %d", len(imagesData)), allLabelKeys, sortedKeys) + // Check that all images have 'build-date' label which is not empty + for _, imageData := range imageDataList { + Expect(imageData.Labels).To(HaveKey("build-date"), + fmt.Sprintf("Image %s is missing 'build-date' label", imageData.Image)) + Expect(imageData.Labels["build-date"]).NotTo(BeEmpty(), + fmt.Sprintf("Image %s has empty 'build-date' label", imageData.Image)) + } - for _, key := range sortedKeys { - fmt.Printf("%s:\n", key) - for _, img := range imagesData { - fmt.Printf(" [%-120s] %s\n", img.Image, img.Labels[key]) - } + // Check that all images have 'short-commit' label which is not empty + for _, imageData := range imageDataList { + Expect(imageData.Labels).To(HaveKey("short-commit"), + fmt.Sprintf("Image %s is missing 'short-commit' label", imageData.Image)) + Expect(imageData.Labels["short-commit"]).NotTo(BeEmpty(), + fmt.Sprintf("Image %s has empty 'short-commit' label", imageData.Image)) } }) }) From 96963e954515fc5377db83a6e9c2c496f9b0726c Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 27 Aug 2025 16:22:25 +0200 Subject: [PATCH 3/7] Refactored, to use map label -> expected value --- test/acceptance/releases_images_test.go | 37 +++++++++++-------------- test/support/test_constants.go | 15 ++++++++++ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index bf8f938..37f3c80 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -66,29 +66,24 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { imageDataList = append(imageDataList, imageData) } - // Check that all images have 'architecture' label with value 'x86_64' + // Check that all images have required labels with correct values + requiredLabels := support.RequiredImageLabels() for _, imageData := range imageDataList { - Expect(imageData.Labels).To(HaveKey("architecture"), - fmt.Sprintf("Image %s is missing 'architecture' label", imageData.Image)) - Expect(imageData.Labels["architecture"]).To(Equal("x86_64"), - fmt.Sprintf("Image %s has incorrect architecture label: expected 'x86_64', got '%s'", - imageData.Image, imageData.Labels["architecture"])) - } - - // Check that all images have 'build-date' label which is not empty - for _, imageData := range imageDataList { - Expect(imageData.Labels).To(HaveKey("build-date"), - fmt.Sprintf("Image %s is missing 'build-date' label", imageData.Image)) - Expect(imageData.Labels["build-date"]).NotTo(BeEmpty(), - fmt.Sprintf("Image %s has empty 'build-date' label", imageData.Image)) - } + for labelName, expectedValue := range requiredLabels { + Expect(imageData.Labels).To(HaveKey(labelName), + fmt.Sprintf("Image %s is missing '%s' label", imageData.Image, labelName)) - // Check that all images have 'short-commit' label which is not empty - for _, imageData := range imageDataList { - Expect(imageData.Labels).To(HaveKey("short-commit"), - fmt.Sprintf("Image %s is missing 'short-commit' label", imageData.Image)) - Expect(imageData.Labels["short-commit"]).NotTo(BeEmpty(), - fmt.Sprintf("Image %s has empty 'short-commit' label", imageData.Image)) + if expectedValue != "" { + // Specific value expected + Expect(imageData.Labels[labelName]).To(Equal(expectedValue), + fmt.Sprintf("Image %s has incorrect '%s' label: expected '%s', got '%s'", + imageData.Image, labelName, expectedValue, imageData.Labels[labelName])) + } else { + // Label must not be empty + Expect(imageData.Labels[labelName]).NotTo(BeEmpty(), + fmt.Sprintf("Image %s has empty '%s' label", imageData.Image, labelName)) + } + } } }) }) diff --git a/test/support/test_constants.go b/test/support/test_constants.go index 329abba..39092ec 100644 --- a/test/support/test_constants.go +++ b/test/support/test_constants.go @@ -93,3 +93,18 @@ func GetOSArchMatrix() OSArchMatrix { "windows": {"amd64"}, } } + +// RequiredImageLabels returns a map of required image labels. +// If the value is not empty, it's the expected value for the label. +// If the value is empty, the label must exist but can have any non-empty value. +func RequiredImageLabels() map[string]string { + return map[string]string{ + "architecture": "x86_64", + "build-date": "", + "short-commit": "", + "vcs-ref": "", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "1.3.0", + } +} From 301f6645d261bf19e0b6ec85bb6392ed2f2dd270 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 27 Aug 2025 18:23:50 +0200 Subject: [PATCH 4/7] Human readable reporting on wrong image labels --- test/acceptance/releases_images_test.go | 48 ++++++++++++++++++------- test/support/test_constants.go | 6 +--- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index 37f3c80..f86c376 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -53,6 +53,7 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { It("snapshot.json images have correct labels", func() { var imageDataList []support.ImageData + imageLablelsErrors := make(map[string][]string) // Collect all images and their labels for imageName, imageDefinition := range snapshotData.Images { @@ -69,21 +70,44 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { // Check that all images have required labels with correct values requiredLabels := support.RequiredImageLabels() for _, imageData := range imageDataList { + var currentImageErrors []string + for labelName, expectedValue := range requiredLabels { - Expect(imageData.Labels).To(HaveKey(labelName), - fmt.Sprintf("Image %s is missing '%s' label", imageData.Image, labelName)) - - if expectedValue != "" { - // Specific value expected - Expect(imageData.Labels[labelName]).To(Equal(expectedValue), - fmt.Sprintf("Image %s has incorrect '%s' label: expected '%s', got '%s'", - imageData.Image, labelName, expectedValue, imageData.Labels[labelName])) - } else { - // Label must not be empty - Expect(imageData.Labels[labelName]).NotTo(BeEmpty(), - fmt.Sprintf("Image %s has empty '%s' label", imageData.Image, labelName)) + if _, exists := imageData.Labels[labelName]; !exists { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: missing", labelName)) + continue + } + + if expectedValue != "" { // Specific value expected + if imageData.Labels[labelName] != expectedValue { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: %s, expected: %s", + labelName, imageData.Labels[labelName], expectedValue)) + } + } else { // Label must not be empty + if imageData.Labels[labelName] == "" { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: missing", labelName)) + } + } + } + + if len(currentImageErrors) > 0 { + imageLablelsErrors[imageData.Image] = currentImageErrors + } + } + + // Format errors in a human-readable way + if len(imageLablelsErrors) > 0 { + var errorReport string + for image, errors := range imageLablelsErrors { + errorReport += fmt.Sprintf("%s:\n", image) + for _, error := range errors { + errorReport += fmt.Sprintf("%s\n", error) } } + Fail(fmt.Sprintf("Label validation errors found:\n%s", errorReport)) } }) }) diff --git a/test/support/test_constants.go b/test/support/test_constants.go index 39092ec..096d4a5 100644 --- a/test/support/test_constants.go +++ b/test/support/test_constants.go @@ -94,17 +94,13 @@ func GetOSArchMatrix() OSArchMatrix { } } -// RequiredImageLabels returns a map of required image labels. -// If the value is not empty, it's the expected value for the label. -// If the value is empty, the label must exist but can have any non-empty value. +// If no value is provided, the label must exist, but can have any non-empty value. func RequiredImageLabels() map[string]string { return map[string]string{ "architecture": "x86_64", "build-date": "", - "short-commit": "", "vcs-ref": "", "vcs-type": "git", "vendor": "Red Hat, Inc.", - "version": "1.3.0", } } From e1b788d6efa17d6d94a1a818a5b8ca0caa9d99d1 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 27 Aug 2025 18:38:59 +0200 Subject: [PATCH 5/7] Lint errors repaired --- test/acceptance/releases_images_test.go | 4 ++-- test/support/common.go | 1 - test/support/image_utils.go | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index f86c376..6d44b2f 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -102,9 +102,9 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { if len(imageLablelsErrors) > 0 { var errorReport string for image, errors := range imageLablelsErrors { - errorReport += fmt.Sprintf("%s:\n", image) + errorReport += image + ":\n" for _, error := range errors { - errorReport += fmt.Sprintf("%s\n", error) + errorReport += error + "\n" } } Fail(fmt.Sprintf("Label validation errors found:\n%s", errorReport)) diff --git a/test/support/common.go b/test/support/common.go index cdc48b9..53c2147 100644 --- a/test/support/common.go +++ b/test/support/common.go @@ -94,5 +94,4 @@ func LogMapByProvidedKeys[V any](message string, data map[string]V, keysToLog [] result += fmt.Sprintf(" [%-53v] %v\n", key, data[key]) } log.Print(result) - } diff --git a/test/support/image_utils.go b/test/support/image_utils.go index dad1f9d..5bc9642 100644 --- a/test/support/image_utils.go +++ b/test/support/image_utils.go @@ -44,7 +44,9 @@ func PullImageIfNotPresentLocally(ctx context.Context, imageDefinition string) e } defer pullResp.Close() // ensure the pull operation completes. - io.Copy(io.Discard, pullResp) + if _, err := io.Copy(io.Discard, pullResp); err != nil { + return fmt.Errorf("failed to read pull response: %w", err) + } return nil } From 58866688956323850f87b56809801035d38f79b8 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Wed, 27 Aug 2025 18:41:43 +0200 Subject: [PATCH 6/7] Lint errors repaired --- test/acceptance/releases_images_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index 6d44b2f..6b269ac 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -107,7 +107,7 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { errorReport += error + "\n" } } - Fail(fmt.Sprintf("Label validation errors found:\n%s", errorReport)) + Fail("Label validation errors found:\n" + errorReport) } }) }) From 6af96339a6da975d23fd0090753ecd978fef6505 Mon Sep 17 00:00:00 2001 From: Petr Pinkas Date: Thu, 28 Aug 2025 14:45:59 +0200 Subject: [PATCH 7/7] Type repaired, strings performance improved --- test/acceptance/releases_images_test.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index 6b269ac..cd9f5f0 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -2,6 +2,7 @@ package acceptance import ( "fmt" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -53,7 +54,7 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { It("snapshot.json images have correct labels", func() { var imageDataList []support.ImageData - imageLablelsErrors := make(map[string][]string) + imageLabelsErrors := make(map[string][]string) // Collect all images and their labels for imageName, imageDefinition := range snapshotData.Images { @@ -94,20 +95,22 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { } if len(currentImageErrors) > 0 { - imageLablelsErrors[imageData.Image] = currentImageErrors + imageLabelsErrors[imageData.Image] = currentImageErrors } } // Format errors in a human-readable way - if len(imageLablelsErrors) > 0 { - var errorReport string - for image, errors := range imageLablelsErrors { - errorReport += image + ":\n" + if len(imageLabelsErrors) > 0 { + var errorReport strings.Builder + for image, errors := range imageLabelsErrors { + errorReport.WriteString(image) + errorReport.WriteString(":\n") for _, error := range errors { - errorReport += error + "\n" + errorReport.WriteString(error) + errorReport.WriteString("\n") } } - Fail("Label validation errors found:\n" + errorReport) + Fail("Label validation errors found:\n" + errorReport.String()) } }) })