github.com/letsencrypt/boulder@v0.20251208.0/test/integration-test.py (about)

     1  #!/usr/bin/env python3
     2  # -*- coding: utf-8 -*-
     3  """
     4  This file contains basic infrastructure for running the integration test cases.
     5  Most test cases are in v2_integration.py. There are a few exceptions: Test cases
     6  that don't test either the v1 or v2 API are in this file, and test cases that
     7  have to run at a specific point in the cycle (e.g. after all other test cases)
     8  are also in this file.
     9  """
    10  import argparse
    11  import datetime
    12  import inspect
    13  import os
    14  import re
    15  import subprocess
    16  
    17  import requests
    18  import startservers
    19  import v2_integration
    20  from helpers import *
    21  
    22  # Set the environment variable RACE to anything other than 'true' to disable
    23  # race detection. This significantly speeds up integration testing cycles
    24  # locally.
    25  race_detection = True
    26  if os.environ.get('RACE', 'true') != 'true':
    27      race_detection = False
    28  
    29  def run_go_tests(filterPattern=None,verbose=False):
    30      """
    31      run_go_tests launches the Go integration tests. The go test command must
    32      return zero or an exception will be raised. If the filterPattern is provided
    33      it is used as the value of the `--test.run` argument to the go test command.
    34      """
    35      cmdLine = ["go", "test"]
    36      if filterPattern is not None and filterPattern != "":
    37          cmdLine = cmdLine + ["--test.run", filterPattern]
    38      cmdLine = cmdLine + ["-tags", "integration", "-count=1", "-race"]
    39      if verbose:
    40          cmdLine = cmdLine + ["-v"]
    41      cmdLine = cmdLine +  ["./test/integration"]
    42      subprocess.check_call(cmdLine, stderr=subprocess.STDOUT)
    43  
    44  exit_status = 1
    45  
    46  def main():
    47      parser = argparse.ArgumentParser(description='Run integration tests')
    48      parser.add_argument('--chisel', dest="run_chisel", action="store_true",
    49                          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")
    55      parser.add_argument('--gotest', dest="run_go", action="store_true",
    56                          help="run Go integration tests")
    57      parser.add_argument('--gotestverbose', dest="run_go_verbose", action="store_true",
    58                          help="run Go integration tests with verbose output")
    59      parser.add_argument('--filter', dest="test_case_filter", action="store",
    60                          help="Regex filter for test cases")
    61      # allow any ACME client to run custom command for integration
    62      # testing (without having to implement its own busy-wait loop)
    63      parser.add_argument('--custom', metavar="CMD", help="run custom command")
    64      parser.set_defaults(run_chisel=False, test_case_filter="", skip_setup=False, coverage=False, coverage_dir=None)
    65      args = parser.parse_args()
    66  
    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  
    73      if not (args.run_chisel or args.custom  or args.run_go is not None):
    74          raise(Exception("must run at least one of the letsencrypt or chisel tests with --chisel, --gotest, or --custom"))
    75  
    76      if not startservers.install(race_detection=race_detection, coverage=args.coverage):
    77          raise(Exception("failed to build"))
    78  
    79      if not startservers.start(coverage_dir=args.coverage_dir):
    80          raise(Exception("startservers failed"))
    81  
    82      if args.run_chisel:
    83          run_chisel(args.test_case_filter)
    84  
    85      if args.run_go:
    86          run_go_tests(args.test_case_filter, False)
    87  
    88      if args.run_go_verbose:
    89          run_go_tests(args.test_case_filter, True)
    90  
    91      if args.custom:
    92          run(args.custom.split())
    93  
    94      # Skip the last-phase checks when the test case filter is one, because that
    95      # means we want to quickly iterate on a single test case.
    96      if not args.test_case_filter:
    97          run_cert_checker()
    98          check_balance()
    99  
   100      # If coverage is enabled, process the coverage data
   101      if args.coverage:
   102          process_covdata(args.coverage_dir)
   103  
   104      if not startservers.check():
   105          raise(Exception("startservers.check failed"))
   106  
   107      global exit_status
   108      exit_status = 0
   109  
   110  def run_chisel(test_case_filter):
   111      for key, value in inspect.getmembers(v2_integration):
   112        if callable(value) and key.startswith('test_') and re.search(test_case_filter, key):
   113          value()
   114      for key, value in globals().items():
   115        if callable(value) and key.startswith('test_') and re.search(test_case_filter, key):
   116          value()
   117  
   118  def check_balance():
   119      """Verify that gRPC load balancing across backends is working correctly.
   120  
   121      Fetch metrics from each backend and ensure the grpc_server_handled_total
   122      metric is present, which means that backend handled at least one request.
   123      """
   124      addresses = [
   125          "localhost:8003", # SA
   126          "localhost:8103", # SA
   127          "localhost:8009", # publisher
   128          "localhost:8109", # publisher
   129          "localhost:8004", # VA
   130          "localhost:8104", # VA
   131          "localhost:8001", # CA
   132          "localhost:8101", # CA
   133          "localhost:8002", # RA
   134          "localhost:8102", # RA
   135      ]
   136      for address in addresses:
   137          metrics = requests.get("http://%s/metrics" % address)
   138          if "grpc_server_handled_total" not in metrics.text:
   139              raise(Exception("no gRPC traffic processed by %s; load balancing problem?")
   140                  % address)
   141  
   142  def run_cert_checker():
   143      run(["./bin/boulder", "cert-checker", "-config", "%s/cert-checker.json" % config_dir])
   144  
   145  def process_covdata(coverage_dir):
   146      """Process coverage data and generate reports."""
   147      if not os.path.exists(coverage_dir):
   148          raise(Exception("Coverage directory does not exist: %s" % coverage_dir))
   149  
   150      # Generate text report
   151      coverage_dir = os.path.abspath(coverage_dir)
   152      cov_text = os.path.join(coverage_dir, "integration.coverprofile")
   153      # this works, but if it takes a long time consider merging with `go tool covdata merge` first
   154      # https://go.dev/blog/integration-test-coverage#merging-raw-profiles-with-go-tool-covdata-merge
   155      run(["go", "tool", "covdata", "textfmt", "-i", coverage_dir, "-o", cov_text])
   156  
   157  if __name__ == "__main__":
   158      main()