浏览代码

Merge branch 'master' into session

Borys Palka 5 年之前
父节点
当前提交
2d676facd0
共有 7 个文件被更改,包括 218 次插入25 次删除
  1. 19 0
      DESIGN.md
  2. 2 6
      README.md
  3. 1 2
      azure-pipelines.yml
  4. 10 10
      package.json
  5. 1 0
      src/tokens.ts
  6. 21 5
      tests/run-test.py
  7. 164 2
      tests/src/session.spec.ts

+ 19 - 0
DESIGN.md

@@ -0,0 +1,19 @@
+# `@jupyterlab/debugger` user interactions and experience
+
+- The debugger UI will only ever exist as a single instance within JupyterLab:
+  - An expanded mode which is a `MainAreaWidget<Debugger>`
+    - In expanded mode, the debugger will contain a tab panel for text editors that are launched when a user steps into code that has been stopped via breakpoints.
+    - If the user adds a breakpoint and steps into code from the sidebar, then it should automatically switch to expanded mode to accommodate displaying code files.
+    - Code files in the debugger will _always_ be read-only. They should support adding breakpoints.
+  - A condensed mode which is a condensed sidebar view of the debugger within the `right` or `left` areas of the JupyterLab shell
+- The debugger will support debugging and inspecting environment for three types of activities in JupyterLab:
+  1. Notebooks
+  1. Code consoles
+  1. Text editors that are backed by a kernel
+- A `JupyterFrontendPlugin` will exist to track each of the activities and each time the `currentChanged` signal fires on one of the trackers, the debugger UI will reflect the state of that activity, _e.g._:
+  - If a user has a single open notebook and opens the debugger, it will open either "docked" in the sidebar or in the main area depending on the last known position of the debugger.
+  - If the user then opens a new notebook or switches to a code console, then the debugger will update to show the state of the newly focused kernel.
+- The debugger should be state-less insofar as it can arbitrarily switch to displaying the breakpoints and variables of a new kernel based on a user switching from one notebook to another, _etc._:
+  - The debugger should automatically start a debugging session with the kernel transparently without end-user intervention.
+  - Any UI information that cannot be retrieved from the kernel needs to be stored in a different channel (for example, inside a `StateDB`) or discarded. In particular, if the kernel cannot return a list of breakpoints that have been set, then it becomes the debugger UI's responsibility to rehydrate and dehydrate these as needed.
+  - When the application is `restored` or when a debugger UI is instantiated, it should appear docked or expanded (this can perhaps be `type Debugger.Mode = 'condensed' | 'expanded'`) based on its last known state from a previous session.

+ 2 - 6
README.md

@@ -7,19 +7,17 @@ This extension is under active development and is not yet available.
 ## Prerequisites
 
 - JupyterLab 1.1+
+- xeus-python 0.5+
 
 ## Development
 
 ```bash
 # Create a new conda environment
-conda create -n jupyterlab-debugger -c conda-forge jupyterlab nodejs xeus-python ptvsd
+conda create -n jupyterlab-debugger -c conda-forge jupyterlab nodejs xeus-python=0.5 ptvsd
 
 # Activate the conda environment
 conda activate jupyterlab-debugger
 
-# Create a directory for the kernel debug logs in the folder where JupyterLab is started
-mkdir xpython_debug_logs
-
 # Install dependencies
 jlpm
 
@@ -41,8 +39,6 @@ XEUS_LOG=1 jupyter lab --no-browser --watch
 
 ### Tests
 
-Make sure `xeus-python` is installed and `jupyter --paths` points to where the kernel is installed.
-
 To run the tests:
 
 ```bash

+ 1 - 2
azure-pipelines.yml

@@ -16,7 +16,7 @@ steps:
 
 - bash: |
     source activate jupyterlab-debugger
-    conda install --yes --quiet -c conda-forge nodejs xeus-python ptvsd python=$PYTHON_VERSION
+    conda install --yes --quiet -c conda-forge nodejs xeus-python=0.5 ptvsd python=$PYTHON_VERSION
     python -m pip install -U --pre jupyterlab
   displayName: Install dependencies
 
@@ -28,7 +28,6 @@ steps:
 
 - bash: |
     source activate jupyterlab-debugger
-    export JUPYTER_PATH=${CONDA_PREFIX}/share/jupyter
     export XEUS_LOG=1
     jlpm run test
   displayName: Run the tests

+ 10 - 10
package.json

@@ -39,15 +39,15 @@
     "watch": "tsc -b --watch"
   },
   "dependencies": {
-    "@jupyterlab/application": "^1.1.0-alpha.1",
-    "@jupyterlab/apputils": "^1.1.0-alpha.1",
-    "@jupyterlab/codeeditor": "^1.1.0-alpha.1",
-    "@jupyterlab/console": "^1.1.0-alpha.1",
-    "@jupyterlab/coreutils": "^3.1.0-alpha.1",
-    "@jupyterlab/fileeditor": "^1.1.0-alpha.1",
-    "@jupyterlab/launcher": "^1.1.0-alpha.1",
-    "@jupyterlab/notebook": "^1.1.0-alpha.1",
-    "@jupyterlab/services": "^4.1.0-alpha.1",
+    "@jupyterlab/application": "^1.1.0",
+    "@jupyterlab/apputils": "^1.1.0",
+    "@jupyterlab/codeeditor": "^1.1.0",
+    "@jupyterlab/console": "^1.1.0",
+    "@jupyterlab/coreutils": "^3.1.0",
+    "@jupyterlab/fileeditor": "^1.1.0",
+    "@jupyterlab/launcher": "^1.1.0",
+    "@jupyterlab/notebook": "^1.1.0",
+    "@jupyterlab/services": "^4.1.0",
     "@phosphor/algorithm": "^1.2.0",
     "@phosphor/coreutils": "^1.3.1",
     "@phosphor/disposable": "^1.2.0",
@@ -58,7 +58,7 @@
   "devDependencies": {
     "@babel/core": "^7.5.5",
     "@babel/preset-env": "^7.5.5",
-    "@jupyterlab/testutils": "^1.1.0-alpha.1",
+    "@jupyterlab/testutils": "^1.1.0",
     "@types/chai": "^4.1.3",
     "@types/jest": "^24.0.17",
     "chai": "^4.2.0",

+ 1 - 0
src/tokens.ts

@@ -114,6 +114,7 @@ export namespace IDebugger {
       terminateThreads: DebugProtocol.TerminateThreadsArguments;
       threads: {};
       updateCell: IUpdateCellArguments;
+      variables: DebugProtocol.VariablesArguments;
     };
 
     /**

+ 21 - 5
tests/run-test.py

@@ -2,13 +2,29 @@
 # Distributed under the terms of the Modified BSD License.
 
 import os
-from jupyterlab.tests.test_app import run_jest, JestApp
+
+from jupyterlab.tests.test_app import JestApp
 
 HERE = os.path.realpath(os.path.dirname(__file__))
 
-if __name__ == '__main__':
-    # xeus-python requires the xpython_debug_logs folder
+
+def run(jest_dir):
     jest_app = JestApp.instance()
-    os.mkdir(os.path.join(jest_app.notebook_dir, 'xpython_debug_logs'))
+    jest_app.jest_dir = jest_dir
+    jest_app.initialize()
+    jest_app.install_kernel(
+        kernel_name='xpython',
+        kernel_spec={
+            'argv': [
+                'xpython',
+                '-f', '{connection_file}'
+            ],
+            'display_name': 'xpython',
+            'language': 'python'
+        }
+    )
+    jest_app.start()
 
-    run_jest(HERE)
+
+if __name__ == '__main__':
+    run(HERE)

+ 164 - 2
tests/src/session.spec.ts

@@ -5,7 +5,15 @@ import { expect } from 'chai';
 
 import { ClientSession, IClientSession } from '@jupyterlab/apputils';
 
-import { createClientSession } from '@jupyterlab/testutils';
+import { createClientSession, sleep } from '@jupyterlab/testutils';
+
+import { find } from '@phosphor/algorithm';
+
+import { PromiseDelegate } from '@phosphor/coreutils';
+
+import { DebugProtocol } from 'vscode-debugprotocol';
+
+import { IDebugger } from '../../lib/tokens';
 
 import { DebugSession } from '../../lib/session';
 
@@ -72,7 +80,7 @@ describe('DebugSession', () => {
       expect(reply.body.sourcePath).to.contain('.py');
     });
 
-    it('should handle replies with success false', async () => {
+    it.skip('should handle replies with success false', async () => {
       const reply = await debugSession.sendRequest('evaluate', {
         expression: 'a'
       });
@@ -83,3 +91,157 @@ describe('DebugSession', () => {
     });
   });
 });
+
+describe('protocol', () => {
+  const code = [
+    'i = 0',
+    'i += 1',
+    'i += 1',
+    'j = i**2',
+    'j += 1',
+    'print(i, j)'
+  ].join('\n');
+
+  const breakpoints: DebugProtocol.SourceBreakpoint[] = [
+    { line: 3 },
+    { line: 5 }
+  ];
+
+  let client: IClientSession;
+  let debugSession: DebugSession;
+  let threadId: number = 1;
+
+  beforeEach(async () => {
+    client = await createClientSession({
+      kernelPreference: {
+        name: 'xpython'
+      }
+    });
+    await (client as ClientSession).initialize();
+    await client.kernel.ready;
+    debugSession = new DebugSession({ client });
+    await debugSession.start();
+
+    debugSession.eventMessage.connect(
+      (sender: DebugSession, event: IDebugger.ISession.Event) => {
+        const eventName = event.event;
+        if (eventName === 'thread') {
+          const msg = event as DebugProtocol.ThreadEvent;
+          threadId = msg.body.threadId;
+        }
+      }
+    );
+
+    const reply = await debugSession.sendRequest('updateCell', {
+      cellId: 0,
+      nextId: 1,
+      code
+    });
+    await debugSession.sendRequest('setBreakpoints', {
+      breakpoints,
+      source: { path: reply.body.sourcePath },
+      sourceModified: false
+    });
+    await debugSession.sendRequest('configurationDone', {});
+
+    // trigger an execute_request
+    client.kernel.requestExecute({ code });
+
+    // TODO: handle events instead
+    await sleep(2000);
+  });
+
+  afterEach(async () => {
+    await debugSession.stop();
+    debugSession.dispose();
+    await client.shutdown();
+    client.dispose();
+  });
+
+  describe('#stackTrace', () => {
+    it('should return the correct stackframes', async () => {
+      const reply = await debugSession.sendRequest('stackTrace', {
+        threadId
+      });
+      expect(reply.success).to.be.true;
+      const stackFrames = reply.body.stackFrames;
+      expect(stackFrames.length).to.equal(2);
+      const frame = stackFrames[0];
+      // first breakpoint
+      expect(frame.line).to.equal(3);
+    });
+  });
+
+  describe('#scopes', () => {
+    it('should return the correct scopes', async () => {
+      const stackFramesReply = await debugSession.sendRequest('stackTrace', {
+        threadId
+      });
+      const frameId = stackFramesReply.body.stackFrames[0].id;
+      const scopesReply = await debugSession.sendRequest('scopes', {
+        frameId
+      });
+      const scopes = scopesReply.body.scopes;
+      expect(scopes.length).to.equal(1);
+      expect(scopes[0].name).to.equal('Locals');
+    });
+  });
+
+  const getVariables = async () => {
+    const stackFramesReply = await debugSession.sendRequest('stackTrace', {
+      threadId
+    });
+    const frameId = stackFramesReply.body.stackFrames[0].id;
+    const scopesReply = await debugSession.sendRequest('scopes', {
+      frameId
+    });
+    const scopes = scopesReply.body.scopes;
+    const variablesReference = scopes[0].variablesReference;
+    const variablesReply = await debugSession.sendRequest('variables', {
+      variablesReference
+    });
+    return variablesReply.body.variables;
+  };
+
+  describe('#variables', () => {
+    it('should return the variables and their values', async () => {
+      const variables = await getVariables();
+      expect(variables.length).to.be.greaterThan(0);
+      const i = find(variables, variable => variable.name === 'i');
+      expect(i).to.exist;
+      expect(i.type).to.equal('int');
+      expect(i.value).to.equal('1');
+    });
+  });
+
+  describe('#continue', () => {
+    it.skip('should proceed to the next breakpoint', async () => {
+      let events: string[] = [];
+      const eventsFuture = new PromiseDelegate<string[]>();
+      debugSession.eventMessage.connect((sender, event) => {
+        events.push(event.event);
+        // aggregate the next 2 debug events
+        if (events.length === 2) {
+          eventsFuture.resolve(events);
+        }
+      });
+
+      await debugSession.sendRequest('continue', { threadId });
+
+      // wait for debug events
+      const debugEvents = await eventsFuture.promise;
+      expect(debugEvents).to.deep.equal(['continued', 'stopped']);
+
+      const variables = await getVariables();
+      const i = find(variables, variable => variable.name === 'i');
+      expect(i).to.exist;
+      expect(i.type).to.equal('int');
+      expect(i.value).to.equal('2');
+
+      const j = find(variables, variable => variable.name === 'j');
+      expect(j).to.exist;
+      expect(j.type).to.equal('int');
+      expect(j.value).to.equal('4');
+    });
+  });
+});