package buildstep import ( "context" "errors" "os" "path/filepath" "reflect" "sort" "strings" "testing" "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/seatgen" sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // contracts writes a contracts module of one seat, its generated files as the generator writes them, and answers its // directory and the generated paths. func contracts(t *testing.T, manifest string) (string, []string) { t.Helper() dir := t.TempDir() for p, text := range map[string]string{ "module.json": manifest, "seats/one-store/seat.json": `{"format":1,"seat":"one-store","scope":"mesh","grain":"whole","domain":"Module","decision":"ADR 1","verbs":[{"name":"query","input":{"type":"object"},"answer":{"type":"object"}}]}`, // A definition at the seat-list grain is read and generates nothing (ADR 0635 §1, §2). "seats/other/seat.json": `{"format":1,"seat":"other","scope":"mesh","grain":"seat-list","domain":"Module","decision":"ADR 1","verbs":[{"name":"ask","input":{"type":"object"}}]}`, } { writeContract(t, filepath.Join(dir, p), text) } m, err := seatgen.Load(dir, "example.org/c/go", "@example/c") if err != nil { t.Fatal(err) } files, err := seatgen.Generate(m) if err != nil { t.Fatal(err) } var paths []string for p, b := range files { writeContract(t, filepath.Join(dir, p), string(b)) paths = append(paths, p) } return dir, paths } func writeContract(t *testing.T, path, text string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(text), 0o644); err != nil { t.Fatal(err) } } // **CAT-BG5, CAT-BN2**: a contracts module's generated files are regenerated and compared, each build module its own // root; a file that differs, one missing and one the generator did not write are each named; a clean tree and a module // naming no generated-from name none; a build ask answers them in its out-of-step. func TestAContractsModulesGeneratedFilesAreComparedWithTheGenerator(t *testing.T) { marked := `{"module":"c","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"],"generated-from":"."}]}}` dir, paths := contracts(t, marked) var goFile, tsFile string for _, p := range paths { if strings.HasPrefix(p, "go/") && strings.HasSuffix(p, ".go") && goFile == "" { goFile = p } if strings.HasPrefix(p, "ts/") && strings.HasSuffix(p, ".ts") && tsFile == "" { tsFile = p } } for _, p := range paths { if strings.Contains(p, "other") || p == "go/go.mod" || p == "ts/package.json" { t.Errorf("%s is generated: a seat-list definition generates nothing and a contracts module has no root package", p) } } if goFile == "" || tsFile == "" { t.Fatalf("the generator wrote no Go or no TypeScript file: %v", paths) } for _, root := range []string{GeneratedRootGo, GeneratedRootTS} { if got, err := OutOfStep(dir, root); err != nil || len(got) != 0 { t.Fatalf("a clean tree, root %s: %v %v", root, got, err) } } writeContract(t, filepath.Join(dir, goFile), "package changed\n") writeContract(t, filepath.Join(dir, "go/extra.go"), "package extra\n") writeContract(t, filepath.Join(dir, tsFile), "// changed\n") got, err := OutOfStep(dir, GeneratedRootGo) if want := sorted(goFile, "go/extra.go"); err != nil || !reflect.DeepEqual(got, want) { t.Errorf("Go root: %v %v, want %v", got, err, want) } if got, _ := OutOfStep(dir, GeneratedRootTS); !reflect.DeepEqual(got, []string{tsFile}) { t.Errorf("TypeScript root: %v, want %s alone", got, tsFile) } _ = os.Remove(filepath.Join(dir, goFile)) if got, _ := OutOfStep(dir, GeneratedRootGo); !reflect.DeepEqual(got, sorted(goFile, "go/extra.go")) { t.Errorf("a missing file is not named: %v", got) } plain, _ := contracts(t, `{"module":"p","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]}]}}`) writeContract(t, filepath.Join(plain, "go/extra.go"), "package extra\n") if got, err := OutOfStep(plain, GeneratedRootGo); err != nil || len(got) != 0 { t.Errorf("a module naming no generated-from compared: %v %v", got, err) } // The build ask answers them beside what it built, and with a failed build too. built := 0 steps := AskSteps{Compare: CompareStep(GeneratedRootGo, func(ws string) string { return ws }), Fetch: func(context.Context, map[string]any) (any, error) { return map[string]any{"workspace": filepath.Dir(dir)}, nil }, Build: func(context.Context, map[string]any) (any, error) { if built++; built > 1 { return nil, errors.New("the second artifact does not build") } return map[string]any{"digest": "sha256:aa"}, nil }} job := sdkstep.BuildJob{Module: "c", Path: filepath.Base(dir), Fetch: &sdkstep.Fetch{Repository: "novox/c", Commit: "0123456789abcdef0123456789abcdef01234567"}, Artifacts: []sdkstep.AskArtifact{{Artifact: "go"}}} out, err := RunAsk(context.Background(), steps, ask(job)) if a, ok := out.(sdkstep.BuildAnswer); err != nil || !ok || !reflect.DeepEqual(a.OutOfStep, sorted(goFile, "go/extra.go")) || len(a.Artifacts) != 1 { t.Errorf("the build ask's out-of-step: %+v %v", out, err) } job.Artifacts = append(job.Artifacts, sdkstep.AskArtifact{Artifact: "go-other"}) built = 0 out, err = RunAsk(context.Background(), steps, ask(job)) if a, ok := out.(sdkstep.BuildAnswer); err == nil || !ok || len(a.OutOfStep) != 2 { t.Errorf("a failed build answered no out-of-step: %+v %v", out, err) } } func sorted(paths ...string) []string { sort.Strings(paths) return paths }