mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-27 02:00:24 -04:00
Introduce a Plugin system, implement <py-terminal> as a plugin (#917)
This PR does two things: 1. introduce the first minimal version of a Plugin system. Plugins are subclasses of the Plugin class, and pyscript will call the relevant methods/hooks at the right time. Currently, I have added only the minimal sets of hooks which were needed for this PR. 2. Implement <py-terminal> as a plugin.
This commit is contained in:
@@ -3,6 +3,7 @@ import './styles/pyscript_base.css';
|
||||
import { loadConfigFromElement } from './pyconfig';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
import { type Plugin, PluginManager } from './plugin';
|
||||
import { make_PyScript, initHandlers, mountElements } from './components/pyscript';
|
||||
import { PyLoader } from './components/pyloader';
|
||||
import { PyodideRuntime } from './pyodide';
|
||||
@@ -11,6 +12,8 @@ import { handleFetchError, showWarning, globalExport } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
import { createCustomElements } from './components/elements';
|
||||
import { UserError, withUserErrorHandler } from "./exceptions"
|
||||
import { type Stdio, StdioMultiplexer, DEFAULT_STDIO } from './stdio';
|
||||
import { PyTerminalPlugin } from './plugins/pyterminal';
|
||||
|
||||
type ImportType = { [key: string]: unknown };
|
||||
type ImportMapType = {
|
||||
@@ -35,6 +38,8 @@ const logger = getLogger('pyscript/main');
|
||||
|
||||
6. setup the environment, install packages
|
||||
|
||||
6.5: call the Plugin.afterSetup() hook
|
||||
|
||||
7. connect the py-script web component. This causes the execution of all the
|
||||
user scripts
|
||||
|
||||
@@ -51,16 +56,33 @@ More concretely:
|
||||
- PyScriptApp.afterRuntimeLoad() implements all the points >= 5.
|
||||
*/
|
||||
|
||||
class PyScriptApp {
|
||||
|
||||
|
||||
export class PyScriptApp {
|
||||
config: AppConfig;
|
||||
loader: PyLoader;
|
||||
runtime: Runtime;
|
||||
PyScript: any; // XXX would be nice to have a more precise type for the class itself
|
||||
plugins: PluginManager;
|
||||
_stdioMultiplexer: StdioMultiplexer;
|
||||
|
||||
constructor() {
|
||||
// initialize the builtin plugins
|
||||
this.plugins = new PluginManager();
|
||||
this.plugins.add(new PyTerminalPlugin(this));
|
||||
|
||||
this._stdioMultiplexer = new StdioMultiplexer();
|
||||
this._stdioMultiplexer.addListener(DEFAULT_STDIO);
|
||||
}
|
||||
|
||||
// lifecycle (1)
|
||||
main() {
|
||||
this.loadConfig();
|
||||
this.showLoader();
|
||||
this.plugins.configure(this.config);
|
||||
|
||||
this.showLoader(); // this should be a plugin
|
||||
this.plugins.beforeLaunch(this.config);
|
||||
|
||||
this.loadRuntime();
|
||||
}
|
||||
|
||||
@@ -108,7 +130,11 @@ class PyScriptApp {
|
||||
showWarning('Multiple runtimes are not supported yet.<br />Only the first will be used');
|
||||
}
|
||||
const runtime_cfg = this.config.runtimes[0];
|
||||
this.runtime = new PyodideRuntime(this.config, runtime_cfg.src, runtime_cfg.name, runtime_cfg.lang);
|
||||
this.runtime = new PyodideRuntime(this.config,
|
||||
this._stdioMultiplexer,
|
||||
runtime_cfg.src,
|
||||
runtime_cfg.name,
|
||||
runtime_cfg.lang);
|
||||
this.loader.log(`Downloading ${runtime_cfg.name}...`);
|
||||
const script = document.createElement('script'); // create a script DOM node
|
||||
script.src = this.runtime.src;
|
||||
@@ -138,6 +164,9 @@ class PyScriptApp {
|
||||
await this.setupVirtualEnv(runtime);
|
||||
await mountElements(runtime);
|
||||
|
||||
// lifecycle (6.5)
|
||||
this.plugins.afterSetup(runtime);
|
||||
|
||||
this.loader.log('Executing <py-script> tags...');
|
||||
this.executeScripts(runtime);
|
||||
|
||||
@@ -195,6 +224,12 @@ class PyScriptApp {
|
||||
customElements.define('py-script', this.PyScript);
|
||||
}
|
||||
|
||||
// ================= registraton API ====================
|
||||
|
||||
registerStdioListener(stdio: Stdio) {
|
||||
this._stdioMultiplexer.addListener(stdio);
|
||||
}
|
||||
|
||||
async register_importmap(runtime: Runtime) {
|
||||
// make importmap ES modules available from python using 'import'.
|
||||
//
|
||||
|
||||
70
pyscriptjs/src/plugin.ts
Normal file
70
pyscriptjs/src/plugin.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { PyScriptApp } from './main';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Runtime } from './runtime';
|
||||
|
||||
export class Plugin {
|
||||
|
||||
/** Validate the configuration of the plugin and handle default values.
|
||||
*
|
||||
* Individual plugins are expected to check that the config keys/sections
|
||||
* which are relevant to them contains valid values, and to raise an error
|
||||
* if they contains unknown keys.
|
||||
*
|
||||
* This is also a good place where set default values for those keys which
|
||||
* are not specified by the user.
|
||||
*
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
configure(config: AppConfig) {
|
||||
}
|
||||
|
||||
/** The preliminary initialization phase is complete and we are about to
|
||||
* download and launch the Python interpreter.
|
||||
*
|
||||
* We can assume that the page is already shown to the user and that the
|
||||
* DOM content has been loaded. This is a good place where to add tags to
|
||||
* the DOM, if needed.
|
||||
*
|
||||
* This hook should **NOT** contain expensive operations, else it delays
|
||||
* the download of the python interpreter which is initiated later.
|
||||
*/
|
||||
beforeLaunch(config: AppConfig) {
|
||||
}
|
||||
|
||||
/** The Python interpreter has been launched, the virtualenv has been
|
||||
* installed and we are ready to execute user code.
|
||||
*
|
||||
* The <py-script> tags will be executed after this hook.
|
||||
*/
|
||||
afterSetup(runtime: Runtime) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PluginManager {
|
||||
_plugins: Plugin[];
|
||||
|
||||
constructor() {
|
||||
this._plugins = [];
|
||||
}
|
||||
|
||||
add(p: Plugin) {
|
||||
this._plugins.push(p);
|
||||
}
|
||||
|
||||
configure(config: AppConfig) {
|
||||
for (const p of this._plugins)
|
||||
p.configure(config);
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
for (const p of this._plugins)
|
||||
p.beforeLaunch(config);
|
||||
}
|
||||
|
||||
afterSetup(runtime: Runtime) {
|
||||
for (const p of this._plugins)
|
||||
p.afterSetup(runtime);
|
||||
}
|
||||
}
|
||||
123
pyscriptjs/src/plugins/pyterminal.ts
Normal file
123
pyscriptjs/src/plugins/pyterminal.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { PyScriptApp } from '../main';
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import { Plugin } from '../plugin';
|
||||
import { UserError } from "../exceptions"
|
||||
import { getLogger } from '../logger';
|
||||
import { type Stdio } from '../stdio';
|
||||
|
||||
const logger = getLogger('py-terminal');
|
||||
|
||||
export class PyTerminalPlugin extends Plugin {
|
||||
app: PyScriptApp;
|
||||
|
||||
constructor(app: PyScriptApp) {
|
||||
super();
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
configure(config: AppConfig) {
|
||||
// validate the terminal config and handle default values
|
||||
const t = config.terminal;
|
||||
if (t !== undefined &&
|
||||
t !== true &&
|
||||
t !== false &&
|
||||
t !== "auto") {
|
||||
const got = JSON.stringify(t);
|
||||
throw new UserError('Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`);
|
||||
}
|
||||
if (t === undefined) {
|
||||
config.terminal = "auto"; // default value
|
||||
}
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfig) {
|
||||
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to
|
||||
// the document, unless it's already present.
|
||||
const t = config.terminal;
|
||||
if (t === true || t === "auto") {
|
||||
if (document.querySelector('py-terminal') === null) {
|
||||
logger.info("No <py-terminal> found, adding one");
|
||||
const termElem = document.createElement('py-terminal');
|
||||
if (t === "auto")
|
||||
termElem.setAttribute("auto", "");
|
||||
document.body.appendChild(termElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterSetup() {
|
||||
// the Python interpreter has been initialized and we are ready to
|
||||
// execute user code:
|
||||
//
|
||||
// 1. define the "py-terminal" custom element
|
||||
//
|
||||
// 2. if there is a <py-terminal> tag on the page, it will register
|
||||
// a Stdio listener just before the user code executes, ensuring
|
||||
// that we capture all the output
|
||||
//
|
||||
// 3. everything which was written to stdout BEFORE this moment will
|
||||
// NOT be shown on the py-terminal; in particular, pyodide
|
||||
// startup messages will not be shown (but they will go to the
|
||||
// console as usual). This is by design, else we would display
|
||||
// e.g. "Python initialization complete" on every page, which we
|
||||
// don't want.
|
||||
//
|
||||
// 4. (in the future we might want to add an option to start the
|
||||
// capture earlier, but I don't think it's important now).
|
||||
const PyTerminal = make_PyTerminal(this.app);
|
||||
customElements.define('py-terminal', PyTerminal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function make_PyTerminal(app: PyScriptApp) {
|
||||
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminal extends HTMLElement implements Stdio {
|
||||
outElem: HTMLElement;
|
||||
autoShowOnNextLine: boolean;
|
||||
|
||||
connectedCallback() {
|
||||
// should we use a shadowRoot instead? It looks unnecessarily
|
||||
// complicated to me, but I'm not really sure about the
|
||||
// implications
|
||||
this.outElem = document.createElement('pre');
|
||||
this.outElem.className = 'py-terminal';
|
||||
this.appendChild(this.outElem);
|
||||
|
||||
if (this.isAuto()) {
|
||||
this.classList.add('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = true;
|
||||
}
|
||||
else {
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
|
||||
logger.info('Registering stdio listener');
|
||||
app.registerStdioListener(this);
|
||||
}
|
||||
|
||||
isAuto() {
|
||||
return this.hasAttribute("auto");
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
stdout_writeline(msg: string) {
|
||||
this.outElem.innerText += msg + "\n";
|
||||
if (this.autoShowOnNextLine) {
|
||||
this.classList.remove('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
this.stdout_writeline(msg);
|
||||
}
|
||||
// end of the Stdio interface
|
||||
}
|
||||
|
||||
return PyTerminal;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { loadPyodide as loadPyodideDeclaration, PyodideInterface, PyProxy }
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript.py';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Stdio } from './stdio';
|
||||
|
||||
declare const loadPyodide: typeof loadPyodideDeclaration;
|
||||
|
||||
@@ -17,6 +18,7 @@ interface Micropip {
|
||||
|
||||
export class PyodideRuntime extends Runtime {
|
||||
src: string;
|
||||
stdio: Stdio;
|
||||
name?: string;
|
||||
lang?: string;
|
||||
interpreter: PyodideInterface;
|
||||
@@ -24,12 +26,14 @@ export class PyodideRuntime extends Runtime {
|
||||
|
||||
constructor(
|
||||
config: AppConfig,
|
||||
stdio: Stdio,
|
||||
src = 'https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js',
|
||||
name = 'pyodide-default',
|
||||
lang = 'python',
|
||||
) {
|
||||
logger.info('Runtime config:', { name, lang, src });
|
||||
super(config);
|
||||
this.stdio = stdio;
|
||||
this.src = src;
|
||||
this.name = name;
|
||||
this.lang = lang;
|
||||
@@ -54,8 +58,8 @@ export class PyodideRuntime extends Runtime {
|
||||
async loadInterpreter(): Promise<void> {
|
||||
logger.info('Loading pyodide');
|
||||
this.interpreter = await loadPyodide({
|
||||
stdout: console.log,
|
||||
stderr: console.log,
|
||||
stdout: (msg: string) => { this.stdio.stdout_writeline(msg); },
|
||||
stderr: (msg: string) => { this.stdio.stderr_writeline(msg); },
|
||||
fullStdLib: false,
|
||||
});
|
||||
|
||||
|
||||
61
pyscriptjs/src/stdio.ts
Normal file
61
pyscriptjs/src/stdio.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export interface Stdio {
|
||||
stdout_writeline: (msg: string) => void;
|
||||
stderr_writeline: (msg: string) => void;
|
||||
}
|
||||
|
||||
/** Default implementation of Stdio: stdout and stderr are both sent to the
|
||||
* console
|
||||
*/
|
||||
export const DEFAULT_STDIO: Stdio = {
|
||||
stdout_writeline: console.log,
|
||||
stderr_writeline: console.log
|
||||
}
|
||||
|
||||
/** Stdio provider which captures and store the messages.
|
||||
* Useful for tests.
|
||||
*/
|
||||
export class CaptureStdio implements Stdio {
|
||||
captured_stdout: string;
|
||||
captured_stderr: string;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.captured_stdout = "";
|
||||
this.captured_stderr = "";
|
||||
}
|
||||
|
||||
stdout_writeline(msg: string) {
|
||||
this.captured_stdout += msg + "\n";
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
this.captured_stderr += msg + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/** Redirect stdio streams to multiple listeners
|
||||
*/
|
||||
export class StdioMultiplexer implements Stdio {
|
||||
_listeners: Stdio[];
|
||||
|
||||
constructor() {
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
addListener(obj: Stdio) {
|
||||
this._listeners.push(obj);
|
||||
}
|
||||
|
||||
stdout_writeline(msg: string) {
|
||||
for(const obj of this._listeners)
|
||||
obj.stdout_writeline(msg);
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
for(const obj of this._listeners)
|
||||
obj.stderr_writeline(msg);
|
||||
}
|
||||
}
|
||||
@@ -318,3 +318,17 @@ textarea {
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* ===== py-terminal plugin ===== */
|
||||
/* XXX: it would be nice if these rules were stored in e.g. pyterminal.css and
|
||||
bundled together at build time (by rollup?) */
|
||||
|
||||
.py-terminal {
|
||||
min-height: 10em;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.py-terminal-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -264,19 +264,21 @@ class TestOutput(PyScriptTest):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-script>
|
||||
display('0')
|
||||
display('this goes to the DOM')
|
||||
print('print from python')
|
||||
console.log('print from js')
|
||||
console.error('error from js');
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
inner_text = self.page.inner_text("html")
|
||||
assert "0" == inner_text
|
||||
console_text = self.console.all.lines
|
||||
assert "print from python" in console_text
|
||||
assert "print from js" in console_text
|
||||
assert "error from js" in console_text
|
||||
inner_text = self.page.inner_text("py-script")
|
||||
assert inner_text == "this goes to the DOM"
|
||||
assert self.console.log.lines == [
|
||||
self.PY_COMPLETE,
|
||||
"print from python",
|
||||
"print from js",
|
||||
]
|
||||
assert self.console.error.lines[-1] == "error from js"
|
||||
|
||||
def test_console_line_break(self):
|
||||
self.pyscript_run(
|
||||
|
||||
134
pyscriptjs/tests/integration/test_py_terminal.py
Normal file
134
pyscriptjs/tests/integration/test_py_terminal.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from .support import PyScriptTest
|
||||
|
||||
|
||||
class TestPyTerminal(PyScriptTest):
|
||||
def test_py_terminal(self):
|
||||
"""
|
||||
1. <py-terminal> should redirect stdout and stderr to the DOM
|
||||
|
||||
2. they also go to the console as usual
|
||||
|
||||
3. note that the console also contains PY_COMPLETE, which is a pyodide
|
||||
initialization message, but py-terminal doesn't. This is by design
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal></py-terminal>
|
||||
|
||||
<py-script>
|
||||
import sys
|
||||
print('hello world')
|
||||
print('this goes to stderr', file=sys.stderr)
|
||||
print('this goes to stdout')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
term_lines = term.inner_text().splitlines()
|
||||
assert term_lines == [
|
||||
"hello world",
|
||||
"this goes to stderr",
|
||||
"this goes to stdout",
|
||||
]
|
||||
assert self.console.log.lines == [
|
||||
self.PY_COMPLETE,
|
||||
"hello world",
|
||||
"this goes to stderr",
|
||||
"this goes to stdout",
|
||||
]
|
||||
|
||||
def test_two_terminals(self):
|
||||
"""
|
||||
Multiple <py-terminal>s can cohexist.
|
||||
A <py-terminal> receives only output from the moment it is added to
|
||||
the DOM.
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal id="term1"></py-terminal>
|
||||
|
||||
<py-script>
|
||||
import js
|
||||
print('one')
|
||||
term2 = js.document.createElement('py-terminal')
|
||||
term2.id = 'term2'
|
||||
js.document.body.append(term2)
|
||||
|
||||
print('two')
|
||||
print('three')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term1 = self.page.locator("#term1")
|
||||
term2 = self.page.locator("#term2")
|
||||
term1_lines = term1.inner_text().splitlines()
|
||||
term2_lines = term2.inner_text().splitlines()
|
||||
assert term1_lines == ["one", "two", "three"]
|
||||
assert term2_lines == ["two", "three"]
|
||||
|
||||
def test_auto_attribute(self):
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-terminal auto></py-terminal>
|
||||
|
||||
<button id="my-button" py-onClick="print('hello world')">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_hidden()
|
||||
self.page.locator("button").click()
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_auto(self):
|
||||
"""
|
||||
config.terminal == "auto" is the default: a <py-terminal auto> is
|
||||
automatically added to the page
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<button id="my-button" py-onClick="print('hello world')">Click me</button>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_hidden()
|
||||
assert "No <py-terminal> found, adding one" in self.console.info.text
|
||||
#
|
||||
self.page.locator("button").click()
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_true(self):
|
||||
"""
|
||||
If we set config.terminal == true, a <py-terminal> is automatically added
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = true
|
||||
</py-config>
|
||||
|
||||
<py-script>
|
||||
print('hello world')
|
||||
</py-script>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
expect(term).to_be_visible()
|
||||
assert term.inner_text() == "hello world\n"
|
||||
|
||||
def test_config_false(self):
|
||||
"""
|
||||
If we set config.terminal == false, no <py-terminal> is added
|
||||
"""
|
||||
self.pyscript_run(
|
||||
"""
|
||||
<py-config>
|
||||
terminal = false
|
||||
</py-config>
|
||||
"""
|
||||
)
|
||||
term = self.page.locator("py-terminal")
|
||||
assert term.count() == 0
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AppConfig } from '../../src/pyconfig';
|
||||
import { Runtime } from '../../src/runtime';
|
||||
import { PyodideRuntime } from '../../src/pyodide';
|
||||
import { CaptureStdio } from '../../src/stdio';
|
||||
|
||||
import { TextEncoder, TextDecoder } from 'util'
|
||||
global.TextEncoder = TextEncoder
|
||||
@@ -8,9 +9,11 @@ global.TextDecoder = TextDecoder
|
||||
|
||||
describe('PyodideRuntime', () => {
|
||||
let runtime: PyodideRuntime;
|
||||
let stdio: CaptureStdio = new CaptureStdio();
|
||||
beforeAll(async () => {
|
||||
const config: AppConfig = {};
|
||||
runtime = new PyodideRuntime(config);
|
||||
runtime = new PyodideRuntime(config, stdio);
|
||||
|
||||
/**
|
||||
* Since import { loadPyodide } from 'pyodide';
|
||||
* is not used inside `src/pyodide.ts`, the function
|
||||
@@ -50,6 +53,12 @@ describe('PyodideRuntime', () => {
|
||||
expect(await runtime.run("2+3")).toBe(5);
|
||||
});
|
||||
|
||||
it('should capture stdout', async () => {
|
||||
stdio.reset();
|
||||
await runtime.run("print('hello')");
|
||||
expect(stdio.captured_stdout).toBe("hello\n");
|
||||
});
|
||||
|
||||
it('should check if runtime is able to load a package', async () => {
|
||||
await runtime.loadPackage("numpy");
|
||||
await runtime.run("import numpy as np");
|
||||
67
pyscriptjs/tests/unit/stdio.test.ts
Normal file
67
pyscriptjs/tests/unit/stdio.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type Stdio, CaptureStdio, StdioMultiplexer } from '../../src/stdio';
|
||||
|
||||
describe('CaptureStdio', () => {
|
||||
it('captured streams are initialized to empty string', () => {
|
||||
let stdio = new CaptureStdio();
|
||||
expect(stdio.captured_stdout).toBe("");
|
||||
expect(stdio.captured_stderr).toBe("");
|
||||
});
|
||||
|
||||
it('stdout() and stderr() captures', () => {
|
||||
let stdio = new CaptureStdio();
|
||||
stdio.stdout_writeline("hello");
|
||||
stdio.stdout_writeline("world");
|
||||
stdio.stderr_writeline("this is an error");
|
||||
expect(stdio.captured_stdout).toBe("hello\nworld\n");
|
||||
expect(stdio.captured_stderr).toBe("this is an error\n");
|
||||
});
|
||||
|
||||
it('reset() works', () => {
|
||||
let stdio = new CaptureStdio();
|
||||
stdio.stdout_writeline("aaa");
|
||||
stdio.stderr_writeline("bbb");
|
||||
stdio.reset();
|
||||
expect(stdio.captured_stdout).toBe("");
|
||||
expect(stdio.captured_stderr).toBe("");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('StdioMultiplexer', () => {
|
||||
let a: CaptureStdio;
|
||||
let b: CaptureStdio;
|
||||
let multi: StdioMultiplexer;
|
||||
|
||||
beforeEach(() => {
|
||||
a = new CaptureStdio();
|
||||
b = new CaptureStdio();
|
||||
multi = new StdioMultiplexer();
|
||||
});
|
||||
|
||||
it('works without listeners', () => {
|
||||
// no listeners, messages are ignored
|
||||
multi.stdout_writeline('out 1');
|
||||
multi.stderr_writeline('err 1');
|
||||
expect(a.captured_stdout).toBe("");
|
||||
expect(a.captured_stderr).toBe("");
|
||||
expect(b.captured_stdout).toBe("");
|
||||
expect(b.captured_stderr).toBe("");
|
||||
});
|
||||
|
||||
it('redirects to multiple listeners', () => {
|
||||
multi.addListener(a);
|
||||
multi.stdout_writeline('out 1');
|
||||
multi.stderr_writeline('err 1');
|
||||
|
||||
multi.addListener(b);
|
||||
multi.stdout_writeline('out 2');
|
||||
multi.stderr_writeline('err 2');
|
||||
|
||||
expect(a.captured_stdout).toBe("out 1\nout 2\n");
|
||||
expect(a.captured_stderr).toBe("err 1\nerr 2\n");
|
||||
|
||||
expect(b.captured_stdout).toBe("out 2\n");
|
||||
expect(b.captured_stderr).toBe("err 2\n");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user