diff --git a/tests/unit/test_monkey_document.py b/tests/unit/test_monkey_document.py index 73dfb04..d666f72 100644 --- a/tests/unit/test_monkey_document.py +++ b/tests/unit/test_monkey_document.py @@ -37,9 +37,20 @@ def _parse(pages=None, **kwargs): return parse_document_package(_package(pages, **kwargs)) +def _assert_rejected_on_purpose(exc: ResultInvalidError) -> None: + """拒绝必须出自显式校验,不能是 Python 自己绊倒后被外层边界吸收。 + + 外层边界会把 TypeError/KeyError 也归为 ResultInvalidError,所以只断言异常类型, + 「显式校验」与「碰巧在别处失败」无法区分(变异检查证实过: 删掉校验后测试仍绿)。 + 原因链里出现 TypeError/KeyError 即说明有校验缺口。 + """ + assert not isinstance(exc.__cause__, TypeError | KeyError), repr(exc.__cause__) + + def _invalid(pages=None, **kwargs): - with pytest.raises(ResultInvalidError): + with pytest.raises(ResultInvalidError) as ei: _parse(pages, **kwargs) + _assert_rejected_on_purpose(ei.value) class TestHappyPath: @@ -195,6 +206,22 @@ class TestTableAndImage: assert elements[1].bbox == (1.0, 2.0, 3.0, 4.0) assert all(e.page_index == 0 for e in elements) + @pytest.mark.parametrize( + ("blocks", "reason"), + [ + pytest.param("not-a-list", "blocks 无效", id="blocks-not-list"), + pytest.param(["not-a-dict"], "块结构无效", id="sub-block-not-dict"), + pytest.param([{"bbox": list(_BBOX)}], "块缺少 type", id="sub-block-without-type"), + pytest.param([{"type": " ", "bbox": list(_BBOX)}], "块缺少 type", id="blank-type"), + ], + ) + def test_malformed_nested_blocks_are_rejected_with_their_own_reason(self, blocks, reason): + """外层边界会把 TypeError 等也归为 ResultInvalid,所以必须断言**原因**,否则"显式校验" + 与"碰巧在别处失败"无法区分(变异检查发现仅断言类型时删掉校验测试仍绿)。""" + block = {"type": "table", "bbox": list(_BBOX), "blocks": blocks} + with pytest.raises(ResultInvalidError, match=reason): + _parse([_page([block])]) + def test_empty_non_body_sub_block_is_not_emitted(self): caption = _body("table_caption", [_span("")]) elements, _, _ = _parse([_page([_table_block(extra_subs=[caption])])])