1+ // Copyright (c) Microsoft Corporation. All rights reserved.
2+ // Licensed under the MIT License.
3+
14/* eslint-disable no-unused-expressions */
25/* eslint-disable @typescript-eslint/no-explicit-any */
36import * as TypeMoq from 'typemoq' ;
47import * as sinon from 'sinon' ;
5- import { Disposable } from 'vscode' ;
8+ import { Disposable , EventEmitter , NotebookDocument , Uri } from 'vscode' ;
69import { expect } from 'chai' ;
710
811import { IInterpreterService } from '../../client/interpreter/contracts' ;
912import { PythonEnvironment } from '../../client/pythonEnvironments/info' ;
10- import { getNativeRepl , NativeRepl } from '../../client/repl/nativeRepl' ;
13+ import * as NativeReplModule from '../../client/repl/nativeRepl' ;
1114import * as persistentState from '../../client/common/persistentState' ;
15+ import * as PythonServer from '../../client/repl/pythonServer' ;
16+ import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis' ;
17+ import * as replController from '../../client/repl/replController' ;
18+ import { executeCommand } from '../../client/common/vscodeApis/commandApis' ;
1219
1320suite ( 'REPL - Native REPL' , ( ) => {
1421 let interpreterService : TypeMoq . IMock < IInterpreterService > ;
@@ -19,38 +26,51 @@ suite('REPL - Native REPL', () => {
1926 let setReplControllerSpy : sinon . SinonSpy ;
2027 let getWorkspaceStateValueStub : sinon . SinonStub ;
2128 let updateWorkspaceStateValueStub : sinon . SinonStub ;
29+ let createReplControllerStub : sinon . SinonStub ;
30+ let mockNotebookController : any ;
2231
2332 setup ( ( ) => {
33+ ( NativeReplModule as any ) . nativeRepl = undefined ;
34+
35+ mockNotebookController = {
36+ id : 'mockController' ,
37+ dispose : sinon . stub ( ) ,
38+ updateNotebookAffinity : sinon . stub ( ) ,
39+ createNotebookCellExecution : sinon . stub ( ) ,
40+ variableProvider : null ,
41+ } ;
42+
2443 interpreterService = TypeMoq . Mock . ofType < IInterpreterService > ( ) ;
2544 interpreterService
2645 . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
2746 . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
2847 disposable = TypeMoq . Mock . ofType < Disposable > ( ) ;
2948 disposableArray = [ disposable . object ] ;
3049
31- setReplDirectoryStub = sinon . stub ( NativeRepl . prototype as any , 'setReplDirectory ' ) . resolves ( ) ; // Stubbing private method
32- // Use a spy instead of a stub for setReplController
33- setReplControllerSpy = sinon . spy ( NativeRepl . prototype , 'setReplController' ) ;
50+ createReplControllerStub = sinon . stub ( replController , 'createReplController ' ) . returns ( mockNotebookController ) ;
51+ setReplDirectoryStub = sinon . stub ( NativeReplModule . NativeRepl . prototype as any , 'setReplDirectory' ) . resolves ( ) ;
52+ setReplControllerSpy = sinon . spy ( NativeReplModule . NativeRepl . prototype , 'setReplController' ) ;
3453 updateWorkspaceStateValueStub = sinon . stub ( persistentState , 'updateWorkspaceStateValue' ) . resolves ( ) ;
3554 } ) ;
3655
37- teardown ( ( ) => {
56+ teardown ( async ( ) => {
3857 disposableArray . forEach ( ( d ) => {
3958 if ( d ) {
4059 d . dispose ( ) ;
4160 }
4261 } ) ;
4362 disposableArray = [ ] ;
4463 sinon . restore ( ) ;
64+ executeCommand ( 'workbench.action.closeActiveEditor' ) ;
4565 } ) ;
4666
4767 test ( 'getNativeRepl should call create constructor' , async ( ) => {
48- const createMethodStub = sinon . stub ( NativeRepl , 'create' ) ;
68+ const createMethodStub = sinon . stub ( NativeReplModule . NativeRepl , 'create' ) ;
4969 interpreterService
5070 . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
5171 . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
5272 const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
53- await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
73+ await NativeReplModule . getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
5474
5575 expect ( createMethodStub . calledOnce ) . to . be . true ;
5676 } ) ;
@@ -61,7 +81,7 @@ suite('REPL - Native REPL', () => {
6181 . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
6282 . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
6383 const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
64- const nativeRepl = await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
84+ const nativeRepl = await NativeReplModule . getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
6585
6686 nativeRepl . sendToNativeRepl ( undefined , false ) ;
6787
@@ -74,7 +94,7 @@ suite('REPL - Native REPL', () => {
7494 . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
7595 . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
7696 const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
77- const nativeRepl = await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
97+ const nativeRepl = await NativeReplModule . getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
7898
7999 nativeRepl . sendToNativeRepl ( undefined , false ) ;
80100
@@ -87,34 +107,81 @@ suite('REPL - Native REPL', () => {
87107 . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
88108 . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
89109
90- await NativeRepl . create ( interpreter as PythonEnvironment ) ;
110+ await NativeReplModule . NativeRepl . create ( interpreter as PythonEnvironment ) ;
91111
92112 expect ( setReplDirectoryStub . calledOnce ) . to . be . true ;
93113 expect ( setReplControllerSpy . calledOnce ) . to . be . true ;
94-
95- setReplDirectoryStub . restore ( ) ;
96- setReplControllerSpy . restore ( ) ;
114+ expect ( createReplControllerStub . calledOnce ) . to . be . true ;
97115 } ) ;
98116
99- test ( 'Closing REPL should dispose of the previous instance and create a new one on reopening' , async ( ) => {
100- const disposeSpy = sinon . spy ( NativeRepl . prototype , 'dispose' ) ;
101- const createStub = sinon . stub ( NativeRepl , 'create' ) . callThrough ( ) ;
102- interpreterService
103- . setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
104- . returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
105- const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
106-
107- const firstRepl = await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
108-
109- firstRepl . dispose ( ) ;
117+ test ( 'watchNotebookClosed should clean up resources when notebook is closed' , async ( ) => {
118+ const notebookCloseEmitter = new EventEmitter < NotebookDocument > ( ) ;
119+ sinon . stub ( vscodeWorkspaceApis , 'onDidCloseNotebookDocument' ) . callsFake ( ( handler ) => {
120+ const disposable = notebookCloseEmitter . event ( handler ) ;
121+ return disposable ;
122+ } ) ;
110123
111- const secondRepl = await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
124+ const mockPythonServer = {
125+ onCodeExecuted : new EventEmitter < void > ( ) . event ,
126+ execute : sinon . stub ( ) . resolves ( { status : true , output : 'test output' } ) ,
127+ executeSilently : sinon . stub ( ) . resolves ( { status : true , output : 'test output' } ) ,
128+ interrupt : sinon . stub ( ) ,
129+ input : sinon . stub ( ) ,
130+ checkValidCommand : sinon . stub ( ) . resolves ( true ) ,
131+ dispose : sinon . stub ( ) ,
132+ } ;
133+
134+ // Track the number of times createPythonServer was called
135+ let createPythonServerCallCount = 0 ;
136+ sinon . stub ( PythonServer , 'createPythonServer' ) . callsFake ( ( ) => {
137+ // eslint-disable-next-line no-plusplus
138+ createPythonServerCallCount ++ ;
139+ return mockPythonServer ;
140+ } ) ;
112141
113- expect ( disposeSpy . calledOnce ) . to . be . true ;
114- expect ( createStub . calledTwice ) . to . be . true ;
115- expect ( firstRepl ) . to . not . equal ( secondRepl ) ;
142+ const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
116143
117- disposeSpy . restore ( ) ;
118- createStub . restore ( ) ;
144+ // Create NativeRepl directly to have more control over its state, go around private constructor.
145+ const nativeRepl = new ( NativeReplModule . NativeRepl as any ) ( ) ;
146+ nativeRepl . interpreter = interpreter as PythonEnvironment ;
147+ nativeRepl . cwd = '/helloJustMockedCwd/cwd' ;
148+ nativeRepl . pythonServer = mockPythonServer ;
149+ nativeRepl . replController = mockNotebookController ;
150+ nativeRepl . disposables = [ ] ;
151+
152+ // Make the singleton point to our instance for testing
153+ // Otherwise, it gets mixed with Native Repl from .create from test above.
154+ ( NativeReplModule as any ) . nativeRepl = nativeRepl ;
155+
156+ // Reset call count after initial setup
157+ createPythonServerCallCount = 0 ;
158+
159+ // Set notebookDocument to a mock document
160+ const mockReplUri = Uri . parse ( 'untitled:Untitled-999.ipynb?jupyter-notebook' ) ;
161+ const mockNotebookDocument = ( {
162+ uri : mockReplUri ,
163+ toString : ( ) => mockReplUri . toString ( ) ,
164+ } as unknown ) as NotebookDocument ;
165+
166+ nativeRepl . notebookDocument = mockNotebookDocument ;
167+
168+ // Create a mock notebook document for closing event with same URI
169+ const closingNotebookDocument = ( {
170+ uri : mockReplUri ,
171+ toString : ( ) => mockReplUri . toString ( ) ,
172+ } as unknown ) as NotebookDocument ;
173+
174+ notebookCloseEmitter . fire ( closingNotebookDocument ) ;
175+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
176+
177+ expect (
178+ updateWorkspaceStateValueStub . calledWith ( NativeReplModule . NATIVE_REPL_URI_MEMENTO , undefined ) ,
179+ 'updateWorkspaceStateValue should be called with NATIVE_REPL_URI_MEMENTO and undefined' ,
180+ ) . to . be . true ;
181+ expect ( mockPythonServer . dispose . calledOnce , 'pythonServer.dispose() should be called once' ) . to . be . true ;
182+ expect ( createPythonServerCallCount , 'createPythonServer should be called to create a new server' ) . to . equal ( 1 ) ;
183+ expect ( nativeRepl . notebookDocument , 'notebookDocument should be undefined after closing' ) . to . be . undefined ;
184+ expect ( nativeRepl . newReplSession , 'newReplSession should be set to true after closing' ) . to . be . true ;
185+ expect ( mockNotebookController . dispose . calledOnce , 'replController.dispose() should be called once' ) . to . be . true ;
119186 } ) ;
120187} ) ;
0 commit comments