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  }