Using Test Context in Feedback
To provide detailed feedback based on test outcomes, there needs to be a
mechanism for maps to utilize the test context. For instance, if
test_compilation
fails, including the raw compilation error message in the
feedback can guide students to correct their code.
Assuming --artifacts=artifacts
is specified when invoking socassess, by
default, only the artifacts/report.xml
will be generated. However, the
artifacts
folder can be used as the bridge for maps to access test context.
Consider this example with a test named test_and_provide_context
, which has a
parameter called artifacts
:
from pathlib import Path
def test_and_provide_context(artifacts: Path):
...
Here, artifacts
is a fixture defined in conftest.py
, with the implementation
as follows:
# inside conftest.py
@pytest.fixture(scope="session")
def artifacts(request) -> Path:
"""Contains the folder path to store artifacts."""
opt = request.config.getoption("--artifacts")
return Path(opt)
This setup allows storing any desired test context that maps might later access, such as:
from pathlib import Path
def test_and_provide_context(artifacts: Path):
(artifacts / 'test_case_context.txt').write_text("""
test_and_provide_context: log line #1
test_and_provide_context: log line #2
test_and_provide_context: log line #3
test_and_provide_context: log line ...
""".strip())
assert True
When the test test_and_provide_context
runs, it creates a file named
test_case_context.txt
containing several log lines.
socassess permits access to the artifacts folder through userargs.artifacts
.
Here shows an example:
from socassess import userargs
detail = {
frozenset([
'test_it::test_and_provide_context::passed',
]): {
'feedback': """
Congrats! test_and_provide_context passed.
In addition, here are more details about it:
{content}
""".strip(),
'function': (userargs.artifacts / 'test_case_context.txt').read_text,
},
}
Note the new key function
. The value of function
must be a
callable; hence, in
the example, it is .read_text
instead of .read_text()
.
Upon encountering such a callable, socassess will execute it and use its result
to fill {content}
. Therefore, the automated feedback will be:
## detail
Congrats! test_and_provide_context passed.
In addition, here are more details about it:
test_and_provide_context: log line #1
test_and_provide_context: log line #2
test_and_provide_context: log line #3
test_and_provide_context: log line ...
Using a Function with Parameters
It is feasible to use the same function in multiple places with only minor
differences. In such cases, a params
parameter can be provided to the
function, requiring it to be defined as def func(params)
. This approach is
particularly useful when the function itself is complex. Here is an example:
# test cases (always pass)
# note that we assume they are executed sequentially
# so it is fine for them to append text to the same file
from pathlib import Path
def test_and_provide_context_1(artifacts: Path):
f = (artifacts / 'test_case_context.txt').open('a')
f.write("test_and_provide_context_1: log line #1\n")
f.write("test_and_provide_context_1: log line #2\n")
f.write("test_and_provide_context_1: log line #3\n")
f.write("test_and_provide_context_1: log line ...\n")
assert True
def test_and_provide_context_2(artifacts: Path):
f = (artifacts / 'test_case_context.txt').open('a')
f.write("test_and_provide_context_2: log line #1\n")
f.write("test_and_provide_context_2: log line #2\n")
f.write("test_and_provide_context_2: log line #3\n")
f.write("test_and_provide_context_2: log line ...\n")
assert True
The essential change is to replace
{
'feedback': ...,
'function': myfunc,
...
}
with
{
'feedback': ...,
'function': { 'name': myfunc, 'params': myparams },
...
}
The params
can be any type, such as a str
, a list
, or a dict
. Here we
define def shared_func(params)
with params
assigned to
test_and_provide_context_1
or test_and_provide_context_2
separately.
# maps
from socassess import userargs
def shared_func(params: str):
content = (userargs.artifacts / 'test_case_context.txt').open('r')
filtered_lines = []
for line in content:
if params in line:
filtered_lines.append(line)
return f"""
{params} passed.
In addition, here are more details about it:
{''.join(filtered_lines)}
""".strip()
detail = {
frozenset([
'test_it::test_and_provide_context_1::passed',
]): {
'feedback': "Congrats! {content}",
'function': {
'name': shared_func,
'params': 'test_and_provide_context_1',
}
},
frozenset([
'test_it::test_and_provide_context_2::passed',
]): {
'feedback': "Congrats! {content}",
'function': {
'name': shared_func,
'params': 'test_and_provide_context_2',
}
},
}
The feedback will be:
## detail
Congrats! test_and_provide_context_1 passed.
In addition, here are more details about it:
test_and_provide_context_1: log line #1
test_and_provide_context_1: log line #2
test_and_provide_context_1: log line #3
test_and_provide_context_1: log line ...
Congrats! test_and_provide_context_2 passed.
In addition, here are more details about it:
test_and_provide_context_2: log line #1
test_and_provide_context_2: log line #2
test_and_provide_context_2: log line #3
test_and_provide_context_2: log line ...