Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 108 additions & 11 deletions cmd/lab/main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Command lab reads a checkout of this repository and reports what it
// examined. It has two verbs that do work, one that judges and one that only
// reports, it takes no flags, and it writes nothing to the tree it reads.
// reports, two that answer for the program itself, it takes no flags, and it
// writes nothing to the tree it reads.
package main

import (
"fmt"
"io"
"os"
"runtime/debug"
"strings"
"time"

"github.com/Flowfin/lab/internal/check"
Expand Down Expand Up @@ -60,9 +63,10 @@ const (
// that prevents misuse when somebody later asks what does. And a notice an
// operator has to run a verb to see is weaker than one sitting in the download
// beside the binary, because the operator who most needs it is the one who runs
// the thing without asking it for help first. Both routes exist for that
// reason rather than either alone; the download half is issue #36 and has no
// archive to be carried in yet.
// the thing without asking it for help first. Both routes exist for that reason
// rather than either alone, and the download half is in the tree now: the
// release workflow copies these three files in beside the binaries, so an
// operator who never runs a verb still holds them.
//
// Nothing outside this package holds these strings to the tree. The paths leg
// of the invariants scan reads this repository's own documents, which is the
Expand All @@ -75,19 +79,27 @@ var documentsAnOperatorIsOwed = []string{
"docs/privacy.md",
}

// documentsParagraph is the pointer the operator is owed, and it is one string
// rather than one per place that prints it. Both the usage text and the version
// output end with it, and a second copy would drift against the first the day
// one of the three files moves - which is the same failure the paragraph itself
// is written against, one level up.
const documentsParagraph = `NOTICE.md says what this program is for, LICENSE carries the terms it is under,
and docs/privacy.md says what stays on the host. Reading them is on you; this
text only says where they are.
`

const usage = `lab reads this repository and reports what it examined.

lab check [path] walk the tree at path, default ".", and report
lab list [path] list the experiments at path, default ".", oldest
unanswered first
lab version print the version this binary was built from
lab help print this text

lab writes nothing to the tree it reads.

NOTICE.md says what this program is for, LICENSE carries the terms it is under,
and docs/privacy.md says what stays on the host. Reading them is on you; this
text only says where they are.
`
` + documentsParagraph

// THIS IS WHERE THE RUNNER READS THE TIME, and it is read once. Everything
// downstream is given the value rather than asking again, so a run has one
Expand All @@ -103,9 +115,10 @@ text only says where they are.
// reading on.
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, edges{
walk: check.Walk,
list: check.List,
now: time.Now(),
walk: check.Walk,
list: check.List,
now: time.Now(),
buildInfo: debug.ReadBuildInfo,
}))
}

Expand All @@ -126,6 +139,14 @@ type edges struct {
walk func(string, time.Time) (check.Result, error)
list func(string, time.Time) (check.Listing, error)
now time.Time

// buildInfo is what the toolchain stamped into this binary. It is an
// edge for the same reason the clock is: a test asserting what the
// version verb prints cannot build a tagged binary to assert it
// against, and one that read the real build info would assert whatever
// the machine running the suite happened to produce, which is a
// different string on a checkout, on a tag and on a modified tree.
buildInfo func() (*debug.BuildInfo, bool)
}

// run is main with its edges passed in, so that what the command prints and
Expand All @@ -145,6 +166,25 @@ func run(args []string, out, errOut io.Writer, e edges) int {
fmt.Fprint(out, usage)
return exitClean

case "version":
if len(args) > 1 {
fmt.Fprintf(errOut, "lab version takes no arguments\n")
return exitCannot
}

info, ok := e.buildInfo()
if !ok {
// The toolchain stamps this into every binary it builds in
// module mode, so reaching here means the caller is holding
// something this repository's build did not produce. Saying
// which of the two it is beats printing an empty version and
// letting the reader take it for one.
fmt.Fprintf(errOut, "lab version: this binary carries no build information, so there is no version in it to report\n")
return exitCannot
}
fmt.Fprint(out, versionText(info))
return exitClean

case "check":
root, ok := pathArgument(args, errOut)
if !ok {
Expand Down Expand Up @@ -203,3 +243,60 @@ func pathArgument(args []string, errOut io.Writer) (string, bool) {
return "", false
}
}

// versionText renders what the version verb prints.
//
// WHERE THE VERSION COMES FROM AND WHY IT IS NOT A CONSTANT. The toolchain
// stamps the main module's version into the binary from the tags version
// control holds, so a build at a tag reports that tag and a build from a
// checkout with no tag on it reports a version the toolchain derived from the
// commit instead. A constant in a source file would be a second answer to the
// same question, and it would disagree with the tag silently, which makes every
// report from that build misleading rather than merely wrong.
//
// WHAT IT DOES NOT DECIDE. Nothing here says whether the string is a release
// version. That judgement already exists in internal/bom, where it refuses a
// published artefact that cannot be resolved back to a release, and a second
// copy of the pattern here would be the same rule answered in two places. What
// this prints instead is the string itself with the commit beside it, so the
// reader can see which of the two they are holding rather than being told.
func versionText(info *debug.BuildInfo) string {
var b strings.Builder

version := strings.TrimSpace(info.Main.Version)
if version == "" {
// An empty stamp is a different statement from a stamp the reader
// cannot resolve, and printing "lab " with nothing after it would
// read as the second.
version = "(the toolchain stamped no version)"
}
fmt.Fprintf(&b, "lab %s\n", version)

settings := make(map[string]string, len(info.Settings))
for _, setting := range info.Settings {
settings[setting.Key] = setting.Value
}

if revision := settings["vcs.revision"]; revision != "" {
if built := settings["vcs.time"]; built != "" {
fmt.Fprintf(&b, "built from commit %s, %s\n", revision, built)
} else {
fmt.Fprintf(&b, "built from commit %s\n", revision)
}
}

// The one line that changes what the two above are worth. A build from a
// tree carrying changes version control does not hold is described by
// neither the tag nor the commit, and the stamp says so rather than
// leaving a reader to infer it from a suffix on the version string.
if settings["vcs.modified"] == "true" {
fmt.Fprint(&b, "The tree this was built from carried changes version control did not hold,\nso neither the version nor the commit above describes every byte in this binary.\n")
}

// Three strings, one question, and the reader is told which of the three
// they may be holding rather than being left to work it out from a shape.
fmt.Fprint(&b, "\nThe version is what the toolchain stamped from version control when this binary\nwas built rather than a constant written into the source, so it is one of three\nthings. A tag, which is what a release carries. A version of the shape\nv0.0.0-<timestamp>-<commit>, which the toolchain derives from a commit no tag\nnames, and which is what building from an ordinary checkout produces. Or\n(devel), the placeholder it writes when it was told nothing at all, which is\nwhat running the source without building it produces.\n")

fmt.Fprint(&b, "\n"+documentsParagraph)
return b.String()
}
162 changes: 161 additions & 1 deletion cmd/lab/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"os"
"path/filepath"
"runtime/debug"
"strings"
"testing"
"time"
Expand All @@ -26,7 +27,23 @@ var fixedNow = time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
// passes. The walk is named at the call site because several cases contrive
// it; the listing and the clock are the real ones unless a case says otherwise.
func ordinary(walk func(string, time.Time) (check.Result, error)) edges {
return edges{walk: walk, list: check.List, now: fixedNow}
return edges{walk: walk, list: check.List, now: fixedNow, buildInfo: stamped}
}

// stamped is the build information a test that is not contriving one gets. It
// is a value written out here rather than the real stamp, because the real one
// is a different string on a checkout, at a tag and on a modified tree, and a
// test asserting against it would assert whatever the machine running the suite
// happened to produce.
func stamped() (*debug.BuildInfo, bool) {
return &debug.BuildInfo{
Main: debug.Module{Path: "github.com/Flowfin/lab", Version: "v1.2.3"},
Settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "0123456789abcdef0123456789abcdef01234567"},
{Key: "vcs.time", Value: "2026-08-09T11:00:00Z"},
{Key: "vcs.modified", Value: "false"},
},
}, true
}

// TestExitCodes reaches every code this runner can return, and names the
Expand Down Expand Up @@ -247,3 +264,146 @@ func TestHelpNamesTheDocumentsAnOperatorIsOwed(t *testing.T) {
}
}
}

// TestVersionNamesTheDocumentsAnOperatorIsOwed is the version half of the rule
// the help test holds for the usage text. Both routes exist because an operator
// reaches for one or the other and not reliably for both, so a paragraph that
// only one of them prints reaches only half of them.
//
// It asserts the same two things that test does. That the output names each
// document, and that each document it names is in this tree to be found - a
// binary pointing at a file somebody moved is worse than one pointing nowhere,
// because the reader follows it.
func TestVersionNamesTheDocumentsAnOperatorIsOwed(t *testing.T) {
var out, errOut bytes.Buffer
if got := run([]string{"version"}, &out, &errOut, ordinary(realWalk)); got != exitClean {
t.Fatalf("exit code %d, want %d", got, exitClean)
}

for _, document := range documentsAnOperatorIsOwed {
if !strings.Contains(out.String(), document) {
t.Errorf("the version output does not name %s:\n%s", document, out.String())
}
if _, err := os.Stat(filepath.Join("..", "..", filepath.FromSlash(document))); err != nil {
t.Errorf("the version output names %s and it is not in this tree: %v", document, err)
}
}
}

// TestVersionReportsWhatTheToolchainStamped asserts that the version the verb
// prints is the one it was handed rather than anything written in this package.
// The failure it prevents is a constant in the source drifting from the tag a
// binary was built at, which is the defect issue #44 opens with: it disagrees
// silently, so every report from that build is misleading rather than wrong in
// a way somebody notices.
func TestVersionReportsWhatTheToolchainStamped(t *testing.T) {
e := ordinary(realWalk)
e.buildInfo = func() (*debug.BuildInfo, bool) {
return &debug.BuildInfo{
Main: debug.Module{Path: "github.com/Flowfin/lab", Version: "v9.9.9"},
Settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "feedfacefeedfacefeedfacefeedfacefeedface"},
{Key: "vcs.time", Value: "2026-01-02T03:04:05Z"},
},
}, true
}

var out, errOut bytes.Buffer
if got := run([]string{"version"}, &out, &errOut, e); got != exitClean {
t.Fatalf("exit code %d, want %d", got, exitClean)
}

for _, want := range []string{"v9.9.9", "feedfacefeedfacefeedfacefeedfacefeedface", "2026-01-02T03:04:05Z"} {
if !strings.Contains(out.String(), want) {
t.Errorf("the version output does not carry %q:\n%s", want, out.String())
}
}
if strings.Contains(out.String(), "v1.2.3") {
t.Errorf("the version output carries a version nothing handed it:\n%s", out.String())
}
}

// TestVersionSaysWhenTheTreeWasModified holds the disclosure that decides what
// the two lines above it are worth. A build from a tree carrying changes
// version control did not hold is described by neither the tag nor the commit,
// and a reader who takes the commit for a description of the bytes in front of
// them is the person this line exists for.
//
// The negative leg is the half worth having. A clean build must not print it,
// because a disclosure that appears on every run is one nobody reads.
func TestVersionSaysWhenTheTreeWasModified(t *testing.T) {
const disclosure = "carried changes version control did not hold"

withModified := func(modified string) string {
e := ordinary(realWalk)
e.buildInfo = func() (*debug.BuildInfo, bool) {
return &debug.BuildInfo{
Main: debug.Module{Path: "github.com/Flowfin/lab", Version: "v1.2.3"},
Settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "0123456789abcdef0123456789abcdef01234567"},
{Key: "vcs.modified", Value: modified},
},
}, true
}
var out, errOut bytes.Buffer
if got := run([]string{"version"}, &out, &errOut, e); got != exitClean {
t.Fatalf("exit code %d, want %d", got, exitClean)
}
return out.String()
}

if got := withModified("true"); !strings.Contains(got, disclosure) {
t.Errorf("a build from a modified tree does not disclose it:\n%s", got)
}
if got := withModified("false"); strings.Contains(got, disclosure) {
t.Errorf("a build from a clean tree discloses a modification that did not happen:\n%s", got)
}
}

// TestVersionWithoutBuildInformationCannot covers the one route where the verb
// has nothing to answer with. The toolchain stamps build information into every
// binary it builds in module mode, so a binary carrying none is not one this
// repository's build produced, and saying that is worth more than printing an
// empty version the reader would take for a real one.
//
// The code is the one record 0011 gives a runner that could not do its job,
// rather than the one a refusal returns.
func TestVersionWithoutBuildInformationCannot(t *testing.T) {
e := ordinary(realWalk)
e.buildInfo = func() (*debug.BuildInfo, bool) { return nil, false }

var out, errOut bytes.Buffer
if got := run([]string{"version"}, &out, &errOut, e); got != exitCannot {
t.Fatalf("exit code %d, want %d", got, exitCannot)
}
if out.Len() != 0 {
t.Errorf("it wrote to standard output when it had no version to report:\n%s", out.String())
}
if !strings.Contains(errOut.String(), "no build information") {
t.Errorf("the message does not say what was missing:\n%s", errOut.String())
}
}

// TestVersionTakesNoArguments holds the verb to the shape the other three have.
// A verb that quietly ignored a word after it would let "lab version check" run
// as a version report, and the reader would have no way to see that the thing
// they asked for did not happen.
func TestVersionTakesNoArguments(t *testing.T) {
var out, errOut bytes.Buffer
if got := run([]string{"version", "somewhere"}, &out, &errOut, ordinary(realWalk)); got != exitCannot {
t.Fatalf("exit code %d, want %d", got, exitCannot)
}
if out.Len() != 0 {
t.Errorf("it printed a version for an invocation it refused:\n%s", out.String())
}
}

// TestTheUsageTextNamesTheVersionVerb keeps the help text and the verbs the
// command answers from drifting apart. A verb missing from the usage text is a
// verb only somebody reading the source finds, and the usage text is the only
// listing of them in this repository.
func TestTheUsageTextNamesTheVersionVerb(t *testing.T) {
if !strings.Contains(usage, "lab version") {
t.Errorf("the usage text does not name the version verb:\n%s", usage)
}
}
Loading