#!/usr/bin/env bash # -e Stops execution in the instance of a command or pipeline error # -u Treat unset variables as an error and exit immediately set -eu if type realpath >/dev/null 2>&1 ; then cd "$(realpath -- $(dirname -- "$0"))" fi # # Defaults # export RACE="false" STAGE="starting" STATUS="FAILURE" RUN=() UNIT_PACKAGES=() UNIT_FLAGS=() FILTER=() # # Print Functions # function print_outcome() { if [ "$STATUS" == SUCCESS ] then echo -e "\e[32m"$STATUS"\e[0m" else echo -e "\e[31m"$STATUS"\e[0m while running \e[31m"$STAGE"\e[0m" fi } function print_list_of_integration_tests() { go test -tags integration -list=. ./test/integration/... | grep '^Test' exit 0 } function exit_msg() { # complain to STDERR and exit with error echo "$*" >&2 exit 2 } function check_arg() { if [ -z "$OPTARG" ] then exit_msg "No arg for --$OPT option, use: -h for help">&2 fi } function print_usage_exit() { echo "$USAGE" exit 0 } function print_heading { echo echo -e "\e[34m\e[1m"$1"\e[0m" } function run_and_expect_silence() { echo "$@" result_file=$(mktemp -t bouldertestXXXX) "$@" 2>&1 | tee "${result_file}" # Fail if result_file is nonempty. if [ -s "${result_file}" ]; then rm "${result_file}" exit 1 fi rm "${result_file}" } # # Testing Helpers # function run_unit_tests() { go test "${UNIT_FLAGS[@]}" "${UNIT_PACKAGES[@]}" "${FILTER[@]}" } # # Main CLI Parser # USAGE="$(cat -- <<-EOM Usage: Boulder test suite CLI, intended to be run inside of a Docker container: docker compose run --use-aliases boulder ./$(basename "${0}") [OPTION]... With no options passed, runs standard battery of tests (lint, unit, and integration) -l, --lints Adds lint to the list of tests to run -u, --unit Adds unit to the list of tests to run -v, --unit-verbose Enables verbose output for unit tests -w, --unit-without-cache Disables go test caching for unit tests -p , --unit-test-package= Run unit tests for specific go package(s) -e, --enable-race-detection Enables race detection for unit and integration tests -n, --config-next Changes BOULDER_CONFIG_DIR from test/config to test/config-next -i, --integration Adds integration to the list of tests to run -s, --start-py Adds start to the list of tests to run -m, --gomod-vendor Adds gomod-vendor to the list of tests to run -g, --generate Adds generate to the list of tests to run -o, --list-integration-tests Outputs a list of the available integration tests -f , --filter= Run only those tests matching the regular expression Note: This option disables the '"back in time"' integration test setup For tests, the regular expression is split by unbracketed slash (/) characters into a sequence of regular expressions Example: TestAkamaiPurgerDrainQueueFails/TestWFECORS -h, --help Shows this help message EOM )" while getopts luvweciosmgnhp:f:-: OPT; do if [ "$OPT" = - ]; then # long option: reformulate OPT and OPTARG OPT="${OPTARG%%=*}" # extract long option name OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty) OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` fi case "$OPT" in l | lints ) RUN+=("lints") ;; u | unit ) RUN+=("unit") ;; v | unit-verbose ) UNIT_FLAGS+=("-v") ;; w | unit-without-cache ) UNIT_FLAGS+=("-count=1") ;; p | unit-test-package ) check_arg; UNIT_PACKAGES+=("${OPTARG}") ;; e | enable-race-detection ) RACE="true"; UNIT_FLAGS+=("-race") ;; i | integration ) RUN+=("integration") ;; o | list-integration-tests ) print_list_of_integration_tests ;; f | filter ) check_arg; FILTER+=("${OPTARG}") ;; s | start-py ) RUN+=("start") ;; m | gomod-vendor ) RUN+=("gomod-vendor") ;; g | generate ) RUN+=("generate") ;; n | config-next ) BOULDER_CONFIG_DIR="test/config-next" ;; h | help ) print_usage_exit ;; ??* ) exit_msg "Illegal option --$OPT" ;; # bad long option ? ) exit 2 ;; # bad short option (error reported via getopts) esac done shift $((OPTIND-1)) # remove parsed options and args from $@ list # The list of segments to run. Order doesn't matter. Note: gomod-vendor # is specifically left out of the defaults, because we don't want to run # it locally (it could delete local state). if [ -z "${RUN[@]+x}" ] then RUN+=("lints" "unit" "integration") fi # Filter is used by unit and integration but should not be used for both at the same time if [[ "${RUN[@]}" =~ unit ]] && [[ "${RUN[@]}" =~ integration ]] && [[ -n "${FILTER[@]+x}" ]] then exit_msg "Illegal option: (-f, --filter) when specifying both (-u, --unit) and (-i, --integration)" fi # If unit + filter: set correct flags for go test if [[ "${RUN[@]}" =~ unit ]] && [[ -n "${FILTER[@]+x}" ]] then FILTER=(--test.run "${FILTER[@]}") fi # If integration + filter: set correct flags for test/integration-test.py if [[ "${RUN[@]}" =~ integration ]] && [[ -n "${FILTER[@]+x}" ]] then FILTER=(--filter "${FILTER[@]}") fi # If unit test packages are not specified: set flags to run unit tests # for all boulder packages if [ -z "${UNIT_PACKAGES[@]+x}" ] then # '-p=1' configures unit tests to run serially, rather than in parallel. Our # unit tests depend on mutating a database and then cleaning up after # themselves. If these test were run in parallel, they could fail spuriously # due to one test modifying a table (especially registrations) while another # test is reading from it. # https://github.com/letsencrypt/boulder/issues/1499 # https://pkg.go.dev/cmd/go#hdr-Testing_flags UNIT_FLAGS+=("-p=1") UNIT_PACKAGES+=("./...") fi print_heading "Boulder Test Suite CLI" print_heading "Settings:" # On EXIT, trap and print outcome trap "print_outcome" EXIT settings="$(cat -- <<-EOM RUN: ${RUN[@]} BOULDER_CONFIG_DIR: $BOULDER_CONFIG_DIR UNIT_PACKAGES: ${UNIT_PACKAGES[@]} UNIT_FLAGS: ${UNIT_FLAGS[@]} FILTER: ${FILTER[@]} EOM )" echo "$settings" print_heading "Starting..." # # Run various linters. # STAGE="lints" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Lints" golangci-lint run --timeout 9m ./... # Implicitly loads staticcheck.conf from the root of the boulder repository staticcheck ./... python3 test/grafana/lint.py # Check for common spelling errors using codespell. # Update .codespell.ignore.txt if you find false positives (NOTE: ignored # words should be all lowercase). run_and_expect_silence codespell \ --ignore-words=.codespell.ignore.txt \ --skip=.git,.gocache,go.sum,go.mod,vendor,bin,*.pyc,*.pem,*.der,*.resp,*.req,*.csr,.codespell.ignore.txt,.*.swp # Check test JSON configs are formatted consistently ./test/format-configs.py 'test/config*/*.json' run_and_expect_silence git diff --exit-code . fi # # Unit Tests. # STAGE="unit" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Unit Tests" run_unit_tests fi # # Integration tests # STAGE="integration" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Integration Tests" python3 test/integration-test.py --chisel --gotest "${FILTER[@]}" fi # Test that just ./start.py works, which is a proxy for testing that # `docker compose up` works, since that just runs start.py (via entrypoint.sh). STAGE="start" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Start Test" python3 start.py & for I in {1..115}; do sleep 1 curl -s http://localhost:4001/directory && echo "Boulder took ${I} seconds to come up" && break done if [ "${I}" -eq 115 ]; then echo "Boulder did not come up after ${I} seconds during ./start.py." exit 1 fi fi # Run go mod vendor (happens only in CI) to check that the versions in # vendor/ really exist in the remote repo and match what we have. STAGE="gomod-vendor" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Go Mod Tidy" go mod tidy print_heading "Running Go Mod Vendor" go mod vendor run_and_expect_silence git diff --exit-code . fi # Run generate to make sure all our generated code can be re-generated with # current tools. # Note: Some of the tools we use seemingly don't understand ./vendor yet, and # so will fail if imports are not available in $GOPATH. STAGE="generate" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Generate" # Additionally, we need to run go install before go generate because the stringer command # (using in ./grpc/) checks imports, and depends on the presence of a built .a # file to determine an import really exists. See # https://golang.org/src/go/internal/gcimporter/gcimporter.go#L30 # Without this, we get error messages like: # stringer: checking package: grpc/bcodes.go:6:2: could not import # github.com/letsencrypt/boulder/probs (can't find import: # github.com/letsencrypt/boulder/probs) go install ./probs go install ./vendor/google.golang.org/grpc/codes run_and_expect_silence go generate ./... run_and_expect_silence git diff --exit-code . fi # Because set -e stops execution in the instance of a command or pipeline # error; if we got here we assume success STATUS="SUCCESS"