git » gofer » commit aa387f6

make: Generate coverage report

author Alberto Bertogli
2020-06-13 03:01:22 UTC
committer Alberto Bertogli
2020-06-13 12:14:28 UTC
parent 60c4f659d7bc71cc1548db694cdbf42a8dc099ee

make: Generate coverage report

This patch extends the build system to be able to easily generate a
coverage report.

Makefile +11 -1
coverage_test.go +39 -0
test/util/cover-report.sh +30 -0
test/util/exp.go +3 -0
test/util/gocovcat.go +91 -0

diff --git a/Makefile b/Makefile
index 332bb80..e4818ec 100644
--- a/Makefile
+++ b/Makefile
@@ -24,4 +24,14 @@ test: vet
 	go test ./...
 	setsid -w ./test/test.sh
 
-.PHONY: gofer vet test
+cover:
+	rm -r .cover/
+	mkdir .cover/
+	go test -tags coverage \
+		-covermode=count \
+		-coverprofile=".cover/pkg-tests.out"\
+		-coverpkg=./... ./...
+	COVER_DIR=$$PWD/.cover/ setsid -w ./test/test.sh
+	COVER_DIR=$$PWD/.cover/ ./test/util/cover-report.sh
+
+.PHONY: gofer vet test cover
diff --git a/coverage_test.go b/coverage_test.go
new file mode 100644
index 0000000..1ca53b4
--- /dev/null
+++ b/coverage_test.go
@@ -0,0 +1,39 @@
+// Test file used to build a coverage-enabled binary.
+//
+// Go lacks support for properly building a coverage binary, it can only build
+// coverage test binaries.  As a workaround, we have a test that just runs
+// main. We then build a binary of this test, which we use instead of the
+// normal binary in integration tests.
+//
+// This is hacky and horrible.
+//
+// The test has a build label so it's not accidentally executed during normal
+// "go test" invocations.
+// +build coveragebin
+
+package main
+
+import (
+	"os"
+	"os/signal"
+	"syscall"
+	"testing"
+)
+
+func TestRunMain(t *testing.T) {
+	done := make(chan bool)
+
+	signals := make(chan os.Signal, 1)
+	go func() {
+		<-signals
+		done <- true
+	}()
+	signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGTERM)
+
+	go func() {
+		main()
+		done <- true
+	}()
+
+	<-done
+}
diff --git a/test/util/cover-report.sh b/test/util/cover-report.sh
new file mode 100755
index 0000000..32995a8
--- /dev/null
+++ b/test/util/cover-report.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -e
+
+if [ "$V" == "1" ]; then
+	set -v
+fi
+
+UTILDIR="$( realpath `dirname "${0}"` )/"
+
+# Run from the repo root.
+cd "$(realpath `dirname ${0}`)/../../"
+
+echo "## Coverage"
+
+# Merge all coverage output into a single file.
+# Ignore protocol buffer-generated files, as they are not relevant.
+go run "${UTILDIR}/gocovcat.go" "${COVER_DIR}"/*.out \
+> "${COVER_DIR}/all.out"
+
+# Generate reports based on the merged output.
+go tool cover -func="$COVER_DIR/all.out" | sort -k 3 -n > "$COVER_DIR/func.txt"
+go tool cover -html="$COVER_DIR/all.out" -o "$COVER_DIR/coverage.html"
+
+TOTAL=$(cat .cover/func.txt | grep "total:" | awk '{print $3}')
+echo
+echo "Total:" $TOTAL
+echo
+echo "Coverage report can be found in:"
+echo file://$COVER_DIR/coverage.html
diff --git a/test/util/exp.go b/test/util/exp.go
index f6daa1d..802c8c2 100644
--- a/test/util/exp.go
+++ b/test/util/exp.go
@@ -1,3 +1,6 @@
+// +build ignore
+
+// Fetch an URL, and check if the response matches what we expect.
 package main
 
 import (
diff --git a/test/util/gocovcat.go b/test/util/gocovcat.go
new file mode 100755
index 0000000..5b3e759
--- /dev/null
+++ b/test/util/gocovcat.go
@@ -0,0 +1,91 @@
+//usr/bin/env go run "$0" "$@"; exit $?
+//
+// From: https://git.lukeshu.com/go/cmd/gocovcat/
+//
+// +build ignore
+
+// Copyright 2017 Luke Shumaker <lukeshu@parabola.nu>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+// Command gocovcat combines multiple go cover runs, and prints the
+// result on stdout.
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+func handleErr(err error) {
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "%v\n", err)
+		os.Exit(1)
+	}
+}
+
+func main() {
+	modeBool := false
+	blocks := map[string]int{}
+	for _, filename := range os.Args[1:] {
+		file, err := os.Open(filename)
+		handleErr(err)
+		buf := bufio.NewScanner(file)
+		for buf.Scan() {
+			line := buf.Text()
+
+			if strings.HasPrefix(line, "mode: ") {
+				m := strings.TrimPrefix(line, "mode: ")
+				switch m {
+				case "set":
+					modeBool = true
+				case "count", "atomic":
+					// do nothing
+				default:
+					fmt.Fprintf(os.Stderr, "Unrecognized mode: %s\n", m)
+					os.Exit(1)
+				}
+			} else {
+				sp := strings.LastIndexByte(line, ' ')
+				block := line[:sp]
+				cntStr := line[sp+1:]
+				cnt, err := strconv.Atoi(cntStr)
+				handleErr(err)
+				blocks[block] += cnt
+			}
+		}
+		handleErr(buf.Err())
+	}
+	keys := make([]string, 0, len(blocks))
+	for key := range blocks {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	modeStr := "count"
+	if modeBool {
+		modeStr = "set"
+	}
+	fmt.Printf("mode: %s\n", modeStr)
+	for _, block := range keys {
+		cnt := blocks[block]
+		if modeBool && cnt > 1 {
+			cnt = 1
+		}
+		fmt.Printf("%s %d\n", block, cnt)
+	}
+}