forked from RagnarGrootKoerkamp/BAPCtools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tools.py
executable file
·1173 lines (1031 loc) · 41 KB
/
tools.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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
"""Can be run on multiple levels:
- from the root of the git repository
- from a contest directory
- from a problem directory
the tool will know where it is (by looking for the .git directory) and run on
everything inside it
- Ragnar Groot Koerkamp
Parts of this are copied from/based on run_program.py, written by Raymond van
Bommel.
"""
import argparse
import hashlib
import os
import sys
import tempfile
import shutil
import colorama
import json
import re
from pathlib import Path
from typing import Literal, cast
# Local imports
import config
import constraints
import export
import generate
import fuzz
import latex
import run
import skel
import slack
import solve_stats
import download_submissions
import stats
import validate
import signal
from problem import Problem
import contest
from contest import *
from util import *
if not is_windows():
import argcomplete # For automatic shell completions
# Initialize colorama for printing coloured output. On Windows, this captures
# stdout and replaces ANSI colour codes by calls to change the terminal colour.
#
# This initialization is disabled on GITLAB CI, since Colorama detects that
# the terminal is not a TTY and will strip all colour codes. Instead, we just
# disable this call since capturing of stdout/stderr isn't needed on Linux
# anyway.
# See:
# - https://github.com/conan-io/conan/issues/4718#issuecomment-473102953
# - https://docs.gitlab.com/runner/faq/#how-can-i-get-colored-output-on-the-web-terminal
if not os.getenv('GITLAB_CI', False) and not os.getenv('CI', False):
colorama.init()
# List of high level todos:
# TODO: Do more things in parallel (running testcases, building submissions)
# TODO: Get rid of old problem.path and settings objects in tools.py.
# This mostly needs changes in the less frequently used subcommands.
if sys.version_info < (3, 10):
fatal('BAPCtools requires at least Python 3.10.')
# Get the list of relevant problems.
# Either use the problems.yaml, or check the existence of problem.yaml and sort
# by shortname.
def get_problems():
def is_problem_directory(path):
return (path / 'problem.yaml').is_file()
contest: Optional[Path] = None
problem: Optional[Path] = None
level: Optional[Literal['problem', 'problemset']] = None
if config.args.contest:
# TODO #102: replace cast with typed Namespace field
contest = cast(Path, config.args.contest).resolve()
os.chdir(contest)
level = 'problemset'
if config.args.problem:
# TODO #102: replace cast with typed Namespace field
problem = cast(Path, config.args.problem).resolve()
level = 'problem'
os.chdir(problem.parent)
elif is_problem_directory(Path('.')):
problem = Path().cwd()
level = 'problem'
os.chdir('..')
else:
level = 'problemset'
# We create one tmpdir per contest.
h = hashlib.sha256(bytes(Path().cwd())).hexdigest()[-6:]
tmpdir = Path(tempfile.gettempdir()) / ('bapctools_' + h)
tmpdir.mkdir(parents=True, exist_ok=True)
def parse_problems_yaml(problemlist):
if problemlist is None:
fatal(f'Did not find any problem in {problemsyaml}.')
problemlist = problemlist
if problemlist is None:
problemlist = []
if not isinstance(problemlist, list):
fatal(f'problems.yaml must contain a problems: list.')
labels = dict[str, str]() # label -> shortname
problems = []
for p in problemlist:
shortname = p['id']
if 'label' not in p:
fatal(f'Found no label for problem {shortname} in problems.yaml.')
label = p['label']
if label == '':
fatal(f'Found empty label for problem {shortname} in problems.yaml.')
if label in labels:
fatal(
f'problems.yaml: label {label} found twice for problem {shortname} and {labels[label]}.'
)
labels[label] = shortname
if Path(shortname).is_dir():
problems.append((shortname, label))
else:
error(f'No directory found for problem {shortname} mentioned in problems.yaml.')
return problems
def fallback_problems():
problem_paths = list(filter(is_problem_directory, glob(Path('.'), '*/')))
label = chr(ord('Z') - len(problem_paths) + 1) if contest_yaml().get('testsession') else 'A'
problems = []
for i, path in enumerate(problem_paths):
problems.append((path, label))
label = inc_label(label)
return problems
problems = []
if level == 'problem':
assert problem
# If the problem is mentioned in problems.yaml, use that ID.
problemsyaml = problems_yaml()
if problemsyaml:
problem_labels = parse_problems_yaml(problemsyaml)
for shortname, label in problem_labels:
if shortname == problem.name:
problems = [Problem(Path(problem.name), tmpdir, label)]
break
if len(problems) == 0:
label = None
for p, l in fallback_problems():
if p.name == problem.name:
label = l
problems = [Problem(Path(problem.name), tmpdir, label)]
else:
level = 'problemset'
# If problems.yaml is available, use it.
problemsyaml = problems_yaml()
if problemsyaml:
problems = []
problem_labels = parse_problems_yaml(problemsyaml)
for shortname, label in problem_labels:
problems.append(Problem(Path(shortname), tmpdir, label))
else:
# Otherwise, fallback to all directories with a problem.yaml and sort by shortname.
problems = []
for path, label in fallback_problems():
problems.append(Problem(path, tmpdir, label))
if len(problems) == 0:
fatal('Did not find problem.yaml. Are you running this from a problem directory?')
if config.args.order:
# Sort by position of id in order
def get_pos(id):
if id in config.args.order:
return config.args.order.index(id)
else:
return len(config.args.order) + 1
problems.sort(key=lambda p: (get_pos(p.label), p.label))
if config.args.order_from_ccs:
# Sort by increasing difficulty, extracted from the CCS api.
# Get active contest.
cid = get_contest_id()
solves = dict()
# Read set of problems
contest_problems = call_api_get_json(f'/contests/{cid}/problems?public=true')
assert isinstance(problems, list)
for p in contest_problems:
solves[p['id']] = 0
scoreboard = call_api_get_json(f'/contests/{cid}/scoreboard?public=true')
for team in scoreboard['rows']:
for p in team['problems']:
if p['solved']:
solves[p['problem_id']] += 1
# Convert away from defaultdict, so any non matching keys below raise an error.
solves = dict(solves)
verbose('solves: ' + str(solves))
# Sort the problems
# Use negative solves instead of reversed, to preserver stable order.
problems.sort(key=lambda p: (-solves[p.name], p.label))
order = ', '.join(map(lambda p: str(p.label), problems))
verbose('order: ' + order)
contest_name = Path().cwd().name
# Filter problems by submissions/testcases, if given.
if level == 'problemset' and (config.args.submissions or config.args.testcases):
submissions = config.args.submissions or []
testcases = config.args.testcases or []
def keep_problem(problem):
for s in submissions:
x = resolve_path_argument(problem, s, 'submissions')
if x:
if is_relative_to(problem.path, x):
return True
for t in testcases:
x = resolve_path_argument(problem, t, 'data', suffixes=['.in'])
if x:
if is_relative_to(problem.path, x):
return True
return False
problems = [p for p in problems if keep_problem(p)]
config.level = level
return problems, level, contest_name, tmpdir
# NOTE: This is one of the few places that prints to stdout instead of stderr.
def print_sorted(problems):
for problem in problems:
print(f'{problem.label:<2}: {problem.path}')
def split_submissions_and_testcases(s):
# Everything containing data/, .in, or .ans goes into testcases.
submissions = []
testcases = []
for p in s:
ps = str(p)
if 'data' in ps or 'sample' in ps or 'secret' in ps or '.in' in ps or '.ans' in ps:
# Strip potential .ans and .in
if p.suffix in ['.ans', '.in']:
testcases.append(p.with_suffix(''))
else:
testcases.append(p)
else:
submissions.append(p)
return (submissions, testcases)
# We set argument_default=SUPPRESS in all parsers,
# to make sure no default values (like `False` or `0`) end up in the parsed arguments object.
# If we would not do this, it would not be possible to check which keys are explicitly set from the command line.
# This check is necessary when loading the personal config file in `read_personal_config`.
class SuppressingParser(argparse.ArgumentParser):
def __init__(self, **kwargs):
super(SuppressingParser, self).__init__(**kwargs, argument_default=argparse.SUPPRESS)
def build_parser():
parser = SuppressingParser(
description="""
Tools for ICPC style problem sets.
Run this from one of:
- the repository root, and supply `contest`
- a contest directory
- a problem directory
""",
formatter_class=argparse.RawTextHelpFormatter,
)
# Global options
global_parser = SuppressingParser(add_help=False)
global_parser.add_argument(
'--verbose',
'-v',
action='count',
help='Verbose output; once for what\'s going on, twice for all intermediate output.',
)
group = global_parser.add_mutually_exclusive_group()
group.add_argument('--contest', type=Path, help='Path to the contest to use.')
group.add_argument(
'--problem',
type=Path,
help='Path to the problem to use. Can be relative to contest if given.',
)
global_parser.add_argument(
'--no-bar',
action='store_true',
help='Do not show progress bars in non-interactive environments.',
)
global_parser.add_argument(
'--error',
'-e',
action='store_true',
help='Print full error of failing commands and some succeeding commands.',
)
global_parser.add_argument(
'--force-build', action='store_true', help='Force rebuild instead of only on changed files.'
)
global_parser.add_argument(
'--jobs',
'-j',
type=int,
help='The number of jobs to use. Default: cpu_count()/2.',
)
global_parser.add_argument(
'--memory',
'-m',
help='The maximum amount of memory in MB a subprocess may use. Does not work for Java. Default: 2048.',
)
global_parser.add_argument(
'--api',
help='CCS API endpoint to use, e.g. https://www.domjudge.org/demoweb. Defaults to the value in contest.yaml.',
)
global_parser.add_argument('--username', '-u', help='The username to login to the CCS.')
global_parser.add_argument('--password', '-p', help='The password to login to the CCS.')
global_parser.add_argument(
'--cp', action='store_true', help='Copy the output pdf instead of symlinking it.'
)
global_parser.add_argument(
'--language', dest='languages', action='append', help='Set language.'
)
subparsers = parser.add_subparsers(
title='actions', dest='action', parser_class=SuppressingParser
)
subparsers.required = True
# New contest
contestparser = subparsers.add_parser(
'new_contest', parents=[global_parser], help='Add a new contest to the current directory.'
)
contestparser.add_argument('contestname', nargs='?', help='The name of the contest')
# New problem
problemparser = subparsers.add_parser(
'new_problem', parents=[global_parser], help='Add a new problem to the current directory.'
)
problemparser.add_argument('problemname', nargs='?', help='The name of the problem,')
problemparser.add_argument('--author', help='The author of the problem,')
problemparser.add_argument(
'--validation',
help='Use validation to use for this problem.',
choices=[
'default',
'custom',
'custom interactive',
'custom multi-pass',
'custom interactive multi-pass',
],
)
problemparser.add_argument('--skel', help='Skeleton problem directory to copy from.')
# Copy directory from skel.
skelparser = subparsers.add_parser(
'skel',
parents=[global_parser],
help='Copy the given directories from skel to the current problem directory.',
)
skelparser.add_argument(
'directory',
nargs='+',
help='Directories to copy from skel/problem/, relative to the problem directory.',
)
skelparser.add_argument('--skel', help='Skeleton problem directory to copy from.')
# Rename problem
renameproblemparser = subparsers.add_parser(
'rename_problem', parents=[global_parser], help='Rename a problem, including its directory.'
)
renameproblemparser.add_argument('problemname', nargs='?', help='The new name of the problem,')
# Problem statements
pdfparser = subparsers.add_parser(
'pdf', parents=[global_parser], help='Build the problem statement pdf.'
)
pdfparser.add_argument(
'--all',
'-a',
action='store_true',
help='Create problem statements for individual problems as well.',
)
pdfparser.add_argument('--no-timelimit', action='store_true', help='Do not print timelimits.')
pdfparser.add_argument(
'--watch',
'-w',
action='store_true',
help='Continuously compile the pdf whenever a `problem_statement.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
)
pdfparser.add_argument(
'--open',
'-o',
nargs='?',
const=True,
type=Path,
help='Open the continuously compiled pdf (with a specified program).',
)
pdfparser.add_argument('--web', action='store_true', help='Create a web version of the pdf.')
pdfparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.')
# Solution slides
solparser = subparsers.add_parser(
'solutions', parents=[global_parser], help='Build the solution slides pdf.'
)
orderparser = solparser.add_mutually_exclusive_group()
orderparser.add_argument(
'--order', action='store', help='The order of the problems, e.g.: "CAB"'
)
orderparser.add_argument(
'--order-from-ccs',
action='store_true',
help='Order the problems by increasing difficulty, extracted from the CCS.',
)
solparser.add_argument(
'--contest-id',
action='store',
help='Contest ID to use when reading from the API. Only useful with --order-from-ccs. Defaults to value of contest_id in contest.yaml.',
)
solparser.add_argument(
'--watch',
'-w',
action='store_true',
help='Continuously compile the pdf whenever a `solution.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.',
)
solparser.add_argument(
'--open',
'-o',
nargs='?',
const=True,
type=Path,
help='Open the continuously compiled pdf (with a specified program).',
)
solparser.add_argument('--web', action='store_true', help='Create a web version of the pdf.')
solparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.')
# Validation
validate_parser = subparsers.add_parser(
'validate', parents=[global_parser], help='validate all grammar'
)
validate_parser.add_argument('testcases', nargs='*', type=Path, help='The testcases to run on.')
input_answer_group = validate_parser.add_mutually_exclusive_group()
input_answer_group.add_argument(
'--input', '-i', action='store_true', help='Only validate input.'
)
input_answer_group.add_argument('--answer', action='store_true', help='Only validate answer.')
input_answer_group.add_argument(
'--invalid', action='store_true', help='Only check invalid files for validity.'
)
move_or_remove_group = validate_parser.add_mutually_exclusive_group()
move_or_remove_group.add_argument(
'--remove', action='store_true', help='Remove failing testcases.'
)
move_or_remove_group.add_argument('--move-to', help='Move failing testcases to this directory.')
validate_parser.add_argument(
'--no-testcase-sanity-checks',
action='store_true',
help='Skip sanity checks on testcases.',
)
validate_parser.add_argument(
'--timeout', '-t', type=int, help='Override the default timeout. Default: 30.'
)
# constraints validation
constraintsparser = subparsers.add_parser(
'constraints',
parents=[global_parser],
help='prints all the constraints found in problemset and validators',
)
constraintsparser.add_argument(
'--no-generate', '-G', action='store_true', help='Do not run `generate`.'
)
# Stats
subparsers.add_parser(
'stats', parents=[global_parser], help='show statistics for contest/problem'
)
# Generate Testcases
genparser = subparsers.add_parser(
'generate', parents=[global_parser], help='Generate testcases according to .gen files.'
)
genparser.add_argument(
'--check-deterministic',
action='store_true',
help='Rerun all generators to make sure generators are deterministic.',
)
genparser.add_argument(
'--timeout', '-t', type=int, help='Override the default timeout. Default: 30.'
)
genparser_group = genparser.add_mutually_exclusive_group()
genparser_group.add_argument(
'--add',
nargs='*',
type=Path,
help='Add case(s) to generators.yaml.',
metavar='TARGET_DIRECTORY=generators/manual',
)
genparser_group.add_argument(
'--clean', '-C', action='store_true', help='Delete all cached files.'
)
genparser_group.add_argument(
'--reorder',
action='store_true',
help='Reorder cases by difficulty inside the given directories.',
)
genparser.add_argument(
'--interaction',
'-i',
action='store_true',
help='Use the solution to generate .interaction files.',
)
genparser.add_argument(
'testcases',
nargs='*',
type=Path,
help='The testcases to generate, given as directory, .in/.ans file, or base name.',
)
genparser.add_argument(
'--default-solution',
'-s',
type=Path,
help='The default solution to use for generating .ans files. Not compatible with generator.yaml.',
)
genparser.add_argument(
'--no-validators',
action='store_true',
help='Ignore results of input and answer validation. Validators are still run.',
)
genparser.add_argument(
'--no-solution',
action='store_true',
help='Skip generating .ans/.interaction files with the solution.',
)
genparser.add_argument(
'--no-visualizer',
action='store_true',
help='Skip generating graphics with the visualizer.',
)
genparser.add_argument(
'--no-testcase-sanity-checks',
action='store_true',
help='Skip sanity checks on testcases.',
)
# Fuzzer
fuzzparser = subparsers.add_parser(
'fuzz',
parents=[global_parser],
help='Generate random testcases and search for inconsistencies in AC submissions.',
)
fuzzparser.add_argument('--time', type=int, help=f'Number of seconds to run for. Default: 600')
fuzzparser.add_argument('--timelimit', '-t', type=int, help=f'Time limit for submissions.')
fuzzparser.add_argument(
'submissions',
nargs='*',
type=Path,
help='The generator.yaml rules to use, given as directory, .in/.ans file, or base name, and submissions to run.',
)
fuzzparser.add_argument(
'--timeout', type=int, help='Override the default timeout. Default: 30.'
)
# Run
runparser = subparsers.add_parser(
'run', parents=[global_parser], help='Run multiple programs against some or all input.'
)
runparser.add_argument(
'submissions',
nargs='*',
type=Path,
help='optionally supply a list of programs and testcases to run',
)
runparser.add_argument('--samples', action='store_true', help='Only run on the samples.')
runparser.add_argument(
'--no-generate',
'-G',
action='store_true',
help='Do not run `generate` before running submissions.',
)
runparser.add_argument(
'--all',
'-a',
action='count',
default=0,
help='Run all testcases. Use twice to continue even after timeouts.',
)
runparser.add_argument(
'--default-solution',
'-s',
type=Path,
help='The default solution to use for generating .ans files. Not compatible with generators.yaml.',
)
runparser.add_argument(
'--table', action='store_true', help='Print a submissions x testcases table for analysis.'
)
runparser.add_argument(
'--overview', '-o', action='store_true', help='Print a live overview for the judgings.'
)
runparser.add_argument('--tree', action='store_true', help='Show a tree of verdicts.')
runparser.add_argument('--depth', type=int, help='Depth of verdict tree.')
runparser.add_argument(
'--timeout',
type=int,
help='Override the default timeout. Default: 1.5 * timelimit + 1.',
)
runparser.add_argument('--timelimit', '-t', type=int, help='Override the default timelimit.')
runparser.add_argument(
'--no-testcase-sanity-checks',
action='store_true',
help='Skip sanity checks on testcases.',
)
runparser.add_argument(
'--sanitizer',
action='store_true',
help='Run submissions with additional sanitizer flags (currently only C++). Note that this sets --memory unlimited.',
)
timelimitparser = subparsers.add_parser(
'timelimit', parents=[global_parser], help='Determine the timelimit for a problem.'
)
timelimitparser.add_argument(
'submissions',
nargs='*',
type=Path,
help='optionally supply a list of programs and testcases on which the timelimit should be based.',
)
timelimitparser.add_argument(
'--all',
'-a',
action='store_true',
help='Run all submissions, not only AC and TLE.',
)
timelimitparser.add_argument(
'--write',
'-w',
action='store_true',
help='Write .timelimit file.',
)
# Test
testparser = subparsers.add_parser(
'test', parents=[global_parser], help='Run a single program and print the output.'
)
testparser.add_argument('submissions', nargs=1, type=Path, help='A single submission to run')
testcasesgroup = testparser.add_mutually_exclusive_group()
testcasesgroup.add_argument(
'testcases',
nargs='*',
default=[],
type=Path,
help='Optionally a list of testcases to run on.',
)
testcasesgroup.add_argument('--samples', action='store_true', help='Only run on the samples.')
testcasesgroup.add_argument(
'--interactive',
'-i',
action='store_true',
help='Run submission in interactive mode: stdin is from the command line.',
)
testparser.add_argument(
'--timeout',
type=int,
help='Override the default timeout. Default: 1.5 * timelimit + 1.',
)
# Sort
subparsers.add_parser(
'sort', parents=[global_parser], help='sort the problems for a contest by name'
)
# All
allparser = subparsers.add_parser(
'all', parents=[global_parser], help='validate input, validate answers, and run programs'
)
allparser.add_argument('--no-timelimit', action='store_true', help='Do not print timelimits.')
allparser.add_argument(
'--no-testcase-sanity-checks',
action='store_true',
help='Skip sanity checks on testcases.',
)
allparser.add_argument(
'--check-deterministic',
action='store_true',
help='Rerun all generators to make sure generators are deterministic.',
)
allparser.add_argument(
'--timeout', '-t', type=int, help='Override the default timeout. Default: 30.'
)
allparser.add_argument(
'--overview', '-o', action='store_true', help='Print a live overview for the judgings.'
)
# Build DOMjudge zip
zipparser = subparsers.add_parser(
'zip', parents=[global_parser], help='Create zip file that can be imported into DOMjudge'
)
zipparser.add_argument('--skip', action='store_true', help='Skip recreation of problem zips.')
zipparser.add_argument(
'--force', '-f', action='store_true', help='Skip validation of input and answers.'
)
zipparser.add_argument(
'--kattis',
action='store_true',
help='Make a zip more following the kattis problemarchive.com format.',
)
zipparser.add_argument('--no-solutions', action='store_true', help='Do not compile solutions')
# Build a zip with all samples.
subparsers.add_parser(
'samplezip', parents=[global_parser], help='Create zip file of all samples.'
)
subparsers.add_parser(
'gitlabci', parents=[global_parser], help='Print a list of jobs for the given contest.'
)
exportparser = subparsers.add_parser(
'export', parents=[global_parser], help='Export the problem or contest to DOMjudge.'
)
exportparser.add_argument(
'--contest-id',
action='store',
help='Contest ID to use when writing to the API. Defaults to value of contest_id in contest.yaml.',
)
updateproblemsyamlparser = subparsers.add_parser(
'update_problems_yaml',
parents=[global_parser],
help='Update the problems.yaml with current names and timelimits.',
)
updateproblemsyamlparser.add_argument(
'--colors',
help='Set the colors of the problems. Comma-separated list of hex-codes.',
)
updateproblemsyamlparser.add_argument(
'--sort',
action='store_true',
help='Sort the problems by id.',
)
# Print the corresponding temporary directory.
tmpparser = subparsers.add_parser(
'tmp',
parents=[global_parser],
help='Print the tmpdir corresponding to the current problem.',
)
tmpparser.add_argument(
'--clean',
action='store_true',
help='Delete the temporary cache directory for the current problem/contest.',
)
solvestatsparser = subparsers.add_parser(
'solve_stats',
parents=[global_parser],
help='Make solve stats plots using Matplotlib. All teams on the public scoreboard are included (including spectator/company teams).',
)
solvestatsparser.add_argument(
'--contest-id',
action='store',
help='Contest ID to use when reading from the API. Defaults to value of contest_id in contest.yaml.',
)
solvestatsparser.add_argument(
'--post-freeze',
action='store_true',
help='When given, the solve stats will include submissions from after the scoreboard freeze.',
)
download_submissions_parser = subparsers.add_parser(
'download_submissions',
parents=[global_parser],
help='Download all submissions for a contest and write them to submissions/.',
)
download_submissions_parser.add_argument(
'--contest-id',
action='store',
help='Contest ID to use when reading from the API. Defaults to value of contest_id in contest.yaml.',
)
create_slack_channel_parser = subparsers.add_parser(
'create_slack_channels',
parents=[global_parser],
help='Create a slack channel for each problem',
)
create_slack_channel_parser.add_argument('--token', help='A user token is of the form xoxp-...')
join_slack_channel_parser = subparsers.add_parser(
'join_slack_channels',
parents=[global_parser],
help='Join a slack channel for each problem',
)
join_slack_channel_parser.add_argument('--token', help='A bot/user token is of the form xox...')
join_slack_channel_parser.add_argument('username', help='Slack username')
if not is_windows():
argcomplete.autocomplete(parser)
return parser
# Takes a Namespace object returned by argparse.parse_args().
def run_parsed_arguments(args):
# Process arguments
config.args = args
config.set_default_args()
action = config.args.action
# Split submissions and testcases when needed.
if action in ['run', 'fuzz', 'timelimit']:
if config.args.submissions:
config.args.submissions, config.args.testcases = split_submissions_and_testcases(
config.args.submissions
)
else:
config.args.testcases = []
# Skel commands.
if action == 'new_contest':
skel.new_contest()
return
if action == 'new_problem':
skel.new_problem()
return
# Get problem_paths and cd to contest
problems, level, contest, tmpdir = get_problems()
# Check for incompatible actions at the problem/problemset level.
if level != 'problem':
if action == 'test':
fatal('Testing a submission only works for a single problem.')
if action == 'skel':
fatal('Copying skel directories only works for a single problem.')
if action != 'generate' and config.args.testcases and config.args.samples:
fatal('--samples can not go together with an explicit list of testcases.')
if config.args.add is not None:
# default to 'generators/manual'
if len(config.args.add) == 0:
config.args.add = [Path('generators/manual')]
# Paths *must* be inside generators/.
checked_paths = []
for path in config.args.add:
if path.parts[0] != 'generators':
warn(f'Path {path} does not match "generators/*". Skipping.')
else:
checked_paths.append(path)
config.args.add = checked_paths
if config.args.reorder is not None:
# default to 'data/secret'
if not config.args.testcases:
config.args.testcases = [Path('data/secret')]
# Paths *must* be inside data/.
checked_paths = []
for path in config.args.testcases:
if path.parts[0] != 'data':
warn(f'Path {path} does not match "data/*". Skipping.')
else:
checked_paths.append(path)
config.args.testcases = checked_paths
# Handle one-off subcommands.
if action == 'tmp':
if level == 'problem':
level_tmpdir = tmpdir / problems[0].name
else:
level_tmpdir = tmpdir
if config.args.clean:
log(f'Deleting {tmpdir}!')
if level_tmpdir.is_dir():
shutil.rmtree(level_tmpdir)
if level_tmpdir.is_file():
level_tmpdir.unlink()
else:
print(level_tmpdir)
return
if action == 'stats':
stats.stats(problems)
return
if action == 'sort':
print_sorted(problems)
return
if action == 'samplezip':
sampleout = Path('samples.zip')
if level == 'problem':
sampleout = problems[0].path / sampleout
statement_language = export.force_single_language(problems)
export.build_samples_zip(problems, sampleout, statement_language)
return
if action == 'rename_problem':
if level == 'problemset':
fatal('rename_problem only works for a problem')
skel.rename_problem(problems[0])
return
if action == 'gitlabci':
skel.create_gitlab_jobs(contest, problems)
return
if action == 'skel':
skel.copy_skel_dir(problems)
return
if action == 'solve_stats':
if level == 'problem':
fatal('solve_stats only works for a contest')
config.args.jobs = (os.cpu_count() or 1) // 2
solve_stats.generate_solve_stats(config.args.post_freeze)
return
if action == 'download_submissions':
if level == 'problem':
fatal('download_submissions only works for a contest')
download_submissions.download_submissions()
return
if action == 'create_slack_channels':
slack.create_slack_channels(problems)
return
if action == 'join_slack_channels':
slack.join_slack_channels(problems, config.args.username)
return
problem_zips = []
success = True
for problem in problems:
if (
level == 'problemset'
and action in ['pdf', 'export', 'update_problems_yaml']
and not config.args.all
):
continue
print(Style.BRIGHT, 'PROBLEM ', problem.name, Style.RESET_ALL, sep='', file=sys.stderr)
if action in ['generate']:
success &= generate.generate(problem)
if action in ['all', 'constraints', 'run'] and not config.args.no_generate:
# Call `generate` with modified arguments.
old_args = argparse.Namespace(**vars(config.args))
config.args.jobs = (os.cpu_count() or 1) // 2
config.args.add = None
config.args.verbose = 0
config.args.no_visualizer = True
success &= generate.generate(problem)
config.args = old_args
if action in ['fuzz']:
success &= fuzz.Fuzz(problem).run()
if action in ['pdf', 'all']:
# only build the pdf on the problem level, or on the contest level when
# --all is passed.
if level == 'problem' or (level == 'problemset' and config.args.all):
success &= latex.build_problem_pdfs(problem)
if action in ['solutions']:
if level == 'problem':
success &= latex.build_problem_pdfs(problem, solutions=True, web=config.args.web)
if action in ['validate', 'all']:
if not (action == 'validate' and (config.args.input or config.args.answer)):