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