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