From 88b70df5de229fc18b85b6cd9f24f572566deb46 Mon Sep 17 00:00:00 2001 From: Neil C Smith Date: Mon, 22 Apr 2024 16:07:23 +0100 Subject: [PATCH] Add ScriptStackFrame API class with inline and error trapping support. Move and rewrite EvalStackFrame as ScriptStackFrame API. Add ability to trap errors and filter allowed commands. Add flags to eval command for --inline, --trap-errors and --allowed-commands. Add tests for script execution. --- pom.xml | 4 +- praxiscore-script/pom.xml | 14 +- .../org/praxislive/script/ScriptExecutor.java | 42 +- .../praxislive/script/ScriptStackFrame.java | 400 ++++++++++++++++++ .../praxislive/script/ast/CompositeNode.java | 47 +- .../org/praxislive/script/ast/RootNode.java | 17 +- .../praxislive/script/ast/SubcommandNode.java | 6 +- .../praxislive/script/commands/AtCmds.java | 3 +- .../script/commands/EvalStackFrame.java | 202 --------- .../script/commands/ScriptCmds.java | 69 +-- .../script/DefaultScriptServiceTest.java | 308 ++++++++++++++ .../script/ast/AddressNodeTest.java | 102 ++--- 12 files changed, 852 insertions(+), 362 deletions(-) create mode 100644 praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java delete mode 100644 praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java create mode 100644 praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java diff --git a/pom.xml b/pom.xml index 848b3615..79763f1c 100644 --- a/pom.xml +++ b/pom.xml @@ -144,9 +144,9 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.12.1 - 17 + 21 diff --git a/praxiscore-script/pom.xml b/praxiscore-script/pom.xml index 106d1bbf..11629de7 100644 --- a/praxiscore-script/pom.xml +++ b/praxiscore-script/pom.xml @@ -12,8 +12,18 @@ - org.junit.vintage - junit-vintage-engine + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine test diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java index 4f9cb8b2..f88f0192 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java @@ -31,7 +31,6 @@ import org.praxislive.core.Lookup; import org.praxislive.core.types.PError; import org.praxislive.script.commands.CoreCommandsInstaller; -import org.praxislive.script.commands.ScriptCmds; import static java.lang.System.Logger.Level; @@ -42,36 +41,27 @@ class ScriptExecutor { private static final System.Logger log = System.getLogger(ScriptExecutor.class.getName()); - private List stack; - private Queue queue; - private Env env; - private Command evaluator; - private Map commandMap; - private Namespace rootNS; + private final List stack; + private final Queue queue; + private final Env env; + private final Map commandMap; + private final Namespace rootNS; - public ScriptExecutor(Env env, boolean inline) { - this.env = env; + ScriptExecutor(Env context, final ComponentAddress ctxt) { + this.env = context; stack = new LinkedList<>(); queue = new LinkedList<>(); - if (inline) { - evaluator = ScriptCmds.INLINE_EVAL; - } else { - evaluator = ScriptCmds.EVAL; - } + commandMap = buildCommandMap(); rootNS = new NS(); - buildCommandMap(); - } - - public ScriptExecutor(Env context, final ComponentAddress ctxt) { - this(context, true); rootNS.addVariable(Env.CONTEXT, new ConstantImpl(ctxt)); } - private void buildCommandMap() { - commandMap = new HashMap<>(); + private Map buildCommandMap() { + Map map = new HashMap<>(); CommandInstaller installer = new CoreCommandsInstaller(); - installer.install(commandMap); - Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(commandMap)); + installer.install(map); + Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(map)); + return map; } public void queueEvalCall(Call call) { @@ -150,7 +140,11 @@ private void checkAndStartEval() { Call call = queue.peek(); var args = call.args(); try { - stack.add(0, evaluator.createStackFrame(rootNS, args)); + var script = args.get(0).toString(); + var stackFrame = ScriptStackFrame.forScript(rootNS, script) + .inline() + .build(); + stack.add(0, stackFrame); processStack(); break; } catch (Exception ex) { diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java new file mode 100644 index 00000000..666b477c --- /dev/null +++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java @@ -0,0 +1,400 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.praxislive.core.Value; +import org.praxislive.core.Call; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.syntax.InvalidSyntaxException; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PError; +import org.praxislive.core.types.PString; +import org.praxislive.script.ast.RootNode; +import org.praxislive.script.ast.ScriptParser; + +import static java.lang.System.Logger.Level; + +/** + * A stackframe implementation that supports parsing and running of Pcl scripts. + */ +public final class ScriptStackFrame implements StackFrame { + + private static final String TRAP = "_TRAP"; + + private static final System.Logger log = System.getLogger(ScriptStackFrame.class.getName()); + + private final Namespace namespace; + private final RootNode rootNode; + private final boolean trapErrors; + private final List scratchList; + + private State state; + private String activeCommand; + private Call pending; + private List result; + private boolean doProcess; + + private ScriptStackFrame(Namespace namespace, + RootNode rootNode, + boolean trapErrors) { + this.namespace = namespace; + this.rootNode = rootNode; + this.state = State.Incomplete; + this.trapErrors = trapErrors; + this.scratchList = new ArrayList<>(); + rootNode.reset(); + if (trapErrors) { + namespace.createVariable(TRAP, PArray.EMPTY); + } + rootNode.init(namespace); + doProcess = true; + } + + @Override + public State getState() { + return state; + } + + @Override + public StackFrame process(Env context) { + if (state != State.Incomplete) { + throw new IllegalStateException(); + } + if (!doProcess) { + return null; + } + while (!rootNode.isDone() && state == State.Incomplete) { + try { + return processNextCommand(context); + } catch (Exception ex) { + postError(List.of(PError.of(ex))); + } + } + if (rootNode.isDone() && state == State.Incomplete) { + + try { + processResultFromNode(); + } catch (Exception ex) { + result = List.of(PError.of(ex)); + state = State.Error; + } + } + return null; + } + + @Override + public void postResponse(Call call) { + if (pending != null && pending.matchID() == call.matchID()) { + pending = null; + if (call.isReply()) { + log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Return call : \n" + call); + postResponse(call.args()); + } else { + log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Error call : \n" + call); + postError(call.args()); + } + doProcess = true; + } else { + log.log(Level.TRACE, () -> "EvalStackFrame - Received invalid call : \n" + call); + } + + } + + @Override + public void postResponse(State state, List args) { + if (this.state != State.Incomplete) { + throw new IllegalStateException(); + } + switch (state) { + case Incomplete -> + throw new IllegalArgumentException(); + case OK -> + postResponse(args); + case Error -> + postError(args); + default -> { + this.state = state; + this.result = List.copyOf(args); + } + } + doProcess = true; + } + + @Override + public List result() { + if (state == State.Incomplete) { + throw new IllegalStateException(); + } + if (result == null) { + return List.of(); + } else { + return result; + } + } + + private void postResponse(List args) { + try { + scratchList.clear(); + scratchList.addAll(args); + rootNode.postResponse(scratchList); + } catch (Exception ex) { + state = State.Error;//@TODO proper error reporting + } + } + + private void postError(List args) { + Variable trap = namespace.getVariable(TRAP); + if (trap != null) { + rootNode.skipCurrentLine(); + PArray existing = PArray.from(trap.getValue()).orElse(PArray.EMPTY); + Value response = args.isEmpty() ? null : args.getFirst(); + trap.setValue(addErrorToTrap(existing, response)); + } else { + result = List.copyOf(args); + state = State.Error; + } + } + + private PArray addErrorToTrap(PArray trap, Value response) { + String msg; + if (response == null) { + msg = activeCommand + " : Error"; + } else { + msg = PError.from(response) + .map(err -> activeCommand + " : " + err.exceptionType().getSimpleName() + + " : " + err.message()) + .orElse(activeCommand + " : Error : " + response); + } + return Stream.concat(trap.stream(), Stream.of(PString.of(msg))) + .collect(PArray.collector()); + } + + private void processResultFromNode() throws Exception { + scratchList.clear(); + Variable trap = namespace.getVariable(TRAP); + if (trapErrors && trap != null && !trap.getValue().isEmpty()) { + String errors = PArray.from(trap.getValue()) + .orElse(PArray.EMPTY) + .asListOf(String.class) + .stream() + .collect(Collectors.joining("\n")); + result = List.of(PString.of(errors)); + state = State.Error; + } else { + rootNode.writeResult(scratchList); + result = List.copyOf(scratchList); + state = State.OK; + } + } + + private StackFrame processNextCommand(Env context) + throws Exception { + + scratchList.clear(); + rootNode.writeNextCommand(scratchList); + if (scratchList.size() < 1) { + throw new Exception(); + } + Value cmdArg = scratchList.get(0); + activeCommand = cmdArg.toString(); + if (cmdArg instanceof ControlAddress) { + routeCall(context, scratchList); + return null; + } + String cmdStr = cmdArg.toString(); + if (cmdStr.isEmpty()) { + throw new IllegalArgumentException("Empty command"); + } + Command cmd = namespace.getCommand(cmdStr); + if (cmd != null) { + scratchList.remove(0); + return cmd.createStackFrame(namespace, List.copyOf(scratchList)); + } + if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) { + routeCall(context, scratchList); + return null; + } + + throw new IllegalArgumentException("Command not found"); + + } + + private void routeCall(Env context, List argList) + throws Exception { + ControlAddress ad = ControlAddress.from(argList.get(0)) + .orElseThrow(Exception::new); + argList.remove(0); + Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList)); + log.log(Level.TRACE, () -> "Sending Call" + call); + pending = call; + context.getPacketRouter().route(call); + } + + /** + * Create a {@link ScriptStackFrame.Builder} for the provided namespace and + * script. By default the script will be evaluated in a dedicated child + * namespace. Neither the builder or the stack frame are reusable. + * + * @param namespace namespace to run script in + * @param script script to parse and run + * @return builder + * @throws InvalidSyntaxException if the script cannot be parsed + */ + public static Builder forScript(Namespace namespace, String script) { + RootNode root = ScriptParser.getInstance().parse(script); + return new Builder(namespace, root); + } + + /** + * A builder for {@link ScriptStackFrame}. + * + * @see #forScript(org.praxislive.script.Namespace, java.lang.String) + */ + public static class Builder { + + private final Namespace namespace; + private final RootNode root; + + private boolean inline; + private List allowedCommands; + private boolean trapErrors; + + private Builder(Namespace namespace, RootNode root) { + this.namespace = namespace; + this.root = root; + } + + /** + * Run the script directly in the provided namespace rather than a + * child. + * + * @return this for chaining + */ + public Builder inline() { + if (trapErrors) { + throw new IllegalStateException("Inline and trap errors cannot be used together"); + } + this.inline = true; + return this; + } + + /** + * Trap errors. Error messages will be aggregated and script execution + * will attempt to continue. If no allowed commands have been specified, + * an empty list of allowed commands will be set. + * + * @return this for chaining + */ + public Builder trapErrors() { + if (inline) { + throw new IllegalStateException("Inline and trap errors cannot be used together"); + } + this.trapErrors = true; + if (this.allowedCommands == null) { + this.allowedCommands = List.of(); + } + return this; + } + + /** + * Specify a list of allowed commands to filter those available from the + * provided namespace. + * + * @param commands list of allowed commands + * @return this for chaining + */ + public Builder allowedCommands(List commands) { + this.allowedCommands = List.copyOf(commands); + return this; + } + + /** + * Build the ScriptStackFrame. + * + * @return script stackframe + */ + public ScriptStackFrame build() { + Namespace ns; + if (inline) { + ns = namespace; + } else { + ns = namespace.createChild(); + } + if (allowedCommands != null) { + ns = new FilteredNamespace(ns, allowedCommands); + } + return new ScriptStackFrame(ns, root, trapErrors); + } + + } + + private static class FilteredNamespace implements Namespace { + + private final Namespace delegate; + private final List allowed; + + private FilteredNamespace(Namespace delegate, List allowed) { + this.delegate = Objects.requireNonNull(delegate); + this.allowed = List.copyOf(allowed); + } + + @Override + public void addCommand(String id, Command cmd) { + if (allowed.contains(id)) { + delegate.addCommand(id, cmd); + } else { + throw new UnsupportedOperationException(); + } + } + + @Override + public void addVariable(String id, Variable var) { + delegate.addVariable(id, var); + } + + @Override + public Namespace createChild() { + return new FilteredNamespace(delegate.createChild(), allowed); + } + + @Override + public Command getCommand(String id) { + if (allowed.contains(id)) { + return delegate.getCommand(id); + } else { + return null; + } + } + + @Override + public Variable getVariable(String id) { + return delegate.getVariable(id); + } + + } + +} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java index 50f59598..ec0267ba 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2018 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -19,7 +19,6 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.ast; import java.util.List; @@ -28,19 +27,20 @@ /** * - * + * */ -public abstract class CompositeNode extends Node { - - private Node[] children; +abstract class CompositeNode extends Node { + + private final List children; + private int active; private Namespace namespace; public CompositeNode(List children) { - this.children = children.toArray(new Node[children.size()]); + this.children = List.copyOf(children); } - @Override + @Override public void init(Namespace namespace) { if (namespace == null) { throw new NullPointerException(); @@ -50,9 +50,7 @@ public void init(Namespace namespace) { } this.namespace = namespace; active = 0; - for (Node child : children) { - child.init(namespace); - } + children.forEach(child -> child.init(namespace)); } @Override @@ -63,8 +61,8 @@ public boolean isDone() { if (active < 0) { return isThisDone(); } else { - while (active < children.length) { - if (!children[active].isDone()) { + while (active < children.size()) { + if (!children.get(active).isDone()) { return false; } active++; @@ -77,13 +75,13 @@ public boolean isDone() { protected abstract boolean isThisDone(); @Override - public void writeNextCommand(List args) + public void writeNextCommand(List args) throws Exception { if (namespace == null) { throw new IllegalStateException(); } if (active >= 0) { - children[active].writeNextCommand(args); + children.get(active).writeNextCommand(args); } else { writeThisNextCommand(args); } @@ -92,15 +90,14 @@ public void writeNextCommand(List args) protected abstract void writeThisNextCommand(List args) throws Exception; - @Override - public void postResponse(List args) + public void postResponse(List args) throws Exception { if (namespace == null) { throw new IllegalStateException(); } if (active >= 0) { - children[active].postResponse(args); + children.get(active).postResponse(args); } else { postThisResponse(args); } @@ -116,11 +113,19 @@ public void reset() { } namespace = null; } - - protected Node[] getChildren() { + + protected List getChildren() { return children; } - + protected void skipActive() { + if (active < 0) { + return; + } + active++; + if (active >= children.size()) { + active = -1; + } + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java index afd29eab..794a6f2e 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java @@ -19,7 +19,6 @@ * Please visit https://www.praxislive.org if you need additional information or * have any questions. */ - package org.praxislive.script.ast; import java.util.List; @@ -27,7 +26,7 @@ /** * - * + * */ public class RootNode extends CompositeNode { @@ -52,13 +51,17 @@ protected void postThisResponse(List args) { @Override public void writeResult(List args) throws Exception { - Node[] children = getChildren(); - if (children.length > 0) { - children[children.length - 1].writeResult(args); + List children = getChildren(); + if (!children.isEmpty()) { + Node last = children.getLast(); + if (last.isDone()) { + last.writeResult(args); + } } - } - + public void skipCurrentLine() { + skipActive(); + } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java index 6fc9660a..0c2829c6 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java @@ -26,7 +26,7 @@ /** * - * + * */ public class SubcommandNode extends CompositeNode { @@ -52,7 +52,7 @@ protected void postThisResponse(List args) { @Override public void writeResult(List args) throws Exception { - Node[] children = getChildren(); - children[children.length - 1].writeResult(args); + List children = getChildren(); + children.get(children.size() - 1).writeResult(args); } } diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java index f6a9ee83..f1270eeb 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java @@ -39,6 +39,7 @@ import org.praxislive.script.CommandInstaller; import org.praxislive.script.Env; import org.praxislive.script.Namespace; +import org.praxislive.script.ScriptStackFrame; import org.praxislive.script.StackFrame; /** @@ -171,7 +172,7 @@ public StackFrame process(Env env) { try { Namespace child = namespace.createChild(); child.createConstant(Env.CONTEXT, ctxt); - return ScriptCmds.INLINE_EVAL.createStackFrame(child, List.of(script)); + return ScriptStackFrame.forScript(child, script.toString()).inline().build(); } catch (Exception ex) { state = State.Error; result = List.of(PError.of(ex)); diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java deleted file mode 100644 index 9769f552..00000000 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2024 Neil C Smith. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License version 3 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * version 3 for more details. - * - * You should have received a copy of the GNU Lesser General Public License version 3 - * along with this work; if not, see http://www.gnu.org/licenses/ - * - * - * Please visit https://www.praxislive.org if you need additional information or - * have any questions. - */ -package org.praxislive.script.commands; - -import java.util.LinkedList; -import java.util.List; -import org.praxislive.core.Value; -import org.praxislive.core.Call; -import org.praxislive.core.ControlAddress; -import org.praxislive.core.types.PError; -import org.praxislive.script.Command; -import org.praxislive.script.Env; -import org.praxislive.script.Namespace; -import org.praxislive.script.StackFrame; -import org.praxislive.script.ast.RootNode; - -import static java.lang.System.Logger.Level; - -/** - * - */ -class EvalStackFrame implements StackFrame { - - private static final System.Logger log = System.getLogger(EvalStackFrame.class.getName()); - - private Namespace namespace; - private RootNode rootNode; - private State state; - private Call pending; - private List result; - private List argList; - private boolean doProcess; - - public EvalStackFrame(Namespace namespace, RootNode rootNode) { - this.namespace = namespace; - this.rootNode = rootNode; - this.state = State.Incomplete; - this.argList = new LinkedList<>(); - rootNode.reset(); - rootNode.init(namespace); - doProcess = true; - } - - @Override - public State getState() { - return state; - } - - @Override - public StackFrame process(Env context) { - if (state != State.Incomplete) { - throw new IllegalStateException(); - } - if (!doProcess) { - return null; - } - try { - if (rootNode.isDone()) { - processResultFromNode(); - return null; - } else { - return processNextCommand(context); - } - } catch (Exception ex) { - result = List.of(PError.of(ex)); - state = State.Error; - return null; - } finally { - doProcess = false; - } - - } - - @Override - public void postResponse(Call call) { - if (pending != null && pending.matchID() == call.matchID()) { - pending = null; - if (call.isReply()) { - log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Return call : \n" + call); - postResponse(call.args()); - } else { - log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Error call : \n" + call); - this.state = State.Error; - this.result = call.args(); - } - doProcess = true; - } else { - log.log(Level.TRACE, () -> "EvalStackFrame - Received invalid call : \n" + call); - } - - } - - @Override - public void postResponse(State state, List args) { - if (this.state != State.Incomplete) { - throw new IllegalStateException(); - } - switch (state) { - case Incomplete: - throw new IllegalArgumentException(); - case OK: - postResponse(args); - break; - default: - this.state = state; - this.result = args; - } - doProcess = true; - } - - @Override - public List result() { - if (state == State.Incomplete) { - throw new IllegalStateException(); - } - if (result == null) { - return List.of(); - } else { - return result; - } - } - - private void postResponse(List args) { - try { - argList.clear(); - argList.addAll(args); - rootNode.postResponse(argList); - } catch (Exception ex) { - state = State.Error;//@TODO proper error reporting - } - } - - private void processResultFromNode() throws Exception { - argList.clear(); - rootNode.writeResult(argList); - result = List.copyOf(argList); - state = State.OK; - - } - - private StackFrame processNextCommand(Env context) - throws Exception { - - argList.clear(); - rootNode.writeNextCommand(argList); - if (argList.size() < 1) { - throw new Exception(); - } - Value cmdArg = argList.get(0); - if (cmdArg instanceof ControlAddress) { - routeCall(context, argList); - return null; - } - String cmdStr = cmdArg.toString(); - if (cmdStr.isEmpty()) { - throw new Exception(); - } - Command cmd = namespace.getCommand(cmdStr); - if (cmd != null) { - argList.remove(0); - return cmd.createStackFrame(namespace, List.copyOf(argList)); - } - if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) { - routeCall(context, argList); - return null; - } - - throw new Exception(); - - } - - private void routeCall(Env context, List argList) - throws Exception { - ControlAddress ad = ControlAddress.from(argList.get(0)) - .orElseThrow(Exception::new); - argList.remove(0); - Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList)); - log.log(Level.TRACE, () -> "Sending Call" + call); - pending = call; - context.getPacketRouter().route(call); - } - -} diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java index c5c168cc..9f6cc67c 100644 --- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java +++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java @@ -22,17 +22,18 @@ package org.praxislive.script.commands; import java.io.File; +import java.util.ArrayDeque; import java.util.List; import java.util.Map; +import java.util.Queue; import org.praxislive.core.Value; -import org.praxislive.core.syntax.InvalidSyntaxException; +import org.praxislive.core.types.PArray; import org.praxislive.core.types.PResource; import org.praxislive.script.Command; import org.praxislive.script.CommandInstaller; import org.praxislive.script.Namespace; +import org.praxislive.script.ScriptStackFrame; import org.praxislive.script.StackFrame; -import org.praxislive.script.ast.RootNode; -import org.praxislive.script.ast.ScriptParser; /** * @@ -41,7 +42,6 @@ public class ScriptCmds implements CommandInstaller { private final static ScriptCmds instance = new ScriptCmds(); public final static Command EVAL = new Eval(); - public final static Command INLINE_EVAL = new InlineEval(); public final static Command INCLUDE = new Include(); private ScriptCmds() { @@ -62,34 +62,44 @@ private static class Eval implements Command { @Override public StackFrame createStackFrame(Namespace namespace, List args) throws Exception { - if (args.size() != 1) { - throw new Exception(); + if (args.isEmpty()) { + throw new IllegalArgumentException("No script passed to eval"); } - String script = args.get(0).toString(); - try { - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace.createChild(), astRoot); - } catch (InvalidSyntaxException ex) { - throw new Exception(ex); + Queue queue = new ArrayDeque<>(args); + boolean inline = false; + boolean trap = false; + List allowed = null; + String script = null; + while (!queue.isEmpty()) { + String arg = queue.poll().toString(); + if ("--inline".equals(arg)) { + inline = true; + } else if ("--trap-errors".equals(arg)) { + trap = true; + if (allowed == null) { + allowed = List.of(); + } + } else if ("--allowed-commands".equals(arg)) { + allowed = PArray.from(queue.poll()).orElseThrow().asListOf(String.class); + } else { + script = arg; + break; + } } - } - } - - private static class InlineEval implements Command { - - @Override - public StackFrame createStackFrame(Namespace namespace, List args) - throws Exception { - if (args.size() != 1) { - throw new Exception(); + if (!queue.isEmpty()) { + throw new IllegalArgumentException("Additional arguments after script"); } - String script = args.get(0).toString(); - try { - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace, astRoot); - } catch (InvalidSyntaxException ex) { - throw new Exception(ex); + var bld = ScriptStackFrame.forScript(namespace, script); + if (inline) { + bld.inline(); + } + if (trap) { + bld.trapErrors(); + } + if (allowed != null) { + bld.allowedCommands(allowed); } + return bld.build(); } } @@ -105,8 +115,7 @@ public StackFrame createStackFrame(Namespace namespace, List args) throws PResource res = PResource.from(args.get(0)).orElseThrow(); File file = new File(res.value()); String script = Utils.loadStringFromFile(file); - RootNode astRoot = ScriptParser.getInstance().parse(script); - return new EvalStackFrame(namespace.createChild(), astRoot); + return ScriptStackFrame.forScript(namespace, script).build(); } catch (Exception ex) { throw new Exception(ex); } diff --git a/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java new file mode 100644 index 00000000..1a5f473d --- /dev/null +++ b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java @@ -0,0 +1,308 @@ +package org.praxislive.script; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.praxislive.core.Call; +import org.praxislive.core.Clock; +import org.praxislive.core.ControlAddress; +import org.praxislive.core.Lookup; +import org.praxislive.core.Packet; +import org.praxislive.core.Root; +import org.praxislive.core.RootHub; +import org.praxislive.core.Value; +import org.praxislive.core.types.PArray; +import org.praxislive.core.types.PError; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + */ +public class DefaultScriptServiceTest { + + private static final boolean VERBOSE = Boolean.getBoolean("praxis.test.verbose"); + private static final int TIMEOUT = Integer.getInteger("praxis.test.timeout", 1000000); + + public DefaultScriptServiceTest() { + } + + @Test + public void testInlineScript() throws Exception { + logTest("testInlineScript"); + String script = """ + set V1 "One" + set V2 "Two" + set V3 $V1 + set RET [array $V1 $V2 $V3] + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + var result = hub.poll(); + logCall("Result received", result); + assertTrue(result.isReply()); + assertEquals(1, result.args().size()); + var expected = Stream.of("One", "Two", "One") + .map(Value::ofObject) + .collect(PArray.collector()); + assertEquals(expected, result.args().get(0)); + } + + } + + @Test + public void testAtScript() throws Exception { + logTest("testAtScript"); + String script = """ + .foo + @ /bar { + set V 42 + @ ./baz test:component { + .value $V + } + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Call to /hub.foo received", call); + assertTrue(call.isRequest()); + assertEquals("/hub.foo", call.to().toString()); + assertTrue(call.args().isEmpty()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar.add-child received", call); + assertTrue(call.isRequest()); + assertEquals("/bar.add-child", call.to().toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar/baz.value received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.value", call.to().toString()); + assertEquals(1, call.args().size()); + assertEquals("42", call.args().get(0).toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Result received", call); + + assertTrue(call.isReply()); + + } + + } + + @Test + public void testEvalInline() throws Exception { + logTest("testEvalInline"); + String script = """ + eval --inline { + set X 42 + /hub.value $X + } + /hub.value $X + eval { + set Y 84 + /hub.value $Y + } + /hub.value $Y + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + + for (String expected : new String[]{"42", "42", "84"}) { + Call call = hub.poll(); + logCall("Value received", call); + assertEquals("/hub.value", call.to().toString()); + assertEquals(expected, call.args().get(0).toString()); + hub.dispatch(call.reply()); + } + + Call call = hub.poll(); + logCall("Expected execution failure", call); + assertTrue(call.isError()); + + } + } + + @Test + public void testEvalTrapErrors() throws Exception { + logTest("testEvalTrapErrors"); + String script = """ + eval --trap-errors { + set X 42 + /hub.value "FOO" + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Value received", call); + assertEquals("/hub.value", call.to().toString()); + assertEquals("FOO", call.args().get(0).toString()); + hub.dispatch(call.reply()); + call = hub.poll(); + logCall("Error result received", call); + assertTrue(call.isError()); + } + + } + + @Test + public void testEvalTrapErrorsNested() throws Exception { + logTest("testEvalTrapErrorsNested"); + String script = """ + set allowed [array "@"] + eval --trap-errors --allowed-commands $allowed { + @ /bar { + set V 42 + @ ./baz test:component { + .error + .value "FOO" + } + } + set Y [set X] + } + """; + var root = new DefaultScriptService(); + try (var hub = new RootHubImpl("script", root)) { + hub.start(); + hub.send("/script.eval", "/hub.result", script); + Call call = hub.poll(); + logCall("Call to /bar.add-child received", call); + assertTrue(call.isRequest()); + assertEquals("/bar.add-child", call.to().toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Call to /bar/baz.error received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.error", call.to().toString()); + hub.dispatch(call.error(PError.of("BAZ ERROR"))); + + call = hub.poll(); + logCall("Call to /bar/baz.value received", call); + assertTrue(call.isRequest()); + assertEquals("/bar/baz.value", call.to().toString()); + assertEquals(1, call.args().size()); + assertEquals("FOO", call.args().get(0).toString()); + hub.dispatch(call.reply()); + + call = hub.poll(); + logCall("Result received", call); + + assertTrue(call.isError()); + String errors = call.args().get(0).toString(); + if (VERBOSE) { + System.out.println(""); + System.out.println("Completed with errors"); + System.out.println("====================="); + System.out.println(errors); + } + assertTrue(errors.contains("set")); + assertTrue(errors.contains("BAZ ERROR")); + } + + } + + private static void logTest(String testName) { + if (VERBOSE) { + System.out.println(""); + System.out.println(""); + System.out.println(testName); + System.out.println("===================="); + } + } + + private static void logCall(String msg, Call call) { + if (VERBOSE) { + System.out.println(msg); + System.out.println(call); + } + } + + private static class RootHubImpl implements RootHub, AutoCloseable { + + private final String rootID; + private final Root root; + private final BlockingQueue queue; + + private Root.Controller ctrl; + + private RootHubImpl(String rootID, Root root) { + this.rootID = rootID; + this.root = root; + this.queue = new LinkedBlockingQueue<>(); + } + + @Override + public boolean dispatch(Packet packet) { + if (rootID.equals(packet.rootID())) { + return ctrl.submitPacket(packet); + } else { + return queue.offer(packet); + } + } + + @Override + public Clock getClock() { + return System::nanoTime; + } + + @Override + public Lookup getLookup() { + return Lookup.EMPTY; + } + + public void send(String to, String from, Object arg) { + send(to, from, List.of(Value.ofObject(arg))); + } + + public void send(String to, String from, List args) { + dispatch(Call.create(ControlAddress.of(to), + ControlAddress.of(from), + getClock().getTime(), + args + )); + } + + public Call poll() throws InterruptedException, TimeoutException { + Packet p = queue.poll(TIMEOUT, TimeUnit.MILLISECONDS); + if (p instanceof Call c) { + return c; + } + throw new TimeoutException("Call poll timed out"); + } + + public void start() { + ctrl = root.initialize(rootID, this); + ctrl.start(); + } + + @Override + public void close() { + ctrl.shutdown(); + try { + ctrl.awaitTermination(10, TimeUnit.SECONDS); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + } + +} diff --git a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java index c96eff8f..2b7f9c4b 100644 --- a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java +++ b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java @@ -1,95 +1,51 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ - package org.praxislive.script.ast; -import org.praxislive.script.ast.AddressNode; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Test; import org.praxislive.core.Value; import org.praxislive.core.ComponentAddress; import org.praxislive.script.Command; import org.praxislive.script.Namespace; import org.praxislive.script.Variable; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import static org.junit.Assert.*; - -/** - * - * - */ -public class AddressNodeTest { - public AddressNodeTest() { - } +import static org.junit.jupiter.api.Assertions.*; - @BeforeClass - public static void setUpClass() throws Exception { - } - @AfterClass - public static void tearDownClass() throws Exception { - } - - @Before - public void setUp() { - } - - @After - public void tearDown() { - } - -// /** -// * Test of init method, of class AddressNode. -// */ -// @Test -// public void testInit() { -// System.out.println("init"); -// Namespace namespace = null; -// AddressNode instance = null; -// instance.init(namespace); -// // TODO review the generated test code and remove the default call to fail. -// fail("The test case is a prototype."); -// } -// -// /** -// * Test of reset method, of class AddressNode. -// */ -// @Test -// public void testReset() { -// System.out.println("reset"); -// AddressNode instance = null; -// instance.reset(); -// // TODO review the generated test code and remove the default call to fail. -// fail("The test case is a prototype."); -// } +public class AddressNodeTest { /** * Test of writeResult method, of class AddressNode. */ @Test public void testWriteResult() { - System.out.println("writeResult"); - List args = new ArrayList(); Namespace ns = new NS(); - String[] ads = {"./to/here", "./to/here.control", "./to/here!port", ".control2"}; - for (String ad : ads) { - AddressNode instance = new AddressNode(ad); - instance.init(ns); - instance.writeResult(args); - System.out.println(ad + " : " + args.get(0)); - args.clear(); - } + List scratch = new ArrayList<>(); + List relative = List.of( + "./to/here", + "./to/here.control", + "./to/here!port", + ".control2"); + List absolute = List.of( + "/test/address/to/here", + "/test/address/to/here.control", + "/test/address/to/here!port", + "/test/address.control2"); + List result = relative.stream() + .map(address -> { + var addressNode = new AddressNode(address); + addressNode.init(ns); + scratch.clear(); + addressNode.writeResult(scratch); + return scratch.get(0).toString(); + }).toList(); + assertEquals(absolute, result); + } private class NS implements Namespace, Variable { + @Override public Variable getVariable(String id) { if ("_CTXT".equals(id)) { return this; @@ -97,30 +53,36 @@ public Variable getVariable(String id) { return null; } + @Override public void addVariable(String id, Variable var) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public Namespace createChild() { throw new UnsupportedOperationException("Not supported yet."); } + @Override public void setValue(Value value) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public Value getValue() { return ComponentAddress.of("/test/address"); } + @Override public Command getCommand(String id) { throw new UnsupportedOperationException("Not supported yet."); } + @Override public void addCommand(String id, Command cmd) { throw new UnsupportedOperationException("Not supported yet."); } } -} \ No newline at end of file +}