git » dnss » commit 18b1577

tests: Add coverage and integration test scripts

author Alberto Bertogli
2018-04-15 14:10:42 UTC
committer Alberto Bertogli
2018-04-15 14:29:21 UTC
parent a49786f3cf0d830874a85844af7bd9379f5e9542

tests: Add coverage and integration test scripts

This patch adds scripts for integration tests against external services,
and to generate code coverage reports.

Note the script for testing against external services is brittle and is
not hermetic, so it is prone to false negatives. However, it is a
valuable tool to check that dnss can inter-operate with common public
providers.

coverage_test.go +39 -0
tests/all.sh +19 -0
{tools => tests}/bench +4 -4
tests/coverage.sh +35 -0
tests/external.sh +150 -0
tests/gocovcat.go +91 -0

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/tests/all.sh b/tests/all.sh
new file mode 100755
index 0000000..fcde331
--- /dev/null
+++ b/tests/all.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# The tests are run from the repository root.
+cd "$(realpath `dirname ${0}`)/../"
+
+set -ev
+
+#####################################
+go test ./...
+
+#####################################
+setsid -w tests/bench
+
+#####################################
+setsid -w tests/external.sh
+
+#####################################
+setsid -w tests/coverage.sh
+
diff --git a/tools/bench b/tests/bench
similarity index 93%
rename from tools/bench
rename to tests/bench
index 45a007e..0d02663 100755
--- a/tools/bench
+++ b/tests/bench
@@ -8,13 +8,13 @@
 #
 # Examples:
 #   # Run the benchmarks, recording the output IFF the tree is not dirty.
-#   ./tools/bench
+#   ./tests/bench
 #
 #   # Diff between two recorded commits.
-#   ./tools/bench diff 8b25916 HEAD
+#   ./tests/bench diff 8b25916 HEAD
 #
 #   # Run the benchmarks without recording, and compare against a commit.
-#   ./tools/bench rundiff 8b25916
+#   ./tests/bench rundiff 8b25916
 #
 
 set -e
@@ -36,7 +36,7 @@ NO_RECORD=
 
 # Don't record results for a dirty tree.
 # Note this tool is explicitly excluded so we can easily test old commits.
-DIRTY=$(git status --porcelain | grep -v tools/bench | grep -v "^??" | wc -l)
+DIRTY=$(git status --porcelain | grep -v tests/bench | grep -v "^??" | wc -l)
 if [ "$DIRTY" -gt 0 ]; then
 		echo "Dirty tree, not recording results"
 		NO_RECORD=1
diff --git a/tests/coverage.sh b/tests/coverage.sh
new file mode 100755
index 0000000..8285192
--- /dev/null
+++ b/tests/coverage.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+#
+# Run tests in coverage mode, generating HTML and function reports.
+#
+
+set -e
+
+# The tests are run from the repository root.
+cd "$(realpath `dirname ${0}`)/../"
+
+# Recreate the coverage output directory, to avoid including stale results
+# from previous runs.
+rm -rf .coverage
+mkdir -p .coverage
+export COVER_DIR="$PWD/.coverage"
+
+
+go test \
+	-covermode=count \
+	-coverprofile="$COVER_DIR/pkg-tests.out" \
+	-coverpkg=./... \
+	./...
+
+# These will run in coverage mode due to $COVER_DIR being set.
+setsid -w ./tests/external.sh
+
+# Merge all coverage output into a single file.
+go run "tests/gocovcat.go" .coverage/*.out \
+        > .coverage/all.out
+
+go tool cover -func=.coverage/all.out | sort -k 3 -n > ".func.txt"
+go tool cover -html=.coverage/all.out -o .coverage/dnss.cover.html
+
+grep -i total .func.txt
+echo "file:///$PWD/.coverage/dnss.cover.html"
diff --git a/tests/external.sh b/tests/external.sh
new file mode 100755
index 0000000..3a68cd2
--- /dev/null
+++ b/tests/external.sh
@@ -0,0 +1,150 @@
+#!/bin/bash
+#
+# Integration tests against external hosts.
+#
+# The goal is to test how dnss interacts with publicly available services.
+#
+# These tests use the network and public internet to talk to:
+# - the machine's configured DNS server
+# - dns.google.com
+# - 1.1.1.1.
+#
+# So the tests are not hermetic and could fail for external reasons.
+
+
+set -e
+
+# Set traps to kill our subprocesses when we exit (for any reason).
+trap ":" TERM      # Avoid the EXIT handler from killing bash.
+trap "exit 2" INT  # Ctrl-C, make sure we fail in that case.
+trap "kill 0" EXIT # Kill children on exit.
+
+# The tests are run from the repository root.
+cd "$(realpath `dirname ${0}`)/../"
+
+# Build the dnss binary.
+if [ "$COVER_DIR" != "" ]; then
+	go test -covermode=count -coverpkg=./... -c -tags coveragebin
+	mv dnss.test dnss
+else
+	go build
+fi
+
+
+# Run dnss in the background (sets $PID to its process id).
+function dnss() {
+	# Set the coverage arguments each time, as we don't want the different
+	# runs to override the generated profile.
+	if [ "$COVER_DIR" != "" ]; then
+		COVER_ARGS="-test.run=^TestRunMain$ \
+			-test.coverprofile=$COVER_DIR/it-`date +%s.%N`.out"
+	fi
+
+	./dnss $COVER_ARGS \
+		-v 3 -monitoring_listen_addr :1900 \
+		-testing__insecure_http \
+		"$@" > .dnss.log 2>&1 &
+	PID=$!
+}
+
+# Wait until there's something listening on the given port.
+function wait_until_ready() {
+	PROTO=$1
+	PORT=$2
+
+	while ! bash -c "true < /dev/$PROTO/localhost/$PORT" 2>/dev/null ; do
+		sleep 0.01
+	done
+}
+
+# Resolve some queries.
+function resolve() {
+	wait_until_ready tcp 1053
+
+	kdig @127.0.0.1:1053 +tcp  example.com a > .dig.log
+	grep -E -q '^example.com.*A'  .dig.log
+
+	kdig @127.0.0.1:1053 +notcp  example.com a > .dig.log
+	grep -E -q '^example.com.*A'  .dig.log
+
+	kdig @127.0.0.1:1053 +notcp  com.ar NS > .dig.log
+	grep -E -q '^com.ar.*NS'  .dig.log
+}
+
+# HTTP GET, using wget.
+function get() {
+	URL=$1
+
+	wget -O/dev/null $URL > .wget.log 2>&1
+}
+
+echo "## Misc"
+# Missing arguments (expect it to fail).
+dnss
+if wait $PID; then
+	echo "Expected dnss to fail, but it worked"
+	exit 1
+fi
+
+
+echo "## Launching HTTPS server"
+dnss -enable_https_to_dns -https_server_addr "localhost:1999"
+HTTP_PID=$PID
+mv .dnss.log .dnss.http.log
+
+wait_until_ready tcp 1999
+
+echo "## JSON against dnss"
+dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \
+	-https_upstream "http://localhost:1999/dns-query"
+
+resolve
+kill $PID
+
+echo "## DoH against dnss"
+dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \
+	-experimental__doh_mode \
+	-https_upstream "http://localhost:1999/dns-query"
+
+# Exercise DoH via GET (dnss always uses POST).
+get "http://localhost:1999/resolve?&dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB"
+
+if get "http://localhost:1999/resolve?&dns=invalidbase64@"; then
+	echo "GET with invalid base64 did not fail"
+	exit 1
+fi
+
+if get "http://localhost:1999/resolve?nothing"; then
+	echo "GET with nonsense query did not fail"
+	exit 1
+fi
+
+resolve
+kill $PID
+
+kill $HTTP_PID
+
+
+# TODO: uncomment when 1.1.1.1 supports -07.
+#echo "## DoH against 1.1.1.1"
+#dnss -enable_dns_to_https -dns_listen_addr "localhost:1053" \
+#	-experimental__doh_mode \
+#	-https_upstream "https://1.1.1.1/dns-query"
+#
+#resolve
+#kill $PID
+
+
+echo "## JSON against default (checks default works)"
+dnss -enable_dns_to_https -dns_listen_addr "localhost:1053"
+
+resolve
+
+# Take this opportunity to query some URLs, to exercise their code when they
+# have requests.
+get http://localhost:1900/debug/dnsserver/cache/dump
+get http://localhost:1900/debug/dnsserver/cache/flush
+
+kill $PID
+
+echo SUCCESS
diff --git a/tests/gocovcat.go b/tests/gocovcat.go
new file mode 100644
index 0000000..5b3e759
--- /dev/null
+++ b/tests/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)
+	}
+}