diff --git a/CHANGELOG.md b/CHANGELOG.md index d199ba896..5ed2fdc0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # ActFramework Change Log +##1.10.0** +* support plugin test engine #1339 + **1.9.2** * Act-test: It shall not prepend url context when specified url starts from `http` #1427 * 716a67d0 2020-10-13 | Bump junit from 4.11 to 4.13.1 in /legacy-testapp [dependabot[bot]] @@ -54,6 +57,7 @@ * In case Route mapping exception occurred, it shall display the relevant source file and highlight the place where mapping failed #1313 * update fastjson to 1.2.71 * API doc - URL path variable in POST endpoint info is incorrect #1284 +>>>>>>> master * Scenario manager - support loading test scenario files from child folders recursively #1337 * Param value loader framework - allow inject another controller class #1336 * `EnhancedAdaptiveMap.asMap(EnhancedAdaptiveMap)` generated `Map` shall implement hashCode and equals methods #1333 diff --git a/VERSION_MATRIX.md b/VERSION_MATRIX.md index 8b75fb2ec..941a48c77 100644 --- a/VERSION_MATRIX.md +++ b/VERSION_MATRIX.md @@ -1,6 +1,6 @@ # Version Matrix -| act 1.8.28 | 1.8.29 | 1.8.30a | 1.8.31 | 1.8.32 | 1.9.0a | 1.9.1b | +| act 1.8.28 | 1.8.29 | 1.8.30a | 1.8.31 | 1.8.32 | 1.9.0a | 1.9.2 | | --- ----: | ----: | -----: | -----: | -----: | -----: | -----: | | aaa 1.6.1 | 1.7.0 | 1.7.0 | 1.7.3 | 1.8.0 | 1.10.0 | 1.10.0 | | beetl 1.6.1 | 1.7.0 | 1.7.0 | 1.7.1 | 1.7.2 | 1.8.0 | 1.8.0 | diff --git a/pom.xml b/pom.xml index ab3ee4f18..bb68db24b 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ org.actframework act jar - 1.9.3-SNAPSHOT + 1.10.0-SNAPSHOT ACT Framework The ACT full stack MVC framework diff --git a/src/main/java/act/apidoc/SampleData.java b/src/main/java/act/apidoc/SampleData.java index b03c8e360..944fc848a 100644 --- a/src/main/java/act/apidoc/SampleData.java +++ b/src/main/java/act/apidoc/SampleData.java @@ -229,6 +229,8 @@ private static T generate( return (T) new File("/path/to/upload/file"); } else if (ISObject.class.isAssignableFrom(spec.rawType())) { return (T) SObject.of("/path/to/upload/file", ""); + } else if (Throwable.class.isAssignableFrom(spec.rawType())) { + return null; } else { return (T) generateSamplePojo(spec, typeParamLookup, typeChain, nameChain); } diff --git a/src/main/java/act/test/DefaultTestEngine.java b/src/main/java/act/test/DefaultTestEngine.java new file mode 100644 index 000000000..fc0955777 --- /dev/null +++ b/src/main/java/act/test/DefaultTestEngine.java @@ -0,0 +1,73 @@ +package act.test; + +/*- + * #%L + * ACT Framework + * %% + * Copyright (C) 2014 - 2020 ActFramework + * %% + * Licensed 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. + * #L% + */ + +import act.util.ProgressGauge; +import act.util.SingletonBase; +import org.osgl.$; +import org.osgl.util.Keyword; + +import javax.validation.ValidationException; + +public class DefaultTestEngine extends SingletonBase implements TestEngine { + + public static final Keyword NAME = Keyword.of("default"); + + @Override + public String getName() { + return NAME.toString(); + } + + @Override + public boolean isEmpty(Scenario scenario) { + return $.not(scenario.interactions); + } + + @Override + public void validate(Scenario scenario, TestSession session) throws ValidationException { + scenario.validate(session); + } + + @Override + public boolean run(Scenario scenario, TestSession session, ProgressGauge gauge) { + return scenario.runInteractions(session, gauge); + } + + @Override + public void setup() { + + } + + @Override + public void setupSession(TestSession session) { + session.prepareHttp(); + session.reset(); + } + + @Override + public void teardownSession(TestSession session) { + } + + @Override + public void teardown() { + } + +} diff --git a/src/main/java/act/test/Interaction.java b/src/main/java/act/test/Interaction.java index e9e7d6217..c1dd8490e 100644 --- a/src/main/java/act/test/Interaction.java +++ b/src/main/java/act/test/Interaction.java @@ -31,13 +31,12 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; -import org.osgl.exception.UnexpectedException; import org.osgl.http.H; import org.osgl.util.E; import org.osgl.util.IO; -import org.osgl.util.N; import org.osgl.util.S; +import javax.validation.ValidationException; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -65,8 +64,10 @@ public class Interaction implements ScenarioPart { private transient Metric metric = Act.metricPlugin().metric(ACT_TEST_INTERACTION); @Override - public void validate(TestSession session) throws UnexpectedException { - E.unexpectedIf(null == request, "request spec not specified in interaction[%s]", this); + public void validate(TestSession session) throws ValidationException { + if (null == request) { + throw new ValidationException(S.fmt("request spec not specified in interaction[%s]", this)); + } //E.unexpectedIf(null == response, "response spec not specified"); act.metric.Timer timer = metric.startTimer("validate"); try { diff --git a/src/main/java/act/test/InteractionPart.java b/src/main/java/act/test/InteractionPart.java index ff0cc27e1..ec99a293f 100644 --- a/src/main/java/act/test/InteractionPart.java +++ b/src/main/java/act/test/InteractionPart.java @@ -22,15 +22,17 @@ import org.osgl.exception.UnexpectedException; +import javax.xml.bind.ValidationException; + public interface InteractionPart { /** * Check if the interaction part is valid. * - * If the data is not valid then throw out {@link UnexpectedException} + * If the data is not valid then throw out {@link ValidationException} * * @param interaction * the interaction in which this part is in - * @throws {@link UnexpectedException} if the data is not valid + * @throws {@link ValidationException} if the data is not valid */ - void validate(Interaction interaction) throws UnexpectedException; + void validate(Interaction interaction) throws ValidationException; } diff --git a/src/main/java/act/test/RequestSpec.java b/src/main/java/act/test/RequestSpec.java index 01850ea2d..e4bb030f6 100644 --- a/src/main/java/act/test/RequestSpec.java +++ b/src/main/java/act/test/RequestSpec.java @@ -29,6 +29,7 @@ import org.osgl.http.H; import org.osgl.util.*; +import javax.validation.ValidationException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -90,7 +91,7 @@ public void resolveParent(RequestTemplateManager manager) { } @Override - public void validate(Interaction interaction) throws UnexpectedException { + public void validate(Interaction interaction) throws ValidationException { if (S.notBlank(email)) { return; } @@ -107,7 +108,9 @@ public void validate(Interaction interaction) throws UnexpectedException { method = H.Method.DELETE; url = delete; } - E.unexpectedIf(null == method, "method not specified in request spec of interaction[%s]", interaction); + if (null == method) { + throw new ValidationException(S.fmt("method not specified in request spec of interaction[%s]", interaction)); + } if (null == url || ".".equals(url)) { url = ""; } diff --git a/src/main/java/act/test/ResponseSpec.java b/src/main/java/act/test/ResponseSpec.java index 06896d99a..564435d84 100644 --- a/src/main/java/act/test/ResponseSpec.java +++ b/src/main/java/act/test/ResponseSpec.java @@ -9,9 +9,9 @@ * Licensed 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. @@ -21,13 +21,13 @@ */ import act.util.AdaptiveBeanBase; -import act.util.EnhancedAdaptiveMap; import com.alibaba.fastjson.JSON; import org.osgl.$; import org.osgl.exception.UnexpectedException; import org.osgl.http.H; import org.osgl.util.S; +import javax.validation.ValidationException; import java.lang.reflect.Field; import java.util.LinkedHashMap; import java.util.List; @@ -50,7 +50,7 @@ public enum Type { public Type __type; @Override - public void validate(Interaction interaction) throws UnexpectedException { + public void validate(Interaction interaction) throws ValidationException { checkForEmpty(interaction); } @@ -61,7 +61,7 @@ public String toString() { private void checkForEmpty(Interaction interaction) { if (size() == 0) { - throw new UnexpectedException("Empty response spec found in interaction[%s]", interaction); + throw new ValidationException(S.fmt("Empty response spec found in interaction[%s]", interaction)); } Map map = this.toMap(); String accept; @@ -110,7 +110,7 @@ private void checkForEmpty(Interaction interaction) { if (S.notBlank(downloadFilename)) { return; } - throw new UnexpectedException("Empty response spec found in interaction[%s]", interaction); + throw new ValidationException(S.fmt("Empty response spec found in interaction[%s]", interaction)); } } diff --git a/src/main/java/act/test/Scenario.java b/src/main/java/act/test/Scenario.java index 0e8cb7e50..22a911bab 100644 --- a/src/main/java/act/test/Scenario.java +++ b/src/main/java/act/test/Scenario.java @@ -28,16 +28,18 @@ import act.metric.MetricInfo; import act.metric.Timer; import act.test.util.*; +import act.util.AdaptiveBeanBase; import act.util.ProgressGauge; import org.osgl.$; -import org.osgl.exception.UnexpectedException; import org.osgl.logging.LogManager; import org.osgl.logging.Logger; import org.osgl.util.*; +import javax.persistence.Transient; +import javax.validation.ValidationException; import java.util.*; -public class Scenario implements ScenarioPart { +public class Scenario extends AdaptiveBeanBase implements ScenarioPart { private static final Logger LOGGER = LogManager.get(Scenario.class); @@ -45,6 +47,7 @@ public class Scenario implements ScenarioPart { private static final ThreadLocal current = new ThreadLocal<>(); + public String engine; public String name; public String issueKey; public boolean noIssue; @@ -69,7 +72,7 @@ public class Scenario implements ScenarioPart { public String urlContext; public String partition = PARTITION_DEFAULT; public String source; - private transient Metric metric = Act.metricPlugin().metric(MetricInfo.ACT_TEST_SCENARIO); + private final transient Metric metric = Act.metricPlugin().metric(MetricInfo.ACT_TEST_SCENARIO); public ScenarioManager scenarioManager; public RequestTemplateManager requestTemplateManager; @@ -120,6 +123,8 @@ public String getIgnoreReason() { return S.eq("true", ignore, S.IGNORECASE) ? "ignored" : ignore; } + + public String causeStackTrace() { return null == cause ? null: E.stackTrace(cause); } @@ -136,6 +141,63 @@ public String errorMessageOf(Interaction interaction) { return interaction.errorMessage; } + @Transient + public TestEngine getEngine() { + if (S.blank(engine)) { + return Act.getInstance(DefaultTestEngine.class); + } + TestEngineManager manager = Act.getInstance(TestEngineManager.class); + return manager.getEngine(engine); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getIssueUrl() { + return issueUrl; + } + + public String getIssueUrlIcon() { + return issueUrlIcon; + } + + public String getIgnore() { + return ignore; + } + + public List getInteractions() { + return interactions; + } + + public TestStatus getStatus() { + return status; + } + + public String getUrlContext() { + return urlContext; + } + + public String getPartition() { + return partition; + } + + public String getSource() { + return source; + } + + public String getErrorMessage() { + return errorMessage; + } + + public Throwable getCause() { + return cause; + } + public void resolveDependencies() { if (!allDepends.isEmpty()) { // already resolved @@ -169,7 +231,7 @@ public void resolveSetupDependencies() { } @Override - public void validate(TestSession session) throws UnexpectedException { + public void validate(TestSession session) throws ValidationException { errorIf(S.blank(name), "Scenario name not defined"); for (Interaction interaction : interactions) { interaction.validate(session); @@ -252,13 +314,13 @@ boolean run(TestSession session, ProgressGauge gauge) { } Timer timer = metric.startTimer("run"); try { - return generateTestData(session) && runInteractions(session, gauge); + return generateTestData(session) && getEngine().run(this, session, gauge); } finally { timer.stop(); } } - private boolean runInteractions(TestSession session, ProgressGauge gauge) { + boolean runInteractions(TestSession session, ProgressGauge gauge) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("run interactions for " + name); } diff --git a/src/main/java/act/test/ScenarioDebugHelper.java b/src/main/java/act/test/ScenarioDebugHelper.java index 50a8e74b8..cfc460c98 100644 --- a/src/main/java/act/test/ScenarioDebugHelper.java +++ b/src/main/java/act/test/ScenarioDebugHelper.java @@ -124,7 +124,7 @@ public Result testForm(String partition, ActionContext context) { } @PostAction({"e2e", "test", "tests"}) - @PropertySpec("name, ignore, source, status, issueUrl, title, errorMessage, interactions.status, interactions.description, interactions.stackTrace, interactions.errorMessage") + //@PropertySpec("name, ignore, source, status, issueUrl, title, errorMessage, interactions.status, interactions.description, interactions.stackTrace, interactions.errorMessage") @Async public List run(App app, String partition, ActionContext context, ProgressGauge gauge) { List results = test.run(app, null, partition, false, gauge); diff --git a/src/main/java/act/test/ScenarioPart.java b/src/main/java/act/test/ScenarioPart.java index fc2d52796..69f50b6e7 100644 --- a/src/main/java/act/test/ScenarioPart.java +++ b/src/main/java/act/test/ScenarioPart.java @@ -22,6 +22,8 @@ import org.osgl.exception.UnexpectedException; +import javax.xml.bind.ValidationException; + public interface ScenarioPart { /** * Check if the data is valid. @@ -30,7 +32,7 @@ public interface ScenarioPart { * * @throws {@link UnexpectedException} if the data is not valid */ - void validate(TestSession session) throws UnexpectedException; + void validate(TestSession session) throws ValidationException; void reset(); } diff --git a/src/main/java/act/test/Test.java b/src/main/java/act/test/Test.java index a8501c805..110e157eb 100644 --- a/src/main/java/act/test/Test.java +++ b/src/main/java/act/test/Test.java @@ -259,7 +259,7 @@ public void run(final App app) { if (null != o) { delay.set($.convert(o).to(Long.class)); } - boolean run = shallRunAutomatedTest(app); + boolean run = shouldRunAutomatedTest(app); if (run) { app.jobManager().post(SysEventId.POST_STARTED, new Runnable() { @Override @@ -287,12 +287,12 @@ public void run() { } } - public static boolean shallRunAutomatedTest(App app) { + public static boolean shouldRunAutomatedTest(App app) { return $.bool(app.config().get("test.run")) || $.bool(app.config().get("e2e.run")) || "test".equalsIgnoreCase(Act.profile()) || "e2e".equalsIgnoreCase(Act.profile()); } @GetAction("test/result") - @PropertySpec("error, scenario.partition, scenarios.name, scenarios.ignoreReason, scenarios.ignore, scenarios.source, scenarios.status, " + + @PropertySpec("error, scenarios.partition, scenarios.name, scenarios.ignoreReason, scenarios.ignore, scenarios.source, scenarios.status, " + "scenarios.issueUrl, scenarios.issueUrlIcon, scenarios.title, scenarios.errorMessage, " + "scenarios.interactions.status, scenarios.interactions.description, " + "scenarios.interactions.stackTrace, scenarios.interactions.errorMessage") @@ -319,10 +319,12 @@ public List run(App app, Keyword testId, String partition, boolean shu this.error = null; this.result = C.list(); this.gauge = gauge; + TestEngineManager engineManager = Act.getInstance(TestEngineManager.class); try { eventBus.trigger(TestStart.INSTANCE); app.captchaManager().disable(); registerTypeConverters(); + engineManager.setupEngines(); RequestTemplateManager requestTemplateManager = new RequestTemplateManager(); requestTemplateManager.load(); final ScenarioManager scenarioManager = new ScenarioManager(); @@ -347,7 +349,7 @@ public List run(App app, Keyword testId, String partition, boolean shu if (S.notBlank(partition) && S.neq(partition, scenario.partition)) { continue; } - if (scenario.interactions.isEmpty()) { + if (scenario.getEngine().isEmpty(scenario)) { continue; } if (!candidates.contains(scenario)) { @@ -458,6 +460,7 @@ public List run(App app, Keyword testId, String partition, boolean shu } else { app.captchaManager().enable(); } + engineManager.teardownEngines(); eventBus.trigger(TestStop.INSTANCE); } } diff --git a/src/main/java/act/test/TestEngine.java b/src/main/java/act/test/TestEngine.java new file mode 100644 index 000000000..b3dd7a624 --- /dev/null +++ b/src/main/java/act/test/TestEngine.java @@ -0,0 +1,90 @@ +package act.test; + +/*- + * #%L + * ACT Framework + * %% + * Copyright (C) 2014 - 2020 ActFramework + * %% + * Licensed 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. + * #L% + */ + +import act.util.ProgressGauge; + +import javax.validation.ValidationException; + +/** + * A `TestEngine` runs {@link Scenario test scenario} and {@link TestSession test session}. + */ +public interface TestEngine { + + /** + * Returns name of this test engine. + * + * Note test engine name must be unique across implmentations. + * + * @return test engine name + */ + String getName(); + + /** + * Check if there are any test steps provisioned in a {@link Scenario test scenario}. + * @param scenario the test scenario to be tested. + * @return `true` if there are test steps in the scenario or `false` otherwise. + */ + boolean isEmpty(Scenario scenario); + + /** + * Validate a {@link Scenario test scenario}. + * + * If there are any issue with the test scenario then a {@link ValidationException} + * shall be raised. + * + * @param scenario a test scenario to be validated. + * @param session the test session that contains the scenario. + * @throws ValidationException in case any issue with the test session + */ + void validate(Scenario scenario, TestSession session) throws ValidationException; + + /** + * Run a {@link Scenario test scenario}. + * + * @param scenario the scenario to run by the test engine. + * @param session the test session that contains the scenario. + * @param gauge the progress gauge to track progress. + * @return `true` if run pass, `false` otherwise. + */ + boolean run(Scenario scenario, TestSession session, ProgressGauge gauge); + + /** + * Set up the test engine before running any test. + */ + void setup(); + + /** + * Prepare to run a {@link TestSession} + */ + void setupSession(TestSession session); + + /** + * Tear down after running a {@link TestSession} + */ + void teardownSession(TestSession session); + + /** + * Tear down after running all tests + */ + void teardown(); + +} diff --git a/src/main/java/act/test/TestEngineManager.java b/src/main/java/act/test/TestEngineManager.java new file mode 100644 index 000000000..d661b5168 --- /dev/null +++ b/src/main/java/act/test/TestEngineManager.java @@ -0,0 +1,59 @@ +package act.test; + +/*- + * #%L + * ACT Framework + * %% + * Copyright (C) 2014 - 2020 ActFramework + * %% + * Licensed 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. + * #L% + */ + +import org.osgl.inject.annotation.MapKey; +import org.osgl.util.Keyword; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Map; +import java.util.Set; + +@Singleton +public class TestEngineManager { + + @Inject + @MapKey("name") + private Map engineLookup; + + public Set engineNames() { + return engineLookup.keySet(); + } + + public TestEngine getEngine(String name) { + TestEngine engine = engineLookup.get(Keyword.of(name)); + return null == engine ? engineLookup.get(DefaultTestEngine.NAME) : engine; + } + + public void setupEngines() { + for (TestEngine engine : engineLookup.values()) { + engine.setup(); + } + } + + public void teardownEngines() { + for (TestEngine engine: engineLookup.values()) { + engine.teardown(); + } + } + +} diff --git a/src/main/java/act/test/TestSession.java b/src/main/java/act/test/TestSession.java index 918c27b62..f7ac38f38 100644 --- a/src/main/java/act/test/TestSession.java +++ b/src/main/java/act/test/TestSession.java @@ -72,15 +72,14 @@ static TestSession current() { return current.get(); } - private boolean proceed; private Scenario running; - private Scenario target; - private List dependencies = new ArrayList<>(); - private App app; + private final Scenario target; + private final List dependencies = new ArrayList<>(); + private final App app; private int port = 5460; private OkHttpClient http; private CookieStore cookieStore; - private transient Metric metric = Act.metricPlugin().metric(MetricInfo.ACT_TEST_SCENARIO); + private final transient Metric metric = Act.metricPlugin().metric(MetricInfo.ACT_TEST_SCENARIO); $.Var lastData = $.var(); $.Var lastHeaders = $.var(); @@ -93,6 +92,7 @@ static TestSession current() { public TestSession(Scenario scenario, RequestTemplateManager requestTemplateManager) { this.requestTemplateManager = requestTemplateManager; dependencies.addAll(scenario.allDepends); + Collections.sort(dependencies, new ScenarioComparator(false)); target = scenario; app = Act.app(); if (null != app) { @@ -114,18 +114,21 @@ public int port() { public void run(ProgressGauge gauge) { current.set(this); - prepareHttp(); - gauge.incrMaxHintBy(dependencies.size() + 2); - reset(); - gauge.step(); - proceed = runAll(gauge, dependencies); - if (proceed) { - proceed = runOne(target, gauge); + target.getEngine().setupSession(this); + try { + gauge.incrMaxHintBy(dependencies.size() + 2); + gauge.step(); + boolean proceed = runAll(gauge, dependencies); + if (proceed) { + runOne(target, gauge); + } + gauge.step(); + } finally { + target.getEngine().teardownSession(this); } - gauge.step(); } - private boolean reset() { + boolean reset() { Collections.sort(dependencies, new ScenarioComparator(target.partition)); for (Scenario scenario : dependencies) { scenario.reset(); @@ -167,9 +170,10 @@ private boolean runOne(Scenario scenario, ProgressGauge gauge) { gauge.clearPayload(); gauge.setPayload(Test.PG_PAYLOAD_SCENARIO, scenario.title()); boolean okay; + TestEngine engine = scenario.getEngine(); try { running = $.requireNotNull(scenario); - scenario.validate(this); + engine.validate(scenario, this); constants.putAll(running.constants); okay = createFixtures() && scenario.run(this, gauge); scenario.status = TestStatus.of(okay); @@ -260,7 +264,7 @@ boolean verify(RequestSpec req, String operation) { } } - private void prepareHttp() { + void prepareHttp() { if (isTraceEnabled()) { trace("prepareHTTP for scenario: " + target.name); } diff --git a/src/main/java/act/test/util/NamedLogic.java b/src/main/java/act/test/util/NamedLogic.java index 169a204af..da7bfa827 100644 --- a/src/main/java/act/test/util/NamedLogic.java +++ b/src/main/java/act/test/util/NamedLogic.java @@ -25,6 +25,7 @@ import act.test.verifier.Verifier; import act.util.LogSupport; import org.osgl.$; +import org.osgl.exception.UnexpectedException; import org.osgl.util.*; import java.util.*; @@ -143,9 +144,13 @@ public T convert(LinkedHashMap o) { E.illegalArgumentIf(null == logic, "%s not found: %s", toType.getName(), key); logic = $.cloneOf(logic); logic.init(entry.getValue()); - if (revert && logic instanceof Verifier) { - final Verifier v = $.cast(logic); - return $.cast(new ReversedVerifier(v)); + if (revert) { + if (logic instanceof ReversibleLogic) { + final ReversibleLogic v = $.cast(logic); + return $.cast(v.reversed()); + } else { + throw new UnexpectedException("reverse decorator used on non reversible logic: " + key); + } } return logic; } @@ -174,9 +179,13 @@ public T convert(String o) { T logic = register.get(toType, key); E.illegalArgumentIf(null == logic, "%s not found: %s", toType.getName(), key); logic = $.cloneOf(logic); - if (revert && logic instanceof Verifier) { - final Verifier v = $.cast(logic); - return $.cast(new ReversedVerifier(v)); + if (revert) { + if (logic instanceof ReversibleLogic) { + final ReversibleLogic v = $.cast(logic); + return $.cast(v.reversed()); + } else { + throw new UnexpectedException("reverse decorator used on non reversible logic: " + key); + } } return logic; } diff --git a/src/main/java/act/test/util/ReversibleLogic.java b/src/main/java/act/test/util/ReversibleLogic.java new file mode 100644 index 000000000..cf0a55502 --- /dev/null +++ b/src/main/java/act/test/util/ReversibleLogic.java @@ -0,0 +1,29 @@ +package act.test.util; + +/*- + * #%L + * ACT Framework + * %% + * Copyright (C) 2014 - 2020 ActFramework + * %% + * Licensed 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. + * #L% + */ + +public interface ReversibleLogic { + /** + * Return a reversed logic of this logic + * @return the reversed logic of this logic + */ + T reversed(); +} diff --git a/src/main/java/act/test/util/ScenarioManager.java b/src/main/java/act/test/util/ScenarioManager.java index 8b3601b97..52ef1aaea 100644 --- a/src/main/java/act/test/util/ScenarioManager.java +++ b/src/main/java/act/test/util/ScenarioManager.java @@ -26,6 +26,7 @@ import act.app.RuntimeDirs; import act.conf.AppConfig; import act.test.Scenario; +import act.test.TestEngineManager; import org.osgl.$; import org.osgl.exception.UnexpectedException; import org.osgl.util.C; @@ -188,6 +189,7 @@ private void parseOne(String content, String fileName) { for (Map.Entry entry : loaded.entrySet()) { String key = entry.getKey(); Scenario scenario = entry.getValue(); + trySetTestEngine(scenario, fileName); scenario.scenarioManager = this; scenario.requestTemplateManager = this.requestTemplateManager; scenario.name = key; @@ -259,4 +261,26 @@ private void parseOne(String content, String fileName) { } } + /** + * In case engine is not set on the scenario and the scenarios file name part + * matches a certain test engine name, e.g. `selenium`, then default that + * engine to the scenario. + * + * @param scenario the scenario + * @param fileName the file name + */ + private void trySetTestEngine(Scenario scenario, String fileName) { + if ($.bool(scenario.engine)) { + return; + } + C.Set parts = C.Set(S.split(fileName, File.separatorChar).map(Keyword.F.FROM_STRING)); + TestEngineManager tem = Act.getInstance(TestEngineManager.class); + Set engines = tem.engineNames(); + Set set = parts.withIn(engines); + if (set.isEmpty()) { + return; + } + scenario.engine = set.iterator().next().toString(); + } + } diff --git a/src/main/java/act/test/verifier/ReversedVerifier.java b/src/main/java/act/test/verifier/ReversedVerifier.java index 314e8f996..08948f8f6 100644 --- a/src/main/java/act/test/verifier/ReversedVerifier.java +++ b/src/main/java/act/test/verifier/ReversedVerifier.java @@ -25,7 +25,7 @@ @NoAutoRegister public class ReversedVerifier extends Verifier { - private Verifier v; + Verifier v; public ReversedVerifier(Verifier v) { this.v = $.requireNotNull(v); diff --git a/src/main/java/act/test/verifier/Verifier.java b/src/main/java/act/test/verifier/Verifier.java index e0edf3383..9c562758f 100644 --- a/src/main/java/act/test/verifier/Verifier.java +++ b/src/main/java/act/test/verifier/Verifier.java @@ -21,9 +21,10 @@ */ import act.test.util.NamedLogic; +import act.test.util.ReversibleLogic; import org.osgl.util.converter.TypeConverterRegistry; -public abstract class Verifier extends NamedLogic { +public abstract class Verifier extends NamedLogic implements ReversibleLogic { public abstract boolean verify(Object value); @@ -37,7 +38,15 @@ public static void registerTypeConverters() { TypeConverterRegistry.INSTANCE.register(new FromString(Verifier.class)); } + @Override + public Verifier reversed() { + if (this instanceof ReversedVerifier) { + return ((ReversedVerifier)this).v; + } + return new ReversedVerifier(this); + } + public Verifier meOrReversed(boolean reversed) { - return reversed ? new ReversedVerifier(this) : this; + return reversed ? this.reversed() : this; } } diff --git a/testapps/GHIssues/pom.xml b/testapps/GHIssues/pom.xml index d9ba8cb88..d3d25c54f 100644 --- a/testapps/GHIssues/pom.xml +++ b/testapps/GHIssues/pom.xml @@ -5,14 +5,14 @@ 4.0.0 act-ghissues - 1.9.1-SNAPSHOT + 1.10.0-SNAPSHOT ActFramework Github Issue Reproduce App org.actframework act-starter-parent - 1.9.0.2 + 1.9.2.0 @@ -25,7 +25,7 @@ org.actframework act - 1.9.2-SNAPSHOT + 1.10.0-SNAPSHOT org.actframework