From b32af34cad475f27cef49495be197344911a8049 Mon Sep 17 00:00:00 2001 From: Michael McQuade Date: Fri, 21 Aug 2026 15:10:41 -0500 Subject: [PATCH] test(generator): pin that generating a spec twice gives the same bytes Generated clients are committed and reviewed, so output that shifts between runs shows up as churn in a diff nobody made. Nothing held the property, and the usual way to lose it is a map iterated somewhere on the way out. Generating the corpus spec twice and comparing the bytes holds it. The five real specs I have are deterministic today, so this adds no fix. The corpus gains a second named error shape and one with no name of its own, which the guard needed: with a single error type in the package, iterating the map that holds them is stable however it is written, and the test could not fail. With two, disabling the ordering makes it fail on errors.go, which is what a guard has to do to be worth having. --- internal/generator/e2e_combinations_test.go | 51 +++++++++++++++++++++ testdata/combinations.yaml | 14 ++++++ 2 files changed, 65 insertions(+) diff --git a/internal/generator/e2e_combinations_test.go b/internal/generator/e2e_combinations_test.go index edbcd9a..3ad7555 100644 --- a/internal/generator/e2e_combinations_test.go +++ b/internal/generator/e2e_combinations_test.go @@ -77,6 +77,8 @@ func TestE2E_Combinations(t *testing.T) { {"operations.go", "params.Either", "a union-typed parameter"}, {"errors.go", "func (e *ProblemResponse) Error() string", "a typed error wrapper"}, {"errors.go", "e.Detail.Detail", "a message field found by its conventional name"}, + {"errors.go", "func parseFailureResponse", "a second error shape in one package"}, + {"errors.go", "func parseListAlertsResponse422Error", "an error body with no name of its own"}, {"types.go", "Name string `json:\"name\"`", "a property two allOf entries declare, required by one"}, } { if !containsCollapsed(byName[want.file], want.decl) { @@ -99,3 +101,52 @@ func TestE2E_Combinations(t *testing.T) { func containsCollapsed(haystack, needle string) bool { return strings.Contains(strings.Join(strings.Fields(haystack), " "), strings.Join(strings.Fields(needle), " ")) } + +// TestGenerationIsDeterministic generates one spec twice and compares the bytes. +// Generated clients are committed and reviewed, so output that shifts between +// runs shows up as churn in a diff nobody made, and the usual cause is a map +// iterated somewhere on the way out. +func TestGenerationIsDeterministic(t *testing.T) { + specPath := filepath.Join(projectRoot(), "testdata", "combinations.yaml") + + generate := func() map[string]string { + t.Helper() + result, err := parser.Parse(specPath, parser.Config{}) + if err != nil { + t.Fatalf("Parse: %v", err) + } + pkg, err := analyzer.New(result.Model).Analyze("combinations") + if err != nil { + t.Fatalf("Analyze: %v", err) + } + gen, err := New(pkg) + if err != nil { + t.Fatalf("New: %v", err) + } + files, err := gen.Generate() + if err != nil { + t.Fatalf("Generate: %v", err) + } + out := make(map[string]string, len(files)) + for _, f := range files { + out[f.Name] = string(f.Content) + } + return out + } + + first, second := generate(), generate() + + if len(first) != len(second) { + t.Fatalf("file counts differ: %d and %d", len(first), len(second)) + } + for name, content := range first { + other, ok := second[name] + if !ok { + t.Errorf("%s was generated once and not the other time", name) + continue + } + if content != other { + t.Errorf("%s differs between two runs of the same spec", name) + } + } +} diff --git a/testdata/combinations.yaml b/testdata/combinations.yaml index 7a5bfcc..56acc20 100644 --- a/testdata/combinations.yaml +++ b/testdata/combinations.yaml @@ -95,6 +95,15 @@ paths: content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/Alert" } } + "422": + description: An error body with no name of its own. + content: + application/json: + schema: { type: array, items: { type: string } } + default: + description: A second named error shape, so the package holds more than one. + content: + application/json: { schema: { $ref: "#/components/schemas/Failure" } } /reports: get: operationId: listReports @@ -213,6 +222,11 @@ components: "^x-": { type: string } refined: { $ref: "#/components/schemas/Refined" } required: [id] + Failure: + type: object + properties: + code: { type: integer } + message: { type: string } Problem: type: object properties: