WolfDavid commited on
Commit
fbd985f
·
1 Parent(s): e594cd3

test(02-02): add failing unit-joining tests

Browse files

- one named test per joining rule A0-A7 and per chain breaker
- offsets tile every fixture sentence; build_units is pure over dicts

Files changed (1) hide show
  1. tests/test_units.py +290 -0
tests/test_units.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mode-C morphemes -> whole-word units (D-05 joining rule A0-A7), one named test per rule.
2
+
3
+ Every test tokenises with the real Sudachi core dictionary (loaded once per module) and asserts
4
+ on ``build_units``. The unit SPLITS asserted here are the D-05 contract from 02-02-PLAN.md; only
5
+ Sudachi's morpheme output (surfaces, readings, POS tuples) is "observed". If a split disagrees
6
+ with a test, the rule table or its implementation is wrong - never the expectation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ import pytest
14
+
15
+ from japanese_avatar.nlp.tokenizer import morphemes, warmup
16
+ from japanese_avatar.nlp.units import TAPPABLE_POS, build_units
17
+
18
+ SENTENCES = [
19
+ "食べました",
20
+ "食べさせられた",
21
+ "美味しかったです",
22
+ "学生です",
23
+ "きれいじゃなかった",
24
+ "食べたくなかった",
25
+ "美味しくない",
26
+ "人気のない通り",
27
+ "話せるようになりたい",
28
+ "行かなければならない",
29
+ "食べて",
30
+ "読んで",
31
+ "読んでください",
32
+ "日本語を勉強しています",
33
+ "勉強します",
34
+ "田中さんは東京に",
35
+ "三人で",
36
+ "一人で",
37
+ "お願いします",
38
+ "こんにちは",
39
+ "今日は天気がいいですね。",
40
+ "はい、そうです。",
41
+ ]
42
+
43
+
44
+ @pytest.fixture(scope="module", autouse=True)
45
+ def tokenizer_loaded():
46
+ t0 = time.perf_counter()
47
+ seconds = warmup()
48
+ print(f"\nsudachi core load: {seconds:.3f} s (warmup call {time.perf_counter() - t0:.3f} s)")
49
+
50
+
51
+ def units_of(text: str) -> list[dict]:
52
+ return build_units(morphemes(text))
53
+
54
+
55
+ def surfaces(units: list[dict]) -> list[str]:
56
+ return [u["surface"] for u in units]
57
+
58
+
59
+ def tappable(units: list[dict]) -> list[bool]:
60
+ return [u["tappable"] for u in units]
61
+
62
+
63
+ def test_morphemes_are_plain_dicts():
64
+ ms = morphemes("食べました")
65
+ assert len(ms) == 3
66
+ for m in ms:
67
+ assert set(m) == {"surface", "reading", "lemma", "norm", "pos", "oov"}
68
+ assert isinstance(m["pos"], list) and len(m["pos"]) == 6
69
+ assert all(isinstance(p, str) for p in m["pos"])
70
+ assert isinstance(m["oov"], bool)
71
+ assert type(m) is dict
72
+ assert ms[0]["surface"] == "食べ"
73
+ assert ms[0]["reading"] == "タベ" # katakana, as Sudachi returns it
74
+ assert ms[0]["lemma"] == "食べる"
75
+ assert ms[0]["pos"][0] == "動詞"
76
+ assert morphemes("") == []
77
+
78
+
79
+ def test_a1_auxiliary_chain():
80
+ units = units_of("食べました")
81
+ assert len(units) == 1
82
+ u = units[0]
83
+ assert u == {
84
+ "surface": "食べました",
85
+ "reading": "たべました",
86
+ "lemma": "食べる",
87
+ "level_key": "食べる",
88
+ "pos": "動詞",
89
+ "tappable": True,
90
+ "is_name": False,
91
+ "start": 0,
92
+ "end": 5,
93
+ "morpheme_count": 3,
94
+ }
95
+ units = units_of("食べさせられた")
96
+ assert surfaces(units) == ["食べさせられた"]
97
+ assert units[0]["lemma"] == "食べる"
98
+ assert units[0]["morpheme_count"] == 4
99
+
100
+
101
+ def test_a1_stops_at_desu_da():
102
+ units = units_of("美味しかったです")
103
+ assert surfaces(units) == ["美味しかった", "です"]
104
+ assert units[0]["lemma"] == "美味しい"
105
+ assert tappable(units) == [True, False]
106
+
107
+ units = units_of("学生です")
108
+ assert surfaces(units) == ["学生", "です"]
109
+ assert tappable(units) == [True, False]
110
+
111
+ units = units_of("きれいじゃなかった")
112
+ assert surfaces(units) == ["きれい", "じゃ", "なかった"]
113
+ assert tappable(units) == [True, False, True]
114
+
115
+
116
+ def test_a2_nai_attaches_after_aux():
117
+ units = units_of("食べたくなかった")
118
+ assert surfaces(units) == ["食べたくなかった"]
119
+ assert units[0]["lemma"] == "食べる"
120
+ assert units[0]["reading"] == "たべたくなかった"
121
+
122
+ # The 形容詞 tail: 美味しく is 形容詞,一般 / 連用形-一般, so ない attaches under A2.
123
+ units = units_of("美味しくない")
124
+ assert surfaces(units) == ["美味しくない"]
125
+ assert units[0]["lemma"] == "美味しい"
126
+ assert units[0]["reading"] == "おいしくない"
127
+ assert units[0]["tappable"] is True
128
+
129
+
130
+ def test_a2_nai_after_particle_is_a_word():
131
+ units = units_of("人気のない通り")
132
+ assert surfaces(units) == ["人気", "の", "ない", "通り"]
133
+ assert tappable(units) == [True, False, True, True]
134
+ assert units[2]["lemma"] == "ない"
135
+ assert units[2]["level_key"] == "無い"
136
+ assert units[2]["pos"] == "形容詞"
137
+
138
+
139
+ def test_a0_breaker_units_accept_nothing():
140
+ # じゃ (lemma だ) is a breaker: なかっ may not join it although じゃ is 助動詞; た then
141
+ # joins なかっ via A1 with a 形容詞 tail.
142
+ units = units_of("きれいじゃなかった")
143
+ assert surfaces(units) == ["きれい", "じゃ", "なかった"]
144
+ assert units[1]["lemma"] == "だ"
145
+ assert units[2]["lemma"] == "ない"
146
+ assert units[2]["morpheme_count"] == 2
147
+
148
+ # よう (形状詞,助動詞語幹) is a breaker: に (助動詞, lemma だ) is its own unit after it.
149
+ units = units_of("話せるようになりたい")
150
+ assert surfaces(units) == ["話せる", "よう", "に", "なりたい"]
151
+
152
+ # の (助詞) is a breaker: ない is its own unit after it.
153
+ units = units_of("人気のない通り")
154
+ assert surfaces(units)[1:3] == ["の", "ない"]
155
+
156
+
157
+ def test_a3_te_de():
158
+ units = units_of("食べて")
159
+ assert surfaces(units) == ["食べて"]
160
+ assert units[0]["lemma"] == "食べる"
161
+
162
+ units = units_of("読んで")
163
+ assert surfaces(units) == ["読んで"]
164
+ assert units[0]["lemma"] == "読む"
165
+ assert units[0]["reading"] == "よんで"
166
+
167
+ # ば is 助詞,接続助詞 but not て/で: it starts its own non-tappable unit; A0 keeps なら
168
+ # off it, then ない (助動詞) joins なら via A1.
169
+ units = units_of("行かなければならない")
170
+ assert surfaces(units) == ["行かなけれ", "ば", "ならない"]
171
+ assert tappable(units) == [True, False, True]
172
+ assert units[0]["lemma"] == "行く"
173
+ assert units[2]["lemma"] == "なる"
174
+
175
+
176
+ def test_a4_te_auxiliary_verb():
177
+ units = units_of("読んでください")
178
+ assert surfaces(units) == ["読んでください"]
179
+ assert units[0]["lemma"] == "読む"
180
+ assert units[0]["reading"] == "よんでください"
181
+
182
+ units = units_of("日本語を勉強しています")
183
+ assert surfaces(units) == ["日本語", "を", "勉強しています"]
184
+ last = units[-1]
185
+ assert last["lemma"] == "勉強する"
186
+ assert last["level_key"] == "勉強"
187
+ assert last["pos"] == "名詞"
188
+ assert last["tappable"] is True
189
+ assert last["reading"] == "べんきょうしています"
190
+ assert last["morpheme_count"] == 5
191
+
192
+
193
+ def test_a5_suru_noun():
194
+ units = units_of("勉強します")
195
+ assert surfaces(units) == ["勉強します"]
196
+ assert units[0]["lemma"] == "勉強する"
197
+ assert units[0]["level_key"] == "勉強"
198
+
199
+ units = units_of("勉強")
200
+ assert surfaces(units) == ["勉強"]
201
+ assert units[0]["lemma"] == "勉強"
202
+
203
+
204
+ def test_a6_suffix():
205
+ units = units_of("田中さんは東京に")
206
+ assert surfaces(units) == ["田中さん", "は", "東京", "に"]
207
+ assert units[0]["is_name"] is True
208
+ assert units[0]["lemma"] == "田中"
209
+ assert units[0]["reading"] == "たなかさん"
210
+ assert units[0]["tappable"] is True
211
+ assert units[2]["is_name"] is True
212
+ assert units[2]["lemma"] == "東京"
213
+ assert [u["is_name"] for u in units] == [True, False, True, False]
214
+
215
+ units = units_of("三人で")
216
+ assert surfaces(units) == ["三人", "で"]
217
+ assert units[0]["tappable"] is True
218
+ assert units[0]["lemma"] == "三"
219
+ assert units[0]["reading"] == "さんにん"
220
+ # Sudachi's normalized_form for the OOV numeral 三 is the digit "3"; recorded, not judged.
221
+ assert units[0]["level_key"] == "3"
222
+ assert units[1]["tappable"] is False
223
+
224
+ units = units_of("一人で")
225
+ assert surfaces(units) == ["一人", "で"]
226
+ assert units[0]["morpheme_count"] == 1
227
+
228
+
229
+ def test_a7_prefix():
230
+ units = units_of("お願いします")
231
+ assert surfaces(units) == ["お願い", "します"]
232
+ assert tappable(units) == [True, True]
233
+ assert units[0]["lemma"] == "願う"
234
+ assert units[0]["reading"] == "おねがい"
235
+ assert units[0]["pos"] == "動詞"
236
+ assert units[0]["morpheme_count"] == 2
237
+ assert units[1]["lemma"] == "する"
238
+
239
+
240
+ def test_non_tappable_units():
241
+ units = units_of("話せるようになりたい")
242
+ by_surface = {u["surface"]: u for u in units}
243
+ assert by_surface["よう"]["tappable"] is False
244
+ assert by_surface["に"]["tappable"] is False
245
+ assert by_surface["話せる"]["tappable"] is True
246
+ assert by_surface["話せる"]["lemma"] == "話せる"
247
+ assert by_surface["話せる"]["level_key"] == "話す"
248
+ assert by_surface["なりたい"]["lemma"] == "なる"
249
+ assert by_surface["なりたい"]["tappable"] is True
250
+
251
+ for mark in ("。", "、"):
252
+ units = units_of(mark)
253
+ assert len(units) == 1
254
+ assert units[0]["tappable"] is False
255
+ assert units[0]["pos"] == "補助記号"
256
+
257
+ units = units_of("こんにちは")
258
+ assert len(units) == 1
259
+ assert units[0]["tappable"] is True
260
+ assert units[0]["lemma"] == "こんにちは"
261
+ assert units[0]["level_key"] == "今日は"
262
+ assert units[0]["pos"] == "感動詞"
263
+
264
+ assert "助詞" not in TAPPABLE_POS and "助動詞" not in TAPPABLE_POS
265
+ assert {"名詞", "動詞", "形容詞", "形状詞"} <= TAPPABLE_POS
266
+
267
+
268
+ @pytest.mark.parametrize("text", SENTENCES)
269
+ def test_offsets_tile_the_text(text):
270
+ units = units_of(text)
271
+ assert units, text
272
+ assert "".join(u["surface"] for u in units) == text
273
+ assert units[0]["start"] == 0
274
+ for prev, cur in zip(units, units[1:], strict=False):
275
+ assert cur["start"] == prev["end"]
276
+ for u in units:
277
+ assert u["end"] == u["start"] + len(u["surface"])
278
+ assert u["morpheme_count"] >= 1
279
+ assert text[u["start"] : u["end"]] == u["surface"]
280
+ assert units[-1]["end"] == len(text)
281
+
282
+
283
+ def test_build_units_is_pure_over_dicts():
284
+ ms = morphemes("食べました")
285
+ before = [dict(m) for m in ms]
286
+ units = build_units(ms)
287
+ assert ms == before # input not mutated
288
+ assert build_units(ms) == units # deterministic
289
+ assert build_units([]) == []
290
+ assert all(type(u) is dict for u in units)