Skip to content

Commit

Permalink
Add generate() and trivial_solve() methods on an Input
Browse files Browse the repository at this point in the history
  • Loading branch information
jackrosenthal committed Feb 16, 2024
1 parent 122fb1d commit fefd947
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 0 deletions.
19 changes: 19 additions & 0 deletions algobowl/lib/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
import dataclasses
import enum
import pathlib
import random
import types
import typing

from typing_extensions import Self

from algobowl.lib import problem_tester


Expand Down Expand Up @@ -56,6 +59,22 @@ def write(self, f):
"""Write an input file."""
raise NotImplementedError

@classmethod
def generate(cls, rng: random.Random) -> Self:
"""Generate an input."""
raise NotImplementedError

def trivial_solve(self) -> "BaseOutput":
"""Solve this input in the most trivial way possible.
This should solve the input in a valid but computationally cheap manner.
Perhaps you want to aim to solve the problem with the worst answer
possible. Problems are not required to implement this, but it does
provide some fuzz testing of outputs and the verifier when combined with
the generate() method.
"""
raise NotImplementedError


@dataclasses.dataclass
class BaseOutput:
Expand Down
36 changes: 36 additions & 0 deletions algobowl/lib/problem_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import enum
import io
import pathlib
import random
import sys

import pytest
Expand All @@ -21,6 +22,11 @@ def problem(problem_dir):
return problemlib.Problem(problem_dir)


@pytest.fixture(params=[1337, 0xDEADBEEF, 0xDEADD00D, 55378008])
def rng(request):
return random.Random(request.param)


def stringio_from_path(path, ascii_format=False):
contents = path.read_text(encoding="ascii")
assert "\r\n" not in contents
Expand Down Expand Up @@ -107,6 +113,36 @@ def test_rejected_output(problem, rejected_output_path, ascii_format):
assert output.score == reformatted_output.score


def test_generate_input(problem, rng):
try:
input = problem.get_module().Input.generate(rng)
except NotImplementedError:
pytest.skip("Input.generate() is not required (yet!)")

# The input should be writable.
input_buf = io.StringIO()
input.write(input_buf)

# We should be able to parse the generated input.
problem.parse_input(io.StringIO(input_buf.getvalue()))

# Solve the generated input.
try:
output = input.trivial_solve()
except NotImplementedError:
pytest.skip("Input.trivial_solve() not implemented")

# The solved output should be valid.
output.verify()

# The solved output should be writable.
output_buf = io.StringIO()
output.write(output_buf)

# We should be able to parse the written output.
problem.parse_output(input, io.StringIO(output_buf.getvalue()))


def load_inputs_from_dir(path):
return list(path.glob("*.in"))

Expand Down
36 changes: 36 additions & 0 deletions docs/problem_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,23 @@ def write(self, f):
print(*self.list_of_ints, file=f)
```

The `Input` class should also provide a `generate` method to generate a random
input. This will be used to create default inputs for each group should they
fail to upload an input.

The `generate` method takes a pre-seeded
[`random.Random`](https://docs.python.org/3/library/random.html#random.Random)
object to generate random numbers. *You should not use random numbers except
from this object.* The tests use a fixed-set of seeds so they are reproducible.

For example:

```python
@classmethod
def generate(cls, rng):
return cls(list_of_ints=[rng.randint(0, 100) for _ in range(20)], ...)
```

## Writing example inputs

Now that you've written the `Input` class, it's time to write example inputs.
Expand Down Expand Up @@ -306,6 +323,25 @@ Ideally, you want to bring your module to 100% test coverage, as then you know
that each edge case students may hit is tested. Keep adding example inputs and
outputs until you've reached 100% test coverage.

## Optional: Implement a trivial solver

While it's not required, it's highly recommended to implement a trivial solver
for the problem. This solver will be used by tests to provide a little fuzz
coverage when coupled with `Input.generate()`.

You should implement the absolute most trivial solution for the problem
possible. Think: *how can I make the answer as bad as possible?*

The method is called `trivial_solve` on your `Input` class, and returns
an `Output` object.

For example:

```python
def trivial_solve(self):
return Output(...)
```

## Uploading for review

Create a new branch and commit your changes:
Expand Down
9 changes: 9 additions & 0 deletions example_problems/number_in_range/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ def write(self, f):
print(self.low, file=f)
print(self.high, file=f)

@classmethod
def generate(cls, rng):
low = rng.randint(1, 100)
high = rng.randint(low, 100)
return cls(low=low, high=high)

def trivial_solve(self):
return Output(input=self, score=self.high)


@dataclasses.dataclass
class Output(problemlib.BaseOutput):
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"requests>=2.0",
"tabulate>=0.8",
"toml>=0.10",
"typing-extensions",
]

WEB_DEPENDS = [
Expand Down

0 comments on commit fefd947

Please sign in to comment.