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