Skip to content

Commit 06f4e5f

Browse files
author
ag
committed
adds support for test coverage for all test types
1 parent 6a10da0 commit 06f4e5f

5 files changed

Lines changed: 110 additions & 29 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ tags
4040

4141
# ProxySQL log files
4242
test/proxysql/*.log*
43+
44+
# Coverage files
45+
test/coverage

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ To run all integration tests:
133133
docker compose run --use-aliases boulder ./test.sh --integration
134134
```
135135

136+
To run unit tests and integration tests with coverage:
137+
138+
```shell
139+
docker compose run --use-aliases boulder ./test.sh --unit --integration --coverage --coverage-dir=./test/coverage/mytestrun
140+
```
141+
136142
To run specific integration tests (example runs TestAkamaiPurgerDrainQueueFails and TestWFECORS):
137143

138144
```shell

test.sh

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ UNIT_PACKAGES=()
1919
UNIT_FLAGS=()
2020
INTEGRATION_FLAGS=()
2121
FILTER=()
22+
COVERAGE="false"
23+
COVERAGE_DIR="test/coverage/$(date +%Y-%m-%d_%H-%M-%S)"
2224

2325
#
2426
# Cleanup Functions
@@ -105,6 +107,9 @@ With no options passed, runs standard battery of tests (lint, unit, and integrat
105107
-i, --integration Adds integration to the list of tests to run
106108
-s, --start-py Adds start to the list of tests to run
107109
-g, --generate Adds generate to the list of tests to run
110+
-c, --coverage Enables coverage for tests
111+
-d <DIR>, --coverage-directory=<DIR> Directory to store coverage files in
112+
Default: test/coverage/<timestamp>
108113
-f <REGEX>, --filter=<REGEX> Run only those tests matching the regular expression
109114
110115
Note:
@@ -120,7 +125,7 @@ With no options passed, runs standard battery of tests (lint, unit, and integrat
120125
EOM
121126
)"
122127

123-
while getopts luvwecismgnhp:f:-: OPT; do
128+
while getopts luvwecisgnhd:p:f:-: OPT; do
124129
if [ "$OPT" = - ]; then # long option: reformulate OPT and OPTARG
125130
OPT="${OPTARG%%=*}" # extract long option name
126131
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
@@ -138,6 +143,8 @@ while getopts luvwecismgnhp:f:-: OPT; do
138143
s | start-py ) RUN+=("start") ;;
139144
g | generate ) RUN+=("generate") ;;
140145
n | config-next ) BOULDER_CONFIG_DIR="test/config-next" ;;
146+
c | coverage ) COVERAGE="true" ;;
147+
d | coverage-dir ) check_arg; COVERAGE_DIR="${OPTARG}" ;;
141148
h | help ) print_usage_exit ;;
142149
??* ) exit_msg "Illegal option --$OPT" ;; # bad long option
143150
? ) exit 2 ;; # bad short option (error reported via getopts)
@@ -197,10 +204,15 @@ settings="$(cat -- <<-EOM
197204
UNIT_PACKAGES: ${UNIT_PACKAGES[@]}
198205
UNIT_FLAGS: ${UNIT_FLAGS[@]}
199206
FILTER: ${FILTER[@]}
200-
207+
COVERAGE: $COVERAGE
208+
COVERAGE_DIR: $COVERAGE_DIR
201209
EOM
202210
)"
203211

212+
if [ "${COVERAGE}" == "true" ]; then
213+
mkdir -p "$COVERAGE_DIR"
214+
fi
215+
204216
echo "$settings"
205217
print_heading "Starting..."
206218

@@ -226,6 +238,12 @@ STAGE="unit"
226238
if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
227239
print_heading "Running Unit Tests"
228240
flush_redis
241+
242+
if [ "${COVERAGE}" == "true" ]; then
243+
UNIT_CSV=$(IFS=,; echo "${UNIT_PACKAGES[*]}")
244+
UNIT_FLAGS+=("-cover" "-covermode=atomic" "-coverprofile=${COVERAGE_DIR}/unit.coverprofile" "-coverpkg=${UNIT_CSV}")
245+
fi
246+
229247
run_unit_tests
230248
fi
231249

@@ -236,11 +254,27 @@ STAGE="integration"
236254
if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
237255
print_heading "Running Integration Tests"
238256
flush_redis
257+
258+
# Set up test parameters
259+
INTEGRATION_ARGS=("--chisel")
260+
261+
# Add verbose flag if requested
239262
if [[ "${INTEGRATION_FLAGS[@]}" =~ "-v" ]] ; then
240-
python3 test/integration-test.py --chisel --gotestverbose "${FILTER[@]}"
263+
INTEGRATION_ARGS+=("--gotestverbose")
241264
else
242-
python3 test/integration-test.py --chisel --gotest "${FILTER[@]}"
265+
INTEGRATION_ARGS+=("--gotest")
266+
fi
267+
268+
# Add coverage settings if enabled
269+
if [ "${COVERAGE}" == "true" ]; then
270+
INTEGRATION_ARGS+=("--coverage" "--coverage-dir=${COVERAGE_DIR}")
243271
fi
272+
273+
# Add any filters
274+
INTEGRATION_ARGS+=("${FILTER[@]}")
275+
276+
# Run the integration tests with all collected arguments
277+
python3 test/integration-test.py "${INTEGRATION_ARGS[@]}"
244278
fi
245279

246280
# Test that just ./start.py works, which is a proxy for testing that

test/integration-test.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,15 @@
1010
import argparse
1111
import datetime
1212
import inspect
13-
import json
1413
import os
15-
import random
1614
import re
17-
import requests
1815
import subprocess
19-
import shlex
20-
import signal
21-
import time
2216

17+
import requests
2318
import startservers
24-
2519
import v2_integration
2620
from helpers import *
2721

28-
from acme import challenges
29-
3022
# Set the environment variable RACE to anything other than 'true' to disable
3123
# race detection. This significantly speeds up integration testing cycles
3224
# locally.
@@ -55,6 +47,11 @@ def main():
5547
parser = argparse.ArgumentParser(description='Run integration tests')
5648
parser.add_argument('--chisel', dest="run_chisel", action="store_true",
5749
help="run integration tests using chisel")
50+
parser.add_argument('--coverage', dest="coverage", action="store_true",
51+
help="run integration tests with coverage")
52+
parser.add_argument('--coverage-dir', dest="coverage_dir", action="store",
53+
default=f"test/coverage/{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}",
54+
help="directory to store coverage data")
5855
parser.add_argument('--gotest', dest="run_go", action="store_true",
5956
help="run Go integration tests")
6057
parser.add_argument('--gotestverbose', dest="run_go_verbose", action="store_true",
@@ -64,24 +61,45 @@ def main():
6461
# allow any ACME client to run custom command for integration
6562
# testing (without having to implement its own busy-wait loop)
6663
parser.add_argument('--custom', metavar="CMD", help="run custom command")
67-
parser.set_defaults(run_chisel=False, test_case_filter="", skip_setup=False)
64+
parser.set_defaults(run_chisel=False, test_case_filter="", skip_setup=False, coverage=False, coverage_dir=None)
6865
args = parser.parse_args()
6966

67+
if args.coverage and args.coverage_dir:
68+
if not os.path.exists(args.coverage_dir):
69+
os.makedirs(args.coverage_dir)
70+
if not os.path.isdir(args.coverage_dir):
71+
raise(Exception("coverage-dir must be a directory"))
72+
7073
if not (args.run_chisel or args.custom or args.run_go is not None):
7174
raise(Exception("must run at least one of the letsencrypt or chisel tests with --chisel, --gotest, or --custom"))
7275

73-
if not startservers.install(race_detection=race_detection):
76+
if not startservers.install(race_detection=race_detection, coverage=args.coverage):
7477
raise(Exception("failed to build"))
7578

76-
if not startservers.start():
79+
if not args.test_case_filter:
80+
now = datetime.datetime.utcnow()
81+
82+
six_months_ago = now+datetime.timedelta(days=-30*6)
83+
if not startservers.start(fakeclock=fakeclock(six_months_ago), coverage_dir=args.coverage_dir):
84+
raise(Exception("startservers failed (mocking six months ago)"))
85+
setup_six_months_ago()
86+
startservers.stop()
87+
88+
twenty_days_ago = now+datetime.timedelta(days=-20)
89+
if not startservers.start(fakeclock=fakeclock(twenty_days_ago), coverage_dir=args.coverage_dir):
90+
raise(Exception("startservers failed (mocking twenty days ago)"))
91+
setup_twenty_days_ago()
92+
startservers.stop()
93+
94+
if not startservers.start(fakeclock=None, coverage_dir=args.coverage_dir):
7795
raise(Exception("startservers failed"))
7896

7997
if args.run_chisel:
8098
run_chisel(args.test_case_filter)
8199

82100
if args.run_go:
83101
run_go_tests(args.test_case_filter, False)
84-
102+
85103
if args.run_go_verbose:
86104
run_go_tests(args.test_case_filter, True)
87105

@@ -94,6 +112,10 @@ def main():
94112
run_cert_checker()
95113
check_balance()
96114

115+
# If coverage is enabled, process the coverage data
116+
if args.coverage:
117+
process_covdata(args.coverage_dir)
118+
97119
if not startservers.check():
98120
raise(Exception("startservers.check failed"))
99121

@@ -128,12 +150,24 @@ def check_balance():
128150
]
129151
for address in addresses:
130152
metrics = requests.get("http://%s/metrics" % address)
131-
if not "grpc_server_handled_total" in metrics.text:
153+
if "grpc_server_handled_total" not in metrics.text:
132154
raise(Exception("no gRPC traffic processed by %s; load balancing problem?")
133155
% address)
134156

135157
def run_cert_checker():
136158
run(["./bin/boulder", "cert-checker", "-config", "%s/cert-checker.json" % config_dir])
137159

160+
def process_covdata(coverage_dir):
161+
"""Process coverage data and generate reports."""
162+
if not os.path.exists(coverage_dir):
163+
raise(Exception("Coverage directory does not exist: %s" % coverage_dir))
164+
165+
# Generate text report
166+
coverage_dir = os.path.abspath(coverage_dir)
167+
cov_text = os.path.join(coverage_dir, "integration.coverprofile")
168+
# this works, but if it takes a long time consider merging with `go tool covdata merge` first
169+
# https://go.dev/blog/integration-test-coverage#merging-raw-profiles-with-go-tool-covdata-merge
170+
run(["go", "tool", "covdata", "textfmt", "-i", coverage_dir, "-o", cov_text])
171+
138172
if __name__ == "__main__":
139173
main()

test/startservers.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import atexit
22
import collections
33
import os
4-
import shutil
54
import signal
65
import socket
76
import subprocess
8-
import sys
9-
import tempfile
10-
import threading
11-
import time
127

13-
from helpers import waithealth, waitport, config_dir, CONFIG_NEXT
8+
from helpers import config_dir, waithealth, waitport
149

1510
Service = collections.namedtuple('Service', ('name', 'debug_port', 'grpc_port', 'host_override', 'cmd', 'deps'))
1611

@@ -130,7 +125,7 @@
130125
None),
131126
Service('pardot-test-srv',
132127
# Uses port 9601 to mock Salesforce OAuth2 token API and 9602 to mock
133-
# the Pardot API.
128+
# the Pardot API.
134129
9601, None, None,
135130
('./bin/pardot-test-srv', '--config', os.path.join(config_dir, 'pardot-test-srv.json'),),
136131
None),
@@ -186,24 +181,33 @@ def _service_toposort(services):
186181
# to run the load-generator).
187182
challSrvProcess = None
188183

189-
def install(race_detection):
184+
def install(race_detection, coverage=False):
190185
# Pass empty BUILD_TIME and BUILD_ID flags to avoid constantly invalidating the
191186
# build cache with new BUILD_TIMEs, or invalidating it on merges with a new
192187
# BUILD_ID.
193188
go_build_flags='-tags "integration"'
194189
if race_detection:
195190
go_build_flags += ' -race'
196191

192+
if coverage:
193+
go_build_flags += ' -cover' # https://go.dev/blog/integration-test-coverage
194+
197195
return subprocess.call(["/usr/bin/make", "GO_BUILD_FLAGS=%s" % go_build_flags]) == 0
198196

199-
def run(cmd):
197+
def run(cmd, fakeclock, coverage_dir=None):
200198
e = os.environ.copy()
201199
e.setdefault("GORACE", "halt_on_error=1")
200+
if coverage_dir:
201+
abs_coverage_dir = os.path.abspath(coverage_dir)
202+
e.setdefault("GOCOVERDIR", abs_coverage_dir)
203+
e.setdefault("GOCOVERMODE", "atomic")
204+
if fakeclock:
205+
e.setdefault("FAKECLOCK", fakeclock)
202206
p = subprocess.Popen(cmd, env=e)
203207
p.cmd = cmd
204208
return p
205209

206-
def start():
210+
def start(fakeclock, coverage_dir=None):
207211
"""Return True if everything builds and starts.
208212
209213
Give up and return False if anything fails to build, or dies at
@@ -232,7 +236,7 @@ def start():
232236
print("Starting service", service.name)
233237
try:
234238
global processes
235-
p = run(service.cmd)
239+
p = run(service.cmd, fakeclock, coverage_dir)
236240
processes.append(p)
237241
if service.grpc_port is not None:
238242
waithealth(' '.join(p.args), service.grpc_port, service.host_override)

0 commit comments

Comments
 (0)