Skip to content

Commit cb8518b

Browse files
committed
Improve unit testing of CDS "compiler" module
Improves unit testing for the TypeScript code in the CDS extractor's "compiler" module. Aligns the unit testing coverage for the "compiler" module to be comparable to other modules (libraries) in the source code for the CDS extractor.
1 parent 0842f31 commit cb8518b

File tree

4 files changed

+1514
-1
lines changed

4 files changed

+1514
-1
lines changed

extractors/cds/tools/test/src/cds/compiler/command.test.ts

Lines changed: 256 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import * as childProcess from 'child_process';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
24

3-
import { determineCdsCommand, resetCdsCommandCache } from '../../../../src/cds/compiler';
5+
import { determineCdsCommand, resetCdsCommandCache } from '../../../../src/cds/compiler/command';
6+
import { fileExists } from '../../../../src/filesystem';
7+
import { cdsExtractorLog } from '../../../../src/logging';
48

59
// Mock dependencies
610
jest.mock('child_process', () => ({
711
execFileSync: jest.fn(),
812
spawnSync: jest.fn(),
913
}));
1014

15+
jest.mock('fs', () => ({
16+
existsSync: jest.fn(),
17+
readdirSync: jest.fn(),
18+
}));
19+
1120
jest.mock('path', () => {
1221
const original = jest.requireActual('path');
1322
return {
@@ -25,6 +34,10 @@ jest.mock('../../../../src/filesystem', () => ({
2534
recursivelyRenameJsonFiles: jest.fn(),
2635
}));
2736

37+
jest.mock('../../../../src/logging', () => ({
38+
cdsExtractorLog: jest.fn(),
39+
}));
40+
2841
describe('cds compiler command', () => {
2942
beforeEach(() => {
3043
jest.clearAllMocks();
@@ -117,5 +130,247 @@ describe('cds compiler command', () => {
117130
// Verify execFileSync was called minimal times (once for cds during cache initialization)
118131
expect(childProcess.execFileSync).toHaveBeenCalledTimes(1);
119132
});
133+
134+
it('should return fallback command when all commands fail', () => {
135+
// Mock all commands to fail
136+
(childProcess.execFileSync as jest.Mock).mockImplementation(() => {
137+
throw new Error('Command not found');
138+
});
139+
140+
// Execute
141+
const result = determineCdsCommand(undefined, '/mock/source/root');
142+
143+
// Verify - should return the fallback command even if it doesn't work
144+
expect(result).toBe('npx -y --package @sap/cds-dk cds');
145+
});
146+
147+
it('should handle cache directory discovery with available directories', () => {
148+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
149+
150+
// Mock file system operations
151+
(fs.existsSync as jest.Mock).mockReturnValue(true);
152+
(fs.readdirSync as jest.Mock).mockReturnValue([
153+
{ name: 'cds-v6.1.3', isDirectory: () => true },
154+
{ name: 'cds-v6.2.0', isDirectory: () => true },
155+
{ name: 'other-dir', isDirectory: () => true },
156+
]);
157+
158+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
159+
160+
// Mock fileExists to return true for cache directories with cds binary
161+
mockFileExists.mockImplementation((filePath: string) => {
162+
return filePath.includes('cds-v6') && filePath.endsWith('node_modules/.bin/cds');
163+
});
164+
165+
// Mock successful execution for cache directory commands
166+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('6.1.3'));
167+
168+
// Execute
169+
const result = determineCdsCommand(undefined, '/mock/source/root');
170+
171+
// Verify - should use the first available cache directory
172+
expect(result).toBe(
173+
'/mock/source/root/.cds-extractor-cache/cds-v6.1.3/node_modules/.bin/cds',
174+
);
175+
});
176+
177+
it('should handle cache directory discovery with filesystem errors', () => {
178+
const mockCdsExtractorLog = cdsExtractorLog as jest.MockedFunction<typeof cdsExtractorLog>;
179+
180+
// Mock existsSync to return true, but readdirSync to throw an error
181+
(fs.existsSync as jest.Mock).mockReturnValue(true);
182+
(fs.readdirSync as jest.Mock).mockImplementation(() => {
183+
throw new Error('Permission denied');
184+
});
185+
186+
// Mock execFileSync to succeed for fallback commands
187+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('4.6.0'));
188+
189+
// Execute
190+
const result = determineCdsCommand(undefined, '/mock/source/root');
191+
192+
// Verify - should fall back to global command
193+
expect(result).toBe('cds');
194+
expect(mockCdsExtractorLog).toHaveBeenCalledWith(
195+
'debug',
196+
'Failed to discover cache directories: Error: Permission denied',
197+
);
198+
});
199+
200+
it('should prefer provided cache directory over discovered ones', () => {
201+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
202+
203+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
204+
205+
// Mock fileExists to return true for the provided cache directory
206+
mockFileExists.mockImplementation((filePath: string) => {
207+
return filePath === '/custom/cache/node_modules/.bin/cds';
208+
});
209+
210+
// Mock successful execution for the provided cache directory
211+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('6.2.0'));
212+
213+
// Execute with custom cache directory
214+
const result = determineCdsCommand('/custom/cache', '/mock/source/root');
215+
216+
// Verify - should use the provided cache directory
217+
expect(result).toBe('/custom/cache/node_modules/.bin/cds');
218+
});
219+
220+
it('should handle node command format correctly', () => {
221+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
222+
223+
// Mock cache directory discovery with node command - but no cache directories exist
224+
(fs.existsSync as jest.Mock).mockReturnValue(false);
225+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
226+
227+
// Mock fileExists to return false (no cache directories)
228+
mockFileExists.mockReturnValue(false);
229+
230+
// Mock execFileSync to handle node command execution
231+
(childProcess.execFileSync as jest.Mock).mockImplementation(
232+
(command: string, args: string[]) => {
233+
if (command === 'node') {
234+
return Buffer.from('6.1.3');
235+
}
236+
if (command === 'sh' && args.join(' ').includes('cds --version')) {
237+
throw new Error('Command not found');
238+
}
239+
if (command === 'sh' && args.join(' ').includes('npx -y --package @sap/cds-dk cds')) {
240+
return Buffer.from('6.1.3');
241+
}
242+
throw new Error('Command not found');
243+
},
244+
);
245+
246+
// Execute
247+
const result = determineCdsCommand(undefined, '/mock/source/root');
248+
249+
// Should fall back to npx command since cache directories don't exist
250+
expect(result).toBe('npx -y --package @sap/cds-dk cds');
251+
});
252+
253+
it('should handle version parsing failures gracefully', () => {
254+
// Mock cache directory discovery - no cache directories exist
255+
(fs.existsSync as jest.Mock).mockReturnValue(false);
256+
257+
// Mock command to return output without version number for global command
258+
(childProcess.execFileSync as jest.Mock).mockImplementation(
259+
(command: string, args: string[]) => {
260+
if (command === 'sh' && args.join(' ').includes('cds --version')) {
261+
return Buffer.from('No version info');
262+
}
263+
throw new Error('Command not found');
264+
},
265+
);
266+
267+
// Execute
268+
const result = determineCdsCommand(undefined, '/mock/source/root');
269+
270+
// Verify - should still return the command even without version info
271+
expect(result).toBe('cds');
272+
});
273+
274+
it('should try fallback npx commands when global command fails', () => {
275+
// Mock all commands to fail except one fallback
276+
(childProcess.execFileSync as jest.Mock).mockImplementation(
277+
(_command: string, args: string[]) => {
278+
const fullCommand = args.join(' ');
279+
if (fullCommand === "-c 'npx -y --package @sap/cds cds' --version") {
280+
return Buffer.from('6.1.3');
281+
}
282+
throw new Error('Command not found');
283+
},
284+
);
285+
286+
// Execute
287+
const result = determineCdsCommand(undefined, '/mock/source/root');
288+
289+
// Verify - should use the fallback command
290+
expect(result).toBe('npx -y --package @sap/cds cds');
291+
});
292+
293+
it('should log discovery of multiple cache directories', () => {
294+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
295+
const mockCdsExtractorLog = cdsExtractorLog as jest.MockedFunction<typeof cdsExtractorLog>;
296+
297+
// Mock file system operations to discover multiple cache directories
298+
(fs.existsSync as jest.Mock).mockReturnValue(true);
299+
(fs.readdirSync as jest.Mock).mockReturnValue([
300+
{ name: 'cds-v6.1.3', isDirectory: () => true },
301+
{ name: 'cds-v6.2.0', isDirectory: () => true },
302+
{ name: 'cds-v7.0.0', isDirectory: () => true },
303+
]);
304+
305+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
306+
307+
// Mock fileExists to return true for all cache directories
308+
mockFileExists.mockImplementation((filePath: string) => {
309+
return filePath.includes('cds-v') && filePath.endsWith('node_modules/.bin/cds');
310+
});
311+
312+
// Mock successful execution
313+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('6.1.3'));
314+
315+
// Execute
316+
determineCdsCommand(undefined, '/mock/source/root');
317+
318+
// Verify - should log the discovery of multiple directories
319+
expect(mockCdsExtractorLog).toHaveBeenCalledWith(
320+
'info',
321+
'Discovered 3 CDS cache directories',
322+
);
323+
});
324+
325+
it('should log discovery of single cache directory', () => {
326+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
327+
const mockCdsExtractorLog = cdsExtractorLog as jest.MockedFunction<typeof cdsExtractorLog>;
328+
329+
// Mock file system operations to discover one cache directory
330+
(fs.existsSync as jest.Mock).mockReturnValue(true);
331+
(fs.readdirSync as jest.Mock).mockReturnValue([
332+
{ name: 'cds-v6.1.3', isDirectory: () => true },
333+
]);
334+
335+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
336+
337+
// Mock fileExists to return true for the cache directory
338+
mockFileExists.mockImplementation((filePath: string) => {
339+
return filePath.includes('cds-v6') && filePath.endsWith('node_modules/.bin/cds');
340+
});
341+
342+
// Mock successful execution
343+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('6.1.3'));
344+
345+
// Execute
346+
determineCdsCommand(undefined, '/mock/source/root');
347+
348+
// Verify - should log the discovery of single directory
349+
expect(mockCdsExtractorLog).toHaveBeenCalledWith('info', 'Discovered 1 CDS cache directory');
350+
});
351+
352+
it('should handle cache directory without valid cds binary', () => {
353+
const mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>;
354+
355+
// Mock file system operations
356+
(fs.existsSync as jest.Mock).mockReturnValue(true);
357+
(fs.readdirSync as jest.Mock).mockReturnValue([
358+
{ name: 'cds-v6.1.3', isDirectory: () => true },
359+
]);
360+
361+
(path.join as jest.Mock).mockImplementation((...args: string[]) => args.join('/'));
362+
363+
// Mock fileExists to return false for the cache directory (no cds binary)
364+
mockFileExists.mockReturnValue(false);
365+
366+
// Mock successful execution for fallback
367+
(childProcess.execFileSync as jest.Mock).mockReturnValue(Buffer.from('4.6.0'));
368+
369+
// Execute
370+
const result = determineCdsCommand(undefined, '/mock/source/root');
371+
372+
// Verify - should fall back to global command
373+
expect(result).toBe('cds');
374+
});
120375
});
121376
});

0 commit comments

Comments
 (0)