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 }} 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 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 { 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() 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 + 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: [] 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..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= @@ -134,6 +135,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= @@ -249,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= @@ -260,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= @@ -304,6 +308,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 +330,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 +371,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 +418,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 +448,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 +499,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 +588,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= 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)) - }) -} diff --git a/integration_tests/library/libraries_test.go b/integration_tests/library/libraries_test.go index 03de2dcb..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,9 +91,9 @@ 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: 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") 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") 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) }) } 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) + } +} diff --git a/internal/valtools/.gitignore b/internal/valtools/.gitignore new file mode 100644 index 00000000..d8f7cdcc --- /dev/null +++ b/internal/valtools/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/output/ diff --git a/internal/valtools/LICENSE b/internal/valtools/LICENSE new file mode 100644 index 00000000..0363628a --- /dev/null +++ b/internal/valtools/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. diff --git a/internal/valtools/README.md b/internal/valtools/README.md new file mode 100644 index 00000000..b8f0405f --- /dev/null +++ b/internal/valtools/README.md @@ -0,0 +1,264 @@ + +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 +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 +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. + +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 +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 the 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. + + - entrypoint: 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 package'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 +`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`. + + 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_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`) + + * `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 + +### Auxiliary targets + +In addition to `vt-all`, the following targets can be useful to run +directly: + + * `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 + 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 + +Run `vt-help` target to see a more complete list of targets. diff --git a/internal/valtools/checkmat/checkmat_test.go b/internal/valtools/checkmat/checkmat_test.go new file mode 100644 index 00000000..77799548 --- /dev/null +++ b/internal/valtools/checkmat/checkmat_test.go @@ -0,0 +1,609 @@ +// 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) + } + + if out := buf.String(); 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) + } + + if out := buf.String(); 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) + } + + if out := buf.String(); 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) + } + + if wantBad := 1; 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) + } + + if out := buf.String(); 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) + } + + if out := buf.String(); 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/internal/valtools/checkmat/main.go b/internal/valtools/checkmat/main.go new file mode 100644 index 00000000..f0dd98fa --- /dev/null +++ b/internal/valtools/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/internal/valtools/filecov/filecov_test.go b/internal/valtools/filecov/filecov_test.go new file mode 100644 index 00000000..8e06e389 --- /dev/null +++ b/internal/valtools/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/internal/valtools/filecov/main.go b/internal/valtools/filecov/main.go new file mode 100644 index 00000000..77f449b0 --- /dev/null +++ b/internal/valtools/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 != "" { + lpath, err := filepath.EvalSymlinks(localpath) + if err != nil { + return err + } + lprefix = lpath + 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/internal/valtools/fmttests/fmttests_test.go b/internal/valtools/fmttests/fmttests_test.go new file mode 100644 index 00000000..fd2cd57f --- /dev/null +++ b/internal/valtools/fmttests/fmttests_test.go @@ -0,0 +1,400 @@ +// 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 + Output 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, + Output: te.Output, + } + 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 bufStdout bytes.Buffer + var bufStderr bytes.Buffer + s, err := processEvents(makeEventReader(t, tt.events), + tt.subtests, &bufStdout, &bufStderr) + 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)) + } + + 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) + } + + if nfailedWant := 3; len(s.Failed) != nfailedWant { + t.Errorf("failed: want %d, got %d", nfailedWant, len(s.Failed)) + } + + if npassedWant := 3; len(s.Passed) != npassedWant { + t.Errorf("passed: want %d, got %d", npassedWant, len(s.Passed)) + } + + if nskippedWant := 0; len(s.Skipped) != nskippedWant { + t.Errorf("skipped: want %d, got %d", nskippedWant, len(s.Skipped)) + } + + stdoutWant := strings.Join([]string{ + "[foo] TestFoo1: passed", + "[foo] TestFoo2: failed", + "[bar] TestBar: passed", + "[baz] TestBaz/1: failed", + "[baz] TestBaz: failed", + }, "\n") + "\n" + if stdout := bufStdout.String(); stdout != stdoutWant { + t.Errorf("stdout:\n want %q,\n got %q", stdoutWant, stdout) + } + + stderrWant := strings.Join([]string{ + "TestFoo2 failed", + "error was ...", + "TestBaz/1 failed", + }, "\n") + "\n" + if stderr := bufStderr.String(); stderr != stderrWant { + t.Errorf("stderr: want %q, got %q", stderrWant, stderr) + } +} + +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 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/internal/valtools/fmttests/main.go b/internal/valtools/fmttests/main.go new file mode 100644 index 00000000..274fde93 --- /dev/null +++ b/internal/valtools/fmttests/main.go @@ -0,0 +1,164 @@ +// 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] +} + +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 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, 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 + + 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": + 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 + 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 "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) + } + + 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(wout, "[%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, os.Stderr) + 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/internal/valtools/rules.mk b/internal/valtools/rules.mk new file mode 100644 index 00000000..657fa527 --- /dev/null +++ b/internal/valtools/rules.mk @@ -0,0 +1,155 @@ +vtdir := $(dir $(lastword $(MAKEFILE_LIST))) +vtdir := $(patsubst %/,%,$(vtdir)) +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_DOC_DO_GEN ?= yes +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-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-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:) + $(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 +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)' + go build -o '$(VT_BIN_DIR)/' ./... + +.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 + +.PHONY: vt-checkmat +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)/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) + +$(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-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)' + '$(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)' + '$(vtdir)/scripts/metadata' >'$(prefix).metadata.json' + +.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 '$@' '$<' diff --git a/internal/valtools/scripts/cover-unlisted b/internal/valtools/scripts/cover-unlisted new file mode 100755 index 00000000..3ba837cd --- /dev/null +++ b/internal/valtools/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 diff --git a/internal/valtools/scripts/metadata b/internal/valtools/scripts/metadata new file mode 100755 index 00000000..92d1f6c5 --- /dev/null +++ b/internal/valtools/scripts/metadata @@ -0,0 +1,48 @@ +#!/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, with the addition of +# the Go version. + +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'} + +go_ver=$(go env GOVERSION) +go_ver=${go_ver#go} + +jq -n \ + --arg d "$dt" \ + --arg u "$user" \ + --arg s "$sysname" \ + --arg v "$version" \ + --arg r "$release" \ + --arg m "$machine" \ + --arg V "$mver" \ + --arg g "$go_ver" \ + '{"date": $d, + "executor": $u, + "info": { + "env_vars": {"METWORX_VERSION": $V}, + "sys": { + "sysname": $s, + "version": $v, + "release": $r, + "machine": $m, + "Go version": $g + } + } + }' diff --git a/internal/valtools/scripts/run-tests b/internal/valtools/scripts/run-tests new file mode 100755 index 00000000..5733f740 --- /dev/null +++ b/internal/valtools/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" diff --git a/internal/valtools/scripts/write-scores b/internal/valtools/scripts/write-scores new file mode 100755 index 00000000..396080dc --- /dev/null +++ b/internal/valtools/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 + } + }' 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") } } 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" 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