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()