1#!/usr/bin/env python3 2# 3# Copyright © 2021 Google LLC 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the "Software"), 7# to deal in the Software without restriction, including without limitation 8# the rights to use, copy, modify, merge, publish, distribute, sublicense, 9# and/or sell copies of the Software, and to permit persons to whom the 10# Software is furnished to do so, subject to the following conditions: 11# 12# The above copyright notice and this permission notice (including the next 13# paragraph) shall be included in all copies or substantial portions of the 14# Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22# IN THE SOFTWARE. 23 24import argparse 25import io 26import re 27import socket 28import time 29 30 31class Connection: 32 def __init__(self, host, port, verbose): 33 self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 self.s.connect((host, port)) 35 self.s.setblocking(0) 36 self.verbose = verbose 37 38 def send_line(self, line): 39 if self.verbose: 40 print(f"IRC: sending {line}") 41 self.s.sendall((line + '\n').encode()) 42 43 def wait(self, secs): 44 for i in range(secs): 45 if self.verbose: 46 while True: 47 try: 48 data = self.s.recv(1024) 49 except io.BlockingIOError: 50 break 51 if data == "": 52 break 53 for line in data.decode().split('\n'): 54 print(f"IRC: received {line}") 55 time.sleep(1) 56 57 def quit(self): 58 self.send_line("QUIT") 59 self.s.shutdown(socket.SHUT_WR) 60 self.s.close() 61 62 63def read_flakes(results): 64 flakes = [] 65 csv = re.compile("(.*),(.*),(.*)") 66 for line in open(results, 'r').readlines(): 67 match = csv.match(line) 68 if match.group(2) == "Flake": 69 flakes.append(match.group(1)) 70 return flakes 71 72def main(): 73 parser = argparse.ArgumentParser() 74 parser.add_argument('--host', type=str, 75 help='IRC server hostname', required=True) 76 parser.add_argument('--port', type=int, 77 help='IRC server port', required=True) 78 parser.add_argument('--results', type=str, 79 help='results.csv file from deqp-runner or piglit-runner', required=True) 80 parser.add_argument('--known-flakes', type=str, 81 help='*-flakes.txt file passed to deqp-runner or piglit-runner', required=True) 82 parser.add_argument('--channel', type=str, 83 help='Known flakes report channel', required=True) 84 parser.add_argument('--url', type=str, 85 help='$CI_JOB_URL', required=True) 86 parser.add_argument('--runner', type=str, 87 help='$CI_RUNNER_DESCRIPTION', required=True) 88 parser.add_argument('--branch', type=str, 89 help='optional branch name') 90 parser.add_argument('--branch-title', type=str, 91 help='optional branch title') 92 parser.add_argument('--job', type=str, 93 help='$CI_JOB_ID', required=True) 94 parser.add_argument('--verbose', "-v", action="store_true", 95 help='log IRC interactions') 96 args = parser.parse_args() 97 98 flakes = read_flakes(args.results) 99 if not flakes: 100 exit(0) 101 102 known_flakes = [] 103 for line in open(args.known_flakes).readlines(): 104 line = line.strip() 105 if not line or line.startswith("#"): 106 continue 107 known_flakes.append(re.compile(line)) 108 109 irc = Connection(args.host, args.port, args.verbose) 110 111 # The nick needs to be something unique so that multiple runners 112 # connecting at the same time don't race for one nick and get blocked. 113 # freenode has a 16-char limit on nicks (9 is the IETF standard, but 114 # various servers extend that). So, trim off the common prefixes of the 115 # runner name, and append the job ID so that software runners with more 116 # than one concurrent job (think swrast) don't collide. For freedreno, 117 # that gives us a nick as long as db410c-N-JJJJJJJJ, and it'll be a while 118 # before we make it to 9-digit jobs (we're at 7 so far). 119 nick = args.runner 120 nick = nick.replace('mesa-', '') 121 nick = nick.replace('google-freedreno-', '') 122 nick += f'-{args.job}' 123 irc.send_line(f"NICK {nick}") 124 irc.send_line(f"USER {nick} unused unused: Gitlab CI Notifier") 125 irc.wait(10) 126 irc.send_line(f"JOIN {args.channel}") 127 irc.wait(1) 128 129 branchinfo = "" 130 if args.branch: 131 branchinfo = f" on branch {args.branch} ({args.branch_title})" 132 irc.send_line( 133 f"PRIVMSG {args.channel} :Flakes detected in job {args.url} on {args.runner}{branchinfo}:") 134 135 for flake in flakes: 136 status = "NEW " 137 for known in known_flakes: 138 if known.match(flake): 139 status = "" 140 break 141 142 irc.send_line(f"PRIVMSG {args.channel} :{status}{flake}") 143 144 irc.send_line( 145 f"PRIVMSG {args.channel} :See {args.url}/artifacts/browse/results/") 146 147 irc.quit() 148 149 150if __name__ == '__main__': 151 main() 152