| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.brooklyn.core.workflow; |
| |
| import com.fasterxml.jackson.core.JsonProcessingException; |
| import com.google.common.collect.Iterables; |
| import org.apache.brooklyn.api.entity.EntityLocal; |
| import org.apache.brooklyn.api.entity.EntitySpec; |
| import org.apache.brooklyn.api.mgmt.Task; |
| import org.apache.brooklyn.api.typereg.RegisteredType; |
| import org.apache.brooklyn.core.entity.Dumper; |
| import org.apache.brooklyn.core.entity.EntityAsserts; |
| import org.apache.brooklyn.core.resolve.jackson.BeanWithTypePlanTransformer; |
| import org.apache.brooklyn.core.resolve.jackson.BeanWithTypeUtils; |
| import org.apache.brooklyn.core.sensor.Sensors; |
| import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; |
| import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport; |
| import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan; |
| import org.apache.brooklyn.core.workflow.steps.appmodel.SetSensorWorkflowStep; |
| import org.apache.brooklyn.core.workflow.steps.flow.LogWorkflowStep; |
| import org.apache.brooklyn.entity.stock.BasicApplication; |
| import org.apache.brooklyn.test.Asserts; |
| import org.apache.brooklyn.test.ClassLogWatcher; |
| import org.apache.brooklyn.util.collections.MutableList; |
| import org.apache.brooklyn.util.collections.MutableMap; |
| import org.apache.brooklyn.util.collections.MutableSet; |
| import org.apache.brooklyn.util.core.config.ConfigBag; |
| import org.apache.brooklyn.util.exceptions.Exceptions; |
| import org.apache.brooklyn.util.text.StringEscapes; |
| import org.apache.brooklyn.util.text.Strings; |
| import org.testng.annotations.Test; |
| |
| import java.time.Instant; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.BiConsumer; |
| import java.util.function.BiFunction; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| |
| public class WorkflowInputOutputExtensionTest extends BrooklynMgmtUnitTestSupport { |
| |
| protected void loadTypes() { |
| WorkflowBasicTest.addWorkflowStepTypes(mgmt); |
| } |
| |
| @Test |
| public void testParameterReference() { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) |
| .configure(WorkflowEffector.STEPS, MutableList.<Object>of() |
| .append("set-sensor p1 = ${p1}") |
| ) |
| ); |
| eff.apply((EntityLocal)app); |
| |
| Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); |
| Object result = invocation.getUnchecked(); |
| Asserts.assertNull(result); |
| |
| EntityAsserts.assertAttributeEquals(app, Sensors.newSensor(Object.class, "p1"), "p1v"); |
| } |
| |
| @Test |
| public void testMapOutputFromExplicitOutput() { |
| doTestMapOutput(cfg -> cfg.put(WorkflowEffector.OUTPUT, |
| MutableMap.of("c", "${c}", "d", "${d}", "e", "${e}") )); |
| } |
| |
| @Test |
| public void testMapOutputFromLastStep() { |
| doTestMapOutput(cfg -> { |
| List<Object> step = cfg.get(WorkflowEffector.STEPS); |
| step.add("let map result = { c: ${c}, d: ${d}, e: ${e} }"); |
| cfg.put(WorkflowEffector.STEPS, step); |
| cfg.put(WorkflowEffector.OUTPUT, "${result}"); |
| }); |
| } |
| |
| @Test |
| public void testMapOutputFromReturn() { |
| doTestMapOutput(cfg -> { |
| List<Object> step = cfg.get(WorkflowEffector.STEPS); |
| step.add("let map result = { c: ${c}, d: ${d}, e: ${e} }"); |
| step.add("return ${result}"); |
| cfg.put(WorkflowEffector.STEPS, step); |
| }); |
| } |
| |
| @Test |
| public void testMapOutputFromExplicitInput() { |
| doTestMapOutput(cfg -> { |
| List<Object> step = cfg.get(WorkflowEffector.STEPS); |
| step.add("let map result = { c: ${c}, d: ${d}, e: ${E} }"); |
| cfg.put(WorkflowEffector.STEPS, step); |
| cfg.put(WorkflowEffector.OUTPUT, "${result}"); |
| cfg.put(WorkflowEffector.INPUT, MutableMap.of("E", "b")); |
| }); |
| } |
| |
| public void doTestMapOutput(Consumer<ConfigBag> mod) { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| ConfigBag cfg = ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.STEPS, MutableList.<Object>of() |
| .append(MutableMap.of("s", "let map v = { x: { y: a }, z: b }", "id", "s1", "output", "${v}")) |
| .append("let c = ${x.y}") // reference output of previous step |
| .append("let d = ${v.z}") // reference workflow var |
| .append("let e = ${workflow.step.s1.output.z}") // reference explicit output step |
| ); |
| // mod says how results are returned |
| mod.accept(cfg); |
| |
| WorkflowEffector eff = new WorkflowEffector(cfg); |
| eff.apply((EntityLocal)app); |
| |
| Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); |
| Map result = (Map) invocation.getUnchecked(); |
| Asserts.assertEquals(result.get("c"), "a"); |
| Asserts.assertEquals(result.get("d"), "b"); |
| Asserts.assertEquals(result.get("e"), "b"); |
| } |
| |
| public RegisteredType addBeanWithType(String typeName, String version, String plan) { |
| loadTypes(); |
| return BrooklynAppUnitTestSupport.addRegisteredTypeBean(mgmt, typeName, version, |
| new BasicTypeImplementationPlan(BeanWithTypePlanTransformer.FORMAT, plan)); |
| } |
| |
| ClassLogWatcher lastLogWatcher; |
| |
| Object invokeWorkflowStepsWithLogging(List<Object> steps) throws Exception { |
| try (ClassLogWatcher logWatcher = new ClassLogWatcher(LogWorkflowStep.class)) { |
| lastLogWatcher = logWatcher; |
| |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("p1", MutableMap.of("defaultValue", "p1v"))) |
| .configure(WorkflowEffector.STEPS, steps)); |
| eff.apply((EntityLocal)app); |
| |
| Task<?> invocation = app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null); |
| return invocation.getUnchecked(); |
| } |
| } |
| |
| // test complex object in an expression |
| @Test |
| public void testMapOutputAsComplexFreemarkerVar() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let map my_map = { x: 1 }", |
| MutableMap.of("s", "let my_map_copy = ${my_map}", "output", "${my_map_copy}"))); |
| Asserts.assertEquals(output, MutableMap.of("x", 1)); |
| } |
| |
| @Test |
| public void testLetNullishRhsThenLhs() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let integer x = ${x} ?? 1", |
| MutableMap.of("s", "let x = ${x} ?? 2", "output", "${x}"))); |
| Asserts.assertEquals(output, 1); |
| } |
| |
| @Test |
| public void testLetTransformMaps() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let map a = { i: 1, z: { a: 1 } }", |
| "let map b = { ii: 2, z: { b: 2 } }", |
| "transform map x = ${a} ${b} | merge", |
| "return ${x}")); |
| Asserts.assertEquals(output, MutableMap.of("i", 1, "ii", 2, "z", MutableMap.of("b", 2))); |
| } |
| |
| @Test |
| public void testLetTransformMapsDeep() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let map a = { i: 1, z: { a: 1 } }", |
| "let map b = { ii: 2, z: { b: 2 } }", |
| "transform map x = ${a} ${b} | merge deep", |
| "return ${x}")); |
| Asserts.assertEquals(output, MutableMap.of("i", 1, "ii", 2, "z", MutableMap.of("a", 1, "b", 2))); |
| } |
| |
| @Test |
| public void testTransformMergeLists() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let list a = [ 1, 2 ]", |
| MutableMap.of("step", "let b", "value", MutableList.of(2, 3)), |
| "transform list x = ${a} ${b} | merge", |
| "return ${x}")); |
| Asserts.assertEquals(output, MutableList.of(1, 2, 2, 3)); |
| } |
| |
| @Test |
| public void testTransformMergeSets() throws Exception { |
| Object o1 = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let list a = [ 1, 2 ]", |
| MutableMap.of("step", "let b", "value", MutableList.of(2, 3)), |
| "transform x = ${a} ${b} | merge set", |
| "return ${x}")); |
| Asserts.assertEquals(o1, MutableSet.of(1, 2, 3)); |
| o1 = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let list a = [ 1, 2 ]", |
| MutableMap.of("step", "let b", "value", MutableList.of(2, 3)), |
| "transform set x = ${a} ${b} | merge", |
| "return ${x}")); |
| Asserts.assertEquals(o1, MutableSet.of(1, 2, 3)); |
| } |
| |
| @Test |
| public void testTransformLaxMergeAllowsNull() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let list a = [ 1 ]", |
| "transform list x = ${a} ${b} | merge lax", |
| "return ${x}")); |
| Asserts.assertEquals(output, MutableList.of(1)); |
| } |
| |
| @Test |
| public void testSimpleTransforms() throws Exception { |
| Function<String,Object> transform = filter -> { |
| try { |
| return invokeWorkflowStepsWithLogging(MutableList.of( |
| "let list a = [ 1, 2 ]", |
| "transform x = ${a} | "+filter, |
| "return ${x}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } |
| }; |
| Asserts.assertEquals(transform.apply("min"), 1); |
| Asserts.assertEquals(transform.apply("max"), 2); |
| Asserts.assertEquals(transform.apply("sum"), 3); |
| Asserts.assertEquals(transform.apply("size"), 2); |
| Asserts.assertEquals(transform.apply("average"), 1.5); |
| Asserts.assertEquals(transform.apply("first"), 1); |
| Asserts.assertEquals(transform.apply("last"), 2); |
| } |
| |
| @Test |
| public void testTransformMergeLaxTrimRemovesNull() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| MutableMap.of("step", "let a", "value", MutableList.of(2, null)), |
| "transform list x = ${a} ${a[1]} | merge lax | trim", |
| "return ${x}")); |
| Asserts.assertEquals(output, MutableList.of(2)); |
| } |
| |
| @Test |
| public void testLetKey() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let map a = { i: 1 }", |
| "let integer a.ii = 2", |
| "return ${a}")); |
| Asserts.assertEquals(output, MutableMap.of("i", 1, "ii", 2)); |
| } |
| |
| @Test |
| public void testLetMathOps() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let double x = 99 / 0 ?? ${x} ?? 4 * 4 - 5 * 3", |
| "let integer x = ${x} * 8 / 2.0", |
| MutableMap.of("s", "let x = ${x} / 5", "output", "${x}"))); |
| Asserts.assertEquals(output, 0.8); |
| } |
| |
| @Test |
| public void testLetMathMode() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| MutableMap.of("s", "let x = 11 % 7 % 4", "output", "${x}"))); |
| // should get 0, ie (11 % 7) % 4, not 11 % (7 % 4). |
| Asserts.assertEquals(output, 0); |
| } |
| |
| @Test |
| public void testLetInterpolationMode() { |
| Consumer<Map<String,Object>> invoke = input -> { try { |
| invokeWorkflowStepsWithLogging(MutableList.of("let person = Anna", |
| input, |
| "log NOTE: ${x}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } }; |
| BiFunction<Map<String,Object>,String,String> assertLetGives = (input,expected) -> { |
| invoke.accept(input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| |
| // disabled |
| assertLetGives.apply(MutableMap.of("step", "let x = \"${person}\"", "interpolation_mode", "disabled"), "\"${person}\""); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_mode", "disabled"), "${person}"); |
| assertLetGives.apply(MutableMap.of("step", "let x", "value", "\"${person}\"", "interpolation_mode", "disabled"), "\"${person}\""); |
| |
| // words forced |
| assertLetGives.apply(MutableMap.of("step", "let x = \"${person}\"", "interpolation_mode", "words"), "${person}"); |
| assertLetGives.apply(MutableMap.of("step", "let x = \"Anna\"", "interpolation_mode", "words"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_mode", "words"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x", "value", "\"${person}\"", "interpolation_mode", "words"), "${person}"); |
| |
| // full forced |
| assertLetGives.apply(MutableMap.of("step", "let x = \"${person}\"", "interpolation_mode", "full"), "\"Anna\""); |
| assertLetGives.apply(MutableMap.of("step", "let x = \"Anna\"", "interpolation_mode", "full"), "\"Anna\""); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_mode", "full"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x", "value", "\"${person}\"", "interpolation_mode", "full"), "\"Anna\""); |
| |
| // defaults - words for shorthand |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x = \"${person}\""), "${person}"); |
| assertLetGives.apply(MutableMap.of("step", "let x = \"Anna\""), "Anna"); |
| |
| // defaults - full for separate value |
| assertLetGives.apply(MutableMap.of("step", "let x", "value", "${person}"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x", "value", "\"${person}\""), "\"Anna\""); |
| } |
| |
| @Test |
| public void testLetInterpolationErrorMode() { |
| Consumer<Map<String,Object>> invoke = input -> { try { |
| invokeWorkflowStepsWithLogging(MutableList.of("let person = Anna", |
| input, |
| "log NOTE: ${x}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } }; |
| BiFunction<Map<String,Object>,String,String> assertLetGives = (input,expected) -> { |
| invoke.accept(input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| |
| // does nothing if value found |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_errors", "fail"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_errors", "blank"), "Anna"); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${person}", "interpolation_errors", "ignore"), "Anna"); |
| |
| // but if value not found |
| Asserts.assertFailsWith(() -> assertLetGives.apply(MutableMap.of("step", "let x = ${unknown_person}"), "not-used"), |
| e -> Asserts.expectedFailureContains(e, "unknown_person")); |
| Asserts.assertFailsWith(() -> assertLetGives.apply(MutableMap.of("step", "let x = ${unknown_person}", "interpolation_errors", "fail"), "not-used"), |
| e -> Asserts.expectedFailureContains(e, "unknown_person")); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${unknown_person}", "interpolation_errors", "blank"), ""); |
| assertLetGives.apply(MutableMap.of("step", "let x = ${unknown_person}", "interpolation_errors", "ignore"), "${unknown_person}"); |
| } |
| |
| @Test |
| public void testLoadInterpolationMode() { |
| BiConsumer<String,Map<String,Object>> invoke = (otherVarName, input) -> { try { |
| invokeWorkflowStepsWithLogging(MutableList.of( |
| "let person = Anna", |
| "let "+otherVarName+" = Other", |
| MutableMap.<String,Object>of("step", "load x = classpath://document-with-interpolated-expression.txt").add(input), |
| "log NOTE: ${x}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } }; |
| BiFunction<Map<String,Object>,String,String> assertLoadWithOtherVarGives = (input,expected) -> { |
| invoke.accept("other", input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| BiFunction<Map<String,Object>,String,String> assertLoadWithoutOtherVarGives = (input,expected) -> { |
| invoke.accept("ignored", input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| |
| assertLoadWithOtherVarGives.apply(null, "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithOtherVarGives.apply(MutableMap.of("interpolation_mode", "disabled"), "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithOtherVarGives.apply(MutableMap.of("interpolation_mode", "full"), "This is a test. Person is Anna and other is Other."); |
| |
| assertLoadWithoutOtherVarGives.apply(null, "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "disabled"), "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "full"), "This is a test. Person is Anna and other is ${other}."); |
| |
| |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_error", "fail"), "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "disabled", "interpolation_errors", "fail"), "This is a test. Person is ${person} and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "full"), "This is a test. Person is Anna and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "full", "interpolation_errors", "ignore"), "This is a test. Person is Anna and other is ${other}."); |
| assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "full", "interpolation_errors", "blank"), "This is a test. Person is Anna and other is ."); |
| Asserts.assertFailsWith(() -> assertLoadWithoutOtherVarGives.apply(MutableMap.of("interpolation_mode", "full", "interpolation_error", "fail"), "not-used"), |
| e -> Asserts.expectedFailureContains(e, "other")); |
| } |
| |
| @Test |
| public void testInterpolationNotRecursiveSoDisablingWorks() throws Exception { |
| // outwith inputs, interpolation is not recursive. for inputs, it is permitted to be. |
| Object result = invokeWorkflowStepsWithLogging(MutableList.of("let person1 = Anna", |
| MutableMap.of("step", "let person2 = ${person1}", "interpolation_mode", "disabled"), |
| MutableMap.of("step", "log P2: ${person2}"), |
| MutableMap.of("step", "let person3 = ${person2}"), |
| MutableMap.of("step", "log P3: ${person3}"), |
| "return ${person3}")); |
| Asserts.assertEquals(result, "${person1}"); |
| String p2 = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("P2:")).findAny().get(), "P2: "); |
| Asserts.assertEquals(p2, "${person1}"); |
| String p3 = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("P3:")).findAny().get(), "P3: "); |
| Asserts.assertEquals(p3, "${person1}"); |
| } |
| |
| @Test |
| public void testLetQuoteVar() { |
| Consumer<String> invoke = input -> { try { |
| invokeWorkflowStepsWithLogging(MutableList.of( |
| "let person = Anna", |
| "let note = "+input, |
| "log NOTE: ${note}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } }; |
| BiFunction<String,String,String> assertLetGives = (input,expected) -> { |
| invoke.accept(input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| |
| assertLetGives.apply("\"${person}\"", "${person}"); |
| assertLetGives.apply("\"${person}\" is ${person}", "${person} is Anna"); |
| assertLetGives.apply("\"${person} is\" ${person}", "${person} is Anna"); |
| assertLetGives.apply("\"${person}\" is '${person}'", "${person} is ${person}"); |
| assertLetGives.apply("\"${person}\" is '\"${person}\"'", "${person} is \"${person}\""); |
| assertLetGives.apply("\"${person}\" is 'person'", "${person} is person"); |
| |
| //Asserts.assertFails(() -> invoke.accept("\"${person}\" is ''person '")); |
| // now this is allowed as it will attempt to parse it as one big string if words do not parse |
| assertLetGives.apply("\"${person}\" is ''person '", "\"Anna\" is ''person '"); |
| |
| assertLetGives.apply("\"${person} is person \"", "${person} is person "); |
| Asserts.assertFails(() -> invoke.accept("\"\"${person}\" is person \"")); |
| assertLetGives.apply("\"'${person}' is person \"", "'${person}' is person "); |
| assertLetGives.apply("\"\\\"${person}\\\" is person \"", "\"${person}\" is person "); |
| Asserts.assertFails(() -> invoke.accept("\"\\\"${person}\\\" is \"person \"")); |
| assertLetGives.apply("\"\\\"${person}\" is \"person \"", "\"${person} is person "); |
| |
| // if there are spaces inside the interpolated expression, don't use the quote strategy |
| assertLetGives.apply("${ person } is \"${person}\"", "Anna is \"Anna\""); |
| } |
| |
| @Test |
| public void testSetSensorQuoteVar() { |
| Consumer<String> invoke = input -> { try { |
| invokeWorkflowStepsWithLogging(MutableList.of( |
| "let person = Anna", |
| "set-sensor note = "+input, |
| "let note = ${entity.sensor.note}", |
| "log NOTE: ${note}")); |
| } catch (Exception e) { |
| throw Exceptions.propagate(e); |
| } }; |
| BiFunction<String,String,String> assertSetSensorGives = (input,expected) -> { |
| invoke.accept(input); |
| String note = Strings.removeFromStart(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("NOTE:")).findAny().get(), "NOTE: "); |
| Asserts.assertEquals(note, expected); |
| return note; |
| }; |
| |
| assertSetSensorGives.apply("\"${person}\" is ${person}", "\"Anna\" is Anna"); |
| assertSetSensorGives.apply("\"${person}\" is '${person}'", "\"Anna\" is 'Anna'"); |
| assertSetSensorGives.apply("\"${person} is\" ${person}", "\"Anna is\" Anna"); |
| assertSetSensorGives.apply("\"${person}\" is ''${person}''", "\"Anna\" is ''Anna''"); |
| assertSetSensorGives.apply("\"${person}\" is 'person'", "\"Anna\" is 'person'"); |
| assertSetSensorGives.apply("\"${person}\" is ''person '", "\"Anna\" is ''person '"); // doesn't fail because quotes preserved |
| assertSetSensorGives.apply("\"${person} is person \"", "Anna is person "); |
| Asserts.assertFails(() -> invoke.accept("\"\"${person}\" is person \"")); |
| assertSetSensorGives.apply("\"'${person}' is person \"", "'Anna' is person "); |
| assertSetSensorGives.apply("\"\\\"${person}\\\" is person \"", "\"Anna\" is person "); |
| Asserts.assertFails(() -> invoke.accept("\"\\\"${person}\\\" is \"person \"")); |
| assertSetSensorGives.apply("\"\\\"${person}\" is \"person \"", "\"\\\"Anna\" is \"person \""); |
| } |
| |
| @Test |
| public void testTransformTrimString() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let person_spaces = \" Anna \"", |
| "let string person = ${person_spaces}", |
| "transform person_tidied = ${person_spaces} | trim", |
| "log PERSON: ${person}", |
| "log PERSON_TIDIED: ${person_tidied}")); |
| Asserts.assertEquals(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("PERSON:")).findAny().get(), "PERSON: Anna "); |
| Asserts.assertEquals(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith("PERSON_TIDIED:")).findAny().get(), "PERSON_TIDIED: Anna"); |
| } |
| |
| private void assertLogMessageFor(String prefix, String value) { |
| Asserts.assertEquals(lastLogWatcher.getMessages().stream().filter(s -> s.startsWith(prefix+": ")).findAny().get(), |
| prefix+": "+value); |
| } |
| |
| private Object let(String prefix, Object expression) throws Exception { |
| return invokeWorkflowStepsWithLogging(MutableList.of( |
| MutableMap.<String,Object>of("step", "let x1", "value", expression), |
| "let "+prefix+" x2 = ${x1}", "return ${x2}")); |
| } |
| |
| private Object transform(String filter, Object expression) throws Exception { |
| boolean inline = expression instanceof String && Strings.isNonEmpty((String)expression) && ((String)expression).trim().equals(expression); |
| Map<String,Object> letStep = MutableMap.<String,Object>of("step", "let x1" + (inline ? " = "+expression : "")); |
| if (!inline) letStep.put("value", expression); |
| return invokeWorkflowStepsWithLogging(MutableList.of(letStep, |
| "transform x2 = ${x1} | "+filter, "return ${x2}")); |
| } |
| |
| private void checkTransform(String prefix, Object expression, Object expectedResult) throws Exception { |
| Object out = transform(prefix, expression); |
| Asserts.assertEquals(out, expectedResult); |
| } |
| |
| private void checkTransformBidi(String prefix, Object expression, String expectedResult) throws Exception { |
| Object out = transform(prefix, expression); |
| |
| Object backIn = BeanWithTypeUtils.newYamlMapper(null, false, null, false).readValue((String) out, Object.class); |
| Asserts.assertEquals(backIn, expression); |
| |
| Asserts.assertEquals(out, expectedResult, "YAML encoding (for whitespace) different to expected; interesting, but not a real fault"); |
| } |
| |
| static String q(String s) { |
| return StringEscapes.JavaStringEscapes.wrapJavaString(s); |
| } |
| |
| @Test |
| public void testTransformYaml() throws Exception { |
| // null not permitted as return type; would be nice to support but not vital |
| // checkLetBidi("yaml string", null, "null"); |
| |
| checkTransform("yaml", 2, 2); |
| checkTransform("yaml", "2", 2); |
| checkTransform("yaml", q("2"), 2); //unwrapped once by shorthand, again by step |
| checkTransform("yaml", q(q("2")), "2"); //finally we get the string |
| |
| checkTransform("yaml parse", 2, 2); |
| checkTransform("yaml parse", "2", 2); |
| checkTransform("yaml parse", q("2"), 2); |
| checkTransform("yaml parse", q(q("2")), "2"); |
| |
| checkTransform("yaml string", 2, "2"); |
| checkTransform("yaml string", "2", "2"); |
| checkTransform("yaml string", q("2"), "2"); |
| checkTransform("yaml string", q(q("2")), "\"2\""); |
| |
| checkTransform("yaml encode", 2, "2"); |
| checkTransform("yaml encode", "2", "\"2\""); |
| checkTransform("yaml encode", q("2"), "\"2\""); |
| // for these especially the exact yaml is not significant, but included for interest |
| checkTransform("yaml encode", q(q("2")), "'\"2\"'"); |
| |
| checkTransformBidi("yaml encode", " ", "' '"); |
| checkTransformBidi("yaml encode", "\n", "|2+\n\n"); // 2 is totally unnecessary but returned |
| checkTransformBidi("yaml encode", " \n ", "\" \\n \""); // double quotes used if spaces and newlines significant |
| |
| checkTransform("yaml", "ignore \n---\n x: 1 \n", MutableMap.of("x", 1)); |
| checkTransform("yaml parse", "ignore \n---\n x: 1 \n y: 2", MutableMap.of("x", 1, "y", 2)); |
| checkTransform("yaml string", "ignore \n---\n x: 1 \n", " x: 1 \n"); |
| checkTransformBidi("yaml encode", "ignore \n---\n x: 1 \n", "\"ignore \\n---\\n x: 1 \\n\""); |
| checkTransform("trim | yaml encode", "ignore \n---\n x: 1 \n", "\"ignore \\n---\\n x: 1\""); |
| checkTransform("yaml string | trim | yaml encode", "ignore \n---\n x: 1 \n", "\"x: 1\""); |
| checkTransform("json string | trim", "ignore \n---\n x: 1 \n", "ignore \n---\n x: 1"); |
| |
| checkTransformBidi("yaml string", MutableMap.of("a", 1), "a: 1"); |
| checkTransformBidi("yaml string", MutableMap.of("a", MutableList.of(null), "x", "1"), "a:\n- null\nx: \"1\""); |
| checkTransform("yaml", MutableMap.of("x", 1), MutableMap.of("x", 1)); |
| } |
| |
| @Test |
| public void testTransformJson() throws Exception { |
| checkTransform("json", 2, 2); |
| checkTransform("json", "2", 2); |
| checkTransform("json", q("2"), 2); //unwrapped once by shorthand, again by step |
| checkTransform("json", q(q("2")), "2"); //finally we get the string |
| |
| checkTransform("json parse", 2, 2); |
| checkTransform("json parse", "2", 2); |
| checkTransform("json parse", q("2"), 2); |
| checkTransform("json parse", q(q("2")), "2"); |
| |
| checkTransform("json string", 2, "2"); |
| checkTransform("json string", "2", "2"); |
| checkTransform("json string", q("2"), "2"); |
| checkTransform("json string", q(q("2")), "\"2\""); |
| |
| checkTransform("json encode", 2, "2"); |
| checkTransform("json encode", "2", "\"2\""); |
| checkTransform("json encode", q("2"), "\"2\""); |
| // for these especially the exact yaml is not significant, but included for interest |
| checkTransform("json encode", q(q("2")), "\"\\\"2\\\"\""); |
| |
| checkTransform("json string", " ", " "); |
| checkTransform("json string", "\n", "\n"); |
| checkTransform("json string", " \n ", " \n "); |
| |
| checkTransformBidi("json encode", " ", "\" \""); |
| checkTransformBidi("json encode", "\n", "\"\\n\""); |
| checkTransformBidi("json encode", " \n ", "\" \\n \""); |
| |
| checkTransformBidi("json string", MutableMap.of("a", 1), "{\"a\":1}"); |
| checkTransform("json encode", MutableMap.of("a", 1), "{\"a\":1}"); |
| checkTransform("json string | json encode", MutableMap.of("a", 1), q("{\"a\":1}")); |
| checkTransformBidi("json string", MutableMap.of("a", MutableList.of(null), "x", "1"), "{\"a\":[null],\"x\":\"1\"}"); |
| checkTransform("json", MutableMap.of("x", 1), MutableMap.of("x", 1)); |
| } |
| |
| @Test |
| public void testTransformBash() throws Exception { |
| checkTransform("bash", 2, q("2")); |
| checkTransform("bash encode", 2, q("2")); |
| checkTransform("bash", "2", q("2")); |
| checkTransform("bash", q("2"), q("2")); //unwrapped once by shorthand, again by step |
| checkTransform("bash", q(q("2")), q("\"2\"")); //finally we get the string |
| |
| checkTransform("bash", "hello(n $o!)", "\"hello(n \\$o\"'!'\")\""); |
| checkTransform("bash", "two\nlines", "\"two\nlines\""); |
| checkTransform("bash", MutableMap.of("x", 1), q("{\"x\":1}")); |
| } |
| |
| public static class MockObject { |
| String id; |
| String name; |
| } |
| |
| @Test |
| public void testTransformYamlCoercion() throws Exception { |
| addBeanWithType("mock-object", "1", "type: " + MockObject.class.getName()); |
| Object result = invokeWorkflowStepsWithLogging(MutableList.of( |
| MutableMap.<String, Object>of("step", "let x1", "value", |
| "ignore\n" + |
| "---\n" + |
| " id: x\n" + |
| " name: foo"), |
| "transform mock-object result = ${x1} | yaml", |
| "return ${result}")); |
| Asserts.assertThat(result, r -> r instanceof MockObject); |
| Asserts.assertEquals(((MockObject)result).id, "x"); |
| Asserts.assertEquals(((MockObject)result).name, "foo"); |
| } |
| |
| @Test |
| public void testOutputYamlAndJson() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let map x = { i: [ 1, 'A' ] }", |
| "let map result = {}", |
| "transform result.y = ${x} | yaml string", |
| "transform result.j = ${x} | json string", // will give the stringified map |
| "transform result.j2 = ${result.j} | json string", // no change to the string |
| "transform result.e = ${x} | json encode", |
| "transform result.es = ${x} | json string | json encode", |
| "transform map result.m0 = ${x} | json", |
| "transform map result.m1 = ${result.j} | json", |
| "return ${result}")); |
| String s = "{\"i\":[1,\"A\"]}"; |
| Object m = MutableMap.of("i", MutableList.of(1, "A")); |
| Asserts.assertEquals(output, MutableMap.of("y", "i:\n- 1\n- A\n", "j", s, |
| "j2", s, "e", s, "es", q(s), |
| "m0", m, "m1", m)); |
| |
| String s0 = "{ i: [ 1, 'A' ] }"; |
| output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "let string x = "+ q(s0), // wrap to preserve the make the second thing in list be 'A' (including single quotes) |
| "let map result = {}", |
| "transform result.mp = ${x} | yaml", // parsing a string gives a map |
| "transform result.mp2 = ${result.mp} | yaml parse", // parsing a map has no effect |
| "transform result.mo = ${result.mp} | json", // json referencing a map has no discernable effect (though it serializes and deserializes) |
| "transform result.so = ${x} | json string", // pass through |
| "transform result.ss0 = ${result.so} | json encode", // encoding a string gives the double-stringified map, you can parse to get the string back |
| "transform result.ss1 = ${result.mp} | json encode", // stringifies |
| "transform result.ss2 = ${result.mp} | json string | json encode", // specifying string on non-string gives single encoding (again) |
| "return ${result}")); |
| Asserts.assertEquals(output, MutableMap.<String,Object>of("mp", m, "mp2", m, "mo", m, |
| "so", s0, "ss0", q(s0), "ss1", s, "ss2", q(s))); |
| } |
| |
| // test complex object in an expression |
| @Test |
| public void testAccessLocalOutput() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| MutableMap.of("id" , "l1", |
| "step", "let x = ${x} + ${workflow.step.l1.output.prev} ?? 1", |
| "output", MutableMap.of("prev", "${x}")), |
| "log ${x}", |
| MutableMap.of( |
| "step", "let x = ${x} + 2 * ${prev}", |
| "output", MutableMap.of("prev", "${x}")), |
| "log ${x}", |
| MutableMap.of("step" , "goto l1", |
| "condition", MutableMap.of("target", "${x}", "less-than", 10)), |
| "return ${workflow.step.l1.output.prev}" |
| )); |
| Asserts.assertEquals(output, 4); |
| Asserts.assertEquals(lastLogWatcher.getMessages(), MutableList.of( |
| "1", "3", "4", "12" |
| )); |
| } |
| |
| @Test |
| public void testLoadData() throws Exception { |
| Object output = invokeWorkflowStepsWithLogging(MutableList.of( |
| "load x = classpath://hello-world.txt", |
| "return ${x}")); |
| Asserts.assertStringContains((String)output, "The file hello-world.war contains its source code."); |
| } |
| |
| @Test |
| public void testSetSensorAtomicRequire() { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "init") |
| .configure(WorkflowEffector.STEPS, MutableList.of( |
| MutableMap.of( |
| "step", "set-sensor integer x = 0", |
| "require", MutableMap.of("when", "absent") |
| ) |
| ))); |
| eff.apply((EntityLocal)app); |
| |
| eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.STEPS, MutableList.of( |
| "let x = ${entity.sensor.x} ?? 0", |
| "let x2 = ${x} + 1", |
| MutableMap.of( |
| "step", "set-sensor x = ${x2}", |
| "require", "${x}" |
| ) |
| ))); |
| eff.apply((EntityLocal)app); |
| |
| // fails with Absent exception if value required |
| Asserts.assertFailsWith(() -> app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null).getUnchecked(), |
| e -> { |
| Asserts.expectedFailureContains(e, "x", "non-absent requirement"); |
| SetSensorWorkflowStep.SensorRequirementFailedAbsent e0 = (SetSensorWorkflowStep.SensorRequirementFailedAbsent) Exceptions.getFirstThrowableOfType(e, SetSensorWorkflowStep.SensorRequirementFailed.class); |
| Asserts.assertNull(e0.getSensorValue()); |
| return true; |
| }); |
| |
| // works if we require absent, and initializes it |
| app.invoke(app.getEntityType().getEffectorByName("init").get(), null).getUnchecked(); |
| |
| // fails with normal exception if we require absent when it isn't |
| Asserts.assertFailsWith(() -> app.invoke(app.getEntityType().getEffectorByName("init").get(), null).getUnchecked(), |
| e -> { |
| Asserts.expectedFailureContains(e, "x"); |
| // value not in output, not any mention of absence |
| Asserts.expectedFailureDoesNotContain(e, "0", "absent"); |
| // but value is in field |
| SetSensorWorkflowStep.SensorRequirementFailed e0 = Exceptions.getFirstThrowableOfType(e, SetSensorWorkflowStep.SensorRequirementFailed.class); |
| Asserts.assertEquals(e0.getSensorValue(), 0); |
| return true; |
| }); |
| |
| // subsequently works at least once, but probably gets some errors due to concurrency |
| List<Task<?>> tasks = MutableList.of(); |
| int NUM = 100; |
| for (int i=0; i<NUM; i++) tasks.add(app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null)); |
| long numErrors = tasks.stream().filter(t -> { |
| t.blockUntilEnded(); |
| return t.isError(); |
| }).count(); |
| Integer numSuccess = app.sensors().get(Sensors.newIntegerSensor("x")); |
| if (numSuccess==0) tasks.iterator().next().getUnchecked(); // show failure if they all failed |
| Asserts.assertEquals(numErrors + numSuccess, NUM, "Tally mismatch, had "+numErrors+" errors when sensor showed "+numSuccess); |
| } |
| |
| @Test |
| public void testSwitch() throws JsonProcessingException { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("x", null)) |
| .configure(WorkflowEffector.STEPS, MutableList.<Object>of() |
| .append(MutableMap.of("step", "switch ${x}", "cases", |
| MutableList.of( |
| MutableMap.of("condition", "A", "step", "return Aaa"), |
| MutableMap.of("condition", "B", "step", "return Bbb"), |
| "return Zzz"))) |
| ) |
| ); |
| eff.apply((EntityLocal) app); |
| |
| Asserts.assertEquals(app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), MutableMap.of("x", "A")).getUnchecked(), "Aaa"); |
| Asserts.assertEquals(app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), MutableMap.of("x", "B")).getUnchecked(), "Bbb"); |
| Asserts.assertEquals(app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), MutableMap.of("x", "C")).getUnchecked(), "Zzz"); |
| } |
| |
| @Test |
| public void testSwitchNoValueSingleDefaultCase() throws JsonProcessingException { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("x", null)) |
| .configure(WorkflowEffector.STEPS, MutableList.<Object>of() |
| .append(MutableMap.of("step", "switch", "cases", |
| MutableList.of( |
| "return Zzz"))) |
| ) |
| ); |
| eff.apply((EntityLocal) app); |
| |
| Asserts.assertEquals(app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), MutableMap.of("x", "A")).getUnchecked(), "Zzz"); |
| } |
| |
| @Test |
| public void testUtil() throws JsonProcessingException { |
| loadTypes(); |
| BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class)); |
| |
| WorkflowEffector eff = new WorkflowEffector(ConfigBag.newInstance() |
| .configure(WorkflowEffector.EFFECTOR_NAME, "myWorkflow") |
| .configure(WorkflowEffector.EFFECTOR_PARAMETER_DEFS, MutableMap.of("x", null)) |
| .configure(WorkflowEffector.STEPS, MutableList.<Object>of( |
| "set-sensor random = ${workflow.util.random}", |
| "set-sensor random2 = ${workflow.util.random}", |
| "set-sensor now = ${workflow.util.now}", |
| "set-sensor now_utc = ${workflow.util.now_utc}", |
| "set-sensor now_instant = ${workflow.util.now_instant}", |
| "set-sensor now_stamp = ${workflow.util.now_stamp}", |
| "set-sensor now_iso = ${workflow.util.now_iso}", |
| "set-sensor now_nice = ${workflow.util.now_nice}" |
| )) |
| ); |
| eff.apply((EntityLocal) app); |
| |
| app.invoke(app.getEntityType().getEffectorByName("myWorkflow").get(), null).getUnchecked(); |
| Dumper.dumpInfo(app); |
| |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Double.class, "random"), x -> x>0); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Double.class, "random2"), x -> x>0); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Double.class, "random"), x -> !app.sensors().get(Sensors.newSensor(Double.class, "random2")).equals(x)); |
| |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Long.class, "now"), l -> l > System.currentTimeMillis() - 5*1000 && l <= System.currentTimeMillis()); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Long.class, "now_utc"), l -> l > System.currentTimeMillis() - 5*1000 && l <= System.currentTimeMillis()); |
| |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(Instant.class, "now_instant"), l -> l.toEpochMilli() > System.currentTimeMillis() - 5*1000 && l.toEpochMilli() <= System.currentTimeMillis()); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(String.class, "now_iso"), l -> l.startsWith("202") && l.endsWith("Z")); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(String.class, "now_nice"), l -> l.startsWith("202")); |
| EntityAsserts.assertAttribute(app, Sensors.newSensor(String.class, "now_stamp"), l -> l.startsWith("202")); |
| |
| } |
| |
| } |