src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/highlight/highlighter_test.go (about) 1 package highlight 2 3 import ( 4 "reflect" 5 "strings" 6 "testing" 7 "time" 8 9 "src.elv.sh/pkg/diag" 10 "src.elv.sh/pkg/eval" 11 "src.elv.sh/pkg/parse" 12 "src.elv.sh/pkg/testutil" 13 "src.elv.sh/pkg/tt" 14 "src.elv.sh/pkg/ui" 15 ) 16 17 var any = anyMatcher{} 18 var noTips []ui.Text 19 20 var styles = ui.RuneStylesheet{ 21 '?': ui.Stylings(ui.FgBrightWhite, ui.BgRed), 22 '$': ui.FgMagenta, 23 '\'': ui.FgYellow, 24 'v': ui.FgGreen, 25 } 26 27 func TestHighlighter_HighlightRegions(t *testing.T) { 28 // Force commands to be delivered synchronously. 29 testutil.Set(t, &maxBlockForLate, testutil.Scaled(100*time.Millisecond)) 30 hl := NewHighlighter(Config{ 31 HasCommand: func(name string) bool { return name == "ls" }, 32 }) 33 34 tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), 35 Args("ls").Rets( 36 ui.MarkLines( 37 "ls", styles, 38 "vv", 39 ), 40 noTips), 41 Args(" ls\n").Rets( 42 ui.MarkLines( 43 " ls\n", styles, 44 " vv"), 45 noTips), 46 Args("ls $x 'y'").Rets( 47 ui.MarkLines( 48 "ls $x 'y'", styles, 49 "vv $$ '''"), 50 noTips), 51 // Non-bareword commands do not go through command highlighting. 52 Args("'ls'").Rets(ui.T("'ls'", ui.FgYellow)), 53 Args("a$x").Rets( 54 ui.MarkLines( 55 "a$x", styles, 56 " $$"), 57 noTips, 58 ), 59 ) 60 } 61 62 func TestHighlighter_ParseErrors(t *testing.T) { 63 hl := NewHighlighter(Config{}) 64 tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), 65 // Parse error is highlighted and returned 66 Args("ls ]").Rets( 67 ui.MarkLines( 68 "ls ]", styles, 69 "vv ?"), 70 matchTexts("1:4")), 71 // Multiple parse errors 72 Args("ls $? ]").Rets( 73 ui.MarkLines( 74 "ls $? ]", styles, 75 "vv $? ?"), 76 matchTexts("1:5", "1:7")), 77 // Errors at the end are ignored 78 Args("ls $").Rets(any, noTips), 79 Args("ls [").Rets(any, noTips), 80 ) 81 } 82 83 func TestHighlighter_AutofixesAndCheckErrors(t *testing.T) { 84 ev := eval.NewEvaler() 85 ev.AddModule("mod1", &eval.Ns{}) 86 hl := NewHighlighter(Config{ 87 Check: func(t parse.Tree) (string, []diag.RangeError) { 88 autofixes, err := ev.CheckTree(t, nil) 89 compErrors := eval.UnpackCompilationErrors(err) 90 rangeErrors := make([]diag.RangeError, len(compErrors)) 91 for i, compErr := range compErrors { 92 rangeErrors[i] = compErr 93 } 94 return strings.Join(autofixes, "; "), rangeErrors 95 }, 96 AutofixTip: func(s string) ui.Text { return ui.T("autofix: " + s) }, 97 }) 98 99 tt.Test(t, tt.Fn(hl.Get).Named("hl.Get"), 100 // Check error is highlighted and returned 101 Args("ls $a").Rets( 102 ui.MarkLines( 103 "ls $a", styles, 104 "vv ??"), 105 matchTexts("1:4")), 106 // Multiple check errors 107 Args("ls $a $b").Rets( 108 ui.MarkLines( 109 "ls $a $b", styles, 110 "vv ?? ??"), 111 matchTexts("1:4", "1:7")), 112 // Check errors at the end are ignored 113 Args("set _").Rets(any, noTips), 114 115 // Autofix 116 Args("nop $mod1:").Rets( 117 ui.MarkLines( 118 "nop $mod1:", styles, 119 "vvv ??????"), 120 matchTexts( 121 "1:5", // error 122 "autofix: use mod1", // autofix 123 )), 124 ) 125 } 126 127 type c struct { 128 given string 129 wantInitial ui.Text 130 wantLate ui.Text 131 mustLate bool 132 } 133 134 var lateTimeout = testutil.Scaled(100 * time.Millisecond) 135 136 func testThat(t *testing.T, hl *Highlighter, c c) { 137 initial, _ := hl.Get(c.given) 138 if !reflect.DeepEqual(c.wantInitial, initial) { 139 t.Errorf("want %v from initial Get, got %v", c.wantInitial, initial) 140 } 141 if c.wantLate == nil { 142 return 143 } 144 select { 145 case <-hl.LateUpdates(): 146 late, _ := hl.Get(c.given) 147 if !reflect.DeepEqual(c.wantLate, late) { 148 t.Errorf("want %v from late Get, got %v", c.wantLate, late) 149 } 150 case <-time.After(lateTimeout): 151 t.Errorf("want %v from LateUpdates, but timed out after %v", 152 c.wantLate, lateTimeout) 153 } 154 } 155 156 func TestHighlighter_HasCommand_LateResult_Async(t *testing.T) { 157 // When the HasCommand callback takes longer than maxBlockForLate, late 158 // results are delivered asynchronously. 159 testutil.Set(t, &maxBlockForLate, testutil.Scaled(time.Millisecond)) 160 hl := NewHighlighter(Config{ 161 // HasCommand is slow and only recognizes "ls". 162 HasCommand: func(cmd string) bool { 163 time.Sleep(testutil.Scaled(10 * time.Millisecond)) 164 return cmd == "ls" 165 }}) 166 167 testThat(t, hl, c{ 168 given: "ls", 169 wantInitial: ui.T("ls"), 170 wantLate: ui.T("ls", ui.FgGreen), 171 }) 172 testThat(t, hl, c{ 173 given: "echo", 174 wantInitial: ui.T("echo"), 175 wantLate: ui.T("echo", ui.FgRed), 176 }) 177 } 178 179 func TestHighlighter_HasCommand_LateResult_Sync(t *testing.T) { 180 // When the HasCommand callback takes shorter than maxBlockForLate, late 181 // results are delivered asynchronously. 182 testutil.Set(t, &maxBlockForLate, testutil.Scaled(100*time.Millisecond)) 183 hl := NewHighlighter(Config{ 184 // HasCommand is fast and only recognizes "ls". 185 HasCommand: func(cmd string) bool { 186 time.Sleep(testutil.Scaled(time.Millisecond)) 187 return cmd == "ls" 188 }}) 189 190 testThat(t, hl, c{ 191 given: "ls", 192 wantInitial: ui.T("ls", ui.FgGreen), 193 }) 194 testThat(t, hl, c{ 195 given: "echo", 196 wantInitial: ui.T("echo", ui.FgRed), 197 }) 198 } 199 200 func TestHighlighter_HasCommand_LateResultOutOfOrder(t *testing.T) { 201 // When late results are delivered out of order, the ones that do not match 202 // the current code are dropped. In this test, hl.Get is called with "l" 203 // first and then "ls". The late result for "l" is delivered after that of 204 // "ls" and is dropped. 205 206 // Make sure that the HasCommand callback takes longer than maxBlockForLate. 207 testutil.Set(t, &maxBlockForLate, testutil.Scaled(time.Millisecond)) 208 209 hlSecond := make(chan struct{}) 210 hl := NewHighlighter(Config{ 211 HasCommand: func(cmd string) bool { 212 if cmd == "l" { 213 // Make sure that the second highlight has been requested before 214 // returning. 215 <-hlSecond 216 time.Sleep(testutil.Scaled(10 * time.Millisecond)) 217 return false 218 } 219 time.Sleep(testutil.Scaled(10 * time.Millisecond)) 220 close(hlSecond) 221 return cmd == "ls" 222 }}) 223 224 hl.Get("l") 225 226 testThat(t, hl, c{ 227 given: "ls", 228 wantInitial: ui.T("ls"), 229 wantLate: ui.T("ls", ui.FgGreen), 230 mustLate: true, 231 }) 232 233 // Make sure that no more late updates are delivered. 234 select { 235 case late := <-hl.LateUpdates(): 236 t.Errorf("want nothing from LateUpdates, got %v", late) 237 case <-time.After(testutil.Scaled(50 * time.Millisecond)): 238 // We have waited for 50 ms and there are no late updates; test passes. 239 } 240 } 241 242 // Matchers. 243 244 type anyMatcher struct{} 245 246 func (anyMatcher) Match(tt.RetValue) bool { return true } 247 248 type textsMatcher struct{ substrings []string } 249 250 func matchTexts(s ...string) textsMatcher { return textsMatcher{s} } 251 252 func (m textsMatcher) Match(v tt.RetValue) bool { 253 texts := v.([]ui.Text) 254 if len(texts) != len(m.substrings) { 255 return false 256 } 257 for i, text := range texts { 258 if !strings.Contains(text.String(), m.substrings[i]) { 259 return false 260 } 261 } 262 return true 263 }