1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
1
4
/* eslint-disable no-unused-expressions */
2
5
/* eslint-disable @typescript-eslint/no-explicit-any */
3
6
import * as TypeMoq from 'typemoq' ;
4
7
import * as sinon from 'sinon' ;
5
- import { Disposable } from 'vscode' ;
8
+ import { Disposable , EventEmitter , NotebookDocument , Uri } from 'vscode' ;
6
9
import { expect } from 'chai' ;
7
10
8
11
import { IInterpreterService } from '../../client/interpreter/contracts' ;
9
12
import { PythonEnvironment } from '../../client/pythonEnvironments/info' ;
10
- import { getNativeRepl , NativeRepl } from '../../client/repl/nativeRepl' ;
13
+ import * as NativeReplModule from '../../client/repl/nativeRepl' ;
11
14
import * 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' ;
12
19
13
20
suite ( 'REPL - Native REPL' , ( ) => {
14
21
let interpreterService : TypeMoq . IMock < IInterpreterService > ;
@@ -19,38 +26,51 @@ suite('REPL - Native REPL', () => {
19
26
let setReplControllerSpy : sinon . SinonSpy ;
20
27
let getWorkspaceStateValueStub : sinon . SinonStub ;
21
28
let updateWorkspaceStateValueStub : sinon . SinonStub ;
29
+ let createReplControllerStub : sinon . SinonStub ;
30
+ let mockNotebookController : any ;
22
31
23
32
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
+
24
43
interpreterService = TypeMoq . Mock . ofType < IInterpreterService > ( ) ;
25
44
interpreterService
26
45
. setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
27
46
. returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
28
47
disposable = TypeMoq . Mock . ofType < Disposable > ( ) ;
29
48
disposableArray = [ disposable . object ] ;
30
49
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' ) ;
34
53
updateWorkspaceStateValueStub = sinon . stub ( persistentState , 'updateWorkspaceStateValue' ) . resolves ( ) ;
35
54
} ) ;
36
55
37
- teardown ( ( ) => {
56
+ teardown ( async ( ) => {
38
57
disposableArray . forEach ( ( d ) => {
39
58
if ( d ) {
40
59
d . dispose ( ) ;
41
60
}
42
61
} ) ;
43
62
disposableArray = [ ] ;
44
63
sinon . restore ( ) ;
64
+ executeCommand ( 'workbench.action.closeActiveEditor' ) ;
45
65
} ) ;
46
66
47
67
test ( 'getNativeRepl should call create constructor' , async ( ) => {
48
- const createMethodStub = sinon . stub ( NativeRepl , 'create' ) ;
68
+ const createMethodStub = sinon . stub ( NativeReplModule . NativeRepl , 'create' ) ;
49
69
interpreterService
50
70
. setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
51
71
. returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
52
72
const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
53
- await getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
73
+ await NativeReplModule . getNativeRepl ( interpreter as PythonEnvironment , disposableArray ) ;
54
74
55
75
expect ( createMethodStub . calledOnce ) . to . be . true ;
56
76
} ) ;
@@ -61,7 +81,7 @@ suite('REPL - Native REPL', () => {
61
81
. setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
62
82
. returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
63
83
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 ) ;
65
85
66
86
nativeRepl . sendToNativeRepl ( undefined , false ) ;
67
87
@@ -74,7 +94,7 @@ suite('REPL - Native REPL', () => {
74
94
. setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
75
95
. returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
76
96
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 ) ;
78
98
79
99
nativeRepl . sendToNativeRepl ( undefined , false ) ;
80
100
@@ -87,12 +107,81 @@ suite('REPL - Native REPL', () => {
87
107
. setup ( ( i ) => i . getActiveInterpreter ( TypeMoq . It . isAny ( ) ) )
88
108
. returns ( ( ) => Promise . resolve ( ( { path : 'ps' } as unknown ) as PythonEnvironment ) ) ;
89
109
90
- await NativeRepl . create ( interpreter as PythonEnvironment ) ;
110
+ await NativeReplModule . NativeRepl . create ( interpreter as PythonEnvironment ) ;
91
111
92
112
expect ( setReplDirectoryStub . calledOnce ) . to . be . true ;
93
113
expect ( setReplControllerSpy . calledOnce ) . to . be . true ;
114
+ expect ( createReplControllerStub . calledOnce ) . to . be . true ;
115
+ } ) ;
116
+
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
+ } ) ;
123
+
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
+ } ) ;
141
+
142
+ const interpreter = await interpreterService . object . getActiveInterpreter ( ) ;
94
143
95
- setReplDirectoryStub . restore ( ) ;
96
- setReplControllerSpy . 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 ;
97
186
} ) ;
98
187
} ) ;
0 commit comments