"""Plans built to a board's content outline. Two shapes and one rule they share: no question appears twice in a plan. Two domains can map to the same categories of ours — the board splits genitourinary from nephrology and we do not — so a pool-per-domain is not enough on its own. """ import random import unittest from decimal import Decimal from sqlalchemy import create_engine from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool import test_quiz_builder # noqa: F401 — imports every model from app.database import Base from app.models.exam import BlueprintCategoryLink, Exam, ExamBlueprint, QuestionExamLink from app.models.question import Question from app.models.question_category import QuestionCategory from app.models.user import User from app.services import exam_blueprint class BlueprintPlanTests(unittest.TestCase): def setUp(self): self.engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) Base.metadata.create_all(self.engine) self.db = Session(self.engine) self.db.add(User(id=1, name="Mod", email="m@example.test", hashed_password="unused", role="moderator")) self.db.add(Exam(id=1, slug="boards", name="Boards")) self.db.add_all([QuestionCategory(id=1, name="Kidney", user_id=1), QuestionCategory(id=2, name="Chest", user_id=1)]) self.db.flush() for qid in range(1, 121): self.db.add(Question(id=qid, question_category_id=1 if qid <= 60 else 2, user_id=1, question_text=f"Q{qid}", question_type="mcq", options=["a", "b"], correct_answer="a")) self.db.add(QuestionExamLink(question_id=qid, exam_id=1)) # Two domains, one shared category: exactly the case that repeats. self.db.add_all([ ExamBlueprint(id=1, exam_id=1, code="19", title="Nephrology", weight=Decimal("50"), sort_order=0), ExamBlueprint(id=2, exam_id=1, code="20", title="Genitourinary", weight=Decimal("30"), sort_order=1), ExamBlueprint(id=3, exam_id=1, code="12", title="Pulmonology", weight=Decimal("20"), sort_order=2), ]) self.db.flush() self.db.add_all([BlueprintCategoryLink(blueprint_id=1, category_id=1), BlueprintCategoryLink(blueprint_id=2, category_id=1), BlueprintCategoryLink(blueprint_id=3, category_id=2)]) self.db.commit() def tearDown(self): self.db.close() self.engine.dispose() def test_papers_never_repeat_a_question_across_blocks(self): papers = exam_blueprint.sample_papers(self.db, 1, 10, 6, rng=random.Random(3)) drawn = [q for ids, _ in papers for q in ids] self.assertEqual(len(drawn), len(set(drawn))) def test_two_domains_sharing_a_category_do_not_deal_the_same_question(self): # Nephrology and Genitourinary both point at Kidney. Without a record # of what has gone, a question is dealt once from each. self.assertEqual(exam_blueprint.sample_papers(self.db, 1, 20, 0), [], "asking for no papers draws nothing") one = exam_blueprint.sample_papers(self.db, 1, 20, 1, rng=random.Random(3)) ids = one[0][0] self.assertEqual(len(ids), len(set(ids))) def test_a_paper_keeps_its_length_and_the_board_s_proportions(self): papers = exam_blueprint.sample_papers(self.db, 1, 10, 1, rng=random.Random(3)) ids, report = papers[0] self.assertEqual(len(ids), 10) given = {row["code"]: row["given"] for row in report} self.assertEqual(given["19"], 5) # 50% self.assertEqual(given["12"], 2) # 20% def test_the_plan_shortens_rather_than_repeating_when_the_bank_runs_out(self): # Twelve papers of forty is four hundred and eighty from a bank of 120. papers = exam_blueprint.sample_papers(self.db, 1, 40, 12, rng=random.Random(3)) drawn = [q for ids, _ in papers for q in ids] self.assertEqual(len(drawn), len(set(drawn))) self.assertLessEqual(len(drawn), 120) def test_domain_blocks_are_sized_by_weight_not_by_what_is_lying_about(self): # Chunking every question into blocks gave preventive care six blocks # and a plan of a hundred and sixty. Weight decides how many. blocks = exam_blueprint.domain_blocks(self.db, 1, 10, 6, rng=random.Random(3)) counts = {} for code, _, ids in blocks: counts[code] = counts.get(code, 0) + 1 self.assertGreater(counts["19"], counts["12"], "the heavier domain gets more blocks") ids = [q for _, _, b in blocks for q in b] self.assertEqual(len(ids), len(set(ids))) def test_every_domain_with_questions_gets_at_least_one_block(self): blocks = exam_blueprint.domain_blocks(self.db, 1, 10, 2, rng=random.Random(3)) # Two blocks asked for, three domains: nothing the board examines is # left out of a plan built from its outline. self.assertEqual({code for code, _, _ in blocks}, {"19", "20", "12"}) def test_blocks_carry_the_board_s_own_titles(self): blocks = exam_blueprint.domain_blocks(self.db, 1, 10, 6, rng=random.Random(3)) self.assertTrue(any(title.startswith("Nephrology") for _, title, _ in blocks)) self.assertTrue(any("of" in title for _, title, _ in blocks), "a split domain says so") def test_an_exam_with_no_blueprint_builds_nothing(self): self.db.add(Exam(id=2, slug="step", name="Step")) self.db.commit() self.assertEqual(exam_blueprint.sample_papers(self.db, 2, 40, 4), []) self.assertEqual(exam_blueprint.domain_blocks(self.db, 2, 40, 4), []) if __name__ == "__main__": unittest.main()