# Mutations the apply tests must catch. One row each: file | exact text | replacement | why
# `python3 .github/scripts/run_tests.py --mutants --only apply` applies each row to a scratch copy
# of apply/ and fails when the tests still pass. The text must occur exactly once in the file.
gwt_apply/dupes.py | if a.spouses & b.spouses: | if False: | preflight flags a namesake who shares a spouse with a tree person
gwt_apply/ops.py | if str(ev.get("type")).lower() != etype.lower(): | if str(ev.get("type")) != etype: | add_event matches an existing event type whatever its case
gwt_apply/client.py | if r.status_code >= 500 or "database is locked" in r.text[:2000].lower(): | if r.status_code >= 500: | a "database is locked" answer is retried even when its status is not 5xx
gwt_apply/ops.py | if s.get("place") and not (isinstance(s["place"], str) and ( | if False and (True and ( | validate refuses an update_event place that is a handle
gwt_apply/ops.py | and fold(str(row.get("place_type"))) == fold(op["type"]) | and True | add_place reuses a place only when its type matches too
gwt_apply/ops.py | if etype in SINGLE_VITAL and role == "Primary" and roles.get(ev["handle"]) == "Primary": | if etype in SINGLE_VITAL and role == "Primary": | a second Birth is refused only when the existing one is the person's Primary
gwt_apply/ops.py | if name == "merge_person" and set(op.get("use_loser") or []) - set(USE_LOSER): | if False: | validate refuses an unknown use_loser vital
gwt_apply/ops.py | lost += [f"answer lost on try {attempt}: {w}" for w in self.c.unknown_write] | lost += [] | a write whose answer was lost and whose re-read finds the change is done, not skip
gwt_apply/ops.py | isinstance(op["confidence"], bool) or op["confidence"] not in (0, 1, 2, 3, 4)): | False): | validate refuses an add_citation confidence that is not a whole number 0-4
gwt_apply/ops.py | diff = next((r for r in rows if not r[1]), None) | diff = None | an existing object that differs in a field the op sets is a conflict, never reused
gwt_apply/ops.py | if first[1] is not op: | if False: | validate refuses two add_citation ops on one page with another confidence or date
gwt_apply/ops.py | out.append(("confidence", theirs == op["confidence"], theirs, op["confidence"])) | out.append(("confidence", True, theirs, op["confidence"])) | a firm citation never reuses a weak one of the same page
gwt_apply/ops.py | rows.append(("place", ev.get("place") == place, shown, op["place"])) | rows.append(("place", True, shown, op["place"])) | an undated event with another place is not folded into the existing one
gwt_apply/ops.py | str(n.get("type")).lower() == op["type"].lower() | True | a note of another type with the same text is not reused
gwt_apply/ops.py | (row.get(f) or "").strip() == op[f].strip() | True | a source with the same title but another author is not reused
gwt_apply/ops.py | out.append(("date", date_key(row.get("date")) == date_key(parse_date(op["date"])), | out.append(("date", True, | a citation of the same page with another date is not reused
gwt_apply/ops.py | rows.append(("description", theirs == op["description"].strip(), | rows.append(("description", True, | an undated event with another description is not folded into the existing one
gwt_apply/dupes.py | return fold(a.first) == fold(b.first) != "" and bool(words - PARTICLES) | return fold(a.first) == fold(b.first) != "" and bool(words) | preflight does not call two couples the same on a shared "de" alone
gwt_apply/dupes.py | if not _same_couple(mine, other): | if False: | preflight flags only couples that look alike, not every pair of couples
gwt_apply/dupes.py | if any(c.handles == pair for c in couples + seen): | if any(c.handles == pair for c in seen): | preflight leaves an add_family of two tree parents who already have a family to the apply, which reuses it
gwt_apply/dupes.py | if any(c.handles == pair for c in couples + seen): | if any(c.handles == pair for c in couples): | preflight leaves a second add_family of the same parents to the apply, which reuses the first one's family
gwt_apply/dupes.py | return _person_pass(ops, tree, ids, new) + _couple_pass(ops, tree, couples, new) | return _person_pass(ops, tree, ids, new) | preflight flags a couple already in the tree
gwt_apply/ops.py | if not str(op.get("new_couple_reason") or "").strip() or not op.get("new_couple_of"): | if False: | validate refuses an allow_new_couple without its reason and compared families
gwt_apply/cli.py | if not writes: | if False: | a dry run or a snapshot leaves no log anywhere unless --log is given
gwt_apply/cli.py | outcome.setdefault("error", f"{type(exc).__name__}: {exc}"[:500]) | pass | the end line of a crashed or killed run carries the error
gwt_apply/cli.py | writes=args.apply and not args.save_snapshot) | writes=args.apply) | --save-snapshot --apply keeps no log unless --log is given
gwt_apply/cli.py | prefix="gwt-apply-revert", writes=args.apply) | prefix="gwt-apply-revert", writes=True) | a revert plan keeps no log unless --log is given
gwt_apply/client.py | self.log(method="DELETE", path=f"/{NAMESPACE[cls]}/{handle}", planned=True) | self.log(method="DELETE", path=f"/{NAMESPACE[cls]}/{handle}", status=None) | a dry-run delete is logged as planned, never status null
gwt_apply/client.py | self.log(method="POST", path=path, body=body or {}, planned=True) | self.log(method="POST", path=path, body=body or {}, status=None) | a dry-run merge is logged as planned, never status null
gwt_apply/client.py | self.log(method="PUT", path=f"/media/{handle}/file?uploadmissing=1", planned=True) | self.log(method="PUT", path=f"/media/{handle}/file?uploadmissing=1", status=None) | a dry-run upload is logged as planned, never status null
gwt_apply/cli.py | code = exc.code if isinstance(exc.code, int) else 1 | code = None | a run stopped by SIGTERM ends its log with exit 1
gwt_apply/cli.py | outcome["counts"] = {"undone" | outcome["xcounts"] = {"undone" | a revert's end line carries its counts
gwt_apply/cli.py | folder = "_" if folder in ("", ".", "..") else folder | folder = folder or "_" | a tree named .. never puts the log outside the state dir
gwt_apply/dupes.py | if stable_handle(op["key"]) in in_tree: | if False: | a rerun does not flag the family the list made, even after its parents were edited in the tree
gwt_apply/dupes.py | return ((_same_spouse(a.father, b.father) and _same_spouse(a.mother, b.mother)) | return ((_same_spouse(a.father, b.father) or _same_spouse(a.mother, b.mother)) | preflight needs both spouses to match, so a second marriage is not flagged
gwt_apply/dupes.py | or (_same_spouse(a.father, b.mother) and _same_spouse(a.mother, b.father))) | or False) | preflight flags a tree couple written the other way round
gwt_apply/dupes.py | continue  # the earlier op already says these are two couples | pass | an op named in an earlier op's new_couple_of needs no waiver of its own
gwt_apply/dupes.py | if not any(op["op"] == "add_person" or (op["op"] == "add_family" and op.get("father") and op.get("mother")) | if not any(op["op"] == "add_person" | a list of only add_family ops is still compared with the tree's couples
gwt_apply/dupes.py | return fold(a.first) == fold(b.first) != "" and bool(words - PARTICLES) | return fold(a.first) == fold(b.first) and bool(words - PARTICLES) | two spouses with no given name are not the same spouse
gwt_apply/dupes.py | continue  # a parent nobody can find: the apply reports it | pass | preflight leaves a family with a missing parent to the apply instead of crashing
gwt_apply/ops.py | for f in ("children", "attach", "namesake_of", "new_couple_of", "use_loser"): | for f in ("children", "attach", "namesake_of", "use_loser"): | validate refuses a new_couple_of that is neither a list nor a text
gwt_apply/ops.py | if handle is None and self.verify: | if False: | --verify reads an op on a Gramps ID merged away on the winner that carries the merge note
gwt_apply/ops.py | self.reasons.append(f"differs: {after['_class']} {self.label(after)} {k} {old} != {new}") | pass | --verify names the field that differs and both values
gwt_apply/ops.py | if len(rows) != 1: | if not rows: | --verify never picks a winner when two objects carry the same merge note
gwt_apply/ops.py | self.c.pending, self.c.deleted = planned | pass | a retried dry-run op does not read its own earlier try's plan as already done
gwt_apply/cli.py | rows += [{ | rows + [{ | --json lists the ops a run never reached as not run
gwt_apply/dupes.py | earlier = made.get(other.handle) if other.handle else other.ref | earlier = None if other.handle else other.ref | an earlier op's new_couple_of still waives the later op when a partial run already made the earlier family
gwt_apply/dupes.py | if other.ref in compared or (other.handle and other.handle in compared): | if other.ref in compared: | new_couple_of may name a family an earlier list made by its key:<key>
gwt_apply/ops.py | if name == "unlink_child" and "drop_link_sources" in op and not isinstance(op["drop_link_sources"], bool): | if False: | validate refuses an unlink_child drop_link_sources that is not true or false
gwt_apply/ops.py | if other and not op.get("drop_link_sources"): | if False: | unlink_child refuses to lose the other notes and citations of a link unless drop_link_sources says so
gwt_apply/ops.py | if n and text.startswith(LINK_MARKS) and | if n and text.startswith(PROBABLE) and | unlink_child removes a [wrong link] mark with the link, not only a [probable] one
gwt_apply/ops.py | and h not in rest and self._referrers | and self._referrers | unlink_child keeps a link mark that the family also holds outside the link
gwt_apply/ops.py | and self._referrers("Note", h) == {("family", fam)}: | : | unlink_child keeps a link mark that another object also holds
gwt_apply/ops.py | if entry is None and not listed and not retry: | if entry is None and not retry: | unlink_child repairs a link only the person's side has, instead of skipping it
gwt_apply/ops.py | if fam in (child.get("parent_family_list") or []): | if False: | unlink_child takes the family off the child's parent families too
gwt_apply/ops.py | retry = op["key"] in self._unlink_notes  # | retry = False  # | unlink_child whose answer was lost still deletes the link's mark on the retry
gwt_apply/ops.py | if entry is not None and not (family.get("father_handle") or family.get("mother_handle") | if False and not (family.get("father_handle") or family.get("mother_handle") | unlink_child warns when it leaves a family with nobody in it
gwt_apply/revert.py | elif m == "DELETE" and _cls_of_path(path)[0] == "Note": | elif False: | --revert makes a note the run deleted again from the pre-run copy
gwt_apply/ops.py | if held: | if False: | unlink_child keeps a link mark another writer took up between its tries
gwt_apply/ops.py | if not self.c.exists("Note", handle): | if False: | unlink_child whose mark delete lost its answer finds the mark gone and is done
gwt_apply/ops.py | self._referrers("Note", h) == {("family", fam)}: | self._referrers("Note", h) <= {("family", fam)}: | unlink_child never deletes a mark when the server names no holder for it
gwt_apply/revert.py | if client.get_live(s["cls"], s["handle"]) is None: | if True: | --revert recreating a note whose answer was lost does not post it twice
gwt_apply/revert.py | if (cls, h) in created:  # e.g. a mark | if False:  # e.g. a mark | --revert skips a note the same run made and deleted instead of asking for it by hand
gwt_apply/revert.py | elif live(cls, h) is not None: | elif False: | --revert run twice skips a deleted note that is already back
gwt_apply/cli.py | for attempt in range(1, args.retries + 1): | for attempt in range(1, 2): | a timeout in the duplicate preflight's list reads is retried, not the end of the run
gwt_apply/cli.py | if n < 1: | if False: | --retries below 1 is refused, never a run that reports ops done without running them
gwt_apply/cli.py | lock.refresh() | pass | a retried read before the first op refreshes the lock before the next try
gwt_apply/cli.py | _retried("login", client.login, client, args) | client.login() | a timeout on the run's first login is retried
gwt_apply/snapshot.py | if "format" not in data and isinstance(data.get("objects"), dict): | if False: | --snapshot reads a gwt-check dump as a snapshot
gwt_apply/snapshot.py | if not dump.get("tree"): | if False: | a gwt-check dump that records no tree is refused as a snapshot
gwt_apply/snapshot.py | if isinstance(started, (int, float)) and not isinstance(started, bool): | if False: | a gwt-check dump is dated by read_started, as gwt-check dates it
gwt_apply/snapshot.py | return isinstance(rows, list) and all(isinstance(o, dict) and isinstance(o.get("handle"), str) for o in rows) | return True | a gwt-check dump with rows that are not objects with handles is refused, not a traceback
gwt_apply/snapshot.py | except (OverflowError, OSError, ValueError):  # out of range or NaN | except ValueError:  # out of range or NaN | a gwt-check dump with a read_started out of range is dated by dumped_at, not a traceback
gwt_apply/ops.py | if h == place["handle"]: | if False: | update_place refuses a parent inside the place, a loop
gwt_apply/ops.py | if [r.get("ref") for r in place.get("placeref_list") or []] != [parent]: | if True: | a rerun of an update_place move leaves a dated PlaceRef to the same parent as it is
gwt_apply/ops.py | if s.get("enclosed_by") == op.get("place"): | if False: | validate refuses a place that encloses itself
gwt_apply/ops.py | if current.get("value") != name: | if True: | a rerun of an update_place rename is a skip
gwt_apply/ops.py | and fold(str(row.get("place_type"))) == fold(str(place.get("place_type"))) | and True | update_place may rename a place to a name another type of place under the same parent has
gwt_apply/ops.py | if "name" in s and not (isinstance(s["name"], str) and s["name"].strip()): | if False: | validate refuses an update_place name that is empty or not a string
gwt_apply/ops.py | if handle in (target.get("tag_list") or []): | if False: | a tag already on the object is skipped, not added twice
gwt_apply/ops.py | target["tag_list"] = [*(target.get("tag_list") or []), handle] | target["tag_list"] = [handle] | add_tag keeps the tags the object already has
gwt_apply/ops.py | if cls in ("ChildRef", "PersonRef"): | if False: | validate refuses an add_tag on a child_ref or an association, which have no tags
gwt_apply/ops.py | if not isinstance(spec["note"], str) or not spec["note"]: | if False: | validate refuses an add_tag note target that is null
gwt_apply/ops.py | keep = [h for h in tags if h not in handles] | keep = [] | remove_tag keeps the other tags of the object
gwt_apply/ops.py | if len(keep) == len(tags): | if False: | a remove_tag whose tag is already off is a skip, so a rerun writes nothing
gwt_apply/ops.py | if not handles: | if False: | remove_tag with a name the tree has no tag for is a conflict, not a skip
gwt_apply/ops.py | new = next((n for n, r in enumerate(refs) if r.get("ref") == h), -1) if h else -1 | new = i | move_event recomputes birth_ref_index and death_ref_index when the list shifts
gwt_apply/ops.py | self._reindex(src, old) | pass | move_event recomputes the old owner's birth and death indexes
gwt_apply/ops.py | if dst.get(index_key, -1) == -1:  # appended last | if False:  # | a moved Primary birth or death becomes the new owner's birth or death when it had none
gwt_apply/ops.py | if [{k: v for k, v in r.items() if k != "_class"} for r in theirs] != [ref]: | if False: | move_event onto an owner that lists the event with another reference is a conflict
gwt_apply/ops.py | if len(mine) > 1: | if False: | move_event refuses an event the old owner lists twice
gwt_apply/ops.py | if (scls, src["handle"]) == (dcls, dst["handle"]): | if False: | move_event from an owner to itself under another name is a conflict, never an orphaned event
gwt_apply/ops.py | if name == "move_event" and _event_owner(op.get("from")) == _event_owner(op.get("to")): | if False: | validate refuses a move_event whose from and to are the same
gwt_apply/ops.py | if cls not in ("Person", "Family"): | if False: | validate refuses a move_event owner that is not a person or a family
gwt_apply/ops.py | refs += [op.get("event"), *_target_refs(op.get("from")), *_target_refs(op.get("to"))] | refs += [*_target_refs(op.get("from")), *_target_refs(op.get("to"))] | validate refuses a move_event event that is a handle
gwt_apply/ops.py | self.actions.append(f"warning: {what} is now on both | self.actions.append(f"note: {what} is now on both | a move_event stopped between its two writes says the event is on both owners, on every try
gwt_apply/ops.py | ref["role"] = {"Primary": "Family", "Family": "Primary"}.get(ref.get("role"), ref.get("role")) | pass | move_event between a person and a family swaps Primary and Family
gwt_apply/ops.py | ref["role"] = op["role"] | pass | move_event takes the role the op gives
gwt_apply/ops.py | if str(ev.get("type")) == etype:  # the same rule as add_event | if False:  # | move_event refuses a second Primary birth or death on the new person
gwt_apply/ops.py | new = next((n for n, rh in prim if types.get(rh) == etype), -1) | new = -1 | when the indexed birth or death leaves, the index moves to the other Primary one left
gwt_apply/ops.py | self.c.get("Tag", h)  # read, so a snapshot apply | pass  # | remove_tag reads the tag it takes off, so a snapshot apply sees a rename
gwt_apply/ops.py | self._reread_found = True  # the new owner's write | pass  # | a move_event whose first answer was lost says the re-read found the write
gwt_apply/ops.py | if dcls == "Person" and index_key and ref.get("role") == "Primary": | if dcls == "Person" and index_key: | a birth only witnessed never blocks a move_event onto a person who has a birth
gwt_apply/ops.py | prim = [r["ref"] for r in old if r.get("role") == "Primary" and r.get("ref")] | prim = [r["ref"] for r in old if r.get("ref")] | a birth the new person only witnessed does not block a moved Primary birth
gwt_apply/ops.py | prim = [(n, r["ref"]) for n, r in enumerate(refs) if r.get("role") == "Primary" and r.get("ref")] | prim = [(n, r["ref"]) for n, r in enumerate(refs) if r.get("ref")] | when the indexed birth leaves, a birth the person only witnessed never becomes their birth
gwt_apply/ops.py | self._reread_found = False  # every try | pass  # every try | the re-read flag of one op never labels the next op's lost answer
gwt_apply/ops.py | if now != op["expect"].strip(): | if False: | update_source never overwrites a title someone changed since it was read
gwt_apply/ops.py | if (row["handle"] != source["handle"] and (row.get("title") or "").strip() == new | if (False | update_source refuses a title another source with the same author already has
gwt_apply/ops.py | res.noop = res.status == "skip" and self._noop | res.noop = res.status == "skip" | a skip of what this list made by key is never called a no-op
gwt_apply/ops.py | self._noop = True | pass | an update or tag that found the tree as it asks is a no-op skip
