blob: ed5c7379bb5b49b83ce185002037ede5645cac22 [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
Valeriy Savchenko21bacc22020-07-10 10:54:18 +030046import SATestUtils as utils
Valeriy Savchenko38b455e2020-06-03 18:04:31 +030047from ProjectMap import DownloadType, ProjectInfo
Anna Zaksf0c41162011-10-06 23:26:27 +000048
George Karpenkovf37d3a52018-02-08 21:22:42 +000049import glob
50import logging
51import math
George Karpenkov3abfc3b2017-09-22 01:41:16 +000052import multiprocessing
George Karpenkovf37d3a52018-02-08 21:22:42 +000053import os
54import plistlib
55import shutil
56import sys
57import threading
58import time
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +030059import zipfile
Artem Dergachev1a3b8012020-05-15 14:27:30 +030060
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030061from queue import Queue
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +030062# mypy has problems finding InvalidFileException in the module
63# and this is we can shush that false positive
64from plistlib import InvalidFileException # type:ignore
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030065from subprocess import CalledProcessError, check_call
Valeriy Savchenko21bacc22020-07-10 10:54:18 +030066from typing import Dict, IO, List, NamedTuple, Optional, TYPE_CHECKING, Tuple
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030067
Anna Zaksf0c41162011-10-06 23:26:27 +000068
George Karpenkov13d37482018-07-30 23:01:20 +000069###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +000070# Helper functions.
George Karpenkov13d37482018-07-30 23:01:20 +000071###############################################################################
Anna Zaksf0c41162011-10-06 23:26:27 +000072
Valeriy Savchenko495fd642020-06-09 12:33:56 +030073class StreamToLogger:
74 def __init__(self, logger: logging.Logger,
75 log_level: int = logging.INFO):
76 self.logger = logger
77 self.log_level = log_level
78
79 def write(self, message: str):
80 # Rstrip in order not to write an extra newline.
81 self.logger.log(self.log_level, message.rstrip())
82
83 def flush(self):
84 pass
85
86 def fileno(self) -> int:
87 return 0
88
89
Valeriy Savchenko4902ca62020-05-21 18:28:36 +030090LOCAL = threading.local()
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +030091
92
93def init_logger(name: str):
94 # TODO: use debug levels for VERBOSE messages
95 logger = logging.getLogger(name)
96 logger.setLevel(logging.DEBUG)
97 LOCAL.stdout = StreamToLogger(logger, logging.INFO)
98 LOCAL.stderr = StreamToLogger(logger, logging.ERROR)
99
100
101init_logger("main")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300102
103
104def stderr(message: str):
105 LOCAL.stderr.write(message)
106
107
108def stdout(message: str):
109 LOCAL.stdout.write(message)
110
111
George Karpenkovf37d3a52018-02-08 21:22:42 +0000112logging.basicConfig(
George Karpenkovf37d3a52018-02-08 21:22:42 +0000113 format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
Ted Kremenekf9a539d2012-08-28 20:40:04 +0000114
Valeriy Savchenkoc98872e2020-05-14 13:31:01 +0300115
George Karpenkov13d37482018-07-30 23:01:20 +0000116###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000117# Configuration setup.
George Karpenkov13d37482018-07-30 23:01:20 +0000118###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000119
George Karpenkova8076602017-10-02 17:59:12 +0000120
Ted Kremenek42c14422012-08-28 20:40:02 +0000121# Find Clang for static analysis.
George Karpenkovbe6c3292017-09-21 22:12:49 +0000122if 'CC' in os.environ:
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300123 cc_candidate: Optional[str] = os.environ['CC']
George Karpenkovbe6c3292017-09-21 22:12:49 +0000124else:
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300125 cc_candidate = utils.which("clang", os.environ['PATH'])
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300126if not cc_candidate:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300127 stderr("Error: cannot find 'clang' in PATH")
George Karpenkov65839bd2017-10-26 01:13:22 +0000128 sys.exit(1)
Ted Kremenek42c14422012-08-28 20:40:02 +0000129
Valeriy Savchenko7cebfa42020-05-22 11:59:39 +0300130CLANG = cc_candidate
131
Ted Kremenekf9a539d2012-08-28 20:40:04 +0000132# Number of jobs.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300133MAX_JOBS = int(math.ceil(multiprocessing.cpu_count() * 0.75))
Ted Kremenekf9a539d2012-08-28 20:40:04 +0000134
Ted Kremenek42c14422012-08-28 20:40:02 +0000135# Names of the project specific scripts.
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000136# The script that downloads the project.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300137DOWNLOAD_SCRIPT = "download_project.sh"
Ted Kremenek42c14422012-08-28 20:40:02 +0000138# The script that needs to be executed before the build can start.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300139CLEANUP_SCRIPT = "cleanup_run_static_analyzer.sh"
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000140# This is a file containing commands for scan-build.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300141BUILD_SCRIPT = "run_static_analyzer.cmd"
Ted Kremenek42c14422012-08-28 20:40:02 +0000142
George Karpenkov5c23d6a2018-06-29 22:05:32 +0000143# A comment in a build script which disables wrapping.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300144NO_PREFIX_CMD = "#NOPREFIX"
George Karpenkov5c23d6a2018-06-29 22:05:32 +0000145
Ted Kremenek42c14422012-08-28 20:40:02 +0000146# The log file name.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300147LOG_DIR_NAME = "Logs"
148BUILD_LOG_NAME = "run_static_analyzer.log"
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000149# Summary file - contains the summary of the failures. Ex: This info can be be
Ted Kremenek42c14422012-08-28 20:40:02 +0000150# displayed when buildbot detects a build failure.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300151NUM_OF_FAILURES_IN_SUMMARY = 10
Ted Kremenek42c14422012-08-28 20:40:02 +0000152
153# The scan-build result directory.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300154OUTPUT_DIR_NAME = "ScanBuildResults"
155REF_PREFIX = "Ref"
Ted Kremenek42c14422012-08-28 20:40:02 +0000156
George Karpenkova8076602017-10-02 17:59:12 +0000157# The name of the directory storing the cached project source. If this
158# directory does not exist, the download script will be executed.
159# That script should create the "CachedSource" directory and download the
160# project source into it.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300161CACHED_SOURCE_DIR_NAME = "CachedSource"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000162
163# The name of the directory containing the source code that will be analyzed.
164# Each time a project is analyzed, a fresh copy of its CachedSource directory
165# will be copied to the PatchedSource directory and then the local patches
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300166# in PATCHFILE_NAME will be applied (if PATCHFILE_NAME exists).
167PATCHED_SOURCE_DIR_NAME = "PatchedSource"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000168
169# The name of the patchfile specifying any changes that should be applied
170# to the CachedSource before analyzing.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300171PATCHFILE_NAME = "changes_for_analyzer.patch"
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000172
Ted Kremenek42c14422012-08-28 20:40:02 +0000173# The list of checkers used during analyzes.
Alp Tokerd4733632013-12-05 04:47:09 +0000174# Currently, consists of all the non-experimental checkers, plus a few alpha
Jordan Rose10ad0812013-04-05 17:55:07 +0000175# checkers we don't want to regress on.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300176CHECKERS = ",".join([
George Karpenkovaf76b4a2017-09-30 00:05:24 +0000177 "alpha.unix.SimpleStream",
178 "alpha.security.taint",
179 "cplusplus.NewDeleteLeaks",
180 "core",
181 "cplusplus",
182 "deadcode",
183 "security",
184 "unix",
185 "osx",
186 "nullability"
187])
Ted Kremenek42c14422012-08-28 20:40:02 +0000188
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300189VERBOSE = 0
190
191
George Karpenkov13d37482018-07-30 23:01:20 +0000192###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000193# Test harness logic.
George Karpenkov13d37482018-07-30 23:01:20 +0000194###############################################################################
Ted Kremenek42c14422012-08-28 20:40:02 +0000195
George Karpenkova8076602017-10-02 17:59:12 +0000196
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300197def run_cleanup_script(directory: str, build_log_file: IO):
George Karpenkova8076602017-10-02 17:59:12 +0000198 """
199 Run pre-processing script if any.
200 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300201 cwd = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
202 script_path = os.path.join(directory, CLEANUP_SCRIPT)
203
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300204 utils.run_script(script_path, build_log_file, cwd,
205 out=LOCAL.stdout, err=LOCAL.stderr,
206 verbose=VERBOSE)
Devin Coughlin2cb767d2015-11-07 18:27:35 +0000207
George Karpenkova8076602017-10-02 17:59:12 +0000208
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300209class TestInfo(NamedTuple):
George Karpenkova8076602017-10-02 17:59:12 +0000210 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300211 Information about a project and settings for its analysis.
George Karpenkova8076602017-10-02 17:59:12 +0000212 """
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300213 project: ProjectInfo
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300214 override_compiler: bool = False
215 extra_analyzer_config: str = ""
216 is_reference_build: bool = False
217 strictness: int = 0
Anna Zaks4720a732011-11-05 05:20:48 +0000218
Devin Coughlinbace0322015-09-14 21:22:24 +0000219
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300220# typing package doesn't have a separate type for Queue, but has a generic stub
221# We still want to have a type-safe checked project queue, for this reason,
222# we specify generic type for mypy.
223#
224# It is a common workaround for this situation:
225# https://mypy.readthedocs.io/en/stable/common_issues.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime
226if TYPE_CHECKING:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300227 TestQueue = Queue[TestInfo] # this is only processed by mypy
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300228else:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300229 TestQueue = Queue # this will be executed at runtime
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000230
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000231
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300232class RegressionTester:
233 """
234 A component aggregating all of the project testing.
235 """
Valeriy Savchenko38b455e2020-06-03 18:04:31 +0300236 def __init__(self, jobs: int, projects: List[ProjectInfo],
237 override_compiler: bool, extra_analyzer_config: str,
238 regenerate: bool, strictness: bool):
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300239 self.jobs = jobs
Valeriy Savchenko38b455e2020-06-03 18:04:31 +0300240 self.projects = projects
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300241 self.override_compiler = override_compiler
242 self.extra_analyzer_config = extra_analyzer_config
243 self.regenerate = regenerate
244 self.strictness = strictness
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000245
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300246 def test_all(self) -> bool:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300247 projects_to_test: List[TestInfo] = []
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000248
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300249 # Test the projects.
Valeriy Savchenko38b455e2020-06-03 18:04:31 +0300250 for project in self.projects:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300251 projects_to_test.append(
252 TestInfo(project,
253 self.override_compiler,
254 self.extra_analyzer_config,
255 self.regenerate, self.strictness))
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300256 if self.jobs <= 1:
257 return self._single_threaded_test_all(projects_to_test)
258 else:
259 return self._multi_threaded_test_all(projects_to_test)
260
261 def _single_threaded_test_all(self,
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300262 projects_to_test: List[TestInfo]) -> bool:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300263 """
264 Run all projects.
265 :return: whether tests have passed.
266 """
267 success = True
268 for project_info in projects_to_test:
269 tester = ProjectTester(project_info)
270 success &= tester.test()
271 return success
272
273 def _multi_threaded_test_all(self,
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300274 projects_to_test: List[TestInfo]) -> bool:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300275 """
276 Run each project in a separate thread.
277
278 This is OK despite GIL, as testing is blocked
279 on launching external processes.
280
281 :return: whether tests have passed.
282 """
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300283 tasks_queue = TestQueue()
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300284
285 for project_info in projects_to_test:
286 tasks_queue.put(project_info)
287
288 results_differ = threading.Event()
289 failure_flag = threading.Event()
290
291 for _ in range(self.jobs):
292 T = TestProjectThread(tasks_queue, results_differ, failure_flag)
293 T.start()
294
295 # Required to handle Ctrl-C gracefully.
296 while tasks_queue.unfinished_tasks:
297 time.sleep(0.1) # Seconds.
298 if failure_flag.is_set():
299 stderr("Test runner crashed\n")
300 sys.exit(1)
301 return not results_differ.is_set()
302
303
304class ProjectTester:
305 """
306 A component aggregating testing for one project.
307 """
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300308 def __init__(self, test_info: TestInfo, silent: bool = False):
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300309 self.project = test_info.project
310 self.override_compiler = test_info.override_compiler
311 self.extra_analyzer_config = test_info.extra_analyzer_config
312 self.is_reference_build = test_info.is_reference_build
313 self.strictness = test_info.strictness
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300314 self.silent = silent
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300315
316 def test(self) -> bool:
317 """
318 Test a given project.
319 :return tests_passed: Whether tests have passed according
320 to the :param strictness: criteria.
321 """
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300322 if not self.project.enabled:
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300323 self.out(
324 f" \n\n--- Skipping disabled project {self.project.name}\n")
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300325 return True
326
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300327 self.out(f" \n\n--- Building project {self.project.name}\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300328
329 start_time = time.time()
330
331 project_dir = self.get_project_dir()
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300332 self.vout(f" Build directory: {project_dir}.\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300333
334 # Set the build results directory.
335 output_dir = self.get_output_dir()
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300336
337 self.build(project_dir, output_dir)
338 check_build(output_dir)
339
340 if self.is_reference_build:
341 cleanup_reference_results(output_dir)
342 passed = True
343 else:
344 passed = run_cmp_results(project_dir, self.strictness)
345
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300346 self.out(f"Completed tests for project {self.project.name} "
347 f"(time: {time.time() - start_time:.2f}).\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300348
349 return passed
350
351 def get_project_dir(self) -> str:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300352 return os.path.join(os.path.abspath(os.curdir), self.project.name)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300353
354 def get_output_dir(self) -> str:
355 if self.is_reference_build:
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300356 dirname = REF_PREFIX + OUTPUT_DIR_NAME
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300357 else:
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300358 dirname = OUTPUT_DIR_NAME
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300359
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300360 return os.path.join(self.get_project_dir(), dirname)
361
362 def build(self, directory: str, output_dir: str) -> Tuple[float, int]:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300363 build_log_path = get_build_log_path(output_dir)
364
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300365 self.out(f"Log file: {build_log_path}\n")
366 self.out(f"Output directory: {output_dir}\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300367
368 remove_log_file(output_dir)
369
370 # Clean up scan build results.
371 if os.path.exists(output_dir):
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300372 self.vout(f" Removing old results: {output_dir}\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300373
374 shutil.rmtree(output_dir)
375
376 assert(not os.path.exists(output_dir))
377 os.makedirs(os.path.join(output_dir, LOG_DIR_NAME))
378
379 # Build and analyze the project.
380 with open(build_log_path, "w+") as build_log_file:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300381 if self.project.mode == 1:
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300382 self._download_and_patch(directory, build_log_file)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300383 run_cleanup_script(directory, build_log_file)
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300384 build_time, memory = self.scan_build(directory, output_dir,
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300385 build_log_file)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300386 else:
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300387 build_time, memory = self.analyze_preprocessed(directory,
388 output_dir)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300389
390 if self.is_reference_build:
391 run_cleanup_script(directory, build_log_file)
392 normalize_reference_results(directory, output_dir,
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300393 self.project.mode)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300394
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300395 self.out(f"Build complete (time: {utils.time_to_str(build_time)}, "
396 f"peak memory: {utils.memory_to_str(memory)}). "
397 f"See the log for more details: {build_log_path}\n")
398
399 return build_time, memory
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300400
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300401 def scan_build(self, directory: str, output_dir: str,
402 build_log_file: IO) -> Tuple[float, int]:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300403 """
404 Build the project with scan-build by reading in the commands and
405 prefixing them with the scan-build options.
406 """
407 build_script_path = os.path.join(directory, BUILD_SCRIPT)
408 if not os.path.exists(build_script_path):
409 stderr(f"Error: build script is not defined: "
410 f"{build_script_path}\n")
411 sys.exit(1)
412
413 all_checkers = CHECKERS
414 if 'SA_ADDITIONAL_CHECKERS' in os.environ:
415 all_checkers = (all_checkers + ',' +
416 os.environ['SA_ADDITIONAL_CHECKERS'])
417
418 # Run scan-build from within the patched source directory.
419 cwd = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
420
421 options = f"--use-analyzer '{CLANG}' "
422 options += f"-plist-html -o '{output_dir}' "
423 options += f"-enable-checker {all_checkers} "
424 options += "--keep-empty "
425 options += f"-analyzer-config '{self.generate_config()}' "
426
427 if self.override_compiler:
428 options += "--override-compiler "
429
430 extra_env: Dict[str, str] = {}
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300431
432 execution_time = 0.0
433 peak_memory = 0
434
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300435 try:
436 command_file = open(build_script_path, "r")
437 command_prefix = "scan-build " + options + " "
438
439 for command in command_file:
440 command = command.strip()
441
442 if len(command) == 0:
443 continue
444
445 # Custom analyzer invocation specified by project.
446 # Communicate required information using environment variables
447 # instead.
448 if command == NO_PREFIX_CMD:
449 command_prefix = ""
450 extra_env['OUTPUT'] = output_dir
451 extra_env['CC'] = CLANG
452 extra_env['ANALYZER_CONFIG'] = self.generate_config()
453 continue
454
455 if command.startswith("#"):
456 continue
457
458 # If using 'make', auto imply a -jX argument
459 # to speed up analysis. xcodebuild will
460 # automatically use the maximum number of cores.
461 if (command.startswith("make ") or command == "make") and \
462 "-j" not in command:
463 command += f" -j{MAX_JOBS}"
464
465 command_to_run = command_prefix + command
466
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300467 self.vout(f" Executing: {command_to_run}\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300468
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300469 time, mem = utils.check_and_measure_call(
470 command_to_run, cwd=cwd,
471 stderr=build_log_file,
472 stdout=build_log_file,
473 env=dict(os.environ, **extra_env),
474 shell=True)
475
476 execution_time += time
477 peak_memory = max(peak_memory, mem)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300478
479 except CalledProcessError:
480 stderr("Error: scan-build failed. Its output was: \n")
481 build_log_file.seek(0)
482 shutil.copyfileobj(build_log_file, LOCAL.stderr)
483 sys.exit(1)
484
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300485 return execution_time, peak_memory
486
487 def analyze_preprocessed(self, directory: str,
488 output_dir: str) -> Tuple[float, int]:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300489 """
490 Run analysis on a set of preprocessed files.
491 """
492 if os.path.exists(os.path.join(directory, BUILD_SCRIPT)):
493 stderr(f"Error: The preprocessed files project "
494 f"should not contain {BUILD_SCRIPT}\n")
Anna Zaks4720a732011-11-05 05:20:48 +0000495 raise Exception()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000496
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300497 prefix = CLANG + " --analyze "
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000498
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300499 prefix += "--analyzer-output plist "
500 prefix += " -Xclang -analyzer-checker=" + CHECKERS
501 prefix += " -fcxx-exceptions -fblocks "
502 prefix += " -Xclang -analyzer-config "
503 prefix += f"-Xclang {self.generate_config()} "
George Karpenkova8076602017-10-02 17:59:12 +0000504
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300505 if self.project.mode == 2:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300506 prefix += "-std=c++11 "
Anna Zaks4720a732011-11-05 05:20:48 +0000507
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300508 plist_path = os.path.join(directory, output_dir, "date")
509 fail_path = os.path.join(plist_path, "failures")
510 os.makedirs(fail_path)
George Karpenkova8076602017-10-02 17:59:12 +0000511
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300512 execution_time = 0.0
513 peak_memory = 0
514
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300515 for full_file_name in glob.glob(directory + "/*"):
516 file_name = os.path.basename(full_file_name)
517 failed = False
Devin Coughlin9ea80332016-01-23 01:09:07 +0000518
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300519 # Only run the analyzes on supported files.
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300520 if utils.has_no_extension(file_name):
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000521 continue
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300522 if not utils.is_valid_single_input_file(file_name):
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300523 stderr(f"Error: Invalid single input file {full_file_name}.\n")
524 raise Exception()
George Karpenkov318cd1f2017-10-24 23:52:46 +0000525
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300526 # Build and call the analyzer command.
527 plist_basename = os.path.join(plist_path, file_name)
528 output_option = f"-o '{plist_basename}.plist' "
529 command = f"{prefix}{output_option}'{file_name}'"
George Karpenkov318cd1f2017-10-24 23:52:46 +0000530
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300531 log_path = os.path.join(fail_path, file_name + ".stderr.txt")
532 with open(log_path, "w+") as log_file:
533 try:
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300534 self.vout(f" Executing: {command}\n")
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300535
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300536 time, mem = utils.check_and_measure_call(
537 command, cwd=directory, stderr=log_file,
538 stdout=log_file, shell=True)
539
540 execution_time += time
541 peak_memory = max(peak_memory, mem)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300542
543 except CalledProcessError as e:
544 stderr(f"Error: Analyzes of {full_file_name} failed. "
545 f"See {log_file.name} for details. "
546 f"Error code {e.returncode}.\n")
547 failed = True
548
549 # If command did not fail, erase the log file.
550 if not failed:
551 os.remove(log_file.name)
552
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300553 return execution_time, peak_memory
554
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300555 def generate_config(self) -> str:
556 out = "serialize-stats=true,stable-report-filename=true"
557
558 if self.extra_analyzer_config:
559 out += "," + self.extra_analyzer_config
560
561 return out
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000562
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300563 def _download_and_patch(self, directory: str, build_log_file: IO):
564 """
565 Download the project and apply the local patchfile if it exists.
566 """
567 cached_source = os.path.join(directory, CACHED_SOURCE_DIR_NAME)
568
569 # If the we don't already have the cached source, run the project's
570 # download script to download it.
571 if not os.path.exists(cached_source):
572 self._download(directory, build_log_file)
573 if not os.path.exists(cached_source):
574 stderr(f"Error: '{cached_source}' not found after download.\n")
575 exit(1)
576
577 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
578
579 # Remove potentially stale patched source.
580 if os.path.exists(patched_source):
581 shutil.rmtree(patched_source)
582
583 # Copy the cached source and apply any patches to the copy.
584 shutil.copytree(cached_source, patched_source, symlinks=True)
585 self._apply_patch(directory, build_log_file)
586
587 def _download(self, directory: str, build_log_file: IO):
588 """
589 Run the script to download the project, if it exists.
590 """
591 if self.project.source == DownloadType.GIT:
592 self._download_from_git(directory, build_log_file)
593 elif self.project.source == DownloadType.ZIP:
594 self._unpack_zip(directory, build_log_file)
595 elif self.project.source == DownloadType.SCRIPT:
596 self._run_download_script(directory, build_log_file)
597 else:
598 raise ValueError(
599 f"Unknown source type '{self.project.source}' is found "
600 f"for the '{self.project.name}' project")
601
602 def _download_from_git(self, directory: str, build_log_file: IO):
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300603 repo = self.project.origin
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300604 cached_source = os.path.join(directory, CACHED_SOURCE_DIR_NAME)
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300605
606 check_call(f"git clone --recursive {repo} {cached_source}",
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300607 cwd=directory, stderr=build_log_file,
608 stdout=build_log_file, shell=True)
609 check_call(f"git checkout --quiet {self.project.commit}",
610 cwd=cached_source, stderr=build_log_file,
611 stdout=build_log_file, shell=True)
612
613 def _unpack_zip(self, directory: str, build_log_file: IO):
Valeriy Savchenko00997d12020-07-10 11:20:36 +0300614 zip_files = list(glob.glob(directory + "/*.zip"))
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300615
616 if len(zip_files) == 0:
617 raise ValueError(
618 f"Couldn't find any zip files to unpack for the "
619 f"'{self.project.name}' project")
620
621 if len(zip_files) > 1:
622 raise ValueError(
623 f"Couldn't decide which of the zip files ({zip_files}) "
624 f"for the '{self.project.name}' project to unpack")
625
626 with zipfile.ZipFile(zip_files[0], "r") as zip_file:
627 zip_file.extractall(os.path.join(directory,
628 CACHED_SOURCE_DIR_NAME))
629
630 @staticmethod
631 def _run_download_script(directory: str, build_log_file: IO):
632 script_path = os.path.join(directory, DOWNLOAD_SCRIPT)
Valeriy Savchenko21bacc22020-07-10 10:54:18 +0300633 utils.run_script(script_path, build_log_file, directory,
634 out=LOCAL.stdout, err=LOCAL.stderr,
635 verbose=VERBOSE)
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300636
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300637 def _apply_patch(self, directory: str, build_log_file: IO):
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300638 patchfile_path = os.path.join(directory, PATCHFILE_NAME)
639 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
640
641 if not os.path.exists(patchfile_path):
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300642 self.out(" No local patches.\n")
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300643 return
644
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300645 self.out(" Applying patch.\n")
Valeriy Savchenkobbb8f172020-06-02 17:46:50 +0300646 try:
647 check_call(f"patch -p1 < '{patchfile_path}'",
648 cwd=patched_source,
649 stderr=build_log_file,
650 stdout=build_log_file,
651 shell=True)
652
653 except CalledProcessError:
654 stderr(f"Error: Patch failed. "
655 f"See {build_log_file.name} for details.\n")
656 sys.exit(1)
657
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300658 def out(self, what: str):
659 if not self.silent:
660 stdout(what)
661
662 def vout(self, what: str):
663 if VERBOSE >= 1:
664 self.out(what)
665
George Karpenkova8076602017-10-02 17:59:12 +0000666
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300667class TestProjectThread(threading.Thread):
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300668 def __init__(self, tasks_queue: TestQueue,
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300669 results_differ: threading.Event,
670 failure_flag: threading.Event):
671 """
672 :param results_differ: Used to signify that results differ from
673 the canonical ones.
674 :param failure_flag: Used to signify a failure during the run.
675 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300676 self.tasks_queue = tasks_queue
677 self.results_differ = results_differ
678 self.failure_flag = failure_flag
679 super().__init__()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000680
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300681 # Needed to gracefully handle interrupts with Ctrl-C
682 self.daemon = True
683
684 def run(self):
685 while not self.tasks_queue.empty():
686 try:
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300687 test_info = self.tasks_queue.get()
Valeriy Savchenko5b4f1432020-07-10 10:52:25 +0300688 init_logger(test_info.project.name)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300689
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300690 tester = ProjectTester(test_info)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300691 if not tester.test():
692 self.results_differ.set()
693
694 self.tasks_queue.task_done()
695
Valeriy Savchenkoa5b25032020-05-22 18:47:04 +0300696 except BaseException:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300697 self.failure_flag.set()
698 raise
Anna Zaksf0c41162011-10-06 23:26:27 +0000699
George Karpenkova8076602017-10-02 17:59:12 +0000700
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300701###############################################################################
702# Utility functions.
703###############################################################################
George Karpenkov3c128cb2017-10-30 19:40:33 +0000704
705
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300706def check_build(output_dir: str):
George Karpenkova8076602017-10-02 17:59:12 +0000707 """
708 Given the scan-build output directory, checks if the build failed
709 (by searching for the failures directories). If there are failures, it
710 creates a summary file in the output directory.
711
712 """
Anna Zaksf0c41162011-10-06 23:26:27 +0000713 # Check if there are failures.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300714 failures = glob.glob(output_dir + "/*/failures/*.stderr.txt")
715 total_failed = len(failures)
716
717 if total_failed == 0:
718 clean_up_empty_plists(output_dir)
719 clean_up_empty_folders(output_dir)
720
721 plists = glob.glob(output_dir + "/*/*.plist")
722 stdout(f"Number of bug reports "
723 f"(non-empty plist files) produced: {len(plists)}\n")
George Karpenkova8076602017-10-02 17:59:12 +0000724 return
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000725
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300726 stderr("Error: analysis failed.\n")
727 stderr(f"Total of {total_failed} failures discovered.\n")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000728
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300729 if total_failed > NUM_OF_FAILURES_IN_SUMMARY:
730 stderr(f"See the first {NUM_OF_FAILURES_IN_SUMMARY} below.\n")
731
732 for index, failed_log_path in enumerate(failures, start=1):
733 if index >= NUM_OF_FAILURES_IN_SUMMARY:
George Karpenkovff555ce2017-10-26 19:00:22 +0000734 break
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300735
736 stderr(f"\n-- Error #{index} -----------\n")
737
738 with open(failed_log_path, "r") as failed_log:
739 shutil.copyfileobj(failed_log, LOCAL.stdout)
740
741 if total_failed > NUM_OF_FAILURES_IN_SUMMARY:
742 stderr("See the results folder for more.")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000743
George Karpenkov65839bd2017-10-26 01:13:22 +0000744 sys.exit(1)
Anna Zaksf0c41162011-10-06 23:26:27 +0000745
Anna Zaksf0c41162011-10-06 23:26:27 +0000746
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300747def cleanup_reference_results(output_dir: str):
George Karpenkova8076602017-10-02 17:59:12 +0000748 """
749 Delete html, css, and js files from reference results. These can
750 include multiple copies of the benchmark source and so get very large.
751 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300752 extensions = ["html", "css", "js"]
753
754 for extension in extensions:
755 for file_to_rm in glob.glob(f"{output_dir}/*/*.{extension}"):
756 file_to_rm = os.path.join(output_dir, file_to_rm)
757 os.remove(file_to_rm)
Devin Coughlin9ea80332016-01-23 01:09:07 +0000758
759 # Remove the log file. It leaks absolute path names.
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300760 remove_log_file(output_dir)
Devin Coughlin9ea80332016-01-23 01:09:07 +0000761
George Karpenkova8076602017-10-02 17:59:12 +0000762
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300763def run_cmp_results(directory: str, strictness: int = 0) -> bool:
George Karpenkov1b51cbd2017-10-05 17:32:06 +0000764 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300765 Compare the warnings produced by scan-build.
766 strictness defines the success criteria for the test:
767 0 - success if there are no crashes or analyzer failure.
768 1 - success if there are no difference in the number of reported bugs.
769 2 - success if all the bug reports are identical.
770
771 :return success: Whether tests pass according to the strictness
772 criteria.
George Karpenkov1b51cbd2017-10-05 17:32:06 +0000773 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300774 tests_passed = True
775 start_time = time.time()
Anna Zaks4720a732011-11-05 05:20:48 +0000776
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300777 ref_dir = os.path.join(directory, REF_PREFIX + OUTPUT_DIR_NAME)
778 new_dir = os.path.join(directory, OUTPUT_DIR_NAME)
Anna Zaksf0c41162011-10-06 23:26:27 +0000779
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300780 # We have to go one level down the directory tree.
781 ref_list = glob.glob(ref_dir + "/*")
782 new_list = glob.glob(new_dir + "/*")
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000783
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300784 # Log folders are also located in the results dir, so ignore them.
785 ref_log_dir = os.path.join(ref_dir, LOG_DIR_NAME)
786 if ref_log_dir in ref_list:
787 ref_list.remove(ref_log_dir)
788 new_list.remove(os.path.join(new_dir, LOG_DIR_NAME))
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000789
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300790 if len(ref_list) != len(new_list):
791 stderr(f"Mismatch in number of results folders: "
792 f"{ref_list} vs {new_list}")
793 sys.exit(1)
Anna Zaksf0c41162011-10-06 23:26:27 +0000794
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300795 # There might be more then one folder underneath - one per each scan-build
796 # command (Ex: one for configure and one for make).
797 if len(ref_list) > 1:
798 # Assume that the corresponding folders have the same names.
799 ref_list.sort()
800 new_list.sort()
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000801
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300802 # Iterate and find the differences.
803 num_diffs = 0
804 for ref_dir, new_dir in zip(ref_list, new_list):
805 assert(ref_dir != new_dir)
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000806
Valeriy Savchenko53953892020-05-27 16:06:45 +0300807 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300808 stdout(f" Comparing Results: {ref_dir} {new_dir}\n")
809
810 patched_source = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
811
Valeriy Savchenko35dd0142020-06-03 12:29:41 +0300812 ref_results = CmpRuns.ResultsDirectory(ref_dir)
813 new_results = CmpRuns.ResultsDirectory(new_dir, patched_source)
814
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300815 # Scan the results, delete empty plist files.
816 num_diffs, reports_in_ref, reports_in_new = \
Valeriy Savchenko35dd0142020-06-03 12:29:41 +0300817 CmpRuns.dump_scan_build_results_diff(ref_results, new_results,
Valeriy Savchenko98f737f2020-05-25 15:56:51 +0300818 delete_empty=False,
819 out=LOCAL.stdout)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300820
821 if num_diffs > 0:
822 stdout(f"Warning: {num_diffs} differences in diagnostics.\n")
823
824 if strictness >= 2 and num_diffs > 0:
825 stdout("Error: Diffs found in strict mode (2).\n")
826 tests_passed = False
827
828 elif strictness >= 1 and reports_in_ref != reports_in_new:
829 stdout("Error: The number of results are different "
830 " strict mode (1).\n")
831 tests_passed = False
832
833 stdout(f"Diagnostic comparison complete "
834 f"(time: {time.time() - start_time:.2f}).\n")
835
836 return tests_passed
George Karpenkova8076602017-10-02 17:59:12 +0000837
Ted Kremenek3a0678e2015-09-08 03:50:52 +0000838
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300839def normalize_reference_results(directory: str, output_dir: str,
840 build_mode: int):
George Karpenkov3abfc3b2017-09-22 01:41:16 +0000841 """
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300842 Make the absolute paths relative in the reference results.
843 """
844 for dir_path, _, filenames in os.walk(output_dir):
845 for filename in filenames:
846 if not filename.endswith('plist'):
847 continue
848
849 plist = os.path.join(dir_path, filename)
850 data = plistlib.readPlist(plist)
851 path_prefix = directory
852
853 if build_mode == 1:
854 path_prefix = os.path.join(directory, PATCHED_SOURCE_DIR_NAME)
855
856 paths = [source[len(path_prefix) + 1:]
857 if source.startswith(path_prefix) else source
858 for source in data['files']]
859 data['files'] = paths
860
861 # Remove transient fields which change from run to run.
862 for diagnostic in data['diagnostics']:
863 if 'HTMLDiagnostics_files' in diagnostic:
864 diagnostic.pop('HTMLDiagnostics_files')
865
866 if 'clang_version' in data:
867 data.pop('clang_version')
868
869 plistlib.writePlist(data, plist)
870
871
872def get_build_log_path(output_dir: str) -> str:
873 return os.path.join(output_dir, LOG_DIR_NAME, BUILD_LOG_NAME)
874
875
876def remove_log_file(output_dir: str):
877 build_log_path = get_build_log_path(output_dir)
878
879 # Clean up the log file.
880 if os.path.exists(build_log_path):
Valeriy Savchenko53953892020-05-27 16:06:45 +0300881 if VERBOSE >= 1:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300882 stdout(f" Removing log file: {build_log_path}\n")
883
884 os.remove(build_log_path)
885
886
887def clean_up_empty_plists(output_dir: str):
888 """
889 A plist file is created for each call to the analyzer(each source file).
890 We are only interested on the once that have bug reports,
891 so delete the rest.
892 """
893 for plist in glob.glob(output_dir + "/*/*.plist"):
894 plist = os.path.join(output_dir, plist)
895
896 try:
Valeriy Savchenko98f737f2020-05-25 15:56:51 +0300897 with open(plist, "rb") as plist_file:
898 data = plistlib.load(plist_file)
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300899 # Delete empty reports.
900 if not data['files']:
901 os.remove(plist)
902 continue
903
Valeriy Savchenkofb4b5652020-06-01 17:15:38 +0300904 except InvalidFileException as e:
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300905 stderr(f"Error parsing plist file {plist}: {str(e)}")
906 continue
907
908
909def clean_up_empty_folders(output_dir: str):
910 """
911 Remove empty folders from results, as git would not store them.
912 """
913 subdirs = glob.glob(output_dir + "/*")
914 for subdir in subdirs:
915 if not os.listdir(subdir):
916 os.removedirs(subdir)
917
918
Valeriy Savchenko4902ca62020-05-21 18:28:36 +0300919if __name__ == "__main__":
Valeriy Savchenkod9944da2020-06-03 15:29:42 +0300920 print("SATestBuild.py should not be used on its own.")
921 print("Please use 'SATest.py build' instead")
922 sys.exit(1)