mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-24 06:11:11 -05:00
Add test coverage
This commit is contained in:
254
src/services/tree-sitter/__tests__/index.test.ts
Normal file
254
src/services/tree-sitter/__tests__/index.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { parseSourceCodeForDefinitionsTopLevel } from '../index';
|
||||
import { listFiles } from '../../glob/list-files';
|
||||
import { loadRequiredLanguageParsers } from '../languageParser';
|
||||
import { fileExistsAtPath } from '../../../utils/fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../glob/list-files');
|
||||
jest.mock('../languageParser');
|
||||
jest.mock('../../../utils/fs');
|
||||
jest.mock('fs/promises');
|
||||
|
||||
describe('Tree-sitter Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(fileExistsAtPath as jest.Mock).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('parseSourceCodeForDefinitionsTopLevel', () => {
|
||||
it('should handle non-existent directory', async () => {
|
||||
(fileExistsAtPath as jest.Mock).mockResolvedValue(false);
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/non/existent/path');
|
||||
expect(result).toBe('This directory does not exist or you do not have permission to access it.');
|
||||
});
|
||||
|
||||
it('should handle empty directory', async () => {
|
||||
(listFiles as jest.Mock).mockResolvedValue([[], new Set()]);
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
expect(result).toBe('No source code definitions found.');
|
||||
});
|
||||
|
||||
it('should parse TypeScript files correctly', async () => {
|
||||
const mockFiles = [
|
||||
'/test/path/file1.ts',
|
||||
'/test/path/file2.tsx',
|
||||
'/test/path/readme.md'
|
||||
];
|
||||
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
rootNode: 'mockNode'
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn().mockReturnValue([
|
||||
{
|
||||
node: {
|
||||
startPosition: { row: 0 },
|
||||
endPosition: { row: 0 }
|
||||
},
|
||||
name: 'name.definition'
|
||||
}
|
||||
])
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
ts: { parser: mockParser, query: mockQuery },
|
||||
tsx: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
(fs.readFile as jest.Mock).mockResolvedValue(
|
||||
'export class TestClass {\n constructor() {}\n}'
|
||||
);
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
|
||||
expect(result).toContain('file1.ts');
|
||||
expect(result).toContain('file2.tsx');
|
||||
expect(result).not.toContain('readme.md');
|
||||
expect(result).toContain('export class TestClass');
|
||||
});
|
||||
|
||||
it('should handle multiple definition types', async () => {
|
||||
const mockFiles = ['/test/path/file.ts'];
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
rootNode: 'mockNode'
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn().mockReturnValue([
|
||||
{
|
||||
node: {
|
||||
startPosition: { row: 0 },
|
||||
endPosition: { row: 0 }
|
||||
},
|
||||
name: 'name.definition.class'
|
||||
},
|
||||
{
|
||||
node: {
|
||||
startPosition: { row: 2 },
|
||||
endPosition: { row: 2 }
|
||||
},
|
||||
name: 'name.definition.function'
|
||||
}
|
||||
])
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
ts: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
const fileContent =
|
||||
'class TestClass {\n' +
|
||||
' constructor() {}\n' +
|
||||
' testMethod() {}\n' +
|
||||
'}';
|
||||
|
||||
(fs.readFile as jest.Mock).mockResolvedValue(fileContent);
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
|
||||
expect(result).toContain('class TestClass');
|
||||
expect(result).toContain('testMethod()');
|
||||
expect(result).toContain('|----');
|
||||
});
|
||||
|
||||
it('should handle parsing errors gracefully', async () => {
|
||||
const mockFiles = ['/test/path/file.ts'];
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Parsing error');
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn()
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
ts: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
(fs.readFile as jest.Mock).mockResolvedValue('invalid code');
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
expect(result).toBe('No source code definitions found.');
|
||||
});
|
||||
|
||||
it('should respect file limit', async () => {
|
||||
const mockFiles = Array(100).fill(0).map((_, i) => `/test/path/file${i}.ts`);
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
rootNode: 'mockNode'
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn().mockReturnValue([])
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
ts: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
|
||||
// Should only process first 50 files
|
||||
expect(mockParser.parse).toHaveBeenCalledTimes(50);
|
||||
});
|
||||
|
||||
it('should handle various supported file extensions', async () => {
|
||||
const mockFiles = [
|
||||
'/test/path/script.js',
|
||||
'/test/path/app.py',
|
||||
'/test/path/main.rs',
|
||||
'/test/path/program.cpp',
|
||||
'/test/path/code.go'
|
||||
];
|
||||
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
rootNode: 'mockNode'
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn().mockReturnValue([{
|
||||
node: {
|
||||
startPosition: { row: 0 },
|
||||
endPosition: { row: 0 }
|
||||
},
|
||||
name: 'name'
|
||||
}])
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
js: { parser: mockParser, query: mockQuery },
|
||||
py: { parser: mockParser, query: mockQuery },
|
||||
rs: { parser: mockParser, query: mockQuery },
|
||||
cpp: { parser: mockParser, query: mockQuery },
|
||||
go: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
(fs.readFile as jest.Mock).mockResolvedValue('function test() {}');
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
|
||||
expect(result).toContain('script.js');
|
||||
expect(result).toContain('app.py');
|
||||
expect(result).toContain('main.rs');
|
||||
expect(result).toContain('program.cpp');
|
||||
expect(result).toContain('code.go');
|
||||
});
|
||||
|
||||
it('should normalize paths in output', async () => {
|
||||
const mockFiles = ['/test/path/dir\\file.ts'];
|
||||
(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]);
|
||||
|
||||
const mockParser = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
rootNode: 'mockNode'
|
||||
})
|
||||
};
|
||||
|
||||
const mockQuery = {
|
||||
captures: jest.fn().mockReturnValue([{
|
||||
node: {
|
||||
startPosition: { row: 0 },
|
||||
endPosition: { row: 0 }
|
||||
},
|
||||
name: 'name'
|
||||
}])
|
||||
};
|
||||
|
||||
(loadRequiredLanguageParsers as jest.Mock).mockResolvedValue({
|
||||
ts: { parser: mockParser, query: mockQuery }
|
||||
});
|
||||
|
||||
(fs.readFile as jest.Mock).mockResolvedValue('class Test {}');
|
||||
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel('/test/path');
|
||||
|
||||
// Should use forward slashes regardless of platform
|
||||
expect(result).toContain('dir/file.ts');
|
||||
expect(result).not.toContain('dir\\file.ts');
|
||||
});
|
||||
});
|
||||
});
|
||||
128
src/services/tree-sitter/__tests__/languageParser.test.ts
Normal file
128
src/services/tree-sitter/__tests__/languageParser.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { loadRequiredLanguageParsers } from '../languageParser';
|
||||
import Parser from 'web-tree-sitter';
|
||||
|
||||
// Mock web-tree-sitter
|
||||
const mockSetLanguage = jest.fn();
|
||||
jest.mock('web-tree-sitter', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
setLanguage: mockSetLanguage
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
// Add static methods to Parser mock
|
||||
const ParserMock = Parser as jest.MockedClass<typeof Parser>;
|
||||
ParserMock.init = jest.fn().mockResolvedValue(undefined);
|
||||
ParserMock.Language = {
|
||||
load: jest.fn().mockResolvedValue({
|
||||
query: jest.fn().mockReturnValue('mockQuery')
|
||||
}),
|
||||
prototype: {} // Add required prototype property
|
||||
} as unknown as typeof Parser.Language;
|
||||
|
||||
describe('Language Parser', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('loadRequiredLanguageParsers', () => {
|
||||
it('should initialize parser only once', async () => {
|
||||
const files = ['test.js', 'test2.js'];
|
||||
await loadRequiredLanguageParsers(files);
|
||||
await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.init).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should load JavaScript parser for .js and .jsx files', async () => {
|
||||
const files = ['test.js', 'test.jsx'];
|
||||
const parsers = await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-javascript.wasm')
|
||||
);
|
||||
expect(parsers.js).toBeDefined();
|
||||
expect(parsers.jsx).toBeDefined();
|
||||
expect(parsers.js.query).toBeDefined();
|
||||
expect(parsers.jsx.query).toBeDefined();
|
||||
});
|
||||
|
||||
it('should load TypeScript parser for .ts and .tsx files', async () => {
|
||||
const files = ['test.ts', 'test.tsx'];
|
||||
const parsers = await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-typescript.wasm')
|
||||
);
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-tsx.wasm')
|
||||
);
|
||||
expect(parsers.ts).toBeDefined();
|
||||
expect(parsers.tsx).toBeDefined();
|
||||
});
|
||||
|
||||
it('should load Python parser for .py files', async () => {
|
||||
const files = ['test.py'];
|
||||
const parsers = await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-python.wasm')
|
||||
);
|
||||
expect(parsers.py).toBeDefined();
|
||||
});
|
||||
|
||||
it('should load multiple language parsers as needed', async () => {
|
||||
const files = ['test.js', 'test.py', 'test.rs', 'test.go'];
|
||||
const parsers = await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledTimes(4);
|
||||
expect(parsers.js).toBeDefined();
|
||||
expect(parsers.py).toBeDefined();
|
||||
expect(parsers.rs).toBeDefined();
|
||||
expect(parsers.go).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle C/C++ files correctly', async () => {
|
||||
const files = ['test.c', 'test.h', 'test.cpp', 'test.hpp'];
|
||||
const parsers = await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-c.wasm')
|
||||
);
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-cpp.wasm')
|
||||
);
|
||||
expect(parsers.c).toBeDefined();
|
||||
expect(parsers.h).toBeDefined();
|
||||
expect(parsers.cpp).toBeDefined();
|
||||
expect(parsers.hpp).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw error for unsupported file extensions', async () => {
|
||||
const files = ['test.unsupported'];
|
||||
|
||||
await expect(loadRequiredLanguageParsers(files)).rejects.toThrow(
|
||||
'Unsupported language: unsupported'
|
||||
);
|
||||
});
|
||||
|
||||
it('should load each language only once for multiple files', async () => {
|
||||
const files = ['test1.js', 'test2.js', 'test3.js'];
|
||||
await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledTimes(1);
|
||||
expect(ParserMock.Language.load).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tree-sitter-javascript.wasm')
|
||||
);
|
||||
});
|
||||
|
||||
it('should set language for each parser instance', async () => {
|
||||
const files = ['test.js', 'test.py'];
|
||||
await loadRequiredLanguageParsers(files);
|
||||
|
||||
expect(mockSetLanguage).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user