201 lines
6.9 KiB
Go
201 lines
6.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
func TestRunResultEncodesRequiredFieldsAndCounts(t *testing.T) {
|
|
result, err := newRunResult(testResolvedPipeline(pipeline.DefaultOutputModule), testRunOutput(), "relative-output", "relative-debug")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
encoded, err := encodeRunResult(result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if encoded[len(encoded)-1] != '\n' || bytes.Count(encoded, []byte{'\n'}) != 1 {
|
|
t.Fatalf("encoded result is not one newline-terminated object: %q", encoded)
|
|
}
|
|
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := decoded["schema_version"]; got != runResultSchemaVersion {
|
|
t.Fatalf("schema_version = %q", got)
|
|
}
|
|
if got := decoded["run_id"]; got != "run-123" {
|
|
t.Fatalf("run_id = %q", got)
|
|
}
|
|
if got := decoded["pipeline_id"]; got != "sample" {
|
|
t.Fatalf("pipeline_id = %q", got)
|
|
}
|
|
if got := decoded["validation_status"]; got != "rejected" {
|
|
t.Fatalf("validation_status = %q", got)
|
|
}
|
|
if got := decoded["index_file"]; got != "index.json" {
|
|
t.Fatalf("index_file = %q", got)
|
|
}
|
|
if got := decoded["normalized_output_count"]; got != float64(2) {
|
|
t.Fatalf("normalized_output_count = %v", got)
|
|
}
|
|
if got := decoded["rejected_output_count"]; got != float64(1) {
|
|
t.Fatalf("rejected_output_count = %v", got)
|
|
}
|
|
if got := decoded["warning_count"]; got != float64(1) {
|
|
t.Fatalf("warning_count = %v", got)
|
|
}
|
|
if got := decoded["output_directory"]; got != filepath.Join(mustWorkingDirectory(t), "relative-output") {
|
|
t.Fatalf("output_directory = %q", got)
|
|
}
|
|
if got := decoded["debug_directory"]; got != filepath.Join(mustWorkingDirectory(t), "relative-debug") {
|
|
t.Fatalf("debug_directory = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRunResultRejectsInvalidRequiredValues(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
resolved pipeline.ResolvedPipeline
|
|
output pipeline.RunOutput
|
|
directory string
|
|
}{
|
|
{name: "blank run ID", resolved: testResolvedPipeline(pipeline.DefaultOutputModule), output: testRunOutputWithout(func(output *pipeline.RunOutput) { output.Manifest.RunID = " " }), directory: "output"},
|
|
{name: "blank resolved pipeline ID", resolved: pipeline.ResolvedPipeline{Output: pipeline.ModuleBinding{Module: pipeline.DefaultOutputModule}}, output: testRunOutput(), directory: "output"},
|
|
{name: "blank manifest pipeline ID", resolved: testResolvedPipeline(pipeline.DefaultOutputModule), output: testRunOutputWithout(func(output *pipeline.RunOutput) { output.Manifest.PipelineID = "" }), directory: "output"},
|
|
{name: "mismatched pipeline IDs", resolved: testResolvedPipeline(pipeline.DefaultOutputModule), output: testRunOutputWithout(func(output *pipeline.RunOutput) { output.Manifest.PipelineID = "other" }), directory: "output"},
|
|
{name: "blank validation status", resolved: testResolvedPipeline(pipeline.DefaultOutputModule), output: testRunOutputWithout(func(output *pipeline.RunOutput) { output.Manifest.ValidationStatus = " " }), directory: "output"},
|
|
{name: "blank output directory", resolved: testResolvedPipeline(pipeline.DefaultOutputModule), output: testRunOutput(), directory: " "},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if _, err := newRunResult(tt.resolved, tt.output, tt.directory, ""); err == nil {
|
|
t.Fatal("newRunResult() succeeded")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunResultOmitsIndexFileForOtherOutputModules(t *testing.T) {
|
|
result, err := newRunResult(testResolvedPipeline("test/output"), testRunOutputWithout(func(output *pipeline.RunOutput) {
|
|
output.OutputFiles = nil
|
|
}), "output", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.IndexFile != "" {
|
|
t.Fatalf("index_file = %q", result.IndexFile)
|
|
}
|
|
encoded, err := encodeRunResult(result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := decoded["index_file"]; ok {
|
|
t.Fatalf("encoded non-JSON result contains index_file: %s", encoded)
|
|
}
|
|
if _, ok := decoded["debug_directory"]; ok {
|
|
t.Fatalf("encoded result without debug capture contains debug_directory: %s", encoded)
|
|
}
|
|
}
|
|
|
|
func TestRunResultRequiresOneProductionIndexFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files []contracts.OutputFile
|
|
}{
|
|
{name: "missing", files: nil},
|
|
{name: "duplicate", files: []contracts.OutputFile{{Name: "index.json"}, {Name: "index.json"}}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
output := testRunOutput()
|
|
output.OutputFiles = tt.files
|
|
if _, err := newRunResult(testResolvedPipeline(pipeline.DefaultOutputModule), output, "output", ""); err == nil {
|
|
t.Fatal("newRunResult() succeeded")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteRunResultCompletesAndReportsWriterFailure(t *testing.T) {
|
|
content := []byte("result\n")
|
|
var target bytes.Buffer
|
|
if err := writeRunResult(partialResultWriter{writer: &target, limit: 2}, content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := target.String(); got != string(content) {
|
|
t.Fatalf("written result = %q", got)
|
|
}
|
|
|
|
writerErr := errors.New("result writer failed")
|
|
if err := writeRunResult(failingResultWriter{err: writerErr}, content); !errors.Is(err, writerErr) {
|
|
t.Fatalf("writeRunResult() error = %v", err)
|
|
}
|
|
if err := writeRunResult(zeroResultWriter{}, content); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Fatalf("zero-progress error = %v", err)
|
|
}
|
|
}
|
|
|
|
func testResolvedPipeline(outputModule string) pipeline.ResolvedPipeline {
|
|
return pipeline.ResolvedPipeline{ID: "sample", Output: pipeline.ModuleBinding{Module: outputModule}}
|
|
}
|
|
|
|
func testRunOutput() pipeline.RunOutput {
|
|
return pipeline.RunOutput{
|
|
Manifest: artifacts.RunManifest{RunID: "run-123", PipelineID: "sample", ValidationStatus: "rejected"},
|
|
NormalizeOutputs: []contracts.SerializedOutput{{}, {}},
|
|
Rejected: []contracts.RejectedOutput{{}},
|
|
Warnings: []contracts.Warning{{}},
|
|
OutputFiles: []contracts.OutputFile{{Name: "index.json"}},
|
|
}
|
|
}
|
|
|
|
func testRunOutputWithout(change func(*pipeline.RunOutput)) pipeline.RunOutput {
|
|
output := testRunOutput()
|
|
change(&output)
|
|
return output
|
|
}
|
|
|
|
func mustWorkingDirectory(t *testing.T) string {
|
|
t.Helper()
|
|
workingDirectory, err := filepath.Abs(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return workingDirectory
|
|
}
|
|
|
|
type partialResultWriter struct {
|
|
writer io.Writer
|
|
limit int
|
|
}
|
|
|
|
func (w partialResultWriter) Write(content []byte) (int, error) {
|
|
if len(content) > w.limit {
|
|
content = content[:w.limit]
|
|
}
|
|
return w.writer.Write(content)
|
|
}
|
|
|
|
type failingResultWriter struct{ err error }
|
|
|
|
func (w failingResultWriter) Write([]byte) (int, error) { return 0, w.err }
|
|
|
|
type zeroResultWriter struct{}
|
|
|
|
func (zeroResultWriter) Write([]byte) (int, error) { return 0, nil }
|