github.com/ericwq/aprilsh@v0.0.0-20240517091432-958bc568daa0/frontend/overlay_test.go (about) 1 // Copyright 2022~2023 wangqi. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE file. 4 5 package frontend 6 7 import ( 8 "fmt" 9 "math" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/ericwq/aprilsh/terminal" 15 "github.com/rivo/uniseg" 16 ) 17 18 func TestOverlay(t *testing.T) { 19 co := newConditionalOverlay(12, 2, 14) 20 21 if co.tentative(15) { 22 t.Errorf("expect %t, got %t\n", true, co.tentative(15)) 23 } 24 25 co.expire(13, 14) 26 if co.expirationFrame != 13 || co.predictionTime != 14 { 27 t.Errorf("expire() expirationFrame expect %d, got %d\n", 13, co.expirationFrame) 28 t.Errorf("expire() predictionTime expect %d, got %d\n", 14, co.predictionTime) 29 } 30 31 co.reset() 32 if co.expirationFrame != math.MaxUint64 || co.tentativeUntilEpoch != math.MaxInt64 || co.active != false { 33 t.Errorf("reset() expirationFrame should be %d, got %d\n", -1, co.expirationFrame) 34 } 35 } 36 37 func TestMoveApply(t *testing.T) { 38 tc := []struct { 39 name string 40 activeParam bool 41 confirmedEpoch int64 42 posY, posX int 43 }{ 44 {"apply() active=T, tentative return F", true, 15, 4, 10}, 45 {"apply() active=F", false, 15, 0, 0}, 46 {"apply() active=T, tentative return T", true, 14, 0, 0}, 47 } 48 emu := terminal.NewEmulator3(80, 40, 40) 49 ccm := newConditionalCursorMove(12, 4, 10, 15) 50 51 for _, v := range tc { 52 emu.MoveCursor(0, 0) // default cursor position for early return. 53 ccm.active = v.activeParam 54 ccm.apply(emu, v.confirmedEpoch) 55 posY := emu.GetCursorRow() 56 posX := emu.GetCursorCol() 57 if posX != v.posX || posY != v.posY { 58 t.Errorf("%s posY expect %d, got %d\n", v.name, v.posY, posY) 59 t.Errorf("%s posX expect %d, got %d\n", v.name, v.posX, posX) 60 } 61 } 62 } 63 64 func TestMoveGetValidity(t *testing.T) { 65 tc := []struct { 66 name string 67 lateAck uint64 68 expirationFrame uint64 69 active bool 70 rowEmu, colEmu int 71 rowCcm, colCcm int 72 validity Validity 73 }{ 74 {"getValidity() active=T, row,col in scope, lateAck >=expirationFrame", 20, 15, true, 10, 10, 10, 10, Correct}, 75 {"getValidity() active=T, row,col outof scope", 20, 15, true, 10, 10, 50, 50, IncorrectOrExpired}, 76 {"getValidity() active=T, row,col not equal, lateAck >=expirationFrame", 20, 20, true, 10, 12, 10, 10, IncorrectOrExpired}, 77 {"getValidity() active=T, row,col in scope, lateAck < expirationFrame", 20, 21, true, 10, 10, 10, 10, Pending}, 78 {"getValidity() active=F", 20, 21, false, 10, 10, 10, 10, Inactive}, 79 } 80 81 emu := terminal.NewEmulator3(80, 40, 40) 82 83 for _, v := range tc { 84 emu.MoveCursor(v.rowEmu, v.colEmu) 85 ccm := newConditionalCursorMove(v.expirationFrame, v.rowCcm, v.colCcm, 12) 86 ccm.active = v.active 87 validity := ccm.getValidity(emu, v.lateAck) 88 if validity != v.validity { 89 t.Errorf("%q getValidity() expect %d, got %d\n", v.name, v.validity, validity) 90 } 91 } 92 } 93 94 func TestCellApply(t *testing.T) { 95 underlineRend := terminal.NewRenditions(4) // renditions with underline attribute 96 underlineCell := terminal.Cell{} 97 underlineCell.SetRenditions(underlineRend) 98 plainCell := terminal.Cell{} 99 100 tc := []struct { 101 name string 102 active bool 103 confirmedEpoch int64 104 flag bool 105 row, col int 106 unknown bool 107 contents rune 108 rend *terminal.Renditions 109 cell *terminal.Cell 110 }{ 111 {"active=T flag=T unknown=F update cell and rendition", true, 20, true, 10, 10, false, 'E', &underlineRend, &underlineCell}, 112 {"active=T flag=F unknown=F update cell", true, 20, false, 11, 10, false, 'E', nil, &plainCell}, 113 {"active=T flag=T unknown=T update rendition", true, 20, true, 12, 10, true, 'E', &underlineRend, nil}, 114 {"active=T flag=F unknown=T return", true, 20, false, 13, 10, true, 'E', nil, nil}, 115 {"active=T flag=T unknown=T return", true, 20, true, 14, 10, true, '\x00', nil, nil}, 116 {"tentative early return", true, 9, true, 14, 10, true, 'E', nil, nil}, 117 {"active early return", false, 10, true, 14, 10, true, 'E', nil, nil}, 118 } 119 120 emu := terminal.NewEmulator3(80, 40, 40) 121 for _, v := range tc { 122 predict := newConditionalOverlayCell(10, v.col, 10) 123 124 predict.active = v.active 125 predict.unknown = v.unknown 126 // set content for emulator cell 127 if v.contents != '\x00' { 128 emu.GetCellPtr(v.row, v.col).Append(v.contents) 129 } 130 131 // call apply 132 predict.apply(emu, v.confirmedEpoch, v.row, v.flag) 133 134 // validate cell 135 cell := emu.GetCell(v.row, v.col) 136 if v.cell != nil && cell != *(v.cell) { 137 t.Errorf("%q cell (%d,%d) contents expect\n%v\ngot \n%v\n", v.name, v.row, v.col, *v.cell, cell) 138 } 139 140 // validate rendition 141 rend := emu.GetCell(v.row, v.col).GetRenditions() 142 if v.rend != nil && rend != *v.rend { 143 t.Errorf("%q cell (%d,%d) renditions expect %v, got %v\n", v.name, v.row, v.col, *v.rend, rend) 144 } 145 } 146 } 147 148 func TestCellGetValidity(t *testing.T) { 149 tc := []struct { 150 name string 151 active bool 152 row, col int 153 lateAck uint64 154 unknown bool 155 base string // base content 156 predict string // prediction 157 frame string // frame content 158 validity Validity 159 }{ 160 // the test case only check the first cell in babse, prediction and frame 161 {"active=F, unknown=F", false, 13, 70, 20, false, "", "active", "false", Inactive}, // active is false 162 {"active=T, cursor out of range", true, 41, 70, 0, false, "", "smaller", "lateAck", IncorrectOrExpired}, // row out of range 163 {"active=T, smaller lateAck", true, 13, 70, 0, false, "", "smaller", "lateAck", Pending}, // smaller lateAck 164 {"active=T, unknown=T", true, 13, 70, 20, true, "", "unknow", "true", CorrectNoCredit}, // unknown=T 165 {"active=T, unknown=F, blank predict", true, 13, 70, 20, false, "----", " ", "some", CorrectNoCredit}, // blank prediction 166 {"active=T, unknown=F, found original", true, 12, 70, 20, false, "Else", "Else", "Else", CorrectNoCredit}, // found original 167 {"active=T, unknown=T, isBlank=F correct", true, 14, 70, 5, false, " ", "right", "right", Correct}, // not found original 168 {"active=T, unknown=F, content not match", true, 11, 70, 20, false, "-----", "Alpha", "Beta", IncorrectOrExpired}, 169 } 170 171 emu := terminal.NewEmulator3(80, 40, 40) 172 pe := newPredictionEngine() 173 174 for _, v := range tc { 175 t.Run(v.name, func(t *testing.T) { 176 pe.Reset() 177 178 // set the base content 179 emu.MoveCursor(v.row, v.col) 180 emu.HandleStream(v.base) 181 182 // mimic user input for prediction engine 183 emu.MoveCursor(v.row, v.col) 184 now := time.Now().UnixMilli() 185 for i := range v.predict { 186 pe.handleUserGrapheme(emu, now, rune(v.predict[i])) 187 } 188 189 // mimic the result from server 190 emu.MoveCursor(v.row, v.col) 191 emu.HandleStream(v.frame) 192 193 // get the predict row 194 predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 195 predict := &(predictRow.overlayCells[v.col]) 196 197 predict.active = v.active 198 predict.unknown = v.unknown 199 200 validity := predict.getValidity(emu, v.row, v.lateAck) 201 if validity != v.validity { 202 t.Errorf("%q expect %d, got %d\n", v.name, v.validity, validity) 203 t.Errorf("cell (%d,%d) replacement=%s, originalContents=%s\n", v.row, v.col, predict.replacement, predict.originalContents) 204 } 205 }) 206 } 207 } 208 209 func TestPredictionNewUserInput_Normal(t *testing.T) { 210 tc := []struct { 211 label string 212 row, col int // the specified row and col 213 base string // base content 214 predict string // prediction 215 result string // frame content 216 displayPreference DisplayPreference 217 predictOverwrite bool // predictOverwrite 218 posY, posX int // new cursor position, 0 means doesn't matter 219 }{ 220 /* 0*/ {"insert english", 3, 75, "12345", "abcde", "abcde", Adaptive, false, -1, -1}, 221 /* 1*/ {"insert chinese", 4, 70, "", "四姑娘山", "四姑娘山", Adaptive, false, -1, -1}, 222 /* 2*/ {"Experimental", 4, 60, "", "Experimental", "Experimental", Experimental, false, -1, -1}, 223 /* 3*/ {"insert CUF", 4, 75, "", "\x1B[C", "", Adaptive, false, 4, 76}, 224 /* 4*/ {"insert CUB", 4, 75, "", "\x1B[D", "", Adaptive, false, 4, 74}, 225 /* 5*/ {"insert CR", 4, 75, "", "\r", "", Adaptive, false, 5, 0}, 226 /* 6*/ {"insert CUF", 4, 75, "", "\x1BOC", "", Adaptive, false, 4, 76}, 227 /* 7*/ {"BEL becomeTentative", 5, 70, "", "\x07", "", Adaptive, false, -1, -1}, 228 /* 8*/ {"Never", 4, 75, "", "Never", "", Never, false, 0, 0}, 229 /* 9*/ { 230 "insert chinese with base contents", 6, 71, "上海56789", "四姑娘", "四姑娘上", 231 Adaptive, false, -1, -1, 232 }, 233 /*10*/ {"insert chinese with wrap", 7, 79, "", "四", "四", Adaptive, false, 8, 0}, 234 /*11*/ {"insert control becomeTentative", 9, 0, "", "\x11", "", Adaptive, false, -1, -1}, 235 /*12*/ {"insert overwrite", 10, 75, "12345", "abcde", "abcde", Adaptive, true, -1, -1}, 236 } 237 238 pe := newPredictionEngine() 239 emu := terminal.NewEmulator3(80, 40, 40) 240 241 for k, v := range tc { 242 t.Run(v.label, func(t *testing.T) { 243 pe.Reset() 244 245 // set the base content 246 emu.MoveCursor(v.row, v.col) 247 emu.HandleStream(v.base) 248 249 // set the displayPreference field 250 pe.displayPreference = v.displayPreference 251 pe.predictOverwrite = v.predictOverwrite 252 253 // mimic user input for prediction engine 254 emu.MoveCursor(v.row, v.col) 255 epoch := pe.predictionEpoch 256 pe.inputString(emu, v.predict) 257 258 switch k { 259 case 0, 1, 2, 9, 12: 260 // validate the result against predict cell 261 predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 262 i := 0 263 for _, ch := range v.result { 264 if v.col+i > emu.GetWidth()-1 { 265 break 266 } 267 268 cell := predictRow.overlayCells[v.col+i].replacement 269 if cell.String() != string(ch) { 270 t.Errorf("%s expect %q at (%d,%d), got %q\n", v.label, string(ch), v.row, v.col+i, cell) 271 t.Errorf("predict cell (%d,%d) is %q dw=%t, dwcont=%t\n", v.row, v.col+i, cell, cell.IsDoubleWidth(), cell.IsDoubleWidthCont()) 272 } 273 i += uniseg.StringWidth(string([]rune{ch})) 274 } 275 case 3, 4, 5, 6: 276 // validate the cursor position 277 gotX := pe.cursor().col 278 gotY := pe.cursor().row 279 if gotX != v.posX || gotY != v.posY { 280 t.Errorf("%s expect cursor at (%d,%d), got (%d,%d)\n", v.label, v.posY, v.posX, gotY, gotX) 281 } 282 case 10: 283 // validate the result against predict cell in target row 284 predictRow := pe.getOrMakeRow(v.posY, emu.GetWidth()) 285 i := 0 286 for _, ch := range v.result { 287 cell := predictRow.overlayCells[v.posX+i].replacement 288 if cell.String() != string(ch) { 289 t.Errorf("%s expect %q at (%d,%d), got %q\n", v.label, string(ch), v.posY, v.posX+i, cell) 290 t.Errorf("predict cell (%d,%d) is %q dw=%t, dwcont=%t\n", v.posY, v.posX+i, cell, cell.IsDoubleWidth(), cell.IsDoubleWidthCont()) 291 } 292 i += uniseg.StringWidth(string([]rune{ch})) 293 } 294 case 11, 7: 295 // validate predictionEpoch 296 if pe.predictionEpoch-epoch != 1 { 297 t.Errorf("%q expect %d, got %d, %d->%d\n", v.label, 1, pe.predictionEpoch-epoch, epoch, pe.predictionEpoch) 298 } 299 case 8: 300 // Never do nothing, just ignore it. 301 default: 302 t.Errorf("#test %q test failure. check the test case number.\n", v.label) 303 } 304 }) 305 } 306 } 307 308 func TestPredictionApply(t *testing.T) { 309 tc := []struct { 310 name string 311 row, col int // the specified row and col 312 base string // base content 313 predict string // prediction 314 result string // frame content 315 earlyReturn bool // apply early return 316 }{ 317 /*01*/ {"apply wrapped english input", 9, 75, "", "abcdef", "abcdef", false}, 318 /*02*/ {"apply wrapped chinese input", 10, 75, "", "柠檬水", "柠檬水", false}, 319 /*03*/ {"apply early return", 11, 70, "", "early return", "early return", true}, 320 } 321 322 pe := newPredictionEngine() 323 emu := terminal.NewEmulator3(80, 40, 40) 324 325 for k, v := range tc { 326 pe.Reset() 327 328 // set the base content 329 emu.MoveCursor(v.row, v.col) 330 emu.HandleStream(v.base) 331 332 if v.earlyReturn { 333 pe.SetDisplayPreference(Never) 334 } 335 336 // mimic user input for prediction engine 337 emu.MoveCursor(v.row, v.col) 338 pe.inputString(emu, v.predict) 339 // predictRow := pe.getOrMakeRow(v.row+1, emu.GetWidth()) 340 // predict := predictRow.overlayCells[0].replacement 341 // t.Logf("%q overlay at (%d,%d) is %q\n", v.name, v.row+1, 0, predict.GetContents()) 342 343 // mimic the result from server 344 emu.MoveCursor(v.row, v.col) 345 emu.HandleStream(v.result) 346 // cell := emu.GetMutableCell(v.row+1, 0) // cr to next row 347 // t.Logf("%q emulator at (%d,%d) is %q @%p\n", v.name, v.row+1, 0, cell.GetContents(), cell) 348 349 // apply to emulator 350 pe.cull(emu) 351 pe.apply(emu) 352 // t.Logf("%q apply at (%d,%d) is %q @%p\n", v.name, v.row+1, 0, cell.GetContents(), cell) 353 354 switch k { 355 case 0: 356 for i := 0; i < 5; i++ { 357 cell := emu.GetCell(v.row, v.col+i) 358 if string(v.predict[i]) != cell.GetContents() { 359 t.Errorf("%q expect %q at (%d,%d), got %q\n", v.name, v.predict[i], v.row, v.col+i, cell.GetContents()) 360 } 361 } 362 363 cell := emu.GetCell(v.row+1, 0) // cr to next row 364 if string(v.predict[5]) != cell.GetContents() { 365 t.Errorf("%q expect %q at (%d,%d), got %q\n", v.name, v.predict[5], v.row+1, 0, cell.GetContents()) 366 } 367 case 1: 368 i := 0 369 for _, ch := range "柠檬" { 370 cell := emu.GetCell(v.row, v.col+i*2) 371 if string(ch) != cell.GetContents() { 372 t.Errorf("%q expect %q at (%d,%d), got %q\n", v.name, ch, v.row, v.col+i*2, cell.GetContents()) 373 } 374 i++ 375 } 376 cell := emu.GetCell(v.row+1, 0) // cr to next row 377 if "水" != cell.GetContents() { 378 t.Errorf("%q expect %q at (%d,%d), got %q\n", v.name, "水", v.row+1, 0, cell.GetContents()) 379 } 380 case 2: // early return does nothing. 381 } 382 } 383 } 384 385 func printEmulatorCell(emu *terminal.Emulator, row, col int, sample string, prefix string) { 386 graphemes := uniseg.NewGraphemes(sample) 387 i := 0 388 for graphemes.Next() { 389 chs := graphemes.Runes() 390 391 cell := emu.GetCellPtr(row, col+i) 392 fmt.Printf("%s # cell %p (%d,%d) is %q\n", prefix, cell, row, col+i, cell) 393 i += uniseg.StringWidth(string(chs)) 394 } 395 } 396 397 func printPredictionCell(emu *terminal.Emulator, pe *PredictionEngine, row, col int, sample string, prefix string) { 398 predictRow := pe.getOrMakeRow(row, emu.GetWidth()) 399 graphemes := uniseg.NewGraphemes(sample) 400 i := 0 401 for graphemes.Next() { 402 chs := graphemes.Runes() 403 predict := &(predictRow.overlayCells[col+i]) 404 fmt.Printf("%s # predict cell %p (%d,%d) is %q active=%t, unknown=%t\n", 405 prefix, predict, row, col+i, predict.replacement, predict.active, predict.unknown) 406 i += uniseg.StringWidth(string(chs)) 407 } 408 } 409 410 func TestPrediction_NewUserInput_Backspace(t *testing.T) { 411 tc := []struct { 412 label string 413 row, col int // the specified row and col 414 base string // base content 415 predict string // prediction 416 lateAck uint64 // lateAck control the pending result 417 confirmedEpoch int64 // this control the appply result 418 expect string // the expect content 419 }{ 420 {"input backspace for simple cell", 0, 70, "", "abcde\x1B[D\x1B[D\x1B[D\x7f", 0, 4, "acde"}, 421 {"input backspace for wide cell", 1, 60, "", "abc太学生\x1B[D\x1B[D\x1B[D\x1B[C\x7f", 0, 4, "abc学生"}, 422 {"input backspace for wide cell with base", 2, 60, "东部战区", "\x1B[C\x1B[C\x7f", 0, 5, "东战区"}, 423 {"move cursor right, wide cell right edge", 3, 76, "平潭", "\x1B[C\x1B[C", 0, 5, "平潭"}, 424 {"move cursor left, wide cell left edge", 4, 0, "三号木", "\x1B[C\x1B[D\x1B[D", 0, 5, "三号木"}, 425 {"input backspace left edge", 5, 0, "小鸡腿", "\x1B[C\x7f\x7f", 0, 8, "鸡腿"}, 426 {"input backspace unknown case", 6, 74, "", "gocto\x1B[D\x1B[D\x7f\x7f", 0, 4, "gto"}, 427 {"backspace, predict unknown case", 7, 60, "", "捉鹰打goto\x7f\x7f\x7f\x7f鸟", 0, 4, "捉鹰打鸟"}, 428 } 429 430 emu := terminal.NewEmulator3(80, 40, 40) // TODO why we can't init emulator outside of for loop 431 pe := newPredictionEngine() 432 433 for _, v := range tc { 434 t.Run(v.label, func(t *testing.T) { 435 pe.Reset() 436 // t.Logf("%q predictionEpoch=%d\n", v.name, pe.predictionEpoch) 437 pe.predictionEpoch = 1 // TODO: when it's time to update predictionEpoch? 438 // fmt.Printf("%s base=%q expect=%q, pos=(%d,%d)\n", v.label, v.base, v.expect, emu.GetCursorRow(), emu.GetCursorCol()) 439 440 // set the base content 441 emu.MoveCursor(v.row, v.col) 442 emu.HandleStream(v.base) 443 // printEmulatorCell(emu, v.row, v.col, v.expect, "After Base") 444 445 // mimic user input for prediction engine 446 emu.MoveCursor(v.row, v.col) 447 pe.localFrameLateAcked = v.lateAck 448 pe.inputString(emu, v.predict) 449 // printPredictionCell(emu, pe, v.row, v.col, v.expect, "Predict") 450 451 // merge the last predict 452 pe.cull(emu) 453 // printPredictionCell(emu, pe, v.row, v.col, v.expect, "After Cull") 454 pe.confirmedEpoch = v.confirmedEpoch 455 pe.apply(emu) 456 // printEmulatorCell(emu, v.row, v.col, v.expect, "Merge") 457 458 // predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 459 i := 0 460 graphemes := uniseg.NewGraphemes(v.expect) 461 for graphemes.Next() { 462 chs := graphemes.Runes() 463 464 cell := emu.GetCell(v.row, v.col+i) 465 // fmt.Printf("#test %s (%d,%d) is %s\n", v.label, v.row, v.col+i, cell) 466 // predict := predictRow.overlayCells[v.col+i].replacement 467 if cell.String() != string(chs) { 468 t.Errorf("%s expect %q at (%d,%d), got cell %q dw=%t, dwcont=%t\n", 469 v.label, string(chs), v.row, v.col+i, cell, cell.IsDoubleWidth(), cell.IsDoubleWidthCont()) 470 } 471 472 i += uniseg.StringWidth(string(chs)) 473 } 474 }) 475 } 476 } 477 478 func TestPrediction_NewUserInput_Backspace_Overwrite(t *testing.T) { 479 tc := []struct { 480 label string 481 row, col int // the specified row and col 482 base string // base content 483 predict string // prediction 484 lateAck uint64 // lateAck control the pending result 485 confirmedEpoch int64 // this control the appply result 486 expect string // the expect content 487 }{ 488 {"input backspace for simple cell", 0, 70, "", "abcde\x1B[D\x1B[D\x1B[D\x7f", 0, 4, "a cde"}, 489 {"input backspace for wide cell", 1, 60, "", "abc太学生\x1B[D\x1B[D\x1B[D\x1B[C\x7f", 0, 4, "abc 学生"}, 490 {"input backspace for wide cell with base", 2, 60, "东部战区", "\x1B[C\x1B[C\x7f", 0, 5, "东 战区"}, 491 {"move cursor right, wide cell right edge", 3, 76, "平潭", "\x1B[C\x1B[C", 0, 5, "平潭"}, 492 {"move cursor left, wide cell left edge", 4, 0, "三号木", "\x1B[C\x1B[D\x1B[D", 0, 5, "三号木"}, 493 {"input backspace left edge", 5, 0, "小鸡腿", "\x1B[C\x7f", 0, 8, " 鸡腿"}, 494 {"input backspace unknown case", 6, 74, "", "gocto\x1B[D\x1B[D\x7f\x7f", 0, 4, "g to"}, 495 {"backspace, predict unknown case", 7, 60, "", "捉鹰打goto\x7f\x7f\x7f\x7f鸟", 0, 4, "捉鹰打鸟"}, 496 } 497 498 emu := terminal.NewEmulator3(80, 40, 40) 499 pe := newPredictionEngine() 500 pe.SetPredictOverwrite(true) // set predict overwrite 501 502 for _, v := range tc { 503 t.Run(v.label, func(t *testing.T) { 504 pe.Reset() 505 pe.predictionEpoch = 1 506 // fmt.Printf("%s base=%q expect=%q, pos=(%d,%d)\n", v.label, v.base, v.expect, v.row, v.col) 507 508 // set the base content 509 emu.MoveCursor(v.row, v.col) 510 emu.HandleStream(v.base) 511 // printEmulatorCell(emu, v.row, v.col, v.expect, "Base row") 512 513 // mimic user input for prediction engine 514 emu.MoveCursor(v.row, v.col) 515 pe.localFrameLateAcked = v.lateAck 516 pe.inputString(emu, v.predict) 517 // printPredictionCell(emu, pe, v.row, v.col, v.expect, "Predict row") 518 519 // merge the last predict 520 pe.cull(emu) 521 // printPredictionCell(emu, pe, v.row, v.col, v.expect, "After Cull") 522 pe.confirmedEpoch = v.confirmedEpoch 523 pe.apply(emu) 524 // printEmulatorCell(emu, v.row, v.col, v.expect, "Apply merge") 525 526 // predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 527 i := 0 528 graphemes := uniseg.NewGraphemes(v.expect) 529 for graphemes.Next() { 530 chs := graphemes.Runes() 531 532 cell := emu.GetCell(v.row, v.col+i) 533 // fmt.Printf("#test %q cell (%d,%d),cell=%s\n", v.label, v.row, v.col+i, cell) 534 if cell.String() != string(chs) { 535 t.Errorf("%s expect %q at (%d,%d), got cell %q dw=%t, dwcont=%t\n", 536 v.label, string(chs), v.row, v.col+i, cell, cell.IsDoubleWidth(), cell.IsDoubleWidthCont()) 537 } 538 539 i += uniseg.StringWidth(string(chs)) 540 } 541 }) 542 } 543 } 544 func TestPredictionActive(t *testing.T) { 545 tc := []struct { 546 name string 547 row, col int 548 content rune 549 result bool 550 }{ 551 {"no cursor, no cell prediction", -1, -1, ' ', false}, // test active() 552 {"no cursor, has cell prediction", 1, 0, ' ', true}, // test active() 553 {"has cursor, no cell", 3, 1, ' ', true}, // test active() 554 {"no cursor, has cell", 2, 0, 'n', true}, // test cursor() 555 } 556 557 pe := newPredictionEngine() 558 emu := terminal.NewEmulator3(80, 40, 40) 559 560 for k, v := range tc { 561 pe.Reset() 562 563 switch v.col { 564 case 0: 565 // add cell for col==0 566 predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 567 predict := &(predictRow.overlayCells[v.col]) 568 predict.active = true 569 predict.replacement = terminal.Cell{} 570 predict.replacement.SetContents([]rune{v.content}) 571 case 1: 572 // add cursor for col==1 573 pe.initCursor(emu) 574 } 575 576 switch v.content { 577 case 'n': 578 got := pe.cursor() 579 if got != nil { 580 t.Errorf("%q expect nil,got %p\n", v.name, got) 581 } 582 default: 583 got := pe.active() 584 if got != v.result { 585 t.Errorf("%q expect %t, got %t\n", v.name, v.result, got) 586 } 587 588 // jump the queue for waitTime() test case 589 if k == 1 { 590 // this is the perfect time to add waitTime test case 591 if pe.waitTime() != 50 { 592 t.Errorf("%q expect waitTime = %d, got %d\n", v.name, 50, pe.waitTime()) 593 } 594 } 595 } 596 } 597 } 598 599 func TestPredictionNewlineCarriageReturn(t *testing.T) { 600 tc := []struct { 601 name string 602 posY, posX int 603 predict string 604 gotY, gotX int 605 }{ 606 {"normal CR", 2, 3, "CR\x0D", 3, 0}, 607 {"bottom CR", 39, 0, "CR\x0D", 39, 0}, // TODO gap is too big, why? 608 } 609 pe := newPredictionEngine() 610 emu := terminal.NewEmulator3(80, 40, 40) 611 612 for _, v := range tc { 613 pe.Reset() 614 pe.predictionEpoch = 1 // reset it 615 616 // mimic user input for prediction engine 617 emu.MoveCursor(v.posY, v.posX) 618 pe.inputString(emu, v.predict) 619 pe.cull(emu) 620 621 // validate the cursor position 622 gotX := pe.cursor().col 623 gotY := pe.cursor().row 624 if gotX != v.gotX || gotY != v.gotY { 625 t.Errorf("%s expect cursor at (%d,%d), got (%d,%d)\n", v.name, v.gotY, v.gotX, gotY, gotX) 626 } 627 } 628 } 629 630 func printCursors(pe *PredictionEngine, prefix string) { 631 for i, cursor := range pe.cursors { 632 fmt.Printf("%q #cursor at (%d,%d) %p active=%t, tentativeUntilEpoch=%d\n", 633 prefix, cursor.row, cursor.col, &(pe.cursors[i]), cursor.active, cursor.tentativeUntilEpoch) 634 } 635 fmt.Printf("%q done\n\n", prefix) 636 } 637 638 func TestPredictionKillEpoch(t *testing.T) { 639 tc := struct { 640 name string 641 epoch int64 642 size int 643 }{"4 rows", 3, 4} 644 645 rows := []struct { 646 posY int 647 posX int 648 predict string 649 }{ 650 // rows: 0,5,9,10 651 {0, 0, "history\r\r\r\r\rchannel\r\r\r\rstarts\rworking"}, 652 } 653 654 pe := newPredictionEngine() 655 emu := terminal.NewEmulator3(80, 40, 40) 656 657 // printCursors(pe, "BEFORE newUserInput.") 658 // fill the rows 659 for _, v := range rows { 660 emu.MoveCursor(v.posY, v.posX) 661 pe.inputString(emu, v.predict) 662 // printPredictionCell(emu, pe, v.posY, v.posX, v.predict, "INPUT ") 663 } 664 pe.cull(emu) 665 666 // printCursors(pe, "AFTER newUserInput.") 667 668 // posYs := []int{0, 5, 9, 10} 669 // for _, posY := range posYs { 670 // printPredictionCell(emu, pe, posY, 0, "channel", "PREDICT -") 671 // } 672 673 // it should be 11 674 gotA := len(pe.cursors) 675 // fmt.Println("killEpoch #testing called it explicitily.") 676 pe.killEpoch(tc.epoch, emu) 677 678 // it should be 2 679 gotB := len(pe.cursors) 680 681 // printCursors(pe, "AFTER killEpoch.") 682 if gotB != 2 { 683 t.Errorf("%q A=%d, B=%d\n", tc.name, gotA, gotB) 684 } 685 } 686 687 func TestPredictionCull(t *testing.T) { 688 tc := []struct { 689 label string 690 row, col int // cursor start position 691 base string // base content 692 predict string // prediction 693 frame string // the expect content 694 displayPreference DisplayPreference // display preference 695 localFrameLateAcked uint64 // getValidity use localFrameLateAcked to validity cell or cursor prediction 696 localFrameSent uint64 // the cell prediction expirationFrame is set by localFrameSent+1 697 sendInterval uint 698 }{ 699 /* 0*/ {"displayPreference is never", 0, 0, "", "", "", Never, 0, 0, 0}, 700 /* 1*/ {"IncorrectOrExpired >confirmedEpoch, killEpoch()", 1, 70, "", "right", "wrong", Adaptive, 2, 1, 0}, 701 /* 2*/ {"IncorrectOrExpired <confirmedEpoch, Experimental, reset2()", 2, 72, "", "rig", "won", Experimental, 3, 2, 0}, 702 /* 3*/ {"IncorrectOrExpired <confirmedEpoch, Reset()", 3, 0, "", "right", "wrong", Adaptive, 4, 3, 0}, 703 /* 4*/ {"Correct", 4, 0, "", "correct正确", "correct正确", Adaptive, 5, 4, 0}, 704 /* 5*/ {"Correct validity, delay >250", 5, 0, "", "正确delay>250", "正确delay>250", Adaptive, 6, 5, 0}, 705 /* 6*/ {"Correct validity, delay >5000", 6, 0, "", "delay>5000", "delay>5000", Adaptive, 7, 6, 0}, 706 /* 7*/ {"Correct validity, sendInterval=40", 7, 0, "", "sendInterval=40", "sendInterval=40", Adaptive, 8, 7, 40}, 707 /* 8*/ {"Correct validity, sendInterval=20", 8, 0, "", "sendInterval=20", "sendInterval=20", Adaptive, 9, 8, 20}, 708 /* 9*/ {"Correct validity + wrong cursor", 9, 0, "", "wrong cursor", "wrong cursor", Adaptive, 10, 9, 0}, 709 /*10*/ {"Correct validity + wrong cursor + Experimental", 10, 0, "", "wrong cursor + Experimental", "wrong cursor + Experimental", Experimental, 11, 10, 0}, 710 /*11*/ {"wrong row", 40, 0, "", "wrong row", "wrong row", Adaptive, 12, 11, 0}, 711 /*12*/ {"IncorrectOrExpired + >confirmedEpoch + Experimental", 12, 0, "", "Epoch", "confi", Experimental, 13, 12, 0}, 712 } 713 emu := terminal.NewEmulator3(80, 40, 40) 714 pe := newPredictionEngine() 715 716 for k, v := range tc { 717 t.Run(v.label, func(t *testing.T) { 718 // fmt.Printf("\n%q #testing call cull A.\n", v.name) 719 pe.SetDisplayPreference(v.displayPreference) 720 721 // set the base content 722 emu.MoveCursor(v.row, v.col) 723 // fmt.Printf("#test cull %q HandleStream()\n", v.label) 724 emu.HandleStream(v.base) 725 726 // mimic user input for prediction engine 727 emu.MoveCursor(v.row, v.col) 728 pe.SetLocalFrameSent(v.localFrameSent) 729 730 // fmt.Printf("#test %q cull B1. localFrameSend=%d, localFrameLateAcked=%d, predictionEpoch=%d, confirmedEpoch=%d\n", 731 // v.label, pe.localFrameSent, pe.localFrameLateAcked, pe.predictionEpoch, pe.confirmedEpoch) 732 733 // cull will be called for each rune, except last rune 734 switch k { 735 case 5: 736 delay := []int{0, 0, 251, 0, 0, 0, 0, 0, 0} 737 pe.inputString(emu, v.predict, delay...) 738 case 6: 739 delay := []int{0, 0, 5001, 0, 0, 0, 0, 0, 0} 740 pe.inputString(emu, v.predict, delay...) 741 case 7: 742 pe.SetSendInterval(v.sendInterval) 743 pe.inputString(emu, v.predict) 744 case 8: 745 pe.SetSendInterval(v.sendInterval) 746 pe.inputString(emu, v.predict) 747 case 11: 748 pe.Reset() // clear the previous rows 749 pe.getOrMakeRow(v.row, emu.GetWidth()) // add the illegal row 750 case 12: 751 // fmt.Printf("#test before inputString() %q confirmedEpoch=%d\n", v.label, pe.confirmedEpoch) 752 now := time.Now().UnixMilli() 753 for _, ch := range v.predict { 754 pe.handleUserGrapheme(emu, now, ch) 755 } 756 // fmt.Printf("#test after inputString() %q confirmedEpoch=%d\n", v.label, pe.confirmedEpoch) 757 default: 758 pe.inputString(emu, v.predict) 759 } 760 // fmt.Printf("#test %q cull B2. localFrameSend=%d, localFrameLateAcked=%d, predictionEpoch=%d, confirmedEpoch=%d\n", 761 // v.label, pe.localFrameSent, pe.localFrameLateAcked, pe.predictionEpoch, pe.confirmedEpoch) 762 763 // mimic the result from server 764 emu.MoveCursor(v.row, v.col) 765 emu.HandleStream(v.frame) 766 767 switch k { 768 case 9, 10: 769 emu.MoveCursor(v.row, v.col+1) 770 } 771 772 pe.SetLocalFrameLateAcked(v.localFrameLateAcked) 773 pe.cull(emu) 774 // fmt.Printf("#test %q cull B3. localFrameSend=%d, localFrameLateAcked=%d, predictionEpoch=%d, confirmedEpoch=%d\n", 775 // v.label, pe.localFrameSent, pe.localFrameLateAcked, pe.predictionEpoch, pe.confirmedEpoch) 776 777 switch k { 778 case 1: 779 // validate the result of killEpoch 780 if len(pe.overlays) == 1 && len(pe.cursors) == 0 { 781 // after killEpoch, cull() remove the last cursor because it's correct 782 break 783 } else { 784 t.Errorf("%q should call killEpoch. got overlays=%d, cursors=%d\n", v.label, len(pe.overlays), len(pe.cursors)) 785 } 786 case 6: 787 if !pe.flagging { 788 t.Errorf("%q expect true for flagging, got %t\n", v.label, pe.flagging) 789 } 790 fallthrough 791 case 5: 792 if pe.glitchTrigger == 0 { 793 t.Errorf("%q glitchTrigger should >0, got %d\n", v.label, pe.glitchTrigger) 794 } 795 fallthrough 796 case 2, 4, 12: 797 // validate the result of cell reset2 798 predictRow := pe.getOrMakeRow(v.row, emu.GetWidth()) 799 for i := range v.frame { 800 predict := &(predictRow.overlayCells[v.col+i]) 801 if predict.active { 802 t.Errorf("%q should not be active, got active=%t\n", v.label, predict.active) 803 } 804 } 805 if k == 12 { 806 if pe.confirmedEpoch != 2 { 807 t.Errorf("%q expect confirmedEpoch < tentativeUntilEpoch. got %d\n", v.label, pe.confirmedEpoch) 808 } 809 } 810 811 case 7: 812 if !pe.flagging { 813 t.Errorf("%q expect true for flagging, got %t\n", v.label, pe.flagging) 814 } 815 case 8: 816 if pe.srttTrigger { 817 t.Errorf("%q expect false for srttTrigger, got %t\n", v.label, pe.srttTrigger) 818 } 819 case 10: 820 if len(pe.cursors) != 0 { 821 t.Errorf("%q expect clean cursor prediction, got %d\n", v.label, len(pe.cursors)) 822 } 823 case 11: 824 if len(pe.overlays) != 0 { 825 t.Errorf("%q expect zero rows, got %d\n", v.label, len(pe.overlays)) 826 } 827 default: 828 // validate pe.Reset() 829 if len(pe.overlays) != 0 || len(pe.cursors) != 0 { 830 t.Errorf("%s the engine should be reset. got overlays=%d, cursors=%d\n", v.label, len(pe.overlays), len(pe.cursors)) 831 } 832 } 833 }) 834 } 835 } 836 837 func TestPredictionNewInput(t *testing.T) { 838 emu := terminal.NewEmulator3(80, 40, 40) 839 pe := newPredictionEngine() 840 841 pe.NewUserInput(emu, []rune{}) 842 // the pe and emu doesn't change so we don't validate the result. 843 } 844 845 func TestSetLocalFrameAcked(t *testing.T) { 846 pe := newPredictionEngine() 847 848 var expect uint64 = 7 849 pe.SetLocalFrameAcked(expect) 850 851 if pe.localFrameAcked != expect { 852 t.Errorf("#test SetLocalFrameAcked expect %d, got %d\n", expect, pe.localFrameAcked) 853 } 854 } 855 856 func TestTitleEngine(t *testing.T) { 857 tc := []struct { 858 name string 859 prefix string 860 result string 861 }{ 862 {"english title", " - aprish", " - aprish"}, 863 {"chinese title", "终端模拟器", "终端模拟器 - aprish"}, 864 } 865 te := TitleEngine{} 866 emu := terminal.NewEmulator3(80, 40, 40) 867 for _, v := range tc { 868 te.setPrefix(v.prefix) 869 te.apply(emu) 870 871 got := emu.GetWindowTitle() 872 if v.result != got { 873 t.Errorf("%q window title expect %q, got %q\n", v.name, v.result, got) 874 } 875 got = emu.GetIconLabel() 876 if v.result != got { 877 t.Errorf("%q icon name expect %q, got %q\n", v.name, v.result, got) 878 } 879 } 880 881 omTitle := " [aprish]" 882 om := NewOverlayManager() 883 om.SetTitlePrefix(omTitle) 884 885 if om.title.prefix != omTitle { 886 t.Errorf("jump the queue, expect %q, got %q\n", omTitle, om.title.prefix) 887 } 888 } 889 890 func TestNotificationEngine(t *testing.T) { 891 tc := []struct { 892 name string 893 permanent bool 894 lastWordFromServer int64 // delta value based on now 895 lastAckedState int64 // delta value base on now 896 message string 897 escapeKeyString string 898 messageIsNetworkError bool 899 showQuitKeystroke bool 900 result string 901 }{ 902 {"no message, no expire", false, 60, 80, "", "Ctrl-z", false, true, ""}, 903 { 904 "english message, no expire", false, 60, 80, "hello world", "Ctrl-z", false, true, 905 "aprish: hello world [To quit: Ctrl-z .]", 906 }, 907 {"chinese message, no expire", true, 60, 80, "你好世界", "Ctrl-z", false, false, "aprish: 你好世界"}, 908 { 909 "server late", true, 65001, 80, "你好世界", "Ctrl-z", false, false, 910 "aprish: 你好世界 (1:05 without contact.)", 911 }, 912 { 913 "reply late", false, 65, 10001, "aia group", "Ctrl-z", false, true, 914 "aprish: aia group (10 s without reply.) [To quit: Ctrl-z .]", 915 }, 916 { 917 "no message, server late", false, 65001, 10001, "top gun 2", "Ctrl-z", false, true, 918 "aprish: top gun 2 (1:05 without contact.) [To quit: Ctrl-z .]", 919 }, 920 { 921 "no message, server too late", false, 3802001, 100, "top gun 2", "Ctrl-z", false, true, 922 "aprish: top gun 2 (1:03:22 without contact.) [To quit: Ctrl-z .]", 923 }, 924 { 925 "network error", false, 200, 10001, "***", "Ctrl-z", true, true, 926 "aprish: network error (10 s without reply.) [To quit: Ctrl-z .]", 927 }, 928 { 929 "restore from network failure", false, 200, 20001, "restor from", "Ctrl-z", false, true, 930 "aprish: restor from (20 s without reply.) [To quit: Ctrl-z .]", 931 }, 932 { 933 "no message, server late", false, 65001, 20001, "", "Ctrl-z", false, true, 934 "aprish: Last contact 1:05 ago. [To quit: Ctrl-z .]", 935 }, 936 } 937 938 ne := newNotificationEngien() 939 emu := terminal.NewEmulator3(80, 40, 40) 940 for _, v := range tc { 941 // fmt.Printf("%s start\n", v.name) 942 if !ne.messageIsNetworkError { 943 ne.SetNotificationString(v.message, v.permanent, v.showQuitKeystroke) 944 } 945 ne.SetEscapeKeyString(v.escapeKeyString) 946 ne.ServerHeard(time.Now().UnixMilli() - v.lastWordFromServer) 947 ne.ServerAcked(time.Now().UnixMilli() - v.lastAckedState) 948 949 if v.messageIsNetworkError { 950 ne.SetNetworkError(v.name) 951 } else { 952 ne.ClearNetworkError() 953 ne.SetNotificationString(v.message, v.permanent, v.showQuitKeystroke) 954 } 955 956 ne.apply(emu) 957 958 // build the string from emulator 959 var got strings.Builder 960 for i := 0; i < emu.GetWidth(); i++ { 961 cell := emu.GetCell(0, i) 962 if cell.IsDoubleWidthCont() { 963 continue 964 } 965 966 got.WriteString(cell.GetContents()) 967 } 968 969 // validate the result 970 if len(v.result) != 0 { 971 gotStr := strings.TrimSpace(got.String()) 972 if gotStr != v.result { 973 t.Errorf("%q expect \n%q, got \n%q\n", v.name, v.result, gotStr) 974 } 975 } 976 // fmt.Printf("%s end\n\n", v.name) 977 } 978 } 979 980 func TestNotificationEngine_adjustMessage(t *testing.T) { 981 tc := []struct { 982 name string 983 message string 984 messageExpiration int64 985 expect string 986 }{ 987 {"message expire", "message expire", 0, ""}, 988 {"message ready", "message 准备好了", 20, "message 准备好了"}, 989 } 990 991 ne := newNotificationEngien() 992 for _, v := range tc { 993 ne.SetNotificationString(v.message, false, false) 994 995 // validate the message string 996 if ne.GetNotificationString() != v.message { 997 t.Errorf("%q expect %q, got %q\n", v.name, v.message, ne.GetNotificationString()) 998 } 999 1000 ne.messageExpiration = time.Now().UnixMilli() + v.messageExpiration 1001 ne.adjustMessage() 1002 1003 // validate the empty string 1004 if ne.GetNotificationString() != v.expect { 1005 t.Errorf("%q expect %q, got %q\n", v.name, v.expect, ne.GetNotificationString()) 1006 } 1007 } 1008 1009 if min(7, 8) == 8 { 1010 t.Errorf("min should return %d, for min(7,8), got %d\n", 7, 8) 1011 } 1012 } 1013 1014 func TestOverlayManager_waitTime(t *testing.T) { 1015 tc := []struct { 1016 name string 1017 lastWordFromServer int64 // delta value based on now 1018 lastAckedState int64 // delta value base on now 1019 messageExpiration int64 // delta value base on now 1020 expect int 1021 }{ 1022 {"reply late", 600, 10001, 4000, 1000}, 1023 {"server late", 65001, 100, 4000, 3000}, 1024 {"no server late, no reply late", 65, 100, 400, 400}, 1025 } 1026 1027 om := NewOverlayManager() 1028 for _, v := range tc { 1029 ne := om.GetNotificationEngine() 1030 ne.ServerHeard(time.Now().UnixMilli() - v.lastWordFromServer) 1031 ne.ServerAcked(time.Now().UnixMilli() - v.lastAckedState) 1032 1033 ne.messageExpiration = time.Now().UnixMilli() + v.messageExpiration 1034 1035 got := om.WaitTime() 1036 if got != v.expect { 1037 t.Errorf("%q expect waitTime=%d, got %d\n", v.name, v.expect, got) 1038 } 1039 } 1040 } 1041 1042 func TestOverlayManager_apply(t *testing.T) { 1043 om := NewOverlayManager() 1044 emu := terminal.NewEmulator3(80, 40, 40) 1045 om.GetPredictionEngine() 1046 1047 // all the components of OverlayManager has been tested by previouse test case 1048 // add this for coverage 100% 1049 om.Apply(emu) 1050 } 1051 1052 // add this method for test purpose 1053 func (pe *PredictionEngine) inputString(emu *terminal.Emulator, str string, delay ...int) { 1054 var input []rune 1055 1056 index := 0 1057 graphemes := uniseg.NewGraphemes(str) 1058 for graphemes.Next() { 1059 input = graphemes.Runes() 1060 if len(delay) > index { // delay parameters is provided to simulate network delay 1061 pause := time.Duration(delay[index]) 1062 // fmt.Printf("#test inputString delay %dms.\n", pause) 1063 time.Sleep(time.Millisecond * pause) 1064 index++ 1065 } 1066 // fmt.Printf("#test inputString() user input %s\n", string(input)) 1067 pe.NewUserInput(emu, input) 1068 } 1069 } 1070 1071 func TestOverlayCellResetWithOrig(t *testing.T) { 1072 emu := terminal.NewEmulator3(80, 40, 40) 1073 pe := newPredictionEngine() 1074 1075 emu.MoveCursor(1, 0) 1076 pe.initCursor(emu) 1077 1078 theRow := pe.getOrMakeRow(pe.cursor().row, emu.GetWidth()) 1079 cell := &(theRow.overlayCells[0]) 1080 1081 /* 1082 here is the sample output: 1083 1084 #test before resetWithOrig replacement=, active=false, originalContents=[], size=0, unknown=false 1085 #test before resetWithOrig replacement=, active=false, originalContents=[], size=0, unknown=false 1086 #test before resetWithOrig replacement=, active=false, originalContents=[], size=1, unknown=false 1087 */ 1088 got1 := fmt.Sprintf("#test before resetWithOrig replacement=%s, active=%t, originalContents=%s, size=%d, unknown=%t\n", 1089 cell.replacement, cell.active, cell.originalContents, len(cell.originalContents), cell.unknown) 1090 1091 cell.active = false 1092 cell.unknown = false 1093 cell.resetWithOrig() 1094 got2 := fmt.Sprintf("#test before resetWithOrig replacement=%s, active=%t, originalContents=%s, size=%d, unknown=%t\n", 1095 cell.replacement, cell.active, cell.originalContents, len(cell.originalContents), cell.unknown) 1096 1097 // validate the reset2 is called 1098 if got1 != got2 { 1099 t.Errorf("#test resetWithOrig() expect %s, got %s\n", got1, got2) 1100 } 1101 1102 cell.active = true 1103 cell.unknown = false 1104 cell.resetWithOrig() 1105 got3 := fmt.Sprintf("#test before resetWithOrig replacement=%s, active=%t, originalContents=%s, size=%d, unknown=%t\n", 1106 cell.replacement, cell.active, cell.originalContents, len(cell.originalContents), cell.unknown) 1107 1108 key := "size=1" 1109 if !strings.Contains(got3, key) { 1110 t.Errorf("#test resetWithOrig() expect %s, got %s\n", key, got3) 1111 } 1112 } 1113 1114 func TestOverlayCellString(t *testing.T) { 1115 cell := newConditionalOverlayCell(12, 5, 1) 1116 1117 got := cell.String() 1118 pieces := []string{"{repl:", "orig:", "unknown:", "active:", "}"} 1119 1120 found := 0 1121 for i := range pieces { 1122 if strings.Contains(got, pieces[i]) { 1123 found++ 1124 } 1125 } 1126 1127 if found != len(pieces) { 1128 t.Errorf("#test conditionalOverlayCell String() method expect %s, got %s\n", pieces, &cell) 1129 } 1130 }