github.com/letsencrypt/boulder@v0.20251208.0/test.sh (about)

     1  #!/usr/bin/env bash
     2  
     3  # -e Stops execution in the instance of a command or pipeline error
     4  # -u Treat unset variables as an error and exit immediately
     5  set -eu
     6  
     7  if type realpath >/dev/null 2>&1 ; then
     8    cd "$(realpath -- $(dirname -- "$0"))"
     9  fi
    10  
    11  #
    12  # Defaults
    13  #
    14  export RACE="false"
    15  export USE_VITESS="false"
    16  STAGE="starting"
    17  STATUS="FAILURE"
    18  RUN=()
    19  UNIT_PACKAGES=()
    20  UNIT_FLAGS=()
    21  INTEGRATION_FLAGS=()
    22  FILTER=()
    23  COVERAGE="false"
    24  COVERAGE_DIR="test/coverage/$(date +%Y-%m-%d_%H-%M-%S)"
    25  DB_URL_FILES=(
    26    badkeyrevoker_dburl
    27    cert_checker_dburl
    28    incidents_dburl
    29    revoker_dburl
    30    sa_dburl
    31    sa_ro_dburl
    32  )
    33  
    34  #
    35  # Cleanup Functions
    36  #
    37  
    38  function flush_redis() {
    39    go run ./test/boulder-tools/flushredis/main.go
    40  }
    41  
    42  #
    43  # Print Functions
    44  #
    45  function print_outcome() {
    46    if [ "$STATUS" == SUCCESS ]
    47    then
    48      echo -e "\e[32m"$STATUS"\e[0m"
    49    else
    50      echo -e "\e[31m"$STATUS"\e[0m while running \e[31m"$STAGE"\e[0m"
    51    fi
    52  }
    53  
    54  function exit_msg() {
    55    # complain to STDERR and exit with error
    56    echo "$*" >&2
    57    exit 2
    58  }
    59  
    60  function check_arg() {
    61    if [ -z "$OPTARG" ]
    62    then
    63      exit_msg "No arg for --$OPT option, use: -h for help">&2
    64    fi
    65  }
    66  
    67  function print_usage_exit() {
    68    echo "$USAGE"
    69    exit 0
    70  }
    71  
    72  function print_heading {
    73    echo
    74    echo -e "\e[34m\e[1m"$1"\e[0m"
    75  }
    76  
    77  function run_and_expect_silence() {
    78    echo "$@"
    79    result_file=$(mktemp -t bouldertestXXXX)
    80    "$@" 2>&1 | tee "${result_file}"
    81  
    82    # Fail if result_file is nonempty.
    83    if [ -s "${result_file}" ]; then
    84      rm "${result_file}"
    85      exit 1
    86    fi
    87    rm "${result_file}"
    88  }
    89  
    90  configure_database_endpoints() {
    91    dburl_target_dir="proxysql"
    92    export DB_ADDR="boulder-proxysql:6033"
    93  
    94    if [[ "${USE_VITESS}" == "true" ]]
    95    then
    96      dburl_target_dir="vitess"
    97      export DB_ADDR="boulder-vitess:33577"
    98    fi
    99  
   100    # Configure DBURL symlinks
   101    rm -f test/secrets/*_dburl || true
   102    for file in ${DB_URL_FILES:+${DB_URL_FILES[@]+"${DB_URL_FILES[@]}"}}
   103    do
   104      ln -sf "dburls/${dburl_target_dir}/${file}" "test/secrets/${file}"
   105    done
   106  }
   107  #
   108  # Testing Helpers
   109  #
   110  function run_unit_tests() {
   111    go test "${UNIT_FLAGS[@]}" "${UNIT_PACKAGES[@]}" "${FILTER[@]}"
   112  }
   113  
   114  #
   115  # Main CLI Parser
   116  #
   117  USAGE="$(cat -- <<-EOM
   118  
   119  Usage:
   120  Boulder test suite CLI, intended to be run inside of a Docker container:
   121  
   122    docker compose run --use-aliases boulder ./$(basename "${0}") [OPTION]...
   123  
   124  With no options passed, runs standard battery of tests (lint, unit, and integration)
   125  
   126      -l, --lints                           Adds lint to the list of tests to run
   127      -u, --unit                            Adds unit to the list of tests to run
   128      -v, --verbose                         Enables verbose output for unit and integration tests
   129      -w, --unit-without-cache              Disables go test caching for unit tests
   130      -p <DIR>, --unit-test-package=<DIR>   Run unit tests for specific go package(s)
   131      -e, --enable-race-detection           Enables race detection for unit and integration tests
   132      -n, --config-next                     Changes BOULDER_CONFIG_DIR from test/config to test/config-next
   133      -i, --integration                     Adds integration to the list of tests to run
   134      -s, --start-py                        Adds start to the list of tests to run
   135      -g, --generate                        Adds generate to the list of tests to run
   136      -c, --coverage                        Enables coverage for tests
   137      -d <DIR>, --coverage-directory=<DIR>  Directory to store coverage files in
   138                                            Default: test/coverage/<timestamp>
   139      -f <REGEX>, --filter=<REGEX>          Run only those tests matching the regular expression
   140  
   141                                            Note:
   142                                             This option disables the '"back in time"' integration test setup
   143  
   144                                             For tests, the regular expression is split by unbracketed slash (/)
   145                                             characters into a sequence of regular expressions
   146  
   147                                            Example:
   148                                             TestGenerateValidity/TestWFECORS
   149      -h, --help                            Shows this help message
   150      -b  --use-vitess                      Run tests against Vitess + MySQL 8.0 database
   151  
   152  EOM
   153  )"
   154  
   155  while getopts luvwecisgnhbd:p:f:-: OPT; do
   156    if [ "$OPT" = - ]; then     # long option: reformulate OPT and OPTARG
   157      OPT="${OPTARG%%=*}"       # extract long option name
   158      OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
   159      OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
   160    fi
   161    case "$OPT" in
   162      l | lints )                      RUN+=("lints") ;;
   163      u | unit )                       RUN+=("unit") ;;
   164      v | verbose )                    UNIT_FLAGS+=("-v"); INTEGRATION_FLAGS+=("-v") ;;
   165      w | unit-without-cache )         UNIT_FLAGS+=("-count=1") ;;
   166      p | unit-test-package )          check_arg; UNIT_PACKAGES+=("${OPTARG}") ;;
   167      e | enable-race-detection )      RACE="true"; UNIT_FLAGS+=("-race") ;;
   168      i | integration )                RUN+=("integration") ;;
   169      f | filter )                     check_arg; FILTER+=("${OPTARG}") ;;
   170      s | start-py )                   RUN+=("start") ;;
   171      g | generate )                   RUN+=("generate") ;;
   172      n | config-next )                BOULDER_CONFIG_DIR="test/config-next" ;;
   173      c | coverage )                   COVERAGE="true" ;;
   174      d | coverage-dir )               check_arg; COVERAGE_DIR="${OPTARG}" ;;
   175      b | use-vitess )                 USE_VITESS="true" ;;
   176      h | help )                       print_usage_exit ;;
   177      ??* )                            exit_msg "Illegal option --$OPT" ;;  # bad long option
   178      ? )                              exit 2 ;;  # bad short option (error reported via getopts)
   179    esac
   180  done
   181  shift $((OPTIND-1)) # remove parsed options and args from $@ list
   182  
   183  # Defaults to MariaDB unless USE_VITESS is true.
   184  configure_database_endpoints
   185  
   186  # The list of segments to run. Order doesn't matter.
   187  if [ -z "${RUN[@]+x}" ]
   188  then
   189    RUN+=("lints" "unit" "integration")
   190  fi
   191  
   192  # Filter is used by unit and integration but should not be used for both at the same time
   193  if [[ "${RUN[@]}" =~ unit ]] && [[ "${RUN[@]}" =~ integration ]] && [[ -n "${FILTER[@]+x}" ]]
   194  then
   195    exit_msg "Illegal option: (-f, --filter) when specifying both (-u, --unit) and (-i, --integration)"
   196  fi
   197  
   198  # If unit + filter: set correct flags for go test
   199  if [[ "${RUN[@]}" =~ unit ]] && [[ -n "${FILTER[@]+x}" ]]
   200  then
   201    FILTER=(--test.run "${FILTER[@]}")
   202  fi
   203  
   204  # If integration + filter: set correct flags for test/integration-test.py
   205  if [[ "${RUN[@]}" =~ integration ]] && [[ -n "${FILTER[@]+x}" ]]
   206  then
   207    FILTER=(--filter "${FILTER[@]}")
   208  fi
   209  
   210  # If unit test packages are not specified: set flags to run unit tests
   211  # for all boulder packages
   212  if [ -z "${UNIT_PACKAGES[@]+x}" ]
   213  then
   214    # '-p=1' configures unit tests to run serially, rather than in parallel. Our
   215    # unit tests depend on mutating a database and then cleaning up after
   216    # themselves. If these test were run in parallel, they could fail spuriously
   217    # due to one test modifying a table (especially registrations) while another
   218    # test is reading from it.
   219    # https://github.com/letsencrypt/boulder/issues/1499
   220    # https://pkg.go.dev/cmd/go#hdr-Testing_flags
   221    UNIT_FLAGS+=("-p=1")
   222    UNIT_PACKAGES+=("./...")
   223  fi
   224  
   225  print_heading "Boulder Test Suite CLI"
   226  print_heading "Settings:"
   227  
   228  # On EXIT, trap and print outcome
   229  trap "print_outcome" EXIT
   230  
   231  settings="$(cat -- <<-EOM
   232      RUN:                ${RUN[@]}
   233      BOULDER_CONFIG_DIR: $BOULDER_CONFIG_DIR
   234      GOCACHE:            $(go env GOCACHE)
   235      UNIT_PACKAGES:      ${UNIT_PACKAGES[@]}
   236      UNIT_FLAGS:         ${UNIT_FLAGS[@]}
   237      FILTER:             ${FILTER[@]}
   238      COVERAGE:           $COVERAGE
   239      COVERAGE_DIR:       $COVERAGE_DIR
   240      USE_VITESS:         $USE_VITESS
   241  EOM
   242  )"
   243  
   244  if [ "${COVERAGE}" == "true" ]; then
   245    mkdir -p "$COVERAGE_DIR"
   246  fi
   247  
   248  echo "$settings"
   249  print_heading "Starting..."
   250  
   251  #
   252  # Run various linters.
   253  #
   254  STAGE="lints"
   255  if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
   256    print_heading "Running Lints"
   257    golangci-lint run --timeout 9m ./...
   258    python3 test/grafana/lint.py
   259    # Check for common spelling errors using typos.
   260    # Update .typos.toml if you find false positives
   261    run_and_expect_silence typos
   262    # Check test JSON configs are formatted consistently
   263    run_and_expect_silence ./test/format-configs.py 'test/config*/*.json'
   264  fi
   265  
   266  #
   267  # Unit Tests.
   268  #
   269  STAGE="unit"
   270  if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
   271    print_heading "Running Unit Tests"
   272    flush_redis
   273  
   274    if [ "${COVERAGE}" == "true" ]; then
   275      UNIT_CSV=$(IFS=,; echo "${UNIT_PACKAGES[*]}")
   276      UNIT_FLAGS+=("-cover" "-covermode=atomic" "-coverprofile=${COVERAGE_DIR}/unit.coverprofile" "-coverpkg=${UNIT_CSV}")
   277    fi
   278  
   279    run_unit_tests
   280  fi
   281  
   282  #
   283  # Integration tests
   284  #
   285  STAGE="integration"
   286  if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
   287    print_heading "Running Integration Tests"
   288    flush_redis
   289  
   290    # Set up test parameters
   291    INTEGRATION_ARGS=("--chisel")
   292  
   293    # Add verbose flag if requested
   294    if [[ "${INTEGRATION_FLAGS[@]}" =~ "-v" ]] ; then
   295      INTEGRATION_ARGS+=("--gotestverbose")
   296    else
   297      INTEGRATION_ARGS+=("--gotest")
   298    fi
   299  
   300    # Add coverage settings if enabled
   301    if [ "${COVERAGE}" == "true" ]; then
   302      INTEGRATION_ARGS+=("--coverage" "--coverage-dir=${COVERAGE_DIR}")
   303    fi
   304  
   305    # Add any filters
   306    INTEGRATION_ARGS+=("${FILTER[@]}")
   307  
   308    # Run the integration tests with all collected arguments
   309    python3 test/integration-test.py "${INTEGRATION_ARGS[@]}"
   310  fi
   311  
   312  # Test that just ./start.py works, which is a proxy for testing that
   313  # `docker compose up` works, since that just runs start.py (via entrypoint.sh).
   314  STAGE="start"
   315  if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
   316    print_heading "Running Start Test"
   317    python3 start.py &
   318    for I in {1..115}; do
   319      sleep 1
   320      curl -s http://localhost:4001/directory && echo "Boulder took ${I} seconds to come up" && break
   321    done
   322    if [ "${I}" -eq 115 ]; then
   323      echo "Boulder did not come up after ${I} seconds during ./start.py."
   324      exit 1
   325    fi
   326  fi
   327  
   328  # Run generate to make sure all our generated code can be re-generated with
   329  # current tools.
   330  # Note: Some of the tools we use seemingly don't understand ./vendor yet, and
   331  # so will fail if imports are not available in $GOPATH.
   332  STAGE="generate"
   333  if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then
   334    print_heading "Running Generate"
   335    # Additionally, we need to run go install before go generate because the stringer command
   336    # (using in ./grpc/) checks imports, and depends on the presence of a built .a
   337    # file to determine an import really exists. See
   338    # https://golang.org/src/go/internal/gcimporter/gcimporter.go#L30
   339    # Without this, we get error messages like:
   340    #   stringer: checking package: grpc/bcodes.go:6:2: could not import
   341    #     github.com/letsencrypt/boulder/probs (can't find import:
   342    #     github.com/letsencrypt/boulder/probs)
   343    go install ./probs
   344    go install ./vendor/google.golang.org/grpc/codes
   345    run_and_expect_silence go generate ./...
   346    run_and_expect_silence git diff --exit-code .
   347  fi
   348  
   349  # Because set -e stops execution in the instance of a command or pipeline
   350  # error; if we got here we assume success
   351  STATUS="SUCCESS"