(Easy) Context¶
Sometimes, you might want to capture some definitions in students' submissions for testing purposes. For example, suppose student writes a class Car
that uses their definition of GasStation
. You might want to reference the GasStation
class when overriding tests.
Note that you can access captured context only in the gap_override_test
and any tests' pre hooks or post hooks.
Learn by Example¶
Suppose we are creating a problem that asks students to implement a function add
that adds two numbers, using a given adder
function. The required adder
implementation is add two numbers and mod the result by 10. Below, we first define the golden solution adder named my_adder
, and a custom testing function custom_test
that uses the captured context, and the problem solution where the context is specified.
from gapper import problem, test_case, test_cases
from gapper.core.types import CustomTestData
from typing import Callable
# my_adder is a solution adder defined along with the solution
def my_adder(a, b) -> int:
return a + b % 10
# variable `adder` is not defined here, this is just a type hint
# the name `adder` will be specified in the context to be captured
# see the last comment in this code
# and the name `adder` will become available to custom override
# testing function or the pre-/post-hooks
adder: Callable[[int, int], int]
# custom test that uses the captured context
def custom_test(data: CustomTestData):
assert my_adder(*data.args) == adder(*data.args) # notice here
# adder is not defined, but we can use it
# this is because it will be captured from students' submission context
# test if student's adder behaves the same in the solution as in their submission
assert data.solution(*data.args, adder) == data.submission(
*data.args,
data.case.context.adder, # case.context.adder is the same as adder
)
@test_cases.param_iter(([i, i + 1] for i in range(10)), gap_override_test=custom_test)
@test_case(1, 2, gap_override_test=custom_test)
# we specify the name of the context to be captured
# easy_context is the flag that allows `add` to be used even though it's not defined
@problem(context=["adder"], easy_context=True)
def add(a: int, b: int, the_adder: Callable[[int, int], int]) -> int:
return the_adder(a, b)
Thus if the student's submission is
The test will pass because the student's adder
is semantically equivalent to the solution's my_adder
.
The easy_context
flag, which is set to True
by default, in the @problem
decorator allows you to inject context variables directly. In the example above, you can use add
even though it's not defined. If easy_context
is set to False, you can still access the captured context using case.context.<context_variable_name>
. For example, case.context.adder
If any of the names specified in context
is not present in student's submission, the submission will be rejected without testing.
That is, in the example above, if the student's submission is
, because the file does not containadder
, it will be rejected.