From 9e83980e63158946cd27170778e96cc11bbd602b Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:11 -0400 Subject: [PATCH 01/41] add license --- LICENSE | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0363628a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2024 Metrum Research Group + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 2aff7c450903592fdd0e1ced026e467d8f77feaa Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 02/41] add target to generate docs --- .gitignore | 1 + rules.mk | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 .gitignore create mode 100644 rules.mk diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ae3c1726 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/rules.mk b/rules.mk new file mode 100644 index 00000000..dd0f2eb0 --- /dev/null +++ b/rules.mk @@ -0,0 +1,20 @@ +vtdir := $(dir $(lastword $(MAKEFILE_LIST))) +vtdir := $(patsubst %/,%,$(vtdir)) +ifeq ($(strip $(vtdir)),) +$(error "bug: vtdir is unexpectedly empty") +endif + +VT_BIN_DIR ?= $(vtdir)/bin +VT_DOC_DIR ?= docs/commands + +.PHONY: vt-bin +vt-bin: + rm -rf '$(VT_BIN_DIR)' && mkdir '$(VT_BIN_DIR)' + go build -o '$(VT_BIN_DIR)/' ./... + +.PHONY: vt-gen-docs +vt-gen-docs: + $(MAKE) vt-bin + @test -f '$(VT_BIN_DIR)/docgen' || \ + { printf '"make vt-bin" did not generate $(VT_BIN_DIR)/docgen\n'; exit 1; } + '$(VT_BIN_DIR)/docgen' '$(VT_DOC_DIR)' From b832e830e3535a8ce5d90530d7fd930a88920ac8 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 03/41] add command to check traceability matrix --- checkmat/checkmat_test.go | 613 ++++++++++++++++++++++++++++++++++++++ checkmat/main.go | 265 ++++++++++++++++ rules.mk | 11 + 3 files changed, 889 insertions(+) create mode 100644 checkmat/checkmat_test.go create mode 100644 checkmat/main.go diff --git a/checkmat/checkmat_test.go b/checkmat/checkmat_test.go new file mode 100644 index 00000000..3e5045f9 --- /dev/null +++ b/checkmat/checkmat_test.go @@ -0,0 +1,613 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "gopkg.in/yaml.v3" +) + +func assertCode(t *testing.T, output string, code string, ntimes int) { + t.Helper() + found := strings.Count(output, "["+code+"]") + if found != ntimes { + t.Errorf("expected %dx [%s] in output, got %d\noutput: %q", + ntimes, code, found, output) + } +} + +func TestCheckValidFileNamesBad(t *testing.T) { + var tests = []struct { + name string + entries []entry + want int + }{ + { + name: "one entry", + entries: []entry{ + { + Entrypoint: "foo", + Doc: "/foo/bar", + Tests: []string{"foo/../bar", "/baz"}, + }, + }, + want: 3, + }, + { + name: "multiple entries", + entries: []entry{ + { + Entrypoint: "a", + Doc: "/foo/bar", + Tests: []string{"foo/../bar"}, + }, + { + Entrypoint: "b", + Doc: "./foo/bar", + }, + { + Entrypoint: "c", + Doc: "good", + }, + }, + want: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + bad, err := checkValidFileNames(tt.entries, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != tt.want { + t.Errorf("invalid file names: want %d, got %d", tt.want, bad) + } + out := buf.String() + assertCode(t, out, "01", tt.want) + }) + } +} + +func TestCheckValidFileNamesGood(t *testing.T) { + entries := []entry{ + { + Entrypoint: "a", + Doc: "foo/bar", + Tests: []string{"foo/bar_test.go"}, + }, + { + Entrypoint: "b", + Doc: "foo/bar", + }, + } + + var buf bytes.Buffer + bad, err := checkValidFileNames(entries, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no invalid file names, got %d", bad) + } + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } +} + +func TestCheckMissingFilesBad(t *testing.T) { + dir := t.TempDir() + var tests = []struct { + name string + entries []entry + want int + }{ + { + name: "one entry", + entries: []entry{ + { + Entrypoint: "foo", + Code: "cmd/foo.go", + Doc: "docs/commands/foo.md", + Tests: []string{"cmd/foo_test.go"}, + }, + }, + want: 3, + }, + { + name: "one entry without tests", + entries: []entry{ + { + Entrypoint: "foo", + Code: "cmd/foo.go", + Doc: "docs/commands/foo.md", + }, + }, + want: 2, + }, + { + name: "two entries", + entries: []entry{ + { + Entrypoint: "foo", + Code: "cmd/foo.go", + Doc: "docs/commands/foo.md", + Tests: []string{"cmd/foo_test.go"}, + }, + { + Entrypoint: "bar", + Code: "cmd/bar.go", + Doc: "docs/commands/bar.md", + Tests: []string{"cmd/bar_test.go", "another_test.go"}, + }, + }, + want: 7, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + bad, err := checkMissingFiles(tt.entries, dir, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != tt.want { + t.Errorf("missing files: want %d, got %d", tt.want, bad) + } + out := buf.String() + assertCode(t, out, "02", tt.want) + }) + } +} + +func createEmptyFile(t *testing.T, name string) { + t.Helper() + fh, err := os.Create(name) + if err != nil { + t.Fatal(err) + } + err = fh.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestCheckMissingFilesGood(t *testing.T) { + dir := t.TempDir() + entries := []entry{ + { + Entrypoint: "foo", + Code: "cmd/foo.go", + Doc: "docs/foo.md", + Tests: []string{"cmd/foo_test.go"}, + }, + { + Entrypoint: "bar", + Code: "cmd/bar.go", + Doc: "docs/bar.md", + Tests: []string{"cmd/bar_test.go", "another_test.go"}, + }, + } + + err := os.MkdirAll(filepath.Join(dir, "cmd"), 0777) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(filepath.Join(dir, "docs"), 0777) + if err != nil { + t.Fatal(err) + } + + files := []string{ + filepath.Join("cmd", "foo.go"), + filepath.Join("docs", "foo.md"), + filepath.Join("cmd", "foo_test.go"), + filepath.Join("cmd", "bar.go"), + filepath.Join("docs", "bar.md"), + filepath.Join("cmd", "bar_test.go"), + "another_test.go", + } + for _, f := range files { + createEmptyFile(t, filepath.Join(dir, f)) + } + + var buf bytes.Buffer + bad, err := checkMissingFiles(entries, dir, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no missing files, got %d", bad) + } + + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } +} + +func TestCheckEntrypointDocMismatchBad(t *testing.T) { + var tests = []struct { + name string + entries []entry + want int + }{ + { + name: "one entry", + entries: []entry{ + { + Entrypoint: "foo", + Doc: "docs/commands/bar.md", + }, + }, + want: 1, + }, + { + name: "two entries", + entries: []entry{ + { + Entrypoint: "foo", + Doc: "docs/commands/bar.md", + }, + { + Entrypoint: "bar", + Doc: "docs/commands/baz.md", + }, + }, + want: 2, + }, + { + name: "skip", + entries: []entry{ + { + Entrypoint: "foo", + Doc: "docs/commands/bar.md", + Skip: true, + }, + { + Entrypoint: "bar", + Doc: "docs/commands/baz.md", + }, + }, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + bad, err := checkEntrypointDocMismatch(tt.entries, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != tt.want { + t.Errorf("command/doc mismatches: want %d, got %d", tt.want, bad) + } + out := buf.String() + assertCode(t, out, "03", tt.want) + }) + } +} + +func TestCheckEntrypointDocMismatchGood(t *testing.T) { + entries := []entry{ + { + Entrypoint: "foo bar", + Doc: "docs/commands/foo_bar.md", + }, + { + Entrypoint: "baz", + Doc: "docs/commands/baz.md", + }, + } + + var buf bytes.Buffer + bad, err := checkEntrypointDocMismatch(entries, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no mismatches, got %d", bad) + } + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } +} + +func TestCheckDupEntrypointsBad(t *testing.T) { + entries := []entry{ + { + Entrypoint: "foo", + }, + { + Entrypoint: "bar", + }, + { + Entrypoint: "foo", + }, + { + Entrypoint: "baz", + }, + } + + var buf bytes.Buffer + bad, err := checkDupEntrypoints(entries, &buf) + if err != nil { + t.Fatal(err) + } + + wantBad := 1 + if bad != wantBad { + t.Errorf("expected %d duplicated entry, got %d", wantBad, bad) + } + out := buf.String() + assertCode(t, out, "04", 1) +} + +func TestCheckDupEntrypointsGood(t *testing.T) { + entries := []entry{ + { + Entrypoint: "foo", + }, + { + Entrypoint: "bar", + }, + { + Entrypoint: "baz", + }, + } + + var buf bytes.Buffer + bad, err := checkDupEntrypoints(entries, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no duplicated entries, got %d", bad) + } + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } +} + +func TestCheckMissingEntriesBad(t *testing.T) { + dir := t.TempDir() + files := []string{ + "foo_bar.md", + "baz.md", + } + for _, f := range files { + fname := filepath.Join(dir, f) + createEmptyFile(t, fname) + } + + var tests = []struct { + name string + entries []entry + want int + }{ + { + name: "no entries", + entries: []entry{}, + want: 2, + }, + { + name: "one entry", + entries: []entry{ + { + Entrypoint: "foo bar", + }, + }, + want: 1, + }, + { + name: "other entry", + entries: []entry{ + { + Entrypoint: "baz", + }, + }, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + bad, err := checkMissingEntries(tt.entries, dir, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != tt.want { + t.Errorf("missing entries: want %d, got %d", tt.want, bad) + } + out := buf.String() + assertCode(t, out, "05", tt.want) + }) + } +} + +func TestCheckMissingEntriesGood(t *testing.T) { + dir := t.TempDir() + files := []string{ + "foo_bar.md", + "baz.md", + } + for _, f := range files { + fname := filepath.Join(dir, f) + createEmptyFile(t, fname) + } + + entries := []entry{ + { + Entrypoint: "foo bar", + }, + { + Entrypoint: "baz", + }, + } + + var buf bytes.Buffer + bad, err := checkMissingEntries(entries, dir, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no missing entries, got %d", bad) + } + + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } +} + +func writeEntries(t *testing.T, es []entry, outfile string) { + t.Helper() + bs, err := yaml.Marshal(&es) + if err != nil { + t.Fatal(err) + } + fd, err := os.Create(outfile) + if err != nil { + fd.Close() + t.Fatal(err) + } + _, err = fd.Write(bs) + if err != nil { + t.Fatal(err) + } + + err = fd.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestCheckAll(t *testing.T) { + dir := t.TempDir() + docdir := filepath.Join(dir, "docs", "commands") + err := os.MkdirAll(docdir, 0777) + if err != nil { + t.Fatal(err) + } + + codedir := filepath.Join(dir, "cmd") + err = os.MkdirAll(codedir, 0777) + if err != nil { + t.Fatal(err) + } + + files := []string{ + filepath.Join(docdir, "foo_bar.md"), + filepath.Join(docdir, "baz.md"), + filepath.Join(docdir, "skip.md"), + filepath.Join(codedir, "foobar.go"), + filepath.Join(codedir, "baz.go"), + filepath.Join(codedir, "baz_test.go"), + } + for _, f := range files { + createEmptyFile(t, f) + } + + goodEntries := []entry{ + { + Entrypoint: "foo bar", + Code: "cmd/foobar.go", + Doc: "docs/commands/foo_bar.md", + }, + { + Entrypoint: "baz", + Code: "cmd/baz.go", + Doc: "docs/commands/baz.md", + Tests: []string{"cmd/baz_test.go"}, + }, + { + Entrypoint: "skip", + Skip: true, + }, + } + + yfile := filepath.Join(dir, "docs", "matrix.yaml") + writeEntries(t, goodEntries, yfile) + + t.Run("all good", func(t *testing.T) { + var buf bytes.Buffer + bad, err := check(yfile, docdir, dir, &buf) + if err != nil { + t.Fatal(err) + } + + if bad != 0 { + t.Errorf("expected no missing entries, got %d", bad) + } + + out := buf.String() + if out != "" { + t.Errorf("expected empty output, got %q", out) + } + }) + + // Now force each category of failure. + + badEntries := []entry{ + // Non-existent files (02), command/doc mismatch (03). + { + Entrypoint: "notthere", + Code: "cmd/../notthere.go", + Doc: "docs/commands/nt.md", + Tests: []string{"cmd/notthere_test.go"}, + }, + // Repeated entry (04). + { + Entrypoint: "foo bar", + Code: "cmd/foobar.go", + Doc: "docs/commands/foo_bar.md", + }, + } + + writeEntries(t, append(goodEntries, badEntries...), yfile) + // Documentation file without entry (05). + createEmptyFile(t, filepath.Join(docdir, "noentry.md")) + + t.Run("some bad", func(t *testing.T) { + var buf bytes.Buffer + bad, err := check(yfile, docdir, dir, &buf) + if err != nil { + t.Fatal(err) + } + + wantBad := 7 + if bad != wantBad { + t.Errorf("expected %d missing entries, got %d", wantBad, bad) + } + + out := buf.String() + assertCode(t, out, "01", 1) + assertCode(t, out, "02", 3) + assertCode(t, out, "03", 1) + assertCode(t, out, "04", 1) + assertCode(t, out, "05", 1) + }) + +} diff --git a/checkmat/main.go b/checkmat/main.go new file mode 100644 index 00000000..f0dd98fa --- /dev/null +++ b/checkmat/main.go @@ -0,0 +1,265 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +const usageMessage = `usage: checkmat + +Run various checks on the traceability matrix defined in . Each file in + is taken as the documentation for an entry point, where the base name maps +to an entry point in once the file extension is removed and underscores +are substituted for spaces. + +Verify that + + [01] each file name is valid + + To be considered "valid", a file name must be relative, must use "/" as + the path separator, and must not contain "." or ".." elements. + + [02] each named file exists + + The current working directory is taken as the top-level project directory, + and the files should be relative to this. + + [03] the base file name for the documentation matches the entry point name + + [04] no entry point has more than one entry in + + [05] for each file in , an entry with a matching entry point is found in + + + This check is skipped for any entries with a true "skip" value. + +Exit with status 1 if any issues are found. +` + +func usage() { + fmt.Fprint(flag.CommandLine.Output(), usageMessage) +} + +type entry struct { + Entrypoint string + Code string + Doc string + Tests []string + Skip bool +} + +func readEntries(f string) ([]entry, error) { + bs, err := os.ReadFile(f) + if err != nil { + return nil, err + } + var es []entry + if err := yaml.Unmarshal(bs, &es); err != nil { + return nil, err + } + + return es, nil +} + +func entryFiles(e entry) []string { + var fs []string + if e.Code != "" { + fs = append(fs, e.Code) + } + if e.Doc != "" { + fs = append(fs, e.Doc) + } + fs = append(fs, e.Tests...) + + return fs +} + +func checkValidFileNames(es []entry, w io.Writer) (int, error) { + var bad int + + for _, e := range es { + for _, f := range entryFiles(e) { + if !fs.ValidPath(f) { + bad++ + fmt.Fprintln(w, "[01] file name is invalid:", f) + } + } + } + + return bad, nil +} + +func checkMissingFiles(es []entry, topdir string, w io.Writer) (int, error) { + var bad int + + for _, e := range es { + for _, f := range entryFiles(e) { + // Note: Join() takes care of replacing slashes with + // os.PathSeparator. + f = filepath.Join(topdir, f) + _, err := os.Stat(f) + if errors.Is(err, os.ErrNotExist) { + bad++ + fmt.Fprintln(w, "[02] file does not exist:", f) + } else if err != nil { + return bad, err + } + } + } + + return bad, nil +} + +// TODO: Make it possible to customize how to map the doc file to the entry +// point name. As is it is only useful for command-line subcommands or one-off +// top-level commands, and it assumes that none of the commands have an +// underscore in their name. + +func docToEntrypoint(f string) string { + base := filepath.Base(f) + name := strings.TrimSuffix(base, filepath.Ext(base)) + + return strings.ReplaceAll(name, "_", " ") +} + +func checkEntrypointDocMismatch(es []entry, w io.Writer) (int, error) { + var bad int + + for _, e := range es { + if e.Skip { + continue + } + if docToEntrypoint(e.Doc) != e.Entrypoint { + bad++ + fmt.Fprintf(w, "[03] entry point and doc file mismatch: %q != %q\n", + e.Entrypoint, e.Doc) + } + } + + return bad, nil +} + +func checkDupEntrypoints(es []entry, w io.Writer) (int, error) { + var bad int + + cmds := make(map[string]bool) + for _, e := range es { + if _, found := cmds[e.Entrypoint]; found { + fmt.Fprintf(w, "[04] entry point %q defined more than once\n", + e.Entrypoint) + bad++ + } else { + cmds[e.Entrypoint] = true + } + } + + return bad, nil +} + +func checkMissingEntries(es []entry, docdir string, w io.Writer) (int, error) { + var bad int + + fh, err := os.Open(docdir) + if err != nil { + return bad, err + } + defer fh.Close() + + fnames, err := fh.Readdirnames(-1) + if err != nil { + return bad, err + } + + cmds := make(map[string]bool) + for _, e := range es { + cmds[e.Entrypoint] = true + } + + for _, f := range fnames { + if _, found := cmds[docToEntrypoint(f)]; !found { + fmt.Fprintf(w, "[05] No yaml entry for %q\n", filepath.Join(docdir, f)) + bad++ + } + } + + return bad, nil +} + +// check runs all the check functions on the traceability matrix defined in file +// yaml and returns the total number of issues found. docdir points to a +// directory containing the documentation files. topdir is an absolute path to +// top-level directory to which files in `yaml` are specified as relative. +// +// For each issue found, a message is written to w. +func check(yaml string, docdir string, topdir string, w io.Writer) (int, error) { + var bad int + + entries, err := readEntries(yaml) + if err != nil { + return bad, err + } + + type check func([]entry, io.Writer) (int, error) + checks := []check{ + checkValidFileNames, + func(es []entry, w io.Writer) (int, error) { + return checkMissingFiles(es, topdir, w) + }, + checkEntrypointDocMismatch, + checkDupEntrypoints, + func(es []entry, w io.Writer) (int, error) { + return checkMissingEntries(es, docdir, w) + }, + } + + for _, f := range checks { + n, err := f(entries, w) + if err != nil { + return bad, err + } + bad += n + } + + return bad, nil +} + +func main() { + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + flag.CommandLine.SetOutput(os.Stdout) + } + flag.Usage = usage + flag.Parse() + args := flag.Args() + if len(args) != 2 { + flag.Usage() + os.Exit(2) + } + + wd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(2) + } + + bad, err := check(args[0], args[1], wd, os.Stdout) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(2) + } + + if bad > 0 { + fmt.Printf("\nproblems found: %d\n", bad) + os.Exit(1) + } +} diff --git a/rules.mk b/rules.mk index dd0f2eb0..a74f0365 100644 --- a/rules.mk +++ b/rules.mk @@ -6,6 +6,7 @@ endif VT_BIN_DIR ?= $(vtdir)/bin VT_DOC_DIR ?= docs/commands +VT_MATRIX ?= docs/validation/matrix.yaml .PHONY: vt-bin vt-bin: @@ -18,3 +19,13 @@ vt-gen-docs: @test -f '$(VT_BIN_DIR)/docgen' || \ { printf '"make vt-bin" did not generate $(VT_BIN_DIR)/docgen\n'; exit 1; } '$(VT_BIN_DIR)/docgen' '$(VT_DOC_DIR)' + +$(VT_BIN_DIR)/checkmat: $(vtdir)/checkmat/main.go + +.PHONY: vt-checkmat +vt-checkmat: $(VT_BIN_DIR)/checkmat + '$(VT_BIN_DIR)/checkmat' '$(VT_MATRIX)' '$(VT_DOC_DIR)' + +$(VT_BIN_DIR)/%: $(vtdir)/%/main.go + @mkdir -p '$(VT_BIN_DIR)' + go build -o '$@' '$<' From 7b5d684a1bf003c81e72b7bb39aca4c1d0acee10 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 04/41] add target to copy traceability matrix to output directory --- .gitignore | 1 + rules.mk | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/.gitignore b/.gitignore index ae3c1726..d8f7cdcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /bin/ +/output/ diff --git a/rules.mk b/rules.mk index a74f0365..9e5f7e7a 100644 --- a/rules.mk +++ b/rules.mk @@ -4,6 +4,17 @@ ifeq ($(strip $(vtdir)),) $(error "bug: vtdir is unexpectedly empty") endif +ifeq ($(VT_PKG),) +VT_PKG := $(notdir $(CURDIR)) +endif + +version := $(shell git describe --tags --always HEAD) +version := $(version:v%=%) +name := $(VT_PKG)_$(version) + +VT_OUT_DIR ?= $(vtdir)/output/$(name) +prefix := $(VT_OUT_DIR)/$(name) + VT_BIN_DIR ?= $(vtdir)/bin VT_DOC_DIR ?= docs/commands VT_MATRIX ?= docs/validation/matrix.yaml @@ -26,6 +37,15 @@ $(VT_BIN_DIR)/checkmat: $(vtdir)/checkmat/main.go vt-checkmat: $(VT_BIN_DIR)/checkmat '$(VT_BIN_DIR)/checkmat' '$(VT_MATRIX)' '$(VT_DOC_DIR)' +.PHONY: vt-copymat +vt-copymat: + $(MAKE) vt-gen-docs + $(MAKE) vt-checkmat + @test -z "$$(git status -unormal --porcelain -- '$(VT_DOC_DIR)')" || \ + { printf 'commit changes to $(VT_DOC_DIR) first\n'; exit 1; } + @mkdir -p '$(VT_OUT_DIR)' + cp '$(VT_MATRIX)' '$(prefix).matrix.yaml' + $(VT_BIN_DIR)/%: $(vtdir)/%/main.go @mkdir -p '$(VT_BIN_DIR)' go build -o '$@' '$<' From 18d8b5089f587fdf13636c68c47a389e85c39b92 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 05/41] add target to generate source tarball --- rules.mk | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rules.mk b/rules.mk index 9e5f7e7a..ec51ad41 100644 --- a/rules.mk +++ b/rules.mk @@ -46,6 +46,13 @@ vt-copymat: @mkdir -p '$(VT_OUT_DIR)' cp '$(VT_MATRIX)' '$(prefix).matrix.yaml' +.PHONY: vt-archive +vt-archive: + @mkdir -p '$(VT_OUT_DIR)' + @test -z "$(git status --porcelain -unormal --ignore-submodules=none)" || \ + { printf >&2 'working tree is dirty; commit changes first\n'; exit 1; } + git archive -o '$(prefix).tar.gz' --format=tar.gz HEAD + $(VT_BIN_DIR)/%: $(vtdir)/%/main.go @mkdir -p '$(VT_BIN_DIR)' go build -o '$@' '$<' From 042400b176c7b4fd87a038e0ad62392921dcdaa9 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 06/41] add command to format 'go test' output --- fmttests/fmttests_test.go | 290 ++++++++++++++++++++++++++++++++++++++ fmttests/main.go | 147 +++++++++++++++++++ rules.mk | 2 + 3 files changed, 439 insertions(+) create mode 100644 fmttests/fmttests_test.go create mode 100644 fmttests/main.go diff --git a/fmttests/fmttests_test.go b/fmttests/fmttests_test.go new file mode 100644 index 00000000..cea88634 --- /dev/null +++ b/fmttests/fmttests_test.go @@ -0,0 +1,290 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "encoding/json" + "io" + "strings" + "testing" + "time" +) + +type testEvent struct { + Action string + Package string + Test string +} + +func makeEventReader(t *testing.T, tes []testEvent) io.Reader { + t.Helper() + var buf bytes.Buffer + var e event + for _, te := range tes { + e = event{ + Time: time.Time{}, + Action: te.Action, + Package: te.Package, + Test: te.Test, + } + bs, err := json.Marshal(&e) + if err != nil { + t.Fatal(err) + } + buf.Write(bs) + buf.Write([]byte("\n")) + } + return bytes.NewReader(buf.Bytes()) +} + +func TestProcessEvents(t *testing.T) { + var tests = []struct { + name string + events []testEvent + subtests bool + lines []string + nfailed int + npassed int + nskipped int + }{ + { + name: "base", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + { + Action: "pass", + Package: "example.com/ghi/cmd", + Test: "TestBaz", + }, + }, + subtests: false, + lines: []string{ + "[def] TestFoo: passed", + "[cmd] TestBaz: passed", + }, + nfailed: 0, + npassed: 3, + nskipped: 0, + }, + { + name: "subtests via flag", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + { + Action: "pass", + Package: "example.com/ghi/cmd", + Test: "TestBaz", + }, + }, + subtests: true, + lines: []string{ + "[def] TestFoo: passed", + "[def] TestFoo/Bar: passed", + "[cmd] TestBaz: passed", + }, + nfailed: 0, + npassed: 3, + nskipped: 0, + }, + { + name: "include skipped subtests", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "skip", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + { + Action: "pass", + Package: "example.com/ghi/cmd", + Test: "TestBaz", + }, + }, + subtests: false, + lines: []string{ + "[def] TestFoo: passed", + "[def] TestFoo/Bar: skipped", + "[cmd] TestBaz: passed", + }, + nfailed: 0, + npassed: 2, + nskipped: 1, + }, + { + name: "no package", + events: []testEvent{ + { + Action: "pass", + Package: "", + Test: "TestFoo", + }, + }, + subtests: false, + lines: []string{ + "[] TestFoo: passed", + }, + nfailed: 0, + npassed: 1, + nskipped: 0, + }, + { + name: "include failed subtests", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "fail", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + { + Action: "pass", + Package: "example.com/ghi/cmd", + Test: "TestBaz", + }, + }, + subtests: false, + lines: []string{ + "[def] TestFoo: passed", + "[def] TestFoo/Bar: failed", + "[cmd] TestBaz: passed", + }, + nfailed: 1, + npassed: 2, + nskipped: 0, + }, + { + name: "filtered", + events: []testEvent{ + { + Action: "start", + Package: "example.com/abc/def", + }, + { + Action: "run", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + }, + subtests: false, + lines: []string{ + "[def] TestFoo: passed", + }, + nfailed: 0, + npassed: 1, + nskipped: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + s, err := processEvents(makeEventReader(t, tt.events), tt.subtests, &buf) + if err != nil { + t.Fatal(err) + } + + if len(s.Failed) != tt.nfailed { + t.Errorf("failed: want %d, got %d", tt.nfailed, len(s.Failed)) + } + if len(s.Passed) != tt.npassed { + t.Errorf("passed: want %d, got %d", tt.npassed, len(s.Passed)) + } + if len(s.Skipped) != tt.nskipped { + t.Errorf("skipped: want %d, got %d", tt.nskipped, len(s.Skipped)) + } + + out := buf.String() + outWant := strings.Join(tt.lines, "\n") + "\n" + if out != outWant { + t.Errorf("stdout:\n want %q,\n got %q", outWant, out) + } + }) + } +} + +func TestProcessEventsErrors(t *testing.T) { + var tests = []struct { + name string + events []testEvent + subtests bool + lines []string + nfailed int + npassed int + nskipped int + }{ + { + name: "bench", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "bench", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + }, + }, + { + name: "unknown", + events: []testEvent{ + { + Action: "pass", + Package: "example.com/abc/def", + Test: "TestFoo", + }, + { + Action: "unknown", + Package: "example.com/abc/def", + Test: "TestFoo/Bar", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + _, err := processEvents(makeEventReader(t, tt.events), false, &buf) + if err == nil { + t.Errorf("processEvents unexpectedly passed") + } + }) + } +} diff --git a/fmttests/main.go b/fmttests/main.go new file mode 100644 index 00000000..a702b921 --- /dev/null +++ b/fmttests/main.go @@ -0,0 +1,147 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "bufio" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + "strings" + "time" +) + +const usageMessage = `usage: fmttests [-allow-skips] [-subtests] + +Read 'go test -json' lines from standard input and display a formatted output +line for each test result record (action of "pass", "fail", or "skip"). + +Passing subtests are not displayed unless the -subtests flag is specified. +Failed or skipped subtests are always displayed. + +If any "fail" record is encountered, exit with status 1. Any "skip" record will +also trigger an exit with status 1 unless the -allow-skips flag is specified. +` + +var ( + subtests = flag.Bool("subtests", false, "") + allowSkips = flag.Bool("allow-skips", false, "") +) + +func usage() { + fmt.Fprint(flag.CommandLine.Output(), usageMessage) +} + +// Modified from Go's src/cmd/internal/test2json/test2json.go. +type event struct { + Time time.Time `json:",omitempty"` + Action string + Package string `json:",omitempty"` + Test string `json:",omitempty"` + Elapsed float64 `json:",omitempty"` + Output string `json:",omitempty"` +} + +type summary struct { + Failed []string + Passed []string + Skipped []string +} + +func packageBaseName(s string) string { + xs := strings.Split(s, "/") + n := len(xs) + if n == 0 { + return s + } + + return xs[n-1] +} + +// processEvents reads a JSON record of a test event from r. For each result +// record encountered (i.e. a record with an action of "pass", "fail", or +// "skip"), it writes a formatted line to w. The subtests argument controls +// whether results for subtests are written. +// +// processEvents returns a summary instance that records the test names for each +// result record encountered. +func processEvents(r io.Reader, subtests bool, w io.Writer) (summary, error) { + var res summary + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + var e event + + if err := json.Unmarshal(scanner.Bytes(), &e); err != nil { + return res, err + } + if e.Test == "" { + continue + } + + var status string + var alwaysShow bool + switch e.Action { + case "fail": + res.Failed = append(res.Failed, e.Test) + status = "failed" + alwaysShow = true + case "pass": + res.Passed = append(res.Passed, e.Test) + status = "passed" + case "skip": + res.Skipped = append(res.Skipped, e.Test) + status = "skipped" + alwaysShow = true + case "bench": + return res, errors.New("bench output is not supported") + case "cont", "output", "pause", "run", "start": + continue + default: + return res, fmt.Errorf("unknown action: %s", e.Action) + } + + if subtests || alwaysShow || !strings.ContainsAny(e.Test, "/") { + // TODO: Consider other approaches for formatting the package name + // that avoid collisions (e.g., packageBaseName returns "cmd" for + // both ".../foo/cmd" and ".../bar/cmd"). + fmt.Fprintf(w, "[%s] %s: %s\n", + packageBaseName(e.Package), e.Test, status) + } + } + + if err := scanner.Err(); err != nil { + return res, err + } + + return res, nil +} + +func main() { + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + flag.CommandLine.SetOutput(os.Stdout) + } + flag.Usage = usage + flag.Parse() + + if flag.NArg() != 0 { + flag.Usage() + os.Exit(2) + } + + res, err := processEvents(os.Stdin, *subtests, os.Stdout) + if err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(2) + } + + if len(res.Failed) > 0 || (!*allowSkips && len(res.Skipped) > 0) { + fmt.Fprintf(os.Stderr, "failed tests: %d, skipped tests: %d\n", + len(res.Failed), len(res.Skipped)) + os.Exit(1) + } +} diff --git a/rules.mk b/rules.mk index ec51ad41..eda622e2 100644 --- a/rules.mk +++ b/rules.mk @@ -46,6 +46,8 @@ vt-copymat: @mkdir -p '$(VT_OUT_DIR)' cp '$(VT_MATRIX)' '$(prefix).matrix.yaml' +$(VT_BIN_DIR)/fmttests: $(vtdir)/fmttests/main.go + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' From 84075a31f7a7ebc5f49f6b612d357bfa45bdef46 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 07/41] add target to run tests The test runners will enable coverage when GOCOVERDIR is set. --- rules.mk | 12 ++++++++++++ scripts/run-tests | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 scripts/run-tests diff --git a/rules.mk b/rules.mk index eda622e2..05d03792 100644 --- a/rules.mk +++ b/rules.mk @@ -19,6 +19,12 @@ VT_BIN_DIR ?= $(vtdir)/bin VT_DOC_DIR ?= docs/commands VT_MATRIX ?= docs/validation/matrix.yaml +VT_TEST_ALLOW_SKIPS ?= no +VT_TEST_RUNNERS ?= +ifeq ($(strip $(VT_TEST_RUNNERS)),) +$(error "VT_TEST_RUNNERS must point to space-delimited list of test scripts") +endif + .PHONY: vt-bin vt-bin: rm -rf '$(VT_BIN_DIR)' && mkdir '$(VT_BIN_DIR)' @@ -48,6 +54,12 @@ vt-copymat: $(VT_BIN_DIR)/fmttests: $(vtdir)/fmttests/main.go +.PHONY: vt-test +vt-test: $(VT_BIN_DIR)/fmttests + @unset GOCOVERDIR; \ + '$(vtdir)/scripts/run-tests' '$(VT_BIN_DIR)/fmttests' \ + '$(VT_TEST_ALLOW_SKIPS)' $(VT_TEST_RUNNERS) + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' diff --git a/scripts/run-tests b/scripts/run-tests new file mode 100755 index 00000000..5733f740 --- /dev/null +++ b/scripts/run-tests @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright 2024 Metrum Research Group +# SPDX-License-Identifier: MIT + +set -uo pipefail + +test $# -gt 2 || { + printf >&2 'usage: %s ...\n' "$0" + exit 1 +} + +fmt=$1 +shift +allow_skips=$(echo "$1" | tr '[:upper:]' '[:lower:]') +case "$allow_skips" in + 1|yes|y|true|t) + skip_arg=-allow-skips + ;; + *) + skip_arg= + ;; +esac +shift + +status=0 +for runner in "$@" +do + # shellcheck disable=SC2086 + "$runner" -json | "$fmt" $skip_arg || { + status=$? + printf >&2 '%s failed\n' "$runner" + } +done +exit "$status" From 8ffd33781e6b0c640a3ead1346baac2e4247b739 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 08/41] add command to calculate coverage per file --- filecov/filecov_test.go | 353 ++++++++++++++++++++++++++++++++++++++++ filecov/main.go | 201 +++++++++++++++++++++++ rules.mk | 2 + 3 files changed, 556 insertions(+) create mode 100644 filecov/filecov_test.go create mode 100644 filecov/main.go diff --git a/filecov/filecov_test.go b/filecov/filecov_test.go new file mode 100644 index 00000000..106561e0 --- /dev/null +++ b/filecov/filecov_test.go @@ -0,0 +1,353 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "encoding/json" + "math" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "golang.org/x/tools/cover" +) + +func assertNearEqual(t *testing.T, a float64, b float64) { + t.Helper() + if math.IsNaN(a) || math.IsNaN(b) { + t.Fatal("assertNearEqual: NaN values are not allowed") + } + + var tol float64 = 1e-8 + d := math.Abs(a - b) + if d >= tol { + t.Errorf("absolute difference exceeds tolerance (%e)\na=%f\nb=%f", tol, a, b) + } +} + +func assertFileCoverage(t *testing.T, got []*fileCoverage, want []*fileCoverage) { + t.Helper() + + ngot := len(got) + nwant := len(want) + if ngot != nwant { + t.Errorf("expected coverage for %d files, got %d", nwant, ngot) + return + } + + mapWant := make(map[string]float64, nwant) + for _, fc := range want { + if _, found := mapWant[fc.File]; found { + t.Error("repeated coverage for", fc.File) + } + mapWant[fc.File] = fc.Coverage + } + + for _, fc := range got { + perc, found := mapWant[fc.File] + if found { + assertNearEqual(t, fc.Coverage, perc) + } else { + t.Error("no coverage measurement for", fc.File) + } + + } +} + +// See cover.ParseProfilesFromReader for a description of the format. +var testProfile string = `mode: set +example.com/tmod/foo.go:3.16,5.2 1 1 +example.com/tmod/foo.go:7.21,9.2 1 0 +example.com/tmod/foo.go:11.21,13.2 1 0 +example.com/tmod/foo.go:15.19,17.2 1 0 +example.com/tmod/cmd/bar.go:3.18,5.2 1 1 +example.com/tmod/cmd/bar.go:7.25,9.2 1 0 +example.com/tmod/cmd/main.go:7.13,10.2 2 0 +` + +func TestPercentCovered(t *testing.T) { + r := strings.NewReader(testProfile) + profiles, err := cover.ParseProfilesFromReader(r) + if err != nil { + t.Fatal(err) + } + cov := percentCovered(profiles) + assertNearEqual(t, cov.Overall, 25.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "example.com/tmod/foo.go", + Coverage: 25.0, + }, + { + File: "example.com/tmod/cmd/bar.go", + Coverage: 50.0, + }, + { + File: "example.com/tmod/cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} + +func TestPercentCoveredZero(t *testing.T) { + re := regexp.MustCompile(`(?m) 1$`) + r := strings.NewReader(re.ReplaceAllString(testProfile, " 0")) + profiles, err := cover.ParseProfilesFromReader(r) + if err != nil { + t.Fatal(err) + } + cov := percentCovered(profiles) + assertNearEqual(t, cov.Overall, 0.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "example.com/tmod/foo.go", + Coverage: 0.0, + }, + { + File: "example.com/tmod/cmd/bar.go", + Coverage: 0.0, + }, + { + File: "example.com/tmod/cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} + +func TestPercentCoveredNoFiles(t *testing.T) { + r := strings.NewReader("mode: set\n") + profiles, err := cover.ParseProfilesFromReader(r) + if err != nil { + t.Fatal(err) + } + cov := percentCovered(profiles) + assertNearEqual(t, cov.Overall, 0.0) + + if len(cov.Files) != 0 { + t.Errorf("expected coverage for 0 files, got %d", len(cov.Files)) + } +} + +func TestWrite(t *testing.T) { + r := strings.NewReader(testProfile) + profiles, err := cover.ParseProfilesFromReader(r) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = write(&buf, profiles, "", "") + if err != nil { + t.Fatal(err) + } + + var cov coverage + err = json.Unmarshal(buf.Bytes(), &cov) + if err != nil { + t.Fatal(err) + } + + assertNearEqual(t, cov.Overall, 25.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "example.com/tmod/foo.go", + Coverage: 25.0, + }, + { + File: "example.com/tmod/cmd/bar.go", + Coverage: 50.0, + }, + { + File: "example.com/tmod/cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} + +func TestWriteShortenNames(t *testing.T) { + r := strings.NewReader(testProfile) + profiles, err := cover.ParseProfilesFromReader(r) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = write(&buf, profiles, "example.com/tmod", "") + if err != nil { + t.Fatal(err) + } + + var cov coverage + err = json.Unmarshal(buf.Bytes(), &cov) + if err != nil { + t.Fatal(err) + } + + assertNearEqual(t, cov.Overall, 25.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "foo.go", + Coverage: 25.0, + }, + { + File: "cmd/bar.go", + Coverage: 50.0, + }, + { + File: "cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} + +func chdir(t *testing.T, dir string) { + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + err = os.Chdir(dir) + if err != nil { + _ = os.Chdir(old) + t.Fatal(err) + } + + t.Cleanup(func() { + _ = os.Chdir(old) + }) +} + +func setupRunDir(t *testing.T) string { + dir := t.TempDir() + realmodPath := filepath.Join(dir, "realmod") + err := os.MkdirAll(filepath.Join(realmodPath, "cmd"), 0777) + if err != nil { + t.Fatal(err) + } + + modPath := filepath.Join(dir, "mod") + err = os.Symlink(realmodPath, modPath) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile( + filepath.Join(modPath, "go.mod"), + []byte("module example.com/tmod\n\ngo 1.22.5"), + 0666) + if err != nil { + t.Fatal(err) + } + + tfiles := []string{ + "foo.go", + "cmd/bar.go", + "cmd/main.go", + } + for _, f := range tfiles { + fh, err := os.Create(filepath.Join(modPath, f)) + if err != nil { + t.Fatal(err) + } + err = fh.Close() + if err != nil { + t.Fatal(err) + } + } + + chdir(t, modPath) + + return modPath +} + +func TestRun(t *testing.T) { + modPath := setupRunDir(t) + pcontent := strings.ReplaceAll(testProfile, + "example.com/tmod/cmd/main.go", + modPath+"/"+"cmd/main.go") + profPath := filepath.Join(modPath, "coverage.out") + err := os.WriteFile(profPath, []byte(pcontent), 0666) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = run(profPath, "go.mod", &buf) + if err != nil { + t.Fatal(err) + } + + var cov coverage + err = json.Unmarshal(buf.Bytes(), &cov) + if err != nil { + t.Fatal(err) + } + + assertNearEqual(t, cov.Overall, 25.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "foo.go", + Coverage: 25.0, + }, + { + File: "cmd/bar.go", + Coverage: 50.0, + }, + { + File: "cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} + +func TestRunNoGoMod(t *testing.T) { + modPath := setupRunDir(t) + profPath := filepath.Join(modPath, "coverage.out") + err := os.WriteFile(profPath, []byte(testProfile), 0666) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + err = run(profPath, "", &buf) + if err != nil { + t.Fatal(err) + } + + var cov coverage + err = json.Unmarshal(buf.Bytes(), &cov) + if err != nil { + t.Fatal(err) + } + + assertNearEqual(t, cov.Overall, 25.0) + assertFileCoverage(t, cov.Files, + []*fileCoverage{ + { + File: "example.com/tmod/foo.go", + Coverage: 25.0, + }, + { + File: "example.com/tmod/cmd/bar.go", + Coverage: 50.0, + }, + { + File: "example.com/tmod/cmd/main.go", + Coverage: 0.0, + }, + }, + ) +} diff --git a/filecov/main.go b/filecov/main.go new file mode 100644 index 00000000..ff465882 --- /dev/null +++ b/filecov/main.go @@ -0,0 +1,201 @@ +// Copyright 2024 Metrum Research Group +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/mod/modfile" + "golang.org/x/tools/cover" +) + +const usageMessage = `usage: filecov [-mod=] + +Calculate the coverage for each file in along with the overall +coverage and write the JSON result to standard output. is a coverage +profile in the format generated by 'go test' for its -coverprofile argument. + +Options: + + -mod= + The file entries in are prefixed with the module name (e.g., + "example.com/mod/cmd/foo.go") or, in some cases, the absolute path to the + file. If you pass a module's go.mod to this option, the module name from + that file is stripped from the file names in the output (yielding, e.g., + "cmd/foo.go"). This is particularly useful in the common case where all the + files belong to the same module. ` + +var ( + gomod = flag.String("mod", "", "") +) + +func usage() { + fmt.Fprint(flag.CommandLine.Output(), usageMessage) +} + +type tally struct { + covered int64 + total int64 +} + +func tallyCovered(p *cover.Profile) tally { + // This follows Go's src/cmd/cover/html.go. + var total, covered int64 + for _, b := range p.Blocks { + total += int64(b.NumStmt) + if b.Count > 0 { + covered += int64(b.NumStmt) + } + } + + return tally{covered: covered, total: total} +} + +type fileCoverage struct { + File string `json:"file"` + Coverage float64 `json:"coverage"` +} + +type coverage struct { + Overall float64 `json:"overall"` + Files []*fileCoverage `json:"files"` +} + +func percent(covered int64, total int64) float64 { + if total == 0 { + return 0 + } + + return float64(covered) / float64(total) * 100 +} + +func percentCovered(profiles []*cover.Profile) coverage { + var total, covered int64 + var fcovs []*fileCoverage + + for _, profile := range profiles { + ftally := tallyCovered(profile) + fcovs = append(fcovs, + &fileCoverage{ + File: profile.FileName, + Coverage: percent(ftally.covered, ftally.total), + }, + ) + covered += ftally.covered + total += ftally.total + } + + return coverage{Overall: percent(covered, total), Files: fcovs} +} + +func shortenFileNames(cov coverage, modpath string, localpath string) error { + if modpath == "" { + return nil + } + + mprefix := modpath + "/" + sep := string(filepath.Separator) + + var lprefix string + if localpath != "" { + localpath, err := filepath.EvalSymlinks(localpath) + if err != nil { + return err + } + lprefix = localpath + string(filepath.Separator) + } + + for _, entry := range cov.Files { + var prefix, file string + var err error + if lprefix != "" && strings.HasPrefix(entry.File, sep) { + // For the executable, Go writes a path like + // /path/to/bbi/cmd/bbi/main.go instead of the module-prefixed name + // (github.com/metrumresearchgroup/bbi/...) that it writes for the + // other files. + file, err = filepath.EvalSymlinks(entry.File) + if err != nil { + return err + } + prefix = lprefix + } else { + file = entry.File + prefix = mprefix + } + entry.File = strings.TrimPrefix(file, prefix) + } + + return nil +} + +func write(w io.Writer, profiles []*cover.Profile, modpath string, localpath string) error { + cov := percentCovered(profiles) + if modpath != "" { + if err := shortenFileNames(cov, modpath, localpath); err != nil { + return err + } + } + + bs, err := json.MarshalIndent(cov, "", " ") + if err != nil { + return err + } + _, err = w.Write(append(bs, []byte("\n")...)) + + return err +} + +func run(input string, gomod string, w io.Writer) error { + profiles, err := cover.ParseProfiles(input) + if err != nil { + return err + } + + var mpath, lpath string + if gomod != "" { + gomod, err = filepath.Abs(gomod) + if err != nil { + return err + } + + mod, err := os.ReadFile(gomod) + if err != nil { + return err + } + + mpath = modfile.ModulePath(mod) + if mpath == "" { + return fmt.Errorf("could not find module path in %q", gomod) + } + + lpath = filepath.Dir(gomod) + } + + return write(w, profiles, mpath, lpath) +} + +func main() { + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + flag.CommandLine.SetOutput(os.Stdout) + } + flag.Usage = usage + flag.Parse() + args := flag.Args() + + if len(args) != 1 { + flag.Usage() + os.Exit(2) + } + + if err := run(args[0], *gomod, os.Stdout); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(2) + } +} diff --git a/rules.mk b/rules.mk index 05d03792..bb6427aa 100644 --- a/rules.mk +++ b/rules.mk @@ -60,6 +60,8 @@ vt-test: $(VT_BIN_DIR)/fmttests '$(vtdir)/scripts/run-tests' '$(VT_BIN_DIR)/fmttests' \ '$(VT_TEST_ALLOW_SKIPS)' $(VT_TEST_RUNNERS) +$(VT_BIN_DIR)/filecov: $(vtdir)/filecov/main.go + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' From 39af907e65ce21643d60954ee9f3a8bf037ac5a0 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 09/41] add target to run tests with coverage --- rules.mk | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/rules.mk b/rules.mk index bb6427aa..b80138f6 100644 --- a/rules.mk +++ b/rules.mk @@ -62,6 +62,24 @@ vt-test: $(VT_BIN_DIR)/fmttests $(VT_BIN_DIR)/filecov: $(vtdir)/filecov/main.go +# ATTN: Make coverage directory absolute because we cannot rely on +# test subprocesses to be executed from the same directory. +cov_dir := $(abspath $(VT_OUT_DIR)/.coverage) +cov_prof := $(cov_dir).profile + +.PHONY: vt-cover +vt-cover: export GOCOVERDIR=$(cov_dir) +vt-cover: $(VT_BIN_DIR)/filecov +vt-cover: $(VT_BIN_DIR)/fmttests + @mkdir -p '$(VT_OUT_DIR)' + rm -rf '$(cov_dir)' && mkdir '$(cov_dir)' + '$(vtdir)/scripts/run-tests' '$(VT_BIN_DIR)/fmttests' \ + '$(VT_TEST_ALLOW_SKIPS)' $(VT_TEST_RUNNERS) \ + >'$(prefix).check.txt' + go tool covdata textfmt -i '$(cov_dir)' -o '$(cov_prof)' + '$(VT_BIN_DIR)/filecov' -mod go.mod '$(cov_prof)' \ + >'$(prefix).coverage.json' + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' From 850c02aac8f9d1c728e8d358108865ebb5830ce7 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 10/41] add target to generate scores for mpn.scorecard --- rules.mk | 6 ++++ scripts/write-scores | 73 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100755 scripts/write-scores diff --git a/rules.mk b/rules.mk index b80138f6..9a8af03a 100644 --- a/rules.mk +++ b/rules.mk @@ -80,6 +80,12 @@ vt-cover: $(VT_BIN_DIR)/fmttests '$(VT_BIN_DIR)/filecov' -mod go.mod '$(cov_prof)' \ >'$(prefix).coverage.json' +.PHONY: vt-scores +vt-scores: + @mkdir -p '$(VT_OUT_DIR)' + '$(vtdir)/scripts/write-scores' '$(prefix).coverage.json' \ + >'$(prefix).scores.json' + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' diff --git a/scripts/write-scores b/scripts/write-scores new file mode 100755 index 00000000..396080dc --- /dev/null +++ b/scripts/write-scores @@ -0,0 +1,73 @@ +#!/bin/sh +# Copyright 2024 Metrum Research Group +# SPDX-License-Identifier: MIT +# +# shellcheck disable=SC3043 +# Note: shell must support non-POSIX 'local'. + +set -eu + +test $# = 1 || { + printf >&2 'usage: %s \n' "$0" + exit 1 +} + +covresults=$1 +cov=$(jq -e .overall <"$covresults") || { + printf >&2 'reading coverage from %s failed\n' "$covresults" + exit 1 +} + +ask () { + local ans + local res + + while true + do + printf >&2 '%s [yn] ' "$1" + read -r ans + case "$ans" in + y|Y|yes) + res=1 + break + ;; + n|N|no) + res=0 + break + ;; + *) + printf >&2 'Enter y or n.\n' + ;; + esac + done + printf '%s\n' "$res" +} + +# TODO: Check existence of NEWS.md (any others?) instead of prompting? +has_news=$(ask 'Does this package have a NEWS file?') +news_current=$(ask 'Does the version being scored have a NEWS entry?') +has_website=$(ask 'Does this package have a website?') + +jq -n \ + --argjson c "$cov" \ + --argjson w "$has_website" \ + --argjson n "$has_news" \ + --argjson N "$news_current" \ + '{ + "testing": { + "check": 1, + "coverage": ($c / 100) + }, + "documentation": { + "has_website": $w, + "has_news": $n + }, + "maintenance": { + "has_maintainer": 1, + "news_current": $N + }, + "transparency": { + "has_source_control": 1, + "has_bug_reports_url": 1 + } + }' From 37c8099d11031d939d083b3b4cee47c7537013b1 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 11/41] add script to generate metadata for mpn.scorecard --- rules.mk | 5 +++++ scripts/metadata | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100755 scripts/metadata diff --git a/rules.mk b/rules.mk index 9a8af03a..b1c84051 100644 --- a/rules.mk +++ b/rules.mk @@ -86,6 +86,11 @@ vt-scores: '$(vtdir)/scripts/write-scores' '$(prefix).coverage.json' \ >'$(prefix).scores.json' +.PHONY: vt-metadata +vt-metadata: + @mkdir -p '$(VT_OUT_DIR)' + '$(vtdir)/scripts/metadata' >'$(prefix).metadata.json' + .PHONY: vt-archive vt-archive: @mkdir -p '$(VT_OUT_DIR)' diff --git a/scripts/metadata b/scripts/metadata new file mode 100755 index 00000000..c6695803 --- /dev/null +++ b/scripts/metadata @@ -0,0 +1,42 @@ +#!/bin/sh +# Copyright 2024 Metrum Research Group +# SPDX-License-Identifier: MIT + +set -eu + +test $# = 0 || { + printf >&2 'usage: %s\n' "$0" + exit 1 +} + +# Note: This output matches the "metadata" field that mpn.scorecard +# outputs to the .scorecard.json for R packages. + +dt=$(date '+%Y-%m-%d %H:%M:%S') +user=${USER?'USER environment variable is not set'} +sysname=$(uname -s) +version=$(uname -v) +release=$(uname -r) +machine=$(uname -m) +mver=${METWORX_VERSION?'METWORX_VERSION environment variable is not set'} + +jq -n \ + --arg d "$dt" \ + --arg u "$user" \ + --arg s "$sysname" \ + --arg v "$version" \ + --arg r "$release" \ + --arg m "$machine" \ + --arg V "$mver" \ + '{"date": $d, + "executor": $u, + "info": { + "env_vars": {"METWORX_VERSION": $V}, + "sys": { + "sysname": $s, + "version": $v, + "release": $r, + "machine": $m + } + } + }' From 7f4e55f08633fd7904fb22e34fa792d60c185e38 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 12/41] metadata: include Go version --- scripts/metadata | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/metadata b/scripts/metadata index c6695803..92d1f6c5 100755 --- a/scripts/metadata +++ b/scripts/metadata @@ -10,7 +10,8 @@ test $# = 0 || { } # Note: This output matches the "metadata" field that mpn.scorecard -# outputs to the .scorecard.json for R packages. +# outputs to the .scorecard.json for R packages, with the addition of +# the Go version. dt=$(date '+%Y-%m-%d %H:%M:%S') user=${USER?'USER environment variable is not set'} @@ -20,6 +21,9 @@ release=$(uname -r) machine=$(uname -m) mver=${METWORX_VERSION?'METWORX_VERSION environment variable is not set'} +go_ver=$(go env GOVERSION) +go_ver=${go_ver#go} + jq -n \ --arg d "$dt" \ --arg u "$user" \ @@ -28,6 +32,7 @@ jq -n \ --arg r "$release" \ --arg m "$machine" \ --arg V "$mver" \ + --arg g "$go_ver" \ '{"date": $d, "executor": $u, "info": { @@ -36,7 +41,8 @@ jq -n \ "sysname": $s, "version": $v, "release": $r, - "machine": $m + "machine": $m, + "Go version": $g } } }' From 1bc01001c3d647d9162db9e76479dca70b3a34ba Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 13/41] add target to generate pkg.json This file provides required top-level .scorecard.json fields. mpn.scorecard will stitch this and the other json files into a complete object. --- rules.mk | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rules.mk b/rules.mk index b1c84051..1ea0a2bc 100644 --- a/rules.mk +++ b/rules.mk @@ -86,6 +86,15 @@ vt-scores: '$(vtdir)/scripts/write-scores' '$(prefix).coverage.json' \ >'$(prefix).scores.json' +.PHONY: vt-pkg +vt-pkg: + @mkdir -p '$(VT_OUT_DIR)' + jq -n --arg p '$(VT_PKG)' --arg v "$(version)" \ + '{"mpn_scorecard_format": "1.0",'\ + ' "pkg_name": $$p, "pkg_version": $$v,'\ + ' "scorecard_type": "cli"}' \ + >'$(prefix).pkg.json' + .PHONY: vt-metadata vt-metadata: @mkdir -p '$(VT_OUT_DIR)' From 47c132a3e9241187489d2aeb68eaba487bbf4a65 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 14/41] add target to generate all output --- rules.mk | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rules.mk b/rules.mk index 1ea0a2bc..10e00148 100644 --- a/rules.mk +++ b/rules.mk @@ -25,6 +25,14 @@ ifeq ($(strip $(VT_TEST_RUNNERS)),) $(error "VT_TEST_RUNNERS must point to space-delimited list of test scripts") endif +.PHONY: vt-all +vt-all: vt-copymat +vt-all: vt-cover +vt-all: vt-scores +vt-all: vt-pkg +vt-all: vt-metadata +vt-all: vt-archive + .PHONY: vt-bin vt-bin: rm -rf '$(VT_BIN_DIR)' && mkdir '$(VT_BIN_DIR)' From 323f616490d7680f2aff37a0cf494c0a7f367f90 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 15/41] add help target --- rules.mk | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/rules.mk b/rules.mk index 10e00148..ad480b3f 100644 --- a/rules.mk +++ b/rules.mk @@ -25,6 +25,29 @@ ifeq ($(strip $(VT_TEST_RUNNERS)),) $(error "VT_TEST_RUNNERS must point to space-delimited list of test scripts") endif +.PHONY: vt-help +vt-help: + $(info Primary targets:) + $(info * vt-all: create all validation artifacts under $(VT_OUT_DIR)/) + $(info * vt-gen-docs: generate command docs under $(VT_DOC_DIR)/) + $(info ) + $(info Other targets:) + $(info * vt-test: invoke each script listed in VT_TEST_RUNNERS) + $(info ) + $(info Other targets, triggered by vt-all:) + $(info * vt-archive: write source archive to $(prefix).tar.gz) + $(info * vt-bin: install executables for packages under current directory to $(VT_BIN_DIR)/) + $(info * vt-checkmat: check $(VT_MATRIX) with checkmat) + $(info * vt-copymat: copy $(VT_MATRIX) to $(prefix).matrix.yaml) + $(info * vt-cover: invoke each script listed in VT_TEST_RUNNERS with coverage enabled) + $(info * vt-metadata: write scorecard metadata to $(prefix).metadata.json) + $(info * vt-pkg: write package name and version to $(prefix).pkg.json) + $(info * vt-scores: write scorecard scores to $(prefix).scores.json) + @: + +.PHONY: help-valtools +help-valtools: vt-help + .PHONY: vt-all vt-all: vt-copymat vt-all: vt-cover From e18fb393e5a0f5705dbf006b4b899d6b69a8a8d5 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 16/41] add readme --- README.md | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..6dfa5171 --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ + +Tools for validating Go executables +=================================== + + +Overview +-------- + +As part of validating a release of a package, MetrumRG produces two +artifacts, the source code archive and a scorecard report. For non-R +packages, the [mpn.scorecard][ms] R package takes care of *rendering* +the scorecard report. + +[ms]: https://github.com/metrumresearchgroup/mpn.scorecard + +This repository includes Go commands, shell scripts, and Makefile +rules that Go modules can use to generate the source code archive and +the inputs for mpn.scorecard. Its current focus is on validating +functionality exposed via **command-line executables**. + + +Key components +-------------- + + * [rules.mk][]: a Makefile that defines a `vt-all` target that + generates the source code archive and mpn.scorecard inputs + + * [checkmat/][]: Go package that defines an executable for running + various checks on a traceability matrix YAML file + + * [filecov/][]: Go package that defines an executable for calculating + *file*-level coverage from a Go coverage profile + + * [fmttests/][]: Go package that defines an executable for converting + `go test -json` into a custom format meant for inclusion in the + scorecard + + * [scripts/][]: various shell scripts invoked by targets in + [rules.mk][] + +[rules.mk]: ./rules.mk +[checkmat/]: ./checkmat +[filecov/]: ./filecov +[fmttests/]: ./fmttests +[scripts/]: ./scripts/ + + +Setup instructions +------------------ + +High-level summary of the main steps (details in subsections below): + + 1. add this repository as a subtree in the main repository + + 2. add documentation file for each command + + 3. create a traceability matrix YAML + + 4. add scripts for running the tests + + 5. include this subtree's `rules.mk` into the top-level Makefile + +### 1. Add subtree + +This repository is designed to be incorporated as a subtree in the +main project. The suggested location is `internal/valtools/`. + +You can use [git subtree][gs] to manage the subtree. + +[gs]: https://manpages.debian.org/stable/git-man/git-subtree.1.en.html + +Run `go mod tidy` to update `go.mod` with new dependencies, if any, +brought in by the subtree. + +### 2. Command documentation + +Consider a Go module that defines one executable, `foo`, where the +user-facing functionality is exposed via two subcommands, `foo bar` +and `foo baz`. In order to define the traceability matrix, there must +be a directory that contains a documentation file for each of these +commands, replacing any spaces in the name with underscores. + + docs/commands/ + |-- foo.md + |-- foo_bar.md + `-- foo_baz.md + +`docs/commands/` is taken as the directory for the command +documentation unless the `VT_DOC_DIR` variable specifies another +locatino (see [step 5](#step5)). + +The Makefile rules expect the main repository to define a `docgen` +package (suggested location: `internal/tools/docgen`) whose executable +generates the documentation. + +The `docgen` executable should accept one argument, the directory in +which to write the documentation files. The executable is responsible +for ensuring that no stale documentation remains in the directory +(e.g., by removing the directory before writing new files). + +If a module uses `cobra`, `docgen` can likely be defined as a light +wrapper around the `cobra/doc.GenMarkdownTree` function. + +### 3. Define traceability matrix + +Create a traceability matrix YAML file that maps each command to the +code file that defines it, the file that documents it, and the main +files that test it. By default, the Makefile rules look for the +matrix YAML file at `docs/validation/matrix.yaml`. To change this +location, set the `VT_MATRIX` variable (see [step 5](#step5)). + +The file should consist of a sequence of entries with following items: + + * `entrypoint`: the name of the command, as invoked by the user + + * `code`: the path to where the command is defined + + * `doc`: path to the command's main documentation + + * `tests`: list of paths where the command is tested + +Example entry: + + - entrypoint: foo bar + code: cmd/bar.go + doc: docs/commands/foo_bar.md + tests: + - cmd/bar_test.go + - integration/bar_test.go + +The `checkmat` tool will flag any documentation file that does not map +to a command with an entry in the YAML file. In some cases, you may +not want to include a command in the rendered matrix. For example, +the top-level `foo` command may serve only as an entry point for +subcommands, making it "uninteresting" to include in the matrix. For +such cases, you can add a skip entry to the matrix. + + - command: foo + skip: true + +### 4. Add test runners + +The main repository must define one or more scripts for running its +tests. Specify the paths to these scripts via the `VT_TEST_RUNNERS` +variable (see [step 5](#step5)). + +A test script must + + * write `go test` JSON records to standard output when passed the + `-json` argument. It must not write anything else to standard + output in this case. + + * check whether the `GOCOVERDIR` environment variable is set and, if + so, instrument the tests to write coverage data files under it. + +How to handle the `GOCOVERDIR` environment variable is determined by +whether tests are integration tests that use a built executable or +unit tests. + + * integration tests: when `GOCOVERDIR` is set, the script should pass + `-cover` to the `go build` call to build the instrumented + executable(s) to test + + * unit tests: when `GOCOVERDIR` is set, `go test` should be + instructed to write coverage data files to `GOCOVERDIR`. This can + be done by adding `-args -test.gocoverdir="$GOCOVERDIR"` to the end + of the call. + + When tallying coverage, Go does not by default count a statement as + covered if it's only executed via another packge's unit tests. To + change that, list all the module's packages of interest by + specifying `-coverpkg` in the `go test` call. + +Notes: + + * Go 1.20 [introduced support][newcov] for `GOCOVERDIR` and + instrumenting executables. + + * There's a [proposed patch][covarg] to expose `test.gocoverdir` as + top-level argument to `go test`, although it's currently on hold. + +[newcov]: https://go.dev/blog/integration-test-coverage +[covarg]: https://go-review.googlesource.com/c/go/+/456595/14 + + + + +### 5. Wire up Makefile + +To wire up the subtree to the main repository, include the subtree's +`rule.mk` file in the repository's top-level Makefile. Before the +`include` directive, you can specify any Makefile [variables](#vars), +but, at a minimum, you should set `VT_TEST_RUNNERS`. + + VT_TEST_RUNNERS = scripts/run-unit-tests + VT_TEST_RUNNERS += scripts/run-integration-tests + include internal/valtools/rules.mk + + +Running the pipeline +-------------------- + +The `vt-all` target provides the main entry point. It generates the +source archive and mpn.scorecard inputs. + + make vt-all + +The generated files are written under the directory specified by the +variable `VT_OUT_DIR`. By default, this points to +`{subtree}/output/{package}_{version}`. + + + +### Makefile variables + + * `VT_BIN_DIR`: where to install executables (default: + `{subtree}/bin`) + + * `VT_DOC_DIR`: tell `docgen` executable to generate documentation + files under this directory (default: `docs/commands`) + + * `VT_MATRIX`: path to matrix file (default: + `docs/validation/matrix.yaml`) + + * `VT_OUT_DIR`: where to generate the results (default: + `{subtree}/output/{package}_{version}`) + + * `VT_PKG`: name of the package (default: the base name of the + top-level directory). + + * `VT_TEST_ALLOW_SKIPS`: whether to allow skips when running the + `VT_TEST_RUNNERS` scripts (default: `no`) + + * `VT_TEST_RUNNERS`: a space-delimited list of scripts to invoke to + run the test suite + +### Auxillary targets + +In addition to `vt-all`, the following targets can be useful to run +directly: + + * `vt-gen-docs`: invoke the `docgen` exectuable to refresh the + documentation in `VT_DOC_DIR` + + * `vt-test`: invoke each script in `VT_TEST_RUNNERS` *without* + coverage enabled + +Run `vt-help` target to see a more complete list of targets. From bdee43c1864f35d225b338ba4f24745082d7d0a9 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 09:32:22 -0400 Subject: [PATCH 17/41] vt-gen-docs: support disabling doc generation --- README.md | 13 ++++++++++--- rules.mk | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6dfa5171..cbab4dbf 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,9 @@ commands, replacing any spaces in the name with underscores. documentation unless the `VT_DOC_DIR` variable specifies another locatino (see [step 5](#step5)). -The Makefile rules expect the main repository to define a `docgen` -package (suggested location: `internal/tools/docgen`) whose executable -generates the documentation. +By default, the Makefile rules expect the main repository to define a +`docgen` package (suggested location: `internal/tools/docgen`) whose +executable generates the documentation. The `docgen` executable should accept one argument, the directory in which to write the documentation files. The executable is responsible @@ -101,6 +101,10 @@ for ensuring that no stale documentation remains in the directory If a module uses `cobra`, `docgen` can likely be defined as a light wrapper around the `cobra/doc.GenMarkdownTree` function. +If the documentation files are maintained as the primary source +(i.e. the files do not need to be generated), set the `VT_DOC_DO_GEN` +variable to `no` (see [step 5](#step5)). + ### 3. Define traceability matrix Create a traceability matrix YAML file that maps each command to the @@ -219,6 +223,9 @@ variable `VT_OUT_DIR`. By default, this points to * `VT_DOC_DIR`: tell `docgen` executable to generate documentation files under this directory (default: `docs/commands`) + * `VT_DOC_DO_GEN`: whether to run `docgen` executable to generate + documentation files under `VT_DOC_DIR` (default: `yes`) + * `VT_MATRIX`: path to matrix file (default: `docs/validation/matrix.yaml`) diff --git a/rules.mk b/rules.mk index ad480b3f..bc95722f 100644 --- a/rules.mk +++ b/rules.mk @@ -17,6 +17,7 @@ prefix := $(VT_OUT_DIR)/$(name) VT_BIN_DIR ?= $(vtdir)/bin VT_DOC_DIR ?= docs/commands +VT_DOC_DO_GEN ?= yes VT_MATRIX ?= docs/validation/matrix.yaml VT_TEST_ALLOW_SKIPS ?= no @@ -63,10 +64,14 @@ vt-bin: .PHONY: vt-gen-docs vt-gen-docs: +ifeq ($(VT_DOC_DO_GEN),yes) $(MAKE) vt-bin @test -f '$(VT_BIN_DIR)/docgen' || \ { printf '"make vt-bin" did not generate $(VT_BIN_DIR)/docgen\n'; exit 1; } '$(VT_BIN_DIR)/docgen' '$(VT_DOC_DIR)' +else + @: +endif $(VT_BIN_DIR)/checkmat: $(vtdir)/checkmat/main.go From 6dabac5fd3f1a8c64baf04e56535701e00202eb3 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 12:43:16 -0400 Subject: [PATCH 18/41] fmttests: write output for failed tests to stderr Under 'go test -json', details about the test failures are in the action=output records for a test. Relay that output to hopefully give the test runner a better idea of why a test is failing. --- fmttests/fmttests_test.go | 130 +++++++++++++++++++++++++++++++++++--- fmttests/main.go | 28 ++++++-- 2 files changed, 144 insertions(+), 14 deletions(-) diff --git a/fmttests/fmttests_test.go b/fmttests/fmttests_test.go index cea88634..55f1710a 100644 --- a/fmttests/fmttests_test.go +++ b/fmttests/fmttests_test.go @@ -16,6 +16,7 @@ type testEvent struct { Action string Package string Test string + Output string } func makeEventReader(t *testing.T, tes []testEvent) io.Reader { @@ -28,6 +29,7 @@ func makeEventReader(t *testing.T, tes []testEvent) io.Reader { Action: te.Action, Package: te.Package, Test: te.Test, + Output: te.Output, } bs, err := json.Marshal(&e) if err != nil { @@ -211,8 +213,10 @@ func TestProcessEvents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - s, err := processEvents(makeEventReader(t, tt.events), tt.subtests, &buf) + var bufStdout bytes.Buffer + var bufStderr bytes.Buffer + s, err := processEvents(makeEventReader(t, tt.events), + tt.subtests, &bufStdout, &bufStderr) if err != nil { t.Fatal(err) } @@ -227,15 +231,123 @@ func TestProcessEvents(t *testing.T) { t.Errorf("skipped: want %d, got %d", tt.nskipped, len(s.Skipped)) } - out := buf.String() - outWant := strings.Join(tt.lines, "\n") + "\n" - if out != outWant { - t.Errorf("stdout:\n want %q,\n got %q", outWant, out) + stdout := bufStdout.String() + stdoutWant := strings.Join(tt.lines, "\n") + "\n" + if stdout != stdoutWant { + t.Errorf("stdout:\n want %q,\n got %q", stdoutWant, stdout) + } + + stderr := bufStderr.String() + if stderr != "" { + t.Errorf("expected empty stderr, got %q", stderr) } }) } } +func TestProcessEventsFailureOutput(t *testing.T) { + events := []testEvent{ + { + Action: "output", + Package: "example.com/o/foo", + Test: "TestFoo1", + Output: "foo1 output\n", + }, + { + Action: "pass", + Package: "example.com/o/foo", + Test: "TestFoo1", + }, + { + Action: "output", + Package: "example.com/o/foo", + Test: "TestFoo2", + Output: "TestFoo2 failed\n", + }, + { + Action: "output", + Package: "example.com/o/foo", + Test: "TestFoo2", + Output: "error was ...\n", + }, + { + Action: "fail", + Package: "example.com/o/foo", + Test: "TestFoo2", + }, + { + Action: "pass", + Package: "example.com/o/k/bar", + Test: "TestBar", + }, + { + Action: "output", + Package: "example.com/o/k/baz", + Test: "TestBaz/1", + Output: "TestBaz/1 failed\n", + }, + { + Action: "pass", + Package: "example.com/o/k/baz", + Test: "TestBaz/2", + }, + { + Action: "fail", + Package: "example.com/o/k/baz", + Test: "TestBaz/1", + }, + { + Action: "fail", + Package: "example.com/o/k/baz", + Test: "TestBaz", + }, + } + var bufStdout bytes.Buffer + var bufStderr bytes.Buffer + s, err := processEvents(makeEventReader(t, events), + false, &bufStdout, &bufStderr) + if err != nil { + t.Fatal(err) + } + + nfailedWant := 3 + if len(s.Failed) != nfailedWant { + t.Errorf("failed: want %d, got %d", nfailedWant, len(s.Failed)) + } + + npassedWant := 3 + if len(s.Passed) != npassedWant { + t.Errorf("passed: want %d, got %d", npassedWant, len(s.Passed)) + } + + nskippedWant := 0 + if len(s.Skipped) != nskippedWant { + t.Errorf("skipped: want %d, got %d", nskippedWant, len(s.Skipped)) + } + + stdout := bufStdout.String() + stdoutWant := strings.Join([]string{ + "[foo] TestFoo1: passed", + "[foo] TestFoo2: failed", + "[bar] TestBar: passed", + "[baz] TestBaz/1: failed", + "[baz] TestBaz: failed", + }, "\n") + "\n" + if stdout != stdoutWant { + t.Errorf("stdout:\n want %q,\n got %q", stdoutWant, stdout) + } + + stderr := bufStderr.String() + stderrWant := strings.Join([]string{ + "TestFoo2 failed", + "error was ...", + "TestBaz/1 failed", + }, "\n") + "\n" + if stderr != stderrWant { + t.Errorf("stderr: want %q, got %q", stderrWant, stderr) + } +} + func TestProcessEventsErrors(t *testing.T) { var tests = []struct { name string @@ -280,8 +392,10 @@ func TestProcessEventsErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - _, err := processEvents(makeEventReader(t, tt.events), false, &buf) + var bufStdout bytes.Buffer + var bufStderr bytes.Buffer + _, err := processEvents(makeEventReader(t, tt.events), + false, &bufStdout, &bufStderr) if err == nil { t.Errorf("processEvents unexpectedly passed") } diff --git a/fmttests/main.go b/fmttests/main.go index a702b921..72f9d9b9 100644 --- a/fmttests/main.go +++ b/fmttests/main.go @@ -62,16 +62,24 @@ func packageBaseName(s string) string { return xs[n-1] } +type key struct { + Package string + Test string +} + // processEvents reads a JSON record of a test event from r. For each result // record encountered (i.e. a record with an action of "pass", "fail", or -// "skip"), it writes a formatted line to w. The subtests argument controls -// whether results for subtests are written. +// "skip"), it writes a formatted line to wout. If any failures are +// encountered, it writes the failing test's output lines to werr. +// +// The subtests argument controls whether results for subtests are written. // // processEvents returns a summary instance that records the test names for each // result record encountered. -func processEvents(r io.Reader, subtests bool, w io.Writer) (summary, error) { +func processEvents(r io.Reader, subtests bool, wout io.Writer, werr io.Writer) (summary, error) { var res summary + failLines := make(map[key][]string) scanner := bufio.NewScanner(r) for scanner.Scan() { var e event @@ -87,6 +95,10 @@ func processEvents(r io.Reader, subtests bool, w io.Writer) (summary, error) { var alwaysShow bool switch e.Action { case "fail": + k := key{e.Package, e.Test} + for _, line := range failLines[k] { + fmt.Fprint(werr, line) + } res.Failed = append(res.Failed, e.Test) status = "failed" alwaysShow = true @@ -99,7 +111,11 @@ func processEvents(r io.Reader, subtests bool, w io.Writer) (summary, error) { alwaysShow = true case "bench": return res, errors.New("bench output is not supported") - case "cont", "output", "pause", "run", "start": + case "output": + k := key{e.Package, e.Test} + failLines[k] = append(failLines[k], e.Output) + continue + case "cont", "pause", "run", "start": continue default: return res, fmt.Errorf("unknown action: %s", e.Action) @@ -109,7 +125,7 @@ func processEvents(r io.Reader, subtests bool, w io.Writer) (summary, error) { // TODO: Consider other approaches for formatting the package name // that avoid collisions (e.g., packageBaseName returns "cmd" for // both ".../foo/cmd" and ".../bar/cmd"). - fmt.Fprintf(w, "[%s] %s: %s\n", + fmt.Fprintf(wout, "[%s] %s: %s\n", packageBaseName(e.Package), e.Test, status) } } @@ -133,7 +149,7 @@ func main() { os.Exit(2) } - res, err := processEvents(os.Stdin, *subtests, os.Stdout) + res, err := processEvents(os.Stdin, *subtests, os.Stdout, os.Stderr) if err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(2) From 9601bac37de8980e8e2845be09675e587a054dd6 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 31 Jul 2024 12:47:01 -0400 Subject: [PATCH 19/41] add target to check which Go files missing from coverage JSON The scripts for VT_TEST_RUNNERS need to implement fairly complex handling. When GOCOVERDIR is set, they need to instrument a binary for integration tests and to pass '-args -test.gocoverdir=$d' for unit tests. They need to do custom package selection, separating unit tests from integration tests and removing this valtools subtree. And for unit tests, they need to pass -coverpkgs in order for the tests of one package to count toward the coverage of another package. On top of that, the files of packages without tests are not included in the coverage profiles for unit tests invoked with -test.gocoverdir. Take the following module: |-- bar | `-- bar.go |-- foo | |-- foo.go | `-- foo_test.go `-- go.mod If 'go test -coverprofile coverage.out ./...' is called from the top level, the profile will include entries for bar.go. However, if 'go test -cover ./... -args -test.gocoverdir=$d' followed by 'go tool covdata textfmt -i $d -o coverage.out' is called to create the text format profile, entries for bar.go are _not_ present [*]. Given the above complexity, it's helpful to have a way to verify that the Go files in the final coverage JSON match what's expected. [*] tested under Go 1.22.5, which includes d1cb5c0605 (cmd/go: improve handling of no-test packages for coverage, 2023-05-09) --- README.md | 8 ++++++++ rules.mk | 7 +++++++ scripts/cover-unlisted | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100755 scripts/cover-unlisted diff --git a/README.md b/README.md index cbab4dbf..8d17be38 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,14 @@ directly: * `vt-gen-docs`: invoke the `docgen` exectuable to refresh the documentation in `VT_DOC_DIR` + * `vt-cover-unlisted`: display a diff of two file sets: 1) non-test + Go files in the repository that define at least one function and 2) + files included in the coverage JSON under `VT_OUT_DIR` + + This can help identify files that are unexpectedly missing coverage + scores. In this case, you may need to adjust the package selection + or the `-coverpkg` value in one of the test runners scripts. + * `vt-test`: invoke each script in `VT_TEST_RUNNERS` *without* coverage enabled diff --git a/rules.mk b/rules.mk index bc95722f..657fa527 100644 --- a/rules.mk +++ b/rules.mk @@ -33,6 +33,7 @@ vt-help: $(info * vt-gen-docs: generate command docs under $(VT_DOC_DIR)/) $(info ) $(info Other targets:) + $(info * vt-cover-unlisted: show Go files that are not in coverage JSON) $(info * vt-test: invoke each script listed in VT_TEST_RUNNERS) $(info ) $(info Other targets, triggered by vt-all:) @@ -116,6 +117,12 @@ vt-cover: $(VT_BIN_DIR)/fmttests '$(VT_BIN_DIR)/filecov' -mod go.mod '$(cov_prof)' \ >'$(prefix).coverage.json' +.PHONY: vt-cover-unlisted +vt-cover-unlisted: + @test -f '$(prefix).coverage.json' || \ + { printf >&2 'vt-cover-unlisted requires $(prefix).coverage.json\n'; exit 1; } + @'$(vtdir)/scripts/cover-unlisted' '$(prefix).coverage.json' || : + .PHONY: vt-scores vt-scores: @mkdir -p '$(VT_OUT_DIR)' diff --git a/scripts/cover-unlisted b/scripts/cover-unlisted new file mode 100755 index 00000000..3ba837cd --- /dev/null +++ b/scripts/cover-unlisted @@ -0,0 +1,24 @@ +#!/bin/sh +# Copyright 2024 Metrum Research Group +# SPDX-License-Identifier: MIT + +set -eu + +test $# = 1 || { + printf >&2 'usage: %s \n' "$0" + exit 1 +} + +tdir=$(mktemp -d "${TMPDIR:-/tmp}"/valtools-XXXXX) +trap 'rm -rf "$tdir"' 0 + +jq -r '.files | .[] | .file' <"$1" | sort >"$tdir"/files-in-coverage + +# Limit to files with a function definition because Go, by design, +# does not consider those. See Go's d1cb5c0605 (cmd/go: improve +# handling of no-test packages for coverage, 2023-05-09). +git grep -l --full-name '^func ' ':(top)*.go' | \ + grep -Ev '_test.go$' | \ + sort >"$tdir"/files-in-tree + +git diff --no-index "$tdir"/files-in-tree "$tdir"/files-in-coverage From 8db79bc2ef7d2a943e8c4c80442b38ab5ecf83eb Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 5 Aug 2024 21:43:42 -0400 Subject: [PATCH 20/41] readme: add missing word --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d17be38..58cebb0d 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,8 @@ files that test it. By default, the Makefile rules look for the matrix YAML file at `docs/validation/matrix.yaml`. To change this location, set the `VT_MATRIX` variable (see [step 5](#step5)). -The file should consist of a sequence of entries with following items: +The file should consist of a sequence of entries with the following +items: * `entrypoint`: the name of the command, as invoked by the user From 01105785a00f5723d7f054fc0a9f1364e3e75b78 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Wed, 7 Aug 2024 09:05:33 -0400 Subject: [PATCH 21/41] readme: fix stale example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58cebb0d..2bed8038 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ the top-level `foo` command may serve only as an entry point for subcommands, making it "uninteresting" to include in the matrix. For such cases, you can add a skip entry to the matrix. - - command: foo + - entrypoint: foo skip: true ### 4. Add test runners From c03f4fcfad5c2fa77fa1c377ebd02545b92be0f3 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 22/41] rcmd/configure_test.go: prevent tempdir check from skipping Two of the tests lead to a checkIsTempDir() call that skips unless on macOS. This is just a small detail of the test (and arguably not even worth checking), so it doesn't make sense to mark the entire test as skipped on other systems. --- rcmd/configure_test.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/rcmd/configure_test.go b/rcmd/configure_test.go index deb9125f..f93b1b9e 100644 --- a/rcmd/configure_test.go +++ b/rcmd/configure_test.go @@ -48,18 +48,10 @@ func checkEnvVarsValid(t *testing.T, testCase configureArgsTestCase, actualResul } func checkIsTempDir(t *testing.T, tmpDir string) { - switch runtime.GOOS { - case "darwin": + if runtime.GOOS == "darwin" { assert.True(t, strings.Contains(tmpDir, "var/folders"), "R_LIBS_USER not set to temp directory: Dir found: %s", tmpDir) - break - case "linux": - t.Skip("tmp dir check not implemented for linux") - break - case "windows": - t.Skip("tmp dir check not implemented for linux") - break - default: - t.Skip("tmp dir check not implemented for detected os") + } else { + t.Log("checkIsTempDir: skipping because not on darwin system") } } From bc756c0b7504e88826d3109171e61048061e7434 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 23/41] integration_tests/baseline: delete root_test.go This is already covered by integration_tests/version/version_test.go. --- integration_tests/baseline/root_test.go | 26 ------------------------- 1 file changed, 26 deletions(-) delete mode 100644 integration_tests/baseline/root_test.go diff --git a/integration_tests/baseline/root_test.go b/integration_tests/baseline/root_test.go deleted file mode 100644 index 2729a3ed..00000000 --- a/integration_tests/baseline/root_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package baseline - -import ( - "testing" - - "github.com/metrumresearchgroup/command" - "github.com/stretchr/testify/assert" - - "github.com/metrumresearchgroup/pkgr/cmd" - . "github.com/metrumresearchgroup/pkgr/testhelper" -) - -const ( - baselineRootE2ETest1 = "BSLNRT-E2E-001" -) - -func TestRoot(t *testing.T) { - t.Run(MakeTestName(baselineRootE2ETest1, "-v flag prints version"), func(t *testing.T) { - rootCmd := command.New("pkgr", "-v") - capture, err := rootCmd.CombinedOutput() - if err != nil { - t.Fatalf("error occurred running pkgr -v: %s", err) - } - assert.Equal(t, cmd.VERSION, string(capture)) - }) -} From 08a7e4b8fc128f46bc77bb366fee07b2ccf61a5d Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 24/41] integration_tests/mixed-source: drop skip for inactive test There's an issue tracking this, but there's probably not much value in reporting it as a skip. --- integration_tests/mixed-source/mixed_source_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/mixed-source/mixed_source_test.go b/integration_tests/mixed-source/mixed_source_test.go index f69998c2..83e7cc15 100644 --- a/integration_tests/mixed-source/mixed_source_test.go +++ b/integration_tests/mixed-source/mixed_source_test.go @@ -124,7 +124,7 @@ func TestMixedSource(t *testing.T) { g.Assert(t, goldenInstall, testCapture) }) }) - t.Skip(MakeTestName(mixedSourceE2ETest2, "repo and package customizations synchronize when compatible. SKIPPING till issue #329 fixes this bug.")) + // See https://github.com/metrumresearchgroup/pkgr/issues/329 /*t.Run(MakeTestName(mixedSourceE2ETest2, "repo and package customizations synchronize when compatible"), func(t *testing.T) { DeleteTestFolder(t, "test-library") DeleteTestFolder(t, "test-cache") From 774923c1e6cd1d1e5e50195059d0b81099d2079c Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 25/41] integration_tests/library: fix skip message typo --- integration_tests/library/libraries_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/library/libraries_test.go b/integration_tests/library/libraries_test.go index 03de2dcb..333589d8 100644 --- a/integration_tests/library/libraries_test.go +++ b/integration_tests/library/libraries_test.go @@ -94,7 +94,7 @@ func TestLibraryRenv(t *testing.T) { t.Run(MakeTestName(librariesE2ETest5, "lockfile type renv errors when renv isn't available"), func(t *testing.T) { if renv != "" { - t.Skip("Skipping: empty PKGR_TESTS_SYS_RENV indicates renv is available") + t.Skip("Skipping: non-empty PKGR_TESTS_SYS_RENV indicates renv is available") } DeleteTestFolder(t, "test-cache") From 8e5658a177e8ef406ea8d36976dcdf7c448b69d8 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 26/41] integration_tests/library: shorten a subtest name If renv is available (as it is on Metworx), we skip the TestLibraryRenv subtest that checks an error when renv is unavailable. This is the one subtest name that makes it through to the scorecard because it's skipped. Reword the subtest to make it a bit shorter and remove the test ID. (In general, we should go through the test suite in remove the test IDs, which are no longer relevant under the current validation approach, but this was the only test ID that made it to the scorecard.) --- integration_tests/library/libraries_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration_tests/library/libraries_test.go b/integration_tests/library/libraries_test.go index 333589d8..6a55c107 100644 --- a/integration_tests/library/libraries_test.go +++ b/integration_tests/library/libraries_test.go @@ -16,7 +16,6 @@ const ( librariesE2ETest2 = "LIB-E2E-002" librariesE2ETest3 = "LIB-E2E-003" librariesE2ETest4 = "LIB-E2E-004" - librariesE2ETest5 = "LIB-E2E-005" librariesE2ETest6 = "LIB-E2E-006" librariesE2ETest7 = "LIB-E2E-007" ) @@ -92,7 +91,7 @@ func TestLibraryRenv(t *testing.T) { } }) - t.Run(MakeTestName(librariesE2ETest5, "lockfile type renv errors when renv isn't available"), func(t *testing.T) { + t.Run("lockfile type renv errors renv if is unavailable", func(t *testing.T) { if renv != "" { t.Skip("Skipping: non-empty PKGR_TESTS_SYS_RENV indicates renv is available") } From ae5394d06499a2570c5fcf1828ec57d6c65d40d3 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 27/41] integration_tests/version: loosen output check The version can by overridden at build time TestVersion shouldn't assume any particular output. Reduce this to a smoke tests that checks for a non-empty output. --- integration_tests/version/version_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration_tests/version/version_test.go b/integration_tests/version/version_test.go index d1b67d5c..0d4f0fe7 100644 --- a/integration_tests/version/version_test.go +++ b/integration_tests/version/version_test.go @@ -1,6 +1,7 @@ package version_test import ( + "bytes" "testing" "github.com/metrumresearchgroup/command" @@ -20,7 +21,7 @@ func TestVersion(t *testing.T) { t.Fatal(err) } // this will always get the git tag for regular releases - assert.Equal(t, "dev", string(res)) + assert.True(t, len(bytes.TrimSpace(res)) != 0) }) t.Run("short flag -v works", func(t *testing.T) { @@ -30,7 +31,7 @@ func TestVersion(t *testing.T) { t.Fatal(err) } // this will always get the git tag for regular releases - assert.Equal(t, "dev", string(res)) + assert.True(t, len(bytes.TrimSpace(res)) != 0) }) } From 5b140a7cfcc8cd56d23d17c607b21c164116d267 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 28/41] cmd/pkgr: delete commented code for generating docs Generating the docs will be handled by a dedicated executable. --- cmd/pkgr/pkgr.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/pkgr/pkgr.go b/cmd/pkgr/pkgr.go index 9f12908a..380bcf1e 100644 --- a/cmd/pkgr/pkgr.go +++ b/cmd/pkgr/pkgr.go @@ -9,12 +9,6 @@ import ( "github.com/metrumresearchgroup/pkgr/cmd" ) -// if want to generate docs -// import "github.com/spf13/cobra/doc" -// err := doc.GenMarkdownTree(cmd.RootCmd, "../../docs/bbi") -// if err != nil { -// panic(err) -// } func main() { setGOMAXPROCS() cmd.Execute() From 292c31ba0ff89d27bf2f1730bead13b335f9adbb Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:54:41 -0400 Subject: [PATCH 29/41] cmd: hide debug and experiment subcommands The debug and experiment subcommands are internal debugging tools. Mark them as hidden so that they aren't listed as available commands and aren't skipped by cobra.GenMarkdownTree (which will be used in an upcoming commit to write a directory of documentation files). --- cmd/debug.go | 3 ++- cmd/experiment.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/debug.go b/cmd/debug.go index 1ccbf83d..5cb34886 100644 --- a/cmd/debug.go +++ b/cmd/debug.go @@ -28,7 +28,8 @@ var debugCmd = &cobra.Command{ Long: ` debug internal settings `, - RunE: rDebug, + RunE: rDebug, + Hidden: true, } func rDebug(cmd *cobra.Command, args []string) error { diff --git a/cmd/experiment.go b/cmd/experiment.go index b830756a..56a6a7c9 100644 --- a/cmd/experiment.go +++ b/cmd/experiment.go @@ -31,7 +31,8 @@ var experimentCmd = &cobra.Command{ Long: ` JUST FOR EXPERIMENTATION `, - RunE: rExperiment, + RunE: rExperiment, + Hidden: true, } func rExperiment(cmd *cobra.Command, args []string) error { From f69fdf0e41d11e12892f4e49429640f47963ea0d Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Tue, 13 Aug 2024 10:29:23 -0400 Subject: [PATCH 30/41] treewide: absorb some assignments into if statement --- checkmat/checkmat_test.go | 21 +++++++++------------ fmttests/fmttests_test.go | 15 +++++---------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/checkmat/checkmat_test.go b/checkmat/checkmat_test.go index 3e5045f9..37139f51 100644 --- a/checkmat/checkmat_test.go +++ b/checkmat/checkmat_test.go @@ -99,8 +99,8 @@ func TestCheckValidFileNamesGood(t *testing.T) { if bad != 0 { t.Errorf("expected no invalid file names, got %d", bad) } - out := buf.String() - if out != "" { + + if out := buf.String(); out != "" { t.Errorf("expected empty output, got %q", out) } } @@ -233,8 +233,7 @@ func TestCheckMissingFilesGood(t *testing.T) { t.Errorf("expected no missing files, got %d", bad) } - out := buf.String() - if out != "" { + if out := buf.String(); out != "" { t.Errorf("expected empty output, got %q", out) } } @@ -324,8 +323,8 @@ func TestCheckEntrypointDocMismatchGood(t *testing.T) { if bad != 0 { t.Errorf("expected no mismatches, got %d", bad) } - out := buf.String() - if out != "" { + + if out := buf.String(); out != "" { t.Errorf("expected empty output, got %q", out) } } @@ -352,8 +351,7 @@ func TestCheckDupEntrypointsBad(t *testing.T) { t.Fatal(err) } - wantBad := 1 - if bad != wantBad { + if wantBad := 1; bad != wantBad { t.Errorf("expected %d duplicated entry, got %d", wantBad, bad) } out := buf.String() @@ -382,8 +380,8 @@ func TestCheckDupEntrypointsGood(t *testing.T) { if bad != 0 { t.Errorf("expected no duplicated entries, got %d", bad) } - out := buf.String() - if out != "" { + + if out := buf.String(); out != "" { t.Errorf("expected empty output, got %q", out) } } @@ -476,8 +474,7 @@ func TestCheckMissingEntriesGood(t *testing.T) { t.Errorf("expected no missing entries, got %d", bad) } - out := buf.String() - if out != "" { + if out := buf.String(); out != "" { t.Errorf("expected empty output, got %q", out) } } diff --git a/fmttests/fmttests_test.go b/fmttests/fmttests_test.go index 55f1710a..ed6fa87a 100644 --- a/fmttests/fmttests_test.go +++ b/fmttests/fmttests_test.go @@ -310,22 +310,18 @@ func TestProcessEventsFailureOutput(t *testing.T) { t.Fatal(err) } - nfailedWant := 3 - if len(s.Failed) != nfailedWant { + if nfailedWant := 3; len(s.Failed) != nfailedWant { t.Errorf("failed: want %d, got %d", nfailedWant, len(s.Failed)) } - npassedWant := 3 - if len(s.Passed) != npassedWant { + if npassedWant := 3; len(s.Passed) != npassedWant { t.Errorf("passed: want %d, got %d", npassedWant, len(s.Passed)) } - nskippedWant := 0 - if len(s.Skipped) != nskippedWant { + if nskippedWant := 0; len(s.Skipped) != nskippedWant { t.Errorf("skipped: want %d, got %d", nskippedWant, len(s.Skipped)) } - stdout := bufStdout.String() stdoutWant := strings.Join([]string{ "[foo] TestFoo1: passed", "[foo] TestFoo2: failed", @@ -333,17 +329,16 @@ func TestProcessEventsFailureOutput(t *testing.T) { "[baz] TestBaz/1: failed", "[baz] TestBaz: failed", }, "\n") + "\n" - if stdout != stdoutWant { + if stdout := bufStdout.String(); stdout != stdoutWant { t.Errorf("stdout:\n want %q,\n got %q", stdoutWant, stdout) } - stderr := bufStderr.String() stderrWant := strings.Join([]string{ "TestFoo2 failed", "error was ...", "TestBaz/1 failed", }, "\n") + "\n" - if stderr != stderrWant { + if stderr := bufStderr.String(); stderr != stderrWant { t.Errorf("stderr: want %q, got %q", stderrWant, stderr) } } From 230f9dc519819460de2d65a2dd90fa0b9abe8d5c Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Tue, 13 Aug 2024 10:29:23 -0400 Subject: [PATCH 31/41] filecov: rename variable to avoid shadowing Flagged by golangci-lint: shadow: declaration of "localpath" shadows declaration at line 98 (govet) --- filecov/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/filecov/main.go b/filecov/main.go index ff465882..77f449b0 100644 --- a/filecov/main.go +++ b/filecov/main.go @@ -105,11 +105,11 @@ func shortenFileNames(cov coverage, modpath string, localpath string) error { var lprefix string if localpath != "" { - localpath, err := filepath.EvalSymlinks(localpath) + lpath, err := filepath.EvalSymlinks(localpath) if err != nil { return err } - lprefix = localpath + string(filepath.Separator) + lprefix = lpath + string(filepath.Separator) } for _, entry := range cov.Files { From 928bcb287d793653b89ceadc75ee6343770fd7e1 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Tue, 13 Aug 2024 10:29:23 -0400 Subject: [PATCH 32/41] treewide: clean up whitespace to silence golangci-lint --- checkmat/checkmat_test.go | 1 - filecov/filecov_test.go | 2 +- fmttests/fmttests_test.go | 1 + fmttests/main.go | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/checkmat/checkmat_test.go b/checkmat/checkmat_test.go index 37139f51..77799548 100644 --- a/checkmat/checkmat_test.go +++ b/checkmat/checkmat_test.go @@ -606,5 +606,4 @@ func TestCheckAll(t *testing.T) { assertCode(t, out, "04", 1) assertCode(t, out, "05", 1) }) - } diff --git a/filecov/filecov_test.go b/filecov/filecov_test.go index 106561e0..8e06e389 100644 --- a/filecov/filecov_test.go +++ b/filecov/filecov_test.go @@ -36,6 +36,7 @@ func assertFileCoverage(t *testing.T, got []*fileCoverage, want []*fileCoverage) nwant := len(want) if ngot != nwant { t.Errorf("expected coverage for %d files, got %d", nwant, ngot) + return } @@ -54,7 +55,6 @@ func assertFileCoverage(t *testing.T, got []*fileCoverage, want []*fileCoverage) } else { t.Error("no coverage measurement for", fc.File) } - } } diff --git a/fmttests/fmttests_test.go b/fmttests/fmttests_test.go index ed6fa87a..fd2cd57f 100644 --- a/fmttests/fmttests_test.go +++ b/fmttests/fmttests_test.go @@ -38,6 +38,7 @@ func makeEventReader(t *testing.T, tes []testEvent) io.Reader { buf.Write(bs) buf.Write([]byte("\n")) } + return bytes.NewReader(buf.Bytes()) } diff --git a/fmttests/main.go b/fmttests/main.go index 72f9d9b9..274fde93 100644 --- a/fmttests/main.go +++ b/fmttests/main.go @@ -114,6 +114,7 @@ func processEvents(r io.Reader, subtests bool, wout io.Writer, werr io.Writer) ( case "output": k := key{e.Package, e.Test} failLines[k] = append(failLines[k], e.Output) + continue case "cont", "pause", "run", "start": continue From 24f85d62ba4aa267484f93cc91103b0995106940 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 33/41] go.mod: update for internal/valtools addition This is the result of $ go get -u golang.org/x/tools $ go mod tidy The x/tools update is required to get a version that defines cover.ParseProfilesFromReader. --- go.mod | 2 ++ go.sum | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 59f993da..4e1c5aaa 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,8 @@ require ( github.com/thoas/go-funk v0.8.0 github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca go.uber.org/automaxprocs v1.4.0 + golang.org/x/mod v0.20.0 + golang.org/x/tools v0.24.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 pault.ag/go/debian v0.0.0-20180722221659-90aeb542bd40 diff --git a/go.sum b/go.sum index 5172b162..2ef43389 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -304,6 +305,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -325,8 +327,13 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -361,8 +368,14 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -402,6 +415,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -425,6 +445,13 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -469,17 +496,42 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -533,12 +585,16 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= From 72305a616db19a3e40d3366139213955ad4f4ff7 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 34/41] internal/tools: define executable for generating command docs --- go.sum | 3 +++ internal/tools/docgen/main.go | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 internal/tools/docgen/main.go diff --git a/go.sum b/go.sum index 2ef43389..ffd5bc5c 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,7 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -250,6 +251,7 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= @@ -261,6 +263,7 @@ github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/internal/tools/docgen/main.go b/internal/tools/docgen/main.go new file mode 100644 index 00000000..874e1d5f --- /dev/null +++ b/internal/tools/docgen/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + + "github.com/metrumresearchgroup/pkgr/cmd" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func gen(cmd *cobra.Command, dir string) error { + if err := os.RemoveAll(dir); err != nil { + return err + } + + if err := os.MkdirAll(dir, 0777); err != nil { + return err + } + + return doc.GenMarkdownTree(cmd, dir) +} + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0]) + os.Exit(2) + } + + outdir := os.Args[1] + + root := cmd.RootCmd + root.Long = "" // Disable Synopsis section with version. + + root.DisableAutoGenTag = true + for _, c := range root.Commands() { + // Reduce update noise. + c.DisableAutoGenTag = true + } + + if err := gen(root, outdir); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} From 0fdf19c07976c29b076f2ce5caec039d11523fca Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 35/41] docs: generate command docs This is the result of 'make vt-gen-docs'. --- docs/commands/pkgr.md | 34 +++++++++++++++++++++ docs/commands/pkgr_add.md | 42 ++++++++++++++++++++++++++ docs/commands/pkgr_clean.md | 42 ++++++++++++++++++++++++++ docs/commands/pkgr_clean_cache.md | 48 ++++++++++++++++++++++++++++++ docs/commands/pkgr_clean_pkgdbs.md | 43 ++++++++++++++++++++++++++ docs/commands/pkgr_inspect.md | 46 ++++++++++++++++++++++++++++ docs/commands/pkgr_install.md | 41 +++++++++++++++++++++++++ docs/commands/pkgr_load.md | 42 ++++++++++++++++++++++++++ docs/commands/pkgr_plan.md | 42 ++++++++++++++++++++++++++ docs/commands/pkgr_remove.md | 41 +++++++++++++++++++++++++ docs/commands/pkgr_run.md | 42 ++++++++++++++++++++++++++ 11 files changed, 463 insertions(+) create mode 100644 docs/commands/pkgr.md create mode 100644 docs/commands/pkgr_add.md create mode 100644 docs/commands/pkgr_clean.md create mode 100644 docs/commands/pkgr_clean_cache.md create mode 100644 docs/commands/pkgr_clean_pkgdbs.md create mode 100644 docs/commands/pkgr_inspect.md create mode 100644 docs/commands/pkgr_install.md create mode 100644 docs/commands/pkgr_load.md create mode 100644 docs/commands/pkgr_plan.md create mode 100644 docs/commands/pkgr_remove.md create mode 100644 docs/commands/pkgr_run.md diff --git a/docs/commands/pkgr.md b/docs/commands/pkgr.md new file mode 100644 index 00000000..d82f1d3e --- /dev/null +++ b/docs/commands/pkgr.md @@ -0,0 +1,34 @@ +## pkgr + +package manager + +### Options + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + -h, --help help for pkgr + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages + -v, --version print the version +``` + +### SEE ALSO + +* [pkgr add](pkgr_add.md) - add one or more packages +* [pkgr clean](pkgr_clean.md) - clean up cached information +* [pkgr inspect](pkgr_inspect.md) - inspect a full installation +* [pkgr install](pkgr_install.md) - install a package +* [pkgr load](pkgr_load.md) - Checks that installed packages can be loaded +* [pkgr plan](pkgr_plan.md) - plan a full installation +* [pkgr remove](pkgr_remove.md) - remove one or more packages +* [pkgr run](pkgr_run.md) - Run R with the configuration settings used with other R commands + diff --git a/docs/commands/pkgr_add.md b/docs/commands/pkgr_add.md new file mode 100644 index 00000000..83b1905b --- /dev/null +++ b/docs/commands/pkgr_add.md @@ -0,0 +1,42 @@ +## pkgr add + +add one or more packages + +### Synopsis + + + add package/s to the configuration file and optionally install + + +``` +pkgr add [package name1] [package name2] [package name3] ... [flags] +``` + +### Options + +``` + -h, --help help for add + --install install package/s after adding +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_clean.md b/docs/commands/pkgr_clean.md new file mode 100644 index 00000000..6726caac --- /dev/null +++ b/docs/commands/pkgr_clean.md @@ -0,0 +1,42 @@ +## pkgr clean + +clean up cached information + +### Synopsis + +clean up cached source files and binaries, as well as the saved package database. + +``` +pkgr clean [flags] +``` + +### Options + +``` + --all clean all cached items + -h, --help help for clean +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager +* [pkgr clean cache](pkgr_clean_cache.md) - Subcommand to clean cached source and binary files. +* [pkgr clean pkgdbs](pkgr_clean_pkgdbs.md) - Subcommand to clean cached pkgdbs + diff --git a/docs/commands/pkgr_clean_cache.md b/docs/commands/pkgr_clean_cache.md new file mode 100644 index 00000000..4e22dbef --- /dev/null +++ b/docs/commands/pkgr_clean_cache.md @@ -0,0 +1,48 @@ +## pkgr clean cache + +Subcommand to clean cached source and binary files. + +### Synopsis + +This command is a subcommand of the "clean" command. + + Using this command deletes cached source and binary files. Use the + --src and --binary options to specify which repos to clean each + file type from. + + + +``` +pkgr clean cache [flags] +``` + +### Options + +``` + --binaries-only Clean only binary files from the cache + -h, --help help for cache + --repos string Comma separated list of repositories to be cleaned. Defaults to all. (default "ALL") + --src-only Clean only src files from the cache +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr clean](pkgr_clean.md) - clean up cached information + diff --git a/docs/commands/pkgr_clean_pkgdbs.md b/docs/commands/pkgr_clean_pkgdbs.md new file mode 100644 index 00000000..98fb80c5 --- /dev/null +++ b/docs/commands/pkgr_clean_pkgdbs.md @@ -0,0 +1,43 @@ +## pkgr clean pkgdbs + +Subcommand to clean cached pkgdbs + +### Synopsis + +This command parses the currently-cached pkgdbs and removes all + of them by default, or specific ones if desired. Identify specific repos using the "repos" argument, i.e. + pkgr clean pkgdbs --repos="CRAN,r_validated" + Repo names should match names in the pkgr.yml file. + +``` +pkgr clean pkgdbs [flags] +``` + +### Options + +``` + -h, --help help for pkgdbs + --repos string Set the repos you wish to clear the pkgdbs for. (default "ALL") +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr clean](pkgr_clean.md) - clean up cached information + diff --git a/docs/commands/pkgr_inspect.md b/docs/commands/pkgr_inspect.md new file mode 100644 index 00000000..4e67db54 --- /dev/null +++ b/docs/commands/pkgr_inspect.md @@ -0,0 +1,46 @@ +## pkgr inspect + +inspect a full installation + +### Synopsis + + + see the inspect for an install + + +``` +pkgr inspect [flags] +``` + +### Options + +``` + --deps show dependency tree + -h, --help help for inspect + --installed-from show package installation source + --json output as clean json + --reverse show reverse dependencies + --tree show full recursive dependency tree +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_install.md b/docs/commands/pkgr_install.md new file mode 100644 index 00000000..6ea3e110 --- /dev/null +++ b/docs/commands/pkgr_install.md @@ -0,0 +1,41 @@ +## pkgr install + +install a package + +### Synopsis + + + install a package + + +``` +pkgr install [flags] +``` + +### Options + +``` + -h, --help help for install +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_load.md b/docs/commands/pkgr_load.md new file mode 100644 index 00000000..ff9cc160 --- /dev/null +++ b/docs/commands/pkgr_load.md @@ -0,0 +1,42 @@ +## pkgr load + +Checks that installed packages can be loaded + +### Synopsis + +Attempts to load user packages specified in pkgr.yml to validate that each package has been installed +successfully and can be used. Use the --all flag to load all packages in the user-library dependency tree instead of just user-level packages. + +``` +pkgr load [flags] +``` + +### Options + +``` + --all load user packages as well as their dependencies + -h, --help help for load + --json output a JSON object of package info at the end +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_plan.md b/docs/commands/pkgr_plan.md new file mode 100644 index 00000000..7ee71e11 --- /dev/null +++ b/docs/commands/pkgr_plan.md @@ -0,0 +1,42 @@ +## pkgr plan + +plan a full installation + +### Synopsis + + + see the plan for an install + + +``` +pkgr plan [flags] +``` + +### Options + +``` + -h, --help help for plan + --show-deps show the (required) dependencies for each package +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_remove.md b/docs/commands/pkgr_remove.md new file mode 100644 index 00000000..591ec73a --- /dev/null +++ b/docs/commands/pkgr_remove.md @@ -0,0 +1,41 @@ +## pkgr remove + +remove one or more packages + +### Synopsis + + + remove package/s from the configuration file + + +``` +pkgr remove [package name1] [package name2] [package name3] ... [flags] +``` + +### Options + +``` + -h, --help help for remove +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + diff --git a/docs/commands/pkgr_run.md b/docs/commands/pkgr_run.md new file mode 100644 index 00000000..cdd87a4b --- /dev/null +++ b/docs/commands/pkgr_run.md @@ -0,0 +1,42 @@ +## pkgr run + +Run R with the configuration settings used with other R commands + +### Synopsis + + + allows for interactive use and debugging based on the configuration specified by pkgr + + +``` +pkgr run R [flags] +``` + +### Options + +``` + -h, --help help for run + --pkg string package environment to set +``` + +### Options inherited from parent commands + +``` + --config string config file (default is pkgr.yml) + --debug use debug mode + --library string library to install packages + --logjson log as json + --loglevel string level for logging + --no-rollback Disable rollback + --no-secure disable TLS certificate verification + --no-update don't update installed packages + --preview preview action, but don't actually run command + --strict Enable strict mode + --threads int number of threads to execute with + --update whether to update installed packages +``` + +### SEE ALSO + +* [pkgr](pkgr.md) - package manager + From 5949e87d1f6f47560dbe3cf3f5e61c7d73540eca Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 36/41] add script for running unit tests --- scripts/run-unit-tests | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100755 scripts/run-unit-tests diff --git a/scripts/run-unit-tests b/scripts/run-unit-tests new file mode 100755 index 00000000..98569580 --- /dev/null +++ b/scripts/run-unit-tests @@ -0,0 +1,40 @@ +#!/bin/sh + +set -eu + +ls_pkgs () { + go list ./... | + grep -vE '/internal/valtools/[a-z]+$' | + grep -vE '/internal/tools/docgen$' | + grep -vF '/integration_tests/' | + tr '\n' ' ' +} + +pkgs=$(ls_pkgs) +pkgs=${pkgs% *} +cpkgs=$(printf '%s' "$pkgs" | tr ' ' ',') + +tdir=$(mktemp -d "${TMPDIR:-/tmp}"/pkgr-test-XXXXX) +trap 'rm -rf "$tdir"' 0 +# Prevent tests from touching user's real cache. +export XDG_CACHE_HOME="$tdir" + +run () { + go test -p 1 -count 1 "$@" +} + +# Unset R_LIBS_{USER,SITE} to prevent the warning from +# rcmd.configureEnv tripping up TestLoad's check for expected output. +unset R_LIBS_USER +unset R_LIBS_SITE +export PKGR_TESTS_SYS_RENV="${PKGR_TESTS_SYS_RENV-1}" +if test -n "${GOCOVERDIR-}" +then + printf >&2 'testing with -cover\n' + # shellcheck disable=SC2086 + run -cover -coverpkg="$cpkgs" "$@" $pkgs \ + -args -test.gocoverdir="$GOCOVERDIR" +else + # shellcheck disable=SC2086 + run "$@" $pkgs +fi From 2357def24c83f5878037c2b22526cc9cbf357f02 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 37/41] add script for running integration tests --- scripts/run-integration-tests | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100755 scripts/run-integration-tests diff --git a/scripts/run-integration-tests b/scripts/run-integration-tests new file mode 100755 index 00000000..37c0f19c --- /dev/null +++ b/scripts/run-integration-tests @@ -0,0 +1,70 @@ +#!/bin/sh + +set -eu + +tdir=$(mktemp -d "${TMPDIR:-/tmp}"/pkgr-test-XXXXX) +trap 'rm -rf "$tdir"' 0 + +bin=$tdir/bin +mkdir "$bin" + +cache=$tdir/cache +mkdir "$cache" +# Prevent integration_tests/baseline/cache_test.go from touching +# user's real cache. +export XDG_CACHE_HOME="$cache" + +if test -n "${GOCOVERDIR-}" +then + printf >&2 'building binary with -cover\n' + coverarg=-cover +else + coverarg= +fi + +version=$(git describe --always --dirty) + +# shellcheck disable=SC2086 +go build $coverarg \ + -ldflags "-X github.com/metrumresearchgroup/pkgr/cmd.VERSION=$version" \ + -o "$bin/pkgr" cmd/pkgr/pkgr.go + +export PATH="$bin:$PATH" +# Unset R_LIBS_{USER,SITE} to prevent the warning from +# rcmd.configureEnv tripping up TestLoad's check for expected output. +unset R_LIBS_USER +unset R_LIBS_SITE +export PKGR_TESTS_SYS_RENV="${PKGR_TESTS_SYS_RENV-1}" + +printf >&2 'pkgr path: %s\npkgr version: %s\n' \ + "$(command -v pkgr)" "$(pkgr --version)" + +git clean -xfq \ + 'integration_tests/**/test-cache' 'integration_tests/**/test-library' + +test_dir=$PWD/integration_tests +subdirs=' + bad-customization + baseline + env-vars + library + load + mixed-source + multi-repo + outdated-pkgs + recommended + rollback + tarball-install + version +' + +status=0 +for d in $subdirs +do + cd "$test_dir/$d" + go test -count 1 "$@" ./... || { + status=$? + printf >&2 '%s: failed (exit status: %d)\n' "$d" "$status" + } +done +exit "$status" From 947524e1f697bd586b56579ee1cbfc333e137d96 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 38/41] make: wire up internal/valtools --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index ddadb21d..c87cd4b4 100644 --- a/Makefile +++ b/Makefile @@ -71,3 +71,8 @@ outdated-test-reset: outdated-test: install outdated-test-reset cd ${TEST_HOME}/outdated-pkgs; pkgr plan cd ${TEST_HOME}/outdated-pkgs; pkgr install + +VT_TEST_ALLOW_SKIPS = yes +VT_TEST_RUNNERS = scripts/run-unit-tests +VT_TEST_RUNNERS += scripts/run-integration-tests +include internal/valtools/rules.mk From edf5d91ab22c41f7961f0d8e99cef3dea37bab9f Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 13:55:25 -0400 Subject: [PATCH 39/41] docs: define a traceability matrix This includes all the test files listed in the previous validation docs except for integration_tests/baseline/root_test.go. That tested the --version option (which is covered by integration_tests/version). If we want to list the test in the traceability matrix, we could include it under the top-level pkgr entry (which would seem a bit odd given that's the entry point to everything) or introduce a version subcommand. But leave it be for now. --- docs/validation/matrix.yaml | 80 +++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/validation/matrix.yaml diff --git a/docs/validation/matrix.yaml b/docs/validation/matrix.yaml new file mode 100644 index 00000000..c554d217 --- /dev/null +++ b/docs/validation/matrix.yaml @@ -0,0 +1,80 @@ +- entrypoint: pkgr + skip: true + +- entrypoint: pkgr add + code: cmd/add.go + doc: docs/commands/pkgr_add.md + tests: + - configlib/add_package_test.go + +- entrypoint: pkgr clean + code: cmd/clean.go + doc: docs/commands/pkgr_clean.md + tests: + - cmd/cleanCache_test.go + - integration_tests/baseline/cache_test.go + +- entrypoint: pkgr clean cache + code: cmd/cleanCache.go + doc: docs/commands/pkgr_clean_cache.md + tests: + - cmd/cleanCache_test.go + - integration_tests/baseline/cache_test.go + +- entrypoint: pkgr clean pkgdbs + code: cmd/cleanPkgdb.go + doc: docs/commands/pkgr_clean_pkgdbs.md + tests: + - integration_tests/baseline/cache_test.go + +- entrypoint: pkgr inspect + code: cmd/inspect.go + doc: docs/commands/pkgr_inspect.md + tests: + - integration_tests/baseline/inspect_test.go + +- entrypoint: pkgr install + code: cmd/install.go + doc: docs/commands/pkgr_install.md + tests: + - cmd/install_test.go + - integration_tests/bad-customization/bad_customization_test.go + - integration_tests/baseline/cache_test.go + - integration_tests/baseline/install_test.go + - integration_tests/env-vars/rpath_env_test.go + - integration_tests/library/libraries_test.go + - integration_tests/mixed-source/mixed_source_test.go + - integration_tests/multi-repo/multi_repo_test.go + - integration_tests/outdated-pkgs/outdated_packages_test.go + - integration_tests/rollback/rollback_test.go + - integration_tests/tarball-install/tarball_install_test.go + +- entrypoint: pkgr load + code: cmd/load.go + doc: docs/commands/pkgr_load.md + tests: + - integration_tests/load/load_test.go + +- entrypoint: pkgr plan + code: cmd/plan.go + doc: docs/commands/pkgr_plan.md + tests: + - cmd/plan_test.go + - integration_tests/baseline/plan_test.go + - integration_tests/env-vars/rpath_env_test.go + - integration_tests/mixed-source/mixed_source_test.go + - integration_tests/multi-repo/multi_repo_test.go + - integration_tests/outdated-pkgs/outdated_packages_test.go + - integration_tests/recommended/recommended_test.go + - integration_tests/tarball-install/tarball_install_test.go + +- entrypoint: pkgr remove + code: cmd/remove.go + doc: docs/commands/pkgr_remove.md + tests: + - configlib/config_test.go + +- entrypoint: pkgr run + code: cmd/run.go + doc: docs/commands/pkgr_run.md + tests: [] From 35eb4df68dbcfaff4c45cda3d3a3275e67d1d405 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Mon, 12 Aug 2024 14:26:11 -0400 Subject: [PATCH 40/41] ci: switch to GitHub Actions With the go.mod update in this series, building pkgr on Drone is now failing with several error messages that say //go:build comment without // +build comment Based on a quick search, it seems like a bug with Go 1.16 (see golang's issue 51436). That lines up with the Go version we pull in to build the binary and the Go version bundled with the r-dev-ci images. We were planning to switch to GitHub Actions soon, but, with the above Drone error, it makes sense to do with this series. The Drone build primarily tested on r-dev-ci:4.1.0. The exceptions were * r-dev-ci:4.0.5 for the mixed-source tests * r-dev-ci-mpn-4.1:2022-02-11 and r-dev-ci-mpn-4.1:cran-latest for the renv library checks For the first point, using 4.0.5 is no longer necessary now that TestMixedSource generates its own repo. Under GitHub Actions, handle the 'renv vs no renv' by setting up two different jobs, one with a system-wide renv installation and one without. Unlike Drone, this leaves out testing an renv version before 0.15 [*], which should be okay given the renv 0.15 release is now over two years old. Note that the library for renv is set up via Rprofile.site rather than communicated through an environment variable (e.g., R_LIBS_USER or R_LIBS_SITE) because getRenvLibrary uses RunRscriptWithArgs, which intentionally discards these. (getRenvLibrary should probably be adjusted to avoid that behavior.) For the OS, hold off on adding a job for the latest Ubuntu because that triggers several 'pkgr install' tests to fail due to a fansi build error. [*] See 0a4a8ee (ci: test configlib and integration_tests/library with system renv, 2022-03-08) and f5c120d (config: invoke renv to discover library path, 2022-01-21) for why testing an renv version before 0.15 was of interest. --- .drone.yml | 296 ------------------------------------ .github/workflows/main.yaml | 93 +++++++++++ 2 files changed, 93 insertions(+), 296 deletions(-) delete mode 100644 .drone.yml create mode 100644 .github/workflows/main.yaml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index a613f801..00000000 --- a/.drone.yml +++ /dev/null @@ -1,296 +0,0 @@ ---- -kind: pipeline -type: docker -name: tests - -global-variables: - environment: &default_environment - GOPATH: "/go" - GOCACHE: "/go/.cache/go-build" - GOENV: "/go/.config/go/env" - GOMODCACHE: "/go/pkg/mod" -steps: - - name: pull - image: omerxx/drone-ecr-auth - commands: - - $(aws ecr get-login --no-include-email --region us-east-1) - - docker pull 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - - docker pull 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.0.5 - - docker pull 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:2022-02-11 - - docker pull 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:cran-latest - volumes: - - name: docker.sock - path: /var/run/docker.sock - - name: build - image: golang:1.16 - environment: - <<: *default_environment - commands: - - make install - - go get ./... - volumes: - - name: go - path: /go - - name: configlib, no system renv - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - cd configlib - - go test -v . - - name: configlib, system renv (< 0.15) - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:2022-02-11 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PKGR_TESTS_SYS_RENV=1 - - cd configlib - - go test -v . - - name: configlib, system renv (>= 0.15) - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:cran-latest - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PKGR_TESTS_SYS_RENV=1 - - cd configlib - - go test -v . - - name: gpsr - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - cd gpsr - - go test -v . - - name: cran - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - cd cran - - go test -v . - - name: baseline - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/baseline - - make test - - name: version - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/version - - make test - - name: rollback - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/rollback - - make test - - name: outdated - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/outdated-pkgs - - make test - - name: load - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/load - - make test - - name: multi-repo - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/multi-repo - - make test - - name: bad-customization - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/bad-customization - - make test - - name: recommended-packages - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/recommended - - make test - - name: rpath-env-var - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/env-vars - - make test - - name: tarball-install - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/tarball-install - - make test - - name: libraries - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.1.0 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/library - - make test - - name: libraries, system renv (< 0.15) - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:2022-02-11 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - export PKGR_TESTS_SYS_RENV=1 - - cd integration_tests/library - - make test - - name: libraries, system renv (>= 0.15) - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci-mpn-4.1:cran-latest - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - export PKGR_TESTS_SYS_RENV=1 - - cd integration_tests/library - - make test - - name: mixed-source-and-customizations - image: 906087756158.dkr.ecr.us-east-1.amazonaws.com/r-dev-ci:4.0.5 - pull: never - volumes: - - name: go - path: /go - environment: - <<: *default_environment - commands: - - export PATH=/go/bin:$PATH - - cd integration_tests/mixed-source - - make test -volumes: - - name: docker.sock - host: - path: /var/run/docker.sock - - name: go - temp: { } - ---- -kind: pipeline -type: docker -name: goreleaser - -platform: - os: linux - arch: amd64 - -steps: - - name: goreleaser - image: goreleaser/goreleaser - commands: - - git config --global user.email "drone@metrumrg.com" - - git config --global user.name "Drony" - - git fetch --tags - - cd cmd/pkgr - - goreleaser --rm-dist - environment: - GITHUB_TOKEN: - from_secret: GITHUB_TOKEN - VERSION: ${DRONE_TAG} - -trigger: - event: - - tag - -depends_on: - - tests diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 00000000..d18c0605 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,93 @@ +name: CI +on: + push: + branches: + - main + - 'scratch/**' + tags: + - 'v*' + pull_request: + +jobs: + check: + runs-on: ${{ matrix.config.os }} + name: ${{ matrix.config.os }} / go ${{ matrix.config.go }} / R ${{ matrix.config.r }} ${{ matrix.config.renv && 'renv' || '' }} + strategy: + fail-fast: false + matrix: + config: + - os: ubuntu-20.04 + go: 1.21.x + r: 4.2.3 + renv: false + - os: ubuntu-20.04 + go: stable + r: 4.3.1 + renv: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.config.go }} + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + - name: Install other system dependencies + shell: bash + run: | + sudo DEBIAN_FRONTEND=noninteractive \ + apt-get install -y libcurl4-openssl-dev + - name: Disable R_LIBS_USER + shell: bash + run: echo 'R_LIBS_USER=:' >>"$GITHUB_ENV" + - name: Adjust Rprofile.site + if: matrix.config.renv + shell: sudo Rscript {0} + run: | + dir.create("/opt/rpkgs") + cat( + '\n\n.libPaths("/opt/rpkgs")\n', + sep = "", + file = file.path(R.home("etc"), "Rprofile.site"), + append = TRUE + ) + - name: Install renv system-wide + if: matrix.config.renv + shell: sudo Rscript {0} + run: install.packages("renv", repos = "https://cran.rstudio.com") + - name: Build + shell: bash + run: go get -t ./... && go build ./... + - name: Unit tests + shell: bash + run: ./scripts/run-unit-tests + env: + PKGR_TESTS_SYS_RENV: ${{ matrix.config.renv && '1' || '' }} + - name: Integration tests + shell: bash + run: ./scripts/run-integration-tests + env: + PKGR_TESTS_SYS_RENV: ${{ matrix.config.renv && '1' || '' }} + release: + if: github.ref_type == 'tag' + name: Make release + needs: check + runs-on: ubuntu-20.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + workdir: cmd/pkgr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d48666086e86e9c2363e12ef70f997df3c2a8e94 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Tue, 13 Aug 2024 16:50:57 -0400 Subject: [PATCH 41/41] readme: fix typos Thanks to Seth for spotting. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2bed8038..b8f0405f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ commands, replacing any spaces in the name with underscores. `docs/commands/` is taken as the directory for the command documentation unless the `VT_DOC_DIR` variable specifies another -locatino (see [step 5](#step5)). +location (see [step 5](#step5)). By default, the Makefile rules expect the main repository to define a `docgen` package (suggested location: `internal/tools/docgen`) whose @@ -172,7 +172,7 @@ unit tests. of the call. When tallying coverage, Go does not by default count a statement as - covered if it's only executed via another packge's unit tests. To + covered if it's only executed via another package's unit tests. To change that, list all the module's packages of interest by specifying `-coverpkg` in the `go test` call. @@ -193,7 +193,7 @@ Notes: ### 5. Wire up Makefile To wire up the subtree to the main repository, include the subtree's -`rule.mk` file in the repository's top-level Makefile. Before the +`rules.mk` file in the repository's top-level Makefile. Before the `include` directive, you can specify any Makefile [variables](#vars), but, at a minimum, you should set `VT_TEST_RUNNERS`. @@ -242,12 +242,12 @@ variable `VT_OUT_DIR`. By default, this points to * `VT_TEST_RUNNERS`: a space-delimited list of scripts to invoke to run the test suite -### Auxillary targets +### Auxiliary targets In addition to `vt-all`, the following targets can be useful to run directly: - * `vt-gen-docs`: invoke the `docgen` exectuable to refresh the + * `vt-gen-docs`: invoke the `docgen` executable to refresh the documentation in `VT_DOC_DIR` * `vt-cover-unlisted`: display a diff of two file sets: 1) non-test