| 1 | """
|
|---|
| 2 | Stress tests for the Flex-O ID lifecycle.
|
|---|
| 3 | Focus: collision avoidance, version ceiling, reproducibility.
|
|---|
| 4 | """
|
|---|
| 5 | import os
|
|---|
| 6 | import sys
|
|---|
| 7 | import pytest
|
|---|
| 8 |
|
|---|
| 9 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|---|
| 10 | from flexoentity import FlexOID, EntityType, EntityState, FlexoEntity
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 | # ──────────────────────────────────────────────────────────────────────────────
|
|---|
| 14 | def test_bulk_generation_uniqueness():
|
|---|
| 15 | """Generate 10,000 IDs and assert uniqueness (statistical test)."""
|
|---|
| 16 | domain = "AF"
|
|---|
| 17 | etype = EntityType.QUESTION
|
|---|
| 18 | estate = EntityState.DRAFT
|
|---|
| 19 | seeds = [f"question {i}" for i in range(10_000)]
|
|---|
| 20 |
|
|---|
| 21 | ids = []
|
|---|
| 22 | for seed in seeds:
|
|---|
| 23 | flexo_id = FlexOID.generate(domain, etype, estate, seed)
|
|---|
| 24 | ids.append(flexo_id)
|
|---|
| 25 |
|
|---|
| 26 | assert len(ids) == len(set(ids)), "ID collisions detected in bulk generation"
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 | def test_disambiguator_trigger():
|
|---|
| 30 | """
|
|---|
| 31 | Force a deterministic collision by hashing same text twice.
|
|---|
| 32 | Should produce -A suffix for second one.
|
|---|
| 33 | """
|
|---|
| 34 | domain = "AF"
|
|---|
| 35 | etype = EntityType.QUESTION
|
|---|
| 36 | estate = EntityState.DRAFT
|
|---|
| 37 | text = "identical question text"
|
|---|
| 38 | id1 = FlexOID.generate(domain, etype, estate, text)
|
|---|
| 39 | id2 = FlexOID.generate(domain, etype, estate, text)
|
|---|
| 40 | assert id1 != id2
|
|---|
| 41 | assert id1.signature == id2.signature
|
|---|
| 42 | assert "-A" in str(id2), f"Expected '-A' suffix in disambiguated ID, got {id2}"
|
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 | def test_id_reproducibility_across_runs():
|
|---|
| 46 | """
|
|---|
| 47 | The same seed on a new process (fresh _seen_hashes)
|
|---|
| 48 | should yield the same base ID (without suffix).
|
|---|
| 49 | """
|
|---|
| 50 | domain = "AF"
|
|---|
| 51 | etype = EntityType.CATALOG
|
|---|
| 52 | estate = EntityState.DRAFT
|
|---|
| 53 | seed = "reproducibility test seed"
|
|---|
| 54 | id1 = FlexOID.generate(domain, etype, estate, seed)
|
|---|
| 55 | # Reset hash cache
|
|---|
| 56 | FlexOID._seen_hashes.clear()
|
|---|
| 57 | id2 = FlexOID.generate(domain, etype, estate, seed)
|
|---|
| 58 | assert id1 == id2
|
|---|
| 59 | assert id1.signature == id2.signature
|
|---|
| 60 |
|
|---|
| 61 |
|
|---|
| 62 | def test_version_ceiling_enforcement():
|
|---|
| 63 | """Simulate approaching @999 to trigger obsolescence guard."""
|
|---|
| 64 | entity = FlexoEntity("AF", EntityType.EXAM, "Final Exam 2025", EntityState.DRAFT)
|
|---|
| 65 | entity.approve()
|
|---|
| 66 | # artificially bump version number to near ceiling
|
|---|
| 67 |
|
|---|
| 68 | entity.flexo_id = FlexOID.from_oid_and_version(entity.flexo_id, 998)
|
|---|
| 69 |
|
|---|
| 70 | # 998 → 999 is allowed
|
|---|
| 71 | entity.sign()
|
|---|
| 72 | assert entity.flexo_id.version == 999
|
|---|
| 73 |
|
|---|
| 74 | # 999 → 1000 should raise RuntimeError
|
|---|
| 75 | with pytest.raises(RuntimeError):
|
|---|
| 76 | entity.sign()
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 | def test_massive_lifecycle_simulation():
|
|---|
| 80 | """
|
|---|
| 81 | Generate 100 random entities, simulate multiple edits and state transitions,
|
|---|
| 82 | ensure all final IDs and fingerprints are unique and valid.
|
|---|
| 83 | """
|
|---|
| 84 | import random
|
|---|
| 85 | texts = [f"random question {i}" for i in range(100)]
|
|---|
| 86 | entities = [FlexoEntity("AF", EntityType.QUESTION, t) for t in texts]
|
|---|
| 87 |
|
|---|
| 88 | for e in entities:
|
|---|
| 89 | # random edit, approval, signing
|
|---|
| 90 | e.modify_content(e.text_seed + " updated")
|
|---|
| 91 | e.approve()
|
|---|
| 92 | if random.random() > 0.3:
|
|---|
| 93 | e.sign()
|
|---|
| 94 | if random.random() > 0.6:
|
|---|
| 95 | e.publish()
|
|---|
| 96 |
|
|---|
| 97 | ids = [e.flexo_id for e in entities]
|
|---|
| 98 | fps = [e.flexo_id.signature for e in entities]
|
|---|
| 99 | assert len(ids) == len(set(ids)), "Duplicate IDs after random lifecycle"
|
|---|
| 100 | assert len(fps) == len(set(fps)), "Duplicate fingerprints after random lifecycle"
|
|---|