11import * 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
610jest . 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+
1120jest . 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+
2841describe ( '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