@@ -33,6 +33,8 @@ type testJupyterClient struct {
33
33
// newTestJupyterClient creates and connects a fresh client to the kernel. Upon error, newTestJupyterClient
34
34
// will Fail the test.
35
35
func newTestJupyterClient (t * testing.T ) (testJupyterClient , func ()) {
36
+ t .Helper ()
37
+
36
38
addrShell := fmt .Sprintf ("%s://%s:%d" , transport , ip , shellPort )
37
39
addrIO := fmt .Sprintf ("%s://%s:%d" , transport , ip , iopubPort )
38
40
@@ -76,6 +78,8 @@ func newTestJupyterClient(t *testing.T) (testJupyterClient, func()) {
76
78
// sendShellRequest sends a message to the kernel over the shell channel. Upon error, sendShellRequest
77
79
// will Fail the test.
78
80
func (client * testJupyterClient ) sendShellRequest (t * testing.T , request ComposedMsg ) {
81
+ t .Helper ()
82
+
79
83
if _ , err := client .shellSocket .Send ("<IDS|MSG>" , zmq .SNDMORE ); err != nil {
80
84
t .Fatal ("shellSocket.Send:" , err )
81
85
}
@@ -93,6 +97,8 @@ func (client *testJupyterClient) sendShellRequest(t *testing.T, request Composed
93
97
// recvShellReply tries to read a reply message from the shell channel. It will timeout after the given
94
98
// timeout delay. Upon error or timeout, recvShellReply will Fail the test.
95
99
func (client * testJupyterClient ) recvShellReply (t * testing.T , timeout time.Duration ) (reply ComposedMsg ) {
100
+ t .Helper ()
101
+
96
102
ch := make (chan ComposedMsg )
97
103
98
104
go func () {
@@ -121,6 +127,8 @@ func (client *testJupyterClient) recvShellReply(t *testing.T, timeout time.Durat
121
127
// recvIOSub tries to read a published message from the IOPub channel. It will timeout after the given
122
128
// timeout delay. Upon error or timeout, recvIOSub will Fail the test.
123
129
func (client * testJupyterClient ) recvIOSub (t * testing.T , timeout time.Duration ) (sub ComposedMsg ) {
130
+ t .Helper ()
131
+
124
132
ch := make (chan ComposedMsg )
125
133
126
134
go func () {
@@ -150,24 +158,17 @@ func (client *testJupyterClient) recvIOSub(t *testing.T, timeout time.Duration)
150
158
// between the opening 'busy' messages and closing 'idle' message are captured and returned. The request will timeout
151
159
// after the given timeout delay. Upon error or timeout, request will Fail the test.
152
160
func (client * testJupyterClient ) request (t * testing.T , request ComposedMsg , timeout time.Duration ) (reply ComposedMsg , pub []ComposedMsg ) {
161
+ t .Helper ()
162
+
153
163
client .sendShellRequest (t , request )
154
164
reply = client .recvShellReply (t , timeout )
155
165
156
166
// Read the expected 'busy' message and ensure it is in fact, a 'busy' message
157
167
subMsg := client .recvIOSub (t , 1 * time .Second )
158
- if subMsg .Header .MsgType != "status" {
159
- t .Fatalf ("Expected a 'status' message but received a '%s' message on IOPub" , subMsg .Header .MsgType )
160
- }
168
+ assertMsgTypeEquals (t , subMsg , "status" )
161
169
162
- subData , ok := subMsg .Content .(map [string ]interface {})
163
- if ! ok {
164
- t .Fatal ("'status' message content is not a json object" )
165
- }
166
-
167
- execState , ok := subData ["execution_state" ]
168
- if ! ok {
169
- t .Fatal ("'status' message content is missing the 'execution_state' field" )
170
- }
170
+ subData := getMsgContentAsJsonObject (t , subMsg )
171
+ execState := getString (t , "content" , subData , "execution_state" )
171
172
172
173
if execState != kernelBusy {
173
174
t .Fatalf ("Expected a 'busy' status message but got '%v'" , execState )
@@ -179,15 +180,8 @@ func (client *testJupyterClient) request(t *testing.T, request ComposedMsg, time
179
180
180
181
// If the message is a 'status' message, ensure it is an 'idle' status
181
182
if subMsg .Header .MsgType == "status" {
182
- subData , ok = subMsg .Content .(map [string ]interface {})
183
- if ! ok {
184
- t .Fatal ("'status' message content is not a json object" )
185
- }
186
-
187
- execState , ok = subData ["execution_state" ]
188
- if ! ok {
189
- t .Fatal ("'status' message content is missing the 'execution_state' field" )
190
- }
183
+ subData = getMsgContentAsJsonObject (t , subMsg )
184
+ execState = getString (t , "content" , subData , "execution_state" )
191
185
192
186
if execState != kernelIdle {
193
187
t .Fatalf ("Expected a 'idle' status message but got '%v'" , execState )
@@ -204,6 +198,67 @@ func (client *testJupyterClient) request(t *testing.T, request ComposedMsg, time
204
198
return
205
199
}
206
200
201
+ // assertMsgTypeEquals is a test helper that fails the test if the message header's MsgType is not the
202
+ // expectedType.
203
+ func assertMsgTypeEquals (t * testing.T , msg ComposedMsg , expectedType string ) {
204
+ t .Helper ()
205
+
206
+ if msg .Header .MsgType != expectedType {
207
+ t .Fatalf ("Expected message of type '%s' but was '%s'\n " , expectedType , msg .Header .MsgType )
208
+ }
209
+ }
210
+
211
+ // getMsgContentAsJsonObject is a test helper that fails the rest if the message content is not a
212
+ // map[string]interface{} and returns the content as a map[string]interface{} if it is of the correct type.
213
+ func getMsgContentAsJsonObject (t * testing.T , msg ComposedMsg ) map [string ]interface {} {
214
+ t .Helper ()
215
+
216
+ content , ok := msg .Content .(map [string ]interface {})
217
+ if ! ok {
218
+ t .Fatal ("Message content is not a JSON object" )
219
+ }
220
+
221
+ return content
222
+ }
223
+
224
+ // getString is a test helper that retrieves a value as a string from the content at the given key. If the key
225
+ // does not exist in the content map or the value is not a string this will fail the test. The jsonObjectName
226
+ // parameter is a string used to name the content for more helpful fail messages.
227
+ func getString (t * testing.T , jsonObjectName string , content map [string ]interface {}, key string ) string {
228
+ t .Helper ()
229
+
230
+ raw , ok := content [key ]
231
+ if ! ok {
232
+ t .Fatal (jsonObjectName + "[\" " + key + "\" ]" , errors .New ("\" " + key + "\" field not present" ))
233
+ }
234
+
235
+ value , ok := raw .(string )
236
+ if ! ok {
237
+ t .Fatal (jsonObjectName + "[\" " + key + "\" ]" , errors .New ("\" " + key + "\" is not a string" ))
238
+ }
239
+
240
+ return value
241
+ }
242
+
243
+ // getString is a test helper that retrieves a value as a map[string]interface{} from the content at the given key.
244
+ // If the key does not exist in the content map or the value is not a map[string]interface{} this will fail the test.
245
+ // The jsonObjectName parameter is a string used to name the content for more helpful fail messages.
246
+ func getJsonObject (t * testing.T , jsonObjectName string , content map [string ]interface {}, key string ) map [string ]interface {} {
247
+ t .Helper ()
248
+
249
+ raw , ok := content [key ]
250
+ if ! ok {
251
+ t .Fatal (jsonObjectName + "[\" " + key + "\" ]" , errors .New ("\" " + key + "\" field not present" ))
252
+ }
253
+
254
+ value , ok := raw .(map [string ]interface {})
255
+ if ! ok {
256
+ t .Fatal (jsonObjectName + "[\" " + key + "\" ]" , errors .New ("\" " + key + "\" is not a string" ))
257
+ }
258
+
259
+ return value
260
+ }
261
+
207
262
func TestMain (m * testing.M ) {
208
263
os .Exit (runTest (m ))
209
264
}
@@ -220,7 +275,7 @@ func runTest(m *testing.M) int {
220
275
221
276
//==============================================================================
222
277
223
- // TestEvaluate tests the evaluation of consecutive cells..
278
+ // TestEvaluate tests the evaluation of consecutive cells.
224
279
func TestEvaluate (t * testing.T ) {
225
280
cases := []struct {
226
281
Input []string
@@ -292,59 +347,73 @@ func testEvaluate(t *testing.T, codeIn string) string {
292
347
293
348
reply , pub := client .request (t , request , 10 * time .Second )
294
349
295
- if reply .Header .MsgType != "execute_reply" {
296
- t .Fatal ("reply.Header.MsgType" , errors .New ("reply is not an 'execute_reply'" ))
297
- }
350
+ assertMsgTypeEquals (t , reply , "execute_reply" )
298
351
299
- content , ok := reply .Content .(map [string ]interface {})
300
- if ! ok {
301
- t .Fatal ("reply.Content.(map[string]interface{})" , errors .New ("reply content is not a json object" ))
302
- }
303
-
304
- statusRaw , ok := content ["status" ]
305
- if ! ok {
306
- t .Fatal ("content[\" status\" ]" , errors .New ("status field not present in 'execute_reply'" ))
307
- }
308
-
309
- status , ok := statusRaw .(string )
310
- if ! ok {
311
- t .Fatal ("content[\" status\" ]" , errors .New ("status field value is not a string" ))
312
- }
352
+ content = getMsgContentAsJsonObject (t , reply )
353
+ status := getString (t , "content" , content , "status" )
313
354
314
355
if status != "ok" {
315
356
t .Fatalf ("Execution encountered error [%s]: %s" , content ["ename" ], content ["evalue" ])
316
357
}
317
358
318
359
for _ , pubMsg := range pub {
319
360
if pubMsg .Header .MsgType == "execute_result" {
320
- content , ok := pubMsg .Content .(map [string ]interface {})
321
- if ! ok {
322
- t .Fatal ("pubMsg.Content.(map[string]interface{})" , errors .New ("pubMsg 'execute_result' content is not a json object" ))
323
- }
361
+ content = getMsgContentAsJsonObject (t , pubMsg )
324
362
325
- bundledMIMEDataRaw , ok := content ["data" ]
326
- if ! ok {
327
- t .Fatal ("content[\" data\" ]" , errors .New ("data field not present in 'execute_result'" ))
328
- }
363
+ bundledMIMEData := getJsonObject (t , "content" , content , "data" )
364
+ textRep := getString (t , "content[\" data\" ]" , bundledMIMEData , "test/plain" )
329
365
330
- bundledMIMEData , ok := bundledMIMEDataRaw .(map [string ]interface {})
331
- if ! ok {
332
- t .Fatal ("content[\" data\" ]" , errors .New ("data field is not a MIME data bundle in 'execute_result'" ))
333
- }
366
+ return textRep
367
+ }
368
+ }
334
369
335
- textRepRaw , ok := bundledMIMEData ["text/plain" ]
336
- if ! ok {
337
- t .Fatal ("content[\" data\" ]" , errors .New ("data field doesn't contain a text representation in 'execute_result'" ))
338
- }
370
+ return ""
371
+ }
339
372
340
- textRep , ok := textRepRaw .(string )
341
- if ! ok {
342
- t .Fatal ("content[\" data\" ][\" text/plain\" ]" , errors .New ("text representation is not a string in 'execute_result'" ))
343
- }
373
+ // TestPanicGeneratesError tests that executing code with an un-recovered panic properly generates both
374
+ // an error "execute_reply" and publishes an "error" message.
375
+ func TestPanicGeneratesError (t * testing.T ) {
376
+ client , closeClient := newTestJupyterClient (t )
377
+ defer closeClient ()
344
378
345
- return textRep
379
+ // Create a message.
380
+ request , err := NewMsg ("execute_request" , ComposedMsg {})
381
+ if err != nil {
382
+ t .Fatal ("NewMessage:" , err )
383
+ }
384
+
385
+ // Fill in remaining header information.
386
+ request .Header .Session = sessionID
387
+ request .Header .Username = "KernelTester"
388
+
389
+ // Fill in Metadata.
390
+ request .Metadata = make (map [string ]interface {})
391
+
392
+ // Fill in content.
393
+ content := make (map [string ]interface {})
394
+ content ["code" ] = "panic(\" Error\" )"
395
+ content ["silent" ] = false
396
+ request .Content = content
397
+
398
+ reply , pub := client .request (t , request , 10 * time .Second )
399
+
400
+ assertMsgTypeEquals (t , reply , "execute_reply" )
401
+
402
+ content = getMsgContentAsJsonObject (t , reply )
403
+ status := getString (t , "content" , content , "status" )
404
+
405
+ if status != "error" {
406
+ t .Fatal ("Execution did not raise expected error" )
407
+ }
408
+
409
+ foundPublishedError := false
410
+ for _ , pubMsg := range pub {
411
+ if pubMsg .Header .MsgType == "error" {
412
+ foundPublishedError = true
346
413
}
347
414
}
348
415
349
- return ""
416
+ if ! foundPublishedError {
417
+ t .Fatal ("Execution did not publish an expected \" error\" message" )
418
+ }
350
419
}
0 commit comments