CoolFace
Apppublic

sahil-12kumar/IL_CMS_Tools

sourceHugging Faceupdated 5d agoView on Hugging Face
1likes
test_sample_docx.py139 linesDownload Raw Back to tests
1"""The shipped Word template must stay a clean paste-over target for SMEs.2 3samples/sample_questions.docx is generated by make_sample_docx.py; regenerate with4`python make_sample_docx.py --check` if a parser change moves it.5"""6import re7from pathlib import Path8 9import pytest10 11ROOT = Path(__file__).resolve().parent.parent12SAMPLE = ROOT / 'samples' / 'sample_questions.docx'13 14 15@pytest.fixture(scope='module')16def sample(app_mod):17    if not SAMPLE.exists():18        pytest.skip(f'{SAMPLE.name} missing — run make_sample_docx.py')19    qs = app_mod.parse_word(str(SAMPLE))20    app_mod.validate_questions(qs)21    return qs22 23 24@pytest.fixture(scope='module')25def paragraphs():26    if not SAMPLE.exists():27        pytest.skip(f'{SAMPLE.name} missing — run make_sample_docx.py')28    from docx import Document29    return [p.text.strip() for p in Document(str(SAMPLE)).paragraphs if p.text.strip()]30 31 32def test_all_five_questions_parse(sample):33    assert len(sample) == 5, [q['question_text'][:40] for q in sample]34 35 36def test_no_question_is_flagged(sample):37    bad = [(i, q['issues']) for i, q in enumerate(sample, 1) if q['issues']]38    assert bad == [], f'the template must be clean: {bad}'39 40 41def test_every_question_has_an_answer_and_a_solution(sample):42    for i, q in enumerate(sample, 1):43        assert str(q['correct_answer']).strip(), f'Q{i} has no answer key'44        assert q['solution'].strip(), f'Q{i} has no solution'45 46 47# ── only the four line kinds an SME needs ────────────────────────────────────48def test_template_has_no_headings_or_metadata_lines(paragraphs):49    """question -> options -> answer -> solution, and nothing else. No subject50    heading, no title, no 'Question Type:'/'Difficulty Level:' lines."""51    banned = re.compile(r'^(MATHEMATICS|PHYSICS|CHEMISTRY|BOTANY|ZOOLOGY|BIOLOGY)\b'52                        r'|^Question\s*Type\s*[:\-]|^Difficulty', re.I)53    offenders = [p for p in paragraphs if banned.match(p)]54    assert offenders == [], offenders55 56 57def test_template_opens_with_a_question(paragraphs):58    """Any other opening line would itself be parsed as a question."""59    assert re.match(r'^(?:Q\s*)?\d+[.:]\s*\S', paragraphs[0]), paragraphs[0]60 61 62def test_no_phantom_questions(sample):63    """Every parsed question must have an answer — a phantom made from a stray64    line (a heading, or a solution paragraph) would have none."""65    for i, q in enumerate(sample, 1):66        assert q['correct_answer'], f'Q{i} looks like a phantom question'67 68 69# ── both option marker styles ────────────────────────────────────────────────70def test_template_shows_both_option_styles(paragraphs):71    joined = '\n'.join(paragraphs)72    assert re.search(r'^[a-d]\)\s', joined, re.M), 'no lowercase a) b) c) d) example'73    assert re.search(r'^\([A-D]\)\s', joined, re.M), 'no (A) (B) (C) (D) example'74    assert re.search(r'^1\)\s.*\s2\)\s', joined, re.M), 'no numbered 1) 2) 3) 4) example'75 76 77@pytest.mark.parametrize('idx,expected_answer', [(0, 'b'), (1, '3'), (4, 'B')])78def test_both_styles_parse_their_options_and_answer(app_mod, sample, idx, expected_answer):79    q = sample[idx]80    filled = sum(1 for k in 'abcd' if app_mod._option_filled(q.get(f'option_{k}', '')))81    assert filled == 4, f'Q{idx + 1}: {filled}/4 options'82    assert q['correct_answer'] == expected_answer83    # the marker itself must not survive into the option HTML84    assert not re.match(r'^<p>\s*\(?[a-dA-D1-4]\)', q['option_a']), q['option_a']85 86 87def test_letter_answers_map_to_positions_in_the_payload(app_mod, sample):88    """'Answer: b' and 'Answer: B' must both reach the CMS as option 2."""89    assert app_mod.build_payload(sample[0])['question']['correct_answer'] == ['2']90    assert app_mod.build_payload(sample[4])['question']['correct_answer'] == ['2']91 92 93# ── types, maths, figure, band ───────────────────────────────────────────────94def test_types_are_inferred_without_metadata_lines(app_mod, sample):95    """4 options -> single correct; no options -> numerical. No type hint needed."""96    types = [app_mod.normalise_qtype(q['question_type']) for q in sample]97    assert types.count('single correct mcq') == 398    assert types.count('numerical value question') == 299 100 101def test_numerical_questions_have_no_options(app_mod, sample):102    for i, q in enumerate(sample, 1):103        if 'numerical' in app_mod.normalise_qtype(q['question_type']):104            filled = sum(1 for k in 'abcd'105                         if app_mod._option_filled(q.get(f'option_{k}', '')))106            assert filled == 0, f'Q{i} is numerical but has {filled} options'107 108 109def test_template_demonstrates_maths_and_a_figure(sample):110    html = ''.join(q['question_text'] + q['solution'] +111                   ''.join(q.get(f'option_{k}', '') for k in 'abcd') for q in sample)112    if '<math' not in html:113        pytest.skip('OMML2MML.XSL not available — see vendor/README.md')114    assert '<img src="data:image/png;base64,' in html, 'the figure did not survive'115 116 117def test_nothing_failed_to_convert(app_mod, sample):118    fields = [q[f] for q in sample for f in119              ('question_text', 'option_a', 'option_b', 'option_c', 'option_d', 'solution')]120    assert app_mod.conversion_failures(*fields) == []121 122 123def test_numerical_band_is_carried_through(app_mod, sample):124    """Q4 is written "Answer: 0.28-0.32" — the hyphen form is the only band the125    answer-line pattern accepts."""126    banded = [q for q in sample if q.get('answer_range')]127    assert len(banded) == 1, 'expected exactly one banded numerical question'128    ranges = app_mod.build_payload(banded[0])['question']['solutionRanges']129    assert ranges == [{'name': 'Default Range', 'start': 0.28, 'end': 0.32}], ranges130 131 132def test_every_question_builds_a_valid_payload(app_mod, sample):133    for i, q in enumerate(sample, 1):134        p = app_mod.build_payload(q)135        assert p['questiontype'], f'Q{i}: question type did not resolve to a CMS uuid'136        assert p['question']['correct_answer'], f'Q{i}: empty answer in payload'137        assert p['question']['solution'], f'Q{i}: empty solution in payload'138        assert p['status'] == 'draft'139