diff --git a/bin/config.py b/bin/config.py index 0315b7ae2..87a45636f 100644 --- a/bin/config.py +++ b/bin/config.py @@ -96,7 +96,7 @@ grep -Ev '^(h|jobs|time|verbose)$' | sed "s/^/'/;s/$/',/" | tr '\n' ' ' | sed 's/^/ARGS_LIST: Final = [/;s/, $/]\n/' """ # fmt: off -ARGS_LIST: Final = ['1', 'add', 'all', 'answer', 'api', 'author', 'check_deterministic', 'clean', 'colors', 'contest', 'contest_id', 'contestname', 'cp', 'default_solution', 'depth', 'directory', 'error', 'force', 'force_build', 'input', 'interaction', 'interactive', 'invalid', 'kattis', 'language', 'memory', 'more', 'move_to', 'no_bar', 'no_generate', 'no_solution', 'no_solutions', 'no_testcase_sanity_checks', 'no_time_limit', 'no_validators', 'no_visualizer', 'open', 'order', 'order_from_ccs', 'overview', 'password', 'post_freeze', 'problem', 'problemname', 'remove', 'reorder', 'samples', 'sanitizer', 'skel', 'skip', 'sort', 'submissions', 'table', 'testcases', 'time_limit', 'timeout', 'token', 'tree', 'username', 'validation', 'watch', 'web', 'write'] +ARGS_LIST: Final = ['1', 'add', 'all', 'answer', 'api', 'author', 'check_deterministic', 'clean', 'colors', 'contest', 'contest_id', 'contestname', 'cp', 'default_solution', 'depth', 'directory', 'error', 'force', 'force_build', 'input', 'interaction', 'interactive', 'invalid', 'kattis', 'language', 'memory', 'more', 'move_to', 'no_bar', 'no_generate', 'no_solution', 'no_solutions', 'no_testcase_sanity_checks', 'no_time_limit', 'no_validators', 'no_visualizer', 'open', 'order', 'order_from_ccs', 'overview', 'password', 'post_freeze', 'problem', 'problemname', 'remove', 'reorder', 'samples', 'sanitizer', 'skel', 'skip', 'sort', 'submissions', 'table', 'testcases', 'time_limit', 'timeout', 'token', 'tree', 'type', 'username', 'watch', 'web', 'write'] # fmt: on diff --git a/bin/constraints.py b/bin/constraints.py index 09215cd94..a8bdfa751 100644 --- a/bin/constraints.py +++ b/bin/constraints.py @@ -22,7 +22,7 @@ def check_validators(problem): if not in_constraints: warn("No constraint validation of input values found in input validators.") problem.validate_data(validate.Mode.ANSWER, constraints=ans_constraints) - if not problem.interactive and not problem.multipass and not ans_constraints: + if not problem.interactive and not problem.multi_pass and not ans_constraints: log("No constraint validation of answer values found in answer or output validators.") print() diff --git a/bin/export.py b/bin/export.py index 25fb32498..48f764720 100644 --- a/bin/export.py +++ b/bin/export.py @@ -74,11 +74,11 @@ def build_samples_zip(problems, output, statement_language): outputdir = Path(problem.label) attachments_dir = problem.path / "attachments" - if (problem.interactive or problem.multipass) and not attachments_dir.is_dir(): + if (problem.interactive or problem.multi_pass) and not attachments_dir.is_dir(): interactive = "interactive " if problem.interactive else "" - multipass = "multi-pass " if problem.multipass else "" + multi_pass = "multi-pass " if problem.multi_pass else "" util.error( - f"{interactive}{multipass}problem {problem.name} does not have an attachments/ directory." + f"{interactive}{multi_pass}problem {problem.name} does not have an attachments/ directory." ) continue @@ -96,7 +96,7 @@ def build_samples_zip(problems, output, statement_language): util.error(f"Cannot include broken file {f}.") # Add samples for non-interactive and non-multi-pass problems. - if not problem.interactive and not problem.multipass: + if not problem.interactive and not problem.multi_pass: samples = problem.testcases(only_samples=True) if samples: for i in range(0, len(samples)): @@ -128,15 +128,15 @@ def build_problem_zip(problem, output): ("problem_statement/*", True), ("submissions/accepted/**/*", True), ("submissions/*/**/*", False), - ("attachments/**/*", problem.interactive or problem.multipass), + ("attachments/**/*", problem.interactive or problem.multi_pass), ] testcases = [ ("data/secret/**/*.in", True), - ("data/sample/**/*.in", not problem.interactive and not problem.multipass), + ("data/sample/**/*.in", not problem.interactive and not problem.multi_pass), ] - if problem.interactive or problem.multipass: + if problem.interactive or problem.multi_pass: # .interaction files don't need a corresponding .in # therefore we can handle them like all other files files += [("data/sample/**/*.interaction", False)] @@ -148,7 +148,7 @@ def build_problem_zip(problem, output): ("data/sample/**/*.ans.statement", False), ] - if "custom" in problem.settings.validation: + if problem.custom_output: files.append(("output_validators/**/*", True)) if config.args.kattis: diff --git a/bin/fuzz.py b/bin/fuzz.py index 371fcc4ff..7faa02a49 100644 --- a/bin/fuzz.py +++ b/bin/fuzz.py @@ -72,7 +72,7 @@ def _run(self, bar): localbar.done() # Generate .ans. - if not self.fuzz.problem.interactive and not self.fuzz.problem.multipass: + if not self.fuzz.problem.interactive and not self.fuzz.problem.multi_pass: if self.solution and not testcase.ans_path.is_file(): if testcase.ans_path.is_file(): testcase.ans_path.unlink() diff --git a/bin/generate.py b/bin/generate.py index b47a7cc67..37146e793 100644 --- a/bin/generate.py +++ b/bin/generate.py @@ -706,11 +706,11 @@ def validate_ans(t, problem, testcase, meta_yaml, bar): ansfile = problem.tmpdir / "data" / t.hash / "testcase.ans" assert ansfile.is_file() - if problem.interactive or problem.multipass: + if problem.interactive or problem.multi_pass: if ansfile.stat().st_size != 0: interactive = "interaction " if problem.interactive else "" - multipass = "multipass " if problem.multipass else "" - bar.warn(f".ans file for {interactive}{multipass}problem is expected to be empty.") + multi_pass = "multi-pass " if problem.multi_pass else "" + bar.warn(f".ans file for {interactive}{multi_pass}problem is expected to be empty.") else: size = ansfile.stat().st_size if ( @@ -958,8 +958,8 @@ def needed(ext): used_solution = False changed_ans = False - if problem.interactive or problem.multipass: - # Generate empty ans file for interactive/multipass problems + if problem.interactive or problem.multi_pass: + # Generate empty ans file for interactive/multi-pass problems if ".ans" not in meta_yaml["generated_extensions"]: if not ansfile.is_file() or ansfile.stat().st_size != 0: ansfile.write_text("") @@ -1829,7 +1829,7 @@ def build_program(p): build_programs(program.Visualizer, visualizers_used) self.problem.validators(validate.InputValidator) - if not self.problem.interactive and not self.problem.multipass: + if not self.problem.interactive and not self.problem.multi_pass: self.problem.validators(validate.AnswerValidator) self.problem.validators(validate.OutputValidator) diff --git a/bin/interactive.py b/bin/interactive.py index eefd0e9c7..47e53b511 100644 --- a/bin/interactive.py +++ b/bin/interactive.py @@ -64,7 +64,7 @@ def get_validator_command(): validator_dir = output_validator.tmpdir submission_dir = run.submission.tmpdir - nextpass = run.feedbackdir / "nextpass.in" if run.problem.multipass else False + nextpass = run.feedbackdir / "nextpass.in" if run.problem.multi_pass else False if config.args.verbose >= 2: print("Validator: ", *get_validator_command(), file=sys.stderr) @@ -132,7 +132,7 @@ def get_validator_command(): _feedback(run, validator_err), exec_res.err, verdict, - pass_id if run.problem.multipass else None, + pass_id if run.problem.multi_pass else None, ) else: tle_result.timeout_expired |= max_duration >= timeout @@ -170,7 +170,7 @@ def get_validator_command(): _feedback(run, validator_err), exec_res.err, verdict, - pass_id if run.problem.multipass else None, + pass_id if run.problem.multi_pass else None, ) else: tle_result.duration = max_duration @@ -401,7 +401,7 @@ def kill_handler_function(): val_err, team_err, verdict, - pass_id if run.problem.multipass else None, + pass_id if run.problem.multi_pass else None, ) else: tle_result.timeout_expired |= aborted @@ -433,7 +433,7 @@ def kill_handler_function(): val_err, team_err, verdict, - pass_id if run.problem.multipass else None, + pass_id if run.problem.multi_pass else None, ) else: tle_result.duration = max_duration diff --git a/bin/latex.py b/bin/latex.py index c06f76f25..03740a8e9 100644 --- a/bin/latex.py +++ b/bin/latex.py @@ -104,10 +104,10 @@ def flush(): last = line[0] flush() else: - assert problem.multipass + assert problem.multi_pass - multipass_dir = builddir / "multipass" - multipass_dir.mkdir(exist_ok=True) + multi_pass_dir = builddir / "multi_pass" + multi_pass_dir.mkdir(exist_ok=True) lines = sample.read_text() last = "<" @@ -121,8 +121,8 @@ def flush(): def flush(): nonlocal current_sample - in_path = multipass_dir / f"{sample_name}-{pass_id:02}.in" - out_path = multipass_dir / f"{sample_name}-{pass_id:02}.out" + in_path = multi_pass_dir / f"{sample_name}-{pass_id:02}.in" + out_path = multi_pass_dir / f"{sample_name}-{pass_id:02}.out" in_path.write_text(cur_in) out_path.write_text(cur_out) diff --git a/bin/problem.py b/bin/problem.py index fa40a69d3..df1670c1f 100644 --- a/bin/problem.py +++ b/bin/problem.py @@ -22,6 +22,23 @@ from colorama import Fore, Style +# Parse validation mode (only for legacy problem format version) +def parse_legacy_validation(mode: str) -> set[str]: + if mode == "default": + return {mode} + else: + ok = True + parsed = set() + for part in mode.split(): + if part in ["custom", "interactive", "multi-pass"] and part not in parsed: + parsed.add(part) + else: + ok = False + if "custom" not in parsed or not ok: + fatal(f"Unrecognised validation mode {mode}.") + return parsed + + class ProblemLimits: def __init__( self, @@ -104,7 +121,12 @@ def __init__( class ProblemSettings: - def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float] = None): + def __init__( + self, + yamldata: dict[str, Any], + problem: "Problem", + legacy_time_limit: Optional[float] = None, + ): assert isinstance(yamldata, dict) if "name" in yamldata and isinstance(yamldata["name"], str): @@ -120,7 +142,28 @@ def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float] ) if not self.is_legacy() and self.problem_format_version != "2023-07-draft": fatal(f"problem_format_version {self.problem_format_version} not supported") - # TODO: also support 'type'. For now, we only support legacy 'validation'. + + if self.is_legacy(): + mode = parse_legacy_validation(parse_setting(yamldata, "validation", "default")) + else: + if "validation" in yamldata: + warn( + "problem.yaml: 'validation' is removed in 2023-07-draft, please use 'type' instead" + ) + mode = set(parse_setting(yamldata, "type", "pass-fail").split(" ")) + self.interactive: bool = "interactive" in mode + self.multi_pass: bool = "multi-pass" in mode + self.custom_output: bool = ( + self.interactive + or self.multi_pass + or ( + "custom" in mode + if self.is_legacy() + # TODO #424: output_validator should be singular, but DOMjudge does not support this yet, so this should be fixed during export. + else (problem.path / "output_validators").exists() + ) + ) + self.name: dict[str, str] = parse_setting(yamldata, "name", {"en": ""}) self.uuid: str = parse_setting(yamldata, "uuid", "") self.author: str = parse_setting(yamldata, "author", "") @@ -129,7 +172,6 @@ def __init__(self, yamldata: dict[str, Any], legacy_time_limit: Optional[float] self.license: str = parse_setting(yamldata, "license", "unknown") self.rights_owner: str = parse_setting(yamldata, "rights_owner", "") self.limits = ProblemLimits(parse_setting(yamldata, "limits", {}), self, legacy_time_limit) - self.validation: str = parse_setting(yamldata, "validation", "default") self.validator_flags: list[str] = parse_setting(yamldata, "validator_flags", []) self.keywords: str = parse_setting(yamldata, "keywords", "") @@ -279,19 +321,20 @@ def _read_settings(self): yaml_path.write_text(raw) log("Added new UUID to problem.yaml") - self.settings = ProblemSettings(yaml_data, self._get_legacy_time_limit(yaml_data)) - self.limits = self.settings.limits + self.settings = ProblemSettings(yaml_data, self, self._get_legacy_time_limit(yaml_data)) - mode = parse_validation(self.settings.validation) - self.interactive = "interactive" in mode - self.multipass = "multi-pass" in mode + # Aliasing fields makes life easier for us 😛 + self.limits: ProblemLimits = self.settings.limits + self.interactive: bool = self.settings.interactive + self.multi_pass: bool = self.settings.multi_pass + self.custom_output: bool = self.settings.custom_output # Handle dependencies... has_validation_passes = self.limits.validation_passes is not None - if self.multipass and not has_validation_passes: + if self.multi_pass and not has_validation_passes: self.limits.validation_passes = 2 - if not self.multipass and has_validation_passes: - warn("limit: validation_passes is only used for multipass problems. SKIPPED.") + if not self.multi_pass and has_validation_passes: + warn("limit: validation_passes is only used for multi_pass problems. SKIPPED.") def _parse_testdata_yaml(p, path, bar): assert path.is_relative_to(p.path / "data") @@ -444,14 +487,14 @@ def testcases( for f in in_paths: t = testcase.Testcase(p, f, print_warn=True) if ( - (p.interactive or p.multipass) + (p.interactive or p.multi_pass) and mode == validate.Mode.INVALID and t.root in ["invalid_answers", "invalid_outputs"] ): msg = "" if p.interactive: msg += " interactive" - if p.multipass: + if p.multi_pass: msg += " multi-pass" warn(f"Found file {f} for {mode} validation in{msg} problem. Skipping.") continue @@ -504,7 +547,7 @@ def statement_samples(p) -> list[Path | tuple[Path, Path]]: # Non-interactive and Non-multi-pass problems should not have .interaction files. # On the other hand, interactive problems are allowed to have .{in,ans}.statement files, # so that they can emulate a non-interactive problem with on-the-fly generated input. - if not p.interactive and not p.multipass: + if not p.interactive and not p.multi_pass: if len(interaction_paths) != 0: warn( f"Non-interactive/Non-multi-pass problem {p.name} should not have data/sample/*.interaction files." @@ -672,13 +715,18 @@ def _validators( return problem._validators_cache[key] assert hasattr(cls, "source_dirs") + # TODO #424: We should not support multiple output validators inside output_validator/. paths = [p for source_dir in cls.source_dirs for p in glob(problem.path / source_dir, "*")] # Handle default output validation - if cls == validate.OutputValidator and problem.settings.validation == "default": - if paths: + if cls == validate.OutputValidator: + if problem.settings.is_legacy() and not problem.custom_output and paths: error("Validation is default but custom output validator exists (ignoring it)") - paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"] + paths = [] + if not paths: + if problem.custom_output: + fatal("Problem validation type requires output_validators/") + paths = [config.TOOLS_ROOT / "support" / "default_output_validator.cpp"] # TODO: Instead of checking file contents, maybe specify this in generators.yaml? def has_constraints_checking(f): @@ -906,12 +954,12 @@ def validate_data(problem, mode: validate.Mode, constraints: dict | bool | None constraints = {} assert constraints is None or isinstance(constraints, dict) - if (problem.interactive or problem.multipass) and mode == validate.Mode.ANSWER: + if (problem.interactive or problem.multi_pass) and mode == validate.Mode.ANSWER: if (problem.path / "answer_validators").exists(): msg = "" if problem.interactive: msg += " interactive" - if problem.multipass: + if problem.multi_pass: msg += " multi-pass" log(f"Not running answer_validators for{msg} problems.") return True @@ -925,12 +973,12 @@ def validate_data(problem, mode: validate.Mode, constraints: dict | bool | None testcases = problem.testcases(mode=mode) case validate.Mode.ANSWER: assert not problem.interactive - assert not problem.multipass + assert not problem.multi_pass problem.validators(validate.AnswerValidator, check_constraints=check_constraints) testcases = problem.testcases(mode=mode) case validate.Mode.INVALID: problem.validators(validate.InputValidator) - if not problem.interactive and not problem.multipass: + if not problem.interactive and not problem.multi_pass: problem.validators(validate.AnswerValidator) testcases = problem.testcases(mode=mode) case _: diff --git a/bin/run.py b/bin/run.py index c6dd7fce3..0661be4ef 100644 --- a/bin/run.py +++ b/bin/run.py @@ -76,7 +76,7 @@ def run(self, bar, *, interaction=None, submission_args=None): if interaction: assert not interaction.is_relative_to(self.tmpdir) interaction = interaction.open("a") - nextpass = self.feedbackdir / "nextpass.in" if self.problem.multipass else False + nextpass = self.feedbackdir / "nextpass.in" if self.problem.multi_pass else False pass_id = 0 max_duration = 0 tle_result = None @@ -103,7 +103,7 @@ def run(self, bar, *, interaction=None, submission_args=None): result.verdict = Verdict.TIME_LIMIT_EXCEEDED if tle_result is None: tle_result = result - tle_result.pass_id = pass_id if self.problem.multipass else None + tle_result.pass_id = pass_id if self.problem.multi_pass else None else: tle_result.timeout_expired |= result.timeout_expired if not self._continue_with_tle(result.verdict, result.timeout_expired): @@ -163,7 +163,7 @@ def run(self, bar, *, interaction=None, submission_args=None): if interaction: interaction.close() - if self.problem.multipass: + if self.problem.multi_pass: result.pass_id = pass_id if tle_result is not None: @@ -180,7 +180,7 @@ def run(self, bar, *, interaction=None, submission_args=None): self.out_path.unlink() if result.verdict and (self.feedbackdir / "nextpass.in").is_file(): - assert not self.problem.multipass + assert not self.problem.multi_pass bar.warn("Validator created nextpass.in for non multi-pass problem. Ignored.") self.result = result @@ -188,7 +188,7 @@ def run(self, bar, *, interaction=None, submission_args=None): # check if we should continue after tle def _continue_with_tle(self, verdict, timeout_expired): - if not self.problem.multipass: + if not self.problem.multi_pass: return False if verdict != Verdict.TIME_LIMIT_EXCEEDED: return False @@ -374,7 +374,7 @@ def run_testcases( ): runs = [Run(self.problem, self, testcase) for testcase in testcases] max_testcase_len = max(len(run.name) for run in runs) - if self.problem.multipass: + if self.problem.multi_pass: max_testcase_len += 2 max_item_len = max_testcase_len + max_submission_name_len - len(self.name) padding_len = max_submission_name_len - len(self.name) @@ -463,7 +463,7 @@ def process_run(run: Run): timeout = result.duration >= self.limits["timeout"] duration_style = Style.BRIGHT if timeout else "" passmsg = ( - f":{Fore.CYAN}{result.pass_id}{Style.RESET_ALL}" if self.problem.multipass else "" + f":{Fore.CYAN}{result.pass_id}{Style.RESET_ALL}" if self.problem.multi_pass else "" ) testcase = f"{run.name}{Style.RESET_ALL}{passmsg}" style_len = len(f"{Style.RESET_ALL}") diff --git a/bin/skel.py b/bin/skel.py index 9a640f9f4..ca0114036 100644 --- a/bin/skel.py +++ b/bin/skel.py @@ -153,20 +153,26 @@ def new_problem(): author = config.args.author if config.args.author else _ask_variable_string("author") validator_flags = "" - if config.args.validation: - parse_validation(config.args.validation) - validation = config.args.validation + custom_output = False + if config.args.type: + problem_type = config.args.type else: - validation = _ask_variable_choice("validation", ["default", "float", "custom"]) - if validation == "float": - validation = "default" - validator_flags = "validator_flags:\n float_tolerance 1e-6\n" - log("Using default float tolerance of 1e-6") - if validation == "custom": - if _ask_variable_bool("interactive", False): - validation += " interactive" - if _ask_variable_bool("multi-pass", False): - validation += " multi-pass" + problem_type = _ask_variable_choice( + "type", + ["pass-fail", "float", "custom", "interactive", "multi-pass", "interactive multi-pass"], + ) + # The validation type `float` is not official, it only helps setting the `validator_flags`. + if problem_type == "float": + problem_type = "pass-fail" + validator_flags = "validator_flags:\n float_tolerance 1e-6\n" + log("Using default float tolerance of 1e-6") + # Since version 2023-07-draft of the spec, the `custom` validation type is no longer explicit. + # The mere existence of the output_validator(s)/ folder signals non-default output validation. + if problem_type == "custom": + custom_output = True + problem_type = "pass-fail" + if "interactive" in problem_type or "multi-pass" in problem_type: + custom_output = True # Read settings from the contest-level yaml file. variables = contest.contest_yaml() @@ -175,7 +181,7 @@ def new_problem(): "problemname": "\n".join(f" {lang}: {name}" for lang, name in problemname.items()), "dirname": dirname, "author": author, - "validation": validation, + "type": problem_type, "validator_flags": validator_flags, }.items(): variables[k] = v @@ -229,7 +235,7 @@ def new_problem(): variables, exist_ok=True, preserve_symlinks=preserve_symlinks, - skip=[skeldir / "output_validators"] if validation == "default" else None, + skip=[skeldir / "output_validators"] if not custom_output else None, ) # Warn about missing problem statement skeletons for non-en languages diff --git a/bin/stats.py b/bin/stats.py index 472f3b168..527c4e558 100644 --- a/bin/stats.py +++ b/bin/stats.py @@ -173,9 +173,9 @@ def count(path): def value(x): if x[0] == " time" or x[0] == "subs": return x[1](problem) - if x[0] == "A" and (problem.interactive or problem.multipass): + if x[0] == "A" and (problem.interactive or problem.multi_pass): return None # Do not show an entry for the answer validator if it is not required - if x[0] == "O" and problem.settings.validation == "default": + if x[0] == "O" and not problem.custom_output: return None # Do not show an entry for the output validator if it is not required return len(count(x[1])) diff --git a/bin/testcase.py b/bin/testcase.py index 0837e332d..1658fe229 100644 --- a/bin/testcase.py +++ b/bin/testcase.py @@ -218,7 +218,7 @@ def validate_format( return ok assert not self.problem.interactive - assert not self.problem.multipass + assert not self.problem.multi_pass ok = self.validate_format( validate.Mode.ANSWER, diff --git a/bin/tools.py b/bin/tools.py index 98113cb67..7d4a95aa1 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -368,14 +368,15 @@ def build_parser(): 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.", + "--type", + help="The type of the problem.", choices=[ - "default", + "pass-fail", + "float", "custom", - "custom interactive", - "custom multi-pass", - "custom interactive multi-pass", + "interactive", + "multi-pass", + "interactive multi-pass", ], ) problemparser.add_argument("--skel", help="Skeleton problem directory to copy from.") diff --git a/bin/util.py b/bin/util.py index 2629c24a8..2a761684e 100644 --- a/bin/util.py +++ b/bin/util.py @@ -777,23 +777,6 @@ def parse_setting(yamldata: dict[str, Any], key: str, default: T) -> T: return default if value is None else value -# Parse validation mode -def parse_validation(mode: str) -> set[str]: - if mode == "default": - return {mode} - else: - ok = True - parsed = set() - for part in mode.split(): - if part in ["custom", "interactive", "multi-pass"] and part not in parsed: - parsed.add(part) - else: - ok = False - if "custom" not in parsed or not ok: - fatal(f"Unrecognised validation mode {mode}.") - return parsed - - # glob, but without hidden files def glob(path: Path, expression: str, include_hidden: bool = False) -> list[Path]: def keep(p: Path) -> bool: diff --git a/bin/validate.py b/bin/validate.py index b76476e50..2a507143e 100644 --- a/bin/validate.py +++ b/bin/validate.py @@ -312,6 +312,7 @@ def __init__(self, problem, path, **kwargs): validator_type = "output" + # TODO #424: We should not support multiple output validators inside output_validator/. source_dirs = ["output_validator", "output_validators"] def run( diff --git a/skel/problem/problem.yaml b/skel/problem/problem.yaml index e4bb0ba63..1aac2c02d 100644 --- a/skel/problem/problem.yaml +++ b/skel/problem/problem.yaml @@ -3,6 +3,8 @@ problem_format_version: 2023-07-draft name: #lang: name {%problemname%} +# 'pass-fail', 'interactive', 'multi-pass', or 'interactive multi-pass' +type: {%type%} author: {%author%} # Contest name and year source: {%source%} @@ -11,8 +13,6 @@ source_url: {%source_url%} uuid: {%uuid%} license: {%license%} rights_owner: {%rights_owner%} -# 'default', 'custom', or 'custom interactive' -validation: {%validation%} # One or more of: # case_sensitive diff --git a/test/test_problems.py b/test/test_problems.py index ed0130419..a409e8ebb 100644 --- a/test/test_problems.py +++ b/test/test_problems.py @@ -192,8 +192,8 @@ def test_new_contest_problem(self, monkeypatch): "Problem One", "--author", "Ragnar Groot Koerkamp", - "--validation", - "default", + "--type", + "pass-fail", ] ) os.chdir("contest_name") @@ -217,5 +217,5 @@ class TestReadProblemConfig: def test_read_problem_config(self): p = problem.Problem(RUN_DIR / "test/problems/test_problem_config", Path("/tmp/xyz")) assert p.settings.name["en"] == "ABC XYZ" - assert p.settings.validation == "custom" + assert p.custom_output and not p.interactive and not p.multi_pass assert p.limits.time_limit == 3.0