diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 5406d58..a946028 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -295,7 +295,7 @@ independently without save/restore choreography. ## Stage 5 — Improve Selected Large Structural Test Diagnostics -**Status:** Pending +**Status:** Completed ### Goal diff --git a/go.mod b/go.mod index 3749684..7ee6b77 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.16 github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 + github.com/google/go-cmp v0.7.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 0308ba4..3342ad0 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOIt github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/internal/app/analyze_checkpoint_recovery_test.go b/internal/app/analyze_checkpoint_recovery_test.go index 67b5d50..f368217 100644 --- a/internal/app/analyze_checkpoint_recovery_test.go +++ b/internal/app/analyze_checkpoint_recovery_test.go @@ -5,10 +5,12 @@ import ( "errors" "os" "path/filepath" - "reflect" + "slices" "testing" "time" + "github.com/google/go-cmp/cmp" + "gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" @@ -53,7 +55,7 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) { }); !errors.Is(err, failure) { t.Fatalf("executeStages() error = %v, want quest-log failure", err) } - if want := []string{"chronicle-revised", "encounters-revised", "quest-log-revised"}; !reflect.DeepEqual(failingRunner.Calls, want) { + if want := []string{"chronicle-revised", "encounters-revised", "quest-log-revised"}; !slices.Equal(failingRunner.Calls, want) { t.Fatalf("failed-run calls = %#v, want %#v", failingRunner.Calls, want) } @@ -66,8 +68,9 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) { assertAnalyzeArtifactStatus(t, failedAnalyze, "encounters", manifest.AnalyzeArtifactCurrent) assertAnalyzeArtifactStatus(t, failedAnalyze, "quest_log", manifest.AnalyzeArtifactFailed) assertAnalyzeArtifactStatus(t, failedAnalyze, "player_handout", manifest.AnalyzeArtifactStale) - if got := failedAnalyze.AnalyzeArtifacts["unrelated"]; !reflect.DeepEqual(got, unrelated) { - t.Fatalf("unrelated artifact = %#v, want preserved %#v", got, unrelated) + gotUnrelated := failedAnalyze.AnalyzeArtifacts["unrelated"] + if diff := cmp.Diff(unrelated, gotUnrelated); diff != "" { + t.Fatalf("unrelated artifact changed (-want +got):\n%s", diff) } for _, name := range []string{"publish", "notify"} { if got := failed.Stages[name]; got == nil || got.Status != manifest.StatusStale { @@ -82,7 +85,7 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) { }); err != nil { t.Fatalf("retry executeStages() error = %v", err) } - if want := []string{"quest-log-revised", "player-handout-revised"}; !reflect.DeepEqual(retryRunner.Calls, want) { + if want := []string{"quest-log-revised", "player-handout-revised"}; !slices.Equal(retryRunner.Calls, want) { t.Fatalf("retry calls = %#v, want %#v", retryRunner.Calls, want) } @@ -148,7 +151,7 @@ func TestExecuteStagesPersistsAnalyzeCheckpointFailureRecovery(t *testing.T) { if !errors.Is(err, checkpointErr) { t.Fatalf("executeStages() error = %v, want checkpoint failure", err) } - if want := []string{"chronicle", "encounters"}; !reflect.DeepEqual(runner.Calls, want) { + if want := []string{"chronicle", "encounters"}; !slices.Equal(runner.Calls, want) { t.Fatalf("adapter calls = %#v, want %#v", runner.Calls, want) } @@ -322,8 +325,12 @@ func cloneStageRecords(m *manifest.Manifest, names ...string) map[string]manifes func assertStageRecordsUnchanged(t *testing.T, m *manifest.Manifest, want map[string]manifest.StageRecord) { t.Helper() for name, expected := range want { - if got := m.Stages[name]; got == nil || !reflect.DeepEqual(*got, expected) { - t.Fatalf("upstream stage %q = %#v, want unchanged %#v", name, got, expected) + got := m.Stages[name] + if got == nil { + t.Fatalf("upstream stage %q is missing", name) + } + if diff := cmp.Diff(expected, *got); diff != "" { + t.Fatalf("upstream stage %q changed (-want +got):\n%s", name, diff) } } } diff --git a/internal/app/restore_plan_test.go b/internal/app/restore_plan_test.go index 068e83c..2d933f3 100644 --- a/internal/app/restore_plan_test.go +++ b/internal/app/restore_plan_test.go @@ -4,7 +4,7 @@ import ( "context" "os" "path/filepath" - "reflect" + "slices" "strings" "testing" @@ -32,7 +32,7 @@ func TestRestorePlanDefaultScope(t *testing.T) { got := actionRelPaths(plan.Actions) want := []string{"artifacts/session_recap.md", "manifest.json", "transcripts/full.json"} - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("action local paths = %#v, want %#v", got, want) } if plan.DownloadCount != 3 || plan.SkipSameCount != 0 || plan.ConflictCount != 0 { @@ -54,7 +54,7 @@ func TestRestorePlanIncludeAudio(t *testing.T) { } got := actionRelPaths(plan.Actions) want := []string{"audio/alice.flac", "manifest.json"} - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("action local paths = %#v, want %#v", got, want) } } @@ -112,7 +112,7 @@ func TestRestorePlanIncludesPreviousCacheByDefault(t *testing.T) { got := actionRelPaths(plan.Actions) want := []string{"manifest.json", "previous/artifacts/session_recap.md", "previous/manifest.json"} - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("action local paths = %#v, want %#v", got, want) } } @@ -133,7 +133,7 @@ func TestRestorePlanIgnoresCurrentSessionArchivedPreviousCache(t *testing.T) { got := actionRelPaths(plan.Actions) want := []string{"manifest.json"} - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("action local paths = %#v, want %#v", got, want) } } @@ -154,7 +154,7 @@ func TestRestorePlanMissingOptionalPreviousCacheSkipsArtifact(t *testing.T) { got := actionRelPaths(plan.Actions) want := []string{"manifest.json", "previous/manifest.json"} - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("action local paths = %#v, want %#v", got, want) } } diff --git a/internal/config/pipeline_composition_test.go b/internal/config/pipeline_composition_test.go index 2b35c00..81d3c9f 100644 --- a/internal/config/pipeline_composition_test.go +++ b/internal/config/pipeline_composition_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strings" "testing" ) @@ -31,7 +32,7 @@ workspace: if err != nil { t.Fatalf("splitPipelineCompositionEnvelope() error = %v", err) } - if !reflect.DeepEqual(document.root, originalRoot) || !reflect.DeepEqual(document.sources, originalSources) { + if !reflect.DeepEqual(document.root, originalRoot) || !slices.Equal(document.sources, originalSources) { t.Fatalf("splitPipelineCompositionEnvelope() mutated its input") } if compositionFieldIndex(body.root.fields, "composition") >= 0 { @@ -40,7 +41,7 @@ workspace: if compositionFieldIndex(body.root.fields, "workspace") < 0 { t.Fatalf("pipeline body lost ordinary root fields") } - if want := []string{"first.yml", "second.yml"}; !reflect.DeepEqual(envelope.imports, want) { + if want := []string{"first.yml", "second.yml"}; !slices.Equal(envelope.imports, want) { t.Fatalf("imports = %#v, want %#v", envelope.imports, want) } if len(envelope.profiles) != 2 || envelope.profiles[0].name != "production" || envelope.profiles[1].name != "testing" { @@ -126,7 +127,7 @@ storage: t.Fatal("pipeline resolution metadata = nil") } wantSources := []string{absolutePath(t, rootPath), absolutePath(t, platformPath), absolutePath(t, artifactsPath)} - if !reflect.DeepEqual(cfg.resolution.sources, wantSources) || !reflect.DeepEqual(cfg.resolution.imports, wantSources[1:]) { + if !slices.Equal(cfg.resolution.sources, wantSources) || !slices.Equal(cfg.resolution.imports, wantSources[1:]) { t.Fatalf("resolution sources=%#v imports=%#v, want %#v / %#v", cfg.resolution.sources, cfg.resolution.imports, wantSources, wantSources[1:]) } assertPipelineFieldOwner(t, cfg, "whisperx.transcribe_url", absolutePath(t, rootPath)) @@ -424,7 +425,7 @@ func assertPipelineFieldOwner(t *testing.T, cfg *PipelineConfig, path, source st } for _, ownership := range cfg.resolution.ownership { if ownership.path == path { - if !reflect.DeepEqual(ownership.sources, []string{source}) { + if !slices.Equal(ownership.sources, []string{source}) { t.Fatalf("owner of %s = %#v, want %q", path, ownership.sources, source) } return diff --git a/internal/config/pipeline_profiles_test.go b/internal/config/pipeline_profiles_test.go index 15236e5..b95e934 100644 --- a/internal/config/pipeline_profiles_test.go +++ b/internal/config/pipeline_profiles_test.go @@ -3,9 +3,11 @@ package config import ( "os" "path/filepath" - "reflect" + "slices" "strings" "testing" + + "github.com/google/go-cmp/cmp" ) func TestLoadPipelineProfilesSelectDefaultAndExplicitOverlay(t *testing.T) { @@ -45,7 +47,7 @@ whisperx: t.Fatalf("default profile result = workspace=%q language=%q", production.Workspace.Root, production.WhisperX.Language) } assertSelectedPipelineProfile(t, production, "production", "default", productionPath) - if want := []string{absolutePath(t, rootPath), absolutePath(t, importPath), absolutePath(t, productionPath)}; !reflect.DeepEqual(production.resolution.sources, want) { + if want := []string{absolutePath(t, rootPath), absolutePath(t, importPath), absolutePath(t, productionPath)}; !slices.Equal(production.resolution.sources, want) { t.Fatalf("default sources = %#v, want %#v", production.resolution.sources, want) } assertPipelineFieldOwner(t, production, "workspace.root", absolutePath(t, productionPath)) @@ -350,8 +352,8 @@ whisperx: if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(monolithicValues, composedValues) { - t.Fatalf("equivalent effective values differ:\nmonolithic=%#v\ncomposed=%#v", monolithicValues, composedValues) + if diff := cmp.Diff(monolithicValues, composedValues); diff != "" { + t.Fatalf("effective values differ (-monolithic +composed):\n%s", diff) } } diff --git a/internal/stage/analyze_plan_test.go b/internal/stage/analyze_plan_test.go index afe6b2a..3025950 100644 --- a/internal/stage/analyze_plan_test.go +++ b/internal/stage/analyze_plan_test.go @@ -2,11 +2,13 @@ package stage import ( "path/filepath" - "reflect" + "slices" "strings" "testing" "time" + "github.com/google/go-cmp/cmp" + "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/config" @@ -151,7 +153,7 @@ func TestPlanAnalyzeWorkSelectionAndDependencyClosure(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(plan.ExplicitTargets, tt.wantTargets) { + if !slices.Equal(plan.ExplicitTargets, tt.wantTargets) { t.Fatalf("targets = %#v, want %#v", plan.ExplicitTargets, tt.wantTargets) } assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, tt.wantExecution) @@ -163,7 +165,7 @@ func TestPlanAnalyzeWorkSelectionAndDependencyClosure(t *testing.T) { forced = append(forced, item.Key) } } - if !(len(forced) == 0 && len(tt.wantForced) == 0) && !reflect.DeepEqual(forced, tt.wantForced) { + if !(len(forced) == 0 && len(tt.wantForced) == 0) && !slices.Equal(forced, tt.wantForced) { t.Fatalf("forced = %#v, want %#v", forced, tt.wantForced) } }) @@ -263,8 +265,16 @@ func TestPlanAnalyzeWorkStartingRecordsPreserveOnlyValidConfiguredAuthority(t *t } assertAnalyzePlanKeys(t, "invalidated", plan.Invalidated, []string{"invalid_unselected"}) assertAnalyzePlanKeys(t, "removed", plan.Removed, []string{"removed"}) - if !reflect.DeepEqual(before["unrelated"], *stored["unrelated"]) || !reflect.DeepEqual(before["invalid_unselected"], *stored["invalid_unselected"]) { - t.Fatal("planning mutated stored records") + gotUnchanged := map[string]manifest.AnalyzeArtifactRecord{ + "unrelated": *stored["unrelated"], + "invalid_unselected": *stored["invalid_unselected"], + } + wantUnchanged := map[string]manifest.AnalyzeArtifactRecord{ + "unrelated": before["unrelated"], + "invalid_unselected": before["invalid_unselected"], + } + if diff := cmp.Diff(wantUnchanged, gotUnchanged); diff != "" { + t.Fatalf("planning mutated stored records (-before +after):\n%s", diff) } } @@ -306,7 +316,7 @@ func TestPlanAnalyzeWorkOrderIsDeterministicAcrossMapInsertion(t *testing.T) { t.Fatal(err) } got := analyzePlanKeys(plan.ExecutionOrder) - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("attempt %d order = %#v, want %#v", attempt, got, want) } } @@ -350,7 +360,7 @@ func assertAnalyzePlanKeys(t *testing.T, label string, items []analyzePlanItem, if len(got) == 0 && len(want) == 0 { return } - if !reflect.DeepEqual(got, want) { + if !slices.Equal(got, want) { t.Fatalf("%s = %#v, want %#v", label, got, want) } } diff --git a/internal/stage/analyze_reconciliation_test.go b/internal/stage/analyze_reconciliation_test.go index 7aa174e..b0ad60b 100644 --- a/internal/stage/analyze_reconciliation_test.go +++ b/internal/stage/analyze_reconciliation_test.go @@ -2,9 +2,10 @@ package stage import ( "path/filepath" - "reflect" "testing" + "github.com/google/go-cmp/cmp" + "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" @@ -164,8 +165,11 @@ func TestReconcileAnalyzeArtifactsIsReadOnly(t *testing.T) { if _, err := reconcileAnalyzeArtifacts(env.Config.Pipeline.Scriptorium, newAnalyzeIdentityExecution(t, env, m)); err != nil { t.Fatal(err) } - if !reflect.DeepEqual(before, m.Stages["analyze"].AnalyzeArtifacts) || !m.UpdatedAt.Equal(beforeUpdatedAt) { - t.Fatal("reconciliation mutated manifest state") + if diff := cmp.Diff(before, m.Stages["analyze"].AnalyzeArtifacts); diff != "" { + t.Fatalf("reconciliation mutated artifact state (-before +after):\n%s", diff) + } + if !m.UpdatedAt.Equal(beforeUpdatedAt) { + t.Fatalf("reconciliation changed manifest update time: before=%s after=%s", beforeUpdatedAt, m.UpdatedAt) } } diff --git a/internal/stage/publish_output_plan_test.go b/internal/stage/publish_output_plan_test.go index 9b13d5d..8fb6dbc 100644 --- a/internal/stage/publish_output_plan_test.go +++ b/internal/stage/publish_output_plan_test.go @@ -1,11 +1,12 @@ package stage import ( - "reflect" "strings" "testing" "time" + "github.com/google/go-cmp/cmp" + "gitea.maximumdirect.net/eric/narratio/internal/artifacts" ) @@ -56,8 +57,8 @@ func TestBuildPublishOutputPlanModelsEveryOutcome(t *testing.T) { t.Fatalf("outcomes[%d] = %#v, want source %q status %q", index, plan.Outcomes[index], wantSources[index], wantStatuses[index]) } } - if !reflect.DeepEqual(plan.Outcomes[1].Declaration, carried) { - t.Fatalf("carried declaration = %#v, want exact %#v", plan.Outcomes[1].Declaration, carried) + if diff := cmp.Diff(carried, plan.Outcomes[1].Declaration); diff != "" { + t.Fatalf("carried declaration differs (-want +got):\n%s", diff) } if plan.Outcomes[1].Destination != "artifacts/beta-old.md" || plan.Outcomes[1].OriginRun != "20260512T010203Z-11223344" { t.Fatalf("carried outcome = %#v", plan.Outcomes[1])