Ctrl K
ComunidadGuías…

notices_versions_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"testing"
)

// noticesHeading matches one section heading in THIRD-PARTY-NOTICES.md:
// "## <module path> <version>", the line above that dependency's reproduced
// licence. Anchored per line so a path or version inside a licence body cannot
// be mistaken for a heading.
var noticesHeading = regexp.MustCompile(`(?m)^## (\S+) (v\S+)[ \t]*$`)

// TestThirdPartyNoticesMatchGoMod holds THIRD-PARTY-NOTICES.md to the module
// graph it claims to describe. That file is maintained BY HAND - one heading
// per dependency with its licence reproduced in full underneath - and nothing
// regenerates it, yet it ships to users through every channel: the release
// archives carry it beside README and LICENSE (.goreleaser.yaml:29) and the
// deb/rpm install it at /usr/share/doc/pingularity/THIRD-PARTY-NOTICES.md
// (.goreleaser.yaml:68-69). So a dependency bump that edits go.mod and forgets
// the heading ships an attribution naming a version the binary does not
// contain. That is not hypothetical - it is the drift this test was written
// after, where the speedtest-go heading sat at v1.7.10 while go.mod pinned
// v1.7.11.
//
// The check runs both ways round on purpose. A module in go.mod with no section
// is a dependency shipping with its licence unreproduced; a section with no
// module in go.mod is an attribution for something no longer linked in. Either
// is a defect in the same file, and only the pair of them pins it.
//
// What this does NOT check is the licence TEXT under a heading. Whether a bump
// changed the licence still needs a human to read it (for the v1.7.10 -> v1.7.11
// bump above, upstream's LICENSE was byte-identical). This test only makes the
// heading impossible to leave behind.
func TestThirdPartyNoticesMatchGoMod(t *testing.T) {
	_, thisFile, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("runtime.Caller failed; cannot locate the module root")
	}
	root := filepath.Dir(thisFile) // this file lives at the module root

	read := func(name string) string {
		b, err := os.ReadFile(filepath.Join(root, name))
		if err != nil {
			t.Fatalf("read %s: %v", name, err)
		}
		return string(b)
	}

	required := requiredModules(t, read("go.mod"))
	if len(required) == 0 {
		t.Fatal("parsed no requirements out of go.mod - the parser has lost the file's shape, and this test would pass on anything")
	}

	documented := map[string]string{}
	for _, m := range noticesHeading.FindAllStringSubmatch(read("THIRD-PARTY-NOTICES.md"), -1) {
		path, version := m[1], m[2]
		if prev, dup := documented[path]; dup {
			t.Errorf("THIRD-PARTY-NOTICES.md has two sections for %s (%s and %s) - one module, one section", path, prev, version)
		}
		documented[path] = version
	}

	for path, version := range required {
		switch got, ok := documented[path]; {
		case !ok:
			t.Errorf("go.mod requires %s %s but THIRD-PARTY-NOTICES.md has no section for it - add \"## %s %s\" with that release's licence reproduced in full",
				path, version, path, version)
		case got != version:
			t.Errorf("THIRD-PARTY-NOTICES.md says %s %s but go.mod pins %s - update the heading, and check whether the licence text under it changed between the two releases",
				path, got, version)
		}
	}
	for path, version := range documented {
		if _, ok := required[path]; !ok {
			t.Errorf("THIRD-PARTY-NOTICES.md carries a section for %s %s, which go.mod no longer requires - drop it, or restore the dependency",
				path, version)
		}
	}
}

// goDirective matches go.mod's language floor - "go 1.27.0", or "go 1.27".
// Anchored per line AND to end-of-line so a tab-indented require inside a
// require block ("\tgithub.com/... v1.2.3") can never be read as the directive.
// The trailing-comment branch is not hypothetical tidiness: `go 1.27.0 // see
// RELEASING.md` is legal go.mod, and without it this guard fails the build on a
// perfectly good edit while claiming the parser "has lost the file's shape".
// The `^go ` prefix is what keeps a tab-indented require line out.
var goDirective = regexp.MustCompile(`(?m)^go (\d+\.\d+(?:\.\d+)?)[ \t]*(?://.*)?$`)

// readmeGoFloor matches the Quick start's build prerequisite, "requires Go
// 1.27.0+". Every occurrence is checked, so a second copy added elsewhere in
// the README is covered without touching this test.
var readmeGoFloor = regexp.MustCompile(`requires Go (\d+\.\d+(?:\.\d+)?)\+`)

// TestREADMEGoVersionMatchesGoMod holds README.md's build prerequisite to
// go.mod's `go` directive, for the same reason as the notices test above: a
// hand-maintained mirror of go.mod drifts. This one sits in the four most-read
// lines in the repository and nothing regenerates it, while every CI workflow
// takes its toolchain from `go-version-file: go.mod` - so the mirror can be
// wrong with the whole suite green.
//
// The pair has been hand-carried once and dropped once: 510e9d3 carried
// 1.25.12 -> 1.25.13 in both files, then d8fe73f bumped go.mod to 1.27.0 and
// left the README on 1.25.13 - and that commit's own subject was "Build with
// Go 1.27", which is how little a human reviewer catches this line.
//
// The cost is invisible under the default GOTOOLCHAIN=auto, which quietly
// downloads the newer toolchain and builds anyway. Under GOTOOLCHAIN=local (how
// distro-packaged Go ships) or on an offline builder, a contributor who
// installed exactly what the README asked for gets "go.mod requires go >=
// 1.27.0" from the command the README just gave them.
func TestREADMEGoVersionMatchesGoMod(t *testing.T) {
	_, thisFile, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("runtime.Caller failed; cannot locate the module root")
	}
	root := filepath.Dir(thisFile) // this file lives at the module root

	read := func(name string) string {
		b, err := os.ReadFile(filepath.Join(root, name))
		if err != nil {
			t.Fatalf("read %s: %v", name, err)
		}
		return string(b)
	}

	want := goDirective.FindStringSubmatch(read("go.mod"))
	if want == nil {
		t.Fatal("no `go <version>` directive found in go.mod - the parser has lost the file's shape, and this test would pass on anything")
	}
	found := readmeGoFloor.FindAllStringSubmatch(read("README.md"), -1)
	if len(found) == 0 {
		t.Fatal("README.md no longer says \"requires Go <version>+\" - the Quick start's prerequisite was reworded, so this test can no longer see the number it holds to go.mod")
	}
	for _, m := range found {
		if m[1] != want[1] {
			t.Errorf("README.md says %q but go.mod requires go %s - update the Quick start comment; under GOTOOLCHAIN=local a build following the README fails outright",
				m[0], want[1])
		}
	}
}

// requiredModules returns the module path -> version pairs of every require in
// a go.mod, direct and indirect alike (both are linked into the binary, so both
// need attribution). Only require blocks and single-line requires are read, so a
// replace/exclude/retract directive cannot be mistaken for a dependency.
func requiredModules(t *testing.T, gomod string) map[string]string {
	t.Helper()
	req := map[string]string{}
	inBlock := false
	for _, raw := range strings.Split(gomod, "\n") {
		line := strings.TrimSpace(raw)
		if i := strings.Index(line, "//"); i >= 0 { // "// indirect" and friends
			line = strings.TrimSpace(line[:i])
		}
		switch {
		case line == "require (":
			inBlock = true
		case inBlock && line == ")":
			inBlock = false
		case inBlock:
			if f := strings.Fields(line); len(f) == 2 {
				req[f[0]] = f[1]
			}
		case strings.HasPrefix(line, "require "):
			if f := strings.Fields(line); len(f) == 3 {
				req[f[1]] = f[2]
			}
		}
	}
	return req
}