From e106109af13f40a156e6b61c2a9f4fdc90168905 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Tue, 14 Nov 2023 19:04:10 +0000 Subject: [PATCH] test: add code coverage support With golang 1.20.x, we can build a binary with coverage support. " Cover Go 1.20 supports collecting code coverage profiles for programs (applications and integration tests), as opposed to just unit tests. To collect coverage data for a program, build it with go build's -cover flag, then run the resulting binary with the environment variable GOCOVERDIR set to an output directory for coverage profiles. See the 'coverage for integration tests' landing page for more on how to get started. For details on the design and implementation, see the proposal. " Signed-off-by: Ramkumar Chinchani --- .github/workflows/ci.yaml | 4 ++ .github/workflows/coverage.yaml | 105 ++++++++++++++++++++++++++++++++ Makefile | 39 ++++++++++-- build.yaml | 6 +- pkg/stacker/build.go | 15 +++++ pkg/stacker/cover.go | 17 ++++++ 6 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/coverage.yaml create mode 100644 pkg/stacker/cover.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 41d4c415..4dee7251 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,3 +13,7 @@ jobs: uses: ./.github/workflows/build.yaml with: slow-test: false + coverage: + uses: ./.github/workflows/coverage.yaml + with: + slow-test: false diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 00000000..c801eb2d --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,105 @@ +name: Reusable stacker build for coverage +on: + workflow_call: + inputs: + # note >-, args needs to be strings to be used as inputs + # for the reusable build.yaml workflow + go-version: + required: false + type: string + description: 'Stringified JSON object listing go versions' + default: >- + ["1.20.x"] + privilege-level: + required: false + type: string + description: 'Stringified JSON object listing stacker privilege-level' + default: >- + ["unpriv", "priv"] + build-id: + required: false + type: string + description: 'build-id' + default: "${{ github.sha }}" + slow-test: + required: false + type: boolean + description: 'Should slow tests be run?' + default: true + +jobs: + build: + runs-on: ubuntu-22.04 + services: + registry: + image: ghcr.io/project-stacker/registry:2 + ports: + - 5000:5000 + strategy: + matrix: + go-version: ${{fromJson(inputs.go-version)}} + privilege-level: ${{fromJson(inputs.privilege-level)}} + name: "golang ${{ matrix.go-version }} privilege ${{ matrix.privilege-level }}" + steps: + - uses: actions/checkout@v3 + - name: Clean disk space + uses: ./.github/actions/clean-runner + - uses: benjlevesque/short-sha@v2.1 + id: short-sha + - name: Set up golang ${{ matrix.go-version }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + - name: Setup Environment + run: | + gopath=$PWD/.build/gopath + echo "GOPATH=$gopath" >> $GITHUB_ENV + echo "GOCACHE=$gopath/gocache" >> $GITHUB_ENV + echo "PATH=$gopath/bin:$PATH" >> $GITHUB_ENV + echo "SLOW_TEST=${{inputs.slow-test}}" >> $GITHUB_ENV + echo "STACKER_DOCKER_BASE=oci:$PWD/.build/oci-clone:" >> $GITHUB_ENV + + echo "PWD=$PWD" + cat "$GITHUB_ENV" + - name: install dependencies + run: | + ./install-build-deps.sh + echo "running kernel is: $(uname -a)" + - name: docker-clone + run: | + make docker-clone "STACKER_DOCKER_BASE=docker://" CLONE_D="$PWD/.build/oci-clone" + - name: Go-download + run: | + make go-download + - name: Show disk usage before building the binaries + uses: ./.github/actions/show-disk-usage + - name: Build-level1 + run: | + make show-info + make stacker-dynamic VERSION_FULL=${{ inputs.build-id }} + - name: Show disk usage before running the tests + if: always() + uses: ./.github/actions/show-disk-usage + - name: Build and test + run: | + export GOCOVERDIR=$(mktemp -d) + make check-cov GOCOVERDIR=$GOCOVERDIR PRIVILEGE_LEVEL=${{ matrix.privilege-level }} + go tool covdata textfmt -i $GOCOVERDIR -o coverage-${{ matrix.privilege-level }}.txt + go tool covdata percent -i $GOCOVERDIR + ls -altR + env: + REGISTRY_URL: localhost:5000 + ZOT_HOST: localhost + ZOT_PORT: 8080 + - name: Show disk usage after running the tests + if: always() + uses: ./.github/actions/show-disk-usage + - name: Upload code coverage + uses: codecov/codecov-action@v3 + with: + files: coverage-${{ matrix.privilege-level}}.txt + - uses: actions/cache@v3 + id: restore-build + with: + path: stacker + key: ${{ inputs.build-id }} diff --git a/Makefile b/Makefile index bdebeb69..fbe40039 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ TOP_LEVEL := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) BUILD_D = $(TOP_LEVEL)/.build export GOPATH ?= $(BUILD_D)/gopath export GOCACHE ?= $(GOPATH)/gocache +#export GOCOVERDIR ?= GO_SRC=$(shell find pkg cmd -name \*.go) VERSION?=$(shell git describe --tags || git rev-parse HEAD) @@ -14,7 +15,7 @@ BUILD_TAGS = exclude_graphdriver_btrfs exclude_graphdriver_devicemapper containe STACKER_OPTS=--oci-dir=$(BUILD_D)/oci --roots-dir=$(BUILD_D)/roots --stacker-dir=$(BUILD_D)/stacker --storage-type=overlay -build_stacker = go build -tags "$(BUILD_TAGS) $1" -ldflags "-X main.version=$(VERSION_FULL) -X main.lxc_version=$(LXC_VERSION) $2" -o $3 ./cmd/stacker +build_stacker = go build $1 -tags "$(BUILD_TAGS) $2" -ldflags "-X main.version=$(VERSION_FULL) -X main.lxc_version=$(LXC_VERSION) $3" -o $4 ./cmd/stacker # See doc/hacking.md for how to use a local oci or docker repository. STACKER_DOCKER_BASE?=docker://ghcr.io/project-stacker/ @@ -51,15 +52,31 @@ stacker: $(STAGE1_STACKER) $(STACKER_DEPS) cmd/stacker/lxc-wrapper/lxc-wrapper.c --substitute STACKER_BUILD_BASE_IMAGE=$(STACKER_BUILD_BASE_IMAGE) \ --substitute LXC_CLONE_URL=$(LXC_CLONE_URL) \ --substitute LXC_BRANCH=$(LXC_BRANCH) \ - --substitute VERSION_FULL=$(VERSION_FULL) + --substitute VERSION_FULL=$(VERSION_FULL) \ + --substitute WITH_COV=no + +stacker-cov: $(STAGE1_STACKER) $(STACKER_DEPS) cmd/stacker/lxc-wrapper/lxc-wrapper.c + $(STAGE1_STACKER) --debug $(STACKER_OPTS) build \ + -f build.yaml \ + --substitute BUILD_D=$(BUILD_D) \ + --substitute STACKER_BUILD_BASE_IMAGE=$(STACKER_BUILD_BASE_IMAGE) \ + --substitute LXC_CLONE_URL=$(LXC_CLONE_URL) \ + --substitute LXC_BRANCH=$(LXC_BRANCH) \ + --substitute VERSION_FULL=$(VERSION_FULL) \ + --substitute WITH_COV=yes stacker-static: $(STACKER_DEPS) cmd/stacker/lxc-wrapper/lxc-wrapper - $(call build_stacker,static_build,-extldflags '-static',stacker) + $(call build_stacker,,static_build,-extldflags '-static',stacker) + +# can't use a comma in func call args, so do this instead +, := , +stacker-static-cov: $(GO_SRC) go.mod go.sum cmd/stacker/lxc-wrapper/lxc-wrapper + $(call build_stacker,-cover -coverpkg="./pkg/...$(,)./cmd/...",static_build,-extldflags '-static',stacker) # TODO: because we clean lxc-wrapper in the nested build, this always rebuilds. # Could find a better way to do this. stacker-dynamic: $(STACKER_DEPS) cmd/stacker/lxc-wrapper/lxc-wrapper - $(call build_stacker,,,stacker-dynamic) + $(call build_stacker,,,,stacker-dynamic) cmd/stacker/lxc-wrapper/lxc-wrapper: cmd/stacker/lxc-wrapper/lxc-wrapper.c make -C cmd/stacker/lxc-wrapper LDFLAGS=-static LDLIBS="$(shell pkg-config --static --libs lxc) -lpthread -ldl" lxc-wrapper @@ -134,6 +151,20 @@ test: stacker $(REGCLIENT) $(SKOPEO) $(ZOT) $(shell [ -z $(PRIVILEGE_LEVEL) ] || echo --privilege-level=$(PRIVILEGE_LEVEL)) \ $(patsubst %,test/%.bats,$(TEST)) +.PHONY: check-cov +check-cov: lint test-cov + +.PHONY: test-cov +test-cov: stacker-cov $(REGCLIENT) $(SKOPEO) $(ZOT) + sudo -E PATH="$$PATH" \ + LXC_BRANCH=$(LXC_BRANCH) \ + LXC_CLONE_URL=$(LXC_CLONE_URL) \ + STACKER_BUILD_BASE_IMAGE=$(STACKER_BUILD_BASE_IMAGE) \ + STACKER_BUILD_CENTOS_IMAGE=$(STACKER_BUILD_CENTOS_IMAGE) \ + STACKER_BUILD_UBUNTU_IMAGE=$(STACKER_BUILD_UBUNTU_IMAGE) \ + ./test/main.py \ + $(shell [ -z $(PRIVILEGE_LEVEL) ] || echo --privilege-level=$(PRIVILEGE_LEVEL)) \ + $(patsubst %,test/%.bats,$(TEST)) CLONE_D = $(BUILD_D)/oci-clone CLONE_RETRIES = 3 diff --git a/build.yaml b/build.yaml index c466b9f5..ad82e793 100644 --- a/build.yaml +++ b/build.yaml @@ -93,4 +93,8 @@ build: cd /stacker-tree make BUILD_D=/build show-info make BUILD_D=/build -C cmd/stacker/lxc-wrapper clean - make BUILD_D=/build stacker-static + if [ x${{WITH_COV}} = x"yes" ]; then + make BUILD_D=/build stacker-static-cov + else + make -C /stacker-tree stacker-static + fi diff --git a/pkg/stacker/build.go b/pkg/stacker/build.go index 2866a5cb..9f1e6ae0 100644 --- a/pkg/stacker/build.go +++ b/pkg/stacker/build.go @@ -699,6 +699,21 @@ func SetupBuildContainerConfig(config types.StackerConfig, storage types.Storage return err } + // code coverage from inside the container + if isCoverageEnabled() { + log.Infof("coverage enabled: %s", getCoverageDir()) + + err := c.BindMount(getCoverageDir(), "/tmp/.coverage", "") + if err != nil { + return err + } + + err = c.SetConfig("lxc.environment", fmt.Sprintf("GOCOVERDIR=%s", "/tmp/.coverage")) + if err != nil { + return err + } + } + rootfs, err := storage.GetLXCRootfsConfig(name) if err != nil { return err diff --git a/pkg/stacker/cover.go b/pkg/stacker/cover.go new file mode 100644 index 00000000..acca950b --- /dev/null +++ b/pkg/stacker/cover.go @@ -0,0 +1,17 @@ +package stacker + +import "os" + +func isCoverageEnabled() bool { + _, ok := os.LookupEnv("GOCOVERDIR") + return ok +} + +func getCoverageDir() string { + val, ok := os.LookupEnv("GOCOVERDIR") + if ok { + return val + } + + return "" +}