Skip to content

Commit e6e3d31

Browse files
committed
Add tree sitter command parser tests
Fixes microsoft#272780
1 parent 2b3de7c commit e6e3d31

File tree

1 file changed

+136
-0
lines changed

1 file changed

+136
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { deepStrictEqual } from 'assert';
7+
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
8+
import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
9+
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
10+
import { ITreeSitterLibraryService } from '../../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
11+
import { TreeSitterLibraryService } from '../../../../../services/treeSitter/browser/treeSitterLibraryService.js';
12+
import { FileService } from '../../../../../../platform/files/common/fileService.js';
13+
import { NullLogService } from '../../../../../../platform/log/common/log.js';
14+
import { Schemas } from '../../../../../../base/common/network.js';
15+
import { TestIPCFileSystemProvider } from '../../../../../test/electron-browser/workbenchTestServices.js';
16+
import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../../browser/treeSitterCommandParser.js';
17+
18+
suite('TreeSitterCommandParser', () => {
19+
const store = ensureNoDisposablesAreLeakedInTestSuite();
20+
21+
let instantiationService: TestInstantiationService;
22+
let parser: TreeSitterCommandParser;
23+
24+
setup(() => {
25+
const logService = new NullLogService();
26+
const fileService = store.add(new FileService(logService));
27+
const fileSystemProvider = new TestIPCFileSystemProvider();
28+
store.add(fileService.registerProvider(Schemas.file, fileSystemProvider));
29+
30+
instantiationService = workbenchInstantiationService({
31+
fileService: () => fileService,
32+
}, store);
33+
34+
const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService));
35+
treeSitterLibraryService.isTest = true;
36+
instantiationService.stub(ITreeSitterLibraryService, treeSitterLibraryService);
37+
38+
parser = instantiationService.createInstance(TreeSitterCommandParser);
39+
});
40+
41+
suite('Bash command parsing', () => {
42+
async function t(commandLine: string, expectedCommands: string[]) {
43+
const result = await parser.extractSubCommands(TreeSitterCommandParserLanguage.Bash, commandLine);
44+
deepStrictEqual(result, expectedCommands);
45+
}
46+
47+
test('simple commands', () => t('ls -la', ['ls -la']));
48+
test('commands with &&', () => t('echo hello && ls -la', ['echo hello', 'ls -la']));
49+
test('commands with ||', () => t('test -f file.txt || touch file.txt', ['test -f file.txt', 'touch file.txt']));
50+
test('commands with semicolons', () => t('cd /tmp; ls; pwd', ['cd /tmp', 'ls', 'pwd']));
51+
test('pipe chains', () => t('cat file.txt | grep pattern | sort | uniq', ['cat file.txt', 'grep pattern', 'sort', 'uniq']));
52+
test('commands with subshells', () => t('echo $(date +%Y) && ls', ['echo $(date +%Y)', 'date +%Y', 'ls']));
53+
test('complex quoting', () => t('echo "hello && world" && echo \'test\'', ['echo "hello && world"', 'echo \'test\'']));
54+
test('escaped characters', () => t('echo hello\\ world && ls', ['echo hello\\ world', 'ls']));
55+
test('background commands', () => t('sleep 10 & echo done', ['sleep 10', 'echo done']));
56+
test('variable assignments', () => t('VAR=value command1 && echo $VAR', ['VAR=value command1', 'echo $VAR']));
57+
test('redirections', () => t('echo hello > file.txt && cat < file.txt', ['echo hello', 'cat']));
58+
test('arithmetic expansion', () => t('echo $((1 + 2)) && ls', ['echo $((1 + 2))', 'ls']));
59+
60+
suite('control flow and structures', () => {
61+
test('if-then-else', () => t('if [ -f file.txt ]; then cat file.txt; else echo "not found"; fi', ['cat file.txt', 'echo "not found"']));
62+
test('simple iteration', () => t('for file in *.txt; do cat "$file"; done', ['cat "$file"']));
63+
test('function declaration and call', () => t('function test_func() { echo "inside function"; } && test_func', ['echo "inside function"', 'test_func']));
64+
test('heredoc with commands', () => t('cat << EOF\nhello\nworld\nEOF\necho done', ['cat', 'echo done']));
65+
});
66+
67+
suite('edge cases', () => {
68+
test('malformed syntax', () => t('echo "unclosed quote && ls', ['echo']));
69+
test('unmatched parentheses', () => t('echo $(missing closing && ls', ['echo $(missing closing && ls', 'missing closing', 'ls']));
70+
test('very long command lines', () => t('echo ' + 'a'.repeat(10000) + ' && ls', ['echo ' + 'a'.repeat(10000), 'ls']));
71+
test('special characters', () => t('echo "πλαςε 测试 🚀" && ls', ['echo "πλαςε 测试 🚀"', 'ls']));
72+
test('multiline with continuations', () => t('echo hello \\\n&& echo world \\\n&& ls', ['echo hello', 'echo world', 'ls']));
73+
test('commands with comments', () => t('echo hello # this is a comment\nls # another comment', ['echo hello', 'ls']));
74+
});
75+
76+
// TODO: These should be common but the pwsh grammar doesn't handle && yet https://github.com/microsoft/vscode/issues/272704
77+
suite('real-world scenarios', () => {
78+
test('complex Docker commands', () => t('docker run -it --rm -v $(pwd):/app ubuntu:latest bash -c "cd /app && npm install && npm test"', ['docker run -it --rm -v $(pwd):/app ubuntu:latest bash -c "cd /app && npm install && npm test"', 'pwd']));
79+
test('Git workflow commands', () => t('git add . && git commit -m "Update feature" && git push origin main', [
80+
'git add .',
81+
'git commit -m "Update feature"',
82+
'git push origin main'
83+
]));
84+
test('npm/yarn workflow commands', () => t('npm ci && npm run build && npm test && npm run lint', [
85+
'npm ci',
86+
'npm run build',
87+
'npm test',
88+
'npm run lint'
89+
]));
90+
test('build system commands', () => t('make clean && make -j$(nproc) && make install PREFIX=/usr/local', [
91+
'make clean',
92+
'make -j$(nproc)',
93+
'nproc',
94+
'make install PREFIX=/usr/local'
95+
]));
96+
});
97+
});
98+
99+
suite('PowerShell command parsing', () => {
100+
async function t(commandLine: string, expectedCommands: string[]) {
101+
const result = await parser.extractSubCommands(TreeSitterCommandParserLanguage.PowerShell, commandLine);
102+
deepStrictEqual(result, expectedCommands);
103+
}
104+
105+
test('simple commands', () => t('Get-ChildItem -Path C:\\', ['Get-ChildItem -Path C:\\']));
106+
test('commands with semicolons', () => t('Get-Date; Get-Location; Write-Host "done"', ['Get-Date', 'Get-Location', 'Write-Host "done"']));
107+
test('pipeline commands', () => t('Get-Process | Where-Object {$_.CPU -gt 100} | Sort-Object CPU', ['Get-Process ', 'Where-Object {$_.CPU -gt 100} ', 'Sort-Object CPU']));
108+
test('command substitution', () => t('Write-Host $(Get-Date) ; Get-Location', ['Write-Host $(Get-Date)', 'Get-Date', 'Get-Location']));
109+
test('complex parameters', () => t('Get-ChildItem -Path "C:\\Program Files" -Recurse -Include "*.exe"', ['Get-ChildItem -Path "C:\\Program Files" -Recurse -Include "*.exe"']));
110+
test('splatting', () => t('$params = @{Path="C:\\"; Recurse=$true}; Get-ChildItem @params', ['Get-ChildItem @params']));
111+
test('here-strings', () => t('Write-Host @"\nhello\nworld\n"@ ; Get-Date', ['Write-Host @"\nhello\nworld\n"@', 'Get-Date']));
112+
test('method calls', () => t('"hello".ToUpper() ; Get-Date', ['Get-Date']));
113+
test('complex quoting', () => t('Write-Host "She said `"Hello`"" ; Write-Host \'Single quotes\'', ['Write-Host "She said `"Hello`""', 'Write-Host \'Single quotes\'']));
114+
115+
suite('Control flow and structures', () => {
116+
test('logical and', () => t('Test-Path "file.txt" -and Get-Content "file.txt"', ['Test-Path "file.txt" -and Get-Content "file.txt"']));
117+
test('foreach with script block', () => t('ForEach-Object { Write-Host $_.Name } ; Get-Date', ['ForEach-Object { Write-Host $_.Name }', 'Write-Host $_.Name', 'Get-Date']));
118+
test('if-else', () => t('if (Test-Path "file.txt") { Get-Content "file.txt" } else { Write-Host "not found" }', ['Test-Path "file.txt"', 'Get-Content "file.txt"', 'Write-Host "not found"']));
119+
test('error handling', () => t('try { Get-Content "file.txt" } catch { Write-Error "failed" }', ['Get-Content "file.txt"', 'Write-Error "failed"']));
120+
});
121+
});
122+
123+
suite('all shell command parsing', () => {
124+
async function t(commandLine: string, expectedCommands: string[]) {
125+
for (const shell of [TreeSitterCommandParserLanguage.Bash, TreeSitterCommandParserLanguage.PowerShell]) {
126+
const result = await parser.extractSubCommands(shell, commandLine);
127+
deepStrictEqual(result, expectedCommands);
128+
}
129+
}
130+
131+
suite('edge cases', () => {
132+
test('empty strings', () => t('', []));
133+
test('whitespace-only strings', () => t(' \n\t ', []));
134+
});
135+
});
136+
});

0 commit comments

Comments
 (0)