Skip to content

Commit 28b3138

Browse files
committed
test: cover the registry edge cases moved in ROADMAP.md #73 step 2
1 parent 8fb7222 commit 28b3138

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

‎tests/test_revision_groups.py‎

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,138 @@ def test_presession_rewrite_does_not_claim_split_tail(self, temp_docx):
822822
assert "BETA" in text and "beta" not in text
823823
assert "alpha" in text and "gamma" in text
824824

825+
def test_middle_delete_of_dateless_own_insertion_leaves_tail_ungrouped(self, temp_xml):
826+
# The origin insertion is ours but carries no w:date, so
827+
# reconstruction left it ungrouped. Its split-off tail must then be
828+
# registered as *explicitly* ungrouped (None) rather than left
829+
# unregistered — otherwise the delete's own capture would claim it,
830+
# and rejecting the delete's group would rip a leftover piece out of
831+
# a pre-existing insertion.
832+
body = (
833+
"<w:p><w:r><w:t>Before </w:t></w:r>"
834+
f'<w:ins w:id="1" w:author="{AUTHOR_B}"><w:r><w:t>AAA MID BBB</w:t></w:r></w:ins>'
835+
"<w:r><w:t> after</w:t></w:r></w:p>"
836+
)
837+
manager = _make_manager(temp_xml(body))
838+
assert manager._groups == {}
839+
assert manager.group_id_of(1) is None
840+
841+
manager.suggest_deletion("MID ")
842+
843+
# No new group: the delete created no revisions of its own, and the
844+
# tail it split off did not become a phantom group either.
845+
assert manager._groups == {}
846+
revisions = manager.list_revisions()
847+
assert sorted(rev.id for rev in revisions) == [1, 2] # truncated head + split-off tail
848+
assert all(rev.group_id is None for rev in revisions)
849+
assert manager._revision_groups == {2: None}
850+
851+
852+
class TestGroupedCaptureFilter:
853+
"""``_grouped`` keeps only our own, attached, numerically-identified, unseen revisions."""
854+
855+
def test_capture_skips_foreign_detached_nonconforming_and_repeated_elements(self, temp_xml):
856+
# The injector only ever reports elements whose numeric w:id it just
857+
# assigned, so the collector is fed directly here: this pins the
858+
# filter's contract — (i) not ours, (ii) no longer in the DOM,
859+
# (iii) already registered, a non-numeric id, or the same id reported
860+
# twice — as the last line of defence, independent of the injector.
861+
manager = _make_manager(temp_xml("<w:p><w:r><w:t>text</w:t></w:r></w:p>"))
862+
dom = manager.editor.dom
863+
paragraph = dom.getElementsByTagName("w:p")[0]
864+
865+
def make_ins(rev_id, author: str = AUTHOR_B, attached: bool = True):
866+
elem = dom.createElement("w:ins")
867+
elem.setAttribute("w:id", str(rev_id))
868+
elem.setAttribute("w:author", author)
869+
elem.setAttribute("w:date", DATE_A)
870+
if attached:
871+
paragraph.appendChild(elem)
872+
return elem
873+
874+
with manager._grouped() as capture:
875+
collected = manager.editor._tracked_change_collector
876+
assert collected is not None
877+
collected.append(make_ins(10, author=AUTHOR_A)) # a split half of a foreign insertion
878+
collected.append(make_ins(11, attached=False)) # create-then-remove churn
879+
collected.append(make_ins("oops")) # nonconforming producer's id
880+
collected.append(make_ins(12))
881+
collected.append(make_ins(12)) # same id reported twice
882+
collected.append(make_ins(13))
883+
884+
assert capture.group_id == 1
885+
assert manager.group_revisions(1) == (12, 13)
886+
assert manager.group_id_of(10) is None
887+
assert manager.group_id_of(11) is None
888+
assert manager._revision_groups == {12: 1, 13: 1}
889+
890+
def test_capture_with_nothing_kept_allocates_no_group(self, temp_xml):
891+
manager = _make_manager(temp_xml("<w:p><w:r><w:t>text</w:t></w:r></w:p>"))
892+
paragraph = manager.editor.dom.getElementsByTagName("w:p")[0]
893+
894+
with manager._grouped() as capture:
895+
foreign = manager.editor.dom.createElement("w:ins")
896+
foreign.setAttribute("w:id", "10")
897+
foreign.setAttribute("w:author", AUTHOR_A)
898+
foreign.setAttribute("w:date", DATE_A)
899+
paragraph.appendChild(foreign)
900+
collected = manager.editor._tracked_change_collector
901+
assert collected is not None
902+
collected.append(foreign)
903+
904+
assert capture.group_id is None
905+
assert manager._groups == {}
906+
assert manager._group_counter == 1
907+
908+
909+
class TestGroupLiveness:
910+
"""``groups_are_dead`` and ``group_spans`` answer from the document, not the registry."""
911+
912+
def test_empty_and_unknown_queries_answer_without_a_walk(self, temp_xml, monkeypatch):
913+
manager = _make_manager(temp_xml(f"<w:p>{_ins_xml(1, 'one')}</w:p>"))
914+
# Neither method may build the w:id index or walk the DOM when there
915+
# is nothing to look up: the index costs a full-document pass.
916+
monkeypatch.setattr(manager, "_revision_element_index", lambda: pytest.fail("built the w:id index"))
917+
monkeypatch.setattr(
918+
"docx_editor.track_changes.registry._revision_elements", lambda _root: pytest.fail("walked the document")
919+
)
920+
921+
assert manager.groups_are_dead([]) == set()
922+
assert manager.groups_are_dead(iter(())) == set()
923+
assert manager.group_spans([]) == {}
924+
assert manager.group_spans([99]) == {} # unknown group: no members to look for
925+
926+
def test_unknown_live_and_resolved_groups(self, temp_xml):
927+
body = f"<w:p>{_ins_xml(1, 'one ')}</w:p><w:p>{_ins_xml(2, 'two', date=DATE_B)}</w:p>"
928+
manager = _make_manager(temp_xml(body))
929+
assert manager._groups == {1: (1,), 2: (2,)}
930+
931+
# An id this manager never registered reads as dead; live groups do not.
932+
assert manager.groups_are_dead([1, 2, 99]) == {99}
933+
spans = manager.group_spans([1, 2, 99])
934+
assert set(spans) == {1, 2}
935+
first, last = spans[1]
936+
assert first is last and first.getAttribute("w:id") == "1"
937+
# Revisions of groups not asked about are skipped, not reported.
938+
assert set(manager.group_spans([2])) == {2}
939+
940+
manager.accept_group(1)
941+
942+
assert manager.groups_are_dead([1, 2]) == {1}
943+
assert set(manager.group_spans([1, 2])) == {2}
944+
945+
def test_paragraph_mark_only_group_is_live_but_has_no_span(self, temp_xml):
946+
# A pure tracked split: the group's only member is a paragraph-mark
947+
# insertion, which cannot hold a comment marker. The group is alive
948+
# (something is left to resolve) yet offers no span to anchor on.
949+
body = f"<w:p><w:pPr><w:rPr>{_mark_ins_xml(7)}</w:rPr></w:pPr><w:r><w:t>plain</w:t></w:r></w:p>"
950+
manager = _make_manager(temp_xml(body))
951+
gid = manager.group_id_of(7)
952+
assert gid is not None and manager.group_revisions(gid) == (7,)
953+
954+
assert manager.groups_are_dead([gid]) == set()
955+
assert manager.group_spans([gid]) == {}
956+
825957

826958
class TestGroupReconstruction:
827959
"""Parse-time inference of groups for revisions already in the file (#46).

0 commit comments

Comments
 (0)