Add test coverage

This commit is contained in:
Matt Rubens
2025-01-07 01:52:42 -05:00
parent 537514de44
commit 5fbfe9b775
19 changed files with 3106 additions and 493 deletions

View 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');
});
});
});

View 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);
});
});
});