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