-
Notifications
You must be signed in to change notification settings - Fork 0
/
autotest.py
372 lines (331 loc) · 13.4 KB
/
autotest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#! /usr/bin/env python3
usage = """
Python script for advent of code which downloads the problem description,
attempts to extract sample input and corresponding output,
then runs sol.py on the sample input whenever sol.py is modified until
sol.py gives the sample output. When it does, sol.py gets run on the real input
and if that succeeds, the last printed word gets submitted automatically.
call as `autotest.py {year} {day}` with the
environment variable $AOCSession set to the value of your session cookie
suggested code for sol.py:
```
import sys
fname=sys.argv[1] if len(sys.argv)>1 else "input"
f=list(open(fname))
```
files used:
sol.py This program assumes that your solution for the part you are
currently working on is in this file.
Run as `python3 sol.py {input}` where {input} is the name of
a file from which sol.py is expected to read the input
for the day's problem
input Your personal input (https://adventofcode.com/{year}/day/{day}/input)
input1 the automatically extracted sample input
By default this is the first non-inline code block.
It may be wrong, and if so you must manually edit it and restart this
program if you want it to work correctly.
If no appropriate sample input is found, you must create this file
to use this program.
output1 the automatically extracted sample output for part 1
By default, this is the last highlighed thing at the end of a code tag.
It may be wrong, and if so you must manually edit it and restart this
program if you want it to work correctly.
If no appropriate sample output is found, you must create this file
to use this program.
output2 the automatically extracted sample output for part 2
1page.html the page when solving part 1
2page.html the page when solving part 2
wrongAnswers a text file containing a list of answers which have been rejected
Hopefully avoids repeatedly submitting wrong answers
Does not distinguish between part 1 and part 2
tmp stores the output of sol.py on sample input
Can be deleted without consequence except while sol.py is running
tmpreal stores the output of sol.py on the real input
Can be deleted without consequence except while sol.py is running
"""
import sys
import os
import subprocess
import math
from datetime import datetime
import urllib.request as r
print(sys.argv[0])
sesh = os.environ["AOCSession"]
headers={"Cookie":"session="+sesh, "User-Agent":"autotest.py (https://github.com/penteract/adventofcode)"}
def writeTo(file,content):
with open(file,mode="w") as f:
f.write(content)
def readString(file):
with open(file) as f:
return "".join(f)
def get_or_save(url,file):
if file is None or not os.path.isfile(file):
print("requesting url",repr(url))
r1=r.urlopen(r.Request(url,headers=headers))
s = "".join(l.decode() for l in r1)
if file is not None:
writeTo(file,s)
else:
s=readString(file)
return s
def tee_with_exitcode(command,file):
#print (f"(exit `(({command} 2>&1 ; echo $? >&3) | tee ${file} >&4) 3>&1` ) 4>&1")
return subprocess.run(f"(exit `(({command} 2>&1 ; echo $? >&3) | tee {file} >&4) 3>&1` ) 4>&1", shell=True)
def submit(part,answer):
global submittime
url = f"https://adventofcode.com/{year}/day/{day}/answer"
print(f"submitting",repr(answer),"to url",repr(url))
resp = r.urlopen(r.Request(url,data=bytes(f"level={part}&answer={answer}","utf8"),headers=headers))
submittime=datetime.now()
print("time",submittime)
print("response:")
prnt=False
content = ""
correct=False
for line in resp:
line = line.decode()
if "<article>" in line:
prnt=True
if prnt:
print(line,end="")
content+=line
if "</article>" in line:
prnt=False
return resp,content
for arg in sys.argv[1:]:
if "help" in arg or "-h" in arg:
print(usage)
exit(0)
if len(sys.argv)<3:
#sys.argv=[None,"2022","1"]
raise Exception("not given year and day")
year=sys.argv[1]
day=sys.argv[2]
dayurl = f"https://adventofcode.com/{year}/day/{day}"
print("PRIVATE INPUT:")
print(get_or_save(dayurl + "/input", "input"))
bad_answers=set()
bad_toohigh = None
bad_toolow = None
def fromMaybe(x,mx):
return (mx if mx is not None else x)
if os.path.isfile("wrongAnswers"):
with open("wrongAnswers") as f:
for line in f:
if "[TOO HIGH]" in line:
line = line.split()[0]
bad_toohigh = min(int(line), fromMaybe(math.inf, bad_toohigh))
if "[TOO LOW]" in line:
line = line.split()[0]
bad_toolow = max(int(line), fromMaybe(-math.inf, bad_toolow))
bad_answers.add(line.strip())
def add_bad(ans, content):
global bad_toohigh, bad_toolow
extra = ""
if "too high" in content:
bad_toohigh = min(int(ans), fromMaybe(math.inf, bad_toohigh))
extra = " [TOO HIGH]"
if "too low" in content:
bad_toolow = max(int(ans), fromMaybe(-math.inf, bad_toolow))
extra = " [TOO LOW]"
bad_answers.add(ans)
with open("wrongAnswers", mode="a") as f:
print(ans + extra, file=f)
def sanitize(eg):
eg = eg.replace("<em>", "").replace("</em>", "")
eg = eg.replace(">", ">").replace("<", "<").replace("&", "&")
return eg
"""
An observational study on the tags used for the correct output given the sample input:
Prior to 2020, there's no hope (there wasn't always a clear sample input and sample output)
Since 2020, the sample output has usually been the last <em> tag inside a <code> tag.
exceptions
<em><code>ans</code></em>
2020 day 8, 9, 10 (and perhaps the rest of the year)
2021 day 1
2022 day 1
In 2020 there are some days with multiple exaples
2020 day 10 part 1
2020 day 1, there is a <code> tag in the middle of an <em> tag which is not an answer
"""
def get_out(s):
last = s.rfind("</em></code>")
last_ = s.rfind("</code></em>")
if last <= last_:
last = last_
assert last!=-1 #can't find sample output
start = s.rfind("<code>",0,last)+len("<code>")
assert start >= len("<code>") # can't find start of sample output !!!
else:
start = s.rfind("<em>",0,last)+len("<em>")
assert start >= len("<em>") # can't find start of sample output !!!
return start,last
def surrounding_tag(s,i,tag):
"""find a tag surrounding location i"""
opn = s.rfind("<"+tag+">",0,i)
if opn == -1: return False
opn+=len("<"+tag+">")
close = s.find("</"+tag+">",opn)
assert close!=-1 #cannot find matching tag
if close<i:
return False
return (opn,close)
#potentially problematic examples
#https://adventofcode.com/2020/day/2
#https://adventofcode.com/2022/day/1
def find_list(s,start,last,part):
t = surrounding_tag(s,start,"ul")
if not t:
return False
ul = s[t[0]:t[1]]
examples = []
print("found list, looking for multiple examples")
for line in ul.split("<li>"):
try:
start,end = get_out(line)
except Exception:
continue
out = sanitize(line[start:end])
start=line.find("<code>")+len("<code>")
if start<len("<code>"):
continue
end=line.find("</code>")
assert end!=-1 # no matching tag
inp = line[start:end]
if "<" in inp: #input contains a tag - this also
continue
inp = sanitize(inp)
if len(inp)<=1:
continue
i=len(examples)+1
print("example",i,"input:")
print(inp)
print("example",i,"output:")
print(out)
writeTo(f"output{part}-{i}",out)
writeTo(f"input{part}-{i}",inp)
examples.append((f"input{part}-{i}", out))
return examples
def find_examples(part):
"""begin by finding the output. If it's in a list, assume there's a list of examples,
otherwise just assume there's one example"""
s=get_or_save(dayurl, part+"page.html")
outputfile = "output"+part+"-1"
if not os.path.isfile("output"+part+"-1"):
print("Trying to find sample output to save in ",outputfile)
s=get_or_save(dayurl, str(part)+"page.html")
completed=s.count("Your puzzle answer was")
if str(completed+1)!=part:
raise Exception(f"the given part ({part}) cannot be done when {completed} are completed")
start,last=get_out(s)
if part=="2" and start<=s.find("--- Part Two ---"):
raise Exception("Can't find part 2 sample output")
#check if it's in a list
if (result:=find_list(s,start,last,part)):
return result
sampleout = sanitize(s[start:last])
writeTo(outputfile,sampleout)
else:
i=1
examples=[]
while os.path.isfile(f"output{part}-{i}"):
out = readString(f"output{part}-{i}").strip()
if os.path.isfile(f"input{part}-{i}"):
examples.append((f"input{part}-{i}",out))
elif os.path.isfile(f"input{i}"):
examples.append((f"input{i}",out))
else:
if part=="1":
sampleout = out
break
i+=1
if examples:
return examples
if not os.path.isfile("input1"):
print("Trying to find sample input to save in ","input1")
s=get_or_save(dayurl, part+"page.html")
start=s.find("<pre><code>")
end=s.find("</code></pre>")
assert start!=-1 # can't find pre block
assert end!=-1 # can't find end of pre block !!!
eg=sanitize(s[start+len("<pre><code>"):end])
writeTo("input1",eg)
print("sample input:")
print(eg)
return [("input1",sampleout)]
def doPart(part=None):
if part is None:
s = get_or_save(dayurl, None)
completed=s.count("Your puzzle answer was")
if completed > 1:
raise Exception("You've already done enough parts")
part=str(completed+1)
writeTo(part+"page.html",s)
else:
part=str(part)
if part=="2" and day=="25":
submit(part="2", answer="0")
examples = find_examples(part)
sampleouts = { x[1] for x in examples}
print("number of examples:",len(examples))
ns=0
while True:
while ns == (ns := os.stat("sol.py").st_mtime_ns):
if os.system("inotifywait -q sol.py"):
raise Exception("inotifywait did not terminate cleanly")
ns=os.stat("sol.py").st_mtime_ns
error=None
for inp,sampleout in examples:
print("==== trying sample input (10 second timeout)")
p=tee_with_exitcode(f"timeout 10 python3 sol.py {inp} 2>&1", "tmp")
#subprocess.run("(exit `((timeout 10 python3 sol.py input1 2>&1 ; echo $? >&3) | tee tmp >&4) 3>&1) 4>&1", shell=True)
answers = readString("tmp").split()
if not (p.returncode==0 and len(answers)>=1 and answers[-1]==sampleout):
print("==== did not get output matching",sampleout)
error=True
if error is None:
print("==== trying real input (no timeout)")
#p=subprocess.run("python3 sol.py input | tee tmpreal", shell=True)
p=tee_with_exitcode("python3 sol.py input","tmpreal")
print("==== end of program output")
if p.returncode:
print("error when called on real input")
continue
answer = readString("tmpreal").split()
if len(answer)<1:
print("answer is empty when called on real input")
continue
answer=answer[-1]
#do some checks on answer
if(len(answer)<=2):
print(repr(answer),"looks too small. Not submitting")
elif answer in sampleouts:
print(repr(answer), "is the same as the example output. Not submitting")
elif answer in bad_answers:
print(repr(answer), "previously submitted and failed. Not submitting")
elif bad_toohigh is not None and int(answer) >= bad_toohigh:
print(repr(answer),
f"is too high; as {bad_toohigh} was. Not submitting.")
elif bad_toolow is not None and int(answer) <= bad_toolow:
print(repr(answer),
f"is too low; as {bad_toolow} was. Not submitting.")
else:
if not bad_answers or input("Do you want to submit "+repr(answer)+" (y/n)?")=="y":
print("submitting answer:",repr(answer))
resp,content = submit(part=part,answer=answer)
if "That's the right answer!" in content:
succeeded=True
os.system(f"cp sol.py part{part}sol.py")
break
elif "That's not the right answer" in content:
add_bad(answer,content)
else:
print("did not recognise success or incorrect, may be timeout or blank input or already completed")
else:
print("==== not trying real input")
return part
if len(sys.argv)<4:
if doPart() == "1":
doPart("2")
else:
doPart(sys.argv[3])