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