[clang] d72859f - [scan-build-py] Update scan-build-py to allow outputing as SARIF
Haowei Wu via cfe-commits
cfe-commits at lists.llvm.org
Sun Feb 7 18:26:01 PST 2021
Author: Daniel Hwang
Date: 2021-02-07T18:25:50-08:00
New Revision: d72859ffa237bbb82c1ef7302f2d99534183f8ca
URL: https://github.com/llvm/llvm-project/commit/d72859ffa237bbb82c1ef7302f2d99534183f8ca
DIFF: https://github.com/llvm/llvm-project/commit/d72859ffa237bbb82c1ef7302f2d99534183f8ca.diff
LOG: [scan-build-py] Update scan-build-py to allow outputing as SARIF
clang static analysis reports can be generated in html, plist, or sarif
format. This updates scan-build-py to be able to specify SARIF as the
desired output format, as previously it only support plist and html
formats.
Differential Revision: https://reviews.llvm.org/D94251
Added:
Modified:
clang/tools/scan-build-py/libscanbuild/analyze.py
clang/tools/scan-build-py/libscanbuild/arguments.py
clang/tools/scan-build-py/libscanbuild/report.py
clang/tools/scan-build-py/tests/unit/test_analyze.py
clang/tools/scan-build-py/tests/unit/test_report.py
Removed:
################################################################################
diff --git a/clang/tools/scan-build-py/libscanbuild/analyze.py b/clang/tools/scan-build-py/libscanbuild/analyze.py
index a4bb499f86c9..dbe08be9b24a 100644
--- a/clang/tools/scan-build-py/libscanbuild/analyze.py
+++ b/clang/tools/scan-build-py/libscanbuild/analyze.py
@@ -52,7 +52,8 @@ def scan_build():
args = parse_args_for_scan_build()
# will re-assign the report directory as new output
- with report_directory(args.output, args.keep_empty) as args.output:
+ with report_directory(
+ args.output, args.keep_empty, args.output_format) as args.output:
# Run against a build command. there are cases, when analyzer run
# is not required. But we need to set up everything for the
# wrappers, because 'configure' needs to capture the CC/CXX values
@@ -79,7 +80,7 @@ def analyze_build():
args = parse_args_for_analyze_build()
# will re-assign the report directory as new output
- with report_directory(args.output, args.keep_empty) as args.output:
+ with report_directory(args.output, args.keep_empty, args.output_format) as args.output:
# Run the analyzer against a compilation db.
govern_analyzer_runs(args)
# Cover report generation and bug counting.
@@ -336,7 +337,7 @@ def analyze_compiler_wrapper_impl(result, execution):
@contextlib.contextmanager
-def report_directory(hint, keep):
+def report_directory(hint, keep, output_format):
""" Responsible for the report directory.
hint -- could specify the parent directory of the output directory.
@@ -355,7 +356,11 @@ def report_directory(hint, keep):
yield name
finally:
if os.listdir(name):
- msg = "Run 'scan-view %s' to examine bug reports."
+ if output_format != 'sarif':
+ # 'scan-view' currently does not support sarif format.
+ msg = "Run 'scan-view %s' to examine bug reports."
+ else:
+ msg = "View result at %s/results-merged.sarif."
keep = True
else:
if keep:
@@ -433,7 +438,7 @@ def wrapper(*args, **kwargs):
'direct_args', # arguments from command line
'force_debug', # kill non debug macros
'output_dir', # where generated report files shall go
- 'output_format', # it's 'plist', 'html', both or plist-multi-file
+ 'output_format', # it's 'plist', 'html', 'plist-html', 'plist-multi-file', or 'sarif'
'output_failures', # generate crash reports or not
'ctu']) # ctu control options
def run(opts):
@@ -537,6 +542,12 @@ def target():
dir=opts['output_dir'])
os.close(handle)
return name
+ elif opts['output_format'] == 'sarif':
+ (handle, name) = tempfile.mkstemp(prefix='result-',
+ suffix='.sarif',
+ dir=opts['output_dir'])
+ os.close(handle)
+ return name
return opts['output_dir']
try:
diff --git a/clang/tools/scan-build-py/libscanbuild/arguments.py b/clang/tools/scan-build-py/libscanbuild/arguments.py
index e258a4100331..e1d654b331cb 100644
--- a/clang/tools/scan-build-py/libscanbuild/arguments.py
+++ b/clang/tools/scan-build-py/libscanbuild/arguments.py
@@ -244,6 +244,14 @@ def create_analyze_parser(from_build_command):
action='store_const',
help="""Cause the results as a set of .plist files with extra
information on related files.""")
+ format_group.add_argument(
+ '--sarif',
+ '-sarif',
+ dest='output_format',
+ const='sarif',
+ default='html',
+ action='store_const',
+ help="""Cause the results as a result.sarif file.""")
advanced = parser.add_argument_group('advanced options')
advanced.add_argument(
diff --git a/clang/tools/scan-build-py/libscanbuild/report.py b/clang/tools/scan-build-py/libscanbuild/report.py
index 8bd6385fce69..734f530ebfc1 100644
--- a/clang/tools/scan-build-py/libscanbuild/report.py
+++ b/clang/tools/scan-build-py/libscanbuild/report.py
@@ -27,6 +27,7 @@ def document(args):
""" Generates cover report and returns the number of bugs/crashes. """
html_reports_available = args.output_format in {'html', 'plist-html'}
+ sarif_reports_available = args.output_format in {'sarif'}
logging.debug('count crashes and bugs')
crash_count = sum(1 for _ in read_crashes(args.output))
@@ -57,6 +58,11 @@ def document(args):
finally:
for fragment in fragments:
os.remove(fragment)
+
+ if sarif_reports_available:
+ logging.debug('merging sarif files')
+ merge_sarif_files(args.output)
+
return result
@@ -277,6 +283,98 @@ def empty(file_name):
if not duplicate(bug):
yield bug
+def merge_sarif_files(output_dir, sort_files=False):
+ """ Reads and merges all .sarif files in the given output directory.
+
+ Each sarif file in the output directory is understood as a single run
+ and thus appear separate in the top level runs array. This requires
+ modifying the run index of any embedded links in messages.
+ """
+
+ def empty(file_name):
+ return os.stat(file_name).st_size == 0
+
+ def update_sarif_object(sarif_object, runs_count_offset):
+ """
+ Given a SARIF object, checks its dictionary entries for a 'message' property.
+ If it exists, updates the message index of embedded links in the run index.
+
+ Recursively looks through entries in the dictionary.
+ """
+ if not isinstance(sarif_object, dict):
+ return sarif_object
+
+ if 'message' in sarif_object:
+ sarif_object['message'] = match_and_update_run(sarif_object['message'], runs_count_offset)
+
+ for key in sarif_object:
+ if isinstance(sarif_object[key], list):
+ # iterate through subobjects and update it.
+ arr = [update_sarif_object(entry, runs_count_offset) for entry in sarif_object[key]]
+ sarif_object[key] = arr
+ elif isinstance(sarif_object[key], dict):
+ sarif_object[key] = update_sarif_object(sarif_object[key], runs_count_offset)
+ else:
+ # do nothing
+ pass
+
+ return sarif_object
+
+
+ def match_and_update_run(message, runs_count_offset):
+ """
+ Given a SARIF message object, checks if the text property contains an embedded link and
+ updates the run index if necessary.
+ """
+ if 'text' not in message:
+ return message
+
+ # we only merge runs, so we only need to update the run index
+ pattern = re.compile(r'sarif:/runs/(\d+)')
+
+ text = message['text']
+ matches = re.finditer(pattern, text)
+ matches_list = list(matches)
+
+ # update matches from right to left to make increasing character length (9->10) smoother
+ for idx in range(len(matches_list) - 1, -1, -1):
+ match = matches_list[idx]
+ new_run_count = str(runs_count_offset + int(match.group(1)))
+ text = text[0:match.start(1)] + new_run_count + text[match.end(1):]
+
+ message['text'] = text
+ return message
+
+
+
+ sarif_files = (file for file in glob.iglob(os.path.join(output_dir, '*.sarif')) if not empty(file))
+ # exposed for testing since the order of files returned by glob is not guaranteed to be sorted
+ if sort_files:
+ sarif_files = list(sarif_files)
+ sarif_files.sort()
+
+ runs_count = 0
+ merged = {}
+ for sarif_file in sarif_files:
+ with open(sarif_file) as fp:
+ sarif = json.load(fp)
+ if 'runs' not in sarif:
+ continue
+
+ # start with the first file
+ if not merged:
+ merged = sarif
+ else:
+ # extract the run and append it to the merged output
+ for run in sarif['runs']:
+ new_run = update_sarif_object(run, runs_count)
+ merged['runs'].append(new_run)
+
+ runs_count += len(sarif['runs'])
+
+ with open(os.path.join(output_dir, 'results-merged.sarif'), 'w') as out:
+ json.dump(merged, out, indent=4, sort_keys=True)
+
def parse_bug_plist(filename):
""" Returns the generator of bugs from a single .plist file. """
diff --git a/clang/tools/scan-build-py/tests/unit/test_analyze.py b/clang/tools/scan-build-py/tests/unit/test_analyze.py
index 4b6f5d05211e..47d38a4da2cd 100644
--- a/clang/tools/scan-build-py/tests/unit/test_analyze.py
+++ b/clang/tools/scan-build-py/tests/unit/test_analyze.py
@@ -128,7 +128,7 @@ def call(self, params):
class RunAnalyzerTest(unittest.TestCase):
@staticmethod
- def run_analyzer(content, failures_report):
+ def run_analyzer(content, failures_report, output_format='plist'):
with libear.TemporaryDirectory() as tmpdir:
filename = os.path.join(tmpdir, 'test.cpp')
with open(filename, 'w') as handle:
@@ -141,31 +141,46 @@ def run_analyzer(content, failures_report):
'direct_args': [],
'file': filename,
'output_dir': tmpdir,
- 'output_format': 'plist',
+ 'output_format': output_format,
'output_failures': failures_report
}
spy = Spy()
result = sut.run_analyzer(opts, spy.call)
- return (result, spy.arg)
+ output_files = []
+ for entry in os.listdir(tmpdir):
+ output_files.append(entry)
+ return (result, spy.arg, output_files)
def test_run_analyzer(self):
content = "int div(int n, int d) { return n / d; }"
- (result, fwds) = RunAnalyzerTest.run_analyzer(content, False)
+ (result, fwds, _) = RunAnalyzerTest.run_analyzer(content, False)
self.assertEqual(None, fwds)
self.assertEqual(0, result['exit_code'])
def test_run_analyzer_crash(self):
content = "int div(int n, int d) { return n / d }"
- (result, fwds) = RunAnalyzerTest.run_analyzer(content, False)
+ (result, fwds, _) = RunAnalyzerTest.run_analyzer(content, False)
self.assertEqual(None, fwds)
self.assertEqual(1, result['exit_code'])
def test_run_analyzer_crash_and_forwarded(self):
content = "int div(int n, int d) { return n / d }"
- (_, fwds) = RunAnalyzerTest.run_analyzer(content, True)
+ (_, fwds, _) = RunAnalyzerTest.run_analyzer(content, True)
self.assertEqual(1, fwds['exit_code'])
self.assertTrue(len(fwds['error_output']) > 0)
+ def test_run_analyzer_with_sarif(self):
+ content = "int div(int n, int d) { return n / d; }"
+ (result, fwds, output_files) = RunAnalyzerTest.run_analyzer(content, False, output_format='sarif')
+ self.assertEqual(None, fwds)
+ self.assertEqual(0, result['exit_code'])
+
+ pattern = re.compile(r'^result-.+\.sarif$')
+ for f in output_files:
+ if re.match(pattern, f):
+ return
+ self.fail('no result sarif files found in output')
+
class ReportFailureTest(unittest.TestCase):
diff --git a/clang/tools/scan-build-py/tests/unit/test_report.py b/clang/tools/scan-build-py/tests/unit/test_report.py
index 60ec0d855ff3..57f0331c4621 100644
--- a/clang/tools/scan-build-py/tests/unit/test_report.py
+++ b/clang/tools/scan-build-py/tests/unit/test_report.py
@@ -3,6 +3,7 @@
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+import json
import libear
import libscanbuild.report as sut
import unittest
@@ -145,3 +146,516 @@ def test_with_single_file(self):
def test_empty(self):
self.assertEqual(
sut.commonprefix([]), '')
+
+class MergeSarifTest(unittest.TestCase):
+
+ def test_merging_sarif(self):
+ sarif1 = {
+ '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
+ 'runs': [
+ {
+ 'artifacts': [
+ {
+ 'length': 100,
+ 'location': {
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'mimeType': 'text/plain',
+ 'roles': [
+ 'resultFile'
+ ]
+ }
+ ],
+ 'columnKind': 'unicodeCodePoints',
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'threadFlows': [
+ {
+ 'locations': [
+ {
+ 'importance': 'important',
+ 'location': {
+ 'message': {
+ 'text': 'test message 1'
+ },
+ 'physicalLocation': {
+ 'artifactLocation': {
+ 'index': 0,
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'region': {
+ 'endColumn': 5,
+ 'startColumn': 1,
+ 'startLine': 2
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'codeFlows': [
+ {
+ 'threadFlows': [
+ {
+ 'locations': [
+ {
+ 'importance': 'important',
+ 'location': {
+ 'message': {
+ 'text': 'test message 2'
+ },
+ 'physicalLocation': {
+ 'artifactLocation': {
+ 'index': 0,
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'region': {
+ 'endColumn': 23,
+ 'startColumn': 9,
+ 'startLine': 10
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ 'tool': {
+ 'driver': {
+ 'fullName': 'clang static analyzer',
+ 'language': 'en-US',
+ 'name': 'clang',
+ 'rules': [
+ {
+ 'fullDescription': {
+ 'text': 'test rule for merge sarif test'
+ },
+ 'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
+ 'id': 'testId',
+ 'name': 'testName'
+ }
+ ],
+ 'version': 'test clang'
+ }
+ }
+ }
+ ],
+ 'version': '2.1.0'
+ }
+ sarif2 = {
+ '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
+ 'runs': [
+ {
+ 'artifacts': [
+ {
+ 'length': 1523,
+ 'location': {
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'mimeType': 'text/plain',
+ 'roles': [
+ 'resultFile'
+ ]
+ }
+ ],
+ 'columnKind': 'unicodeCodePoints',
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'threadFlows': [
+ {
+ 'locations': [
+ {
+ 'importance': 'important',
+ 'location': {
+ 'message': {
+ 'text': 'test message 3'
+ },
+ 'physicalLocation': {
+ 'artifactLocation': {
+ 'index': 0,
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'region': {
+ 'endColumn': 99,
+ 'startColumn': 99,
+ 'startLine': 17
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'codeFlows': [
+ {
+ 'threadFlows': [
+ {
+ 'locations': [
+ {
+ 'importance': 'important',
+ 'location': {
+ 'message': {
+ 'text': 'test message 4'
+ },
+ 'physicalLocation': {
+ 'artifactLocation': {
+ 'index': 0,
+ 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py'
+ },
+ 'region': {
+ 'endColumn': 305,
+ 'startColumn': 304,
+ 'startLine': 1
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ 'tool': {
+ 'driver': {
+ 'fullName': 'clang static analyzer',
+ 'language': 'en-US',
+ 'name': 'clang',
+ 'rules': [
+ {
+ 'fullDescription': {
+ 'text': 'test rule for merge sarif test'
+ },
+ 'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py',
+ 'id': 'testId',
+ 'name': 'testName'
+ }
+ ],
+ 'version': 'test clang'
+ }
+ }
+ }
+ ],
+ 'version': '2.1.0'
+ }
+
+ contents = [sarif1, sarif2]
+ with libear.TemporaryDirectory() as tmpdir:
+ for idx, content in enumerate(contents):
+ file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
+ with open(file_name, 'w') as handle:
+ json.dump(content, handle)
+
+ sut.merge_sarif_files(tmpdir, sort_files=True)
+
+ self.assertIn('results-merged.sarif', os.listdir(tmpdir))
+ with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
+ merged = json.load(f)
+ self.assertEqual(len(merged['runs']), 2)
+ self.assertEqual(len(merged['runs'][0]['results']), 2)
+ self.assertEqual(len(merged['runs'][1]['results']), 2)
+
+ expected = sarif1
+ for run in sarif2['runs']:
+ expected['runs'].append(run)
+
+ self.assertEqual(merged, expected)
+
+ def test_merge_updates_embedded_link(self):
+ sarif1 = {
+ 'runs': [
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 1-2 [link](sarif:/runs/1/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 2-1 [link](sarif:/runs/0/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 2-2 [link](sarif:/runs/0/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ sarif2 = {
+ 'runs': [
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 3-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 3-2 [link](sarif:/runs/1/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ },
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 4-1 [link](sarif:/runs/0/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 4-2 [link](sarif:/runs/0/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ sarif3 = {
+ 'runs': [
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 5-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 5-2 [link](sarif:/runs/1/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ },
+ {
+ 'results': [
+ {
+ 'codeFlows': [
+ {
+ 'message': {
+ 'text': 'test message 6-1 [link](sarif:/runs/0/results/0)'
+ },
+ 'threadFlows': [
+ {
+ 'message': {
+ 'text': 'test message 6-2 [link](sarif:/runs/0/results/0)'
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+ contents = [sarif1, sarif2, sarif3]
+
+ with libear.TemporaryDirectory() as tmpdir:
+ for idx, content in enumerate(contents):
+ file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
+ with open(file_name, 'w') as handle:
+ json.dump(content, handle)
+
+ sut.merge_sarif_files(tmpdir, sort_files=True)
+
+ self.assertIn('results-merged.sarif', os.listdir(tmpdir))
+ with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
+ merged = json.load(f)
+ self.assertEqual(len(merged['runs']), 6)
+
+ code_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['message']['text'] for x in range(6)]
+ thread_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['threadFlows'][0]['message']['text'] for x in range(6)]
+
+ # The run index should be updated for the second and third sets of runs
+ self.assertEqual(code_flows,
+ [
+ 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)',
+ 'test message 2-1 [link](sarif:/runs/0/results/0)',
+ 'test message 3-1 [link](sarif:/runs/3/results/0) [link2](sarif:/runs/3/results/0)',
+ 'test message 4-1 [link](sarif:/runs/2/results/0)',
+ 'test message 5-1 [link](sarif:/runs/5/results/0) [link2](sarif:/runs/5/results/0)',
+ 'test message 6-1 [link](sarif:/runs/4/results/0)'
+ ])
+ self.assertEquals(thread_flows,
+ [
+ 'test message 1-2 [link](sarif:/runs/1/results/0)',
+ 'test message 2-2 [link](sarif:/runs/0/results/0)',
+ 'test message 3-2 [link](sarif:/runs/3/results/0)',
+ 'test message 4-2 [link](sarif:/runs/2/results/0)',
+ 'test message 5-2 [link](sarif:/runs/5/results/0)',
+ 'test message 6-2 [link](sarif:/runs/4/results/0)'
+ ])
+
+ def test_overflow_run_count(self):
+ sarif1 = {
+ 'runs': [
+ {'results': [{
+ 'message': {'text': 'run 1-0 [link](sarif:/runs/1/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-1 [link](sarif:/runs/2/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-2 [link](sarif:/runs/3/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-3 [link](sarif:/runs/4/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-4 [link](sarif:/runs/5/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-5 [link](sarif:/runs/6/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-6 [link](sarif:/runs/7/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-7 [link](sarif:/runs/8/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-8 [link](sarif:/runs/9/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 1-9 [link](sarif:/runs/0/results/0)'}
+ }]}
+ ]
+ }
+ sarif2 = {
+ 'runs': [
+ {'results': [{
+ 'message': {'text': 'run 2-0 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/2/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-1 [link](sarif:/runs/2/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-2 [link](sarif:/runs/3/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-3 [link](sarif:/runs/4/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-4 [link](sarif:/runs/5/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-5 [link](sarif:/runs/6/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-6 [link](sarif:/runs/7/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-7 [link](sarif:/runs/8/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-8 [link](sarif:/runs/9/results/0)'}
+ }]},
+ {'results': [{
+ 'message': {'text': 'run 2-9 [link](sarif:/runs/0/results/0)'}
+ }]}
+ ]
+ }
+
+ contents = [sarif1, sarif2]
+ with libear.TemporaryDirectory() as tmpdir:
+ for idx, content in enumerate(contents):
+ file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx))
+ with open(file_name, 'w') as handle:
+ json.dump(content, handle)
+
+ sut.merge_sarif_files(tmpdir, sort_files=True)
+
+ self.assertIn('results-merged.sarif', os.listdir(tmpdir))
+ with open(os.path.join(tmpdir, 'results-merged.sarif')) as f:
+ merged = json.load(f)
+ self.assertEqual(len(merged['runs']), 20)
+
+ messages = [merged['runs'][x]['results'][0]['message']['text'] for x in range(20)]
+ self.assertEqual(messages,
+ [
+ 'run 1-0 [link](sarif:/runs/1/results/0)',
+ 'run 1-1 [link](sarif:/runs/2/results/0)',
+ 'run 1-2 [link](sarif:/runs/3/results/0)',
+ 'run 1-3 [link](sarif:/runs/4/results/0)',
+ 'run 1-4 [link](sarif:/runs/5/results/0)',
+ 'run 1-5 [link](sarif:/runs/6/results/0)',
+ 'run 1-6 [link](sarif:/runs/7/results/0)',
+ 'run 1-7 [link](sarif:/runs/8/results/0)',
+ 'run 1-8 [link](sarif:/runs/9/results/0)',
+ 'run 1-9 [link](sarif:/runs/0/results/0)',
+ 'run 2-0 [link](sarif:/runs/11/results/0) [link2](sarif:/runs/12/results/0)',
+ 'run 2-1 [link](sarif:/runs/12/results/0)',
+ 'run 2-2 [link](sarif:/runs/13/results/0)',
+ 'run 2-3 [link](sarif:/runs/14/results/0)',
+ 'run 2-4 [link](sarif:/runs/15/results/0)',
+ 'run 2-5 [link](sarif:/runs/16/results/0)',
+ 'run 2-6 [link](sarif:/runs/17/results/0)',
+ 'run 2-7 [link](sarif:/runs/18/results/0)',
+ 'run 2-8 [link](sarif:/runs/19/results/0)',
+ 'run 2-9 [link](sarif:/runs/10/results/0)'
+ ])
More information about the cfe-commits
mailing list