Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Implements a simple "negative compile" test for C++ on linux. | 6 """Implements a simple "negative compile" test for C++ on linux. |
| 7 | 7 |
| 8 Sometimes a C++ API needs to ensure that various usages cannot compile. To | 8 Sometimes a C++ API needs to ensure that various usages cannot compile. To |
| 9 enable unittesting of these assertions, we use this python script to | 9 enable unittesting of these assertions, we use this python script to |
| 10 invoke gcc on a source file and assert that compilation fails. | 10 invoke gcc on a source file and assert that compilation fails. |
| 11 | 11 |
| 12 For more info, see: | 12 For more info, see: |
| 13 http://dev.chromium.org/developers/testing/no-compile-tests | 13 http://dev.chromium.org/developers/testing/no-compile-tests |
| 14 """ | 14 """ |
| 15 | 15 |
| 16 import StringIO | 16 import StringIO |
| 17 import ast | 17 import ast |
| 18 import locale | |
| 19 import os | 18 import os |
| 20 import re | 19 import re |
| 21 import select | 20 import select |
| 22 import shlex | 21 import shlex |
| 23 import subprocess | 22 import subprocess |
| 24 import sys | 23 import sys |
| 25 import time | 24 import time |
| 26 | 25 |
| 27 | 26 |
| 28 # Matches lines that start with #if and have the substring TEST in the | 27 # Matches lines that start with #if and have the substring TEST in the |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 59 RESULT_FILE_HEADER = """ | 58 RESULT_FILE_HEADER = """ |
| 60 // This file is generated by the no compile test from: | 59 // This file is generated by the no compile test from: |
| 61 // %s | 60 // %s |
| 62 | 61 |
| 63 #include "base/logging.h" | 62 #include "base/logging.h" |
| 64 #include "testing/gtest/include/gtest/gtest.h" | 63 #include "testing/gtest/include/gtest/gtest.h" |
| 65 | 64 |
| 66 """ | 65 """ |
| 67 | 66 |
| 68 | 67 |
| 69 # The GUnit test function to output on a successful test completion. | 68 # The log message on a test completion. |
| 70 SUCCESS_GUNIT_TEMPLATE = """ | 69 LOG_TEMPLATE = """ |
| 71 TEST(%s, %s) { | 70 TEST(%s, %s) took %f secs. Started at %f, ended at %f. |
| 72 LOG(INFO) << "Took %f secs. Started at %f, ended at %f"; | |
| 73 } | |
| 74 """ | 71 """ |
| 75 | 72 |
| 76 # The GUnit test function to output for a disabled test. | 73 # The GUnit test function to output for a successful or disabled test. |
|
dcheng
2017/01/19 05:36:22
Nit: probably OK to just remove "successful or dis
wychen
2017/01/19 15:43:57
But failed tests wouldn't use this template.
| |
| 77 DISABLED_GUNIT_TEMPLATE = """ | 74 GUNIT_TEMPLATE = """ |
| 78 TEST(%s, %s) { } | 75 TEST(%s, %s) { } |
| 79 """ | 76 """ |
| 80 | 77 |
| 81 | 78 |
| 82 # Timeout constants. | 79 # Timeout constants. |
| 83 NCTEST_TERMINATE_TIMEOUT_SEC = 60 | 80 NCTEST_TERMINATE_TIMEOUT_SEC = 60 |
| 84 NCTEST_KILL_TIMEOUT_SEC = NCTEST_TERMINATE_TIMEOUT_SEC + 2 | 81 NCTEST_KILL_TIMEOUT_SEC = NCTEST_TERMINATE_TIMEOUT_SEC + 2 |
| 85 BUSY_LOOP_MAX_TIME_SEC = NCTEST_KILL_TIMEOUT_SEC * 2 | 82 BUSY_LOOP_MAX_TIME_SEC = NCTEST_KILL_TIMEOUT_SEC * 2 |
| 86 | 83 |
| 87 | 84 |
| (...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 234 'name': name, | 231 'name': name, |
| 235 'suite_name': config['suite_name'], | 232 'suite_name': config['suite_name'], |
| 236 'terminate_timeout': now + NCTEST_TERMINATE_TIMEOUT_SEC, | 233 'terminate_timeout': now + NCTEST_TERMINATE_TIMEOUT_SEC, |
| 237 'kill_timeout': now + NCTEST_KILL_TIMEOUT_SEC, | 234 'kill_timeout': now + NCTEST_KILL_TIMEOUT_SEC, |
| 238 'started_at': now, | 235 'started_at': now, |
| 239 'aborted_at': 0, | 236 'aborted_at': 0, |
| 240 'finished_at': 0, | 237 'finished_at': 0, |
| 241 'expectations': expectations} | 238 'expectations': expectations} |
| 242 | 239 |
| 243 | 240 |
| 244 def PassTest(resultfile, test): | 241 def PassTest(resultfile, resultlog, test): |
| 245 """Logs the result of a test started by StartTest(), or a disabled test | 242 """Logs the result of a test started by StartTest(), or a disabled test |
| 246 configuration. | 243 configuration. |
| 247 | 244 |
| 248 Args: | 245 Args: |
| 249 resultfile: File object for .cc file that results are written to. | 246 resultfile: File object for .cc file that results are written to. |
| 247 resultlog: File object for the log file. | |
| 250 test: An instance of the dictionary returned by StartTest(), a | 248 test: An instance of the dictionary returned by StartTest(), a |
| 251 configuration from ExtractTestConfigs(). | 249 configuration from ExtractTestConfigs(). |
| 252 """ | 250 """ |
| 251 resultfile.write(GUNIT_TEMPLATE % ( | |
| 252 test['suite_name'], test['name'])) | |
| 253 | |
| 253 # The 'started_at' key is only added if a test has been started. | 254 # The 'started_at' key is only added if a test has been started. |
| 254 if 'started_at' in test: | 255 if 'started_at' in test: |
| 255 resultfile.write(SUCCESS_GUNIT_TEMPLATE % ( | 256 resultlog.write(LOG_TEMPLATE % ( |
| 256 test['suite_name'], test['name'], | 257 test['suite_name'], test['name'], |
| 257 test['finished_at'] - test['started_at'], | 258 test['finished_at'] - test['started_at'], |
| 258 test['started_at'], test['finished_at'])) | 259 test['started_at'], test['finished_at'])) |
| 259 else: | |
| 260 resultfile.write(DISABLED_GUNIT_TEMPLATE % ( | |
| 261 test['suite_name'], test['name'])) | |
| 262 | 260 |
| 263 | 261 |
| 264 def FailTest(resultfile, test, error, stdout=None, stderr=None): | 262 def FailTest(resultfile, test, error, stdout=None, stderr=None): |
| 265 """Logs the result of a test started by StartTest() | 263 """Logs the result of a test started by StartTest() |
| 266 | 264 |
| 267 Args: | 265 Args: |
| 268 resultfile: File object for .cc file that results are written to. | 266 resultfile: File object for .cc file that results are written to. |
| 269 test: An instance of the dictionary returned by StartTest() | 267 test: An instance of the dictionary returned by StartTest() |
| 270 error: The printable reason for the failure. | 268 error: The printable reason for the failure. |
| 271 stdout: The test's output to stdout. | 269 stdout: The test's output to stdout. |
| 272 stderr: The test's output to stderr. | 270 stderr: The test's output to stderr. |
| 273 """ | 271 """ |
| 274 resultfile.write('#error "%s Failed: %s"\n' % (test['name'], error)) | 272 resultfile.write('#error "%s Failed: %s"\n' % (test['name'], error)) |
| 275 resultfile.write('#error "compile line: %s"\n' % test['cmdline']) | 273 resultfile.write('#error "compile line: %s"\n' % test['cmdline']) |
| 276 if stdout and len(stdout) != 0: | 274 if stdout and len(stdout) != 0: |
| 277 resultfile.write('#error "%s stdout:"\n' % test['name']) | 275 resultfile.write('#error "%s stdout:"\n' % test['name']) |
| 278 for line in stdout.split('\n'): | 276 for line in stdout.split('\n'): |
| 279 resultfile.write('#error " %s:"\n' % line) | 277 resultfile.write('#error " %s:"\n' % line) |
| 280 | 278 |
| 281 if stderr and len(stderr) != 0: | 279 if stderr and len(stderr) != 0: |
| 282 resultfile.write('#error "%s stderr:"\n' % test['name']) | 280 resultfile.write('#error "%s stderr:"\n' % test['name']) |
| 283 for line in stderr.split('\n'): | 281 for line in stderr.split('\n'): |
| 284 resultfile.write('#error " %s"\n' % line) | 282 resultfile.write('#error " %s"\n' % line) |
| 285 resultfile.write('\n') | 283 resultfile.write('\n') |
| 286 | 284 |
| 287 | 285 |
| 288 def WriteStats(resultfile, suite_name, timings): | 286 def WriteStats(resultlog, suite_name, timings): |
| 289 """Logs the peformance timings for each stage of the script into a fake test. | 287 """Logs the peformance timings for each stage of the script. |
| 290 | 288 |
| 291 Args: | 289 Args: |
| 292 resultfile: File object for .cc file that results are written to. | 290 resultlog: File object for the log file. |
| 293 suite_name: The name of the GUnit suite this test belongs to. | 291 suite_name: The name of the GUnit suite this test belongs to. |
| 294 timings: Dictionary with timestamps for each stage of the script run. | 292 timings: Dictionary with timestamps for each stage of the script run. |
| 295 """ | 293 """ |
| 296 stats_template = ("Started %f, Ended %f, Total %fs, Extract %fs, " | 294 stats_template = """ |
| 297 "Compile %fs, Process %fs") | 295 TEST(%s): Started %f, Ended %f, Total %fs, Extract %fs, Compile %fs, Process %fs |
| 296 """ | |
| 298 total_secs = timings['results_processed'] - timings['started'] | 297 total_secs = timings['results_processed'] - timings['started'] |
| 299 extract_secs = timings['extract_done'] - timings['started'] | 298 extract_secs = timings['extract_done'] - timings['started'] |
| 300 compile_secs = timings['compile_done'] - timings['extract_done'] | 299 compile_secs = timings['compile_done'] - timings['extract_done'] |
| 301 process_secs = timings['results_processed'] - timings['compile_done'] | 300 process_secs = timings['results_processed'] - timings['compile_done'] |
| 302 resultfile.write('TEST(%s, Stats) { LOG(INFO) << "%s"; }\n' % ( | 301 resultlog.write(stats_template % ( |
| 303 suite_name, stats_template % ( | 302 suite_name, timings['started'], timings['results_processed'], total_secs, |
| 304 timings['started'], timings['results_processed'], total_secs, | 303 extract_secs, compile_secs, process_secs)) |
| 305 extract_secs, compile_secs, process_secs))) | |
| 306 | 304 |
| 307 | 305 |
| 308 def ProcessTestResult(resultfile, test): | 306 def ProcessTestResult(resultfile, resultlog, test): |
| 309 """Interprets and logs the result of a test started by StartTest() | 307 """Interprets and logs the result of a test started by StartTest() |
| 310 | 308 |
| 311 Args: | 309 Args: |
| 312 resultfile: File object for .cc file that results are written to. | 310 resultfile: File object for .cc file that results are written to. |
| 311 resultlog: File object for the log file. | |
| 313 test: The dictionary from StartTest() to process. | 312 test: The dictionary from StartTest() to process. |
| 314 """ | 313 """ |
| 315 # Snap a copy of stdout and stderr into the test dictionary immediately | 314 # Snap a copy of stdout and stderr into the test dictionary immediately |
| 316 # cause we can only call this once on the Popen object, and lots of stuff | 315 # cause we can only call this once on the Popen object, and lots of stuff |
| 317 # below will want access to it. | 316 # below will want access to it. |
| 318 proc = test['proc'] | 317 proc = test['proc'] |
| 319 (stdout, stderr) = proc.communicate() | 318 (stdout, stderr) = proc.communicate() |
| 320 | 319 |
| 321 if test['aborted_at'] != 0: | 320 if test['aborted_at'] != 0: |
| 322 FailTest(resultfile, test, "Compile timed out. Started %f ended %f." % | 321 FailTest(resultfile, test, "Compile timed out. Started %f ended %f." % |
| 323 (test['started_at'], test['aborted_at'])) | 322 (test['started_at'], test['aborted_at'])) |
| 324 return | 323 return |
| 325 | 324 |
| 326 if proc.poll() == 0: | 325 if proc.poll() == 0: |
| 327 # Handle failure due to successful compile. | 326 # Handle failure due to successful compile. |
| 328 FailTest(resultfile, test, | 327 FailTest(resultfile, test, |
| 329 'Unexpected successful compilation.', | 328 'Unexpected successful compilation.', |
| 330 stdout, stderr) | 329 stdout, stderr) |
| 331 return | 330 return |
| 332 else: | 331 else: |
| 333 # Check the output has the right expectations. If there are no | 332 # Check the output has the right expectations. If there are no |
| 334 # expectations, then we just consider the output "matched" by default. | 333 # expectations, then we just consider the output "matched" by default. |
| 335 if len(test['expectations']) == 0: | 334 if len(test['expectations']) == 0: |
| 336 PassTest(resultfile, test) | 335 PassTest(resultfile, resultlog, test) |
| 337 return | 336 return |
| 338 | 337 |
| 339 # Otherwise test against all expectations. | 338 # Otherwise test against all expectations. |
| 340 for regexp in test['expectations']: | 339 for regexp in test['expectations']: |
| 341 if (regexp.search(stdout) is not None or | 340 if (regexp.search(stdout) is not None or |
| 342 regexp.search(stderr) is not None): | 341 regexp.search(stderr) is not None): |
| 343 PassTest(resultfile, test) | 342 PassTest(resultfile, resultlog, test) |
| 344 return | 343 return |
| 345 expectation_str = ', '.join( | 344 expectation_str = ', '.join( |
| 346 ["r'%s'" % regexp.pattern for regexp in test['expectations']]) | 345 ["r'%s'" % regexp.pattern for regexp in test['expectations']]) |
| 347 FailTest(resultfile, test, | 346 FailTest(resultfile, test, |
| 348 'Expectations [%s] did not match output.' % expectation_str, | 347 'Expectations [%s] did not match output.' % expectation_str, |
| 349 stdout, stderr) | 348 stdout, stderr) |
| 350 return | 349 return |
| 351 | 350 |
| 352 | 351 |
| 353 def CompleteAtLeastOneTest(resultfile, executing_tests): | 352 def CompleteAtLeastOneTest(executing_tests): |
| 354 """Blocks until at least one task is removed from executing_tests. | 353 """Blocks until at least one task is removed from executing_tests. |
| 355 | 354 |
| 356 This function removes completed tests from executing_tests, logging failures | 355 This function removes completed tests from executing_tests, logging failures |
| 357 and output. If no tests can be removed, it will enter a poll-loop until one | 356 and output. If no tests can be removed, it will enter a poll-loop until one |
| 358 test finishes or times out. On a timeout, this function is responsible for | 357 test finishes or times out. On a timeout, this function is responsible for |
| 359 terminating the process in the appropriate fashion. | 358 terminating the process in the appropriate fashion. |
| 360 | 359 |
| 361 Args: | 360 Args: |
| 362 executing_tests: A dict mapping a string containing the test name to the | 361 executing_tests: A dict mapping a string containing the test name to the |
| 363 test dict return from StartTest(). | 362 test dict return from StartTest(). |
| 364 | 363 |
| 365 Returns: | 364 Returns: |
| 366 A list of tests that have finished. | 365 A list of tests that have finished. |
| 367 """ | 366 """ |
| 368 finished_tests = [] | 367 finished_tests = [] |
| 369 busy_loop_timeout = time.time() + BUSY_LOOP_MAX_TIME_SEC | 368 busy_loop_timeout = time.time() + BUSY_LOOP_MAX_TIME_SEC |
| 370 while len(finished_tests) == 0: | 369 while len(finished_tests) == 0: |
| 371 # If we don't make progress for too long, assume the code is just dead. | 370 # If we don't make progress for too long, assume the code is just dead. |
| 372 assert busy_loop_timeout > time.time() | 371 assert busy_loop_timeout > time.time() |
| 373 | 372 |
| 374 # Select on the output pipes. | 373 # Select on the output pipes. |
| 375 read_set = [] | 374 read_set = [] |
| 376 for test in executing_tests.values(): | 375 for test in executing_tests.values(): |
| 377 read_set.extend([test['proc'].stderr, test['proc'].stdout]) | 376 read_set.extend([test['proc'].stderr, test['proc'].stdout]) |
| 378 result = select.select(read_set, [], read_set, NCTEST_TERMINATE_TIMEOUT_SEC) | 377 select.select(read_set, [], read_set, NCTEST_TERMINATE_TIMEOUT_SEC) |
| 379 | 378 |
| 380 # Now attempt to process results. | 379 # Now attempt to process results. |
| 381 now = time.time() | 380 now = time.time() |
| 382 for test in executing_tests.values(): | 381 for test in executing_tests.values(): |
| 383 proc = test['proc'] | 382 proc = test['proc'] |
| 384 if proc.poll() is not None: | 383 if proc.poll() is not None: |
| 385 test['finished_at'] = now | 384 test['finished_at'] = now |
| 386 finished_tests.append(test) | 385 finished_tests.append(test) |
| 387 elif test['terminate_timeout'] < now: | 386 elif test['terminate_timeout'] < now: |
| 388 proc.terminate() | 387 proc.terminate() |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 418 | 417 |
| 419 # Convert filename from underscores to CamelCase. | 418 # Convert filename from underscores to CamelCase. |
| 420 words = os.path.splitext(os.path.basename(sourcefile_path))[0].split('_') | 419 words = os.path.splitext(os.path.basename(sourcefile_path))[0].split('_') |
| 421 words = [w.capitalize() for w in words] | 420 words = [w.capitalize() for w in words] |
| 422 suite_name = 'NoCompile' + ''.join(words) | 421 suite_name = 'NoCompile' + ''.join(words) |
| 423 | 422 |
| 424 test_configs = ExtractTestConfigs(sourcefile_path, suite_name) | 423 test_configs = ExtractTestConfigs(sourcefile_path, suite_name) |
| 425 timings['extract_done'] = time.time() | 424 timings['extract_done'] = time.time() |
| 426 | 425 |
| 427 resultfile = StringIO.StringIO() | 426 resultfile = StringIO.StringIO() |
| 427 resultlog = StringIO.StringIO() | |
| 428 resultfile.write(RESULT_FILE_HEADER % sourcefile_path) | 428 resultfile.write(RESULT_FILE_HEADER % sourcefile_path) |
| 429 | 429 |
| 430 # Run the no-compile tests, but ensure we do not run more than |parallelism| | 430 # Run the no-compile tests, but ensure we do not run more than |parallelism| |
| 431 # tests at once. | 431 # tests at once. |
| 432 timings['header_written'] = time.time() | 432 timings['header_written'] = time.time() |
| 433 executing_tests = {} | 433 executing_tests = {} |
| 434 finished_tests = [] | 434 finished_tests = [] |
| 435 | 435 |
| 436 test = StartTest( | 436 test = StartTest( |
| 437 sourcefile_path, | 437 sourcefile_path, |
| 438 cflags + ' -MMD -MF %s.d -MT %s' % (resultfile_path, resultfile_path), | 438 cflags + ' -MMD -MF %s.d -MT %s' % (resultfile_path, resultfile_path), |
| 439 { 'name': 'NCTEST_SANITY', | 439 { 'name': 'NCTEST_SANITY', |
| 440 'suite_name': suite_name, | 440 'suite_name': suite_name, |
| 441 'expectations': None, | 441 'expectations': None, |
| 442 }) | 442 }) |
| 443 executing_tests[test['name']] = test | 443 executing_tests[test['name']] = test |
| 444 | 444 |
| 445 for config in test_configs: | 445 for config in test_configs: |
| 446 # CompleteAtLeastOneTest blocks until at least one test finishes. Thus, this | 446 # CompleteAtLeastOneTest blocks until at least one test finishes. Thus, this |
| 447 # acts as a semaphore. We cannot use threads + a real semaphore because | 447 # acts as a semaphore. We cannot use threads + a real semaphore because |
| 448 # subprocess forks, which can cause all sorts of hilarity with threads. | 448 # subprocess forks, which can cause all sorts of hilarity with threads. |
| 449 if len(executing_tests) >= parallelism: | 449 if len(executing_tests) >= parallelism: |
| 450 finished_tests.extend(CompleteAtLeastOneTest(resultfile, executing_tests)) | 450 finished_tests.extend(CompleteAtLeastOneTest(executing_tests)) |
| 451 | 451 |
| 452 if config['name'].startswith('DISABLED_'): | 452 if config['name'].startswith('DISABLED_'): |
| 453 PassTest(resultfile, config) | 453 PassTest(resultfile, resultlog, config) |
| 454 else: | 454 else: |
| 455 test = StartTest(sourcefile_path, cflags, config) | 455 test = StartTest(sourcefile_path, cflags, config) |
| 456 assert test['name'] not in executing_tests | 456 assert test['name'] not in executing_tests |
| 457 executing_tests[test['name']] = test | 457 executing_tests[test['name']] = test |
| 458 | 458 |
| 459 # If there are no more test to start, we still need to drain the running | 459 # If there are no more test to start, we still need to drain the running |
| 460 # ones. | 460 # ones. |
| 461 while len(executing_tests) > 0: | 461 while len(executing_tests) > 0: |
| 462 finished_tests.extend(CompleteAtLeastOneTest(resultfile, executing_tests)) | 462 finished_tests.extend(CompleteAtLeastOneTest(executing_tests)) |
| 463 timings['compile_done'] = time.time() | 463 timings['compile_done'] = time.time() |
| 464 | 464 |
| 465 for test in finished_tests: | 465 for test in finished_tests: |
| 466 if test['name'] == 'NCTEST_SANITY': | 466 if test['name'] == 'NCTEST_SANITY': |
| 467 _, stderr = test['proc'].communicate() | 467 _, stderr = test['proc'].communicate() |
| 468 return_code = test['proc'].poll() | 468 return_code = test['proc'].poll() |
| 469 if return_code != 0: | 469 if return_code != 0: |
| 470 sys.stderr.write(stderr) | 470 sys.stderr.write(stderr) |
| 471 continue | 471 continue |
| 472 ProcessTestResult(resultfile, test) | 472 ProcessTestResult(resultfile, resultlog, test) |
| 473 timings['results_processed'] = time.time() | 473 timings['results_processed'] = time.time() |
| 474 | 474 |
| 475 WriteStats(resultfile, suite_name, timings) | 475 WriteStats(resultlog, suite_name, timings) |
| 476 | 476 |
| 477 with open(resultfile_path + '.log', 'w') as fd: | |
| 478 fd.write(resultlog.getvalue()) | |
| 477 if return_code == 0: | 479 if return_code == 0: |
| 478 with open(resultfile_path, 'w') as fd: | 480 with open(resultfile_path, 'w') as fd: |
| 479 fd.write(resultfile.getvalue()) | 481 fd.write(resultfile.getvalue()) |
| 480 | 482 |
| 481 resultfile.close() | 483 resultfile.close() |
| 482 sys.exit(return_code) | 484 sys.exit(return_code) |
| 483 | 485 |
| 484 | 486 |
| 485 if __name__ == '__main__': | 487 if __name__ == '__main__': |
| 486 main() | 488 main() |
| OLD | NEW |