package grammar import ( "bufio" "bytes" "testing" "time" ) func TestFrameRoundTrip(t *testing.T) { var buf bytes.Buffer bodies := [][]byte{ []byte(`{"jsonrpc":"2.0","id":1}`), []byte(`{"method":"textDocument/didOpen"}`), []byte(`{}`), } for _, b := range bodies { if err := writeFrame(&buf, b); err != nil { t.Fatalf("writeFrame: %v", err) } } r := bufio.NewReader(&buf) for i, want := range bodies { got, err := readFrame(r) if err != nil { t.Fatalf("readFrame %d: %v", i, err) } if !bytes.Equal(got, want) { t.Errorf("frame %d = %q, want %q", i, got, want) } } } func TestReadFrameCaseInsensitiveHeader(t *testing.T) { // Some servers vary header casing; the length header must still parse. raw := "content-length: 2\r\n\r\n{}" got, err := readFrame(bufio.NewReader(bytes.NewBufferString(raw))) if err != nil { t.Fatalf("readFrame: %v", err) } if string(got) != "{}" { t.Errorf("got %q, want {}", got) } } func TestUTF16ToRuneCol(t *testing.T) { cases := []struct { line string u16 int want int }{ {"hello", 0, 0}, {"hello", 3, 3}, {"hello", 99, 5}, // clamp past end {"a😀b", 0, 0}, // emoji is 2 UTF-16 units, 1 rune {"a😀b", 1, 1}, // before the emoji {"a😀b", 3, 2}, // after the emoji (1 + 2 units) -> rune col 2 } for _, c := range cases { if got := utf16ToRuneCol(c.line, c.u16); got != c.want { t.Errorf("utf16ToRuneCol(%q, %d) = %d, want %d", c.line, c.u16, got, c.want) } } } func TestLSPRangeToDiagsSingleLine(t *testing.T) { lines := []string{"This is a a sentence."} got := lspRangeToDiags(lines, 0, 8, 0, 11, "Did you mean to repeat this word?", "RepeatedWords") if len(got) != 1 { t.Fatalf("got %d diags, want 1", len(got)) } d := got[0] if d.Line != 0 || d.StartCol != 8 || d.EndCol != 11 { t.Errorf("range = line %d [%d,%d), want line 0 [8,11)", d.Line, d.StartCol, d.EndCol) } if d.Code != "RepeatedWords" { t.Errorf("code = %q", d.Code) } } func TestLSPRangeToDiagsMultiLine(t *testing.T) { lines := []string{"first line", "second", "third line"} got := lspRangeToDiags(lines, 0, 6, 2, 5, "m", "C") if len(got) != 3 { t.Fatalf("got %d diags, want 3 (one per covered line)", len(got)) } // start line: from col 6 to end (10); middle: full; end: 0..5 if got[0].StartCol != 6 || got[0].EndCol != 10 { t.Errorf("start line range [%d,%d), want [6,10)", got[0].StartCol, got[0].EndCol) } if got[1].StartCol != 0 || got[1].EndCol != 6 { t.Errorf("middle line range [%d,%d), want [0,6)", got[1].StartCol, got[1].EndCol) } if got[2].StartCol != 0 || got[2].EndCol != 5 { t.Errorf("end line range [%d,%d), want [0,5)", got[2].StartCol, got[2].EndCol) } } func TestLSPRangeToDiagsSkipsOutOfRangeAndEmpty(t *testing.T) { lines := []string{"only line"} // Line 5 doesn't exist -> skipped, no panic. if got := lspRangeToDiags(lines, 5, 0, 5, 3, "m", "C"); got != nil { t.Errorf("out-of-range line produced %v, want nil", got) } // Zero-width range -> no diag. if got := lspRangeToDiags(lines, 0, 2, 0, 2, "m", "C"); got != nil { t.Errorf("zero-width range produced %v, want nil", got) } } func TestCodeString(t *testing.T) { if got := codeString("RepeatedWords"); got != "RepeatedWords" { t.Errorf("string code = %q", got) } if got := codeString(float64(42)); got != "42" { t.Errorf("number code = %q, want 42", got) } if got := codeString(nil); got != "" { t.Errorf("nil code = %q, want empty", got) } } // TestLiveHarper exercises the real harper-ls end to end. Skipped when the binary // is absent or under -short, so CI without harper stays green. func TestLiveHarper(t *testing.T) { if testing.Short() { t.Skip("skipping live harper test under -short") } if !Available() { t.Skip("harper-ls not on PATH") } c, err := Start() if err != nil { t.Fatalf("Start: %v", err) } defer c.Close() // "a a" is a repeated word; harper should flag it. c.DidOpen("/tmp/glint-grammar-test.md", "This is a a test.\n") select { case batch := <-c.Diagnostics(): if len(batch) == 0 { t.Fatal("harper returned an empty diagnostic batch") } found := false for _, d := range batch { if d.Line == 0 && d.Message != "" { found = true } } if !found { t.Errorf("no usable diagnostic in batch: %+v", batch) } case <-time.After(10 * time.Second): t.Fatal("timed out waiting for harper diagnostics") } }