Artifact 6718204c61fa6a16f1fcd2fad0351473f730f2b0ae18864d32b641ff3d6a84d0:
- File
conformance/repostate_api_test.go
— part of check-in
[cd4a11828f]
at
2026-10-12 20:17:22
on branch main
— fix(checkout): make WouldFork fossil's commit-time fork check (#347) (#348)
Since #291 WouldFork was fossil's leaf_ambiguity, which answers no at once for a version that is not a leaf. That is the case a commit forks in: when a pull brings in a child of the checked-out version, the version stops being a leaf, WouldFork said false, and the commit became a second child. EdgeSync's autosync relies on WouldFork to refuse that commit.
WouldFork is now fossil's commit refusal (src/checkin.c "Would fork", taken when !is_a_leaf(vid)): true when the version has a child on its own branch, read from plink as is_a_leaf does, merge children included. Another open leaf elsewhere on the branch no longer counts; fossil commits there, extending the checkout's leaf, and only warns afterwards.
The repo-state draft's Section 7.5 ambiguity test moves to checkout.LeafAmbiguous, and the repostate.7.5.ambiguity-* conformance cases call it. The superseded #291 changelog text and the WouldFork API doc are corrected. (user: [email protected], size: 7979) [annotate] [blame] [check-ins using]
package conformance import ( "errors" "fmt" "regexp" "sort" "strings" "testing" gofossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/internal/checkout" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" ) // The requirements that are about an API rather than the derived tables: // name resolution, leaves, branches and tagging. Each case loads its script // into go-libfossil (one sweep) and into fossil(1) (one artifact at a // time), then asks both the same question. fossil's answer must be the // case's, so the expectation is checked on every run. // rsAPICase asks an API a question about a scripted repository. type rsAPICase struct { Case arts []rsArt // run returns nil when go-libfossil gives the case's answer. run func(e *rsEnv) error } // rsEnv is a case's script loaded into both implementations. type rsEnv struct { t *testing.T script rsScript goPath string ref *fossilReference // nil without fossil(1) fossil fossilRepo } func TestRepoStateAPI(t *testing.T) { ref := newFossilReference(t) for _, c := range rsAPICases() { check(t, c.Case, func() error { e, err := newEnv(t, ref, c.arts) if err != nil { return err } return c.run(e) }) } } func newEnv(t *testing.T, ref *fossilReference, arts []rsArt) (*rsEnv, error) { e := &rsEnv{t: t, script: rsBuild(arts), ref: ref} path, err := goLoad(t, e.script, loadSweep, nil) if err != nil { return nil, err } e.goPath = path if ref != nil { if e.fossil, err = ref.loadScript(e.script, nil); err != nil { return nil, err } } return e, nil } // fossilRun runs fossil(1) in the case's checkout. func (e *rsEnv) fossilRun(args ...string) (string, error) { return e.ref.run(e.fossil.checkout, args...) } // open opens the go-libfossil repository; the test closes it. func (e *rsEnv) open() (*gofossil.Repo, error) { r, err := gofossil.Open(e.goPath) if err != nil { return nil, err } e.t.Cleanup(func() { r.Close() }) return r, nil } // goRid is the rid go-libfossil stored label under. func (e *rsEnv) goRid(r *repo.Repo, label string) (libfossil.FslID, error) { var rid int64 err := r.DB().QueryRow("SELECT rid FROM blob WHERE uuid = ?", e.script.hashes[label]).Scan(&rid) return libfossil.FslID(rid), err } // labelOfRid names a go-libfossil rid by its label. func (e *rsEnv) labelOfRid(r *repo.Repo, rid int64) string { var uuid string if err := r.DB().QueryRow("SELECT uuid FROM blob WHERE rid = ?", rid).Scan(&uuid); err != nil { return fmt.Sprintf("rid %d (%v)", rid, err) } return labelOf(uuid, e.script.labels) } // labelOfPrefix names the artifact whose hash starts with prefix. func (e *rsEnv) labelOfPrefix(prefix string) string { for uuid, label := range e.script.labels { if strings.HasPrefix(uuid, prefix) { return label } } return prefix } // resolves checks that name resolves to the artifact labelled want, or, when // want is empty, that it does not resolve. In name, "{rid:L}" stands for // the rid each repository stored L under. func resolves(id, req string, arts []rsArt, name, want string) rsAPICase { return rsAPICase{Case: Case{ID: id, Req: req}, arts: arts, run: func(e *rsEnv) error { if e.ref != nil { got, err := e.fossilResolve(name) if err != nil { return err } if got != want { return expectf("fossil(1) resolves %q to %q; the case expects %q", name, got, want) } } got, err := e.goResolve(name) if err != nil { return err } if got != want { return expectf("ResolveVersion(%q) = %q, want %q", name, got, want) } return nil }} } var ridPlaceholder = regexp.MustCompile(`\{rid:([a-z0-9]+)\}`) // fossilResolve is the label fossil's test-name-to-id gives name, or "" for // an error. func (e *rsEnv) fossilResolve(name string) (string, error) { for _, m := range ridPlaceholder.FindAllStringSubmatch(name, -1) { query := "SELECT rid FROM blob WHERE uuid = '" + e.script.hashes[m[1]] + "'" out, err := e.fossilRun("sql", query) if err != nil { return "", err } name = strings.ReplaceAll(name, m[0], strings.TrimSpace(out)) } out, err := e.fossilRun("test-name-to-id", name) if err != nil { return "", err } _, result, ok := strings.Cut(strings.TrimSpace(out), " -> ") if !ok { return "", fmt.Errorf("test-name-to-id: unrecognized output %q", out) } if strings.HasPrefix(result, "ERROR") { return "", nil } return labelOf(result, e.script.labels), nil } // goResolve is the label ResolveVersion gives name, or "" for an error // that says the name is unknown or ambiguous. func (e *rsEnv) goResolve(name string) (string, error) { r, err := e.open() if err != nil { return "", err } for _, m := range ridPlaceholder.FindAllStringSubmatch(name, -1) { rid, err := e.goRid(r.Inner(), m[1]) if err != nil { return "", err } name = strings.ReplaceAll(name, m[0], fmt.Sprint(rid)) } rid, err := r.ResolveVersion(name) if errors.Is(err, gofossil.ErrVersionNotFound) || errors.Is(err, gofossil.ErrAmbiguousVersion) { return "", nil } if err != nil { return "", err } return e.labelOfRid(r.Inner(), rid), nil } // leafLine is one line of `fossil leaves`: the short hash and the tags. var leafLine = regexp.MustCompile(`\[([0-9a-f]{10})\].*tags: ([^)]*)\)`) // openLeaves checks the open leaves of branch: fossil's `leaves`, which // lists open leaves, filtered to that branch, and checkout.BranchLeaves. func openLeaves(id, req string, arts []rsArt, branch string, want ...string) rsAPICase { sort.Strings(want) return rsAPICase{Case: Case{ID: id, Req: req}, arts: arts, run: func(e *rsEnv) error { if e.ref != nil { got, err := e.fossilLeaves(branch) if err != nil { return err } if strings.Join(got, " ") != strings.Join(want, " ") { return expectf("fossil leaves on %s: %v; the case expects %v", branch, got, want) } } got, err := e.goLeaves(branch) if err != nil { return err } if strings.Join(got, " ") != strings.Join(want, " ") { return expectf("BranchLeaves(%q) = %v, want %v", branch, got, want) } return nil }} } func (e *rsEnv) fossilLeaves(branch string) ([]string, error) { out, err := e.fossilRun("leaves") if err != nil { return nil, err } var labels []string for _, m := range leafLine.FindAllStringSubmatch(out, -1) { for _, tag := range strings.Split(m[2], ", ") { if tag == branch { labels = append(labels, e.labelOfPrefix(m[1])) } } } sort.Strings(labels) return labels, nil } func (e *rsEnv) goLeaves(branch string) ([]string, error) { r, err := e.open() if err != nil { return nil, err } rids, err := checkout.BranchLeaves(r.Inner(), branch) if err != nil { return nil, err } labels := []string{} for _, rid := range rids { labels = append(labels, e.labelOfRid(r.Inner(), int64(rid))) } sort.Strings(labels) return labels, nil } // leafAmbiguous checks the ambiguity test for at: fossil's // test-leaf-ambiguity and checkout.LeafAmbiguous. func leafAmbiguous(id, req string, arts []rsArt, at string, want bool) rsAPICase { return rsAPICase{Case: Case{ID: id, Req: req}, arts: arts, run: func(e *rsEnv) error { if e.ref != nil { out, err := e.fossilRun("test-leaf-ambiguity", e.script.hashes[at]) if err != nil { return err } if got := !strings.HasSuffix(strings.TrimSpace(out), " ok"); got != want { return expectf("fossil test-leaf-ambiguity %s: %q; the case expects ambiguous=%v", at, strings.TrimSpace(out), want) } } got, err := e.goLeafAmbiguous(at) if err != nil { return err } if got != want { return expectf("LeafAmbiguous on %s = %v, want %v", at, got, want) } return nil }} } func (e *rsEnv) goLeafAmbiguous(at string) (bool, error) { r, err := e.open() if err != nil { return false, err } rid, err := e.goRid(r.Inner(), at) if err != nil { return false, err } return checkout.LeafAmbiguous(r.Inner(), rid) }