refactor: do not require casex.in exists
All checks were successful
build / build (push) Successful in 2m10s
build / trigger-build-image (push) Successful in 12s

This commit is contained in:
张泊明518370910136 2025-06-09 03:50:23 -04:00
parent d211d7a429
commit 45450b4451
GPG Key ID: D47306D7062CDA9D
4 changed files with 107 additions and 68 deletions

View File

@ -92,6 +92,7 @@ def convert(
"""
Convert given dir of JOJ3 toml config files to JOJ3 json config files
"""
app.pretty_exceptions_enable = False
logger.info(f"Converting files in {root.absolute()}")
for repo_toml_path in root.glob("**/repo.toml"):
for task_toml_path in repo_toml_path.parent.glob("**/*.toml"):

View File

@ -43,14 +43,17 @@ class StreamOut(BaseModel):
InputFile = Union[LocalFile, MemoryFile, PreparedFile, Symlink]
Stdin = Union[InputFile, StreamIn]
Stdout = Union[Collector, StreamOut]
Stderr = Union[Collector, StreamOut]
class Cmd(BaseModel):
args: List[str] = []
env: List[str] = [DEFAULT_PATH_ENV]
stdin: Union[InputFile, StreamIn] = MemoryFile(content="")
stdout: Union[Collector, StreamOut] = Collector(name="stdout")
stderr: Union[Collector, StreamOut] = Collector(name="stderr")
stdin: Stdin = MemoryFile(content="")
stdout: Stdout = Collector(name="stdout")
stderr: Stderr = Collector(name="stderr")
cpu_limit: int = Field(DEFAULT_CPU_LIMIT, serialization_alias="cpuLimit")
clock_limit: int = Field(
DEFAULT_CLOCK_LIMIT_MULTIPLIER * DEFAULT_CPU_LIMIT,
@ -77,9 +80,9 @@ class Cmd(BaseModel):
class OptionalCmd(BaseModel):
args: Optional[List[str]] = None
env: Optional[List[str]] = None
stdin: Optional[Union[InputFile, StreamIn]] = None
stdout: Optional[Union[Collector, StreamOut]] = None
stderr: Optional[Union[Collector, StreamOut]] = None
stdin: Optional[Stdin] = None
stdout: Optional[Stdout] = None
stderr: Optional[Stderr] = None
cpu_limit: Optional[int] = Field(None, serialization_alias="cpuLimit")
clock_limit: Optional[int] = Field(None, serialization_alias="clockLimit")
memory_limit: Optional[int] = Field(None, serialization_alias="memoryLimit")

View File

@ -130,8 +130,7 @@ class Parser(str, Enum):
ELF = "elf"
class Stage(BaseModel):
name: str = "" # Stage name
class Case(BaseModel):
env: List[str] = []
command: str = "" # Command to run
files: StageFiles = StageFiles()
@ -140,9 +139,16 @@ class Stage(BaseModel):
copy_in_cwd: bool = Field(
True, validation_alias=AliasChoices("copy-in-cwd", "copy_in_cwd")
)
score: int = 0
parsers: List[Parser] = [] # list of parsers
limit: Limit = Limit()
score: int = 0
diff: ParserDiff = ParserDiff()
class Stage(Case):
name: str = "" # stage name
skip: List[str] = []
parsers: List[Parser] = [] # list of parsers
dummy: ParserDummy = ParserDummy()
result_status: ParserDummy = Field(
ParserDummy(), validation_alias=AliasChoices("result-status", "result_status")
@ -157,11 +163,8 @@ class Stage(BaseModel):
validation_alias=AliasChoices("result-detail", "result_detail"),
)
file: ParserFile = ParserFile()
skip: List[str] = []
# cases related
cases: Dict[str, "Stage"] = {}
diff: ParserDiff = ParserDiff()
cases: Dict[str, Case] = {}
model_config = ConfigDict(extra="allow")

View File

@ -2,7 +2,7 @@ import re
import shlex
from functools import partial
from pathlib import Path, PurePosixPath
from typing import Any, Callable, Dict, List, Set, Tuple
from typing import Any, Callable, Dict, List, Optional, Tuple
from joj3_config_generator.models import result, task
from joj3_config_generator.models.common import Memory, Time
@ -11,7 +11,6 @@ from joj3_config_generator.models.const import (
DEFAULT_PATH_ENV,
JOJ3_CONFIG_ROOT,
)
from joj3_config_generator.models.task import Parser as ParserEnum
from joj3_config_generator.utils.logger import logger
@ -53,18 +52,18 @@ def get_parser_handler_map(
executor: result.Executor,
task_root: Path,
task_path: Path,
) -> Dict[ParserEnum, Tuple[Callable[[Any, result.Parser], None], Any]]:
) -> Dict[task.Parser, Tuple[Callable[[Any, result.Parser], None], Any]]:
return {
ParserEnum.ELF: (fix_keyword, task_stage.elf),
ParserEnum.CLANG_TIDY: (fix_keyword, task_stage.clangtidy),
ParserEnum.KEYWORD: (fix_keyword, task_stage.keyword),
ParserEnum.CPPCHECK: (fix_keyword, task_stage.cppcheck),
ParserEnum.CPPLINT: (fix_keyword, task_stage.cpplint),
ParserEnum.RESULT_DETAIL: (fix_result_detail, task_stage.result_detail),
ParserEnum.DUMMY: (fix_dummy, task_stage.dummy),
ParserEnum.RESULT_STATUS: (fix_dummy, task_stage.result_status),
ParserEnum.FILE: (fix_file, task_stage.file),
ParserEnum.DIFF: (
task.Parser.ELF: (fix_keyword, task_stage.elf),
task.Parser.CLANG_TIDY: (fix_keyword, task_stage.clangtidy),
task.Parser.KEYWORD: (fix_keyword, task_stage.keyword),
task.Parser.CPPCHECK: (fix_keyword, task_stage.cppcheck),
task.Parser.CPPLINT: (fix_keyword, task_stage.cpplint),
task.Parser.RESULT_DETAIL: (fix_result_detail, task_stage.result_detail),
task.Parser.DUMMY: (fix_dummy, task_stage.dummy),
task.Parser.RESULT_STATUS: (fix_dummy, task_stage.result_status),
task.Parser.FILE: (fix_file, task_stage.file),
task.Parser.DIFF: (
partial(
fix_diff,
task_stage=task_stage,
@ -178,36 +177,31 @@ def fix_diff(
task_path: Path,
) -> None:
base_dir = JOJ3_CONFIG_ROOT / task_path.parent
# all intended testcases that is detected
testcases = get_testcases(task_root, task_path)
# all testcases that is not specified in the toml config
default_cases = sorted(
testcases.difference(
[
casei
for casei in testcases
if any(casei.endswith(casej) for casej in task_stage.cases)
]
)
)
# those in toml config that is not skipped
valid_cases = [
(casej, task_stage.cases[casei])
for casei in task_stage.cases
for casej in testcases
if (casei not in task_stage.skip and casej.endswith(casei))
# cases not specified in the toml config (auto-detected)
unspecified_cases = get_unspecified_cases(task_root, task_path, task_stage.cases)
# cases specified in toml config but not skipped
specified_cases = [
(case, task_stage.cases[case])
for case in task_stage.cases
if case not in task_stage.skip
]
stage_cases = []
parser_cases = []
for case, case_stage in valid_cases:
for case_name, case in specified_cases:
stdin, stdout = get_stdin_stdout(task_root, task_path, case_name, case)
if stdout is None:
logger.warning(
f"In file {task_root / task_path}, "
f"testcase {case_name} has no corresponding .out file, "
"skipped"
)
continue
cmd = result.OptionalCmd(
stdin=result.LocalFile(
src=str(base_dir / (case_stage.in_ or f"{case}.in"))
),
args=shlex.split(case_stage.command) if case_stage.command else None,
cpu_limit=case_stage.limit.cpu,
clock_limit=DEFAULT_CLOCK_LIMIT_MULTIPLIER * case_stage.limit.cpu,
memory_limit=case_stage.limit.mem,
stdin=stdin,
args=shlex.split(case.command) if case.command else None,
cpu_limit=case.limit.cpu,
clock_limit=DEFAULT_CLOCK_LIMIT_MULTIPLIER * case.limit.cpu,
memory_limit=case.limit.mem,
proc_limit=task_stage.limit.proc,
)
if cmd.args == executor.with_.default.args:
@ -224,22 +218,22 @@ def fix_diff(
parser_case = result.DiffCasesConfig(
outputs=[
result.DiffOutputConfig(
score=case_stage.diff.output.score,
score=case.diff.output.score,
file_name="stdout",
answer_path=str(base_dir / (case_stage.out_ or f"{case}.out")),
force_quit_on_diff=case_stage.diff.output.force_quit,
always_hide=case_stage.diff.output.hide,
compare_space=not case_stage.diff.output.ignore_spaces,
max_diff_length=case_stage.diff.output.max_length,
max_diff_lines=case_stage.diff.output.max_lines,
hide_common_prefix=case_stage.diff.output.hide_common_prefix,
answer_path=stdout,
force_quit_on_diff=case.diff.output.force_quit,
always_hide=case.diff.output.hide,
compare_space=not case.diff.output.ignore_spaces,
max_diff_length=case.diff.output.max_length,
max_diff_lines=case.diff.output.max_lines,
hide_common_prefix=case.diff.output.hide_common_prefix,
)
]
)
parser_cases.append(parser_case)
for case in default_cases:
for case_name in unspecified_cases:
cmd = result.OptionalCmd(
stdin=result.LocalFile(src=str(base_dir / f"{case}.in")),
stdin=result.LocalFile(src=str(base_dir / f"{case_name}.in")),
cpu_limit=None,
clock_limit=None,
memory_limit=None,
@ -251,7 +245,7 @@ def fix_diff(
result.DiffOutputConfig(
score=task_stage.diff.default_score,
file_name="stdout",
answer_path=str(base_dir / f"{case}.out"),
answer_path=str(base_dir / f"{case_name}.out"),
)
]
)
@ -260,9 +254,9 @@ def fix_diff(
diff_parser.with_ = result.DiffConfig(name="diff", cases=parser_cases)
def get_testcases(
task_root: Path, task_path: Path
) -> Set[str]: # basedir here should be task_conf.root / task_conf.path
def get_unspecified_cases(
task_root: Path, task_path: Path, cases: Dict[str, task.Case]
) -> List[str]:
testcases = set()
for testcases_path in (task_root / task_path).parent.glob("**/*.in"):
if not testcases_path.with_suffix(".out").exists():
@ -279,4 +273,42 @@ def get_testcases(
)
).removesuffix(".in")
)
return testcases
return sorted(
testcases.difference(
[
casei
for casei in testcases
if any(casei.endswith(casej) for casej in cases)
]
)
)
def get_stdin_stdout(
task_root: Path, task_path: Path, case_name: str, case: task.Case
) -> Tuple[result.Stdin, Optional[str]]:
case_stdout_name = case.out_ if case.out_ else f"{case_name}.out"
stdin: result.Stdin = result.MemoryFile(content="")
stdout = None
for case_stdout_path in (task_root / task_path).parent.glob("**/*.out"):
if case_stdout_path.name != case_stdout_name:
continue
stdout = str(JOJ3_CONFIG_ROOT / case_stdout_path.relative_to(task_root))
case_stdin_path = case_stdout_path.with_suffix(".in")
if case.in_:
case_stdin_path = Path((task_root / task_path).parent / case.in_)
if not case_stdin_path.exists():
logger.warning(
f"In file {task_root / task_path}, "
f"testcase {case_stdout_path} has no .in file, "
"use empty content as stdin"
)
else:
stdin = result.LocalFile(
src=str(
JOJ3_CONFIG_ROOT
/ PurePosixPath(case_stdin_path.relative_to(task_root))
)
)
break
return stdin, stdout