From 86cddda06f375f0175ec1d1db807630cf8bbea3d Mon Sep 17 00:00:00 2001 From: George Stagg Date: Thu, 15 Feb 2024 10:03:46 +0000 Subject: [PATCH] Add RCall/RFunction tests and keep linter happy --- src/tests/webR/webr-main.test.ts | 91 ++++++++++++++++++++++++++++++++ src/webR/robj-worker.ts | 4 +- src/webR/utils.ts | 2 +- src/webR/webr-worker.ts | 10 ++-- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/tests/webR/webr-main.test.ts b/src/tests/webR/webr-main.test.ts index 124a572e..5e737208 100644 --- a/src/tests/webR/webr-main.test.ts +++ b/src/tests/webR/webr-main.test.ts @@ -10,6 +10,7 @@ import { REnvironment, RInteger, RFunction, + RCall, } from '../../webR/robj-main'; const webR = new WebR({ @@ -108,6 +109,19 @@ describe('Evaluate R code', () => { await expect(throws).rejects.toThrow('This is an error from R!'); }); + test('Error conditions are re-thrown in JS when executing an R function', async () => { + const fn = await webR.evalR('sin') as RFunction; + let throws = fn("abc"); + await expect(throws).rejects.toThrow('non-numeric argument to mathematical function'); + throws = fn.exec("abc"); + await expect(throws).rejects.toThrow('non-numeric argument to mathematical function'); + }); + + test('Error conditions are re-thrown in JS when executing an R call', async () => { + const fn = await webR.evalR('quote(sin("abc"))') as RCall; + await expect(fn.eval()).rejects.toThrow('non-numeric argument to mathematical function'); + }); + test('Capture stdout while capturing R code', async () => { const shelter = await new webR.Shelter(); const composite = await shelter.captureR('c(1, 2, 4, 6, 12, 24, 36, 48)', { @@ -139,6 +153,83 @@ describe('Evaluate R code', () => { expect(await condMsg.toString()).toContain('This is a warning message'); shelter.purge(); }); + + test('Capture output when executing an R function', async () => { + const fn = await webR.evalR(` + function(){ + print("Hello, stdout!") + message("Hello, message!") + warning("Hello, warning!") + } + `) as RFunction; + const result = await fn.capture({ + captureConditions: true, + }); + + let outType = await result.output.pluck(1, 'type')!; + let outData = await result.output.pluck(1, 'data')!; + expect(await outType.toString()).toEqual('stdout'); + expect(await outData.toString()).toContain('Hello, stdout!'); + + outType = await result.output.pluck(2, 'type')!; + outData = await result.output.pluck(2, 'data', 'message')!; + expect(await outType.toString()).toEqual('message'); + expect(await outData.toString()).toContain('Hello, message!'); + + outType = await result.output.pluck(3, 'type')!; + outData = await result.output.pluck(3, 'data', 'message')!; + expect(await outType.toString()).toEqual('warning'); + expect(await outData.toString()).toContain('Hello, warning!'); + + webR.globalShelter.purge(); + }); + + test('Capturing graphics throws an Error when OffScreenCanvas is unavailable', async () => { + const shelter = await new webR.Shelter(); + const throws = shelter.captureR('plot(123)', { captureGraphics: true }); + await expect(throws).rejects.toThrow( + 'This environment does not have support for OffscreenCanvas.' + ); + shelter.purge(); + }); + + test('Capturing graphics with mocked OffScreenCanvas', async () => { + // Mock the OffscreenCanvas interface for testing under Node + await webR.evalRVoid(` + webr::eval_js(" + class OffscreenCanvas { + constructor() {} + getContext() { + return { + arc: () => {}, + beginPath: () => {}, + clearRect: () => {}, + clip: () => {}, + setLineDash: () => {}, + rect: () => {}, + restore: () => {}, + save: () => {}, + stroke: () => {}, + }; + } + transferToImageBitmap() { + // No ImageBitmap, create a transferable ArrayBuffer in its place + return new ArrayBuffer(8); + } + } + globalThis.OffscreenCanvas = OffscreenCanvas; + ") + `); + + const shelter = await new webR.Shelter(); + const result = await shelter.captureR(` + plot.new(); + points(0) + `, { captureGraphics: true }); + + expect(result.images.length).toBeGreaterThan(0); + shelter.purge(); + }); }); describe('Create R objects using serialised form', () => { diff --git a/src/webR/robj-worker.ts b/src/webR/robj-worker.ts index 36bd88d6..04a943c1 100644 --- a/src/webR/robj-worker.ts +++ b/src/webR/robj-worker.ts @@ -521,7 +521,7 @@ export class RCall extends RObject { return Module.webr.evalR(this, { env: objs.baseEnv }); } - capture(options: EvalROptions) { + capture(options: EvalROptions = {}) { return Module.webr.captureR(this, options); } } @@ -612,7 +612,7 @@ export class RFunction extends RObject { } } - capture(options: EvalROptions, ...args: (WebRDataRaw | RObject)[]) { + capture(options: EvalROptions = {}, ...args: (WebRDataRaw | RObject)[]) { const prot = { n: 0 }; try { diff --git a/src/webR/utils.ts b/src/webR/utils.ts index 03bac503..72ff288b 100644 --- a/src/webR/utils.ts +++ b/src/webR/utils.ts @@ -84,7 +84,7 @@ export function isCrossOrigin(urlString: string) { } export function isImageBitmap(value: any): value is ImageBitmap { - return (typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap); + return (typeof ImageBitmap !== 'undefined' && value instanceof ImageBitmap); } export function throwUnreachable(context?: string) { diff --git a/src/webR/webr-worker.ts b/src/webR/webr-worker.ts index a3938501..50fa278c 100644 --- a/src/webR/webr-worker.ts +++ b/src/webR/webr-worker.ts @@ -644,7 +644,7 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): { env: objs.globalEnv, captureStreams: true, captureConditions: true, - captureGraphics: typeof OffscreenCanvas !== "undefined", + captureGraphics: typeof OffscreenCanvas !== 'undefined', withAutoprint: false, throwJsException: true, withHandlers: true, @@ -664,10 +664,10 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): { const devEnvObj = new REnvironment({}); protectInc(devEnvObj, prot); if (_options.captureGraphics) { - if (typeof OffscreenCanvas === "undefined") { + if (typeof OffscreenCanvas === 'undefined') { throw new Error( - "This environment does not have support for OffscreenCanvas. " + - "Consider disabling plot capture using `captureGraphics: false`." + 'This environment does not have support for OffscreenCanvas. ' + + 'Consider disabling plot capture using `captureGraphics: false`.' ); } @@ -727,7 +727,7 @@ function captureR(expr: string | RObject, options: EvalROptions = {}): { protectInc(plots, prot); images = plots.toArray().map((idx) => { - return Module.webr.canvas[idx!].offscreen.transferToImageBitmap() + return Module.webr.canvas[idx!].offscreen.transferToImageBitmap(); }); // Close the device and destroy newly created canvas cache entries