Files
summercms/modules/kiosk/check.go
Jakub Zych 51ec2f9daa feat(kiosk): resolve constant plugin IDs, overrides and the CheckT helper
- kiosk.ReadIdentity: ID()/Requires() as literals or constants across packages and modules, read with go/parser, never run
- plugin minimums from go.mod requires; v0.0.0 is an error, a pseudo-version warns
- kiosk.Validate, kiosk.CheckT (no testing import)
- plugin:add reads IDs through kiosk.ReadIdentity, so const-ID plugins register
2026-10-29 17:02:38 +01:00

113 lines
3.2 KiB
Go

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)
}
}