git » chasquid » commit bb90274

test: Add tools to generate test coverage information

author Alberto Bertogli
2018-02-25 01:24:00 UTC
committer Alberto Bertogli
2018-03-02 19:37:37 UTC
parent 751fbd4b554e34f434955b94f7c76c9a73735ee2

test: Add tools to generate test coverage information

This patch adds some tooling and scripts to generate test coverage
information.

Unfortunately, this involves some hacks as Go does not have support for
generating coverage-enabled binaries, or merging coverage reports; but
overall it's not very intrusive.

.gitignore +3 -0
cmd/chasquid-util/chasquid-util.go +4 -0
cmd/dovecot-auth-cli/dovecot-auth-cli.go +2 -0
cmd/mda-lmtp/mda-lmtp.go +3 -0
cmd/smtp-check/smtp-check.go +3 -0
cmd/spf-check/spf-check.go +2 -0
coverage_test.go +39 -0
test/cover.sh +48 -0
test/t-03-queue_persistency/addtoqueue.go +3 -0
test/util/gocovcat.go +91 -0
test/util/lib.sh +23 -0

diff --git a/.gitignore b/.gitignore
index 02ae02d..2269e3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,9 @@ cmd/smtp-check/smtp-check
 cmd/spf-check/spf-check
 cmd/mda-lmtp/mda-lmtp
 
+# Test binary, generated during coverage tests.
+chasquid.test
+
 # Exclude any .pem files, to prevent accidentally including test keys and
 # certificates.
 *.pem
diff --git a/cmd/chasquid-util/chasquid-util.go b/cmd/chasquid-util/chasquid-util.go
index 8ced913..a617e0e 100644
--- a/cmd/chasquid-util/chasquid-util.go
+++ b/cmd/chasquid-util/chasquid-util.go
@@ -1,4 +1,8 @@
 // chasquid-util is a command-line utility for chasquid-related operations.
+//
+// Don't include it in the coverage build.
+// +build !coverage
+
 package main
 
 import (
diff --git a/cmd/dovecot-auth-cli/dovecot-auth-cli.go b/cmd/dovecot-auth-cli/dovecot-auth-cli.go
index aee3a92..fefb43d 100644
--- a/cmd/dovecot-auth-cli/dovecot-auth-cli.go
+++ b/cmd/dovecot-auth-cli/dovecot-auth-cli.go
@@ -1,6 +1,8 @@
 // CLI used for testing the dovecot authentication package.
 //
 // NOT for production use.
+// +build !coverage
+
 package main
 
 import (
diff --git a/cmd/mda-lmtp/mda-lmtp.go b/cmd/mda-lmtp/mda-lmtp.go
index f6ead8d..8be8bfa 100644
--- a/cmd/mda-lmtp/mda-lmtp.go
+++ b/cmd/mda-lmtp/mda-lmtp.go
@@ -1,6 +1,9 @@
 // mda-lmtp is a very basic MDA that uses LMTP to do the delivery.
 //
 // See the usage below for details.
+//
+// +build !coverage
+
 package main
 
 import (
diff --git a/cmd/smtp-check/smtp-check.go b/cmd/smtp-check/smtp-check.go
index 6a1ac84..4ce912f 100644
--- a/cmd/smtp-check/smtp-check.go
+++ b/cmd/smtp-check/smtp-check.go
@@ -1,4 +1,7 @@
 // smtp-check is a command-line too for checking SMTP setups.
+//
+// +build !coverage
+
 package main
 
 import (
diff --git a/cmd/spf-check/spf-check.go b/cmd/spf-check/spf-check.go
index 05fe925..53e609a 100644
--- a/cmd/spf-check/spf-check.go
+++ b/cmd/spf-check/spf-check.go
@@ -1,6 +1,8 @@
 // Command line tool for playing with the SPF library.
 //
 // Not for use in production, just development and experimentation.
+// +build !coverage
+
 package main
 
 import (
diff --git a/coverage_test.go b/coverage_test.go
new file mode 100644
index 0000000..99af46f
--- /dev/null
+++ b/coverage_test.go
@@ -0,0 +1,39 @@
+// Test file used to build a coverage-enabled chasquid 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 chasquid
+// 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/cover.sh b/test/cover.sh
new file mode 100755
index 0000000..d64b0eb
--- /dev/null
+++ b/test/cover.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Runs tests (both go and integration) in coverage-generation mode.
+# Generates an HTML report with the results.
+#
+# The .coverage directory is used to store the data, it will be erased and
+# recreated on each run.
+#
+# This is not very tidy, and relies on some hacky tricks (see
+# coverage_test.go), but works for now.
+
+set -e
+. $(dirname ${0})/util/lib.sh
+
+init
+
+cd "${TBASE}/.."
+
+# Recreate the coverage output directory, to avoid including stale results
+# from previous runs.
+rm -rf .coverage
+mkdir -p .coverage
+export COVER_DIR="$PWD/.coverage"
+
+# Normal go tests.
+go test -tags coverage \
+	-covermode=count \
+	-coverprofile="$COVER_DIR/pkg-tests.out"\
+	-coverpkg=./... ./...
+
+# Integration tests.
+# Will run in coverage mode due to $COVER_DIR being set.
+setsid -w ./test/run.sh
+
+# Merge all coverage output into a single file.
+# Ignore protocol buffer-generated files, as they are not relevant.
+go run "${UTILDIR}/gocovcat.go" .coverage/*.out \
+	| grep -v ".pb.go:" \
+	> .coverage/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/chasquid.html"
+
+echo
+echo "Coverage report can be found in:"
+echo file://$COVER_DIR/chasquid.html
+
diff --git a/test/t-03-queue_persistency/addtoqueue.go b/test/t-03-queue_persistency/addtoqueue.go
index 6c770ba..446ceb0 100644
--- a/test/t-03-queue_persistency/addtoqueue.go
+++ b/test/t-03-queue_persistency/addtoqueue.go
@@ -3,6 +3,9 @@
 //
 // Note that chasquid does NOT support this, we do it before starting up the
 // daemon for testing purposes only.
+//
+// +build ignore
+
 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)
+	}
+}
diff --git a/test/util/lib.sh b/test/util/lib.sh
index aed646d..563424b 100644
--- a/test/util/lib.sh
+++ b/test/util/lib.sh
@@ -29,6 +29,11 @@ function generate_cert() {
 }
 
 function chasquid() {
+	if [ "${COVER_DIR}" != "" ]; then
+		chasquid_cover "$@"
+		return
+	fi
+
 	# HOSTALIASES: so we "fake" hostnames.
 	# PATH: so chasquid can call test-mda without path issues.
 	# MDA_DIR: so our test-mda knows where to deliver emails.
@@ -38,6 +43,24 @@ function chasquid() {
 		go run ${RACE} ${TBASE}/../../chasquid.go "$@"
 }
 
+function chasquid_cover() {
+	# Build the coverage-enabled binary.
+	# See coverage_test.go for more details.
+	( cd ${TBASE}/../../;
+	  go test -covermode=count -coverpkg=./... -c -tags coveragebin )
+
+	# Run the coverage-enabled binary, named "chasquid.test" for hacky
+	# reasons.  See the chasquid function above for details on the
+	# environment variables.
+	HOSTALIASES=${TBASE}/hosts \
+	PATH=${UTILDIR}:${PATH} \
+	MDA_DIR=${TBASE}/.mail \
+		${TBASE}/../../chasquid.test \
+			-test.run "^TestRunMain$" \
+			-test.coverprofile="$COVER_DIR/test-`date +%s.%N`.out" \
+			"$@"
+}
+
 function add_user() {
 	CONFDIR="${CONFDIR:-config}"
 	DOMAIN=$(echo $1 | cut -d @ -f 2)