blob: c7f28abf965420cc4ff8a075dc26f9f0efebdb45 [file] [log] [blame]
Anna Zaksf0c41162011-10-06 23:26:27 +00001#!/usr/bin/env python
2
3"""
4Static Analyzer qualification infrastructure.
5
George Karpenkova8076602017-10-02 17:59:12 +00006The goal is to test the analyzer against different projects,
7check for failures, compare results, and measure performance.
Anna Zaksf0c41162011-10-06 23:26:27 +00008
Ted Kremenek3a0678e2015-09-08 03:50:52 +00009Repository Directory will contain sources of the projects as well as the
10information on how to build them and the expected output.
Anna Zaksf0c41162011-10-06 23:26:27 +000011Repository Directory structure:
12 - ProjectMap file
13 - Historical Performance Data
14 - Project Dir1
15 - ReferenceOutput
16 - Project Dir2
17 - ReferenceOutput
18 ..
Gabor Horvathc3177f22015-07-08 18:39:31 +000019Note that the build tree must be inside the project dir.
Anna Zaksf0c41162011-10-06 23:26:27 +000020
21To test the build of the analyzer one would:
Ted Kremenek3a0678e2015-09-08 03:50:52 +000022 - Copy over a copy of the Repository Directory. (TODO: Prefer to ensure that
George Karpenkova8076602017-10-02 17:59:12 +000023 the build directory does not pollute the repository to min network
24 traffic).
Anna Zaksf0c41162011-10-06 23:26:27 +000025 - Build all projects, until error. Produce logs to report errors.
Ted Kremenek3a0678e2015-09-08 03:50:52 +000026 - Compare results.
Anna Zaksf0c41162011-10-06 23:26:27 +000027
Ted Kremenek3a0678e2015-09-08 03:50:52 +000028The files which should be kept around for failure investigations:
Anna Zaksf0c41162011-10-06 23:26:27 +000029 RepositoryCopy/Project DirI/ScanBuildResults
Ted Kremenek3a0678e2015-09-08 03:50:52 +000030 RepositoryCopy/Project DirI/run_static_analyzer.log
31
32Assumptions (TODO: shouldn't need to assume these.):
Anna Zaksf0c41162011-10-06 23:26:27 +000033 The script is being run from the Repository Directory.
Anna Zaks42a44632011-11-02 20:46:50 +000034 The compiler for scan-build and scan-build are in the PATH.
Anna Zaksf0c41162011-10-06 23:26:27 +000035 export PATH=/Users/zaks/workspace/c2llvm/build/Release+Asserts/bin:$PATH
36
37For more logging, set the env variables:
38 zaks:TI zaks$ export CCC_ANALYZER_LOG=1
39 zaks:TI zaks$ export CCC_ANALYZER_VERBOSE=1
Ted Kremenek3a0678e2015-09-08 03:50:52 +000040
Gabor Horvathda32a862015-08-20 22:59:49 +000041The list of checkers tested are hardcoded in the Checkers variable.
42For testing additional checkers, use the SA_ADDITIONAL_CHECKERS environment
43variable. It should contain a comma separated list.
Anna Zaksf0c41162011-10-06 23:26:27 +000044"""
45import CmpRuns
George Karpenkovbf92c442017-10-24 23:52:48 +000046import SATestUtils
Anna Zaksf0c41162011-10-06 23:26:27 +000047
Gabor Horvath93fde942015-06-30 15:31:17 +000048import argparse
George Karpenkovf37d3a52018-02-08 21:22:42 +000049import csv
50import glob
51import logging
52import math
George Karpenkov3abfc3b2017-09-22 01:41:16 +000053import multiprocessing
George Karpenkovf37d3a52018-02-08 21:22:42 +000054import os
55import plistlib
56import shutil
57import sys
58import threading
59import time
Artem Dergachev1a3b8012020-05-15 14:27:30 +030060
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030061from queue import Queue
62from subprocess import CalledProcessError, check_call
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +030063from typing import (cast, Dict, Iterable, IO, List, NamedTuple, Optional,
64 Tuple, TYPE_CHECKING)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030065
Anna Zaksf0c41162011-10-06 23:26:27 +000066
George Karpenkov13d37482018-07-30 23:01:20 +000067###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +000068# Helper functions.
George Karpenkov13d37482018-07-30 23:01:20 +000069###############################################################################
Anna Zaksf0c41162011-10-06 23:26:27 +000070
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030071LOCAL = threading.local()
72LOCAL.stdout = sys.stdout
73LOCAL.stderr = sys.stderr
74
75
76def stderr(message: str):
77 LOCAL.stderr.write(message)
78
79
80def stdout(message: str):
81 LOCAL.stdout.write(message)
82
83
George Karpenkovf37d3a52018-02-08 21:22:42 +000084logging.basicConfig(
85 level=logging.DEBUG,
86 format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
Ted Kremenekf9a539d2012-08-28 20:40:04 +000087
Valeriy Savchenkoc98872e2020-05-14 13:31:01 +030088
George Karpenkov13d37482018-07-30 23:01:20 +000089###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +000090# Configuration setup.
George Karpenkov13d37482018-07-30 23:01:20 +000091###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +000092
George Karpenkova8076602017-10-02 17:59:12 +000093
Ted Kremenek42c14422012-08-28 20:40:02 +000094# Find Clang for static analysis.
George Karpenkovbe6c3292017-09-21 22:12:49 +000095if 'CC' in os.environ:
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +030096 cc_candidate: Optional[str] = os.environ['CC']
George Karpenkovbe6c3292017-09-21 22:12:49 +000097else:
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +030098 cc_candidate = SATestUtils.which("clang", os.environ['PATH'])
99if not cc_candidate:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300100 stderr("Error: cannot find 'clang' in PATH")
George Karpenkov65839bd2017-10-26 01:13:22 +0000101 sys.exit(1)
Ted Kremenek42c14422012-08-28 20:40:02 +0000102
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300103CLANG = cc_candidate
104
Ted Kremenekf9a539d2012-08-28 20:40:04 +0000105# Number of jobs.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300106MAX_JOBS = int(math.ceil(multiprocessing.cpu_count() * 0.75))
Ted Kremenekf9a539d2012-08-28 20:40:04 +0000107
Ted Kremenek42c14422012-08-28 20:40:02 +0000108# Project map stores info about all the "registered" projects.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300109PROJECT_MAP_FILE = "projectMap.csv"
Ted Kremenek42c14422012-08-28 20:40:02 +0000110
111# Names of the project specific scripts.
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000112# The script that downloads the project.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300113DOWNLOAD_SCRIPT = "download_project.sh"
Ted Kremenek42c14422012-08-28 20:40:02 +0000114# The script that needs to be executed before the build can start.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300115CLEANUP_SCRIPT = "cleanup_run_static_analyzer.sh"
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000116# This is a file containing commands for scan-build.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300117BUILD_SCRIPT = "run_static_analyzer.cmd"
Ted Kremenek42c14422012-08-28 20:40:02 +0000118
George Karpenkov5c23d6a2018-06-29 22:05:32 +0000119# A comment in a build script which disables wrapping.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300120NO_PREFIX_CMD = "#NOPREFIX"
George Karpenkov5c23d6a2018-06-29 22:05:32 +0000121
Ted Kremenek42c14422012-08-28 20:40:02 +0000122# The log file name.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300123LOG_DIR_NAME = "Logs"
124BUILD_LOG_NAME = "run_static_analyzer.log"
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000125# Summary file - contains the summary of the failures. Ex: This info can be be
Ted Kremenek42c14422012-08-28 20:40:02 +0000126# displayed when buildbot detects a build failure.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300127NUM_OF_FAILURES_IN_SUMMARY = 10
Ted Kremenek42c14422012-08-28 20:40:02 +0000128
129# The scan-build result directory.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300130OUTPUT_DIR_NAME = "ScanBuildResults"
131REF_PREFIX = "Ref"
Ted Kremenek42c14422012-08-28 20:40:02 +0000132
George Karpenkova8076602017-10-02 17:59:12 +0000133# The name of the directory storing the cached project source. If this
134# directory does not exist, the download script will be executed.
135# That script should create the "CachedSource" directory and download the
136# project source into it.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300137CACHED_SOURCE_DIR_NAME = "CachedSource"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000138
139# The name of the directory containing the source code that will be analyzed.
140# Each time a project is analyzed, a fresh copy of its CachedSource directory
141# will be copied to the PatchedSource directory and then the local patches
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300142# in PATCHFILE_NAME will be applied (if PATCHFILE_NAME exists).
143PATCHED_SOURCE_DIR_NAME = "PatchedSource"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000144
145# The name of the patchfile specifying any changes that should be applied
146# to the CachedSource before analyzing.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300147PATCHFILE_NAME = "changes_for_analyzer.patch"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000148
Ted Kremenek42c14422012-08-28 20:40:02 +0000149# The list of checkers used during analyzes.
Alp Tokerd4733632013-12-05 04:47:09 +0000150# Currently, consists of all the non-experimental checkers, plus a few alpha
Jordan Rose10ad0812013-04-05 17:55:07 +0000151# checkers we don't want to regress on.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300152CHECKERS = ",".join([
George Karpenkovaf76b4a2017-09-30 00:05:24 +0000153 "alpha.unix.SimpleStream",
154 "alpha.security.taint",
155 "cplusplus.NewDeleteLeaks",
156 "core",
157 "cplusplus",
158 "deadcode",
159 "security",
160 "unix",
161 "osx",
162 "nullability"
163])
Ted Kremenek42c14422012-08-28 20:40:02 +0000164
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300165VERBOSE = 0
166
167
168class StreamToLogger:
169 def __init__(self, logger: logging.Logger,
170 log_level: int = logging.INFO):
171 self.logger = logger
172 self.log_level = log_level
173
174 def write(self, message: str):
175 # Rstrip in order not to write an extra newline.
176 self.logger.log(self.log_level, message.rstrip())
177
178 def flush(self):
179 pass
180
181 def fileno(self) -> int:
182 return 0
183
Ted Kremenek42c14422012-08-28 20:40:02 +0000184
George Karpenkov13d37482018-07-30 23:01:20 +0000185###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000186# Test harness logic.
George Karpenkov13d37482018-07-30 23:01:20 +0000187###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000188
George Karpenkova8076602017-10-02 17:59:12 +0000189
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300190def get_project_map_path(should_exist: bool = True) -> str:
191 project_map_path = os.path.join(os.path.abspath(os.curdir),
192 PROJECT_MAP_FILE)
193
194 if should_exist and not os.path.exists(project_map_path):
195 stderr(f"Error: Cannot find the project map file {project_map_path}"
196 f"\nRunning script for the wrong directory?\n")
197 sys.exit(1)
198
199 return project_map_path
200
201
202def run_cleanup_script(directory: str, build_log_file: IO):
George Karpenkova8076602017-10-02 17:59:12 +0000203 """
204 Run pre-processing script if any.
205 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300206 cwd = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
207 script_path = os.path.join(directory, CLEANUP_SCRIPT)
208
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300209 SATestUtils.run_script(script_path, build_log_file, cwd,
210 out=LOCAL.stdout, err=LOCAL.stderr,
211 verbose=VERBOSE)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000212
George Karpenkova8076602017-10-02 17:59:12 +0000213
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300214def download_and_patch(directory: str, build_log_file: IO):
George Karpenkova8076602017-10-02 17:59:12 +0000215 """
216 Download the project and apply the local patchfile if it exists.
217 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300218 cached_source = os.path.join(directory, CACHED_SOURCE_DIR_NAME)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000219
220 # If the we don't already have the cached source, run the project's
221 # download script to download it.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300222 if not os.path.exists(cached_source):
223 download(directory, build_log_file)
224 if not os.path.exists(cached_source):
225 stderr(f"Error: '{cached_source}' not found after download.\n")
George Karpenkov65839bd2017-10-26 01:13:22 +0000226 exit(1)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000227
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300228 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000229
230 # Remove potentially stale patched source.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300231 if os.path.exists(patched_source):
232 shutil.rmtree(patched_source)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000233
234 # Copy the cached source and apply any patches to the copy.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300235 shutil.copytree(cached_source, patched_source, symlinks=True)
236 apply_patch(directory, build_log_file)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000237
George Karpenkova8076602017-10-02 17:59:12 +0000238
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300239def download(directory: str, build_log_file: IO):
240 """
241 Run the script to download the project, if it exists.
242 """
243 script_path = os.path.join(directory, DOWNLOAD_SCRIPT)
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300244 SATestUtils.run_script(script_path, build_log_file, directory,
245 out=LOCAL.stdout, err=LOCAL.stderr,
246 verbose=VERBOSE)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300247
248
249def apply_patch(directory: str, build_log_file: IO):
250 patchfile_path = os.path.join(directory, PATCHFILE_NAME)
251 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
252
253 if not os.path.exists(patchfile_path):
254 stdout(" No local patches.\n")
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000255 return
256
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300257 stdout(" Applying patch.\n")
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000258 try:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300259 check_call(f"patch -p1 < '{patchfile_path}'",
260 cwd=patched_source,
261 stderr=build_log_file,
262 stdout=build_log_file,
George Karpenkova8076602017-10-02 17:59:12 +0000263 shell=True)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000264
George Karpenkove58044d2017-10-27 22:39:54 +0000265 except CalledProcessError:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300266 stderr(f"Error: Patch failed. "
267 f"See {build_log_file.name} for details.\n")
George Karpenkove58044d2017-10-27 22:39:54 +0000268 sys.exit(1)
Anna Zaksf0c41162011-10-06 23:26:27 +0000269
George Karpenkova8076602017-10-02 17:59:12 +0000270
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300271class ProjectInfo(NamedTuple):
George Karpenkova8076602017-10-02 17:59:12 +0000272 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300273 Information about a project and settings for its analysis.
George Karpenkova8076602017-10-02 17:59:12 +0000274 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300275 name: str
276 build_mode: int
277 override_compiler: bool = False
278 extra_analyzer_config: str = ""
279 is_reference_build: bool = False
280 strictness: int = 0
Anna Zaks4720a732011-11-05 05:20:48 +0000281
Devin Coughlinbace0322015-09-14 21:22:24 +0000282
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300283# typing package doesn't have a separate type for Queue, but has a generic stub
284# We still want to have a type-safe checked project queue, for this reason,
285# we specify generic type for mypy.
286#
287# It is a common workaround for this situation:
288# https://mypy.readthedocs.io/en/stable/common_issues.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime
289if TYPE_CHECKING:
290 ProjectQueue = Queue[ProjectInfo] # this is only processed by mypy
291else:
292 ProjectQueue = Queue # this will be executed at runtime
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000293
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000294
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300295class RegressionTester:
296 """
297 A component aggregating all of the project testing.
298 """
299 def __init__(self, jobs: int, override_compiler: bool,
300 extra_analyzer_config: str, regenerate: bool,
301 strictness: bool):
302 self.jobs = jobs
303 self.override_compiler = override_compiler
304 self.extra_analyzer_config = extra_analyzer_config
305 self.regenerate = regenerate
306 self.strictness = strictness
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000307
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300308 def test_all(self) -> bool:
309 projects_to_test: List[ProjectInfo] = []
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000310
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300311 with open(get_project_map_path(), "r") as map_file:
312 validate_project_file(map_file)
313
314 # Test the projects.
315 for proj_name, proj_build_mode in get_projects(map_file):
316 projects_to_test.append(
317 ProjectInfo(proj_name, int(proj_build_mode),
318 self.override_compiler,
319 self.extra_analyzer_config,
320 self.regenerate, self.strictness))
321 if self.jobs <= 1:
322 return self._single_threaded_test_all(projects_to_test)
323 else:
324 return self._multi_threaded_test_all(projects_to_test)
325
326 def _single_threaded_test_all(self,
327 projects_to_test: List[ProjectInfo]) -> bool:
328 """
329 Run all projects.
330 :return: whether tests have passed.
331 """
332 success = True
333 for project_info in projects_to_test:
334 tester = ProjectTester(project_info)
335 success &= tester.test()
336 return success
337
338 def _multi_threaded_test_all(self,
339 projects_to_test: List[ProjectInfo]) -> bool:
340 """
341 Run each project in a separate thread.
342
343 This is OK despite GIL, as testing is blocked
344 on launching external processes.
345
346 :return: whether tests have passed.
347 """
348 tasks_queue = ProjectQueue()
349
350 for project_info in projects_to_test:
351 tasks_queue.put(project_info)
352
353 results_differ = threading.Event()
354 failure_flag = threading.Event()
355
356 for _ in range(self.jobs):
357 T = TestProjectThread(tasks_queue, results_differ, failure_flag)
358 T.start()
359
360 # Required to handle Ctrl-C gracefully.
361 while tasks_queue.unfinished_tasks:
362 time.sleep(0.1) # Seconds.
363 if failure_flag.is_set():
364 stderr("Test runner crashed\n")
365 sys.exit(1)
366 return not results_differ.is_set()
367
368
369class ProjectTester:
370 """
371 A component aggregating testing for one project.
372 """
373 def __init__(self, project_info: ProjectInfo):
374 self.project_name = project_info.name
375 self.build_mode = project_info.build_mode
376 self.override_compiler = project_info.override_compiler
377 self.extra_analyzer_config = project_info.extra_analyzer_config
378 self.is_reference_build = project_info.is_reference_build
379 self.strictness = project_info.strictness
380
381 def test(self) -> bool:
382 """
383 Test a given project.
384 :return tests_passed: Whether tests have passed according
385 to the :param strictness: criteria.
386 """
387 stdout(f" \n\n--- Building project {self.project_name}\n")
388
389 start_time = time.time()
390
391 project_dir = self.get_project_dir()
Valeriy Savchenko53953892020-05-27 16:06:45 +0300392 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300393 stdout(f" Build directory: {project_dir}.\n")
394
395 # Set the build results directory.
396 output_dir = self.get_output_dir()
397 output_dir = os.path.join(project_dir, output_dir)
398
399 self.build(project_dir, output_dir)
400 check_build(output_dir)
401
402 if self.is_reference_build:
403 cleanup_reference_results(output_dir)
404 passed = True
405 else:
406 passed = run_cmp_results(project_dir, self.strictness)
407
408 stdout(f"Completed tests for project {self.project_name} "
409 f"(time: {time.time() - start_time:.2f}).\n")
410
411 return passed
412
413 def get_project_dir(self) -> str:
414 return os.path.join(os.path.abspath(os.curdir), self.project_name)
415
416 def get_output_dir(self) -> str:
417 if self.is_reference_build:
418 return REF_PREFIX + OUTPUT_DIR_NAME
419 else:
420 return OUTPUT_DIR_NAME
421
422 def build(self, directory: str, output_dir: str):
423 time_start = time.time()
424
425 build_log_path = get_build_log_path(output_dir)
426
427 stdout(f"Log file: {build_log_path}\n")
428 stdout(f"Output directory: {output_dir}\n")
429
430 remove_log_file(output_dir)
431
432 # Clean up scan build results.
433 if os.path.exists(output_dir):
Valeriy Savchenko53953892020-05-27 16:06:45 +0300434 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300435 stdout(f" Removing old results: {output_dir}\n")
436
437 shutil.rmtree(output_dir)
438
439 assert(not os.path.exists(output_dir))
440 os.makedirs(os.path.join(output_dir, LOG_DIR_NAME))
441
442 # Build and analyze the project.
443 with open(build_log_path, "w+") as build_log_file:
444 if self.build_mode == 1:
445 download_and_patch(directory, build_log_file)
446 run_cleanup_script(directory, build_log_file)
447 self.scan_build(directory, output_dir, build_log_file)
448 else:
449 self.analyze_preprocessed(directory, output_dir)
450
451 if self.is_reference_build:
452 run_cleanup_script(directory, build_log_file)
453 normalize_reference_results(directory, output_dir,
454 self.build_mode)
455
456 stdout(f"Build complete (time: {time.time() - time_start:.2f}). "
457 f"See the log for more details: {build_log_path}\n")
458
459 def scan_build(self, directory: str, output_dir: str, build_log_file: IO):
460 """
461 Build the project with scan-build by reading in the commands and
462 prefixing them with the scan-build options.
463 """
464 build_script_path = os.path.join(directory, BUILD_SCRIPT)
465 if not os.path.exists(build_script_path):
466 stderr(f"Error: build script is not defined: "
467 f"{build_script_path}\n")
468 sys.exit(1)
469
470 all_checkers = CHECKERS
471 if 'SA_ADDITIONAL_CHECKERS' in os.environ:
472 all_checkers = (all_checkers + ',' +
473 os.environ['SA_ADDITIONAL_CHECKERS'])
474
475 # Run scan-build from within the patched source directory.
476 cwd = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
477
478 options = f"--use-analyzer '{CLANG}' "
479 options += f"-plist-html -o '{output_dir}' "
480 options += f"-enable-checker {all_checkers} "
481 options += "--keep-empty "
482 options += f"-analyzer-config '{self.generate_config()}' "
483
484 if self.override_compiler:
485 options += "--override-compiler "
486
487 extra_env: Dict[str, str] = {}
488 try:
489 command_file = open(build_script_path, "r")
490 command_prefix = "scan-build " + options + " "
491
492 for command in command_file:
493 command = command.strip()
494
495 if len(command) == 0:
496 continue
497
498 # Custom analyzer invocation specified by project.
499 # Communicate required information using environment variables
500 # instead.
501 if command == NO_PREFIX_CMD:
502 command_prefix = ""
503 extra_env['OUTPUT'] = output_dir
504 extra_env['CC'] = CLANG
505 extra_env['ANALYZER_CONFIG'] = self.generate_config()
506 continue
507
508 if command.startswith("#"):
509 continue
510
511 # If using 'make', auto imply a -jX argument
512 # to speed up analysis. xcodebuild will
513 # automatically use the maximum number of cores.
514 if (command.startswith("make ") or command == "make") and \
515 "-j" not in command:
516 command += f" -j{MAX_JOBS}"
517
518 command_to_run = command_prefix + command
519
Valeriy Savchenko53953892020-05-27 16:06:45 +0300520 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300521 stdout(f" Executing: {command_to_run}\n")
522
523 check_call(command_to_run, cwd=cwd,
524 stderr=build_log_file,
525 stdout=build_log_file,
526 env=dict(os.environ, **extra_env),
527 shell=True)
528
529 except CalledProcessError:
530 stderr("Error: scan-build failed. Its output was: \n")
531 build_log_file.seek(0)
532 shutil.copyfileobj(build_log_file, LOCAL.stderr)
533 sys.exit(1)
534
535 def analyze_preprocessed(self, directory: str, output_dir: str):
536 """
537 Run analysis on a set of preprocessed files.
538 """
539 if os.path.exists(os.path.join(directory, BUILD_SCRIPT)):
540 stderr(f"Error: The preprocessed files project "
541 f"should not contain {BUILD_SCRIPT}\n")
Anna Zaks4720a732011-11-05 05:20:48 +0000542 raise Exception()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000543
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300544 prefix = CLANG + " --analyze "
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000545
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300546 prefix += "--analyzer-output plist "
547 prefix += " -Xclang -analyzer-checker=" + CHECKERS
548 prefix += " -fcxx-exceptions -fblocks "
549 prefix += " -Xclang -analyzer-config "
550 prefix += f"-Xclang {self.generate_config()} "
George Karpenkova8076602017-10-02 17:59:12 +0000551
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300552 if self.build_mode == 2:
553 prefix += "-std=c++11 "
Anna Zaks4720a732011-11-05 05:20:48 +0000554
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300555 plist_path = os.path.join(directory, output_dir, "date")
556 fail_path = os.path.join(plist_path, "failures")
557 os.makedirs(fail_path)
George Karpenkova8076602017-10-02 17:59:12 +0000558
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300559 for full_file_name in glob.glob(directory + "/*"):
560 file_name = os.path.basename(full_file_name)
561 failed = False
Devin Coughlin9ea80332016-01-23 01:09:07 +0000562
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300563 # Only run the analyzes on supported files.
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300564 if SATestUtils.has_no_extension(file_name):
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000565 continue
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300566 if not SATestUtils.is_valid_single_input_file(file_name):
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300567 stderr(f"Error: Invalid single input file {full_file_name}.\n")
568 raise Exception()
George Karpenkov318cd1f2017-10-24 23:52:46 +0000569
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300570 # Build and call the analyzer command.
571 plist_basename = os.path.join(plist_path, file_name)
572 output_option = f"-o '{plist_basename}.plist' "
573 command = f"{prefix}{output_option}'{file_name}'"
George Karpenkov318cd1f2017-10-24 23:52:46 +0000574
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300575 log_path = os.path.join(fail_path, file_name + ".stderr.txt")
576 with open(log_path, "w+") as log_file:
577 try:
Valeriy Savchenko53953892020-05-27 16:06:45 +0300578 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300579 stdout(f" Executing: {command}\n")
580
581 check_call(command, cwd=directory, stderr=log_file,
582 stdout=log_file, shell=True)
583
584 except CalledProcessError as e:
585 stderr(f"Error: Analyzes of {full_file_name} failed. "
586 f"See {log_file.name} for details. "
587 f"Error code {e.returncode}.\n")
588 failed = True
589
590 # If command did not fail, erase the log file.
591 if not failed:
592 os.remove(log_file.name)
593
594 def generate_config(self) -> str:
595 out = "serialize-stats=true,stable-report-filename=true"
596
597 if self.extra_analyzer_config:
598 out += "," + self.extra_analyzer_config
599
600 return out
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000601
George Karpenkova8076602017-10-02 17:59:12 +0000602
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300603class TestProjectThread(threading.Thread):
604 def __init__(self, tasks_queue: ProjectQueue,
605 results_differ: threading.Event,
606 failure_flag: threading.Event):
607 """
608 :param results_differ: Used to signify that results differ from
609 the canonical ones.
610 :param failure_flag: Used to signify a failure during the run.
611 """
612 self.args = args
613 self.tasks_queue = tasks_queue
614 self.results_differ = results_differ
615 self.failure_flag = failure_flag
616 super().__init__()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000617
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300618 # Needed to gracefully handle interrupts with Ctrl-C
619 self.daemon = True
620
621 def run(self):
622 while not self.tasks_queue.empty():
623 try:
624 project_info = self.tasks_queue.get()
625
626 Logger = logging.getLogger(project_info.name)
627 LOCAL.stdout = StreamToLogger(Logger, logging.INFO)
628 LOCAL.stderr = StreamToLogger(Logger, logging.ERROR)
629
630 tester = ProjectTester(project_info)
631 if not tester.test():
632 self.results_differ.set()
633
634 self.tasks_queue.task_done()
635
Valeriy Savchenkoa5b25032020-05-22 18:47:04 +0300636 except BaseException:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300637 self.failure_flag.set()
638 raise
Anna Zaksf0c41162011-10-06 23:26:27 +0000639
George Karpenkova8076602017-10-02 17:59:12 +0000640
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300641###############################################################################
642# Utility functions.
643###############################################################################
George Karpenkov3c128cb2017-10-30 19:40:33 +0000644
645
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300646def check_build(output_dir: str):
George Karpenkova8076602017-10-02 17:59:12 +0000647 """
648 Given the scan-build output directory, checks if the build failed
649 (by searching for the failures directories). If there are failures, it
650 creates a summary file in the output directory.
651
652 """
Anna Zaksf0c41162011-10-06 23:26:27 +0000653 # Check if there are failures.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300654 failures = glob.glob(output_dir + "/*/failures/*.stderr.txt")
655 total_failed = len(failures)
656
657 if total_failed == 0:
658 clean_up_empty_plists(output_dir)
659 clean_up_empty_folders(output_dir)
660
661 plists = glob.glob(output_dir + "/*/*.plist")
662 stdout(f"Number of bug reports "
663 f"(non-empty plist files) produced: {len(plists)}\n")
George Karpenkova8076602017-10-02 17:59:12 +0000664 return
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000665
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300666 stderr("Error: analysis failed.\n")
667 stderr(f"Total of {total_failed} failures discovered.\n")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000668
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300669 if total_failed > NUM_OF_FAILURES_IN_SUMMARY:
670 stderr(f"See the first {NUM_OF_FAILURES_IN_SUMMARY} below.\n")
671
672 for index, failed_log_path in enumerate(failures, start=1):
673 if index >= NUM_OF_FAILURES_IN_SUMMARY:
George Karpenkovff555ce2017-10-26 19:00:22 +0000674 break
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300675
676 stderr(f"\n-- Error #{index} -----------\n")
677
678 with open(failed_log_path, "r") as failed_log:
679 shutil.copyfileobj(failed_log, LOCAL.stdout)
680
681 if total_failed > NUM_OF_FAILURES_IN_SUMMARY:
682 stderr("See the results folder for more.")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000683
George Karpenkov65839bd2017-10-26 01:13:22 +0000684 sys.exit(1)
Anna Zaksf0c41162011-10-06 23:26:27 +0000685
Anna Zaksf0c41162011-10-06 23:26:27 +0000686
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300687def cleanup_reference_results(output_dir: str):
George Karpenkova8076602017-10-02 17:59:12 +0000688 """
689 Delete html, css, and js files from reference results. These can
690 include multiple copies of the benchmark source and so get very large.
691 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300692 extensions = ["html", "css", "js"]
693
694 for extension in extensions:
695 for file_to_rm in glob.glob(f"{output_dir}/*/*.{extension}"):
696 file_to_rm = os.path.join(output_dir, file_to_rm)
697 os.remove(file_to_rm)
Devin Coughlin9ea80332016-01-23 01:09:07 +0000698
699 # Remove the log file. It leaks absolute path names.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300700 remove_log_file(output_dir)
Devin Coughlin9ea80332016-01-23 01:09:07 +0000701
George Karpenkova8076602017-10-02 17:59:12 +0000702
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300703def run_cmp_results(directory: str, strictness: int = 0) -> bool:
George Karpenkov1b51cbd2017-10-05 17:32:06 +0000704 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300705 Compare the warnings produced by scan-build.
706 strictness defines the success criteria for the test:
707 0 - success if there are no crashes or analyzer failure.
708 1 - success if there are no difference in the number of reported bugs.
709 2 - success if all the bug reports are identical.
710
711 :return success: Whether tests pass according to the strictness
712 criteria.
George Karpenkov1b51cbd2017-10-05 17:32:06 +0000713 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300714 tests_passed = True
715 start_time = time.time()
Anna Zaks4720a732011-11-05 05:20:48 +0000716
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300717 ref_dir = os.path.join(directory, REF_PREFIX + OUTPUT_DIR_NAME)
718 new_dir = os.path.join(directory, OUTPUT_DIR_NAME)
Anna Zaksf0c41162011-10-06 23:26:27 +0000719
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300720 # We have to go one level down the directory tree.
721 ref_list = glob.glob(ref_dir + "/*")
722 new_list = glob.glob(new_dir + "/*")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000723
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300724 # Log folders are also located in the results dir, so ignore them.
725 ref_log_dir = os.path.join(ref_dir, LOG_DIR_NAME)
726 if ref_log_dir in ref_list:
727 ref_list.remove(ref_log_dir)
728 new_list.remove(os.path.join(new_dir, LOG_DIR_NAME))
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000729
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300730 if len(ref_list) != len(new_list):
731 stderr(f"Mismatch in number of results folders: "
732 f"{ref_list} vs {new_list}")
733 sys.exit(1)
Anna Zaksf0c41162011-10-06 23:26:27 +0000734
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300735 # There might be more then one folder underneath - one per each scan-build
736 # command (Ex: one for configure and one for make).
737 if len(ref_list) > 1:
738 # Assume that the corresponding folders have the same names.
739 ref_list.sort()
740 new_list.sort()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000741
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300742 # Iterate and find the differences.
743 num_diffs = 0
744 for ref_dir, new_dir in zip(ref_list, new_list):
745 assert(ref_dir != new_dir)
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000746
Valeriy Savchenko53953892020-05-27 16:06:45 +0300747 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300748 stdout(f" Comparing Results: {ref_dir} {new_dir}\n")
749
750 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
751
752 # TODO: get rid of option parser invocation here
Valeriy Savchenko98f737f2020-05-25 15:56:51 +0300753 args = CmpRuns.generate_option_parser().parse_args(
754 ["--root-old", "", "--root-new", patched_source, "", ""])
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300755 # Scan the results, delete empty plist files.
756 num_diffs, reports_in_ref, reports_in_new = \
Valeriy Savchenko98f737f2020-05-25 15:56:51 +0300757 CmpRuns.dump_scan_build_results_diff(ref_dir, new_dir, args,
758 delete_empty=False,
759 out=LOCAL.stdout)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300760
761 if num_diffs > 0:
762 stdout(f"Warning: {num_diffs} differences in diagnostics.\n")
763
764 if strictness >= 2 and num_diffs > 0:
765 stdout("Error: Diffs found in strict mode (2).\n")
766 tests_passed = False
767
768 elif strictness >= 1 and reports_in_ref != reports_in_new:
769 stdout("Error: The number of results are different "
770 " strict mode (1).\n")
771 tests_passed = False
772
773 stdout(f"Diagnostic comparison complete "
774 f"(time: {time.time() - start_time:.2f}).\n")
775
776 return tests_passed
George Karpenkova8076602017-10-02 17:59:12 +0000777
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000778
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300779def normalize_reference_results(directory: str, output_dir: str,
780 build_mode: int):
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000781 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300782 Make the absolute paths relative in the reference results.
783 """
784 for dir_path, _, filenames in os.walk(output_dir):
785 for filename in filenames:
786 if not filename.endswith('plist'):
787 continue
788
789 plist = os.path.join(dir_path, filename)
790 data = plistlib.readPlist(plist)
791 path_prefix = directory
792
793 if build_mode == 1:
794 path_prefix = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
795
796 paths = [source[len(path_prefix) + 1:]
797 if source.startswith(path_prefix) else source
798 for source in data['files']]
799 data['files'] = paths
800
801 # Remove transient fields which change from run to run.
802 for diagnostic in data['diagnostics']:
803 if 'HTMLDiagnostics_files' in diagnostic:
804 diagnostic.pop('HTMLDiagnostics_files')
805
806 if 'clang_version' in data:
807 data.pop('clang_version')
808
809 plistlib.writePlist(data, plist)
810
811
812def get_build_log_path(output_dir: str) -> str:
813 return os.path.join(output_dir, LOG_DIR_NAME, BUILD_LOG_NAME)
814
815
816def remove_log_file(output_dir: str):
817 build_log_path = get_build_log_path(output_dir)
818
819 # Clean up the log file.
820 if os.path.exists(build_log_path):
Valeriy Savchenko53953892020-05-27 16:06:45 +0300821 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300822 stdout(f" Removing log file: {build_log_path}\n")
823
824 os.remove(build_log_path)
825
826
827def clean_up_empty_plists(output_dir: str):
828 """
829 A plist file is created for each call to the analyzer(each source file).
830 We are only interested on the once that have bug reports,
831 so delete the rest.
832 """
833 for plist in glob.glob(output_dir + "/*/*.plist"):
834 plist = os.path.join(output_dir, plist)
835
836 try:
Valeriy Savchenko98f737f2020-05-25 15:56:51 +0300837 with open(plist, "rb") as plist_file:
838 data = plistlib.load(plist_file)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300839 # Delete empty reports.
840 if not data['files']:
841 os.remove(plist)
842 continue
843
844 except plistlib.InvalidFileException as e:
845 stderr(f"Error parsing plist file {plist}: {str(e)}")
846 continue
847
848
849def clean_up_empty_folders(output_dir: str):
850 """
851 Remove empty folders from results, as git would not store them.
852 """
853 subdirs = glob.glob(output_dir + "/*")
854 for subdir in subdirs:
855 if not os.listdir(subdir):
856 os.removedirs(subdir)
857
858
859def get_projects(map_file: IO) -> Iterable[Tuple[str, str]]:
860 """
861 Iterate over all projects defined in the project file handler `map_file`
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000862 from the start.
863 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300864 map_file.seek(0)
865 # TODO: csv format is not very readable, change it to JSON
866 for project_info in csv.reader(map_file):
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300867 if SATestUtils.is_comment_csv_line(project_info):
George Karpenkova8076602017-10-02 17:59:12 +0000868 continue
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300869 # suppress mypy error
870 yield cast(Tuple[str, str], project_info)
George Karpenkova8076602017-10-02 17:59:12 +0000871
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000872
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300873def validate_project_file(map_file: IO):
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000874 """
875 Validate project file.
876 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300877 for project_info in get_projects(map_file):
878 if len(project_info) != 2:
879 stderr("Error: Rows in the project map file "
880 "should have 2 entries.")
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000881 raise Exception()
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300882
883 if project_info[1] not in ('0', '1', '2'):
884 stderr("Error: Second entry in the project map file should be 0"
885 " (single file), 1 (project), or 2(single file c++11).")
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000886 raise Exception()
887
Valeriy Savchenkoc98872e2020-05-14 13:31:01 +0300888
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300889if __name__ == "__main__":
Gabor Horvath93fde942015-06-30 15:31:17 +0000890 # Parse command line arguments.
Valeriy Savchenko53953892020-05-27 16:06:45 +0300891 parser = argparse.ArgumentParser(
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300892 description="Test the Clang Static Analyzer.")
893
Valeriy Savchenko53953892020-05-27 16:06:45 +0300894 parser.add_argument("--strictness", dest="strictness", type=int, default=0,
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300895 help="0 to fail on runtime errors, 1 to fail when the "
896 "number of found bugs are different from the "
897 "reference, 2 to fail on any difference from the "
898 "reference. Default is 0.")
Valeriy Savchenko53953892020-05-27 16:06:45 +0300899 parser.add_argument("-r", dest="regenerate", action="store_true",
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300900 default=False, help="Regenerate reference output.")
Valeriy Savchenko53953892020-05-27 16:06:45 +0300901 parser.add_argument("--override-compiler", action="store_true",
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300902 default=False, help="Call scan-build with "
903 "--override-compiler option.")
Valeriy Savchenko53953892020-05-27 16:06:45 +0300904 parser.add_argument("-j", "--jobs", dest="jobs", type=int,
George Karpenkovf37d3a52018-02-08 21:22:42 +0000905 default=0,
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300906 help="Number of projects to test concurrently")
Valeriy Savchenko53953892020-05-27 16:06:45 +0300907 parser.add_argument("--extra-analyzer-config",
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300908 dest="extra_analyzer_config", type=str,
George Karpenkovac986832018-10-02 21:19:23 +0000909 default="",
910 help="Arguments passed to to -analyzer-config")
Valeriy Savchenko53953892020-05-27 16:06:45 +0300911 parser.add_argument("-v", "--verbose", action="count", default=0)
Gabor Horvath93fde942015-06-30 15:31:17 +0000912
Valeriy Savchenko53953892020-05-27 16:06:45 +0300913 args = parser.parse_args()
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300914
Valeriy Savchenko53953892020-05-27 16:06:45 +0300915 VERBOSE = args.verbose
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300916 tester = RegressionTester(args.jobs, args.override_compiler,
917 args.extra_analyzer_config, args.regenerate,
918 args.strictness)
919 tests_passed = tester.test_all()
920
921 if not tests_passed:
922 stderr("ERROR: Tests failed.")
George Karpenkov65839bd2017-10-26 01:13:22 +0000923 sys.exit(42)