okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f6dbf0b
main
1"""Run all E2E unittest suites, one file, or one test by dotted identifier. 2 3Tests share one concurrent daemon; each ``@exclusive`` test gets a daemon of its 4own, discarded when it ends, and a few of them run at a time beside the shared 5ones. Both queues start with the tests that took longest last time, so the 6slowest never start last. 7""" 8 9import argparse 10from concurrent .futures import ThreadPoolExecutor 11import fcntl 12import io 13import json 14import os 15from pathlib import Path 16import resource 17import sys 18import time 19import unittest 20 21import harness 22import scratch 23 24SUITE_DIR = Path (__file__ ).parent 25sys .path .insert (0 ,str (SUITE_DIR )) 26sys .path .insert (0 ,str (SUITE_DIR .parent )) 27from python_test_runner import suite_for # noqa: E402 28 29DURATIONS = scratch .ROOT / "e2e-durations.json" 30 31 32def cases (suite ): 33for item in suite : 34if isinstance (item ,unittest .TestSuite ): 35yield from cases (item ) 36else : 37yield item 38 39 40def marked (test ,mark ): 41method = getattr (test ,test ._testMethodName ) 42return getattr (method ,mark ,False )or getattr (type (test ),mark ,False ) 43 44 45def execute (test ,daemon ): 46log = daemon .home / "daemon.log" 47offset = log .stat ().st_size if log .exists ()else 0 48stream = io .StringIO () 49started = time .monotonic () 50with harness .on_daemon (daemon ): 51result = unittest .TextTestRunner (stream = stream ,verbosity = 2 ).run (test ) 52duration = time .monotonic ()- started 53if not result .wasSuccessful ()and log .exists (): 54# The daemon's side of a failure; the shared daemon's log interleaves 55# the tests that ran alongside this one. 56with log .open ("rb" )as daemon_log : 57daemon_log .seek (offset ) 58setattr ( 59result ,"daemon_log" ,daemon_log .read ()[- 8000 :].decode (errors = "replace" ) 60 ) 61return test ,result ,duration 62 63 64def execute_alone (test ): 65daemon = harness .Daemon ("exclusive" ) 66try : 67return execute (test ,daemon ) 68finally : 69daemon .discard () 70 71 72def run_suite (suite ,jobs ,exclusive_jobs ): 73if suite .countTestCases ()== 0 : 74raise ValueError ("no E2E tests collected" ) 75try : 76durations = json .loads (DURATIONS .read_text ()) 77except (OSError ,ValueError ): 78durations = {} 79selected = sorted (cases (suite ),key = lambda test :- durations .get (test .id (),0 )) 80exclusive = [test for test in selected if marked (test ,"_e2e_exclusive" )] 81shared = [test for test in selected if test not in exclusive ] 82daemon = harness .Daemon ("shared" ,concurrent = True )if shared else None 83if daemon : 84daemon .boot () 85daemon .install_shared_features () 86with ( 87ThreadPoolExecutor (max_workers = exclusive_jobs )as exclusive_pool , 88ThreadPoolExecutor (max_workers = jobs )as shared_pool , 89 ): 90alone = [exclusive_pool .submit (execute_alone ,test )for test in exclusive ] 91together = [shared_pool .submit (execute ,test ,daemon )for test in shared ] 92outcomes = [future .result ()for future in together + alone ] 93report (outcomes ) 94durations .update ({test .id ():duration for test ,_ ,duration in outcomes }) 95DURATIONS .write_text (json .dumps (durations ,indent = 1 ,sort_keys = True )) 96return all (result .wasSuccessful ()for _ ,result ,_ in outcomes ) 97 98 99def report (outcomes ): 100for test ,result ,duration in outcomes : 101status = "ok" if result .wasSuccessful ()else "FAILED" 102f" { status :6 } { test . id () } ( { duration :.1f } s)" ,file = sys .stderr ) 103for test ,result ,_duration in outcomes : 104for _case ,traceback in result .failures + result .errors : 105f"\n { test . id () } :\n { traceback } " ,file = sys .stderr ) 106if getattr (result ,"daemon_log" ,"" ): 107f"daemon log during { test . id () } :\n { result . daemon_log } " ,file = sys .stderr 109 ) 110"slowest tests:" ,file = sys .stderr ) 111for test ,_result ,duration in sorted (outcomes ,key = lambda value :- value [2 ])[:10 ]: 112f" { duration :.1f } s { test . id () } " ,file = sys .stderr ) 113failing = sum (not result .wasSuccessful ()for _ ,result ,_ in outcomes ) 114f"Ran { len ( outcomes ) } tests; { failing } failing" ,file = sys .stderr ) 115 116 117def main (): 118# Concurrent tests hold hundreds of connections between them, past the 256 119# open files a macOS shell starts with. Like Go, lift the soft limit, to at 120# most macOS's OPEN_MAX. Daemons boot on it unless a test pins their own. 121_ ,hard = resource .getrlimit (resource .RLIMIT_NOFILE ) 122resource .setrlimit (resource .RLIMIT_NOFILE , (min (hard ,10240 ),hard )) 123cpus = os .cpu_count ()or 1 124parser = argparse .ArgumentParser () 125parser .add_argument ( 126"target" ,nargs = "?" ,help = "file.py, file:TestCase.test_method, or area" 127 ) 128parser .add_argument ( 129"-j" , 130"--jobs" , 131type = int , 132default = max (1 ,min (4 ,cpus // 4 )), 133help = "concurrent tests on the shared lane (default: %(default)s)" , 134 ) 135parser .add_argument ( 136"-x" , 137"--exclusive-jobs" , 138type = int , 139default = max (1 ,min (8 ,cpus // 2 )), 140help = "concurrent exclusive daemons (default: %(default)s)" , 141 ) 142args = parser .parse_args () 143if args .target : 144path = SUITE_DIR / args .target 145if path .is_file (): 146suite = suite_for (path ) 147else : 148file_part ,_ ,test_name = args .target .partition (":" ) 149path = SUITE_DIR / ( 150file_part if file_part .endswith (".py" )else file_part + "_test.py" 151 ) 152suite = suite_for (path ,test_name or None ) 153else : 154suite = unittest .TestSuite ( 155suite_for (path )for path in sorted (SUITE_DIR .glob ("*_test.py" )) 156 ) 157if suite .countTestCases ()== 0 : 158parser .error ("no E2E tests collected" ) 159# Concurrent E2E runs compete for the daemons and kernels on this laptop. 160with open (scratch .ROOT / "e2e.lock" ,"w" )as lock : 161fcntl .flock (lock ,fcntl .LOCK_EX ) 162started = time .monotonic () 163try : 164harness .snapshot_daemon () 165successful = run_suite ( 166suite ,max (1 ,args .jobs ),max (1 ,args .exclusive_jobs ) 167 ) 168finally : 169harness .shutdown () 170f"daemon boots: { harness . daemon_boots } ; restart time: { harness . restart_seconds :.1f } s; wall time: { time . monotonic () - started :.1f } s" , 172flush = True , 173 ) 174return 0 if successful else 1 175 176 177if __name__ == "__main__" : 178sys .exit (main ())