66
77import * as sinon from 'sinon' ;
88import { assert } from 'chai' ;
9- import { anything , deepEqual , instance , mock , when } from 'ts-mockito' ;
9+ import { anything , deepEqual , instance , mock , when , verify } from 'ts-mockito' ;
1010import { CancellationTokenSource , ExtensionContext , Memento , Uri } from 'vscode' ;
1111import { CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , JupyterPaths } from './jupyterPaths.node' ;
1212import { dispose } from '../../../platform/common/utils/lifecycle' ;
@@ -48,6 +48,7 @@ suite('Jupyter Paths', () => {
4848 } ;
4949 const unixHomeDir = Uri . file ( '/users/username' ) ;
5050 const macHomeDir = Uri . file ( '/users/username' ) ;
51+ const linuxJupyterPath = path . join ( '.local' , 'share' , 'jupyter' , 'kernels' ) ;
5152 let cancelToken : CancellationTokenSource ;
5253 suiteSetup ( function ( ) {
5354 if ( isWeb ( ) ) {
@@ -322,8 +323,14 @@ suite('Jupyter Paths', () => {
322323 const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
323324 const winJupyterPath = path . join ( 'AppData' , 'Roaming' , 'jupyter' , 'kernels' ) ;
324325
325- assert . strictEqual ( paths . length , 1 , `Expected 1 path, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
326- assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
326+ // New implementation returns data dirs + kernel spec root path
327+ assert . strictEqual ( paths . length , 2 , `Expected 2 paths, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
328+
329+ // First path should be from data directory (.jupyter/data/kernels)
330+ assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , '.jupyter' , 'data' , 'kernels' ) . toString ( ) ) ;
331+
332+ // Second path should be the kernel spec root path
333+ assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
327334 } ) ;
328335
329336 test ( 'Get kernelspec root paths on Windows with JUPYTER_PATH env variable' , async ( ) => {
@@ -336,9 +343,17 @@ suite('Jupyter Paths', () => {
336343 const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
337344 const winJupyterPath = path . join ( 'AppData' , 'Roaming' , 'jupyter' , 'kernels' ) ;
338345
339- assert . strictEqual ( paths . length , 2 , `Expected 2 path, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
346+ // New implementation returns JUPYTER_PATH kernels + data dirs + kernel spec root path
347+ assert . strictEqual ( paths . length , 3 , `Expected 3 paths, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
348+
349+ // First path should be from JUPYTER_PATH
340350 assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( Uri . file ( __filename ) , 'kernels' ) . toString ( ) ) ;
341- assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
351+
352+ // Second path should be from data directory (.jupyter/data/kernels)
353+ assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , '.jupyter' , 'data' , 'kernels' ) . toString ( ) ) ;
354+
355+ // Third path should be the kernel spec root path
356+ assert . strictEqual ( paths [ 2 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
342357 } ) ;
343358 test ( 'Get kernelspec root paths on Windows with JUPYTER_PATH & ALLUSERSPROFILE env variable' , async function ( ) {
344359 when ( platformService . osType ) . thenReturn ( OSType . Windows ) ;
@@ -351,12 +366,215 @@ suite('Jupyter Paths', () => {
351366 const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
352367 const winJupyterPath = path . join ( 'AppData' , 'Roaming' , 'jupyter' , 'kernels' ) ;
353368
354- assert . strictEqual ( paths . length , 3 , `Expected 3 path, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
369+ // New implementation returns JUPYTER_PATH kernels + data dirs + PROGRAMDATA + kernel spec root path
370+ assert . strictEqual ( paths . length , 4 , `Expected 4 paths, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
371+
372+ // First path should be from JUPYTER_PATH
355373 assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( Uri . file ( __filename ) , 'kernels' ) . toString ( ) ) ;
356- assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
374+
375+ // Second path should be from data directory (.jupyter/data/kernels)
376+ assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , '.jupyter' , 'data' , 'kernels' ) . toString ( ) ) ;
377+
378+ // Third path should be from PROGRAMDATA
357379 assert . strictEqual (
358380 paths [ 2 ] . toString ( ) ,
359381 Uri . file ( path . join ( allUserProfilePath , 'jupyter' , 'kernels' ) ) . toString ( )
360382 ) ;
383+
384+ // Fourth path should be the kernel spec root path
385+ assert . strictEqual ( paths [ 3 ] . toString ( ) , Uri . joinPath ( windowsHomeDir , winJupyterPath ) . toString ( ) ) ;
386+ } ) ;
387+
388+ test ( 'Get kernelspec root paths on Linux' , async ( ) => {
389+ when ( platformService . osType ) . thenReturn ( OSType . Linux ) ;
390+ when ( platformService . isWindows ) . thenReturn ( false ) ;
391+ when ( platformService . isMac ) . thenReturn ( false ) ;
392+ when ( platformService . homeDir ) . thenReturn ( unixHomeDir ) ;
393+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
394+
395+ // Clear environment variables that might affect the test
396+ delete process . env [ 'XDG_DATA_HOME' ] ;
397+ delete process . env [ 'JUPYTER_DATA_DIR' ] ;
398+
399+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
400+
401+ // Should include data dirs + system paths, and possibly kernel spec root path
402+ assert . isAtLeast ( paths . length , 3 , `Expected at least 3 paths, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
403+
404+ // First path should be from data directory (without XDG_DATA_HOME, defaults to ~/.local/share/jupyter)
405+ assert . strictEqual (
406+ paths [ 0 ] . toString ( ) ,
407+ Uri . joinPath ( unixHomeDir , '.local' , 'share' , 'jupyter' , 'kernels' ) . toString ( )
408+ ) ;
409+
410+ // Should include system paths
411+ const pathStrings = paths . map ( ( p ) => p . toString ( ) ) ;
412+ assert . include ( pathStrings , Uri . file ( '/usr/share/jupyter/kernels' ) . toString ( ) ) ;
413+ assert . include ( pathStrings , Uri . file ( '/usr/local/share/jupyter/kernels' ) . toString ( ) ) ;
414+
415+ // May include kernel spec root path if available
416+ const hasKernelSpecRootPath = pathStrings . some ( ( p ) => p . includes ( linuxJupyterPath ) ) ;
417+ if ( hasKernelSpecRootPath ) {
418+ assert . include ( pathStrings , Uri . joinPath ( unixHomeDir , linuxJupyterPath ) . toString ( ) ) ;
419+ }
420+ } ) ;
421+
422+ test ( 'Get kernelspec root paths on macOS' , async ( ) => {
423+ when ( platformService . osType ) . thenReturn ( OSType . OSX ) ;
424+ when ( platformService . isWindows ) . thenReturn ( false ) ;
425+ when ( platformService . isMac ) . thenReturn ( true ) ;
426+ when ( platformService . homeDir ) . thenReturn ( macHomeDir ) ;
427+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
428+
429+ // Clear environment variables that might affect the test
430+ delete process . env [ 'XDG_DATA_HOME' ] ;
431+ delete process . env [ 'JUPYTER_DATA_DIR' ] ;
432+
433+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
434+
435+ // Should include at least 3 paths: data dir + system paths
436+ assert . isAtLeast ( paths . length , 3 , `Expected at least 3 paths, got ${ paths . length } , ${ JSON . stringify ( paths ) } ` ) ;
437+
438+ // First path should be from data directory (macOS uses ~/Library/Jupyter)
439+ assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( macHomeDir , 'Library' , 'Jupyter' , 'kernels' ) . toString ( ) ) ;
440+
441+ // Should include system paths
442+ const pathStrings = paths . map ( ( p ) => p . toString ( ) ) ;
443+ assert . include ( pathStrings , Uri . file ( '/usr/share/jupyter/kernels' ) . toString ( ) ) ;
444+ assert . include ( pathStrings , Uri . file ( '/usr/local/share/jupyter/kernels' ) . toString ( ) ) ;
445+ } ) ;
446+
447+ test ( 'Get kernelspec root paths on Linux with Python interpreter' , async ( ) => {
448+ when ( platformService . osType ) . thenReturn ( OSType . Linux ) ;
449+ when ( platformService . isWindows ) . thenReturn ( false ) ;
450+ when ( platformService . isMac ) . thenReturn ( false ) ;
451+ when ( platformService . homeDir ) . thenReturn ( unixHomeDir ) ;
452+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
453+
454+ // Mock getDataDirs to return additional interpreter-specific paths
455+ const mockDataDirs = [
456+ Uri . joinPath ( unixHomeDir , '.local' , 'share' , 'jupyter' ) ,
457+ Uri . joinPath ( Uri . file ( sysPrefix ) , 'share' , 'jupyter' )
458+ ] ;
459+ sinon . stub ( jupyterPaths , 'getDataDirs' ) . resolves ( mockDataDirs ) ;
460+
461+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
462+
463+ // Should include interpreter-specific data dirs converted to kernel paths
464+ const pathStrings = paths . map ( ( p ) => p . toString ( ) ) ;
465+ assert . include ( pathStrings , Uri . joinPath ( unixHomeDir , '.local' , 'share' , 'jupyter' , 'kernels' ) . toString ( ) ) ;
466+ assert . include ( pathStrings , Uri . joinPath ( Uri . file ( sysPrefix ) , 'share' , 'jupyter' , 'kernels' ) . toString ( ) ) ;
467+
468+ sinon . restore ( ) ;
469+ } ) ;
470+
471+ test ( 'Get kernelspec root paths handles cancellation token' , async ( ) => {
472+ when ( platformService . osType ) . thenReturn ( OSType . Windows ) ;
473+ when ( platformService . homeDir ) . thenReturn ( windowsHomeDir ) ;
474+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
475+
476+ // Cancel the token immediately
477+ cancelToken . cancel ( ) ;
478+
479+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
480+
481+ // Should return empty array when cancelled
482+ assert . strictEqual ( paths . length , 0 , `Expected empty array when cancelled, got ${ paths . length } paths` ) ;
483+ } ) ;
484+
485+ test ( 'Get kernelspec root paths handles missing home directory gracefully' , async ( ) => {
486+ when ( platformService . osType ) . thenReturn ( OSType . Linux ) ;
487+ when ( platformService . isWindows ) . thenReturn ( false ) ;
488+ when ( platformService . isMac ) . thenReturn ( false ) ;
489+ when ( platformService . homeDir ) . thenReturn ( undefined ) ; // No home directory
490+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
491+
492+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
493+
494+ // Should still return system paths even without home directory
495+ const pathStrings = paths . map ( ( p ) => p . toString ( ) ) ;
496+ assert . include ( pathStrings , Uri . file ( '/usr/share/jupyter/kernels' ) . toString ( ) ) ;
497+ assert . include ( pathStrings , Uri . file ( '/usr/local/share/jupyter/kernels' ) . toString ( ) ) ;
498+ } ) ;
499+
500+ test ( 'Get kernelspec root paths deduplicates paths' , async ( ) => {
501+ when ( platformService . osType ) . thenReturn ( OSType . Windows ) ;
502+ when ( platformService . homeDir ) . thenReturn ( windowsHomeDir ) ;
503+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
504+
505+ // Create a scenario where paths might be duplicated
506+ const duplicatePath = Uri . joinPath ( windowsHomeDir , '.jupyter' , 'data' ) ;
507+ const mockDataDirs = [
508+ duplicatePath ,
509+ duplicatePath , // Duplicate
510+ Uri . joinPath ( windowsHomeDir , 'AppData' , 'Roaming' , 'jupyter' )
511+ ] ;
512+ sinon . stub ( jupyterPaths , 'getDataDirs' ) . resolves ( mockDataDirs ) ;
513+
514+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
515+
516+ // Should not contain duplicate paths
517+ const pathStrings = paths . map ( ( p ) => p . toString ( ) ) ;
518+ const uniquePaths = [ ...new Set ( pathStrings ) ] ;
519+ assert . strictEqual ( pathStrings . length , uniquePaths . length , 'Paths should be deduplicated' ) ;
520+
521+ sinon . restore ( ) ;
522+ } ) ;
523+
524+ test ( 'Get kernelspec root paths with cached data' , async ( ) => {
525+ const cachedPaths = [
526+ Uri . joinPath ( windowsHomeDir , 'cached1' , 'kernels' ) . toString ( ) ,
527+ Uri . joinPath ( windowsHomeDir , 'cached2' , 'kernels' ) . toString ( )
528+ ] ;
529+ when ( platformService . osType ) . thenReturn ( OSType . Windows ) ;
530+ when ( platformService . homeDir ) . thenReturn ( windowsHomeDir ) ;
531+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( cachedPaths ) ;
532+
533+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
534+
535+ // Should return cached data if available
536+ assert . strictEqual ( paths . length , 2 ) ;
537+ assert . strictEqual ( paths [ 0 ] . toString ( ) , cachedPaths [ 0 ] ) ;
538+ assert . strictEqual ( paths [ 1 ] . toString ( ) , cachedPaths [ 1 ] ) ;
539+ } ) ;
540+
541+ test ( 'Get kernelspec root paths with JUPYTER_PATH on Linux' , async ( ) => {
542+ when ( platformService . osType ) . thenReturn ( OSType . Linux ) ;
543+ when ( platformService . isWindows ) . thenReturn ( false ) ;
544+ when ( platformService . isMac ) . thenReturn ( false ) ;
545+ when ( platformService . homeDir ) . thenReturn ( unixHomeDir ) ;
546+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
547+
548+ const jupyter_Paths = [ '/custom/jupyter/path1' , '/custom/jupyter/path2' ] ;
549+ process . env [ 'JUPYTER_PATH' ] = jupyter_Paths . join ( path . delimiter ) ;
550+
551+ const paths = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
552+
553+ // First paths should be from JUPYTER_PATH with 'kernels' appended
554+ assert . isAtLeast ( paths . length , 2 , `Expected at least 2 paths, got ${ paths . length } ` ) ;
555+ assert . strictEqual ( paths [ 0 ] . toString ( ) , Uri . joinPath ( Uri . file ( jupyter_Paths [ 0 ] ) , 'kernels' ) . toString ( ) ) ;
556+ assert . strictEqual ( paths [ 1 ] . toString ( ) , Uri . joinPath ( Uri . file ( jupyter_Paths [ 1 ] ) , 'kernels' ) . toString ( ) ) ;
557+ } ) ;
558+
559+ test ( 'Enhanced caching behavior works correctly' , async ( ) => {
560+ when ( platformService . osType ) . thenReturn ( OSType . Windows ) ;
561+ when ( platformService . homeDir ) . thenReturn ( windowsHomeDir ) ;
562+ when ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . thenReturn ( [ ] ) ;
563+
564+ // First call
565+ const paths1 = await jupyterPaths . getKernelSpecRootPaths ( cancelToken . token ) ;
566+
567+ // Verify caching method is called
568+ verify ( memento . get ( CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS , anything ( ) ) ) . atLeast ( 1 ) ;
569+
570+ // Second call should use cached result (simulate by using same token)
571+ const newCancelToken = new CancellationTokenSource ( ) ;
572+ disposables . push ( newCancelToken ) ;
573+ const paths2 = await jupyterPaths . getKernelSpecRootPaths ( newCancelToken . token ) ;
574+
575+ assert . strictEqual ( paths1 . length , paths2 . length ) ;
576+ paths1 . forEach ( ( path , index ) => {
577+ assert . strictEqual ( path . toString ( ) , paths2 [ index ] . toString ( ) ) ;
578+ } ) ;
361579 } ) ;
362580} ) ;
0 commit comments