1
0
Fork 0
llvm-premerge-checks/scripts/phabtalk/phabtalk.py
Mikhail Goncharov 7faaec98e7 Attach diffs produced by git-clang-format as lint messages.
E.g.: https://reviews.llvm.org/D71197?id=233029
Will add a link to file and code to apply the patch in the next PR.

+ don't create TARGET_DIR in scripts;
+ updated section about local build;
+ partially specified inputs / outputs of scripts so it's more transparent what are the results;
+ added symlink to compile_command.json (clang-tidy will need it);
+ add IDEA files to .gitignore.
2019-12-11 08:55:38 +01:00

307 lines
No EOL
12 KiB
Python
Executable file

#!/usr/bin/env python3
# Copyright 2019 Google LLC
#
# Licensed under the the Apache License v2.0 with LLVM Exceptions (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://llvm.org/LICENSE.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Upload build results to Phabricator.
As I did not like the Jenkins plugin, we're using this script to upload the
build status, a summary and the test reults to Phabricator."""
import argparse
import os
import re
import socket
import time
from typing import Optional
from lxml import etree
from phabricator import Phabricator
class TestResults:
def __init__(self):
self.result_type = None # type: str
self.unit = [] #type: List
self.lint = []
self.test_stats = {
'pass':0,
'fail':0,
'skip':0
} # type: Dict[str, int]
class PhabTalk:
"""Talk to Phabricator to upload build results.
See https://secure.phabricator.com/conduit/method/harbormaster.sendmessage/
"""
def __init__(self, token: Optional[str], host: Optional[str], dryrun: bool):
self._phab = None # type: Optional[Phabricator]
if not dryrun:
self._phab = Phabricator(token=token, host=host)
self._phab.update_interfaces()
@property
def dryrun(self):
return self._phab is None
def _get_revision_id(self, diff: str):
"""Get the revision ID for a diff from Phabricator."""
if self.dryrun:
return None
result = self._phab.differential.querydiffs(ids=[diff])
return 'D' + result[diff]['revisionID']
def _comment_on_diff(self, diff: str, text: str):
"""Add a comment to a differential based on the diff_id"""
print('Sending comment to diff {}:'.format(diff))
print(text)
self._comment_on_revision(self._get_revision_id(diff), text)
def _comment_on_revision(self, revision: str, text: str):
"""Add comment on a differential based on the revision id."""
transactions = [{
'type' : 'comment',
'value' : text
}]
if self.dryrun:
print('differential.revision.edit =================')
print('Transactions: {}'.format(transactions))
return
# API details at
# https://secure.phabricator.com/conduit/method/differential.revision.edit/
self._phab.differential.revision.edit(objectIdentifier=revision, transactions=transactions)
def _comment_on_diff_from_file(self, diff: str, text_file_path: str, test_results: TestResults, buildresult:str):
"""Comment on a diff, read text from file."""
header = ''
if test_results.result_type is None:
# do this if there are no test results
header = 'Build result: {} - '.format(buildresult)
else:
header = 'Build result: {} - '.format(test_results.result_type)
header += '{} tests passed, {} failed and {} were skipped.\n'.format(
test_results.test_stats['pass'],
test_results.test_stats['fail'],
test_results.test_stats['skip'],
)
for test_case in test_results.unit:
if test_case['result'] == 'fail':
header += ' failed: {}/{}\n'.format(test_case['namespace'], test_case['name'])
text = ''
if text_file_path is not None and os.path.exists(text_file_path):
with open(text_file_path) as input_file:
text = input_file.read()
if len(header+text) == 0:
print('Comment for Phabricator would be empty. Not posting it.')
return
self._comment_on_diff(diff, header + text)
def _report_test_results(self, phid: str, test_results: TestResults, build_result: str):
"""Report failed tests to phabricator.
Only reporting failed tests as the full test suite is too large to upload.
"""
# use jenkins build status if possible
result = self._translate_jenkins_status(build_result)
# fall back to test results if Jenkins status is not availble
if result is None:
result = test_results.result_type
# If we do not have a proper status: fail the build.
if result is None:
result = 'fail'
if self.dryrun:
print('harbormaster.sendmessage =================')
print('type: {}'.format(result))
print('unit: {}'.format(test_results.unit))
print('lint: {}'.format(test_results.lint))
return
# API details at
# https://secure.phabricator.com/conduit/method/harbormaster.sendmessage/
self._phab.harbormaster.sendmessage(
buildTargetPHID=phid,
type=result,
unit=test_results.unit,
lint=test_results.lint)
def _compute_test_results(self, build_result_file: str, clang_format_patch: str) -> TestResults:
result = TestResults()
if build_result_file is None:
# If no result file is specified: assume this is intentional
result.result_type = None
return result
if not os.path.exists(build_result_file):
print('Warning: Could not find test results file: {}'.format(build_result_file))
result.result_type = None
return result
root_node = etree.parse(build_result_file)
result.result_type = 'pass'
for test_case in root_node.xpath('//testcase'):
test_result = self._test_case_status(test_case)
result.test_stats[test_result] += 1
if test_result == 'fail':
failure = test_case.find('failure')
test_result = {
'name' : test_case.attrib['name'],
'namespace' : test_case.attrib['classname'],
'result' : test_result,
'duration' : float(test_case.attrib['time']),
'details' : failure.text
}
result.result_type = 'fail'
result.unit.append(test_result)
if os.path.exists(clang_format_patch):
diffs = self.parse_patch(open(clang_format_patch, 'rt'))
for d in diffs:
lint_message = {
'name': 'Please fix the formatting',
'severity': 'autofix',
'code': 'clang-format',
'path': d['filename'],
'line': d['line'],
'char': 1,
'description': '```\n' + d['diff'] + '\n```',
}
result.lint.append(lint_message)
return result
def parse_patch(self, patch) -> []:
"""Extract the changed lines from `patch` file.
The return value is a list of dictionaries {filename, line, diff}.
Diff must be generated with -U0 (no context lines).
"""
entries = []
lines = []
filename = None
line_number = 0
for line in patch:
match = re.search(r'^(\+\+\+|---) [^/]+/(.*)', line)
if match:
if len(lines) > 0:
entries.append({
'filename': filename,
'diff': ''.join(lines),
'line': line_number,
})
lines = []
filename = match.group(2).rstrip('\r\n')
continue
match = re.search(r'^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))?', line)
if match:
if len(lines) > 0:
entries.append({
'filename': filename,
'diff': ''.join(lines),
'line': line_number,
})
lines = []
line_number = int(match.group(1))
continue
if line.startswith('+') or line.startswith('-'):
lines.append(line)
if len(lines) > 0:
entries.append({
'filename': filename,
'diff': ''.join(lines),
'line': line_number,
})
return entries
@staticmethod
def _test_case_status(test_case) -> str:
"""Get the status of a test case based on an etree node."""
if test_case.find('failure') is not None:
return 'fail'
if test_case.find('skipped') is not None:
return 'skip'
return 'pass'
def report_all(self, diff_id: str, ph_id: str, test_result_file: str,
comment_file: str, build_result: str, clang_format_patch: str):
test_results = self._compute_test_results(test_result_file, clang_format_patch)
self._report_test_results(ph_id, test_results, build_result)
self._comment_on_diff_from_file(diff_id, comment_file, test_results, build_result)
print('reporting completed.')
@staticmethod
def _translate_jenkins_status(jenkins_status: str) -> str:
"""
Translate the build status form Jenkins to Phabricator.
Jenkins semantics: https://jenkins.llvm-merge-guard.org/pipeline-syntax/globals#currentBuild
Phabricator semantics: https://reviews.llvm.org/conduit/method/harbormaster.sendmessage/
"""
if jenkins_status.lower() == 'success':
return 'pass'
if jenkins_status.lower() == 'null':
return 'working'
return 'fail'
def main():
args = _parse_args()
errorcount = 0
while True:
# retry on connenction problems
try:
# TODO: separate build of test results and sending the individual messages (to diff and test results)
p = PhabTalk(args.conduit_token, args.host, args.dryrun)
p.report_all(args.diff_id, args.ph_id, args.test_result_file,
args.comment_file, args.buildresult,
args.clang_format_patch)
except socket.timeout as e:
errorcount += 1
if errorcount > 5:
print('Connection to Pharicator failed, giving up: {}'.format(e))
raise
print('Connection to Pharicator failed, retrying: {}'.format(e))
time.sleep(errorcount*10)
break
def _parse_args():
parser = argparse.ArgumentParser(description='Write build status back to Phabricator.')
parser.add_argument('ph_id', type=str)
parser.add_argument('diff_id', type=str)
parser.add_argument('--comment-file', type=str, dest='comment_file', default=None)
parser.add_argument('--test-result-file', type=str, dest='test_result_file',
default=os.path.join(os.path.curdir,'test-results.xml'))
parser.add_argument('--conduit-token', type=str, dest='conduit_token', default=None)
parser.add_argument('--host', type=str, dest='host', default="None",
help="full URL to API with trailing slash, e.g. https://reviews.llvm.org/api/")
parser.add_argument('--dryrun', action='store_true',help="output results to the console, do not report back to the server")
parser.add_argument('--buildresult', type=str, default=None,
choices=['SUCCESS', 'UNSTABLE', 'FAILURE', 'null'])
parser.add_argument('--clang-format-patch', type=str, default=None,
dest='clang_format_patch',
help="patch produced by git-clang-format")
return parser.parse_args()
if __name__ == '__main__':
main()