# retoor <retoor@molodetz.nl>
import time
from tests.api.quizzes._helpers import (
CHOICE,
FREE_TEXT,
JSON,
NUMERIC,
TRUE_FALSE,
answer_correctly,
attempt_state,
build_published,
signup,
start_attempt,
)
from tests.conftest import BASE_URL
CACHE_VERSION_TTL_SECONDS = 1.5
FILL_BLANK = {
"kind": "fill_blank",
"prompt": "Name the two pragmas",
"points": 2,
"options": "First\nSecond",
"match_values": "journal_mode\nsynchronous",
}
ORDERING = {
"kind": "ordering",
"prompt": "Order the steps",
"points": 3,
"options": "Open\nRead\nClose",
}
MATCHING = {
"kind": "matching",
"prompt": "Match the pragma",
"points": 2,
"options": "journal_mode\nsynchronous",
"match_values": "WAL\nNORMAL",
}
def _single_question(session, question, **settings):
slug = build_published(session, questions=(question,), title=f"Graded {question['kind']}", **settings)
return slug
def _drop_api_key(username):
from devplacepy.database import get_table, refresh_snapshot
from devplacepy.utils import clear_user_cache
refresh_snapshot()
user = get_table("users").find_one(username=username)
get_table("users").update({"uid": user["uid"], "api_key": ""}, ["uid"])
clear_user_cache(user["uid"])
refresh_snapshot()
time.sleep(CACHE_VERSION_TTL_SECONDS)
def _answer(session, slug, payload):
attempt_uid = start_attempt(session, slug)
question = attempt_state(session, slug, attempt_uid)["attempt"]["questions"][0]
body = {"question_uid": question["uid"], **payload(question)}
response = session.post(
f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/answer", data=body, headers=JSON
)
return response, question
def test_a_single_choice_is_graded_deterministically(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, CHOICE)
player, _ = signup()
response, _ = _answer(
player, slug, lambda question: {"option_uids": question["options"][1]["uid"]}
)
answer = response.json()["answer"]
assert answer["graded_by"] == "auto"
assert answer["is_correct"] is True
assert answer["awarded_points"] == 2.0
def test_a_wrong_single_choice_awards_nothing(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, CHOICE)
player, _ = signup()
response, _ = _answer(
player, slug, lambda question: {"option_uids": question["options"][0]["uid"]}
)
assert response.json()["answer"]["awarded_points"] == 0.0
def test_a_true_false_is_graded(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, TRUE_FALSE)
player, _ = signup()
response, _ = _answer(player, slug, lambda question: {"answer_text": "true"})
assert response.json()["answer"]["is_correct"] is True
def test_a_numeric_within_tolerance_is_correct(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, {**NUMERIC, "numeric_tolerance": "1"})
player, _ = signup()
response, _ = _answer(player, slug, lambda question: {"answer_text": "1023"})
assert response.json()["answer"]["is_correct"] is True
def test_a_fill_blank_gives_partial_credit(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FILL_BLANK)
player, _ = signup()
response, _ = _answer(
player, slug, lambda question: {"blanks": "journal_mode,wrong"}
)
answer = response.json()["answer"]
assert answer["awarded_points"] == 1.0
assert answer["is_correct"] is False
def test_a_full_fill_blank_is_correct(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FILL_BLANK)
player, _ = signup()
response, _ = _answer(
player, slug, lambda question: {"blanks": "journal_mode,synchronous"}
)
assert response.json()["answer"]["is_correct"] is True
def test_an_ordering_gives_prefix_credit(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, ORDERING)
player, _ = signup()
def payload(question):
uids = [option["uid"] for option in question["options"]]
return {"option_uids": ",".join([uids[0], uids[2], uids[1]])}
response, _ = _answer(player, slug, payload)
assert response.json()["answer"]["awarded_points"] == 1.0
def test_a_matching_gives_pair_credit(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, MATCHING)
player, _ = signup()
def payload(question):
uids = [option["uid"] for option in question["options"]]
return {"option_uids": ",".join(uids), "matches": "WAL,wrong"}
response, _ = _answer(player, slug, payload)
assert response.json()["answer"]["awarded_points"] == 1.0
def test_a_matching_exposes_its_right_hand_choices(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, MATCHING)
player, _ = signup()
attempt_uid = start_attempt(player, slug)
question = attempt_state(player, slug, attempt_uid)["attempt"]["questions"][0]
assert sorted(question["match_choices"]) == ["NORMAL", "WAL"]
assert all(option["match_value"] is None for option in question["options"])
def test_a_free_text_answer_is_still_graded_without_the_gateway(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
response, _ = _answer(
player,
slug,
lambda question: {"answer_text": "the planner picks a bad index for live rows"},
)
assert response.status_code == 200
assert response.json()["answer"]["answered"] is True
def test_the_degraded_grade_is_stamped_as_a_fallback(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
response, _ = _answer(
player, slug, lambda question: {"answer_text": "a one bucket index for live rows"}
)
assert response.json()["answer"]["graded_by"] == "fallback"
def test_the_fallback_explains_itself_to_the_learner(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
response, _ = _answer(player, slug, lambda question: {"answer_text": "a one bucket index"})
assert "unavailable" in response.json()["answer"]["feedback"]
def test_the_fallback_score_stays_within_the_question_points(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
response, _ = _answer(
player,
slug,
lambda question: {"answer_text": "It keeps the live query off a one bucket index."},
)
answer = response.json()["answer"]
assert 0.0 <= answer["awarded_points"] <= 3.0
assert 0.0 <= answer["confidence"] <= 1.0
def test_an_empty_free_text_answer_scores_zero(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
response, _ = _answer(player, slug, lambda question: {"answer_text": "completely unrelated"})
assert response.json()["answer"]["awarded_points"] == 0.0
def test_the_results_page_surfaces_the_fallback_note(app_server, seeded_db):
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT, allow_review="1")
player, player_name = signup()
_drop_api_key(player_name)
attempt_uid = start_attempt(player, slug)
question = attempt_state(player, slug, attempt_uid)["attempt"]["questions"][0]
player.post(
f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/answer",
data={"question_uid": question["uid"], "answer_text": "a one bucket index"},
headers=JSON,
)
player.post(f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/finish", headers=JSON)
payload = player.get(
f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/results", headers=JSON
).json()
assert payload["fallback_count"] == 1
page = player.get(f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/results").text
assert "quiz-grade-fallback" in page
def test_a_degraded_grade_records_a_system_audit_failure(app_server, seeded_db):
from devplacepy.database import get_table, refresh_snapshot
owner, _ = signup()
slug = _single_question(owner, FREE_TEXT)
player, player_name = signup()
_drop_api_key(player_name)
_answer(player, slug, lambda question: {"answer_text": "a one bucket index"})
refresh_snapshot()
count = get_table("audit_log").count(event_key="quiz.grade.failed", result="failure")
assert count >= 1
def test_only_free_text_ever_reaches_the_grader(app_server, seeded_db):
owner, _ = signup()
slug = build_published(owner, questions=(CHOICE, NUMERIC, TRUE_FALSE), title="No gateway quiz")
player, _ = signup()
attempt_uid = start_attempt(player, slug)
state = attempt_state(player, slug, attempt_uid)
for question in state["attempt"]["questions"]:
payload = answer_correctly(player, slug, attempt_uid, question).json()
assert payload["answer"]["graded_by"] == "auto"