|
| 1 | +import { expect } from 'chai'; |
| 2 | +import * as shlex from '@cmt/shlex'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Tests for the compile command argument handling used by CMakeDriver.runCompileCommand(). |
| 6 | + * |
| 7 | + * The fix (https://github.com/microsoft/vscode-cmake-tools/issues/4836) replaces |
| 8 | + * terminal.sendText() — which is subject to the PTY 4096-byte input buffer |
| 9 | + * truncation — with proc.execute(), which passes args as an array directly to |
| 10 | + * child_process.spawn() and has no such limit. |
| 11 | + * |
| 12 | + * These tests validate the data flow: |
| 13 | + * 1. CompilationDatabase splits a command string into an arguments array via shlex |
| 14 | + * 2. runCompileCommand() extracts args[0] as the executable and args.slice(1) as |
| 15 | + * the spawn arguments |
| 16 | + * 3. The full argument list is preserved regardless of total command length |
| 17 | + */ |
| 18 | + |
| 19 | +// Mirror of proc.buildCmdStr (cannot import proc.ts directly |
| 20 | +// because it transitively depends on 'vscode'). |
| 21 | +function buildCmdStr(command: string, args?: string[]): string { |
| 22 | + let cmdarr = [command]; |
| 23 | + if (args) { |
| 24 | + cmdarr = cmdarr.concat(args); |
| 25 | + } |
| 26 | + return cmdarr.map(a => /[ \n\r\f;\t]/.test(a) ? `"${a}"` : a).join(' '); |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * Mirrors the argument-population logic from CompilationDatabase's constructor: |
| 31 | + * arguments: cur.arguments ? cur.arguments : [...shlex.split(cur.command)] |
| 32 | + */ |
| 33 | +function populateArguments(entry: { command: string; arguments?: string[] }): string[] { |
| 34 | + return entry.arguments ? entry.arguments : [...shlex.split(entry.command)]; |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * Mirrors the executable/args extraction from runCompileCommand(): |
| 39 | + * const executable = args[0]; |
| 40 | + * const execArgs = args.slice(1); |
| 41 | + */ |
| 42 | +function extractExecAndArgs(args: string[]): { executable: string; execArgs: string[] } { |
| 43 | + return { executable: args[0], execArgs: args.slice(1) }; |
| 44 | +} |
| 45 | + |
| 46 | +suite('Compile command argument handling (issue #4836)', () => { |
| 47 | + |
| 48 | + suite('CompilationDatabase argument population', () => { |
| 49 | + test('Pre-split arguments are preserved as-is', () => { |
| 50 | + const entry = { |
| 51 | + command: '/usr/bin/g++ -o main.o -c main.cpp', |
| 52 | + arguments: ['/usr/bin/g++', '-o', 'main.o', '-c', 'main.cpp'] |
| 53 | + }; |
| 54 | + const args = populateArguments(entry); |
| 55 | + expect(args).to.deep.equal(['/usr/bin/g++', '-o', 'main.o', '-c', 'main.cpp']); |
| 56 | + }); |
| 57 | + |
| 58 | + test('Command string is split via shlex when arguments not provided', () => { |
| 59 | + const entry = { |
| 60 | + command: '/usr/bin/g++ -DBOOST_THREAD_VERSION=3 -isystem ../extern -g -std=gnu++11 -o out.o -c main.cpp' |
| 61 | + }; |
| 62 | + const args = populateArguments(entry); |
| 63 | + expect(args[0]).to.equal('/usr/bin/g++'); |
| 64 | + expect(args).to.include('-DBOOST_THREAD_VERSION=3'); |
| 65 | + expect(args).to.include('-std=gnu++11'); |
| 66 | + expect(args[args.length - 1]).to.equal('main.cpp'); |
| 67 | + }); |
| 68 | + |
| 69 | + test('Command with quoted paths is correctly split', () => { |
| 70 | + const entry = { |
| 71 | + command: '"C:\\Program Files\\MSVC\\cl.exe" /nologo /TP "-IC:\\My Project\\include" /Fo"build\\main.obj" /c "C:\\My Project\\main.cpp"' |
| 72 | + }; |
| 73 | + const args = populateArguments(entry); |
| 74 | + expect(args[0]).to.equal('"C:\\Program Files\\MSVC\\cl.exe"'); |
| 75 | + expect(args.length).to.be.greaterThan(1); |
| 76 | + }); |
| 77 | + }); |
| 78 | + |
| 79 | + suite('Executable and arguments extraction', () => { |
| 80 | + test('First element is the executable, rest are arguments', () => { |
| 81 | + const args = ['/usr/bin/g++', '-o', 'main.o', '-c', 'main.cpp']; |
| 82 | + const { executable, execArgs } = extractExecAndArgs(args); |
| 83 | + expect(executable).to.equal('/usr/bin/g++'); |
| 84 | + expect(execArgs).to.deep.equal(['-o', 'main.o', '-c', 'main.cpp']); |
| 85 | + }); |
| 86 | + |
| 87 | + test('Single-element array yields executable with no arguments', () => { |
| 88 | + const args = ['/usr/bin/g++']; |
| 89 | + const { executable, execArgs } = extractExecAndArgs(args); |
| 90 | + expect(executable).to.equal('/usr/bin/g++'); |
| 91 | + expect(execArgs).to.deep.equal([]); |
| 92 | + }); |
| 93 | + |
| 94 | + test('MSVC-style executable with many flags', () => { |
| 95 | + const args = ['cl.exe', '/nologo', '/TP', '/DWIN32', '/D_WINDOWS', '/W3', '/GR', '/EHsc', |
| 96 | + '/MDd', '/Zi', '/Ob0', '/Od', '/RTC1', '/Fo"build\\main.obj"', '/c', 'main.cpp']; |
| 97 | + const { executable, execArgs } = extractExecAndArgs(args); |
| 98 | + expect(executable).to.equal('cl.exe'); |
| 99 | + expect(execArgs.length).to.equal(15); |
| 100 | + expect(execArgs[execArgs.length - 1]).to.equal('main.cpp'); |
| 101 | + }); |
| 102 | + }); |
| 103 | + |
| 104 | + suite('Long command lines exceeding 4096 bytes', () => { |
| 105 | + /** |
| 106 | + * Generate a realistic compile command that exceeds 4096 bytes. |
| 107 | + * This simulates the real-world scenario from issue #4836 where |
| 108 | + * many -I include paths and -D defines push the command past the |
| 109 | + * PTY input buffer limit. |
| 110 | + */ |
| 111 | + function generateLongCommand(minLength: number): { command: string; expectedArgCount: number } { |
| 112 | + const compiler = '/usr/bin/g++'; |
| 113 | + const baseFlags = ['-std=gnu++17', '-g', '-O2', '-Wall', '-Wextra', '-fPIC']; |
| 114 | + // Generate enough -I and -D flags to exceed the target length |
| 115 | + const extraFlags: string[] = []; |
| 116 | + for (let i = 0; extraFlags.join(' ').length < minLength; i++) { |
| 117 | + extraFlags.push(`-I/very/long/path/to/include/directory/number_${i}/nested/deeply`); |
| 118 | + extraFlags.push(`-DSOME_VERY_LONG_DEFINE_NAME_${i}=some_long_value_${i}`); |
| 119 | + } |
| 120 | + const tail = ['-o', 'CMakeFiles/myTarget.dir/src/main.cpp.o', '-c', '/home/user/project/src/main.cpp']; |
| 121 | + const allArgs = [compiler, ...baseFlags, ...extraFlags, ...tail]; |
| 122 | + return { |
| 123 | + command: allArgs.join(' '), |
| 124 | + expectedArgCount: allArgs.length |
| 125 | + }; |
| 126 | + } |
| 127 | + |
| 128 | + test('Command exceeding 4096 chars is fully preserved when split via shlex', () => { |
| 129 | + const { command, expectedArgCount } = generateLongCommand(5000); |
| 130 | + // Verify the command actually exceeds 4096 bytes |
| 131 | + expect(command.length).to.be.greaterThan(4096); |
| 132 | + |
| 133 | + const args = populateArguments({ command }); |
| 134 | + expect(args.length).to.equal(expectedArgCount); |
| 135 | + expect(args[0]).to.equal('/usr/bin/g++'); |
| 136 | + expect(args[args.length - 1]).to.equal('/home/user/project/src/main.cpp'); |
| 137 | + }); |
| 138 | + |
| 139 | + test('Command exceeding 8192 chars is fully preserved', () => { |
| 140 | + const { command, expectedArgCount } = generateLongCommand(10000); |
| 141 | + expect(command.length).to.be.greaterThan(8192); |
| 142 | + |
| 143 | + const args = populateArguments({ command }); |
| 144 | + expect(args.length).to.equal(expectedArgCount); |
| 145 | + }); |
| 146 | + |
| 147 | + test('Long command roundtrips through extraction and buildCmdStr', () => { |
| 148 | + const { command } = generateLongCommand(5000); |
| 149 | + |
| 150 | + const args = populateArguments({ command }); |
| 151 | + const { executable, execArgs } = extractExecAndArgs(args); |
| 152 | + const displayed = buildCmdStr(executable, execArgs); |
| 153 | + |
| 154 | + // The displayed string should contain the executable |
| 155 | + expect(displayed).to.include('/usr/bin/g++'); |
| 156 | + // ... the last source file |
| 157 | + expect(displayed).to.include('/home/user/project/src/main.cpp'); |
| 158 | + // ... and all include paths (spot-check a few) |
| 159 | + expect(displayed).to.include('-I/very/long/path/to/include/directory/number_0/nested/deeply'); |
| 160 | + expect(displayed).to.include('-I/very/long/path/to/include/directory/number_10/nested/deeply'); |
| 161 | + // The displayed string should also exceed 4096 chars |
| 162 | + expect(displayed.length).to.be.greaterThan(4096); |
| 163 | + }); |
| 164 | + |
| 165 | + test('Pre-split arguments array for a long command bypasses shlex entirely', () => { |
| 166 | + // When compile_commands.json provides "arguments" directly (CMake >= 3.something), |
| 167 | + // shlex is never invoked. Verify the array passes through untouched. |
| 168 | + const compiler = '/usr/bin/clang++'; |
| 169 | + const flags: string[] = []; |
| 170 | + for (let i = 0; i < 200; i++) { |
| 171 | + flags.push(`-I/workspace/third_party/library_${i}/include`); |
| 172 | + } |
| 173 | + flags.push('-c', '/workspace/src/main.cpp'); |
| 174 | + const allArgs = [compiler, ...flags]; |
| 175 | + const totalLength = allArgs.join(' ').length; |
| 176 | + expect(totalLength).to.be.greaterThan(4096); |
| 177 | + |
| 178 | + const args = populateArguments({ command: 'ignored', arguments: allArgs }); |
| 179 | + expect(args).to.deep.equal(allArgs); |
| 180 | + expect(args.length).to.equal(allArgs.length); |
| 181 | + |
| 182 | + const { executable, execArgs } = extractExecAndArgs(args); |
| 183 | + expect(executable).to.equal(compiler); |
| 184 | + expect(execArgs.length).to.equal(allArgs.length - 1); |
| 185 | + }); |
| 186 | + }); |
| 187 | + |
| 188 | + suite('buildCmdStr display formatting', () => { |
| 189 | + test('Simple command without spaces', () => { |
| 190 | + expect(buildCmdStr('gcc', ['-c', 'main.cpp'])).to.equal('gcc -c main.cpp'); |
| 191 | + }); |
| 192 | + |
| 193 | + test('Arguments with spaces are quoted', () => { |
| 194 | + expect(buildCmdStr('gcc', ['-I/path with spaces/include', '-c', 'main.cpp'])) |
| 195 | + .to.equal('gcc "-I/path with spaces/include" -c main.cpp'); |
| 196 | + }); |
| 197 | + |
| 198 | + test('Empty args array shows only command', () => { |
| 199 | + expect(buildCmdStr('gcc', [])).to.equal('gcc'); |
| 200 | + }); |
| 201 | + |
| 202 | + test('Undefined args shows only command', () => { |
| 203 | + expect(buildCmdStr('gcc')).to.equal('gcc'); |
| 204 | + }); |
| 205 | + }); |
| 206 | +}); |
0 commit comments