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 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()}") logger.info(f"Converting files in {root.absolute()}")
for repo_toml_path in root.glob("**/repo.toml"): for repo_toml_path in root.glob("**/repo.toml"):
for task_toml_path in repo_toml_path.parent.glob("**/*.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] InputFile = Union[LocalFile, MemoryFile, PreparedFile, Symlink]
Stdin = Union[InputFile, StreamIn]
Stdout = Union[Collector, StreamOut]
Stderr = Union[Collector, StreamOut]
class Cmd(BaseModel): class Cmd(BaseModel):
args: List[str] = [] args: List[str] = []
env: List[str] = [DEFAULT_PATH_ENV] env: List[str] = [DEFAULT_PATH_ENV]
stdin: Union[InputFile, StreamIn] = MemoryFile(content="") stdin: Stdin = MemoryFile(content="")
stdout: Union[Collector, StreamOut] = Collector(name="stdout") stdout: Stdout = Collector(name="stdout")
stderr: Union[Collector, StreamOut] = Collector(name="stderr") stderr: Stderr = Collector(name="stderr")
cpu_limit: int = Field(DEFAULT_CPU_LIMIT, serialization_alias="cpuLimit") cpu_limit: int = Field(DEFAULT_CPU_LIMIT, serialization_alias="cpuLimit")
clock_limit: int = Field( clock_limit: int = Field(
DEFAULT_CLOCK_LIMIT_MULTIPLIER * DEFAULT_CPU_LIMIT, DEFAULT_CLOCK_LIMIT_MULTIPLIER * DEFAULT_CPU_LIMIT,
@ -77,9 +80,9 @@ class Cmd(BaseModel):
class OptionalCmd(BaseModel): class OptionalCmd(BaseModel):
args: Optional[List[str]] = None args: Optional[List[str]] = None
env: Optional[List[str]] = None env: Optional[List[str]] = None
stdin: Optional[Union[InputFile, StreamIn]] = None stdin: Optional[Stdin] = None
stdout: Optional[Union[Collector, StreamOut]] = None stdout: Optional[Stdout] = None
stderr: Optional[Union[Collector, StreamOut]] = None stderr: Optional[Stderr] = None
cpu_limit: Optional[int] = Field(None, serialization_alias="cpuLimit") cpu_limit: Optional[int] = Field(None, serialization_alias="cpuLimit")
clock_limit: Optional[int] = Field(None, serialization_alias="clockLimit") clock_limit: Optional[int] = Field(None, serialization_alias="clockLimit")
memory_limit: Optional[int] = Field(None, serialization_alias="memoryLimit") memory_limit: Optional[int] = Field(None, serialization_alias="memoryLimit")

View File

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

View File

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