From 1913f033be16b7a1779a013ac111727f810ce552 Mon Sep 17 00:00:00 2001 From: yanganto Date: Fri, 27 Dec 2019 16:05:35 +0800 Subject: [PATCH] Support eval and exec mode in wasm - Expose `pyEval`, `pyExec`, `pyExecSingle` in wasm module --- wasm/demo/src/main.js | 2 +- wasm/lib/src/lib.rs | 72 ++++++++++++++++++++++++++---------- wasm/lib/src/vm_class.rs | 2 +- wasm/tests/test_exec_mode.py | 46 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 wasm/tests/test_exec_mode.py diff --git a/wasm/demo/src/main.js b/wasm/demo/src/main.js index 48587640b43..2565d3ee2c0 100644 --- a/wasm/demo/src/main.js +++ b/wasm/demo/src/main.js @@ -36,7 +36,7 @@ function runCodeFromTextarea() { const code = editor.getValue(); try { - rp.pyEval(code, { + rp.pyExec(code, { stdout: output => { const shouldScroll = consoleElement.scrollHeight - consoleElement.scrollTop === diff --git a/wasm/lib/src/lib.rs b/wasm/lib/src/lib.rs index b1f55d36bc2..d6cbaae8bdb 100644 --- a/wasm/lib/src/lib.rs +++ b/wasm/lib/src/lib.rs @@ -8,11 +8,13 @@ extern crate futures; extern crate js_sys; #[macro_use] extern crate rustpython_vm; +extern crate rustpython_compiler; extern crate wasm_bindgen; extern crate wasm_bindgen_futures; extern crate web_sys; use js_sys::{Object, Reflect, TypeError}; +use rustpython_compiler::compile::Mode; use std::panic; use wasm_bindgen::prelude::*; @@ -47,14 +49,35 @@ pub fn setup_console_error() { #[wasm_bindgen(typescript_custom_section)] const TS_CMT_START: &'static str = "/*"; +fn run_py(source: &str, options: Option, mode: Mode) -> Result { + let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true)); + let options = options.unwrap_or_else(Object::new); + let js_vars = { + let prop = Reflect::get(&options, &"vars".into())?; + if prop.is_undefined() { + None + } else if prop.is_object() { + Some(Object::from(prop)) + } else { + return Err(TypeError::new("vars must be an object").into()); + } + }; + + vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?; + + if let Some(js_vars) = js_vars { + vm.add_to_scope("js_vars".into(), js_vars.into())?; + } + vm.run(source, mode) +} #[wasm_bindgen(js_name = pyEval)] /// Evaluate Python code /// /// ```js -/// pyEval(code, options?); +/// var result = pyEval(code, options?); /// ``` /// -/// `code`: `string`: The Python code to run +/// `code`: `string`: The Python code to run in eval mode /// /// `options`: /// @@ -66,26 +89,35 @@ const TS_CMT_START: &'static str = "/*"; /// `undefined` or "console", and it will be a dumb function when giving null. pub fn eval_py(source: &str, options: Option) -> Result { - let options = options.unwrap_or_else(Object::new); - let js_vars = { - let prop = Reflect::get(&options, &"vars".into())?; - if prop.is_undefined() { - None - } else if prop.is_object() { - Some(Object::from(prop)) - } else { - return Err(TypeError::new("vars must be an object").into()); - } - }; - let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true)); - - vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?; + run_py(source, options, Mode::Eval) +} - if let Some(js_vars) = js_vars { - vm.add_to_scope("js_vars".into(), js_vars.into())?; - } +#[wasm_bindgen(js_name = pyExec)] +/// Evaluate Python code +/// +/// ```js +/// pyExec(code, options?); +/// ``` +/// +/// `code`: `string`: The Python code to run in exec mode +/// +/// `options`: The options are the same as eval mode +pub fn exec_py(source: &str, options: Option) { + let _ = run_py(source, options, Mode::Exec); +} - vm.exec(source) +#[wasm_bindgen(js_name = pyExecSingle)] +/// Evaluate Python code +/// +/// ```js +/// var result = pyExecSingle(code, options?); +/// ``` +/// +/// `code`: `string`: The Python code to run in exec single mode +/// +/// `options`: The options are the same as eval mode +pub fn exec_single_py(source: &str, options: Option) -> Result { + run_py(source, options, Mode::Single) } #[wasm_bindgen(typescript_custom_section)] diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs index 3f9a318fa50..b9fa910f1dc 100644 --- a/wasm/lib/src/vm_class.rs +++ b/wasm/lib/src/vm_class.rs @@ -278,7 +278,7 @@ impl WASMVirtualMachine { })? } - fn run(&self, source: &str, mode: compile::Mode) -> Result { + pub(crate) fn run(&self, source: &str, mode: compile::Mode) -> Result { self.assert_valid()?; self.with_unchecked( |StoredVirtualMachine { diff --git a/wasm/tests/test_exec_mode.py b/wasm/tests/test_exec_mode.py new file mode 100644 index 00000000000..1914f102e50 --- /dev/null +++ b/wasm/tests/test_exec_mode.py @@ -0,0 +1,46 @@ +import time +import sys + +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +import pytest + +def print_stack(driver): + stack = driver.execute_script( + "return window.__RUSTPYTHON_ERROR_MSG + '\\n' + window.__RUSTPYTHON_ERROR_STACK" + ) + print(f"RustPython error stack:\n{stack}", file=sys.stderr) + + +@pytest.fixture(scope="module") +def driver(request): + options = Options() + options.add_argument('-headless') + driver = webdriver.Firefox(options=options) + try: + driver.get("http://localhost:8080") + except Exception as e: + print_stack(driver) + raise + time.sleep(5) + yield driver + driver.close() + + +def test_eval_mode(driver): + assert driver.execute_script("return window.rp.pyEval('1+1')") == 2 + +def test_exec_mode(driver): + assert driver.execute_script("return window.rp.pyExec('1+1')") is None + +def test_exec_single_mode(driver): + assert driver.execute_script("return window.rp.pyExecSingle('1+1')") == 2 + assert driver.execute_script( + """ + var output = []; + save_output = function(text) {{ + output.push(text) + }}; + window.rp.pyExecSingle('1+1\\n2+2',{stdout: save_output}); + return output; + """) == ['2\n', '4\n']