1
2
3
4 package runewidth
5
6 import (
7 "crypto/sha256"
8 "fmt"
9 "os"
10 "sort"
11 "testing"
12 "unicode/utf8"
13 )
14
15 var _ sort.Interface = (*table)(nil)
16
17 func init() {
18 os.Setenv("RUNEWIDTH_EASTASIAN", "")
19 handleEnv()
20 }
21
22 func (t table) Len() int {
23 return len(t)
24 }
25
26 func (t table) Less(i, j int) bool {
27 return t[i].first < t[j].first
28 }
29
30 func (t *table) Swap(i, j int) {
31 (*t)[i], (*t)[j] = (*t)[j], (*t)[i]
32 }
33
34 type tableInfo struct {
35 tbl table
36 name string
37 wantN int
38 wantSHA string
39 }
40
41 var tables = []tableInfo{
42 {private, "private", 137468, "a4a641206dc8c5de80bd9f03515a54a706a5a4904c7684dc6a33d65c967a51b2"},
43 {nonprint, "nonprint", 2143, "288904683eb225e7c4c0bd3ee481b53e8dace404ec31d443afdbc4d13729fe95"},
44 {combining, "combining", 465, "3cce13deb5e23f9f7327f2b1ef162328285a7dcf277a98302a8f7cdd43971268"},
45 {doublewidth, "doublewidth", 182440, "3d16eda8650dc2c92d6318d32f0b4a74fda5a278db2d4544b1dd65863394823c"},
46 {ambiguous, "ambiguous", 138739, "d05e339a10f296de6547ff3d6c5aee32f627f6555477afebd4a3b7e3cf74c9e3"},
47 {emoji, "emoji", 3535, "9ec17351601d49c535658de8d129c1d0ccda2e620669fc39a2faaee7dedcef6d"},
48 {narrow, "narrow", 111, "fa897699c5e3cd9141c638d539331b0bdd508b874e22996c5e929767d455fc5a"},
49 {neutral, "neutral", 27333, "5455f5e75c307f70b4e9b2384dc5a8bcd91a4c5e2b24b2b185dfad4d860ee5c2"},
50 }
51
52 func TestTableChecksums(t *testing.T) {
53 for _, ti := range tables {
54 gotN := 0
55 buf := make([]byte, utf8.MaxRune+1)
56 for r := rune(0); r <= utf8.MaxRune; r++ {
57 if inTable(r, ti.tbl) {
58 gotN++
59 buf[r] = 1
60 }
61 }
62 gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
63 if gotN != ti.wantN || gotSHA != ti.wantSHA {
64 t.Errorf("table = %s,\n\tn = %d want %d,\n\tsha256 = %s want %s", ti.name, gotN, ti.wantN, gotSHA, ti.wantSHA)
65 }
66 }
67 }
68
69 func TestRuneWidthChecksums(t *testing.T) {
70 var testcases = []struct {
71 name string
72 eastAsianWidth bool
73 wantSHA string
74 }{
75 {"ea-no", false, "4eb632b105d3b2c800dda9141381d0b8a95250a3a5c7f1a5ca2c4d4daaa85234"},
76 {"ea-yes", true, "c2ddc3bdf42d81d4c23050e21eda46eb639b38b15322d35e8eb6c26f3b83ce92"},
77 }
78
79 for _, testcase := range testcases {
80 c := NewCondition()
81 c.EastAsianWidth = testcase.eastAsianWidth
82 buf := make([]byte, utf8.MaxRune+1)
83 for r := rune(0); r <= utf8.MaxRune; r++ {
84 buf[r] = byte(c.RuneWidth(r))
85 }
86 gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
87 if gotSHA != testcase.wantSHA {
88 t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
89 testcase.name, gotSHA, testcase.wantSHA)
90 }
91
92
93 c.CreateLUT()
94 for r := rune(0); r <= utf8.MaxRune; r++ {
95 buf[r] = byte(c.RuneWidth(r))
96 }
97 gotSHA = fmt.Sprintf("%x", sha256.Sum256(buf))
98 if gotSHA != testcase.wantSHA {
99 t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
100 testcase.name, gotSHA, testcase.wantSHA)
101 }
102 }
103 }
104
105 func TestDefaultLUT(t *testing.T) {
106 var testcases = []struct {
107 name string
108 eastAsianWidth bool
109 wantSHA string
110 }{
111 {"ea-no", false, "4eb632b105d3b2c800dda9141381d0b8a95250a3a5c7f1a5ca2c4d4daaa85234"},
112 {"ea-yes", true, "c2ddc3bdf42d81d4c23050e21eda46eb639b38b15322d35e8eb6c26f3b83ce92"},
113 }
114
115 old := os.Getenv("RUNEWIDTH_EASTASIAN")
116 defer os.Setenv("RUNEWIDTH_EASTASIAN", old)
117
118 CreateLUT()
119 for _, testcase := range testcases {
120 c := DefaultCondition
121
122 if testcase.eastAsianWidth {
123 os.Setenv("RUNEWIDTH_EASTASIAN", "1")
124 } else {
125 os.Setenv("RUNEWIDTH_EASTASIAN", "0")
126 }
127 handleEnv()
128
129 buf := make([]byte, utf8.MaxRune+1)
130 for r := rune(0); r <= utf8.MaxRune; r++ {
131 buf[r] = byte(c.RuneWidth(r))
132 }
133 gotSHA := fmt.Sprintf("%x", sha256.Sum256(buf))
134 if gotSHA != testcase.wantSHA {
135 t.Errorf("TestRuneWidthChecksums = %s,\n\tsha256 = %s want %s",
136 testcase.name, gotSHA, testcase.wantSHA)
137 }
138 }
139
140 DefaultCondition.combinedLut = nil
141 }
142
143 func checkInterval(first, last rune) bool {
144 return first >= 0 && first <= utf8.MaxRune &&
145 last >= 0 && last <= utf8.MaxRune &&
146 first <= last
147 }
148
149 func isCompact(t *testing.T, ti *tableInfo) bool {
150 tbl := ti.tbl
151 for i := range tbl {
152 e := tbl[i]
153 if !checkInterval(e.first, e.last) {
154 t.Errorf("table invalid: table = %s index = %d %v", ti.name, i, e)
155 return false
156 }
157 if i+1 < len(tbl) && e.last+1 >= tbl[i+1].first {
158 t.Errorf("table not compact: table = %s index = %d %v %v", ti.name, i, e, tbl[i+1])
159 return false
160 }
161 }
162 return true
163 }
164
165 func TestSorted(t *testing.T) {
166 for _, ti := range tables {
167 if !sort.IsSorted(&ti.tbl) {
168 t.Errorf("table not sorted: %s", ti.name)
169 }
170 if !isCompact(t, &ti) {
171 t.Errorf("table not compact: %s", ti.name)
172 }
173 }
174 }
175
176 var runewidthtests = []struct {
177 in rune
178 out int
179 eaout int
180 nseout int
181 }{
182 {'世', 2, 2, 2},
183 {'界', 2, 2, 2},
184 {'セ', 1, 1, 1},
185 {'カ', 1, 1, 1},
186 {'イ', 1, 1, 1},
187 {'☆', 1, 2, 2},
188 {'☺', 1, 1, 2},
189 {'☻', 1, 1, 2},
190 {'♥', 1, 2, 2},
191 {'♦', 1, 1, 2},
192 {'♣', 1, 2, 2},
193 {'♠', 1, 2, 2},
194 {'♂', 1, 2, 2},
195 {'♀', 1, 2, 2},
196 {'♪', 1, 2, 2},
197 {'♫', 1, 1, 2},
198 {'☼', 1, 1, 2},
199 {'↕', 1, 2, 2},
200 {'‼', 1, 1, 2},
201 {'↔', 1, 2, 2},
202 {'\x00', 0, 0, 0},
203 {'\x01', 0, 0, 0},
204 {'\u0300', 0, 0, 0},
205 {'\u2028', 0, 0, 0},
206 {'\u2029', 0, 0, 0},
207 {'a', 1, 1, 1},
208 {'⟦', 1, 1, 1},
209 {'👁', 1, 1, 2},
210 }
211
212 func TestRuneWidth(t *testing.T) {
213 c := NewCondition()
214 c.EastAsianWidth = false
215 for _, tt := range runewidthtests {
216 if out := c.RuneWidth(tt.in); out != tt.out {
217 t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=false)", tt.in, out, tt.out)
218 }
219 }
220 c.EastAsianWidth = true
221 for _, tt := range runewidthtests {
222 if out := c.RuneWidth(tt.in); out != tt.eaout {
223 t.Errorf("RuneWidth(%q) = %d, want %d (EastAsianWidth=true)", tt.in, out, tt.eaout)
224 }
225 }
226 c.StrictEmojiNeutral = false
227 for _, tt := range runewidthtests {
228 if out := c.RuneWidth(tt.in); out != tt.nseout {
229 t.Errorf("RuneWidth(%q) = %d, want %d (StrictEmojiNeutral=false)", tt.in, out, tt.eaout)
230 }
231 }
232 }
233
234 var isambiguouswidthtests = []struct {
235 in rune
236 out bool
237 }{
238 {'世', false},
239 {'■', true},
240 {'界', false},
241 {'○', true},
242 {'㈱', false},
243 {'①', true},
244 {'②', true},
245 {'③', true},
246 {'④', true},
247 {'⑤', true},
248 {'⑥', true},
249 {'⑦', true},
250 {'⑧', true},
251 {'⑨', true},
252 {'⑩', true},
253 {'⑪', true},
254 {'⑫', true},
255 {'⑬', true},
256 {'⑭', true},
257 {'⑮', true},
258 {'⑯', true},
259 {'⑰', true},
260 {'⑱', true},
261 {'⑲', true},
262 {'⑳', true},
263 {'☆', true},
264 }
265
266 func TestIsAmbiguousWidth(t *testing.T) {
267 for _, tt := range isambiguouswidthtests {
268 if out := IsAmbiguousWidth(tt.in); out != tt.out {
269 t.Errorf("IsAmbiguousWidth(%q) = %v, want %v", tt.in, out, tt.out)
270 }
271 }
272 }
273
274 var stringwidthtests = []struct {
275 in string
276 out int
277 eaout int
278 }{
279 {"■㈱の世界①", 10, 12},
280 {"スター☆", 7, 8},
281 {"つのだ☆HIRO", 11, 12},
282 }
283
284 func TestStringWidth(t *testing.T) {
285 c := NewCondition()
286 c.EastAsianWidth = false
287 for _, tt := range stringwidthtests {
288 if out := c.StringWidth(tt.in); out != tt.out {
289 t.Errorf("StringWidth(%q) = %d, want %d", tt.in, out, tt.out)
290 }
291 }
292 c.EastAsianWidth = true
293 for _, tt := range stringwidthtests {
294 if out := c.StringWidth(tt.in); out != tt.eaout {
295 t.Errorf("StringWidth(%q) = %d, want %d (EA)", tt.in, out, tt.eaout)
296 }
297 }
298 }
299
300 func TestStringWidthInvalid(t *testing.T) {
301 s := "こんにちわ\x00世界"
302 if out := StringWidth(s); out != 14 {
303 t.Errorf("StringWidth(%q) = %d, want %d", s, out, 14)
304 }
305 }
306
307 func TestTruncateSmaller(t *testing.T) {
308 s := "あいうえお"
309 expected := "あいうえお"
310
311 if out := Truncate(s, 10, "..."); out != expected {
312 t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
313 }
314 }
315
316 func TestTruncate(t *testing.T) {
317 s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
318 expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..."
319 out := Truncate(s, 80, "...")
320 if out != expected {
321 t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
322 }
323 width := StringWidth(out)
324 if width != 79 {
325 t.Errorf("width of Truncate(%q) should be %d, but %d", s, 79, width)
326 }
327 }
328
329 func TestTruncateFit(t *testing.T) {
330 s := "aあいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
331 expected := "aあいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..."
332
333 out := Truncate(s, 80, "...")
334 if out != expected {
335 t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
336 }
337 width := StringWidth(out)
338 if width != 80 {
339 t.Errorf("width of Truncate(%q) should be %d, but %d", s, 80, width)
340 }
341 }
342
343 func TestTruncateJustFit(t *testing.T) {
344 s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
345 expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
346
347 out := Truncate(s, 80, "...")
348 if out != expected {
349 t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
350 }
351 width := StringWidth(out)
352 if width != 80 {
353 t.Errorf("width of Truncate(%q) should be %d, but %d", s, 80, width)
354 }
355 }
356
357 func TestWrap(t *testing.T) {
358 s := `東京特許許可局局長はよく柿喰う客だ/東京特許許可局局長はよく柿喰う客だ
359 123456789012345678901234567890
360
361 END`
362 expected := `東京特許許可局局長はよく柿喰う
363 客だ/東京特許許可局局長はよく
364 柿喰う客だ
365 123456789012345678901234567890
366
367 END`
368
369 if out := Wrap(s, 30); out != expected {
370 t.Errorf("Wrap(%q) = %q, want %q", s, out, expected)
371 }
372 }
373
374 func TestTruncateNoNeeded(t *testing.T) {
375 s := "あいうえおあい"
376 expected := "あいうえおあい"
377
378 if out := Truncate(s, 80, "..."); out != expected {
379 t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
380 }
381 }
382
383 var truncatelefttests = []struct {
384 s string
385 w int
386 prefix string
387 out string
388 }{
389 {"source", 4, "", "ce"},
390 {"source", 4, "...", "...ce"},
391 {"あいうえお", 6, "", "えお"},
392 {"あいうえお", 6, "...", "...えお"},
393 {"あいうえお", 10, "", ""},
394 {"あいうえお", 10, "...", "..."},
395 {"あいうえお", 5, "", " えお"},
396 {"Aあいうえお", 5, "", "うえお"},
397 }
398
399 func TestTruncateLeft(t *testing.T) {
400 t.Parallel()
401
402 for _, tt := range truncatelefttests {
403 if out := TruncateLeft(tt.s, tt.w, tt.prefix); out != tt.out {
404 t.Errorf("TruncateLeft(%q) = %q, want %q", tt.s, out, tt.out)
405 }
406 }
407 }
408
409 var isneutralwidthtests = []struct {
410 in rune
411 out bool
412 }{
413 {'→', false},
414 {'┊', false},
415 {'┈', false},
416 {'~', false},
417 {'└', false},
418 {'⣀', true},
419 {'⣀', true},
420 }
421
422 func TestIsNeutralWidth(t *testing.T) {
423 for _, tt := range isneutralwidthtests {
424 if out := IsNeutralWidth(tt.in); out != tt.out {
425 t.Errorf("IsNeutralWidth(%q) = %v, want %v", tt.in, out, tt.out)
426 }
427 }
428 }
429
430 func TestFillLeft(t *testing.T) {
431 s := "あxいうえお"
432 expected := " あxいうえお"
433
434 if out := FillLeft(s, 15); out != expected {
435 t.Errorf("FillLeft(%q) = %q, want %q", s, out, expected)
436 }
437 }
438
439 func TestFillLeftFit(t *testing.T) {
440 s := "あいうえお"
441 expected := "あいうえお"
442
443 if out := FillLeft(s, 10); out != expected {
444 t.Errorf("FillLeft(%q) = %q, want %q", s, out, expected)
445 }
446 }
447
448 func TestFillRight(t *testing.T) {
449 s := "あxいうえお"
450 expected := "あxいうえお "
451
452 if out := FillRight(s, 15); out != expected {
453 t.Errorf("FillRight(%q) = %q, want %q", s, out, expected)
454 }
455 }
456
457 func TestFillRightFit(t *testing.T) {
458 s := "あいうえお"
459 expected := "あいうえお"
460
461 if out := FillRight(s, 10); out != expected {
462 t.Errorf("FillRight(%q) = %q, want %q", s, out, expected)
463 }
464 }
465
466 func TestEnv(t *testing.T) {
467 old := os.Getenv("RUNEWIDTH_EASTASIAN")
468 defer os.Setenv("RUNEWIDTH_EASTASIAN", old)
469
470 os.Setenv("RUNEWIDTH_EASTASIAN", "0")
471 handleEnv()
472
473 if w := RuneWidth('│'); w != 1 {
474 t.Errorf("RuneWidth('│') = %d, want %d", w, 1)
475 }
476 }
477
478 func TestZeroWidthJoiner(t *testing.T) {
479 c := NewCondition()
480
481 var tests = []struct {
482 in string
483 want int
484 }{
485 {"👩", 2},
486 {"👩\u200d", 2},
487 {"👩\u200d🍳", 2},
488 {"\u200d🍳", 2},
489 {"👨\u200d👨", 2},
490 {"👨\u200d👨\u200d👧", 2},
491 {"🏳️\u200d🌈", 1},
492 {"あ👩\u200d🍳い", 6},
493 {"あ\u200d🍳い", 6},
494 {"あ\u200dい", 4},
495 }
496
497 for _, tt := range tests {
498 if got := c.StringWidth(tt.in); got != tt.want {
499 t.Errorf("StringWidth(%q) = %d, want %d", tt.in, got, tt.want)
500 }
501 }
502 }
503
View as plain text