Skip to content

Commit 24f5a1d

Browse files
authored
No enchantment row lists a tag twice; a test refuses a repeated tag in any data table (#1823)
No enchantment row lists a tag twice, and a test now refuses a repeated tag in any data table (#1823). **The fault.** Five rows on the workbook's Enchantments sheet repeated a tag in their Tags cell: | Cell | Row | Before | |---|---|---| | I5 | Negative_Ultimate_cooldowns_increased_by_100_500 | "Slot.Ultimate, Slot.Ultimate" | | I167 | Negative_Movement_abilities_have_50_increased_cooldown | "Slot.Movement, Slot.Movement" | | I182 | Negative_After_you_use_a_movement_ability_you_lose_50_mo | "Slot.Movement, Slot.Movement, Stat.Utility.Movespeed" | | I193 | Negative_Your_own_ultimate_ability_is_disabled | "Slot.Ultimate, Slot.Ultimate" | | D378 | Positive_Warlord_s_Will_10_Piece_Bonus_Every_30_second | "Stat.Utility.Movespeed, Slot.Movement, Slot.Movement, Keyword.Consume" | **Nothing changes in play.** The issue said the second copy is dropped on import. It is not: `FCataclysmEnchantmentRow::Tags` is a plain FString. Its one game reader, `UCataclysmDropRoll::EnchantmentSuitsSlot`, looks only for `Item.Slot.` tags and skips these, and a repeat would give the same answer anyway. Scoping uses the effect rows' RequiredTags, which have no repeats. **The change:** - `docs/All_Things_Cataclysm.xlsx`, five cells under the coordinator's grant: the second copy removed, order kept. That regenerates five lines, the Tags field only: `EnchantmentsNegative.csv` 5, 167, 182, 193 and `EnchantmentsPositive.csv` 377. `DT_EnchantmentsNegative` and `DT_EnchantmentsPositive` were rebuilt. - New `tools/tests/test_no_tag_column_repeats_a_tag.py` (coordinator ruling: a test, not a refusal in the generator). It scans every column whose name contains "tag" in every CSV under `game/Data`, and names the table, column, row and repeated tag. A scope test asserts the six columns it must find, so a rename cannot make it read nothing. Eight columns in eight tables are scanned today. - No C++ change. **Proof:** `prove_guard` in a git-archive copy, with Negative line 5's repeat put back: `PROVED: 1 failed, 2 passed | restored: 3 passed`; the named failure is `test_no_row_lists_a_tag_twice`. On the tree before the edit, the test failed naming exactly the five rows. **Evidence (window enchant-t183w, 2026-10-28, at abcd2e5 on development db73850)** | Step | Registered | Printed | |---|---|---| | Free memory just before the lock | at least 10,240 MB | 11,233 MB | | Asset rebuild | exactly the two enchantment assets and the sources record | exactly those three; committed 36908a5 | | Whole Unreal suite | 4343 / 0 (no new Unreal test) | `4343 tests performed, 4343 succeeded, 0 failed`; declared 4343, gap 0 | | Memory | below 9,500 MB | `final_used_virtual_mb=7772`, peak 8,107 MB | | Python (JUnit) | 7023 (7020 + 3 new), 0 failures | 7023 / 0 / 0 / 8; ruff `All checks passed!` | After the tested commit only the asset commit and `docs/DECISIONS.md` changed (`git diff --name-only abcd2e5 fd9ffaa`). Closes #1823 (closed by hand after the merge). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent db73850 commit 24f5a1d

8 files changed

Lines changed: 208 additions & 11 deletions

‎docs/All_Things_Cataclysm.xlsx‎

-13 Bytes
Binary file not shown.

‎docs/DECISIONS.md‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,105 @@
22

33
Decisions made outside the Google Drive documents, newest first.
44

5+
## 2026-10-28 — No enchantment lists a tag twice, and a test refuses a repeated tag in any generated table (#1823)
6+
7+
**Affects:** five cells of the "Enchantments" sheet of `docs/All_Things_Cataclysm.xlsx` (I5, I167, I182, I193, D378) and
8+
the lines they generate in `game/Data/EnchantmentsNegative.csv` (5, 167, 182, 193) and
9+
`game/Data/EnchantmentsPositive.csv` (377); a new test, `tools/tests/test_no_tag_column_repeats_a_tag.py`; this file. No
10+
C++. The two enchantment DataTable assets are rebuilt in the window, because their Tags text changes. **Closes
11+
[#1823](https://github.com/sdubois777/Cataclysm/issues/1823).**
12+
13+
### SAID FIRST
14+
15+
- **NOTHING CHANGES IN PLAY.** An enchantment's `Tags` is a plain `FString` (`FCataclysmEnchantmentRow`,
16+
`game/Source/Cataclysm/Data/CataclysmDataRows.h`), not a tag container. The one reader in game code,
17+
`UCataclysmDropRoll::EnchantmentSuitsSlot` (`CataclysmDropRoll.cpp`), splits it and looks only for tags beginning
18+
`Item.Slot.`; these five repeat `Slot.Ultimate` or `Slot.Movement`, and a repeat gives the same answer either way. No
19+
tooltip or item code reads it, and scoping reads the effect rows' `RequiredTags`, which repeat nothing. The issue said
20+
the second copy "is dropped on import"; that is not the mechanism, though its conclusion holds.
21+
- **THE REPEATS WERE IN THE WORKBOOK**, typed that way, not added by the generator.
22+
23+
### THE RULING, VERBATIM (the coordinating session, 2026-10-28)
24+
25+
> Coordinator, #1823:
26+
> (c) The check is a test, as you recommend. It runs over every tag column in every generated game/Data CSV, found by
27+
> column name, and names each row and its repeated tag. The generator stays unchanged: a test catches this before merge,
28+
> and it covers new tables without being edited. Prove it with prove_guard in a copy by re-adding one duplicate.
29+
> GRANT on fix/no-tag-twice-in-an-enchantment from e0351fdcd, in docs/All_Things_Cataclysm.xlsx, exactly five cells on
30+
> the "Enchantments" sheet: I5, I167, I182, I193 and D378. Remove only the second copy of the repeated tag and keep the
31+
> order, to the values in your trial. No other cell changes. The grant runs from the edit until the branch merges.
32+
33+
### THE FIVE CELLS (commit `f0ebf746f`)
34+
35+
| Cell | Row | Before | After |
36+
|---|---|---|---|
37+
| I5 | `Negative_Ultimate_cooldowns_increased_by_100_500` | `Slot.Ultimate, Slot.Ultimate` | `Slot.Ultimate` |
38+
| I167 | `Negative_Movement_abilities_have_50_increased_cooldown` | `Slot.Movement, Slot.Movement` | `Slot.Movement` |
39+
| I182 | `Negative_After_you_use_a_movement_ability_you_lose_50_mo` | `Slot.Movement, Slot.Movement, Stat.Utility.Movespeed` | `Slot.Movement, Stat.Utility.Movespeed` |
40+
| I193 | `Negative_Your_own_ultimate_ability_is_disabled` | `Slot.Ultimate, Slot.Ultimate` | `Slot.Ultimate` |
41+
| D378 | `Positive_Warlord_s_Will_10_Piece_Bonus_Every_30_second` | `Stat.Utility.Movespeed, Slot.Movement, Slot.Movement, Keyword.Consume` | `Stat.Utility.Movespeed, Slot.Movement, Keyword.Consume` |
42+
43+
One script run asserted each old value and every sheet's shape before writing. The generator then changed exactly
44+
these five lines (Negative 5, 167, 182, 193; Positive 377), each only in its Tags field, with every table's line count
45+
unchanged, and `--check` passed. A trial in a git-archive copy at `9293573bf` and again at `e0351fdcd` gave the same
46+
five lines. Measured across every tag column in `game/Data` at `e0351fdcd`, no other row repeated a tag
47+
(EnchantmentEffects RequiredTags 707 rows, PassiveEffects RequiredTags 325, WeaponSkills Tags 403, MinionTypes Tags 9,
48+
MinionScaling RequiresTag 2, ElementVisuals ElementTag 8: none).
49+
50+
### THE TEST
51+
52+
`tools/tests/test_no_tag_column_repeats_a_tag.py`, three tests:
53+
54+
- `test_the_scan_reads_every_tag_column_it_must`: the scope, read first. Six columns the scan must find (the two
55+
enchantment Tags, EnchantmentEffects and PassiveEffects RequiredTags, WeaponSkills and MinionTypes Tags), so a renamed
56+
column cannot leave the check reading nothing.
57+
- `test_no_row_lists_a_tag_twice`: every column whose name contains "tag", in every CSV under `game/Data`; a failure
58+
names the table, column, row and repeated tag.
59+
- `test_a_repeat_is_found_and_a_single_tag_is_not`: the helper on made-up cells, including spacing.
60+
61+
Run on the tree before the edit (`e0351fdcd`), the second test failed naming exactly the five rows above.
62+
63+
### THE PROOF
64+
65+
`tools/prove_guard.py` `break_and_run`, in a git-archive copy outside the repository: the line
66+
`Negative_Ultimate_cooldowns_increased_by_100_500,...,2.0,Slot.Ultimate,True,` of `EnchantmentsNegative.csv` (counted
67+
once) given back its repeat, `"Slot.Ultimate, Slot.Ultimate"`. Printed: "PROVED: 1 failed, 2 passed in 0.15s |
68+
restored: 3 passed in 0.06s"; named_failures
69+
`tools/tests/test_no_tag_column_repeats_a_tag.py::test_no_row_lists_a_tag_twice`.
70+
71+
### JUDGEMENTS OF MY OWN, WITH THE OPTION NOT TAKEN
72+
73+
- **The scan reads the generated CSVs, not the workbook**, because the CSVs are what the game reads and every tag
74+
column reaches one. Not taken: reading the sheets, whose column layout differs per sheet.
75+
- **A column is a tag column when its name contains "tag"**, which today finds eight columns in eight tables. Not
76+
taken: a fixed list, which a new table would escape.
77+
78+
### NO GENRE SOURCE
79+
80+
None. This is authoring hygiene, not a mechanic.
81+
82+
### FOR THE OWNER'S PLAY-CHECK LIST
83+
84+
- Nothing to see: no enchantment behaves differently.
85+
86+
### THE WINDOW'S RUN, t183w
87+
88+
Window t183w, run 2026-10-28 under the lock as `enchant-t183w`, at `abcd2e564` on development `db73850a7` (moved from
89+
`285fae4ce` on `e0351fdcd` as `fix/no-tag-twice-in-an-enchantment-1`; the workbook blob, the zero-context patch ids of
90+
the CSVs and the test file, and the hash of this file's changed lines matched for all three commits). Free memory
91+
immediately before the lock, at 13:39:39, was 11,233 MB, against the floor of 10 GiB (10,240 MB). No C++ changed, so
92+
no new Unreal test and no C++ proof. Every step exited 0:
93+
94+
| Step | Result |
95+
|---|---|
96+
| The build | "35 actions, 32 files compiled" (development's own C++ from #2540, not yet built in this worktree) |
97+
| The DataTable assets | exactly `DT_EnchantmentsNegative.uasset`, `DT_EnchantmentsPositive.uasset` and `datatable_asset_sources.json` changed; committed as `36908a535` |
98+
| The whole Unreal suite | "4343 tests performed, 4343 succeeded, 0 failed"; 4343 declared, gap 0 |
99+
| Memory | `tests=4950 peak_used_virtual_mb=8107 final_used_virtual_mb=7772` |
100+
| Python of record | "7015 passed, 8 skipped"; JUnit 7023 tests, 0 failures, 0 errors, 8 skipped (the asset-hash test passes after the asset commit); ruff "All checks passed!" |
101+
102+
---
103+
5104
## 2026-10-28 — A thrall explodes under Every One Bursts: 3 m, three of its own blows, raised by Volatile (#2540)
6105

7106
**Affects:** `CataclysmMinion.h` and `.cpp` (`ThrallExplosionDamage`, `ThrallBurst` and two constants),
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:2aedabf46927c32b31dbd313feae20e18342ea7ff28930e571812fb1fd0915dc
3-
size 66902
2+
oid sha256:6f17a053a81f1f9ca6956c4be30c22691be0ac1a98b58d9115f47e007511180a
3+
size 66842
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:b37b370e06bdb5e4c347ae816fb2eb8974dfd7b8f99eed65696b47d6be8fc130
3-
size 140795
2+
oid sha256:685b641f6cf0e6152f793e03e6dc7b6d95f7b3a29f13a39a046dc8c3ecf1e204
3+
size 140780

‎game/Data/EnchantmentsNegative.csv‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Name,Effect,EnchantmentType,Weight,Tags,IsNegative,RollsDown
22
Negative_Can_t_use_a_basic_attack,Can’t use a basic attack,Generic,0.0,Scope.BasicOnly,True,
33
Negative_80_99_of_your_health_is_reserved,80%-99% of your health is reserved,Generic,1.0,Stat.Defense.Life,True,
44
Negative_You_lose_1_4_max_resistances_for_every_100_000,"You lose 1-4% max resistances for every 100,000 - 500,000 kills",Generic,3.0,Stat.Defense.Resist,True,2
5-
Negative_Ultimate_cooldowns_increased_by_100_500,Ultimate cooldowns increased by 100%-500%,Generic,2.0,"Slot.Ultimate, Slot.Ultimate",True,
5+
Negative_Ultimate_cooldowns_increased_by_100_500,Ultimate cooldowns increased by 100%-500%,Generic,2.0,Slot.Ultimate,True,
66
Negative_You_have_no_armor,You have no armor,Generic,1.0,Stat.Defense.Armor,True,
77
Negative_DoTs_deal_double_damage_to_you,DoTs deal double damage to you,Generic,2.0,Keyword.DoT.Generic,True,
88
Negative_You_lose_15_of_your_max_hp_every_5_seconds,You lose 15% of your max hp every 5 seconds,Generic,1.0,Stat.Defense.Life,True,
@@ -164,7 +164,7 @@ Negative_Can_t_inflict_bleeding,Can't inflict bleeding,Generic,4.0,Keyword.DoT.B
164164
Negative_You_have_25_50_less_crit_chance,You have 25%-50% less crit chance,Generic,3.0,Stat.Offense.Crit,True,
165165
Negative_Energy_shield_can_now_be_effected_by_bleed,Energy shield can now be effected by bleed,Generic,4.0,"Stat.Defense.EnergyShield, Keyword.DoT.Bleed",True,
166166
Negative_Cannot_evade_melee_attacks,Cannot evade melee attacks,Generic,4.0,Stat.Defense.Evasion,True,
167-
Negative_Movement_abilities_have_50_increased_cooldown,Movement abilities have 50% increased cooldown,Generic,4.0,"Slot.Movement, Slot.Movement",True,
167+
Negative_Movement_abilities_have_50_increased_cooldown,Movement abilities have 50% increased cooldown,Generic,4.0,Slot.Movement,True,
168168
Negative_Movement_abilities_cost_20_50_of_your_maximum,Movement abilities cost 20%-50% of your maximum mana,Generic,4.0,"Slot.Movement, Stat.Resource.Mana",True,
169169
Negative_Cooldowns_are_increased_by_30_50,Cooldowns are increased by 30%-50%,Generic,3.0,Scope.Global,True,
170170
Negative_You_take_10_20_of_the_damage_you_reflect,You take 10%-20% of the damage you reflect,Generic,4.0,,True,
@@ -179,7 +179,7 @@ Negative_Your_cooldown_abilities_have_a_25_chance_to_dea,Your cooldown abilities
179179
Negative_Your_cooldowns_are_increased_by_100_200,Your cooldowns are increased by 100%-200%,Generic,2.0,,True,
180180
Negative_Lose_5_15_attack_speed,Lose 5%-15% attack speed,Generic,4.0,Stat.Offense.Speed,True,
181181
Negative_You_have_10_20_less_chance_to_evade,You have 10%-20% less chance to evade,Generic,4.0,Stat.Defense.Evasion,True,
182-
Negative_After_you_use_a_movement_ability_you_lose_50_mo,After you use a movement ability you lose 50% movespeed for 3 seconds,Generic,4.0,"Slot.Movement, Slot.Movement, Stat.Utility.Movespeed",True,
182+
Negative_After_you_use_a_movement_ability_you_lose_50_mo,After you use a movement ability you lose 50% movespeed for 3 seconds,Generic,4.0,"Slot.Movement, Stat.Utility.Movespeed",True,
183183
Negative_Ranged_skills_have_20_35_reduced_projectile_sp,Ranged skills have 20%-35% reduced projectile speed,Generic,4.0,"Type.Ranged, Type.Projectile",True,
184184
Negative_Your_movement_speed_is_reduced_by_10,Your movement speed is reduced by 10%,Set,5.0,Stat.Utility.Movespeed,True,
185185
Negative_Minion_maximum_health_is_reduced_by_50,Minion maximum health is reduced by 50%,Set,6.0,Stat.Defense.Life,True,
@@ -190,7 +190,7 @@ Negative_Melee_damage_is_reduced_by_50,Melee damage is reduced by 50%,Set,10.0,T
190190
Negative_Ranged_attacks_deal_20_more_damage_to_you,Ranged attacks deal 20% more damage to you,Set,11.0,Type.Projectile,True,
191191
Negative_Your_direct_damage_is_reduced_by_25,Your direct damage is reduced by 25%,Set,12.0,,True,
192192
Negative_You_have_no_health_mana_es_regen,You have no health/mana/es regen,Set,13.0,"Stat.Recovery.Regen, Stat.Resource.Mana, Stat.Defense.EnergyShield",True,
193-
Negative_Your_own_ultimate_ability_is_disabled,Your own ultimate ability is disabled,Set,14.0,"Slot.Ultimate, Slot.Ultimate",True,
193+
Negative_Your_own_ultimate_ability_is_disabled,Your own ultimate ability is disabled,Set,14.0,Slot.Ultimate,True,
194194
Negative_Your_health_is_reduced_by_10,Your health is reduced by 10%,Set,16.0,Stat.Defense.Life,True,
195195
Negative_Your_movement_speed_is_reduced_by_10_1,Your movement speed is reduced by 10%,Set,17.0,Stat.Utility.Movespeed,True,
196196
Negative_All_of_your_life_regeneration_effects_are_reduce,All of your life regeneration effects are reduced by 50%,Set,18.0,,True,

‎game/Data/EnchantmentsPositive.csv‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ Positive_Divine_Retribution_6_Piece_Bonus_When_your_en,"Divine Retribution (6-Pi
374374
Positive_Divine_Retribution_10_Piece_Bonus_Every_5_sec,"Divine Retribution (10-Piece Bonus): Every 5 seconds, you gain a stack of 'Sanctified Power.' When you block an attack, you consume all stacks of 'Sanctified Power' and all of your skills are instantly refreshed. The bonus is permanent for the rest of the dungeon",Set,16.0,"Stat.Defense.Block, Keyword.Consume, Trigger.OnBlock",False,
375375
Positive_Warlord_s_Will_2_Piece_Bonus_Your_armor_is_in,Warlord's Will (2-Piece Bonus): Your armor is increased by 25%,Set,17.0,Stat.Defense.Armor,False,
376376
Positive_Warlord_s_Will_6_Piece_Bonus_When_you_take_da,"Warlord's Will (6-Piece Bonus): When you take damage, you have a 10% chance to summon a ""Warhound"" to fight for you for a short duration. The Warhound taunts enemies and deals damage based on your armor",Set,17.0,"Type.Minion, Stat.Defense.Armor",False,
377-
Positive_Warlord_s_Will_10_Piece_Bonus_Every_30_second,"Warlord's Will (10-Piece Bonus): Every 30 seconds, you gain a stack of 'Tactical Advantage.' When you use a movement ability, you consume all stacks of 'Tactical Advantage' and gain 100% more movement speed and 50% more damage for 5 seconds. This bonus scales with the number of enemies in the area: 10% more damage for each enemy within 8 meters, up to 5.",Set,17.0,"Stat.Utility.Movespeed, Slot.Movement, Slot.Movement, Keyword.Consume",False,
377+
Positive_Warlord_s_Will_10_Piece_Bonus_Every_30_second,"Warlord's Will (10-Piece Bonus): Every 30 seconds, you gain a stack of 'Tactical Advantage.' When you use a movement ability, you consume all stacks of 'Tactical Advantage' and gain 100% more movement speed and 50% more damage for 5 seconds. This bonus scales with the number of enemies in the area: 10% more damage for each enemy within 8 meters, up to 5.",Set,17.0,"Stat.Utility.Movespeed, Slot.Movement, Keyword.Consume",False,
378378
Positive_Reaper_s_Embrace_2_Piece_Bonus_You_gain_10_m,Reaper's Embrace (2-Piece Bonus): You gain 10% more life from all sources,Set,18.0,Stat.Defense.Life,False,
379379
Positive_Reaper_s_Embrace_6_Piece_Bonus_When_a_minion,"Reaper's Embrace (6-Piece Bonus): When a minion or pet dies, it leaves behind a ""Soul"" that can be picked up. Picking up a Soul grants a stack of Soul Strike that increases your damage by 10% for 5 seconds. Max 10 stacks.",Set,18.0,Type.Minion,False,
380380
Positive_Reaper_s_Embrace_10_Piece_Bonus_For_each_stac,"Reaper's Embrace (10-Piece Bonus): For each stack of Soul Strike you gain 5% cull on hit. Killing an enemy instanly spawns one of your minions, up to your max",Set,18.0,Type.Minion,False,

‎game/Data/datatable_asset_sources.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@
5959
"asset": "DT_EnchantmentsNegative",
6060
"csv": "EnchantmentsNegative.csv",
6161
"rows": 196,
62-
"csv_sha256": "0125478821c0ffe6a7cf4e4c70e6eccbb542849322c7d312cdbdb3e9217c10f4"
62+
"csv_sha256": "d498793734a213fc2197b00a2dc9d07f2032f25044e2d689e1d6886fbe6a8ea7"
6363
},
6464
{
6565
"asset": "DT_EnchantmentsPositive",
6666
"csv": "EnchantmentsPositive.csv",
6767
"rows": 379,
68-
"csv_sha256": "72388c6166670e1aaeb72d43e2bb3d5b9b54042a1235bcbb92220941e9e4f7e0"
68+
"csv_sha256": "7f5dc0ae130ff410db4db6d0232c7892f5993934307efde06ba3f700bf8c987d"
6969
},
7070
{
7171
"asset": "DT_EnemyArchetypes",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""No row of any generated table lists the same tag twice in a tag column.
2+
3+
WHY THIS EXISTS. Issue #1823. Five enchantment rows carried a repeated tag, all of
4+
them typed into the design workbook that way: "Slot.Ultimate, Slot.Ultimate" and
5+
"Slot.Movement, Slot.Movement". It changed nothing in play -- an enchantment's
6+
`Tags` is a plain string, and the one reader in the game,
7+
`UCataclysmDropRoll::EnchantmentSuitsSlot`, gives the same answer for a tag listed
8+
once or twice -- but it meant the column could not be read as a count of anything,
9+
and nothing reported it. The five cells were corrected in the same change as this
10+
test.
11+
12+
WHAT IS CHECKED. Every CSV under `game/Data`, which the generator writes from the
13+
workbook and the game reads, and in each every column whose name contains "tag".
14+
The columns are FOUND BY NAME rather than listed, so a new table with a tag column
15+
is covered without this file being edited. Ruled by the coordinating session on
16+
2026-10-28: a test, and not a refusal in `tools/generate_datatables.py`, because
17+
this catches it before merge and covers new tables on its own.
18+
19+
A failure names each table, column, row and repeated tag.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import collections
25+
import csv
26+
import pathlib
27+
28+
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
29+
DATA = REPO_ROOT / "game" / "Data"
30+
31+
#: THE COLUMNS THIS MUST AT LEAST READ, so a renamed column or a moved folder
32+
#: cannot leave the check reading nothing and passing. Each held a tag list when
33+
#: this was written (issue #1823); the scan is not limited to them.
34+
MUST_READ = {
35+
("EnchantmentsPositive.csv", "Tags"),
36+
("EnchantmentsNegative.csv", "Tags"),
37+
("EnchantmentEffects.csv", "RequiredTags"),
38+
("PassiveEffects.csv", "RequiredTags"),
39+
("WeaponSkills.csv", "Tags"),
40+
("MinionTypes.csv", "Tags"),
41+
}
42+
43+
44+
def repeated_tags(cell: str) -> list[str]:
45+
"""The tags a comma-separated cell lists more than once, in first-seen order."""
46+
tags = [tag.strip() for tag in (cell or "").split(",") if tag.strip()]
47+
return [tag for tag, count in collections.Counter(tags).items() if count > 1]
48+
49+
50+
def tag_columns() -> dict[tuple[str, str], list[dict[str, str]]]:
51+
"""Every (table, column) whose column name contains "tag", with the table's rows."""
52+
found: dict[tuple[str, str], list[dict[str, str]]] = {}
53+
for path in sorted(DATA.glob("*.csv")):
54+
with path.open(encoding="utf-8-sig", newline="") as handle:
55+
rows = list(csv.DictReader(handle))
56+
if not rows:
57+
continue
58+
for column in rows[0]:
59+
if column and "tag" in column.lower():
60+
found[(path.name, column)] = rows
61+
return found
62+
63+
64+
def test_the_scan_reads_every_tag_column_it_must():
65+
"""THE SCOPE, checked first, so the test below cannot pass by reading nothing."""
66+
read = set(tag_columns())
67+
missing = sorted(MUST_READ - read)
68+
assert not missing, (
69+
f"These tag columns were not found under {DATA}: {missing}. A column was "
70+
"renamed or a table moved; update MUST_READ, or the check is reading less "
71+
"than it says.")
72+
73+
74+
def test_no_row_lists_a_tag_twice():
75+
wrong = []
76+
for (table, column), rows in tag_columns().items():
77+
for row in rows:
78+
repeats = repeated_tags(row[column])
79+
if repeats:
80+
key = next(iter(row.values()), "")
81+
wrong.append(f"{table} {column}, row {key}: {', '.join(repeats)}")
82+
assert not wrong, (
83+
"These rows list a tag more than once. The tag is written twice in the "
84+
"design workbook, docs/All_Things_Cataclysm.xlsx; remove the second copy "
85+
"there and regenerate:\n " + "\n ".join(wrong))
86+
87+
88+
def test_a_repeat_is_found_and_a_single_tag_is_not():
89+
"""The helper, on made-up cells, so no change to the real tables can hide a
90+
mistake in it."""
91+
assert repeated_tags("Slot.Ultimate, Slot.Ultimate") == ["Slot.Ultimate"]
92+
assert repeated_tags(
93+
"Stat.Utility.Movespeed, Slot.Movement, Slot.Movement, Keyword.Consume"
94+
) == ["Slot.Movement"]
95+
assert repeated_tags("Slot.Movement, Stat.Utility.Movespeed") == []
96+
assert repeated_tags("") == []
97+
# SPACING DOES NOT HIDE A REPEAT, since the cells are written "A, B".
98+
assert repeated_tags("Slot.Heavy,Slot.Heavy ") == ["Slot.Heavy"]

0 commit comments

Comments
 (0)