17ec681f3Smrg#!/usr/bin/env python3 27ec681f3Smrg# 37ec681f3Smrg# Copyright © 2021 Google LLC 47ec681f3Smrg# 57ec681f3Smrg# Permission is hereby granted, free of charge, to any person obtaining a 67ec681f3Smrg# copy of this software and associated documentation files (the "Software"), 77ec681f3Smrg# to deal in the Software without restriction, including without limitation 87ec681f3Smrg# the rights to use, copy, modify, merge, publish, distribute, sublicense, 97ec681f3Smrg# and/or sell copies of the Software, and to permit persons to whom the 107ec681f3Smrg# Software is furnished to do so, subject to the following conditions: 117ec681f3Smrg# 127ec681f3Smrg# The above copyright notice and this permission notice (including the next 137ec681f3Smrg# paragraph) shall be included in all copies or substantial portions of the 147ec681f3Smrg# Software. 157ec681f3Smrg# 167ec681f3Smrg# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 177ec681f3Smrg# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 187ec681f3Smrg# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 197ec681f3Smrg# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 207ec681f3Smrg# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 217ec681f3Smrg# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 227ec681f3Smrg# IN THE SOFTWARE. 237ec681f3Smrg 247ec681f3Smrgimport argparse 257ec681f3Smrgimport io 267ec681f3Smrgimport re 277ec681f3Smrgimport socket 287ec681f3Smrgimport time 297ec681f3Smrg 307ec681f3Smrg 317ec681f3Smrgclass Connection: 327ec681f3Smrg def __init__(self, host, port, verbose): 337ec681f3Smrg self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 347ec681f3Smrg self.s.connect((host, port)) 357ec681f3Smrg self.s.setblocking(0) 367ec681f3Smrg self.verbose = verbose 377ec681f3Smrg 387ec681f3Smrg def send_line(self, line): 397ec681f3Smrg if self.verbose: 407ec681f3Smrg print(f"IRC: sending {line}") 417ec681f3Smrg self.s.sendall((line + '\n').encode()) 427ec681f3Smrg 437ec681f3Smrg def wait(self, secs): 447ec681f3Smrg for i in range(secs): 457ec681f3Smrg if self.verbose: 467ec681f3Smrg while True: 477ec681f3Smrg try: 487ec681f3Smrg data = self.s.recv(1024) 497ec681f3Smrg except io.BlockingIOError: 507ec681f3Smrg break 517ec681f3Smrg if data == "": 527ec681f3Smrg break 537ec681f3Smrg for line in data.decode().split('\n'): 547ec681f3Smrg print(f"IRC: received {line}") 557ec681f3Smrg time.sleep(1) 567ec681f3Smrg 577ec681f3Smrg def quit(self): 587ec681f3Smrg self.send_line("QUIT") 597ec681f3Smrg self.s.shutdown(socket.SHUT_WR) 607ec681f3Smrg self.s.close() 617ec681f3Smrg 627ec681f3Smrg 637ec681f3Smrgdef read_flakes(results): 647ec681f3Smrg flakes = [] 657ec681f3Smrg csv = re.compile("(.*),(.*),(.*)") 667ec681f3Smrg for line in open(results, 'r').readlines(): 677ec681f3Smrg match = csv.match(line) 687ec681f3Smrg if match.group(2) == "Flake": 697ec681f3Smrg flakes.append(match.group(1)) 707ec681f3Smrg return flakes 717ec681f3Smrg 727ec681f3Smrgdef main(): 737ec681f3Smrg parser = argparse.ArgumentParser() 747ec681f3Smrg parser.add_argument('--host', type=str, 757ec681f3Smrg help='IRC server hostname', required=True) 767ec681f3Smrg parser.add_argument('--port', type=int, 777ec681f3Smrg help='IRC server port', required=True) 787ec681f3Smrg parser.add_argument('--results', type=str, 797ec681f3Smrg help='results.csv file from deqp-runner or piglit-runner', required=True) 807ec681f3Smrg parser.add_argument('--known-flakes', type=str, 817ec681f3Smrg help='*-flakes.txt file passed to deqp-runner or piglit-runner', required=True) 827ec681f3Smrg parser.add_argument('--channel', type=str, 837ec681f3Smrg help='Known flakes report channel', required=True) 847ec681f3Smrg parser.add_argument('--url', type=str, 857ec681f3Smrg help='$CI_JOB_URL', required=True) 867ec681f3Smrg parser.add_argument('--runner', type=str, 877ec681f3Smrg help='$CI_RUNNER_DESCRIPTION', required=True) 887ec681f3Smrg parser.add_argument('--branch', type=str, 897ec681f3Smrg help='optional branch name') 907ec681f3Smrg parser.add_argument('--branch-title', type=str, 917ec681f3Smrg help='optional branch title') 927ec681f3Smrg parser.add_argument('--job', type=str, 937ec681f3Smrg help='$CI_JOB_ID', required=True) 947ec681f3Smrg parser.add_argument('--verbose', "-v", action="store_true", 957ec681f3Smrg help='log IRC interactions') 967ec681f3Smrg args = parser.parse_args() 977ec681f3Smrg 987ec681f3Smrg flakes = read_flakes(args.results) 997ec681f3Smrg if not flakes: 1007ec681f3Smrg exit(0) 1017ec681f3Smrg 1027ec681f3Smrg known_flakes = [] 1037ec681f3Smrg for line in open(args.known_flakes).readlines(): 1047ec681f3Smrg line = line.strip() 1057ec681f3Smrg if not line or line.startswith("#"): 1067ec681f3Smrg continue 1077ec681f3Smrg known_flakes.append(re.compile(line)) 1087ec681f3Smrg 1097ec681f3Smrg irc = Connection(args.host, args.port, args.verbose) 1107ec681f3Smrg 1117ec681f3Smrg # The nick needs to be something unique so that multiple runners 1127ec681f3Smrg # connecting at the same time don't race for one nick and get blocked. 1137ec681f3Smrg # freenode has a 16-char limit on nicks (9 is the IETF standard, but 1147ec681f3Smrg # various servers extend that). So, trim off the common prefixes of the 1157ec681f3Smrg # runner name, and append the job ID so that software runners with more 1167ec681f3Smrg # than one concurrent job (think swrast) don't collide. For freedreno, 1177ec681f3Smrg # that gives us a nick as long as db410c-N-JJJJJJJJ, and it'll be a while 1187ec681f3Smrg # before we make it to 9-digit jobs (we're at 7 so far). 1197ec681f3Smrg nick = args.runner 1207ec681f3Smrg nick = nick.replace('mesa-', '') 1217ec681f3Smrg nick = nick.replace('google-freedreno-', '') 1227ec681f3Smrg nick += f'-{args.job}' 1237ec681f3Smrg irc.send_line(f"NICK {nick}") 1247ec681f3Smrg irc.send_line(f"USER {nick} unused unused: Gitlab CI Notifier") 1257ec681f3Smrg irc.wait(10) 1267ec681f3Smrg irc.send_line(f"JOIN {args.channel}") 1277ec681f3Smrg irc.wait(1) 1287ec681f3Smrg 1297ec681f3Smrg branchinfo = "" 1307ec681f3Smrg if args.branch: 1317ec681f3Smrg branchinfo = f" on branch {args.branch} ({args.branch_title})" 1327ec681f3Smrg irc.send_line( 1337ec681f3Smrg f"PRIVMSG {args.channel} :Flakes detected in job {args.url} on {args.runner}{branchinfo}:") 1347ec681f3Smrg 1357ec681f3Smrg for flake in flakes: 1367ec681f3Smrg status = "NEW " 1377ec681f3Smrg for known in known_flakes: 1387ec681f3Smrg if known.match(flake): 1397ec681f3Smrg status = "" 1407ec681f3Smrg break 1417ec681f3Smrg 1427ec681f3Smrg irc.send_line(f"PRIVMSG {args.channel} :{status}{flake}") 1437ec681f3Smrg 1447ec681f3Smrg irc.send_line( 1457ec681f3Smrg f"PRIVMSG {args.channel} :See {args.url}/artifacts/browse/results/") 1467ec681f3Smrg 1477ec681f3Smrg irc.quit() 1487ec681f3Smrg 1497ec681f3Smrg 1507ec681f3Smrgif __name__ == '__main__': 1517ec681f3Smrg main() 152