package kiosk import ( "bytes" "context" "errors" "fmt" "os" "path/filepath" ) // Generate derives the manifest of the plugin in dir and returns the // plugin.yaml bytes and any warnings, without writing anything. func Generate(ctx context.Context, dir string, opts DeriveOptions) ([]byte, []string, error) { m, warnings, err := Derive(ctx, dir, opts) if err != nil { return nil, warnings, err } if err := Validate(m); err != nil { return nil, warnings, err } out, err := Marshal(m) if err != nil { return nil, warnings, err } return out, warnings, nil } // Write generates dir's plugin.yaml and writes it, returning its path. func Write(ctx context.Context, dir string, opts DeriveOptions) (path string, warnings []string, err error) { out, warnings, err := Generate(ctx, dir, opts) if err != nil { return "", warnings, err } path = filepath.Join(dir, FileName) if err := os.WriteFile(path, out, 0o644); err != nil { return "", warnings, fmt.Errorf("kiosk: write %s: %w", path, err) } return path, warnings, nil } // StaleError reports a plugin.yaml that is missing or differs from what // summer plugin:manifest would write now. type StaleError struct { // Path is the plugin.yaml path. Path string // Missing is true when the file does not exist. Missing bool // Want is the freshly generated content; Got is the committed one. Want, Got []byte } // Error names the path and, for a stale file, the first differing line. func (e *StaleError) Error() string { if e.Missing { return fmt.Sprintf("kiosk: %s is missing; generate it with summer plugin:manifest", e.Path) } want := bytes.Split(e.Want, []byte("\n")) got := bytes.Split(e.Got, []byte("\n")) for i := 0; i < len(want) || i < len(got); i++ { var w, g []byte if i < len(want) { w = want[i] } if i < len(got) { g = got[i] } if !bytes.Equal(w, g) { return fmt.Sprintf("kiosk: %s is stale: line %d is %q, want %q", e.Path, i+1, g, w) } } return fmt.Sprintf("kiosk: %s is stale", e.Path) } // Check regenerates dir's plugin.yaml in memory, keeping its overrides, // and returns a *StaleError when the file on disk is missing or differs. func Check(ctx context.Context, dir string) error { want, _, err := Generate(ctx, dir, DeriveOptions{}) if err != nil { return err } path := filepath.Join(dir, FileName) got, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { return &StaleError{Path: path, Missing: true, Want: want} } if err != nil { return fmt.Errorf("kiosk: read %s: %w", path, err) } if !bytes.Equal(want, got) { return &StaleError{Path: path, Want: want, Got: got} } return nil } // TB is the part of testing.TB that CheckT uses; *testing.T satisfies it. type TB interface { Helper() Fatalf(format string, args ...any) } // CheckT fails the test when dir's plugin.yaml is missing or stale. A // plugin repository calls it from one test, so go test catches a manifest // that no longer matches the code: // // func TestPluginManifest(t *testing.T) { kiosk.CheckT(t, ".") } func CheckT(t TB, dir string) { t.Helper() if err := Check(context.Background(), dir); err != nil { t.Fatalf("%v\nregenerate it with: summer plugin:manifest %s", err, dir) } }