diff --git a/algobattle/cli.py b/algobattle/cli.py index da371128..38323a77 100644 --- a/algobattle/cli.py +++ b/algobattle/cli.py @@ -49,7 +49,7 @@ from tomlkit.items import Table as TomlTable from algobattle.battle import Battle -from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, Ui, ProjectConfig +from algobattle.match import AlgobattleConfig, EmptyUi, Match, MatchConfig, MatchupStr, TeamInfo, Ui, ProjectConfig from algobattle.problem import Instance, Problem, Solution from algobattle.program import Generator, Matchup, Solver from algobattle.util import ( @@ -440,6 +440,69 @@ class TestErrors(BaseModel): generator_run: ExceptionInfo | None = None solver_run: ExceptionInfo | None = None + def ok(self) -> bool: + """Return whether the test passed with no problems.""" + return not (self.generator_build or self.solver_build or self.generator_run or self.solver_run) + + +def test_team(config: AlgobattleConfig, team: str, size: int | None = None) -> TestErrors: + problem = config.loaded_problem + console.print(f"Testing programs of team {team}") + errors = TestErrors() + instance = None + + async def gen_builder() -> Generator: + with console.status("Building generator"): + return await Generator.build( + config.teams[team].generator, problem=problem, config=config.as_prog_config(), team_name=team + ) + + try: + with run_async_fn(gen_builder) as gen: + console.print("[success]Generator built successfully") + with console.status("Running generator"): + instance = gen.test(size) + if isinstance(instance, ExceptionInfo): + console.print("[error]Generator didn't run successfully") + errors.generator_run = instance + instance = None + else: + console.print("[success]Generator ran successfully") + except BuildError as e: + console.print("[error]Generator didn't build successfully") + errors.generator_build = ExceptionInfo.from_exception(e) + instance = None + + sol_error = None + + async def sol_builder() -> Solver: + with console.status("Building solver"): + return await Solver.build( + config.teams[team].solver, problem=problem, config=config.as_prog_config(), team_name=team + ) + + try: + with run_async_fn(sol_builder) as sol: + console.print("[success]Solver built successfully") + + instance = instance or cast(Instance, problem.test_instance) + if instance: + with console.status("Running solver"): + sol_error = sol.test(instance) + if isinstance(sol_error, ExceptionInfo): + console.print("[error]Solver didn't run successfully") + errors.solver_run = sol_error + else: + console.print("[success]Solver ran successfully") + else: + console.print("[warning]Cannot test running the solver") + except BuildError as e: + console.print("[error]Solver didn't build successfully") + errors.solver_build = ExceptionInfo.from_exception(e) + instance = None + + return errors + @app.command() def test( @@ -451,66 +514,12 @@ def test( console.print("[error]The folder does not contain an Algobattle project") raise Abort config = AlgobattleConfig.from_file(project) - problem = config.loaded_problem - all_errors: dict[str, Any] = {} + all_errors: dict[str, TestErrors] = {} - for team, team_info in config.teams.items(): - console.print(f"Testing programs of team {team}") - errors = TestErrors() - instance = None - - async def gen_builder() -> Generator: - with console.status("Building generator"): - return await Generator.build( - team_info.generator, problem=problem, config=config.as_prog_config(), team_name=team - ) - - try: - with run_async_fn(gen_builder) as gen: - console.print("[success]Generator built successfully") - with console.status("Running generator"): - instance = gen.test(size) - if isinstance(instance, ExceptionInfo): - console.print("[error]Generator didn't run successfully") - errors.generator_run = instance - instance = None - else: - console.print("[success]Generator ran successfully") - except BuildError as e: - console.print("[error]Generator didn't build successfully") - errors.generator_build = ExceptionInfo.from_exception(e) - instance = None - - sol_error = None - - async def sol_builder() -> Solver: - with console.status("Building solver"): - return await Solver.build( - team_info.solver, problem=problem, config=config.as_prog_config(), team_name=team - ) - - try: - with run_async_fn(sol_builder) as sol: - console.print("[success]Solver built successfully") - - instance = instance or cast(Instance, problem.test_instance) - if instance: - with console.status("Running solver"): - sol_error = sol.test(instance) - if isinstance(sol_error, ExceptionInfo): - console.print("[error]Solver didn't run successfully") - errors.solver_run = sol_error - else: - console.print("[success]Solver ran successfully") - else: - console.print("[warning]Cannot test running the solver") - except BuildError as e: - console.print("[error]Solver didn't build successfully") - errors.solver_build = ExceptionInfo.from_exception(e) - instance = None - - if errors != TestErrors(): - all_errors[team] = errors.model_dump(exclude_defaults=True) + for team in config.teams.keys(): + res = test_team(config, team, size) + if not res.ok(): + all_errors[team] = res if all_errors: err_path = config.project.results.joinpath(f"test-{timestamp()}.json") @@ -604,7 +613,12 @@ def package_problem( @packager.command("programs") def package_programs( project: Annotated[Path, Argument(help="The project folder to use.")] = Path(), - team: Annotated[Optional[str], Option(help="Name of team whose programs should be packaged.")] = None, + team: Annotated[ + Optional[str], + Option( + help="Name of team whose programs should be packaged. If None are specified, every team's are packaged." + ), + ] = None, generator: Annotated[bool, Option(help="Wether to package the generator")] = True, solver: Annotated[bool, Option(help="Wether to package the solver")] = True, test_programs: Annotated[ @@ -612,41 +626,33 @@ def package_programs( ] = True, ) -> None: config = AlgobattleConfig.from_file(project) - if team is None: - match list(config.teams.keys()): - case []: - console.print("[error]The config file doesn't contain a team[/]") - raise Abort - case [name]: - team = name - case _: - console.print( - "[error]The Config file contains multiple teams[/], specify whose programs you want to package" - ) - raise Abort - if team not in config.teams: + if not config.teams: + console.print("[error]The project config file doesn't contain any teams[/]") + raise Abort + if team is not None and team not in config.teams: console.print("[erorr]The selected team isn't in the config file[/]") raise Abort - if test_programs: - test_result = test(project) - if test_result == "error": - console.print("Stopping program packaging since they do not pass tests") - raise Abort out = project.parent if project.is_file() else project - def _package_program(role: Role) -> None: - with console.status(f"Packaging {team}'s {role}"), ZipFile(out / f"{team} {role}.prog", "w") as zipfile: - program_root: Path = getattr(config.teams[team], role) + def _package_program(name: str, info: TeamInfo, role: Role) -> None: + with console.status(f"Packaging {name}'s {role}"), ZipFile(out / f"{name} {role}.prog", "w") as zipfile: + program_root: Path = getattr(info, role) for file in program_root.rglob("*"): if file.is_dir(): continue zipfile.write(file, file.relative_to(program_root)) - console.print(f"[success]Packaged {team}'s {role}") - - if generator: - _package_program(Role.generator) - if solver: - _package_program(Role.solver) + console.print(f"[success]Packaged {name}'s {role}") + + for name, info in [(team, config.teams[team])] if team else config.teams.items(): + if test_programs: + test_result = test_team(config, name) + if not test_result.ok(): + console.print(f"[error]Team {name} does not pass tests") + continue + if generator: + _package_program(name, info, Role.generator) + if solver: + _package_program(name, info, Role.solver) class TimerTotalColumn(ProgressColumn):