author | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-04-15 14:10:42 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-04-15 14:29:21 UTC |
parent | a49786f3cf0d830874a85844af7bd9379f5e9542 |
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) + } +}