40
40
import csv
41
41
import gzip
42
42
import os
43
+ import signal
43
44
import re
44
45
import html
46
+ import time
45
47
import subprocess
46
48
from collections import defaultdict
47
49
from json import dumps
48
50
from multiprocessing import Pool , TimeoutError
49
51
from pprint import pformat
50
52
53
+
51
54
import argparse
52
55
import sys
53
56
from time import gmtime , strftime
@@ -125,29 +128,44 @@ def file_name(name, current_date_time):
125
128
return '{}-{}{}' .format (name [:idx ], current_date_time , name [idx :])
126
129
return '{}-{}' .format (name , current_date_time )
127
130
131
+ def get_tail (output , count = 15 ):
132
+ lines = output .split ("\n " )
133
+ start = max (0 , len (lines ) - count )
134
+ return '\n ' .join (lines [start :])
135
+
136
+ TIMEOUT = 60 * 20 # 20 mins per unittest wait time max ...
128
137
129
138
# ----------------------------------------------------------------------------------------------------------------------
130
139
#
131
140
# exec utils
132
141
#
133
142
# ----------------------------------------------------------------------------------------------------------------------
134
- def _run_cmd (cmd , capture_on_failure = True ):
143
+ def _run_cmd (cmd , timeout = TIMEOUT , capture_on_failure = True ):
135
144
if isinstance (cmd , str ):
136
145
cmd = cmd .split (" " )
137
146
assert isinstance (cmd , (list , tuple ))
138
147
139
- log ("[EXEC] cmd: {} ..." .format (' ' .join (cmd )))
140
- success = True
141
- output = None
148
+ cmd_string = ' ' .join (cmd )
149
+ log ("[EXEC] starting '{}' ..." .format (cmd_string ))
142
150
151
+ start_time = time .monotonic ()
152
+ # os.setsid is used to create a process group, to be able to call os.killpg upon timeout
153
+ proc = subprocess .Popen (cmd , preexec_fn = os .setsid , stdout = subprocess .PIPE , stderr = subprocess .STDOUT )
143
154
try :
144
- output = subprocess .check_output (cmd , stderr = subprocess .STDOUT )
145
- except subprocess .CalledProcessError as e :
146
- log ("[ERR] Could not execute CMD. Reason: {}" .format (e ))
147
- if capture_on_failure :
148
- output = e .output
149
-
150
- return success , output .decode ("utf-8" , "ignore" )
155
+ output = proc .communicate (timeout = timeout )[0 ]
156
+ except subprocess .TimeoutExpired as e :
157
+ delta = time .monotonic () - start_time
158
+ os .killpg (proc .pid , signal .SIGKILL )
159
+ output = proc .communicate ()[0 ]
160
+ msg = "TimeoutExpired: {:.3f}s" .format (delta )
161
+ tail = get_tail (output .decode ('utf-8' , 'ignore' ))
162
+ log ("[ERR] timeout '{}' after {:.3f}s, killing process group {}, last lines of output:\n {}\n {}" , cmd_string , delta , proc .pid , tail , HR )
163
+ else :
164
+ delta = time .monotonic () - start_time
165
+ log ("[EXEC] finished '{}' with exit code {} in {:.3f}s" , cmd_string , proc .returncode , delta )
166
+ msg = "Finished: {:.3f}s" .format (delta )
167
+
168
+ return proc .returncode == 0 , output .decode ("utf-8" , "ignore" ) + "\n " + msg
151
169
152
170
153
171
def scp (results_file_path , destination_path , destination_name = None ):
@@ -157,54 +175,42 @@ def scp(results_file_path, destination_path, destination_name=None):
157
175
return _run_cmd (cmd )[0 ]
158
176
159
177
160
- def _run_unittest (test_path , with_cpython = False ):
178
+ def _run_unittest (test_path , timeout , with_cpython = False ):
161
179
if with_cpython :
162
180
cmd = ["python3" , test_path , "-v" ]
163
181
else :
164
182
cmd = ["mx" , "python3" , "--python.CatchAllExceptions=true" , test_path , "-v" ]
165
- success , output = _run_cmd (cmd )
183
+ output = _run_cmd (cmd , timeout )[ 1 ]
166
184
output = '''
167
185
##############################################################
168
186
#### running: {}
169
187
''' .format (test_path ) + output
170
- return success , output
171
-
188
+ return output
172
189
173
- TIMEOUT = 60 * 20 # 20 mins per unittest wait time max ...
174
190
175
191
176
192
def run_unittests (unittests , timeout , with_cpython = False ):
177
193
assert isinstance (unittests , (list , tuple ))
178
194
num_unittests = len (unittests )
179
195
log ("[EXEC] running {} unittests ... " , num_unittests )
180
196
log ("[EXEC] timeout per unittest: {} seconds" , timeout )
181
- results = []
182
197
183
- pool = Pool ()
198
+ start_time = time .monotonic ()
199
+ pool = Pool (processes = (os .cpu_count () // 4 ) or 1 ) # to account for hyperthreading and some additional overhead
200
+
201
+ out = []
202
+ def callback (result ):
203
+ out .append (result )
204
+ log ("[PROGRESS] {} / {}: \t {:.1f}%" , len (out ), num_unittests , len (out ) * 100 / num_unittests )
205
+
206
+ # schedule all unittest runs
184
207
for ut in unittests :
185
- results .append (pool .apply_async (_run_unittest , args = (ut , with_cpython )))
208
+ pool .apply_async (_run_unittest , args = (ut , timeout , with_cpython ), callback = callback )
209
+
186
210
pool .close ()
187
-
188
- log ("[INFO] collect results ... " )
189
- out = []
190
- timed_out = []
191
- for i , res in enumerate (results ):
192
- try :
193
- _ , output = res .get (timeout )
194
- out .append (output )
195
- except TimeoutError :
196
- log ("[ERR] timeout while getting results for {}, skipping!" , unittests [i ])
197
- timed_out .append (unittests [i ])
198
- log ("[PROGRESS] {} / {}: \t {}%" , i + 1 , num_unittests , int (((i + 1 ) * 100.0 ) / num_unittests ))
199
-
200
- if timed_out :
201
- log (HR )
202
- for t in timed_out :
203
- log ("[TIMEOUT] skipped: {}" , t )
204
- log (HR )
205
- log ("[STATS] processed {} out of {} unittests" , num_unittests - len (timed_out ), num_unittests )
206
- pool .terminate ()
207
211
pool .join ()
212
+ pool .terminate ()
213
+ log ("[STATS] processed {} unittests in {:.3f}s" , num_unittests , time .monotonic () - start_time )
208
214
return out
209
215
210
216
@@ -346,11 +352,12 @@ def process_output(output_lines):
346
352
match = re .match (PTRN_ERROR , line )
347
353
if match :
348
354
error_message = (match .group ('error' ), match .group ('message' ))
349
- error_message_dict = error_messages [unittests [- 1 ]]
350
- d = error_message_dict .get (error_message )
351
- if not d :
352
- d = 0
353
- error_message_dict [error_message ] = d + 1
355
+ if not error_message [0 ] == 'Directory' and not error_message [0 ] == 'Components' :
356
+ error_message_dict = error_messages [unittests [- 1 ]]
357
+ d = error_message_dict .get (error_message )
358
+ if not d :
359
+ d = 0
360
+ error_message_dict [error_message ] = d + 1
354
361
continue
355
362
356
363
# extract java exceptions
@@ -521,6 +528,8 @@ def save_as_csv(report_path, unittests, error_messages, java_exceptions, stats,
521
528
unittest_errmsg = error_messages [unittest ]
522
529
if not unittest_errmsg :
523
530
unittest_errmsg = java_exceptions [unittest ]
531
+ if not unittest_errmsg :
532
+ unittest_errmsg = {}
524
533
525
534
rows .append ({
526
535
Col .UNITTEST : unittest ,
@@ -867,7 +876,7 @@ def main(prog, args):
867
876
action = "store_true" )
868
877
parser .add_argument ("-l" , "--limit" , help = "Limit the number of unittests to run." , default = None , type = int )
869
878
parser .add_argument ("-t" , "--tests_path" , help = "Unittests path." , default = PATH_UNITTESTS )
870
- parser .add_argument ("-T" , "--timeout" , help = "Timeout per unittest run." , default = TIMEOUT , type = int )
879
+ parser .add_argument ("-T" , "--timeout" , help = "Timeout per unittest run (seconds) ." , default = TIMEOUT , type = int )
871
880
parser .add_argument ("-o" , "--only_tests" , help = "Run only these unittests (comma sep values)." , default = None )
872
881
parser .add_argument ("-s" , "--skip_tests" , help = "Run all unittests except (comma sep values)."
873
882
"the only_tets option takes precedence" , default = None )
0 commit comments