Skip to main content
Glama

MCP Language Server

utilities_test.go12.6 kB
package tools import ( "fmt" "os" "strings" "testing" "github.com/isaacphi/mcp-language-server/internal/protocol" "github.com/stretchr/testify/assert" ) // Save original ReadFile function var originalReadFile = os.ReadFile // Create a function we can monkeypatch func readFileHelper(name string) ([]byte, error) { return originalReadFile(name) } // Mock implementation that can be changed in tests var readFileFunc = readFileHelper // Create a modified version of ExtractTextFromLocation that uses our mockable function func extractTextFromLocationForTest(loc protocol.Location) (string, error) { path := strings.TrimPrefix(string(loc.URI), "file://") content, err := readFileFunc(path) if err != nil { return "", fmt.Errorf("failed to read file: %w", err) } lines := strings.Split(string(content), "\n") startLine := int(loc.Range.Start.Line) endLine := int(loc.Range.End.Line) if startLine < 0 || startLine >= len(lines) || endLine < 0 || endLine >= len(lines) { return "", fmt.Errorf("invalid Location range: %v", loc.Range) } // Handle single-line case if startLine == endLine { line := lines[startLine] startChar := int(loc.Range.Start.Character) endChar := int(loc.Range.End.Character) if startChar < 0 || startChar > len(line) || endChar < 0 || endChar > len(line) { return "", fmt.Errorf("invalid character range: %v", loc.Range) } return line[startChar:endChar], nil } // Handle multi-line case var result strings.Builder // First line firstLine := lines[startLine] startChar := int(loc.Range.Start.Character) if startChar < 0 || startChar > len(firstLine) { return "", fmt.Errorf("invalid start character: %v", loc.Range.Start) } result.WriteString(firstLine[startChar:]) // Middle lines for i := startLine + 1; i < endLine; i++ { result.WriteString("\n") result.WriteString(lines[i]) } // Last line lastLine := lines[endLine] endChar := int(loc.Range.End.Character) if endChar < 0 || endChar > len(lastLine) { return "", fmt.Errorf("invalid end character: %v", loc.Range.End) } result.WriteString("\n") result.WriteString(lastLine[:endChar]) return result.String(), nil } func TestExtractTextFromLocation_SingleLine(t *testing.T) { mockContent := "function testFunction() {\n return 'test';\n}" // Store original function and restore after test originalFunc := readFileFunc defer func() { readFileFunc = originalFunc }() // Set up mock implementation readFileFunc = func(name string) ([]byte, error) { return []byte(mockContent), nil } location := protocol.Location{ URI: "file:///path/to/file.js", Range: protocol.Range{ Start: protocol.Position{Line: 0, Character: 9}, End: protocol.Position{Line: 0, Character: 21}, }, } result, err := extractTextFromLocationForTest(location) assert.NoError(t, err) assert.Equal(t, "testFunction", result) } func TestExtractTextFromLocation_MultiLine(t *testing.T) { mockContent := "function testFunction() {\n return 'test';\n}" // Store original function and restore after test originalFunc := readFileFunc defer func() { readFileFunc = originalFunc }() // Set up mock implementation readFileFunc = func(name string) ([]byte, error) { return []byte(mockContent), nil } location := protocol.Location{ URI: "file:///path/to/file.js", Range: protocol.Range{ Start: protocol.Position{Line: 0, Character: 9}, End: protocol.Position{Line: 1, Character: 15}, }, } result, err := extractTextFromLocationForTest(location) assert.NoError(t, err) assert.Equal(t, "testFunction() {\n return 'test'", result) } func TestExtractTextFromLocation_InvalidRange(t *testing.T) { mockContent := "function testFunction() {\n return 'test';\n}" // Store original function and restore after test originalFunc := readFileFunc defer func() { readFileFunc = originalFunc }() // Set up mock implementation readFileFunc = func(name string) ([]byte, error) { return []byte(mockContent), nil } // Out of bounds line location := protocol.Location{ URI: "file:///path/to/file.js", Range: protocol.Range{ Start: protocol.Position{Line: 0, Character: 9}, End: protocol.Position{Line: 5, Character: 15}, }, } _, err := extractTextFromLocationForTest(location) assert.Error(t, err) // Out of bounds character on single line location = protocol.Location{ URI: "file:///path/to/file.js", Range: protocol.Range{ Start: protocol.Position{Line: 0, Character: 9}, End: protocol.Position{Line: 0, Character: 100}, }, } _, err = extractTextFromLocationForTest(location) assert.Error(t, err) } func TestExtractTextFromLocation_FileError(t *testing.T) { // Store original function and restore after test originalFunc := readFileFunc defer func() { readFileFunc = originalFunc }() // Mock implementation that returns an error readFileFunc = func(name string) ([]byte, error) { return nil, os.ErrNotExist } location := protocol.Location{ URI: "file:///path/to/nonexistent.js", Range: protocol.Range{ Start: protocol.Position{Line: 0, Character: 9}, End: protocol.Position{Line: 0, Character: 21}, }, } _, err := extractTextFromLocationForTest(location) assert.Error(t, err) } func TestContainsPosition(t *testing.T) { testCases := []struct { name string r protocol.Range p protocol.Position expected bool }{ { name: "Position inside range - middle", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 7, Character: 15}, expected: true, }, { name: "Position at range start line but after start character", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 5, Character: 15}, expected: true, }, { name: "Position at range start exact", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 5, Character: 10}, expected: true, }, { name: "Position at range end line but before end character", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 10, Character: 15}, expected: true, }, { name: "Position at range end exact", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 10, Character: 20}, expected: false, // End position is exclusive }, { name: "Position before range start line", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 4, Character: 15}, expected: false, }, { name: "Position after range end line", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 11, Character: 15}, expected: false, }, { name: "Position at start line but before start character", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 5, Character: 5}, expected: false, }, { name: "Position at end line but after end character", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 10, Character: 20}, }, p: protocol.Position{Line: 10, Character: 25}, expected: false, }, { name: "Same line range", r: protocol.Range{ Start: protocol.Position{Line: 5, Character: 10}, End: protocol.Position{Line: 5, Character: 20}, }, p: protocol.Position{Line: 5, Character: 15}, expected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := containsPosition(tc.r, tc.p) assert.Equal(t, tc.expected, result, "Expected containsPosition to return %v for range %v and position %v", tc.expected, tc.r, tc.p) }) } } func TestAddLineNumbers(t *testing.T) { testCases := []struct { name string text string startLine int expected string }{ { name: "Single line", text: "function test() {}", startLine: 1, expected: "1|function test() {}\n", }, { name: "Multiple lines", text: "function test() {\n return true;\n}", startLine: 10, expected: "10|function test() {\n11| return true;\n12|}\n", }, { name: "Padding for large line numbers", text: "line1\nline2\nline3", startLine: 998, expected: " 998|line1\n 999|line2\n1000|line3\n", }, { name: "Empty string", text: "", startLine: 1, expected: "1|\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := addLineNumbers(tc.text, tc.startLine) assert.Equal(t, tc.expected, result) }) } } func TestConvertLinesToRanges(t *testing.T) { testCases := []struct { name string linesToShow map[int]bool totalLines int expected []LineRange }{ { name: "Empty map", linesToShow: map[int]bool{}, totalLines: 10, expected: nil, // The function returns nil for empty input }, { name: "Single line", linesToShow: map[int]bool{5: true}, totalLines: 10, expected: []LineRange{{Start: 5, End: 5}}, }, { name: "Consecutive lines", linesToShow: map[int]bool{1: true, 2: true, 3: true}, totalLines: 10, expected: []LineRange{{Start: 1, End: 3}}, }, { name: "Non-consecutive lines", linesToShow: map[int]bool{1: true, 3: true, 5: true}, totalLines: 10, expected: []LineRange{{Start: 1, End: 1}, {Start: 3, End: 3}, {Start: 5, End: 5}}, }, { name: "Mixed consecutive and non-consecutive lines", linesToShow: map[int]bool{1: true, 2: true, 5: true, 6: true, 7: true, 9: true}, totalLines: 10, expected: []LineRange{{Start: 1, End: 2}, {Start: 5, End: 7}, {Start: 9, End: 9}}, }, { name: "Lines outside range are filtered", linesToShow: map[int]bool{-1: true, 0: true, 9: true, 10: true}, totalLines: 10, expected: []LineRange{{Start: 0, End: 0}, {Start: 9, End: 9}}, }, { name: "Unsorted input gets sorted", linesToShow: map[int]bool{5: true, 1: true, 3: true, 2: true}, totalLines: 10, expected: []LineRange{{Start: 1, End: 3}, {Start: 5, End: 5}}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ConvertLinesToRanges(tc.linesToShow, tc.totalLines) assert.Equal(t, tc.expected, result, "Expected ranges to match") }) } } func TestFormatLinesWithRanges(t *testing.T) { testCases := []struct { name string lines []string ranges []LineRange expected string }{ { name: "Empty ranges", lines: []string{"line1", "line2", "line3"}, ranges: []LineRange{}, expected: "", }, { name: "Single range", lines: []string{"line1", "line2", "line3", "line4", "line5"}, ranges: []LineRange{{Start: 1, End: 3}}, expected: "2|line2\n3|line3\n4|line4\n", }, { name: "Multiple ranges with gap", lines: []string{"line1", "line2", "line3", "line4", "line5", "line6", "line7"}, ranges: []LineRange{{Start: 0, End: 1}, {Start: 4, End: 6}}, expected: "1|line1\n2|line2\n...\n5|line5\n6|line6\n7|line7\n", }, { name: "Adjacent ranges get combined - no gap in output", lines: []string{"line1", "line2", "line3", "line4", "line5"}, ranges: []LineRange{{Start: 0, End: 2}, {Start: 3, End: 4}}, expected: "1|line1\n2|line2\n3|line3\n4|line4\n5|line5\n", }, { name: "Real-world example", lines: []string{ "package main", "", "import \"fmt\"", "", "func main() {", " s := \"Hello, World!\"", " fmt.Println(s)", "}", }, ranges: []LineRange{{Start: 4, End: 6}}, expected: "5|func main() {\n6| s := \"Hello, World!\"\n7| fmt.Println(s)\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := FormatLinesWithRanges(tc.lines, tc.ranges) assert.Equal(t, tc.expected, result, "Expected formatted output to match") }) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/isaacphi/mcp-language-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server