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