| #!/usr/bin/env python3 | |
| # | |
| # Copyright © 2021 Google LLC | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a | |
| # copy of this software and associated documentation files (the "Software"), | |
| # to deal in the Software without restriction, including without limitation | |
| # the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
| # and/or sell copies of the Software, and to permit persons to whom the | |
| # Software is furnished to do so, subject to the following conditions: | |
| # | |
| # The above copyright notice and this permission notice (including the next | |
| # paragraph) shall be included in all copies or substantial portions of the | |
| # Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| # IN THE SOFTWARE. | |
| import argparse | |
| import io | |
| import re | |
| import socket | |
| import time | |
| class Connection: | |
| def __init__(self, host, port, verbose): | |
| self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| self.s.connect((host, port)) | |
| self.s.setblocking(0) | |
| self.verbose = verbose | |
| def send_line(self, line): | |
| if self.verbose: | |
| print(f"IRC: sending {line}") | |
| self.s.sendall((line + '\n').encode()) | |
| def wait(self, secs): | |
| for i in range(secs): | |
| if self.verbose: | |
| while True: | |
| try: | |
| data = self.s.recv(1024) | |
| except io.BlockingIOError: | |
| break | |
| if data == "": | |
| break | |
| for line in data.decode().split('\n'): | |
| print(f"IRC: received {line}") | |
| time.sleep(1) | |
| def quit(self): | |
| self.send_line("QUIT") | |
| self.s.shutdown(socket.SHUT_WR) | |
| self.s.close() | |
| def read_flakes(results): | |
| flakes = [] | |
| csv = re.compile("(.*),(.*),(.*)") | |
| for line in open(results, 'r').readlines(): | |
| match = csv.match(line) | |
| if match.group(2) == "Flake": | |
| flakes.append(match.group(1)) | |
| return flakes | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--host', type=str, | |
| help='IRC server hostname', required=True) | |
| parser.add_argument('--port', type=int, | |
| help='IRC server port', required=True) | |
| parser.add_argument('--results', type=str, | |
| help='results.csv file from deqp-runner or piglit-runner', required=True) | |
| parser.add_argument('--known-flakes', type=str, | |
| help='*-flakes.txt file passed to deqp-runner or piglit-runner', required=True) | |
| parser.add_argument('--channel', type=str, | |
| help='Known flakes report channel', required=True) | |
| parser.add_argument('--url', type=str, | |
| help='$CI_JOB_URL', required=True) | |
| parser.add_argument('--runner', type=str, | |
| help='$CI_RUNNER_DESCRIPTION', required=True) | |
| parser.add_argument('--branch', type=str, | |
| help='optional branch name') | |
| parser.add_argument('--branch-title', type=str, | |
| help='optional branch title') | |
| parser.add_argument('--job', type=str, | |
| help='$CI_JOB_ID', required=True) | |
| parser.add_argument('--verbose', "-v", action="store_true", | |
| help='log IRC interactions') | |
| args = parser.parse_args() | |
| flakes = read_flakes(args.results) | |
| if not flakes: | |
| exit(0) | |
| known_flakes = [] | |
| for line in open(args.known_flakes).readlines(): | |
| line = line.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| known_flakes.append(re.compile(line)) | |
| irc = Connection(args.host, args.port, args.verbose) | |
| # The nick needs to be something unique so that multiple runners | |
| # connecting at the same time don't race for one nick and get blocked. | |
| # freenode has a 16-char limit on nicks (9 is the IETF standard, but | |
| # various servers extend that). So, trim off the common prefixes of the | |
| # runner name, and append the job ID so that software runners with more | |
| # than one concurrent job (think swrast) don't collide. For freedreno, | |
| # that gives us a nick as long as db410c-N-JJJJJJJJ, and it'll be a while | |
| # before we make it to 9-digit jobs (we're at 7 so far). | |
| nick = args.runner | |
| nick = nick.replace('mesa-', '') | |
| nick = nick.replace('google-freedreno-', '') | |
| nick += f'-{args.job}' | |
| irc.send_line(f"NICK {nick}") | |
| irc.send_line(f"USER {nick} unused unused: Gitlab CI Notifier") | |
| irc.wait(10) | |
| irc.send_line(f"JOIN {args.channel}") | |
| irc.wait(1) | |
| branchinfo = "" | |
| if args.branch: | |
| branchinfo = f" on branch {args.branch} ({args.branch_title})" | |
| irc.send_line( | |
| f"PRIVMSG {args.channel} :Flakes detected in job {args.url} on {args.runner}{branchinfo}:") | |
| for flake in flakes: | |
| status = "NEW " | |
| for known in known_flakes: | |
| if known.match(flake): | |
| status = "" | |
| break | |
| irc.send_line(f"PRIVMSG {args.channel} :{status}{flake}") | |
| irc.send_line( | |
| f"PRIVMSG {args.channel} :See {args.url}/artifacts/browse/results/") | |
| irc.quit() | |
| if __name__ == '__main__': | |
| main() |