Repository: ipython/ipython

Issue:
notebook.start_kernel is undefined
Hi, in notificationarea.js, when we need to restart a kernel we call `notebook.start_kernel` which doesn't exist.  Not sure how you guys were planning to handle this, but I took a stab at it.

thanks!



Discussion:
Good catch ! I think we can just ask the session to restart the kernel :-) 

I think `that.notebook.session.kernel` is undefined if we never successfully connected to the kernel in the first place

> I think that.notebook.session.kernel is undefined if we never successfully connected to the kernel in the first place

Then imho this is a bug. Star/restart should be almost synonymes. @takluyver ? 


Resolving patch:
diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py
--- a/IPython/html/services/sessions/handlers.py
+++ b/IPython/html/services/sessions/handlers.py
@@ -106,7 +106,11 @@ def patch(self, session_id):
     def delete(self, session_id):
         # Deletes the session with given session_id
         sm = self.session_manager
-        sm.delete_session(session_id)
+        try:
+            sm.delete_session(session_id)
+        except KeyError:
+            # the kernel was deleted but the session wasn't!
+            raise web.HTTPError(410, "Kernel deleted before session")
         self.set_status(204)
         self.finish()
 


Test patch:
diff --git a/IPython/html/tests/services/kernel.js b/IPython/html/tests/services/kernel.js
--- a/IPython/html/tests/services/kernel.js
+++ b/IPython/html/tests/services/kernel.js
@@ -3,20 +3,111 @@
 // Kernel tests
 //
 casper.notebook_test(function () {
+    // test that the kernel is running
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'kernel is running');
+    });
+
+    // test list
+    this.thenEvaluate(function () {
+        IPython._kernels = null;
+        IPython.notebook.kernel.list(function (data) {
+            IPython._kernels = data;
+        });
+    });
+    this.waitFor(function () {
+        return this.evaluate(function () {
+            return IPython._kernels !== null;
+        });
+    });
+    this.then(function () {
+        var num_kernels = this.evaluate(function () {
+            return IPython._kernels.length;
+        });
+        this.test.assertEquals(num_kernels, 1, 'one kernel running');
+    });
+    
+    // test get_info
+    var kernel_info = this.evaluate(function () {
+        return {
+            name: IPython.notebook.kernel.name,
+            id: IPython.notebook.kernel.id
+        };
+    });
+    this.thenEvaluate(function () {
+        IPython._kernel_info = null;
+        IPython.notebook.kernel.get_info(function (data) {
+            IPython._kernel_info = data;
+        });
+    });
+    this.waitFor(function () {
+        return this.evaluate(function () {
+            return IPython._kernel_info !== null;
+        });
+    });
+    this.then(function () {
+        var new_kernel_info = this.evaluate(function () {
+            return IPython._kernel_info;
+        });
+        this.test.assertEquals(kernel_info.name, new_kernel_info.name, 'kernel: name correct');
+        this.test.assertEquals(kernel_info.id, new_kernel_info.id, 'kernel: id correct');
+    });
+
+    // test interrupt
+    this.thenEvaluate(function () {
+        IPython._interrupted = false;
+        IPython.notebook.kernel.interrupt(function () {
+            IPython._interrupted = true;
+        });
+    });
+    this.waitFor(function () {
+        return this.evaluate(function () {
+            return IPython._interrupted;
+        });
+    });
+    this.then(function () {
+        var interrupted = this.evaluate(function () {
+            return IPython._interrupted;
+        });
+        this.test.assert(interrupted, 'kernel was interrupted');
+    });
+
+    // test restart
+    this.thenEvaluate(function () {
+        IPython.notebook.kernel.restart();
+    });
+    this.waitFor(this.kernel_disconnected);
+    this.wait_for_kernel_ready();
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'kernel restarted');
+    });
+
+    // test reconnect
+    this.thenEvaluate(function () {
+        IPython.notebook.kernel.stop_channels();
+    });
+    this.waitFor(this.kernel_disconnected);
+    this.thenEvaluate(function () {
+        IPython.notebook.kernel.reconnect();
+    });
+    this.wait_for_kernel_ready();
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'kernel reconnected');
+    });
+
+    // test kernel_info_request
     this.evaluate(function () {
         IPython.notebook.kernel.kernel_info(
             function(msg){
                 IPython._kernel_info_response = msg;
-            })
+            });
     });
-
     this.waitFor(
         function () {
             return this.evaluate(function(){
                 return IPython._kernel_info_response;
         });
     });
-
     this.then(function () {
         var kernel_info_response =  this.evaluate(function(){
             return IPython._kernel_info_response;
@@ -24,41 +115,203 @@ casper.notebook_test(function () {
         this.test.assertTrue( kernel_info_response.msg_type === 'kernel_info_reply', 'Kernel info request return kernel_info_reply');
         this.test.assertTrue( kernel_info_response.content !== undefined, 'Kernel_info_reply is not undefined');
     });
-    
+
+    // test kill
     this.thenEvaluate(function () {
-        var kernel = IPython.notebook.session.kernel;
-        IPython._channels = [
-            kernel.shell_channel,
-            kernel.iopub_channel,
-            kernel.stdin_channel
-        ];
-        kernel.kill();
+        IPython.notebook.kernel.kill();
     });
-    
-    this.waitFor(function () {
-        return this.evaluate(function(){
-            for (var i=0; i < IPython._channels.length; i++) {
-                var ws = IPython._channels[i];
-                if (ws.readyState !== ws.CLOSED) {
-                    return false;
-                }
-            }
-            return true;
+    this.waitFor(this.kernel_disconnected);
+    this.then(function () {
+        this.test.assert(!this.kernel_running(), 'kernel is not running');
+    });
+
+    // test start
+    var url;
+    this.then(function () {
+        url = this.evaluate(function () {
+            return IPython.notebook.kernel.start();
         });
     });
-    
     this.then(function () {
-        var states = this.evaluate(function() {
-            var states = [];
-            for (var i = 0; i < IPython._channels.length; i++) {
-                states.push(IPython._channels[i].readyState);
-            }
-            return states;
+        this.test.assertEquals(url, "/api/kernels", "start url is correct");
+    });
+    this.wait_for_kernel_ready();
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'kernel is running');
+    });
+
+    // test start with parameters
+    this.thenEvaluate(function () {
+        IPython.notebook.kernel.kill();
+    });
+    this.waitFor(this.kernel_disconnected);
+    this.then(function () {
+        url = this.evaluate(function () {
+            return IPython.notebook.kernel.start({foo: "bar"});
         });
-        
-        for (var i = 0; i < states.length; i++) {
-            this.test.assertEquals(states[i], WebSocket.CLOSED,
-                "Kernel.kill closes websockets[" + i + "]");
+    });
+    this.then(function () {
+        this.test.assertEquals(url, "/api/kernels?foo=bar", "start url with params is correct");
+    });
+    this.wait_for_kernel_ready();
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'kernel is running');
+    });
+
+    // check for events in kill/start cycle
+    this.event_test(
+        'kill/start',
+        [
+            'kernel_killed.Kernel',
+            'kernel_created.Kernel',
+            'kernel_connected.Kernel',
+            'kernel_starting.Kernel',
+            'kernel_ready.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel.kill();
+            });
+            this.waitFor(this.kernel_disconnected);
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel.start();
+            });
         }
+    );
+    // wait for any last idle/busy messages to be handled
+    this.wait_for_kernel_ready();
+
+    // check for events in disconnect/connect cycle
+    this.event_test(
+        'reconnect',
+        [
+            'kernel_reconnecting.Kernel',
+            'kernel_connected.Kernel',
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel.stop_channels();
+                IPython.notebook.kernel.reconnect(1);
+            });
+        }
+    );
+    // wait for any last idle/busy messages to be handled
+    this.wait_for_kernel_ready();
+
+    // check for events in the restart cycle
+    this.event_test(
+        'restart',
+        [
+            'kernel_restarting.Kernel',
+            'kernel_created.Kernel',
+            'kernel_connected.Kernel',
+            'kernel_starting.Kernel',
+            'kernel_ready.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel.restart();
+            });
+        }
+    );
+    // wait for any last idle/busy messages to be handled
+    this.wait_for_kernel_ready();
+
+    // check for events in the interrupt cycle
+    this.event_test(
+        'interrupt',
+        [
+            'kernel_interrupting.Kernel',
+            'kernel_busy.Kernel',
+            'kernel_idle.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel.interrupt();
+            });
+        }
+    );
+    this.wait_for_kernel_ready();
+
+    // check for events after ws close
+    this.event_test(
+        'ws_closed_ok',
+        [
+            'kernel_disconnected.Kernel',
+            'kernel_reconnecting.Kernel',
+            'kernel_connected.Kernel',
+            'kernel_busy.Kernel',
+            'kernel_idle.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel._ws_closed("", false);
+            });
+        }
+    );
+    // wait for any last idle/busy messages to be handled
+    this.wait_for_kernel_ready();
+
+    // check for events after ws close (error)
+    this.event_test(
+        'ws_closed_error',
+        [
+            'kernel_disconnected.Kernel',
+            'kernel_connection_failed.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.kernel._ws_closed("", true);
+            });
+        }
+    );
+
+    // start the kernel back up
+    this.thenEvaluate(function () {
+        IPython.notebook.kernel.restart();
     });
+    this.waitFor(this.kernel_running);
+    this.wait_for_kernel_ready();
+
+    // test handling of autorestarting messages
+    this.event_test(
+        'autorestarting',
+        [
+            'kernel_restarting.Kernel',
+            'kernel_autorestarting.Kernel',
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                var cell = IPython.notebook.get_cell(0);
+                cell.set_text('import os\n' + 'os._exit(1)');
+                cell.execute();
+            });
+        }
+    );
+    this.wait_for_kernel_ready();
+
+    // test handling of failed restart
+    this.event_test(
+        'failed_restart',
+        [
+            'kernel_restarting.Kernel',
+            'kernel_autorestarting.Kernel',
+            'kernel_dead.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                var cell = IPython.notebook.get_cell(0);
+                cell.set_text("import os\n" +
+                              "from IPython.kernel.connect import get_connection_file\n" +
+                              "with open(get_connection_file(), 'w') as f:\n" +
+                              "    f.write('garbage')\n" +
+                              "os._exit(1)");
+                cell.execute();
+            });
+        }, 
+
+        // need an extra-long timeout, because it needs to try
+        // restarting the kernel 5 times!
+        20000
+    );
 });
diff --git a/IPython/html/tests/services/session.js b/IPython/html/tests/services/session.js
--- a/IPython/html/tests/services/session.js
+++ b/IPython/html/tests/services/session.js
@@ -4,40 +4,179 @@
 //
 
 casper.notebook_test(function () {
-    this.evaluate(function () {
-        var kernel = IPython.notebook.session.kernel;
-        IPython._channels = [
-            kernel.shell_channel,
-            kernel.iopub_channel,
-            kernel.stdin_channel
-        ];
-        IPython.notebook.session.delete();
+    var that = this;
+    var get_info = function () {
+        return that.evaluate(function () {
+            return JSON.parse(JSON.stringify(IPython.notebook.session._get_model()));
+        });
+    };
+
+    // test that the kernel is running
+    this.then(function () {
+        this.test.assert(this.kernel_running(), 'session: kernel is running');
+    });
+
+    // test list
+    this.thenEvaluate(function () {
+        IPython._sessions = null;
+        IPython.notebook.session.list(function (data) {
+            IPython._sessions = data;
+        });
     });
-    
     this.waitFor(function () {
-        return this.evaluate(function(){
-            for (var i=0; i < IPython._channels.length; i++) {
-                var ws = IPython._channels[i];
-                if (ws.readyState !== ws.CLOSED) {
-                    return false;
-                }
-            }
-            return true;
+        return this.evaluate(function () {
+            return IPython._sessions !== null;
+        });
+    });
+    this.then(function () {
+        var num_sessions = this.evaluate(function () {
+            return IPython._sessions.length;
         });
+        this.test.assertEquals(num_sessions, 1, 'one session running');
     });
 
+    // test get_info
+    var session_info = get_info();
+    this.thenEvaluate(function () {
+        IPython._session_info = null;
+        IPython.notebook.session.get_info(function (data) {
+            IPython._session_info = data;
+        });
+    });
+    this.waitFor(function () {
+        return this.evaluate(function () {
+            return IPython._session_info !== null;
+        });
+    });
     this.then(function () {
-        var states = this.evaluate(function() {
-            var states = [];
-            for (var i = 0; i < IPython._channels.length; i++) {
-                states.push(IPython._channels[i].readyState);
-            }
-            return states;
+        var new_session_info = this.evaluate(function () {
+            return IPython._session_info;
         });
-        
-        for (var i = 0; i < states.length; i++) {
-            this.test.assertEquals(states[i], WebSocket.CLOSED,
-                "Session.delete closes websockets[" + i + "]");
-        }
+        this.test.assertEquals(session_info.notebook.name, new_session_info.notebook.name, 'session: notebook name correct');
+        this.test.assertEquals(session_info.notebook.path, new_session_info.notebook.path, 'session: notebook path correct');
+        this.test.assertEquals(session_info.kernel.name, new_session_info.kernel.name, 'session: kernel name correct');
+        this.test.assertEquals(session_info.kernel.id, new_session_info.kernel.id, 'session: kernel id correct');
     });
+
+    // test rename_notebook
+    //
+    // TODO: the PATCH request isn't supported by phantom, so this test always
+    // fails, see https://github.com/ariya/phantomjs/issues/11384
+    // when this is fixed we can properly run this test
+    //
+    // this.thenEvaluate(function () {
+    //     IPython._renamed = false;
+    //     IPython.notebook.session.rename_notebook(
+    //         "foo",
+    //         "bar",
+    //         function (data) {
+    //             IPython._renamed = true;
+    //         }
+    //     );
+    // });
+    // this.waitFor(function () {
+    //     return this.evaluate(function () {
+    //         return IPython._renamed;
+    //     });
+    // });
+    // this.then(function () {
+    //     var info = get_info();
+    //     this.test.assertEquals(info.notebook.name, "foo", "notebook was renamed");
+    //     this.test.assertEquals(info.notebook.path, "bar", "notebook path was changed");
+    // });
+
+    // test delete
+    this.thenEvaluate(function () {
+        IPython.notebook.session.delete();
+    });
+    this.waitFor(this.kernel_disconnected);
+    this.then(function () {
+        this.test.assert(!this.kernel_running(), 'session deletes kernel');
+    });
+
+    // check for events when starting the session
+    this.event_test(
+        'start_session',
+        [
+            'kernel_created.Session',
+            'kernel_connected.Kernel',
+            'kernel_starting.Kernel',
+            'kernel_ready.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.session.start();
+            });
+        }
+    );
+    this.wait_for_kernel_ready();
+
+    // check for events when killing the session
+    this.event_test(
+        'delete_session',
+        ['kernel_killed.Session'],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.session.delete();
+            });
+        }
+    );
+
+    // check for events when restarting the session
+    this.event_test(
+        'restart_session',
+        [
+            'kernel_killed.Session',
+            'kernel_created.Session',
+            'kernel_connected.Kernel',
+            'kernel_starting.Kernel',
+            'kernel_ready.Kernel'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.session.restart();
+            });
+        }
+    );
+    this.wait_for_kernel_ready();
+
+    // test handling of failed restart
+    this.event_test(
+        'failed_restart',
+        [
+            'kernel_restarting.Kernel',
+            'kernel_autorestarting.Kernel',
+            'kernel_killed.Session',
+            'kernel_dead.Kernel',
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                var cell = IPython.notebook.get_cell(0);
+                cell.set_text("import os\n" +
+                              "from IPython.kernel.connect import get_connection_file\n" +
+                              "with open(get_connection_file(), 'w') as f:\n" +
+                              "    f.write('garbage')\n" +
+                              "os._exit(1)");
+                cell.execute();
+            });
+        },
+
+        // need an extra-long timeout, because it needs to try
+        // restarting the kernel 5 times!
+        20000
+    );
+
+    // check for events when starting a nonexistant kernel
+    this.event_test(
+        'bad_start_session',
+        [
+            'kernel_killed.Session',
+            'kernel_dead.Session'
+        ],
+        function () {
+            this.thenEvaluate(function () {
+                IPython.notebook.session.restart({kernel_name: 'foo'});
+            });
+        }
+    );
 });
diff --git a/IPython/html/tests/util.js b/IPython/html/tests/util.js
--- a/IPython/html/tests/util.js
+++ b/IPython/html/tests/util.js
@@ -28,10 +28,10 @@ casper.open_new_notebook = function () {
     this.thenEvaluate(function () {
         require(['base/js/namespace', 'base/js/events'], function (IPython, events) {
         
-            events.on('status_idle.Kernel',function () {
+            events.on('kernel_idle.Kernel',function () {
                 IPython._status = 'idle';
             });
-            events.on('status_busy.Kernel',function () {
+            events.on('kernel_busy.Kernel',function () {
                 IPython._status = 'busy';
             });
         });
@@ -58,7 +58,29 @@ casper.page_loaded = function() {
 casper.kernel_running = function() {
     // Return whether or not the kernel is running.
     return this.evaluate(function() {
-        return IPython.notebook.kernel.running;
+        return IPython.notebook.kernel.is_connected();
+    });
+};
+
+casper.kernel_disconnected = function() {
+    return this.evaluate(function() {
+        return IPython.notebook.kernel.is_fully_disconnected();
+    });
+};
+
+casper.wait_for_kernel_ready = function () {
+    this.waitFor(this.kernel_running);
+    this.thenEvaluate(function () {
+        IPython._kernel_ready = false;
+        IPython.notebook.kernel.kernel_info(
+            function () {
+                IPython._kernel_ready = true;
+            });
+    });
+    this.waitFor(function () {
+        return this.evaluate(function () {
+            return IPython._kernel_ready;
+        });
     });
 };
 
@@ -560,6 +582,61 @@ casper.dashboard_test = function (test) {
     });
 };
 
+// note that this will only work for UNIQUE events -- if you want to
+// listen for the same event twice, this will not work!
+casper.event_test = function (name, events, action, timeout) {
+
+    // set up handlers to listen for each of the events
+    this.thenEvaluate(function (events) {
+        var make_handler = function (event) {
+            return function () {
+                IPython._events_triggered.push(event);
+                IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
+                delete IPython._event_handlers[event];
+            };
+        };
+        IPython._event_handlers = {};
+        IPython._events_triggered = [];
+        for (var i=0; i < events.length; i++) {
+            IPython._event_handlers[events[i]] = make_handler(events[i]);
+            IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]);
+        }
+    }, [events]);
+
+    // execute the requested action
+    this.then(action);
+
+    // wait for all the events to be triggered
+    this.waitFor(function () {
+        return this.evaluate(function (events) {
+            return IPython._events_triggered.length >= events.length;
+        }, [events]);
+    }, undefined, undefined, timeout);
+
+    // test that the events were triggered in the proper order
+    this.then(function () {
+        var triggered = this.evaluate(function () {
+            return IPython._events_triggered;
+        });
+        var handlers = this.evaluate(function () {
+            return Object.keys(IPython._event_handlers);
+        });
+        this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered');
+        this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered');
+        for (var i=0; i < events.length; i++) {
+            this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered');
+        }
+    });
+
+    // turn off any remaining event listeners
+    this.thenEvaluate(function () {
+        for (var event in IPython._event_handlers) {
+            IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
+            delete IPython._event_handlers[event];
+        }
+    });
+};
+
 casper.options.waitTimeout=10000;
 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
     this.echo("Timeout for " + casper.get_notebook_server());