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