();
158 | files.add(f);
159 |
160 | TaskListener mockListener = Mockito.mock(TaskListener.class);
161 | Mockito.when(mockListener.getLogger()).thenReturn(System.out);
162 |
163 | CucumberTestResult testresult = parser.parse(files, mockListener);
164 | assertThat("Embedded items found", testresult.getFeatures().iterator().next().getChildren().iterator().next()
165 | .getEmbeddedItems(), hasSize(1));
166 | }
167 |
168 |
169 | private static File getResourceAsFile(String resource) throws Exception {
170 | URL url = CucumberJSONParserTest.class.getResource(resource);
171 | Assert.assertNotNull("Resource " + resource + " could not be found", url);
172 | File f = new File(url.toURI());
173 | return f;
174 | }
175 |
176 | }
177 |
178 |
179 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/DefaultTestResultParserImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2004-2009, Sun Microsystems, Inc.
5 | * Copyright (c) 2013, Cisco Systems, Inc., a California corporation
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | */
25 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
26 |
27 | import java.io.File;
28 | import java.io.IOException;
29 | import java.io.Serializable;
30 | import java.util.ArrayList;
31 | import java.util.List;
32 |
33 | import hudson.AbortException;
34 | import hudson.FilePath;
35 | import hudson.Launcher;
36 | import hudson.Util;
37 | import hudson.model.Run;
38 | import hudson.model.TaskListener;
39 | import hudson.remoting.VirtualChannel;
40 | import hudson.tasks.test.TestResult;
41 | import hudson.tasks.test.TestResultParser;
42 | import jenkins.MasterToSlaveFileCallable;
43 |
44 | // XXX This is a shameless rip of of hudson.tasks.test.DefaultTestResultParserImpl
45 | // however that implementation is brain dead and can not be used in a master/slave envoronment
46 | // as Jobs are not Serializable.
47 | // This will be pushed back upstream and removed once we have an LTS with this fix in.
48 | // Until then - keep a local copy.
49 |
50 | /**
51 | * Default partial implementation of {@link TestResultParser} that handles GLOB dereferencing and other checks
52 | * for user errors, such as misconfigured GLOBs, up-to-date checks on test reports.
53 | *
54 | * The instance of the parser will be serialized to the node that performed the build and the parsing will be
55 | * done remotely on that slave.
56 | *
57 | * @since 1.343
58 | * @author Kohsuke Kawaguchi
59 | * @author James Nord
60 | */
61 | public abstract class DefaultTestResultParserImpl extends TestResultParser implements Serializable {
62 |
63 | private static final long serialVersionUID = 1L;
64 |
65 | public static final boolean IGNORE_TIMESTAMP_CHECK = Boolean.getBoolean(TestResultParser.class.getName()
66 | + ".ignoreTimestampCheck");
67 | public static final String RERUN_CUCUMBER_JSON_REGEX = ".*rerun\\d+.cucumber.json";
68 |
69 |
70 | /**
71 | * This method is executed on the slave that has the report files to parse test reports and builds
72 | * {@link TestResult}.
73 | *
74 | * @param reportFiles List of files to be parsed. Never be empty nor null.
75 | * @param listener Use this to report progress and other problems. Never null.
76 | * @throws InterruptedException If the user cancels the build, it will be received as a thread interruption.
77 | * Do not catch it, and instead just forward that through the call stack.
78 | * @throws IOException If you don't care about handling exceptions gracefully, you can just throw
79 | * IOException and let the default exception handling in Hudson takes care of it.
80 | * @throws AbortException If you encounter an error that you handled gracefully, throw this exception and
81 | * Jenkins will not show a stack trace.
82 | */
83 | protected abstract TestResult
84 | parse(List reportFiles, TaskListener listener) throws InterruptedException, IOException;
85 |
86 |
87 | @Override
88 | public TestResult parseResult(final String testResultLocations,
89 | final Run, ?> build,
90 | final FilePath workspace,
91 | final Launcher launcher,
92 | final TaskListener listener) throws InterruptedException, IOException {
93 | boolean ignoreTimestampCheck = IGNORE_TIMESTAMP_CHECK; // so that the property can be set on the master
94 | long buildTime = build.getTimestamp().getTimeInMillis();
95 | long nowMaster = System.currentTimeMillis();
96 |
97 | ParseResultCallable callable =
98 | new ParseResultCallable(this, testResultLocations, ignoreTimestampCheck, buildTime, nowMaster,
99 | listener);
100 |
101 | return workspace.act(callable);
102 | }
103 |
104 |
105 |
106 |
107 | static final class ParseResultCallable extends MasterToSlaveFileCallable {
108 |
109 | private static final long serialVersionUID = -5438084460911132640L;
110 | private DefaultTestResultParserImpl parserImpl;
111 | private boolean ignoreTimestampCheck;
112 | private long buildTime;
113 | private long nowMaster;
114 | private String testResultLocations;
115 | private TaskListener listener;
116 |
117 |
118 | public ParseResultCallable(DefaultTestResultParserImpl parserImpl, String testResultLocations,
119 | boolean ignoreTimestampCheck, long buildTime, long nowMaster,
120 | TaskListener listener) {
121 | this.parserImpl = parserImpl;
122 | this.testResultLocations = testResultLocations;
123 | this.ignoreTimestampCheck = ignoreTimestampCheck;
124 | this.buildTime = buildTime;
125 | this.nowMaster = nowMaster;
126 | this.listener = listener;
127 | }
128 |
129 |
130 | public TestResult invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
131 | final long nowSlave = System.currentTimeMillis();
132 |
133 | // files older than this timestamp is considered stale
134 | long localBuildTime = buildTime + (nowSlave - nowMaster);
135 |
136 | FilePath[] paths = new FilePath(dir).list(testResultLocations);
137 | if (paths.length == 0)
138 | throw new AbortException("No test reports that matches " + testResultLocations
139 | + " found. Configuration error?");
140 |
141 | // since dir is local, paths all point to the local files
142 | List files = new ArrayList(paths.length);
143 | for (FilePath path : paths) {
144 | if(shouldSkipFile(isRerunAction(), path)) continue;
145 | File report = new File(path.getRemote());
146 | if (ignoreTimestampCheck || localBuildTime - 3000 /* error margin */< report.lastModified()) {
147 | // this file is created during this build
148 | files.add(report);
149 | }
150 | }
151 |
152 | if (files.isEmpty()) {
153 | // none of the files were new
154 | throw new AbortException(
155 | String.format("Test reports were found but none of them are new. Did tests run? %n"
156 | + "For example, %s is %s old%n",
157 | paths[0].getRemote(),
158 | Util.getTimeSpanString(localBuildTime
159 | - paths[0].lastModified())));
160 | }
161 |
162 | return parserImpl.parse(files, listener);
163 | }
164 |
165 | private boolean shouldSkipFile(boolean isRerunAction, FilePath path) {
166 | return !isRerunAction && path.getRemote().matches(RERUN_CUCUMBER_JSON_REGEX);
167 | }
168 |
169 | private boolean isRerunAction() {
170 | return testResultLocations.matches(RERUN_CUCUMBER_JSON_REGEX);
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013, Cisco Systems, Inc., a California corporation
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
25 |
26 | import hudson.Util;
27 | import hudson.XmlFile;
28 | import hudson.model.Action;
29 | import hudson.model.Job;
30 | import hudson.model.Run;
31 | import hudson.model.TaskListener;
32 | import hudson.tasks.junit.TestResult;
33 | import hudson.tasks.test.AbstractTestResultAction;
34 | import hudson.tasks.test.TestResultProjectAction;
35 | import hudson.util.HeapSpaceStringConverter;
36 | import hudson.util.XStream2;
37 | import jenkins.tasks.SimpleBuildStep.LastBuildAction;
38 |
39 | import java.io.File;
40 | import java.io.IOException;
41 | import java.lang.ref.WeakReference;
42 | import java.util.Collection;
43 | import java.util.Collections;
44 | import java.util.logging.Level;
45 | import java.util.logging.Logger;
46 |
47 | import org.kohsuke.stapler.StaplerProxy;
48 | import org.kohsuke.stapler.export.Exported;
49 |
50 | import com.thoughtworks.xstream.XStream;
51 |
52 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
53 |
54 | /**
55 | * {@link Action} that displays the Cucumber test result.
56 | *
57 | *
58 | * The actual test reports are isolated by {@link WeakReference}
59 | * so that it doesn't eat up too much memory.
60 | *
61 | * @author James Nord
62 | * @author Kohsuke Kawaguchi (original junit support)
63 | */
64 | @SuppressFBWarnings(value={"UG_SYNC_SET_UNSYNC_GET"}, justification="the getter and setter are both synchronized")
65 | public class CucumberTestResultAction extends AbstractTestResultAction implements StaplerProxy, LastBuildAction {
66 |
67 | private static final Logger LOGGER = Logger.getLogger(CucumberTestResultAction.class.getName());
68 |
69 | protected static final XStream XSTREAM = new XStream2();
70 |
71 | private transient WeakReference result;
72 |
73 | private int totalCount = -1;
74 | private int failCount = -1;
75 | private int skipCount = -1;
76 |
77 | static {
78 | XSTREAM.alias("result",CucumberTestResult.class);
79 | //XSTREAM.alias("suite",SuiteResult.class);
80 | //XSTREAM.alias("case",CaseResult.class);
81 | //XSTREAM.registerConverter(new HeapSpaceStringConverter(),100);
82 |
83 | XSTREAM.registerConverter(new HeapSpaceStringConverter(),100);
84 | }
85 |
86 |
87 |
88 | public CucumberTestResultAction(Run, ?> owner, CucumberTestResult result, TaskListener listener) {
89 | super();
90 | owner.addAction(this);
91 | setResult(result, listener);
92 | }
93 |
94 | /**
95 | * Overwrites the {@link CucumberTestResult} by a new data set.
96 | */
97 | public synchronized void setResult(CucumberTestResult result, TaskListener listener) {
98 |
99 | totalCount = result.getTotalCount();
100 | failCount = result.getFailCount();
101 | skipCount = result.getSkipCount();
102 |
103 | // persist the data
104 | try {
105 | getDataFile().write(result);
106 | } catch (IOException ex) {
107 | ex.printStackTrace(listener.fatalError("Failed to save the Cucumber test result."));
108 | LOGGER.log(Level.WARNING, "Failed to save the Cucumber test result.", ex);
109 | }
110 |
111 | this.result = new WeakReference(result);
112 | }
113 |
114 | protected XmlFile getDataFile() {
115 | return new XmlFile(XSTREAM,new File(run.getRootDir(), "cucumberResult.xml"));
116 | }
117 |
118 | /**
119 | * Loads a {@link TestResult} from disk.
120 | */
121 | private CucumberTestResult load() {
122 | CucumberTestResult r;
123 | try {
124 | r = (CucumberTestResult)getDataFile().read();
125 | } catch (IOException e) {
126 | LOGGER.log(Level.WARNING, "Failed to load " + getDataFile(), e);
127 | r = new CucumberTestResult(); // return a dummy
128 | }
129 | r.tally();
130 | r.setOwner(this.run);
131 | return r;
132 | }
133 |
134 | @Override
135 | @Exported(visibility = 2)
136 | public int getFailCount() {
137 | return failCount;
138 | }
139 |
140 | @Override
141 | @Exported(visibility = 2)
142 | public int getTotalCount() {
143 | return totalCount;
144 | }
145 |
146 | @Override
147 | @Exported(visibility = 2)
148 | public int getSkipCount() {
149 | return skipCount;
150 | }
151 |
152 |
153 | @Override
154 | @Exported(visibility = 5)
155 | public synchronized CucumberTestResult getResult() {
156 | CucumberTestResult r;
157 | if (result == null) {
158 | r = load();
159 | result = new WeakReference(r);
160 | }
161 | else {
162 | r = result.get();
163 | }
164 |
165 | if (r == null) {
166 | r = load();
167 | result = new WeakReference(r);
168 | }
169 |
170 | if (totalCount == -1) {
171 | totalCount = r.getTotalCount();
172 | failCount = r.getFailCount();
173 | skipCount = r.getSkipCount();
174 | }
175 | return r;
176 | }
177 |
178 | // Can't do this as AbstractTestResult is not generic!!!
179 | // @Override
180 | // public Collection getFailedTests() {
181 | // return getResult().getFailedTests();
182 | // };
183 |
184 | public Object getTarget() {
185 | return getResult();
186 | }
187 |
188 |
189 | @Override
190 | public String getDisplayName() {
191 | return "Cucumber Test Result";
192 | }
193 |
194 | @Override
195 | public String getUrlName() {
196 | return "cucumberTestReport";
197 | }
198 |
199 | /**
200 | * Merge results from other into an existing set of results.
201 | * @param other
202 | * the result to merge with the current results.
203 | * @param listener
204 | */
205 | protected synchronized void mergeResult(CucumberTestResult other, TaskListener listener) {
206 | CucumberTestResult cr = getResult();
207 | for (FeatureResult fr : other.getFeatures()) {
208 | // We need to add =the new results to the existing ones to keep the names stable
209 | // otherwise any embedded items will be attached to the wrong result
210 | // XXX this has the potential to cause a concurrentModificationException or other bad issues if someone is getting all the features...
211 | cr.addFeatureResult(fr);
212 | }
213 | //cr.tally();
214 | // XXX Do we need to add TagResults or call tally()?
215 | // persist the new result to disk
216 | this.setResult(cr, listener);
217 | }
218 |
219 | @Override
220 | public Collection extends Action> getProjectActions() {
221 | // TODO use our own action to not conflict with junit
222 | Job,?> job = run.getParent();
223 | if (/* getAction(Class) produces a StackOverflowError */!Util.filter(job.getActions(), TestResultProjectAction.class).isEmpty()) {
224 | // JENKINS-26077: someone like XUnitPublisher already added one
225 | return Collections.emptySet();
226 | }
227 | return Collections.singleton(new TestResultProjectAction(job));
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/test/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONSupportPluginIT.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2015, CloudBees, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
25 |
26 | import com.gargoylesoftware.htmlunit.html.HtmlPage;
27 | import hudson.model.*;
28 | import hudson.slaves.DumbSlave;
29 | import jenkins.model.Jenkins;
30 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
31 | import org.jenkinsci.plugins.workflow.job.WorkflowJob;
32 | import org.jenkinsci.plugins.workflow.job.WorkflowRun;
33 | import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
34 | import org.junit.Assert;
35 | import org.junit.Rule;
36 | import org.junit.Test;
37 | import org.jvnet.hudson.test.Issue;
38 | import org.jvnet.hudson.test.JenkinsRule;
39 | import org.jvnet.hudson.test.SingleFileSCM;
40 |
41 | import java.net.URL;
42 |
43 | import static org.hamcrest.Matchers.is;
44 | import static org.hamcrest.core.AllOf.allOf;
45 | import static org.hamcrest.core.StringContains.containsString;
46 | import static org.junit.Assert.assertThat;
47 | import static org.junit.Assert.assertTrue;
48 |
49 | public class CucumberJSONSupportPluginIT {
50 |
51 | @Rule
52 | public JenkinsRule jenkinsRule = new JenkinsRule();
53 |
54 | JenkinsRule.WebClient wc;
55 |
56 | @Test
57 | @Issue("JENKINS-28588")
58 | public void testSerializationOnSlave() throws Exception {
59 | DumbSlave slave = jenkinsRule.createOnlineSlave();
60 |
61 | SingleFileSCM scm = new SingleFileSCM("test.json",
62 | getResource("passWithEmbeddedItem.json")
63 | .toURI().toURL());
64 |
65 | FreeStyleProject project = jenkinsRule.createFreeStyleProject("cucumber-plugin-IT");
66 | project.setAssignedNode(slave);
67 | project.setScm(scm);
68 |
69 | CucumberTestResultArchiver resultArchiver = new CucumberTestResultArchiver("test.json");
70 |
71 | project.getPublishersList().add(resultArchiver);
72 |
73 | project.save();
74 |
75 | FreeStyleBuild build = jenkinsRule.buildAndAssertSuccess(project);
76 | jenkinsRule.assertLogContains("test.json", build);
77 | // check we built on the slave not the master...
78 |
79 | assertThat("Needs to build on the salve to check serialization", build.getBuiltOn(), is((Node) slave));
80 | }
81 |
82 | @Test
83 | @Issue("JENKINS-26340")
84 | public void testMergeStability() throws Exception {
85 | WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "merge1");
86 |
87 | job.setDefinition(new CpsFlowDefinition("node {\n" +
88 | " writeFile file: 'pass.json', text: '''" +
89 | getResourceAsString("featurePass.json") +
90 | " '''\n" +
91 | " step($class: 'CucumberTestResultArchiver', testResults: 'pass.json')\n" +
92 | "}\n" +
93 | "semaphore 'wait'\n" +
94 | "node {\n" +
95 | " writeFile file: 'fail.json', text: '''" +
96 | getResourceAsString("featureFail.json") +
97 | " '''\n" +
98 | " step($class: 'CucumberTestResultArchiver', testResults: 'fail.json')\n" +
99 | "}"));
100 |
101 |
102 | WorkflowRun r1 = job.scheduleBuild2(0).getStartCondition().get();
103 | // until after the first parsing has occurred.
104 | SemaphoreStep.waitForStart("wait/1", r1);
105 |
106 | assertTrue(JenkinsRule.getLog(r1), r1.isBuilding());
107 |
108 | // check the scenario is 1 passing
109 | wc = jenkinsRule.createWebClient();
110 | HtmlPage htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature");
111 | assertThat(htmlPage.asText(), containsString("0 failures"));
112 | // resume the build
113 | SemaphoreStep.success("wait/1", true);
114 |
115 | jenkinsRule.waitForCompletion(r1);
116 | // check the scenario is 1 passing and 1 failing
117 | Jenkins.getInstance().reload();
118 | wc = jenkinsRule.createWebClient();
119 | htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature");
120 | assertThat(htmlPage.asText(), containsString("0 failures"));
121 | htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature_2");
122 | assertThat(htmlPage.asText(), containsString("1 failures"));
123 |
124 | // check the build is unstable
125 | jenkinsRule.assertBuildStatus(Result.UNSTABLE, r1);
126 | }
127 |
128 |
129 | @Test
130 | @Issue("JENKINS-26340")
131 | public void testMergeStability2() throws Exception {
132 | WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "merge2");
133 |
134 | job.setDefinition(new CpsFlowDefinition("node {\n" +
135 | " writeFile file: 'fail.json', text: '''" +
136 | getResourceAsString("featureFail.json") +
137 | " '''\n" +
138 | " step($class: 'CucumberTestResultArchiver', testResults: 'fail.json')\n" +
139 | "}\n" +
140 | "semaphore 'wait'\n" +
141 | "node {\n" +
142 | " writeFile file: 'pass.json', text: '''" +
143 | getResourceAsString("featurePass.json") +
144 | " '''\n" +
145 | " step($class: 'CucumberTestResultArchiver', testResults: 'pass.json')\n" +
146 | "}"));
147 |
148 |
149 | WorkflowRun r1 = job.scheduleBuild2(0).getStartCondition().get();
150 | // until after the first parsing has occurred.
151 | SemaphoreStep.waitForStart("wait/1", r1);
152 |
153 | assertTrue(JenkinsRule.getLog(r1), r1.isBuilding());
154 |
155 | // check the scenario is 1 failing
156 | wc = jenkinsRule.createWebClient();
157 | HtmlPage htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature");
158 | assertThat(htmlPage.asText(), containsString("1 failures"));
159 | // resume the build
160 | SemaphoreStep.success("wait/1", true);
161 |
162 | jenkinsRule.waitForCompletion(r1);
163 |
164 | // check the scenario is 1 passing and 1 failing
165 | Jenkins.getInstance().reload();
166 | wc = jenkinsRule.createWebClient();
167 | htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature");
168 | assertThat(htmlPage.asText(), containsString("1 failures"));
169 | htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature_2");
170 | assertThat(htmlPage.asText(), containsString("0 failures"));
171 | // check the build is failure
172 | jenkinsRule.assertBuildStatus(Result.FAILURE, r1);
173 | }
174 |
175 | @Test
176 | public void testSymbol() throws Exception {
177 | WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "symbol");
178 |
179 | job.setDefinition(new CpsFlowDefinition("node {\n" +
180 | " writeFile file: 'pass.json', text: '''" +
181 | getResourceAsString("featurePass.json") +
182 | " '''\n" +
183 | " cucumber 'pass.json'\n" +
184 | "}"));
185 |
186 |
187 | Run r1 = jenkinsRule.buildAndAssertSuccess((Job)job);
188 |
189 | // check the scenario is 1 passing
190 | wc = jenkinsRule.createWebClient();
191 | HtmlPage htmlPage = wc.getPage(r1, "cucumberTestReport/foo-feature");
192 | assertThat(htmlPage.asText(), allOf(containsString("1 tests"), containsString("0 failures")));
193 | }
194 |
195 |
196 | private static URL getResource(String resource) throws Exception {
197 | URL url = CucumberJSONSupportPluginIT.class.getResource(CucumberJSONSupportPluginIT.class.getSimpleName() + "/" + resource);
198 | Assert.assertNotNull("Resource " + resource + " could not be found", url);
199 | return url;
200 | }
201 |
202 | private static String getResourceAsString(String resource) throws Exception {
203 | URL url = getResource(resource);
204 | return org.apache.commons.io.IOUtils.toString(url);
205 | }
206 |
207 | }
208 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResult.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013, Cisco Systems, Inc., a California corporation
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
25 |
26 | import gherkin.formatter.model.Tag;
27 | import hudson.model.Run;
28 | import hudson.tasks.test.MetaTabulatedResult;
29 | import hudson.tasks.test.TestObject;
30 | import hudson.tasks.test.TestResult;
31 |
32 | import java.util.ArrayList;
33 | import java.util.Collection;
34 | import java.util.HashMap;
35 | import java.util.List;
36 | import java.util.Map;
37 | import java.util.TreeMap;
38 |
39 | import org.kohsuke.stapler.StaplerRequest;
40 | import org.kohsuke.stapler.StaplerResponse;
41 | import org.kohsuke.stapler.export.Exported;
42 |
43 | /**
44 | * Represents all the Features from Cucumber.
45 | *
46 | * @author James Nord
47 | */
48 | public class CucumberTestResult extends MetaTabulatedResult {
49 |
50 | public static final String UNTAGGED_TEST_TAG = "@_UNTAGGED_";
51 |
52 | private static final long serialVersionUID = 3499017799686036745L;
53 |
54 | private List featureResults = new ArrayList();
55 |
56 | /**
57 | * Map of features keyed by feature name.
58 | * Recomputed by a call to {@link CucumberTestResult#tally()}
59 | */
60 | private transient Map featuresById = new TreeMap();
61 |
62 | /**
63 | * List of all failed ScenarioResults.
64 | * Recomputed by a call to {@link CucumberTestResult#tally()}
65 | */
66 | private transient List failedScenarioResults = new ArrayList();
67 |
68 | /**
69 | * map of Tags to Scenarios.
70 | * recomputed by a call to {@link CucumberTestResult#tally()}
71 | */
72 | private transient Map tagMap = new HashMap();
73 |
74 | private transient Run, ?> owner;
75 |
76 | /* Recomputed by a call to {@link CucumberTestResult#tally()} */
77 | private transient int passCount;
78 | private transient int failCount;
79 | private transient int skipCount;
80 | private transient float duration;
81 |
82 | private String nameAppendix = "";
83 |
84 | public CucumberTestResult() {
85 | }
86 |
87 |
88 | /**
89 | * Add a FeatureResult to this TestResult
90 | *
91 | * @param result the result of the feature to add.
92 | */
93 | void addFeatureResult(FeatureResult result) {
94 | featureResults.add(result);
95 | result.setParent(this);
96 | passCount += result.getPassCount();
97 | failCount += result.getFailCount();
98 | skipCount += result.getSkipCount();
99 | duration += result.getDuration();
100 | }
101 |
102 |
103 | @Override
104 | public String getName() {
105 | return "cucumber";
106 | }
107 |
108 |
109 | public String getChildTitle() {
110 | return "Feature Name";
111 | }
112 |
113 | @Override
114 | public Collection getChildren() {
115 | return featureResults;
116 | }
117 |
118 | @Exported(inline=true, visibility=9)
119 | public Collection getFeatures() {
120 | return featureResults;
121 | }
122 |
123 | @Override
124 | public boolean hasChildren() {
125 | return !featureResults.isEmpty();
126 | }
127 |
128 |
129 | @Override
130 | public Collection getFailedTests() {
131 | return failedScenarioResults;
132 | }
133 |
134 |
135 | @Override
136 | public Run, ?> getRun() {
137 | return owner;
138 | }
139 |
140 | void setOwner(Run, ?> owner) {
141 | this.owner = owner;
142 | for (FeatureResult fr : featureResults) {
143 | fr.setOwner(owner);
144 | }
145 | for (TagResult tr : tagMap.values()) {
146 | tr.setOwner(owner);
147 | }
148 | }
149 |
150 |
151 | @Override
152 | public TestObject getParent() {
153 | return null;
154 | }
155 |
156 |
157 | @Override
158 | public TestResult findCorrespondingResult(String id) {
159 | TestResult retVal = null;
160 | if (getId().equals(id) || (id == null)) {
161 | retVal = this;
162 | }
163 |
164 | else if (id.startsWith(getId() + "/")) {
165 | String idToFind = id.substring(getId().length() + 1);
166 | if (idToFind.startsWith("@")) {
167 | // tags have no children - actually they do but they are the child of FeatureResult!
168 | retVal = tagMap.get(idToFind);
169 | }
170 | // either a feature or a scenario
171 | else {
172 | int idx = idToFind.indexOf("/");
173 | if (idx == -1) {
174 | retVal = featuresById.get(idToFind);
175 | }
176 | else {
177 | String featureId = idToFind.substring(0, idx);
178 | String restId = idToFind.substring(idx + 1);
179 |
180 | FeatureResult fr = featuresById.get(featureId);
181 | if (fr != null) {
182 | retVal = fr.findCorrespondingResult(restId);
183 | }
184 | }
185 | }
186 | }
187 | return retVal;
188 | }
189 |
190 |
191 | /**
192 | * @return true if the test did not fail - this does not mean it had any successful tests however.
193 | */
194 | @Override
195 | public boolean isPassed() {
196 | return (getFailCount() == 0);
197 | }
198 |
199 |
200 | @Override
201 | public int getSkipCount() {
202 | return skipCount;
203 | }
204 |
205 |
206 | @Override
207 | public int getPassCount() {
208 | return passCount;
209 | }
210 |
211 |
212 | @Override
213 | public int getFailCount() {
214 | return failCount;
215 | }
216 |
217 |
218 | @Override
219 | public float getDuration() {
220 | return duration;
221 | }
222 |
223 |
224 | // @Override - this is an interface method
225 | public String getDisplayName() {
226 | return "Cucumber Test Results " + nameAppendix;
227 | }
228 |
229 |
230 | @Override
231 | public void tally() {
232 | if (failedScenarioResults == null) {
233 | failedScenarioResults = new ArrayList();
234 | }
235 | else {
236 | failedScenarioResults.clear();
237 | }
238 | if (tagMap == null) {
239 | tagMap = new HashMap();
240 | }
241 | else {
242 | tagMap.clear();
243 | }
244 |
245 | passCount = 0;
246 | failCount = 0;
247 | skipCount = 0;
248 | duration = 0.0f;
249 |
250 | if (featuresById == null) {
251 | featuresById = new TreeMap();
252 | }
253 | else {
254 | featuresById.clear();
255 | }
256 |
257 | for (FeatureResult fr : featureResults) {
258 | fr.tally();
259 | passCount += fr.getPassCount();
260 | failCount += fr.getFailCount();
261 | skipCount += fr.getSkipCount();
262 | duration += fr.getDuration();
263 | failedScenarioResults.addAll(fr.getFailedTests());
264 | featuresById.put(fr.getSafeName(), fr);
265 | for (ScenarioResult scenarioResult : fr.getChildren()) {
266 | for (Tag tag : scenarioResult.getParent().getFeature().getTags()) {
267 | TagResult tr = tagMap.get(tag.getName());
268 | if (tr == null) {
269 | tr = new TagResult(tag.getName());
270 | tagMap.put(tag.getName(), tr);
271 | }
272 | tr.addScenarioResult(scenarioResult);
273 | }
274 | if (scenarioResult.getScenario().getTags().isEmpty()) {
275 | TagResult tr = tagMap.get(UNTAGGED_TEST_TAG);
276 | if (tr == null) {
277 | tr = new TagResult(UNTAGGED_TEST_TAG);
278 | tagMap.put(UNTAGGED_TEST_TAG, tr);
279 | }
280 | tr.addScenarioResult(scenarioResult);
281 | }
282 | else {
283 | for (Tag tag : scenarioResult.getScenario().getTags()) {
284 | TagResult tr = tagMap.get(tag.getName());
285 | if (tr == null) {
286 | tr = new TagResult(tag.getName());
287 | tagMap.put(tag.getName(), tr);
288 | }
289 | tr.addScenarioResult(scenarioResult);
290 | }
291 | }
292 | }
293 | }
294 | // tally the tagResults
295 | for (TagResult tr : tagMap.values()) {
296 | tr.setParent(this);
297 | tr.tally();
298 | }
299 | }
300 |
301 | /**
302 | * Map of TagNames to TagResults.
303 | * @return the tagResults keyed by tag.getName().
304 | */
305 | public Map getTagMap() {
306 | return tagMap;
307 | }
308 |
309 | @Override
310 | public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
311 | // TODO Tag support!
312 | if (token.equals(getId())) {
313 | return this;
314 | }
315 | if (token.startsWith("@")) {
316 | TagResult result = tagMap.get(token);
317 | if (result != null) {
318 | return result;
319 | }
320 | }
321 | FeatureResult result = featuresById.get(token);
322 | if (result != null) {
323 | return result;
324 | }
325 | else {
326 | return super.getDynamic(token, req, rsp);
327 | }
328 | }
329 |
330 | @Override
331 | public String getDescription() {
332 | return "Cucumber Test Results " + nameAppendix;
333 | }
334 |
335 | public String getNameAppendix() {
336 | return nameAppendix;
337 | }
338 |
339 | public void setNameAppendix(String nameAppendix) {
340 | this.nameAppendix = nameAppendix;
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/TagResult/body.jelly:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
31 |
32 |
52 |
53 |
54 | ${%Failed Scenarios}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | |
66 | >>>
68 | <<<
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ${%Loading...}
79 |
80 | |
81 |
82 |
83 | ${f.durationString}
84 | |
85 |
86 | ${f.age}
87 | |
88 |
89 |
90 |
91 |
92 |
93 |
94 | ${%Scenarios}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
115 |
116 |
119 |
120 |
123 |
124 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | |
143 | ${p.durationString} |
144 | ${p.failCount} |
145 |
146 | ${h.getDiffString2(p.failCount-prev.failCount)}
147 | |
148 | ${p.skipCount} |
149 |
150 | ${h.getDiffString2(p.skipCount-prev.skipCount)}
151 | |
152 | ${p.totalCount} |
153 |
154 | ${h.getDiffString2(p.totalCount-prev.totalCount)}
155 | |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | ${%All Tags}
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
188 |
189 |
192 |
193 |
196 |
197 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | |
218 | ${p.durationString} |
219 | ${p.failCount} |
220 |
221 | ${h.getDiffString2(p.failCount-prev.failCount)}
222 | |
223 | ${p.skipCount} |
224 |
225 | ${h.getDiffString2(p.skipCount-prev.skipCount)}
226 | |
227 | ${p.totalCount} |
228 |
229 | ${h.getDiffString2(p.totalCount-prev.totalCount)}
230 | |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
--------------------------------------------------------------------------------
/src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResult/body.jelly:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
51 |
52 |
53 | ${%All Failed Scenarios}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | |
63 | >>>
65 | <<<
67 |
68 |
69 |
70 |
71 |
72 |
73 | ${%Loading...}
74 |
75 | |
76 |
77 |
78 | ${f.durationString}
79 | |
80 |
81 | ${f.age}
82 | |
83 |
84 |
85 |
86 |
87 |
88 |
89 | ${%All Features}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | |
121 |
122 |
123 |
124 |
125 | |
126 | ${p.durationString} |
127 | ${p.failCount} |
128 |
129 | ${h.getDiffString2(p.failCount-prev.failCount)}
130 | |
131 | ${p.skipCount} |
132 |
133 | ${h.getDiffString2(p.skipCount-prev.skipCount)}
134 | |
135 | ${p.totalCount} |
136 |
137 | ${h.getDiffString2(p.totalCount-prev.totalCount)}
138 | |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | ${%All Tags}
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | |
184 |
185 |
186 |
187 |
188 | |
189 | ${p.durationString} |
190 | ${p.failCount} |
191 |
192 | ${h.getDiffString2(p.failCount-prev.failCount)}
193 | |
194 | ${p.skipCount} |
195 |
196 | ${h.getDiffString2(p.skipCount-prev.skipCount)}
197 | |
198 | ${p.totalCount} |
199 |
200 | ${h.getDiffString2(p.totalCount-prev.totalCount)}
201 | |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/GherkinCallback.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013, Cisco Systems, Inc., a California corporation
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
25 |
26 | import gherkin.formatter.Formatter;
27 | import gherkin.formatter.Reporter;
28 | import gherkin.formatter.model.Background;
29 | import gherkin.formatter.model.Examples;
30 | import gherkin.formatter.model.Feature;
31 | import gherkin.formatter.model.Match;
32 | import gherkin.formatter.model.Result;
33 | import gherkin.formatter.model.Scenario;
34 | import gherkin.formatter.model.ScenarioOutline;
35 | import gherkin.formatter.model.Step;
36 | import gherkin.formatter.model.Tag;
37 | import hudson.model.TaskListener;
38 |
39 | import java.io.File;
40 | import java.io.IOException;
41 | import java.util.List;
42 | import java.util.logging.Level;
43 | import java.util.logging.Logger;
44 |
45 | /**
46 | * The implementation that gets called back by the Gherkin parser.
47 | *
48 | * @author James Nord
49 | */
50 | class GherkinCallback implements Formatter, Reporter {
51 |
52 | private static final Logger LOG = Logger.getLogger(GherkinCallback.class.getName());
53 | private boolean ignoreBadSteps = false;
54 | private TaskListener listener = null;
55 |
56 | private FeatureResult currentFeatureResult = null;
57 | private ScenarioResult currentScenarioResult = null;
58 | private BackgroundResult currentBackground = null;
59 |
60 | private Step currentStep = null;
61 | private Match currentMatch = null;
62 |
63 | private String currentURI = null;
64 |
65 | private CucumberTestResult testResult;
66 |
67 |
68 | GherkinCallback(CucumberTestResult testResult) {
69 | this.testResult = testResult;
70 | }
71 |
72 |
73 | GherkinCallback(CucumberTestResult testResult, TaskListener listener, boolean ignoreBadSteps){
74 | this(testResult);
75 | this.listener = listener;
76 | this.ignoreBadSteps = ignoreBadSteps;
77 | }
78 |
79 | // Formatter implementation
80 |
81 | // called before a feature to identify the feature
82 | public void uri(String uri) {
83 | LOG.log(Level.FINE, "URI: {0}", uri);
84 | if (currentURI != null) {
85 | LOG.log(Level.SEVERE, "URI received before previous uri handled");
86 | throw new CucumberModelException("URI received before previous uri handled");
87 | }
88 | currentURI = uri;
89 | }
90 |
91 |
92 | public void feature(Feature feature) {
93 | if (LOG.isLoggable(Level.FINE)) {
94 | LOG.log(Level.FINE, "Feature: " + feature.getKeyword() + feature.getName());
95 | List tags = feature.getTags();
96 | for (Tag tag : tags) {
97 | LOG.log(Level.FINE, " " + tag.getName());
98 | }
99 | LOG.log(Level.FINE, " " + feature.getDescription());
100 | }
101 | // a new feature being received signals the end of the previous feature
102 | currentFeatureResult = new FeatureResult(currentURI, feature);
103 | currentURI = null;
104 | testResult.addFeatureResult(currentFeatureResult);
105 | }
106 |
107 |
108 | // applies to a scenario
109 | public void background(Background background) {
110 | LOG.log(Level.FINE, "Background: {0}", background.getName());
111 | if (currentBackground != null) {
112 | LOG.log(Level.SEVERE, "Background: {" + background.getName() + "} received before previous background: {" + currentBackground.getName()+ "} handled");
113 | throw new CucumberModelException("Background: {" + background.getName() + "} received before previous background: {" + currentBackground.getName()+ "} handled");
114 | }
115 | currentBackground = new BackgroundResult(background);
116 | }
117 |
118 |
119 | public void scenario(Scenario scenario) {
120 | if (LOG.isLoggable(Level.FINE)) {
121 | LOG.log(Level.FINE, "Scenario: " + scenario.getKeyword() + " " + scenario.getName());
122 | List tags = scenario.getTags();
123 | for (Tag tag : tags) {
124 | LOG.log(Level.FINE, " " + tag.getName());
125 | }
126 | LOG.log(Level.FINE, " " + scenario.getDescription());
127 | LOG.log(Level.FINE, " " + scenario.getComments());
128 | }
129 | // a new scenario signifies that the previous scenario has been handled.
130 | currentScenarioResult = new ScenarioResult(scenario, currentBackground);
131 | currentBackground = null;
132 | currentFeatureResult.addScenarioResult(currentScenarioResult);
133 | }
134 |
135 |
136 | // appears to not be called.
137 | public void scenarioOutline(ScenarioOutline scenarioOutline) {
138 | LOG.log(Level.FINE, "ScenarioOutline: {0}", scenarioOutline.getName());
139 | }
140 |
141 |
142 | // appears to not be called.
143 | public void examples(Examples examples) {
144 | // not stored in the json - used in the Gherkin only
145 | LOG.log(Level.FINE, "Examples: {0}", examples.getName());
146 | }
147 |
148 | // appears to not be called.
149 | public void startOfScenarioLifeCycle(Scenario scenario) {
150 | LOG.log(Level.FINE, "startOfScenarioLifeCycle: {0}", scenario.getName());
151 | }
152 |
153 | // appears to not be called.
154 | public void endOfScenarioLifeCycle(Scenario scenario) {
155 | LOG.log(Level.FINE, "endOfScenarioLifeCycle: {0}", scenario.getName());
156 | }
157 |
158 | // A step has been called - could be in a background or a Scenario
159 | public void step(Step step) {
160 | if (LOG.isLoggable(Level.FINE)) {
161 | LOG.log(Level.FINE, "Step: " + step.getKeyword() + " " + step.getName());
162 | LOG.log(Level.FINE, " " + step.getRows());
163 | // logger.fine(" " + step.getStackTraceElement());
164 | }
165 | if (currentStep != null) {
166 | String error = "Step: {" + step.getKeyword() + "} name: {" + step.getName() +
167 | "} received before previous step: {" + step.getKeyword() + "} name: {" + step.getName() +
168 | "} handled! Maybe caused by broken JSON, see #JENKINS-21835";
169 | listener.error(error);
170 | LOG.log(Level.SEVERE, error);
171 | if (!ignoreBadSteps) {
172 | throw new CucumberModelException(error);
173 | }
174 | }
175 | currentStep = step;
176 | }
177 |
178 | // marks the end of a feature
179 | public void eof() {
180 | LOG.log(Level.FINE, "eof");
181 | currentFeatureResult = null;
182 | currentScenarioResult = null;
183 | currentBackground = null;
184 | currentStep = null;
185 | currentURI = null;
186 | }
187 |
188 |
189 | public void syntaxError(String state, String event, List legalEvents, String uri, Integer line) {
190 | LOG.log(Level.SEVERE, "syntaxError: - Failed to parse Gherkin json file.");
191 | StringBuilder sb = new StringBuilder("Failed to parse Gherkin json file.");
192 | sb.append("\tline: ").append(line);
193 | sb.append("\turi: ").append(uri);
194 | sb.append("\tState: ").append(state);
195 | sb.append("\tEvent: ").append(event);
196 | throw new CucumberModelException(sb.toString());
197 | }
198 |
199 | public void done() {
200 | // appears to not be called?
201 | LOG.log(Level.FINE, "done");
202 | }
203 |
204 | public void close() {
205 | // appears to not be called?
206 | LOG.log(Level.FINE, "close");
207 | }
208 |
209 |
210 | // Reporter implementation.
211 |
212 | // applies to a scenario - any code that is tagged as @Before
213 | public void before(Match match, Result result) {
214 | if (LOG.isLoggable(Level.FINE)) {
215 | LOG.log(Level.FINE, "rep before match: " + match.getLocation());
216 | LOG.log(Level.FINE, "rep result : " + "(passed) " + Result.PASSED.equals(result.getStatus()));
217 | LOG.log(Level.FINE, "rep result : " + result.getDuration());
218 | LOG.log(Level.FINE, "rep result : " + result.getErrorMessage());
219 | LOG.log(Level.FINE, "rep result : " + result.getError());
220 | }
221 | currentScenarioResult.addBeforeResult(new BeforeAfterResult(match, result));
222 | }
223 |
224 |
225 | // applies to a step, may be in a scenario or a background
226 | public void result(Result result) {
227 | if (LOG.isLoggable(Level.FINE)) {
228 | LOG.log(Level.FINE, "rep result: " + "(passed) " + Result.PASSED.equals(result.getStatus()));
229 | LOG.log(Level.FINE, "rep " + result.getDuration());
230 | LOG.log(Level.FINE, "rep " + result.getErrorMessage());
231 | LOG.log(Level.FINE, "rep " + result.getError());
232 | }
233 | StepResult stepResult = new StepResult(currentStep, currentMatch, result);
234 | if (currentBackground != null) {
235 | currentBackground.addStepResult(stepResult);
236 | }
237 | else {
238 | currentScenarioResult.addStepResult(stepResult);
239 | }
240 | currentStep = null;
241 | currentMatch = null;
242 | }
243 |
244 |
245 | // applies to a scenario - any code that is tagged as @After
246 | public void after(Match match, Result result) {
247 | if (LOG.isLoggable(Level.FINE)) {
248 | LOG.log(Level.FINE, "rep after match : " + match.getLocation());
249 | LOG.log(Level.FINE, "rep result : " + "(passed) " + Result.PASSED.equals(result.getStatus()));
250 | LOG.log(Level.FINE, "rep result : " + result.getDuration());
251 | LOG.log(Level.FINE, "rep result : " + result.getErrorMessage());
252 | LOG.log(Level.FINE, "rep result : " + result.getError());
253 | }
254 | currentScenarioResult.addAfterResult(new BeforeAfterResult(match, result));
255 | }
256 |
257 |
258 | // applies to a step
259 | public void match(Match match) {
260 | // applies to a step.
261 | LOG.log(Level.FINE, "rep match: {0}", match.getLocation());
262 | if (currentMatch != null) {
263 | LOG.log(Level.SEVERE, "Match: " + match.getLocation() + " received before previous Match: " +
264 | currentMatch.getLocation()+ "handled");
265 | throw new CucumberModelException("Match: " + match.getLocation() + " received before previous Match: " +
266 | currentMatch.getLocation()+ "handled");
267 | }
268 | currentMatch = match;
269 | }
270 |
271 |
272 | public void embedding(String mimeType, byte[] data) {
273 | LOG.log(Level.FINE, "rep embedding: {0}", mimeType);
274 | try {
275 | File f = CucumberUtils.createEmbedFile(data);
276 | EmbeddedItem embed = new EmbeddedItem(mimeType, f.getName());
277 | currentScenarioResult.addEmbeddedItem(embed);
278 | }
279 | catch (IOException ex) {
280 | throw new CucumberPluginException("Failed to write embedded data to temporary file", ex);
281 | }
282 | }
283 |
284 |
285 | public void write(String text) {
286 | LOG.log(Level.FINE, "rep write: {0}", text);
287 | }
288 |
289 | }
290 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioToHTML.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2013, Cisco Systems, Inc., a California corporation
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
25 |
26 | import gherkin.formatter.model.Background;
27 | import gherkin.formatter.model.Comment;
28 | import gherkin.formatter.model.DataTableRow;
29 | import gherkin.formatter.model.DescribedStatement;
30 | import gherkin.formatter.model.Match;
31 | import gherkin.formatter.model.Result;
32 | import gherkin.formatter.model.Step;
33 | import gherkin.formatter.model.Tag;
34 | import gherkin.formatter.model.TagStatement;
35 |
36 | import java.util.List;
37 | import java.util.Locale;
38 |
39 | public class ScenarioToHTML {
40 |
41 | private enum RESULT_TYPE {
42 |
43 | /** Step failed as it was not defined */
44 | UNDEFINED("background-color: #ffeeee;"),
45 | /** step passed */
46 | PASSED("background-color: #e6ffcc;"),
47 | /** step failed */
48 | FAILED("background-color: #ffeeee;"),
49 | /** step skipped due to previous failure */
50 | SKIPPED("background-color: #ffffcc;"),
51 | /** line does not have a result */
52 | NO_RESULT(""),
53 | /** glue code is not implemented */
54 | PENDING("background-color: #ffffcc;");
55 |
56 | public final String css;
57 |
58 |
59 | RESULT_TYPE(String css) {
60 | this.css = css;
61 | }
62 |
63 |
64 | public static RESULT_TYPE typeFromResult(Result r) {
65 | return RESULT_TYPE.valueOf(r.getStatus().toUpperCase(Locale.UK));
66 | }
67 | }
68 |
69 | private int indent = 0;
70 |
71 | private ScenarioResult scenarioResult;
72 |
73 |
74 | public ScenarioToHTML(ScenarioResult scenarioResult) {
75 | this.scenarioResult = scenarioResult;
76 | }
77 |
78 |
79 | public static String getHTML(ScenarioResult scenarioResult) {
80 | return new ScenarioToHTML(scenarioResult).getHTML();
81 | }
82 |
83 |
84 | /**
85 | * Builds a Gherkin file from the results of the parsing and formats it for HTML. XXX this should be moved
86 | * elsewhere!
87 | */
88 | public String getHTML() {
89 | // we will be pretty big so start of large to avoild re-allocation.
90 | StringBuilder sb = new StringBuilder(20 * 1024);
91 |
92 | sb.append("\n");
93 | sb.append("\n");
94 | // being gherkin output...
95 |
96 | addTagStatement(sb, scenarioResult.getParent().getFeature());
97 |
98 | for (BeforeAfterResult before : scenarioResult.getBeforeResults()) {
99 | addBeforeAfterResult(sb, "before", before);
100 | }
101 | addBackgroundResult(sb, scenarioResult.getBackgroundResult());
102 |
103 | addTagStatement(sb, scenarioResult.getScenario());
104 |
105 | for (StepResult stepResult : scenarioResult.getStepResults()) {
106 | addStepResult(sb, stepResult);
107 | }
108 | for (BeforeAfterResult after : scenarioResult.getAfterResults()) {
109 | addBeforeAfterResult(sb, "after", after);
110 | }
111 | // end gherkin output...
112 | sb.append("
");
113 | List embeddedItems = scenarioResult.getEmbeddedItems();
114 | if (!embeddedItems.isEmpty()) {
115 | sb.append("Embedded Items
\n");
116 | sb.append("");
117 | for (EmbeddedItem embeddedItem : embeddedItems) {
118 | addEmbeddedItem(sb, embeddedItem);
119 | }
120 | sb.append("
");
121 | }
122 | return sb.toString();
123 | }
124 |
125 | private StringBuilder addEmbeddedItem(StringBuilder stringBuilder, EmbeddedItem embeddedItem) {
126 | stringBuilder.append("");
127 | stringBuilder.append("");
128 | stringBuilder.append(embeddedItem.getFilename());
129 | stringBuilder.append(" of type ");
130 | stringBuilder.append(embeddedItem.getMimetype());
131 | stringBuilder.append("\n");
132 | return stringBuilder;
133 | }
134 |
135 | private StringBuilder addTagStatement(StringBuilder sb, TagStatement tagStatement) {
136 | for (Comment comment : tagStatement.getComments()) {
137 | addComment(sb, comment);
138 | }
139 | for (Tag tag : tagStatement.getTags()) {
140 | createLine(sb, tag.getLine(), RESULT_TYPE.NO_RESULT);
141 | sb.append(tag.getName());
142 | }
143 | createLine(sb, tagStatement.getLine(), RESULT_TYPE.NO_RESULT);
144 | appendKeyword(sb, tagStatement.getKeyword()).append(' ').append(tagStatement.getName());
145 | String descr = tagStatement.getDescription();
146 | indent++;
147 | if (descr != null && !descr.isEmpty()) {
148 | // may have been run on windows?
149 | descr = descr.replace("\r\n", "\n");
150 | String[] lines = descr.split("\\n");
151 | for (int i=0; i < lines.length; i++){
152 | endLine(sb);
153 | createLine(sb, tagStatement.getLine() + i+1, RESULT_TYPE.NO_RESULT);
154 | sb.append("");
155 | sb.append(lines[i]);
156 | sb.append("");
157 | }
158 | }
159 | endLine(sb);
160 | return sb;
161 | }
162 |
163 | public StringBuilder addDescribedStatement(StringBuilder sb, DescribedStatement ds) {
164 | for (Comment comment : ds.getComments()) {
165 | addComment(sb, comment);
166 | }
167 | createLine(sb, ds.getLine(), RESULT_TYPE.NO_RESULT);
168 | appendKeyword(sb, ds.getKeyword());
169 | sb.append(' ');
170 | sb.append(ds.getName());
171 | endLine(sb);
172 | return sb;
173 | }
174 |
175 |
176 | private StringBuilder createLine(StringBuilder sb, Integer line, RESULT_TYPE type) {
177 | String lineStr = String.format("%03d", line);
178 | return createLine(sb, lineStr, type);
179 | }
180 |
181 |
182 | private StringBuilder createLine(StringBuilder sb, String str, RESULT_TYPE type) {
183 | sb.append("\n| ");
184 | sb.append(str);
185 | sb.append(" | ");
186 | sb.append("");
187 | sb.append(" ");
190 | return sb;
191 | }
192 |
193 |
194 | private StringBuilder endLine(StringBuilder sb) {
195 | return sb.append(" | ");
196 | }
197 |
198 |
199 | public StringBuilder addComment(StringBuilder sb, Comment comment) {
200 | createLine(sb, comment.getLine(), RESULT_TYPE.NO_RESULT);
201 | sb.append("");
202 | sb.append(comment.getValue());
203 | sb.append("");
204 |
205 | endLine(sb);
206 | return sb;
207 | }
208 |
209 |
210 | public StringBuilder appendKeyword(StringBuilder sb, String keyword) {
211 | sb.append("").append(keyword).append("");
212 | return sb;
213 | }
214 |
215 |
216 | public StringBuilder addBeforeAfterResult(StringBuilder sb,
217 | String beforeOrAfter,
218 | BeforeAfterResult beforeAfter) {
219 | Match m = beforeAfter.getMatch();
220 | Result r = beforeAfter.getResult();
221 | createLine(sb, beforeOrAfter, RESULT_TYPE.typeFromResult(r));
222 | sb.append(m.getLocation()).append(' ');
223 | addFailure(sb, r);
224 | // XXX add argument formatting
225 | // List args = m.getArguments();
226 | endLine(sb);
227 | return sb;
228 | }
229 |
230 |
231 | public StringBuilder addFailure(StringBuilder sb, Result result) {
232 | if (Result.FAILED.equals(result.getStatus())) {
233 | createLine(sb, "Failure", RESULT_TYPE.FAILED);
234 | String[] stack = result.getErrorMessage().split("\n");
235 |
236 | sb.append(stack[0]).append("
");
237 | for (int i = 1; i < stack.length; i++) {
238 | sb.append(stack[i].replaceAll("\t", " "));
239 | sb.append("
");
240 | }
241 | // Error is always null (only non null when invoked direct as part of the test).
242 | /*
243 | * Throwable t = result.getError(); if (t != null) { StackTraceElement stack[] = t.getStackTrace();
244 | * for (StackTraceElement ste : stack) { sb.append(ste.toString()).append("
"); } }
245 | */
246 | }
247 | else if ("undefined".equals(result.getStatus())) {
248 | createLine(sb, "Undefined", RESULT_TYPE.UNDEFINED);
249 | sb.append("Step is undefined");
250 | // We have no error message.
251 | }
252 | endLine(sb);
253 | return sb;
254 | }
255 |
256 |
257 | public StringBuilder addBackgroundResult(StringBuilder sb, BackgroundResult backgroundResult) {
258 | if (backgroundResult != null) {
259 | Background background = backgroundResult.getBackground();
260 | addDescribedStatement(sb, background);
261 | for (StepResult step : backgroundResult.getStepResults()) {
262 | addStepResult(sb, step);
263 | }
264 | }
265 | return sb;
266 | }
267 |
268 |
269 | public StringBuilder addStepResult(StringBuilder sb, StepResult stepResult) {
270 | Step step = stepResult.getStep();
271 | {
272 | List comments = step.getComments();
273 | if (comments != null) {
274 | for (Comment c : comments) {
275 | addComment(sb, c);
276 | }
277 | }
278 | }
279 | createLine(sb, step.getLine(), RESULT_TYPE.typeFromResult(stepResult.getResult()));
280 | appendKeyword(sb, step.getKeyword());
281 | sb.append(' ');
282 | sb.append(step.getName());
283 | if (step.getRows() != null) {
284 | indent++;
285 |
286 | boolean firstRow = true;
287 | for (DataTableRow dtr : step.getRows()) {
288 | List comments = dtr.getComments();
289 | if (comments != null) {
290 | for (Comment comment : comments) {
291 | addComment(sb, comment);
292 | }
293 | }
294 | createLine(sb, dtr.getLine(), RESULT_TYPE.NO_RESULT);
295 | int colwidth = 100 / (dtr.getCells().size());
296 | // these span multiple lines and divs don't wrap if the argument is too long
297 | // so use a table per row with the same sizes for each column. ugly but works...
298 | // having a large colspan would be nice but then we need to compute all the possibilities up
299 | // front.
300 | sb.append("");
301 | sb.append("");
302 | for (String cell : dtr.getCells()) {
303 | if (firstRow) {
304 | sb.append("| ");
305 | sb.append(cell);
306 | sb.append(" | ");
307 | continue;
308 | }
309 | sb.append("");
310 | sb.append(cell);
311 | sb.append(" | ");
312 | }
313 | sb.append("
");
314 |
315 | firstRow = false;
316 | endLine(sb);
317 | }
318 | indent--;
319 | }
320 | endLine(sb);
321 | // TODO add support for table rows...
322 | addFailure(sb, stepResult.getResult());
323 | return sb;
324 | }
325 |
326 | }
327 |
--------------------------------------------------------------------------------
/src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultArchiver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt,
5 | * Tom Huybrechts, Yahoo!, Inc., Richard Hierlmeier
6 | *
7 | * Permission is hereby granted, free of charge, to any person obtaining a copy
8 | * of this software and associated documentation files (the "Software"), to deal
9 | * in the Software without restriction, including without limitation the rights
10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | * copies of the Software, and to permit persons to whom the Software is
12 | * furnished to do so, subject to the following conditions:
13 | *
14 | * The above copyright notice and this permission notice shall be included in
15 | * all copies or substantial portions of the Software.
16 | *
17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | * THE SOFTWARE.
24 | */
25 | package org.jenkinsci.plugins.cucumber.jsontestsupport;
26 |
27 | import com.google.common.base.Strings;
28 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
29 | import hudson.AbortException;
30 | import hudson.Extension;
31 | import hudson.FilePath;
32 | import hudson.Launcher;
33 | import hudson.matrix.MatrixAggregatable;
34 | import hudson.matrix.MatrixAggregator;
35 | import hudson.matrix.MatrixBuild;
36 | import hudson.model.*;
37 | import hudson.remoting.Callable;
38 | import hudson.tasks.BuildStepDescriptor;
39 | import hudson.tasks.BuildStepMonitor;
40 | import hudson.tasks.Publisher;
41 | import hudson.tasks.Recorder;
42 | import hudson.tasks.test.TestResultAggregator;
43 | import hudson.tasks.test.TestResultProjectAction;
44 | import hudson.util.FormValidation;
45 | import jenkins.security.MasterToSlaveCallable;
46 | import jenkins.tasks.SimpleBuildStep;
47 | import net.sf.json.JSONObject;
48 | import org.apache.tools.ant.types.FileSet;
49 | import org.jenkinsci.Symbol;
50 | import org.kohsuke.stapler.*;
51 |
52 | import java.io.File;
53 | import java.io.IOException;
54 | import java.lang.reflect.Constructor;
55 | import java.util.Collection;
56 | import java.util.Collections;
57 | import java.util.logging.Level;
58 | import java.util.logging.Logger;
59 | import java.util.regex.Matcher;
60 | import java.util.regex.Pattern;
61 |
62 | /**
63 | * Generates HTML report from Cucumber JSON files.
64 | *
65 | * @author James Nord
66 | * @author Kohsuke Kawaguchi (original JUnit code)
67 | */
68 | public class CucumberTestResultArchiver extends Recorder implements MatrixAggregatable, SimpleBuildStep {
69 | private static final Logger LOGGER = Logger.getLogger(CucumberTestResultArchiver.class.getName());
70 |
71 | /**
72 | * {@link FileSet} "includes" string, like "foo/bar/*.xml"
73 | */
74 | private final String testResults;
75 |
76 | private boolean ignoreBadSteps;
77 |
78 | private boolean ignoreDiffTracking;
79 |
80 | @DataBoundConstructor
81 | public CucumberTestResultArchiver(String testResults) {
82 | this.testResults = testResults;
83 | }
84 |
85 | public CucumberTestResultArchiver(String testResults, boolean ignoreBadSteps, boolean ignoreDiffTracking){
86 | this(testResults);
87 | setIgnoreBadSteps(ignoreBadSteps);
88 | setIgnoreDiffTracking(ignoreDiffTracking);
89 | }
90 |
91 | @DataBoundSetter
92 | public void setIgnoreBadSteps(boolean ignoreBadSteps){
93 | this.ignoreBadSteps = ignoreBadSteps;
94 | }
95 |
96 | public boolean getIgnoreBadSteps(){
97 | return ignoreBadSteps;
98 | }
99 |
100 | @DataBoundSetter
101 | public void setIgnoreDiffTracking(boolean ignoreDiffTracking){
102 | this.ignoreDiffTracking = ignoreDiffTracking;
103 | }
104 |
105 | public boolean getIgnoreDiffTracking(){
106 | return ignoreDiffTracking;
107 | }
108 |
109 | @Override
110 | @SuppressFBWarnings(value={"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification="whatever")
111 | public boolean
112 | perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException,
113 | IOException {
114 | return publishReport(build, build.getWorkspace(), launcher, listener);
115 | }
116 |
117 |
118 | @Override
119 | public void perform(Run, ?> run, FilePath filePath, Launcher launcher, TaskListener taskListener) throws InterruptedException, IOException {
120 | publishReport(run, filePath, launcher, taskListener);
121 | }
122 |
123 | @SuppressFBWarnings(value={"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"}, justification="move to java.nio for file stuff")
124 | public boolean
125 | publishReport(Run, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException,
126 | IOException {
127 | // listener.getLogger().println(Messages.JUnitResultArchiver_Recording());
128 |
129 | CucumberTestResultAction action;
130 |
131 | final String _testResults = build.getEnvironment(listener).expand(this.testResults);
132 |
133 | CucumberJSONParser parser = new CucumberJSONParser(ignoreBadSteps);
134 |
135 | CucumberTestResult result = parser.parseResult(_testResults, build, workspace, launcher, listener);
136 | copyEmbeddedItems(build, launcher, result);
137 |
138 |
139 | try {
140 | action = reportResultForAction(CucumberTestResultAction.class, build, listener, result);
141 | } catch (Exception e) {
142 | LOGGER.log(Level.FINE, "Unable to handle results", e);
143 | return false;
144 | }
145 |
146 | if (result.getPassCount() == 0 && result.getFailCount() == 0 && result.getSkipCount() == 0) {
147 | throw new AbortException("No cucumber scenarios appear to have been run.");
148 | }
149 |
150 | if (action.getResult().getTotalCount() == action.getResult().getFailCount()) {
151 | build.setResult(Result.FAILURE);
152 | } else if (action.getResult().getFailCount() > 0) {
153 | build.setResult(Result.UNSTABLE);
154 | }
155 |
156 | parseRerunResults(build, workspace, launcher, listener, _testResults, parser);
157 | return true;
158 | }
159 |
160 | private void copyEmbeddedItems(Run, ?> build, Launcher launcher, CucumberTestResult result) throws IOException, InterruptedException {
161 | // TODO - look at all of the Scenarios and see if there are any embedded items contained with in them
162 | String remoteTempDir = launcher.getChannel().call(new TmpDirCallable());
163 |
164 | // if so we need to copy them to the master.
165 | for (FeatureResult f : result.getFeatures()) {
166 | for (ScenarioResult s : f.getScenarioResults()) {
167 | for (EmbeddedItem item : s.getEmbeddedItems()) {
168 | // this is the wrong place to do the copying...
169 | // XXX Need to do something with MasterToSlaveCallable to makesure we are safe from evil
170 | // injection
171 | FilePath srcFilePath = new FilePath(launcher.getChannel(), remoteTempDir + '/' + item.getFilename());
172 | // XXX when we support the workflow we will need to make sure that these files do not clash....
173 | File destRoot = new File(build.getRootDir(), "/cucumber/embed/" + f.getSafeName() + '/' + s
174 | .getSafeName() + '/');
175 | destRoot.mkdirs();
176 | File destFile = new File(destRoot, item.getFilename());
177 | if (!destFile.getAbsolutePath().startsWith(destRoot.getAbsolutePath())) {
178 | // someone is trying to trick us into writing abitrary files...
179 | throw new IOException("Exploit attempt detected - Build attempted to write to " +
180 | destFile.getAbsolutePath());
181 | }
182 | FilePath destFilePath = new FilePath(destFile);
183 | srcFilePath.copyTo(destFilePath);
184 | srcFilePath.delete();
185 | }
186 | }
187 | }
188 | }
189 |
190 | private void parseRerunResults(Run, ?> build, FilePath workspace, Launcher launcher,
191 | TaskListener listener, String testResultsPath,
192 | CucumberJSONParser parser) throws IOException, InterruptedException {
193 |
194 | parseRerunWithNumberIfExists(1, build, workspace, launcher, listener, testResultsPath, parser);
195 | parseRerunWithNumberIfExists(2, build, workspace, launcher, listener, testResultsPath, parser);
196 | }
197 |
198 | private void parseRerunWithNumberIfExists(int number, Run, ?> build, FilePath workspace,
199 | Launcher launcher, TaskListener listener,
200 | String testResultsPath,
201 | CucumberJSONParser parser) throws IOException, InterruptedException {
202 | String rerunFilePath = filterRerunFilePath(workspace, testResultsPath, number);
203 | if (!Strings.isNullOrEmpty(rerunFilePath)) {
204 | CucumberTestResult rerunResult = parser.parseResult(rerunFilePath, build, workspace, launcher, listener);
205 | rerunResult.setNameAppendix("Rerun " + number);
206 | copyEmbeddedItems(build, launcher, rerunResult);
207 | try {
208 | Class rerunActionClass = Class.forName(getRerunActionClassName(number));
209 | reportResultForAction(rerunActionClass, build, listener, rerunResult);
210 | } catch (Exception e) {
211 | LOGGER.log(Level.FINE, "Unable to process rerun with number " + number, e);
212 | }
213 | }
214 | }
215 |
216 | private String getRerunActionClassName(int number) {
217 | return getClass().getPackage().getName() +
218 | ".rerun.CucumberRerun" + number + "TestResultAction";
219 | }
220 |
221 | private CucumberTestResultAction reportResultForAction(Class actionClass, Run, ?> build,
222 | TaskListener listener,
223 | CucumberTestResult result) throws Exception {
224 | CucumberTestResultAction action = (CucumberTestResultAction) build.getAction(actionClass);
225 | if (action == null) {
226 | Constructor actionClassConstructor = actionClass.getConstructor(Run.class, CucumberTestResult.class, TaskListener.class);
227 | action = (CucumberTestResultAction) actionClassConstructor.newInstance(build, result, listener);
228 | if (!ignoreDiffTracking) {
229 | CHECKPOINT.block();
230 | CHECKPOINT.report();
231 | }
232 | } else {
233 | if (!ignoreDiffTracking) {
234 | CHECKPOINT.block();
235 | }
236 | action.mergeResult(result, listener);
237 | build.save();
238 | if (!ignoreDiffTracking) {
239 | CHECKPOINT.report();
240 | }
241 | }
242 | return action;
243 | }
244 |
245 | private String filterRerunFilePath(FilePath workspace, String testResultsPath, int number) throws IOException, InterruptedException {
246 | FilePath[] paths = workspace.list(testResultsPath);
247 | for (FilePath filePath : paths) {
248 | String remote = filePath.getRemote();
249 | Pattern p = Pattern.compile("rerun" + number + ".cucumber.json");
250 | Matcher m = p.matcher(remote);
251 | if (m.find()) {
252 | return "**/" + remote.substring(m.start());
253 | }
254 | }
255 | return "";
256 | }
257 |
258 |
259 | /**
260 | * This class does explicit checkpointing.
261 | */
262 | public BuildStepMonitor getRequiredMonitorService() {
263 | return BuildStepMonitor.NONE;
264 | }
265 |
266 |
267 | public String getTestResults() {
268 | return testResults;
269 | }
270 |
271 |
272 | @Override
273 | public Collection getProjectActions(AbstractProject, ?> project) {
274 | return Collections. singleton(new TestResultProjectAction((Job)project));
275 | }
276 |
277 |
278 | public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
279 | return new TestResultAggregator(build, launcher, listener);
280 | }
281 |
282 | /**
283 | * Test result tracks the diff from the previous run, hence the checkpoint.
284 | */
285 | private static final CheckPoint CHECKPOINT = new CheckPoint("Cucumber result archiving");
286 |
287 | private static final long serialVersionUID = 1L;
288 |
289 |
290 | public DescriptorImpl getDescriptor() {
291 | return (DescriptorImpl) super.getDescriptor();
292 | }
293 |
294 |
295 | /**
296 | * {@link Callable} that gets the temporary directory from the node.
297 | */
298 | private final static class TmpDirCallable extends MasterToSlaveCallable {
299 |
300 | private static final long serialVersionUID = 1L;
301 |
302 | @Override
303 | public String call() {
304 | return System.getProperty("java.io.tmpdir");
305 | }
306 | }
307 |
308 |
309 |
310 | @Extension
311 | @Symbol("cucumber")
312 | public static class DescriptorImpl extends BuildStepDescriptor {
313 |
314 | public String getDisplayName() {
315 | return "Publish Cucumber test result report - custom";
316 | }
317 |
318 | @Override
319 | public Publisher
320 | newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException {
321 | String testResults = formData.getString("testResults");
322 | boolean ignoreBadSteps = formData.getBoolean("ignoreBadSteps");
323 | boolean ignoreDiffTracking = formData.getBoolean("ignoreDiffTracking");
324 | LOGGER.fine("ignoreBadSteps = "+ ignoreBadSteps);
325 | LOGGER.fine("ignoreDiffTracking ="+ ignoreDiffTracking);
326 | return new CucumberTestResultArchiver(testResults, ignoreBadSteps, ignoreDiffTracking);
327 | }
328 |
329 |
330 | /**
331 | * Performs on-the-fly validation on the file mask wildcard.
332 | */
333 | public FormValidation doCheckTestResults(@AncestorInPath AbstractProject project,
334 | @QueryParameter String value) throws IOException {
335 | if (project != null) {
336 | return FilePath.validateFileMask(project.getSomeWorkspace(), value);
337 | }
338 | return FormValidation.ok();
339 | }
340 |
341 |
342 | public boolean isApplicable(Class extends AbstractProject> jobType) {
343 | return true;
344 | }
345 | }
346 |
347 | }
348 |
--------------------------------------------------------------------------------