Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Package multiple programs at once #158

Merged
merged 3 commits into from
Dec 27, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 94 additions & 88 deletions algobattle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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(
Expand All @@ -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")
Expand Down Expand Up @@ -604,49 +613,46 @@ 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[
bool, Option("--test/--no-test", help="Whether to test the programs before packaging them")
] = 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):
Expand Down
Loading