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  }