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 |
|
---|
24 | import argparse
|
---|
25 | import io
|
---|
26 | import re
|
---|
27 | import socket
|
---|
28 | import time
|
---|
29 |
|
---|
30 |
|
---|
31 | class 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 |
|
---|
63 | def 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 |
|
---|
72 | def 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 |
|
---|
150 | if __name__ == '__main__':
|
---|
151 | main()
|
---|