Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1d190b1
Read a README named in any case
dcadolph Nov 11, 2026
fafa0db
Read go install lines with a valued flag
dcadolph Nov 11, 2026
99c46ca
Treat a leading ellipsis path and numbered caps as placeholders
dcadolph Nov 11, 2026
1e12412
Skip a block under a bare platform heading
dcadolph Nov 11, 2026
7c7c549
Skip guides kept for a retired version
dcadolph Nov 11, 2026
c5d5ff4
Write a shell block captioned with a file name as that file
dcadolph Nov 11, 2026
f063e53
Read tcell's unset TERM error as interactive
dcadolph Nov 11, 2026
23cc85b
Run Python steps on the newest release the project supports
dcadolph Nov 11, 2026
043e737
Read an installer that needs crontab as a missing program
dcadolph Nov 11, 2026
97d5907
Read a longer no-match message as no data
dcadolph Nov 11, 2026
789c587
Read a documented not-found exit code as no data
dcadolph Nov 11, 2026
37fe832
Block a line after a skipped line that would have provided its tool
dcadolph Nov 11, 2026
c891455
Report subcommands the source defines as unreleased
dcadolph Nov 11, 2026
5aa9ce6
Explain a failure by its message, not a trailing location
dcadolph Nov 11, 2026
1293b99
Write a block to the file its tutorial names
dcadolph Nov 11, 2026
68e7b4d
Block a Go build that mixes a document's file with the repo's package
dcadolph Nov 11, 2026
7c40fdb
Read a Node package missing under the named script as blocked
dcadolph Nov 11, 2026
7554920
Run a local npm install for a package this repo publishes
dcadolph Nov 12, 2026
d287f59
Read a bundler's missing entry file as blocked
dcadolph Nov 12, 2026
6e92483
Block a line whose tool says the checkout is not its repo
dcadolph Nov 12, 2026
5b80b1a
Collapse equal flag-check and coverage rows of one binary
dcadolph Nov 12, 2026
46d8da1
Re-pin nodemon to 3,0,0 for its local install route
dcadolph Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,12 @@ verdict claims the documented step works:
- `BLOCKED` with the reason `checkout-state` means a line that would have been a plain
`FAIL` ran in the repository's own checkout and its output says the target already
exists or is already initialized. The collision is with the checkout kibble chose, so
the document may be fine.
the document may be fine. A Go build that finds two packages at the top of the checkout
reads the same way when the document wrote one of the two files and the repository
already held the other. So does a documented tool saying the top of the checkout is not
a repository of its own kind and naming its own init command, when no earlier line of
the document that passed made or cloned one. That line was not tested, and it is not
counted as passing.
- A passing line that writes a titled fence to its file reads `VERIFIED` "wrote X as the
document shows it". The document supplied the content, so only the write is claimed.
- An install whose product is a shell function the documented sourcing defines is
Expand Down Expand Up @@ -312,7 +317,7 @@ What the weekly job prints, with each repository's counts in VERIFIED, FAIL, GAP

```
ripgrep: 3,0,0 as expected
nodemon: 2,0,0 as expected
nodemon: 3,0,0 as expected
fd: 6,0,0 as expected
glow: 5,0,0 as expected
httpie: 0,0,0 as expected
Expand Down
11 changes: 10 additions & 1 deletion corpus/repos.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@
# this commit and at 034d3c1 reads 3,0,0, and those two lines are the whole
# difference. The change is 81ed0b7.
ripgrep https://github.com/BurntSushi/ripgrep.git 3fce3b5bb0236da2df6d99672afb8a719642eca7 3,0,0
nodemon https://github.com/remy/nodemon.git 5de021f6cd5c71814b6c2d1c60af3cc01fa2cc69 2,0,0
# nodemon once expected 2,0,0. Its README installs it globally and, further down,
# with npm install --save-dev nodemon. Since kibble runs a local npm install for
# a package the checkout publishes (7554920), that second line is a route of its
# own and both installs verify. The two routes print the same help, and equal
# check rows of one binary collapse to one (5b80b1a), so the flag check still
# counts once.
# Run on the same commit, kibble before 7554920 reads 2,0,0, at 7554920 4,0,0
# with the flag check doubled, and with the collapse 3,0,0. The extra install is
# the whole difference.
nodemon https://github.com/remy/nodemon.git 5de021f6cd5c71814b6c2d1c60af3cc01fa2cc69 3,0,0
# fd is the one row that moves on a quiet network. Its npm package fetches a
# prebuilt binary, and when that fetch fails the two checks reading the
# installed tool have nothing to read, so 6,0,0 becomes 4,0,0. That is now an
Expand Down
195 changes: 194 additions & 1 deletion dependent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"path"
"regexp"
"strings"

Expand Down Expand Up @@ -52,7 +53,11 @@ func resolveMissingBinaries(run *exampleRun, plan *kplan.Plan, have map[string]b
// the session is no longer a clean test of this line, which is why these
// become blocked rather than skipped: the cascade stops being reported as
// several broken lines without any of them being called fine. A gap counts as
// not having run, since the document's own hole stopped the line.
// not having run, since the document's own hole stopped the line. A Go build
// that finds the document's file beside the checkout's own package is blocked
// here too, since it takes the earlier file writes of the run to see it. So is
// a documented tool saying the top of the checkout is not a repository of its
// own kind, since it takes the document's earlier lines to see none made one.
func resolveDependentFailures(run *exampleRun, plan *kplan.Plan) {
bins := map[string]bool{}
for _, b := range plan.Binaries {
Expand Down Expand Up @@ -85,20 +90,58 @@ func resolveDependentFailures(run *exampleRun, plan *kplan.Plan) {
// Earlier line results per document, for the shared-operand rule, which
// reads across the document's steps rather than within one.
prior := map[string][]lineResult{}
// Go files the checkout held at its top before the session started.
checkout := map[string]bool{}
for _, f := range plan.RootGoFiles {
checkout[f] = true
}
// Files the current document's file blocks wrote from the top of the
// checkout, in run order. The session restores the checkout between
// documents when it could save it, so an earlier document's files are
// gone by the next one and are never counted for it.
written, writtenDoc := map[string]bool{}, ""
for si := range run.Steps {
s := &run.Steps[si]
if s.Doc != writtenDoc {
written, writtenDoc = map[string]bool{}, s.Doc
}
for li := range s.Lines {
l := &s.Lines[li]
if l.Status != StatusFail {
m := reFileWrite.FindStringSubmatch(l.Cmd)
if m != nil && l.Status == StatusVerified && l.cwd == sessionRepoDir {
written[m[1]] = true
}
prior[s.Doc] = append(prior[s.Doc], *l)
continue
}
if doc, own := mixedCheckoutPackages(l.output, written, checkout); doc != "" {
l.Status = StatusBlocked
l.Reason = ReasonCheckoutState
l.Detail = fmt.Sprintf("wrote %s into the repository's own checkout, beside %s of "+
"another Go package, so the claim was not tested as written: %s", doc, own, l.Detail)
continue
}
if bin := notItsRepository(l, prior[s.Doc], bins); bin != "" {
l.Status = StatusBlocked
l.Reason = ReasonCheckoutState
l.Detail = fmt.Sprintf("ran at the top of the checkout, which %s reports is not a %s "+
"repository, and no line of this document that ran created one, so the claim was "+
"not tested as written: %s", bin, bin, l.Detail)
continue
}
if cited := citedSkipped(l.output, skippedCmds); cited != "" {
l.Status = StatusBlocked
l.Reason = ReasonDependsOnSkipped
l.Detail = fmt.Sprintf("failed naming `%s`, which did not run", cited)
continue
}
if word := skippedProvider(s.Lines[:li], l.output); word != "" {
l.Status = StatusBlocked
l.Reason = ReasonMissingFixture
l.Detail = fmt.Sprintf("follows a skipped line that would have provided %s", word)
continue
}
if prior := earlierGapInFamily(s.Lines[:li], l.Cmd, bins); prior != "" {
l.Status = StatusBlocked
l.Reason = ReasonDependsOnSkipped
Expand All @@ -119,6 +162,156 @@ func resolveDependentFailures(run *exampleRun, plan *kplan.Plan) {
}
}

var (
// reFileWrite matches the line a planned file block runs and captures the
// path it writes, relative to the directory the line ran in. The
// terminator may carry a number after the part matched here.
reFileWrite = regexp.MustCompile(`^(?:mkdir -p '[^']+' && )?cat > '([^']+)' <<'KIBBLE_FILE_END`)
// reMixedPackages matches Go refusing a directory that holds two packages
// and captures the file it names for each and the directory.
reMixedPackages = regexp.MustCompile(
`found packages \w+ \(([^)\s]+\.go)\) and \w+ \(([^)\s]+\.go)\) in (\S+)`)
)

// mixedCheckoutPackages returns the document's file and the checkout's file
// when Go reports two packages in the repository root, the document's file
// blocks wrote one of the two files it names, and the checkout held the other
// before the session started. sqlc's tutorials tell the reader in prose to work
// in a new directory, so the session stays in the checkout and writes
// tutorial.go beside sqlc's own root package. A clash between two files the
// document wrote is the document's own, and so is a clash with a file its
// commands made some other way, such as a generator's output. A file written
// into a subdirectory is not the one Go names at the root, and a clash in any
// other directory is left as it reads, since only the root's files are known
// to be the checkout's.
func mixedCheckoutPackages(output string, written, checkout map[string]bool) (doc, own string) {
m := reMixedPackages.FindStringSubmatch(output)
if m == nil || m[3] != sessionRepoDir {
return "", ""
}
switch a, b := m[1], m[2]; {
case written[a] && !written[b] && checkout[b]:
return a, b
case written[b] && !written[a] && checkout[a]:
return b, a
}
return "", ""
}

// reMadeRepository matches a command holding init or clone as a whole word,
// the two ways a tool makes a repository of its own kind.
var reMadeRepository = regexp.MustCompile(`\b(?:init|clone)\b`)

// notItsRepository returns the documented binary a failing line invoked when
// that tool reports the top of the checkout is not a repository of its own
// kind. A version control tool's guide is written for a reader standing in
// such a repository, and the session stands in a plain clone of the project.
// The tool must say there is no repository of its name there and must name its
// own init command, so the report and the remedy are both the tool's words.
// Every such report must name the directory the line ran in or the checkout's
// own path, since a report about any other directory is about one the document
// named. A line that ran in another directory, or that stepped into one and
// back, went where the document sent it. A usage error is the document's own
// wording, and a crash convicts the line whatever else it printed. An earlier
// line of the same document that passed and made or cloned a repository with
// the tool means the document did set one up, so its failure at the top of the
// checkout stands.
func notItsRepository(l *lineResult, earlier []lineResult, bins map[string]bool) string {
if l.cwd != sessionRepoDir || changesDir(l.Cmd) {
return ""
}
bin, _ := kplan.InvokedBinary(l.Cmd, bins)
if bin == "" {
return ""
}
out, name := stripANSI(l.output), regexp.QuoteMeta(bin)
noRepo := regexp.MustCompile(
`(?i)there is no ` + name + ` repo(?:sitory)? in ["'\x60]?([^\s"'\x60>]*)`)
initCmd := regexp.MustCompile(`(?i)\b` + name + `(?: \w+)? init\b`)
reports := noRepo.FindAllStringSubmatch(out, -1)
if reports == nil || !initCmd.MatchString(out) {
return ""
}
for _, m := range reports {
if m[1] != "." && strings.TrimSuffix(m[1], "/") != sessionRepoDir {
return ""
}
}
if reUsageError.MatchString(out) || reCrash.MatchString(out) {
return ""
}
for _, p := range earlier {
if p.Status == StatusVerified && reMadeRepository.MatchString(p.Cmd) && runsBinary(p.Cmd, bin) {
return ""
}
}
return bin
}

// runsBinary reports whether any command of a line runs bin, stepping over
// wrappers such as env as InvokedBinary does.
func runsBinary(cmd, bin string) bool {
line, ok := shell.Parse(cmd)
if !ok {
return false
}
for _, c := range line.Cmds {
if shell.Invoked(c).Name() == bin {
return true
}
}
return false
}

var (
// reAbsentQuoted matches output reporting a quoted name as absent, such
// as Go's `no such tool "air"`, and captures the name.
reAbsentQuoted = regexp.MustCompile(
"(?i)(?:no such (?:tool|file|command)|not found)[^\"'`\\n]{0,20}[\"'`]([\\w.-]+)[\"'`]" +
"|[\"'`]([\\w.-]+)[\"'`][^\\n]{0,20}(?:not found|no such file)")
// reAbsentBare matches a shell reporting a bare name as absent, such as
// "air: command not found", and captures the name.
reAbsentBare = regexp.MustCompile(`(?i)(?:^|[\s:])([\w.-]+): (?:command )?not found`)
)

// skippedProvider returns the name a failing line reports as absent when an
// earlier line of the same block was skipped for a missing fixture or
// dependency and would have provided it. air's README skips `go get -tool
// github.com/air-verse/air@latest`, since it adds a tool to the reader's own
// module, and the next line fails with `go: no such tool "air"`. The name
// must be a whole argument of the skipped line or the last element of a
// module path in it, never part of a URL, and a placeholder skip provides
// nothing.
func skippedProvider(earlier []lineResult, output string) string {
var names []string
for _, line := range strings.Split(output, "\n") {
for _, m := range reAbsentQuoted.FindAllStringSubmatch(line, -1) {
names = append(names, m[1]+m[2])
}
for _, m := range reAbsentBare.FindAllStringSubmatch(line, -1) {
names = append(names, m[1])
}
}
for _, prev := range earlier {
if prev.Status != StatusSkipped ||
(prev.Reason != ReasonMissingFixture && prev.Reason != ReasonMissingDependency) {
continue
}
for _, f := range strings.Fields(prev.Cmd) {
if strings.Contains(f, "://") {
continue
}
mod, _, _ := strings.Cut(f, "@")
for _, n := range names {
if n != "" && (f == n || path.Base(mod) == n) {
return n
}
}
}
}
return ""
}

// sharedOperandNotPassed returns a file-shaped operand of a failing line and
// the first earlier line of the same document that also named it and was
// skipped, blocked, or stopped by a gap. A query on a database whose load was
Expand Down
Loading
Loading