//go:build ignore // Writes test/vectors/mutation_texts.json: the text of the error that // capsule.Open of the Go reference gives for every case of // testdata/vectors/mutations.json, or "ok" for a capsule that opens, and the // checks that it records, step by step with their details, so that the // tests of datekeys-dart require the same text of the opening, byte for // byte, and not only the same code and step. A port of // scripts/mutation-go-texts.go of datekeys-ts, over the synced testdata of // this repository, that of the branch v0.12 of datekeys-go at c531e93. // // Each case is replayed as internal/testkit replays the corpus // (MutationInput.Open), which a program outside the reference cannot // import: the Quicknet profile pinned, or no profile for the registry // "empty"; a release source that answers every request with the release of // the case, or with "testkit: no release" and ERR_RELEASE_UNAVAILABLE; the // .dkk decoded before, as OpenOptions.AccessKey; the identities of the case; // the extensions of the case, known at (id, version) and whose data is valid // only when it is valid_data, with the texts of testkit.KnownExtensions; a // sink that discards the files; and the clock of the case. // // The code and the step of every case are checked against the corpus: a // case where Go and the corpus disagree is written with go_error and // go_step, and reported on the standard error, never resolved silently. // // Run it in the module of the reference implementation, which it imports, // without changing anything there, from the datekeys-go next to this // repository, on the branch v0.12 at c531e93: // // cd ../datekeys-go && go run ../datekeys-dart/tool/mutation_go_texts.go \ // ../datekeys-dart/testdata > ../datekeys-dart/test/vectors/mutation_texts.json package main import ( "bytes" "context" "encoding/hex" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "time" "filippo.io/age" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/profile" "g.activething.com/go/DateKeys/provider" ) type corpus struct { Spec string `json:"spec"` Cases []struct { Name string `json:"name"` DKC struct { Base string `json:"base"` Edits [][]json.RawMessage `json:"edits"` } `json:"dkc"` DKK string `json:"dkk"` Identities []string `json:"identities"` Release *struct { Round uint64 `json:"round"` Signature string `json:"signature"` } `json:"release"` Now string `json:"now"` Registry string `json:"registry"` Extensions []struct { ID string `json:"id"` Version uint64 `json:"version"` ValidData string `json:"valid_data"` } `json:"extensions"` Error string `json:"error"` Step int `json:"step"` } `json:"cases"` } // caseOut is one case: its name, the text of the error or "ok", the checks // as [step, name, ok, detail, code], and, when Go disagrees with the corpus, // what Go gives. type caseOut struct { Name string `json:"name"` Text string `json:"text"` Checks [][]any `json:"checks"` GoError string `json:"go_error,omitempty"` GoStep int `json:"go_step,omitempty"` } type output struct { Spec string `json:"spec"` Generator string `json:"generator"` Description string `json:"description"` Cases []caseOut `json:"cases"` } // known is testkit.KnownExtensions: an extension.Registry and // extension.DataValidator with the same texts. type known []struct { id string version uint64 valid []byte } func (k known) find(id string, version uint64) int { for i := range k { if k[i].id == id && k[i].version == version { return i } } return -1 } func (k known) Known(id string, version uint64) bool { return k.find(id, version) >= 0 } func (k known) ValidateData(e extension.Extension) error { i := k.find(e.ID, e.Version) if i < 0 { return fmt.Errorf("extension %s version %d is not known", e.ID, e.Version) } if !bytes.Equal(k[i].valid, e.Data) { return fmt.Errorf("data %x is not %x", e.Data, k[i].valid) } return nil } // source is testkit's singleSource. type source struct{ release *provider.Release } func (s source) Fetch(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) { if s.release == nil { return provider.Release{}, fmt.Errorf("testkit: no release: %w", datekeys.ErrReleaseUnavailable) } return *s.release, nil } // discard is testkit's DiscardSink. type discard struct{} func (discard) Begin(*capsule.Head) error { return nil } func (discard) Create(int) (io.WriteCloser, error) { return nopCloser{io.Discard}, nil } func (discard) Commit() error { return nil } func (discard) Abort() {} type nopCloser struct{ io.Writer } func (nopCloser) Close() error { return nil } func must[T any](v T, err error) T { if err != nil { panic(err) } return v } // applyEdits is the reading of testdata/README.md, "Edited files": // [at, delete, insert] on the offsets of the unmodified base, sorted and not // overlapping. func applyEdits(base []byte, edits [][]json.RawMessage) []byte { var out []byte pos := 0 for _, e := range edits { var at, del int var insert string if len(e) != 3 || json.Unmarshal(e[0], &at) != nil || json.Unmarshal(e[1], &del) != nil || json.Unmarshal(e[2], &insert) != nil { panic("an edit is [at, delete, insert]") } if at < pos || at+del > len(base) { panic("edits out of order or beyond the base") } out = append(append(out, base[pos:at]...), must(hex.DecodeString(insert))...) pos = at + del } return append(out, base[pos:]...) } func main() { dir := os.Args[1] var c corpus if err := json.Unmarshal(must(os.ReadFile(filepath.Join(dir, "vectors", "mutations.json"))), &c); err != nil { panic(err) } out := output{ Spec: c.Spec, Generator: "tool/mutation_go_texts.go", Description: "The text of the error of capsule.Open for every case of testdata/vectors/mutations.json, or ok for a capsule that opens, and its checks as [step, name, ok, detail, code]; go_error and go_step where Go and the corpus disagree. See the header of tool/mutation_go_texts.go.", } disagree := 0 for _, m := range c.Cases { var base []byte if m.DKC.Base != "" { base = must(os.ReadFile(filepath.Join(dir, "fixtures", m.DKC.Base))) } dkc := applyEdits(base, m.DKC.Edits) reg := must(profile.Default()) if m.Registry == "empty" { reg = must(profile.NewRegistry()) } var src source if m.Release != nil { src.release = &provider.Release{Round: m.Release.Round, Signature: must(hex.DecodeString(m.Release.Signature))} } now := must(time.Parse(time.RFC3339Nano, m.Now)) o := capsule.OpenOptions{Registry: reg, Source: src, Now: func() time.Time { return now }, Sink: discard{}} if m.Extensions != nil { var k known for _, x := range m.Extensions { k = append(k, struct { id string version uint64 valid []byte }{x.ID, x.Version, must(hex.DecodeString(x.ValidData))}) } o.Extensions = k } if m.DKK != "" { o.AccessKey = must(accesskey.Decode(bytes.NewReader(must(hex.DecodeString(m.DKK))))) } for _, s := range m.Identities { o.Identities = append(o.Identities, must(age.ParseX25519Identity(s))) } opened, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(dkc), o) text, code, step := "ok", "ok", 0 if err != nil { text, code = err.Error(), datekeys.Code(err) } co := caseOut{Name: m.Name, Text: text, Checks: [][]any{}} for _, ch := range opened.Inspection.Checks { co.Checks = append(co.Checks, []any{ch.Step, ch.Name, ch.OK, ch.Detail, ch.Error}) } if err != nil { step = opened.Inspection.Checks[len(opened.Inspection.Checks)-1].Step } if code != m.Error || step != m.Step { disagree++ co.GoError, co.GoStep = code, step fmt.Fprintf(os.Stderr, "%s: Go gives %s at step %d, the corpus says %s at step %d\n", m.Name, code, step, m.Error, m.Step) } out.Cases = append(out.Cases, co) } fmt.Fprintf(os.Stderr, "%d cases, %d where Go and the corpus disagree\n", len(out.Cases), disagree) var b bytes.Buffer e := json.NewEncoder(&b) e.SetEscapeHTML(false) if err := e.Encode(out.Cases); err != nil { panic(err) } // One case per line. var sb strings.Builder head := func(v any) string { var h bytes.Buffer enc := json.NewEncoder(&h) enc.SetEscapeHTML(false) if err := enc.Encode(v); err != nil { panic(err) } return strings.TrimSuffix(h.String(), "\n") } sb.WriteString("{\n") sb.WriteString(` "spec": ` + head(out.Spec) + ",\n") sb.WriteString(` "generator": ` + head(out.Generator) + ",\n") sb.WriteString(` "description": ` + head(out.Description) + ",\n") sb.WriteString(` "cases": [`) for i, co := range out.Cases { if i > 0 { sb.WriteString(",") } sb.WriteString("\n " + head(co)) } sb.WriteString("\n ]\n}\n") os.Stdout.WriteString(sb.String()) }