Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #705 (2022-06-23 16:52:54)

updating to latest version of go-rpmdb
- Fixes: #694 
- Update to latest version of `go-rpmdb` in opdev fork from branch `preflight-next`
- Includes a fix for a change in upstream that renamed a method that we were using 
- Includes changes to support `mongo-cli` and other `rpm`s that have a `RPMTAG_SUMMARY` header tag as a value of `6` (STRING) instead of the expect value of `9` (I18NSTRING) 

Testing:
I tested the same image before and after this change and got the same list of files before/after. And the new method and old method look the same to me in the `go-rpmdb` codebase. But I would be interested in better/other ways to make sure this doesn't introduce unintended consequences, thoughts?

Signed-off-by: Adam D. Cornett <adc@redhat.com>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/50307384/badge)](https://coveralls.io/builds/50307384)

Coverage remained the same at 61.67% when pulling **2770174a08a55d3ffe95d4a54c8da14cb98de17c on acornett21:update_go_rpmdb_dependency** into **d4305e841e29fdfdb013cc98d544e5f657e44ce9 on redhat-openshift-ecosystem:main**.

dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/cd58b150-c65c-405e-b6f0-9cd2a1014694/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/705

Patch:
diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml
index 60fb768..18fea53 100644
--- a/.github/workflows/build-multiarch.yml
+++ b/.github/workflows/build-multiarch.yml
@@ -33,6 +33,14 @@ jobs:
     name: create and push a multiarch manifest to the repo
     runs-on: ubuntu-latest
     steps:
+      # Authenticate to container image registry to push the image
+      - name: Podman Login
+        uses: redhat-actions/podman-login@v1
+        with:
+          registry: ${{ secrets.registry }}
+          username: ${{ secrets.user }}
+          password: ${{ secrets.password }}
+
       - name: Create and add to manifest
         run: |
           buildah manifest create ${{ inputs.name }}
@@ -41,14 +49,6 @@ jobs:
           buildah manifest add ${{ inputs.name }} ${{ secrets.registry }}/${{ inputs.name }}:${{ inputs.tag }}-linux-arm64
           buildah manifest add ${{ inputs.name }} ${{ secrets.registry }}/${{ inputs.name }}:${{ inputs.tag }}-linux-s390x
 
-      # Authenticate to container image registry to push the image
-      - name: Podman Login
-        uses: redhat-actions/podman-login@v1
-        with:
-          registry: ${{ secrets.registry }}
-          username: ${{ secrets.user }}
-          password: ${{ secrets.password }}
-          
       - name: Push manifest
         run: |
             podman manifest push ${{ inputs.name }} ${{ secrets.registry }}/${{ inputs.name }}:${{ inputs.tag }}  --all
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 4d34c0e..5d15f7e 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -70,12 +70,13 @@ jobs:
   extract-assets:
     needs: build-release
     uses: ./.github/workflows/release-artifacts.yml
-
     with:
       name: ${{ needs.build-release.outputs.imageName }}
       tag: ${{ needs.build-release.outputs.imageVersion }}
     secrets:
       registry: ${{ secrets.IMAGE_REGISTRY }}
+      user: ${{ secrets.REGISTRY_USER }}
+      password: ${{ secrets.REGISTRY_PASSWORD }}
       token: ${{ secrets.GITHUB_TOKEN }}
 
   copy-to-rhisv:
diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml
index ea5b829..fbd740a 100644
--- a/.github/workflows/release-artifacts.yml
+++ b/.github/workflows/release-artifacts.yml
@@ -12,6 +12,10 @@ on:
     secrets:
       registry:
         required: true
+      user:
+        required: true
+      password:
+        required: true
       token:
         required: true
         
@@ -28,6 +32,13 @@ jobs:
           - ppc64le
           - s390x
     steps:
+    - name: Docker Login
+      uses: docker/login-action@v1
+      with:
+        registry: ${{ secrets.registry }}
+        username: ${{ secrets.user }}
+        password: ${{ secrets.password }}
+
     - name: Extract executable
       uses: shrink/actions-docker-extract@v1
       id: extract
diff --git a/certification/formatters/defaults.go b/certification/formatters/defaults.go
new file mode 100644
index 0000000..aaf6876
--- /dev/null
+++ b/certification/formatters/defaults.go
@@ -0,0 +1,5 @@
+package formatters
+
+const (
+	DefaultFormat = "json"
+)
diff --git a/certification/formatters/errors.go b/certification/formatters/errors.go
new file mode 100644
index 0000000..5c138c8
--- /dev/null
+++ b/certification/formatters/errors.go
@@ -0,0 +1,5 @@
+package formatters
+
+import "errors"
+
+var ErrUnknownFormatter = errors.New("requested format is unknown")
diff --git a/certification/formatters/formatters.go b/certification/formatters/formatters.go
index bceeeae..a32d999 100644
--- a/certification/formatters/formatters.go
+++ b/certification/formatters/formatters.go
@@ -30,11 +30,17 @@ type FormatterFunc = func(context.Context, runtime.Results) (response []byte, fo
 // NewForConfig returns a new formatter based on the user-provided configuration. It relies
 // on config values which should align with known/supported/built-in formatters.
 func NewForConfig(cfg certification.Config) (ResponseFormatter, error) {
-	formatter, defined := availableFormatters[cfg.ResponseFormat()]
+	return NewByName(cfg.ResponseFormat())
+}
+
+// NewByName returns a predefined ResponseFormatter with the given name.
+// TODO: New* funcs in this package may benefit from renaming.
+func NewByName(name string) (ResponseFormatter, error) {
+	formatter, defined := availableFormatters[name]
 	if !defined {
-		return nil, fmt.Errorf(
-			"failed to create a new formatter from config: %s",
-			cfg.ResponseFormat(),
+		return nil, fmt.Errorf("%w: %s",
+			ErrUnknownFormatter,
+			name,
 		)
 	}
 
diff --git a/certification/internal/policy/container/has_modified_files.go b/certification/internal/policy/container/has_modified_files.go
index 37384eb..a42870a 100644
--- a/certification/internal/policy/container/has_modified_files.go
+++ b/certification/internal/policy/container/has_modified_files.go
@@ -49,7 +49,7 @@ func (p *HasModifiedFilesCheck) getDataToValidate(ctx context.Context, imgRef ce
 	// installed packages.
 	packageFiles := make(map[string]struct{}, len(pkgList))
 	for _, pkg := range pkgList {
-		filenames, err := pkg.InstalledFiles()
+		filenames, err := pkg.InstalledFileNames()
 		if err != nil {
 			return nil, fmt.Errorf("could not list installed files: %w", err)
 		}
diff --git a/certification/pyxis/builder.go b/certification/pyxis/builder.go
index 121b611..a20b8fe 100644
--- a/certification/pyxis/builder.go
+++ b/certification/pyxis/builder.go
@@ -13,7 +13,7 @@ import (
 // certificationInputBuilder facilitates the building of CertificationInput for
 // submitting an asset to Pyxis.
 type certificationInputBuilder struct {
-	certificationInput
+	CertificationInput
 }
 
 // NewCertificationInput accepts required values for submitting to Pyxis, and returns a CertificationInputBuilder for
@@ -25,7 +25,7 @@ func NewCertificationInput(project *CertProject) (*certificationInputBuilder, er
 	}
 
 	b := certificationInputBuilder{
-		certificationInput: certificationInput{
+		CertificationInput: CertificationInput{
 			CertProject: project,
 		},
 	}
@@ -39,7 +39,7 @@ func NewCertificationInput(project *CertProject) (*certificationInputBuilder, er
 // unmodifiable CertificationInput.
 //
 // If any required values are not included, an error is thrown.
-func (b *certificationInputBuilder) Finalize() (*certificationInput, error) {
+func (b *certificationInputBuilder) Finalize() (*CertificationInput, error) {
 	// safeguards, make sure things aren't nil for any reason.
 	if b.CertImage == nil {
 		return nil, fmt.Errorf("a CertImage was not provided and is required")
@@ -61,7 +61,7 @@ func (b *certificationInputBuilder) Finalize() (*certificationInput, error) {
 	b.CertImage.ISVPID = b.CertProject.Container.ISVPID
 	b.CertImage.Certified = b.TestResults.Passed
 
-	return &b.certificationInput, nil
+	return &b.CertificationInput, nil
 }
 
 // WithCertImage adds a pyxis.CertImage from the passed io.Reader to the CertificationInput.
diff --git a/certification/pyxis/submit.go b/certification/pyxis/submit.go
index a3e74a2..e1d44de 100644
--- a/certification/pyxis/submit.go
+++ b/certification/pyxis/submit.go
@@ -12,7 +12,7 @@ var defaultRegistryAlias = "docker.io"
 
 // SubmitResults takes certInput and sends requests to Pyxis to create or update entries
 // based on certInput.
-func (p *pyxisClient) SubmitResults(ctx context.Context, certInput *certificationInput) (*CertificationResults, error) {
+func (p *pyxisClient) SubmitResults(ctx context.Context, certInput *CertificationInput) (*CertificationResults, error) {
 	var err error
 
 	certProject := certInput.CertProject
diff --git a/certification/pyxis/types.go b/certification/pyxis/types.go
index 2e74e65..27fe87a 100644
--- a/certification/pyxis/types.go
+++ b/certification/pyxis/types.go
@@ -6,7 +6,7 @@ import (
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters"
 )
 
-type certificationInput struct {
+type CertificationInput struct {
 	CertProject *CertProject
 	CertImage   *CertImage
 	TestResults *TestResults
diff --git a/certification/runtime/result_writer.go b/certification/runtime/result_writer.go
new file mode 100644
index 0000000..9603334
--- /dev/null
+++ b/certification/runtime/result_writer.go
@@ -0,0 +1,33 @@
+package runtime
+
+import (
+	"io"
+	"os"
+)
+
+// ResultWriterFile implements a resultWriter for use at preflight runtime.
+type ResultWriterFile struct {
+	file *os.File
+}
+
+// OpenFile will open the expected file for writing.
+func (f *ResultWriterFile) OpenFile(name string) (io.WriteCloser, error) {
+	file, err := os.OpenFile(
+		name,
+		os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+		0o600)
+	if err != nil {
+		return nil, err
+	}
+
+	f.file = file // so we can close it later.
+	return f, nil
+}
+
+func (f *ResultWriterFile) Close() error {
+	return f.file.Close()
+}
+
+func (f *ResultWriterFile) Write(p []byte) (int, error) {
+	return f.file.Write(p)
+}
diff --git a/cmd/check_container.go b/cmd/check_container.go
index 8a0a377..7729b58 100644
--- a/cmd/check_container.go
+++ b/cmd/check_container.go
@@ -64,7 +64,7 @@ func checkContainerRunE(cmd *cobra.Command, args []string) error {
 
 	// Set our runtime defaults.
 	cfg.Image = containerImage
-	cfg.ResponseFormat = DefaultOutputFormat
+	cfg.ResponseFormat = formatters.DefaultFormat
 
 	// Run the  container check.
 	cmd.SilenceUsage = true
diff --git a/cmd/check_operator.go b/cmd/check_operator.go
index d1a1330..7274610 100644
--- a/cmd/check_operator.go
+++ b/cmd/check_operator.go
@@ -56,7 +56,7 @@ func checkOperatorRunE(cmd *cobra.Command, args []string) error {
 	}
 
 	cfg.Image = operatorImage
-	cfg.ResponseFormat = DefaultOutputFormat
+	cfg.ResponseFormat = formatters.DefaultFormat
 	cfg.Bundle = true
 	cfg.Scratch = true
 
diff --git a/cmd/defaults.go b/cmd/defaults.go
index d71dd7e..813d749 100644
--- a/cmd/defaults.go
+++ b/cmd/defaults.go
@@ -1,7 +1,6 @@
 package cmd
 
 var (
-	DefaultOutputFormat      = "json"
 	DefaultLogFile           = "preflight.log"
 	DefaultLogLevel          = "info"
 	DefaultNamespace         = "default"
diff --git a/cmd/types.go b/cmd/types.go
new file mode 100644
index 0000000..df45c4c
--- /dev/null
+++ b/cmd/types.go
@@ -0,0 +1,176 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"time"
+
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/pyxis"
+	log "github.com/sirupsen/logrus"
+)
+
+// resultWriter defines methods associated with writing check results.
+type resultWriter interface {
+	OpenFile(name string) (io.WriteCloser, error)
+	io.WriteCloser
+}
+
+// resultSubmitter defines methods associated with submitting results to Red HAt.
+type resultSubmitter interface {
+	Submit(context.Context) error
+}
+
+// pyxisClient defines pyxis API interactions that are relevant to check executions in cmd.
+type pyxisClient interface {
+	FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error)
+	GetProject(context.Context) (*pyxis.CertProject, error)
+	SubmitResults(context.Context, *pyxis.CertificationInput) (*pyxis.CertificationResults, error)
+}
+
+// newPyxisClient initializes a pyxisClient with relevant information from cfg.
+// If the the CertificationProjectID, PyxisAPIToken, or PyxisHost are empty, then nil is returned.
+// Callers should treat a nil pyxis client as an indicator that pyxis calls should not be made.
+func newPyxisClient(ctx context.Context, cfg certification.Config) pyxisClient {
+	if cfg.CertificationProjectID() == "" || cfg.PyxisAPIToken() == "" || cfg.PyxisHost() == "" {
+		return nil
+	}
+
+	return pyxis.NewPyxisClient(
+		cfg.PyxisHost(),
+		cfg.PyxisAPIToken(),
+		cfg.CertificationProjectID(),
+		&http.Client{Timeout: 60 * time.Second},
+	)
+}
+
+// containerCertificationSubmitter submits container results to Pyxis, and implements
+// a resultSubmitter.
+type containerCertificationSubmitter struct {
+	certificationProjectID string
+	pyxis                  pyxisClient
+	dockerConfig           string
+	preflightLogFile       string
+}
+
+func (s *containerCertificationSubmitter) Submit(ctx context.Context) error {
+	log.Info("preparing results that will be submitted to Red Hat")
+
+	// get the project info from pyxis
+	certProject, err := s.pyxis.GetProject(ctx)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project: %w", err)
+	}
+
+	// Ensure that a certProject was returned. In theory we would expect pyxis
+	// to throw an error if no project is returned, but in the event that it doesn't
+	// we need to confirm before we proceed in order to prevent a runtime panic
+	// setting the DockerConfigJSON below.
+	if certProject == nil {
+		return fmt.Errorf("no certification project was returned from pyxis")
+	}
+
+	log.Tracef("CertProject: %+v", certProject)
+
+	// read the provided docker config
+	dockerConfigJsonBytes, err := os.ReadFile(s.dockerConfig)
+	if err != nil {
+		return err
+	}
+
+	certProject.Container.DockerConfigJSON = string(dockerConfigJsonBytes)
+
+	// prepare submission. We ignore the error because nil checks for the certProject
+	// are done earlier to prevent panics, and that's the only error case for this function.
+	submission, _ := pyxis.NewCertificationInput(certProject)
+
+	certImage, err := os.Open(path.Join(artifacts.Path(), certification.DefaultCertImageFilename))
+	defer certImage.Close()
+	if err != nil {
+		return fmt.Errorf("could not open file for submission: %s: %w",
+			certification.DefaultCertImageFilename,
+			err,
+		)
+	}
+	preflightResults, err := os.Open(path.Join(artifacts.Path(), certification.DefaultTestResultsFilename))
+	defer preflightResults.Close()
+	if err != nil {
+		return fmt.Errorf(
+			"could not open file for submission: %s: %w",
+			certification.DefaultTestResultsFilename,
+			err,
+		)
+	}
+	rpmManifest, err := os.Open(path.Join(artifacts.Path(), certification.DefaultRPMManifestFilename))
+	defer rpmManifest.Close()
+	if err != nil {
+		return fmt.Errorf(
+			"could not open file for submission: %s: %w",
+			certification.DefaultRPMManifestFilename,
+			err,
+		)
+	}
+	logfile, err := os.Open(s.preflightLogFile)
+	defer logfile.Close()
+	if err != nil {
+		return fmt.Errorf(
+			"could not open file for submission: %s: %w",
+			s.preflightLogFile,
+			err,
+		)
+	}
+	submission.
+		// The engine writes the certified image config to disk in a Pyxis-specific format.
+		WithCertImage(certImage).
+		// Include Preflight's test results in our submission. pyxis.TestResults embeds them.
+		WithPreflightResults(preflightResults).
+		// The certification engine writes the rpmManifest for images not based on scratch.
+		WithRPMManifest(rpmManifest).
+		// Include the preflight execution log file.
+		WithArtifact(logfile, filepath.Base(s.preflightLogFile))
+
+	input, err := submission.Finalize()
+	if err != nil {
+		return fmt.Errorf("unable to finalize data that would be sent to pyxis: %w", err)
+	}
+
+	certResults, err := s.pyxis.SubmitResults(ctx, input)
+	if err != nil {
+		return fmt.Errorf("could not submit to pyxis: %w", err)
+	}
+
+	log.Info("Test results have been submitted to Red Hat.")
+	log.Info("These results will be reviewed by Red Hat for final certification.")
+	log.Infof("The container's image id is: %s.", certResults.CertImage.ID)
+	log.Infof("Please check %s to view scan results.", buildScanResultsURL(s.certificationProjectID, certResults.CertImage.ID))
+	log.Infof("Please check %s to monitor the progress.", buildOverviewURL(s.certificationProjectID))
+
+	return nil
+}
+
+// noopSubmitter is a no-op resultSubmitter that optionally logs a message
+// and a reason as to why results were not submitted.
+type noopSubmitter struct {
+	emitLog bool
+	reason  string
+	log     *log.Logger
+}
+
+func (s *noopSubmitter) Submit(ctx context.Context) error {
+	if s.emitLog {
+		msg := "Results are not being sent for submission."
+		if s.reason != "" {
+			msg = fmt.Sprintf("%s Reason: %s.", msg, s.reason)
+		}
+
+		s.log.Info(msg)
+	}
+
+	return nil
+}
diff --git a/go.mod b/go.mod
index fdf4dca..9fc61a4 100644
--- a/go.mod
+++ b/go.mod
@@ -105,4 +105,4 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
 )
 
-replace github.com/knqyf263/go-rpmdb v0.0.0-20201215100354-a9e3110d8ee1 => github.com/opdev/go-rpmdb v0.0.0-20220329221310-ca9c80a97804
+replace github.com/knqyf263/go-rpmdb v0.0.0-20201215100354-a9e3110d8ee1 => github.com/opdev/go-rpmdb v0.0.0-20220622214440-9ad97f393868
diff --git a/go.sum b/go.sum
index f7693a9..3990493 100644
--- a/go.sum
+++ b/go.sum
@@ -685,8 +685,8 @@ github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
-github.com/opdev/go-rpmdb v0.0.0-20220329221310-ca9c80a97804 h1:SAMvnxdkgSBMB7Ak4sFdeKzzEBvg/eOHRM2h/l3QlJg=
-github.com/opdev/go-rpmdb v0.0.0-20220329221310-ca9c80a97804/go.mod h1:nrkAFnlGZhIkW8bAwWK1PE0rCnMe8uKCUzwrFHpfEsM=
+github.com/opdev/go-rpmdb v0.0.0-20220622214440-9ad97f393868 h1:XXxyt5fiiDKjF0rkzvkre52rznX6Vsib11xMqAvT8Q4=
+github.com/opdev/go-rpmdb v0.0.0-20220622214440-9ad97f393868/go.mod h1:ZQDkL8JLWXdoduwq/aZGDfI3Cn0FT5ta1yhDcb3yQAE=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 h1:+czc/J8SlhPKLOtVLMQc+xDCFBT73ZStMsRhSsUhsSg=


Test patch:
diff --git a/certification/formatters/formatters_test.go b/certification/formatters/formatters_test.go
index 41eb38a..2de88e7 100644
--- a/certification/formatters/formatters_test.go
+++ b/certification/formatters/formatters_test.go
@@ -10,6 +10,12 @@ import (
 )
 
 var _ = Describe("Formatters", func() {
+	Describe("When getting the formatter for the named default format", func() {
+		It("should never fail", func() {
+			_, err := NewByName(DefaultFormat)
+			Expect(err).ToNot(HaveOccurred())
+		})
+	})
 	Describe("When getting a new formatter for a configuration", func() {
 		Context("with a valid configuration", func() {
 			cfg := runtime.Config{
diff --git a/certification/pyxis/builder_test.go b/certification/pyxis/builder_test.go
index 7f4fb63..e9d8f3d 100644
--- a/certification/pyxis/builder_test.go
+++ b/certification/pyxis/builder_test.go
@@ -60,7 +60,7 @@ var _ = Describe("Pyxis Builder tests", func() {
 			})
 
 			It("should return a certification input builder with the project embedded", func() {
-				Expect(builder.certificationInput.CertProject.ID).To(Equal(p.ID))
+				Expect(builder.CertificationInput.CertProject.ID).To(Equal(p.ID))
 			})
 
 			It("should fail to finalize with no cert image", func() {
diff --git a/certification/pyxis/submit_test.go b/certification/pyxis/submit_test.go
index d0005bc..65e8811 100644
--- a/certification/pyxis/submit_test.go
+++ b/certification/pyxis/submit_test.go
@@ -12,7 +12,7 @@ var _ = Describe("Pyxis Submit", func() {
 	ctx := context.Background()
 
 	var pyxisClient *pyxisClient
-	var certInput certificationInput
+	var certInput CertificationInput
 	mux := http.NewServeMux()
 
 	// These go from most explicit to least explicit. They will be check that way by the ServeMux.
@@ -29,7 +29,7 @@ var _ = Describe("Pyxis Submit", func() {
 			"my-awesome-project-id",
 			&http.Client{Transport: localRoundTripper{handler: mux}},
 		)
-		certInput = certificationInput{
+		certInput = CertificationInput{
 			CertProject: &CertProject{CertificationStatus: "Started"},
 			CertImage: &CertImage{
 				Repositories: []Repository{
diff --git a/certification/runtime/result_writer_test.go b/certification/runtime/result_writer_test.go
new file mode 100644
index 0000000..3510c2b
--- /dev/null
+++ b/certification/runtime/result_writer_test.go
@@ -0,0 +1,52 @@
+package runtime
+
+import (
+	"os"
+	"path"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Result Writers", func() {
+	var resultWriterTestDir string
+
+	BeforeEach(func() {
+		// instantiate err to make sure we can equal-assign in the following line.
+		var err error
+		resultWriterTestDir, err = os.MkdirTemp(os.TempDir(), "rw-test-*")
+		Expect(err).ToNot(HaveOccurred())
+		Expect(len(resultWriterTestDir)).ToNot(BeZero())
+	})
+
+	AfterEach(func() {
+		err := os.RemoveAll(resultWriterTestDir)
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	Context("File-based Result Writer", func() {
+		var rw *ResultWriterFile
+
+		BeforeEach(func() {
+			rw = &ResultWriterFile{}
+		})
+
+		Context("When using the file-based result writer", func() {
+			It("should open, be writeable, and close successfully", func() {
+				p := path.Join(resultWriterTestDir, "foo.txt")
+				f, err := rw.OpenFile(p)
+				defer f.Close()
+				Expect(err).ToNot(HaveOccurred())
+				Expect(p).To(BeAnExistingFile())
+
+				toWrite := []byte("testing")
+				_, err = f.Write(toWrite)
+				Expect(err).ToNot(HaveOccurred())
+
+				contents, err := os.ReadFile(p)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(contents).To(Equal(toWrite))
+			})
+		})
+	})
+})