├── src ├── main │ ├── resources │ │ └── org │ │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── cucumber │ │ │ └── jsontestsupport │ │ │ ├── CucumberTestResultArchiver │ │ │ ├── help-testResults.html │ │ │ ├── help-ignoreBadSteps.html │ │ │ ├── help.html │ │ │ └── config.jelly │ │ │ ├── ScenarioResult │ │ │ ├── index.properties │ │ │ ├── scenarioDetails.jelly │ │ │ ├── summary.jelly │ │ │ ├── _list.jelly │ │ │ └── index.jelly │ │ │ ├── __CucumberTestResultAction │ │ │ ├── index.jelly │ │ │ ├── body.jelly │ │ │ ├── floatingBox.jelly │ │ │ ├── list.jelly │ │ │ └── summary.jelly │ │ │ ├── __FeatureResult │ │ │ ├── body.jelly │ │ │ └── list.jelly │ │ │ ├── TagResult │ │ │ ├── __list.jelly │ │ │ └── body.jelly │ │ │ └── CucumberTestResult │ │ │ ├── __list.jelly │ │ │ └── body.jelly │ └── java │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── cucumber │ │ └── jsontestsupport │ │ ├── rerun │ │ ├── CucumberRerun2TestResultAction.java │ │ ├── CucumberRerun1TestResultAction.java │ │ └── CucumberRerunTestResultAction.java │ │ ├── CucumberJSONSupportPlugin.java │ │ ├── CucumberPluginException.java │ │ ├── CucumberModelException.java │ │ ├── EmbeddedItem.java │ │ ├── StepResult.java │ │ ├── BeforeAfterResult.java │ │ ├── CucumberUtils.java │ │ ├── CucumberJSONParser.java │ │ ├── BackgroundResult.java │ │ ├── TagResult.java │ │ ├── FeatureResult.java │ │ ├── DefaultTestResultParserImpl.java │ │ ├── CucumberTestResultAction.java │ │ ├── CucumberTestResult.java │ │ ├── GherkinCallback.java │ │ ├── ScenarioToHTML.java │ │ └── CucumberTestResultArchiver.java ├── test │ ├── resources │ │ └── org │ │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── cucumber │ │ │ └── jsontestsupport │ │ │ ├── CucumberJSONSupportPluginIT │ │ │ ├── featurePass.json │ │ │ ├── featureFail.json │ │ │ └── passWithEmbeddedItem.json │ │ │ └── ScenarioResultTest │ │ │ ├── cucumber-jvm_examples_java-calculator__cucumber-report.json │ │ │ ├── pending.json │ │ │ └── cucumber-embedded-item.json │ └── java │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── cucumber │ │ └── jsontestsupport │ │ ├── CucumberJSONParserTest.java │ │ └── CucumberJSONSupportPluginIT.java └── build │ └── findbugs │ └── findbugs-exclude.xml ├── .gitignore ├── README.md ├── LICENCE.txt └── pom.xml /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultArchiver/help-testResults.html: -------------------------------------------------------------------------------- 1 |
2 | ANT Glob set. 3 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #eclipse files 2 | .project 3 | .settings 4 | .classpath 5 | bin 6 | 7 | #IDEA files 8 | .idea 9 | *.iml 10 | 11 | # Build output 12 | target 13 | /work 14 | 15 | .clover -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultArchiver/help-ignoreBadSteps.html: -------------------------------------------------------------------------------- 1 |
2 | Ignore not closed steps in broken JSON for GherkinParser. 3 | See: #JENKINS-21835 4 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cucumber-testresult-plugin 2 | ========================== 3 | 4 | This plugin allows you to show the results of [Cucumber](https://cucumber.io/) tests within [Jenkins](https://jenkins.io/) 5 | using the standard test reporting mechanism which provides graphs over time and drill down to individual results. 6 | 7 | For more information see [the wiki page](https://wiki.jenkins-ci.org/display/JENKINS/Cucumber+Test+Result+Plugin) 8 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/rerun/CucumberRerun2TestResultAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.cucumber.jsontestsupport.rerun; 2 | 3 | import hudson.model.Run; 4 | import hudson.model.TaskListener; 5 | import org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResult; 6 | 7 | /** 8 | * Used to generate rerun results. Accessed with reflection - do not move or rename without 9 | * modifying {@link org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResultArchiver#getRerunActionClassName(int)} 10 | */ 11 | public class CucumberRerun2TestResultAction extends CucumberRerunTestResultAction { 12 | 13 | public CucumberRerun2TestResultAction(Run owner, CucumberTestResult result, TaskListener listener) { 14 | super(owner, result, listener); 15 | } 16 | 17 | @Override 18 | protected int getNumber() { 19 | return 2; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/rerun/CucumberRerun1TestResultAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.cucumber.jsontestsupport.rerun; 2 | 3 | import hudson.model.Run; 4 | import hudson.model.TaskListener; 5 | import org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResult; 6 | 7 | /** 8 | * Used to generate rerun results. Accessed with reflection - do not move or rename without 9 | * modifying {@link org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResultArchiver#getRerunActionClassName(int)} 10 | */ 11 | public class CucumberRerun1TestResultAction extends CucumberRerunTestResultAction { 12 | 13 | 14 | public CucumberRerun1TestResultAction(Run owner, CucumberTestResult result, TaskListener listener) { 15 | super(owner, result, listener); 16 | } 17 | 18 | @Override 19 | protected int getNumber() { 20 | return 1; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultArchiver/help.html: -------------------------------------------------------------------------------- 1 |
2 | Cucumber test results report in JSON format. 3 | When this option is configured, Jenkins can provide useful information about test results, 4 | such as historical test result trends, a web UI for viewing test reports, tracking failures, 5 | and so on. 6 | 7 |

8 | To use this feature, first set up your build to run tests, then 9 | specify the path to Cucumber JSON files in the 10 | Ant glob syntax, 11 | such as **/build/test-reports/*.json. Be sure not to include any non-report 12 | files into this pattern. You can specify only one file pattern. 13 | 14 |

15 | Once there are a few builds running with test results, you should start seeing 16 | something like this. 17 |

-------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONSupportPluginIT/featurePass.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "foo-feature-1", 4 | "keyword": "Feature", 5 | "id": "foo-feature", 6 | "name": "foo-feature", 7 | "line": 1, 8 | "description": "", 9 | "tags": [ 10 | { 11 | "name": "@a", 12 | "line": 1 13 | } 14 | ], 15 | "elements": [ 16 | { 17 | "keyword": "Scenario", 18 | "id": "foo-scenario", 19 | "name": "Passing", 20 | "line": 5, 21 | "description": "", 22 | "tags": [ 23 | { 24 | "name": "@b", 25 | "line": 4 26 | } 27 | ], 28 | "type": "scenario", 29 | "steps": [ 30 | { 31 | "keyword": "Given ", 32 | "name": "this step passes", 33 | "line": 6, 34 | "match": { 35 | "location": "features/step_definitions/steps.rb:1" 36 | }, 37 | "result": { 38 | "status": "passed", 39 | "duration": 1 40 | } 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/rerun/CucumberRerunTestResultAction.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.cucumber.jsontestsupport.rerun; 2 | 3 | import hudson.XmlFile; 4 | import hudson.model.Run; 5 | import hudson.model.TaskListener; 6 | import org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResult; 7 | import org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResultAction; 8 | 9 | import java.io.File; 10 | 11 | 12 | public abstract class CucumberRerunTestResultAction extends CucumberTestResultAction { 13 | 14 | protected abstract int getNumber(); 15 | 16 | public CucumberRerunTestResultAction(Run owner, CucumberTestResult result, TaskListener listener) { 17 | super(owner, result, listener); 18 | } 19 | 20 | @Override 21 | protected XmlFile getDataFile() { 22 | return new XmlFile(XSTREAM, new File(run.getRootDir(), "cucumberRerunResult" + getNumber() + ".xml")); 23 | } 24 | 25 | @Override 26 | public String getDisplayName() { 27 | return "Cucumber Rerun " + getNumber() + " Result"; 28 | } 29 | 30 | @Override 31 | public String getUrlName() { 32 | return "cucumberRerun" + getNumber(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016, James Nord 4 | 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 | -------------------------------------------------------------------------------- /src/build/findbugs/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResult/index.properties: -------------------------------------------------------------------------------- 1 | ## The MIT License 2 | # 3 | # Copyright (c) 2013, Cisco Systems, Inc., a California corporation 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | #/ 23 | took=Took {0}. -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONSupportPluginIT/featureFail.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uri": "foo-feature-2", 4 | "keyword": "Feature", 5 | "id": "foo-feature", 6 | "name": "foo-feature", 7 | "line": 1, 8 | "description": "", 9 | "tags": [ 10 | { 11 | "name": "@a", 12 | "line": 1 13 | } 14 | ], 15 | "elements": [ 16 | { 17 | "keyword": "Scenario", 18 | "id": "foo-scenario", 19 | "name": "Failing", 20 | "line": 9, 21 | "description": "", 22 | "tags": [ 23 | { 24 | "name": "@c", 25 | "line": 8 26 | } 27 | ], 28 | "type": "scenario", 29 | "steps": [ 30 | { 31 | "keyword": "Given ", 32 | "name": "this step fails", 33 | "line": 10, 34 | "match": { 35 | "location": "features/step_definitions/steps.rb:4" 36 | }, 37 | "result": { 38 | "status": "failed", 39 | "error_message": " (RuntimeError)\n./features/step_definitions/steps.rb:4:in /^this step fails$/'\nfeatures/one_passing_one_failing.feature:10:in Given this step fails'", 40 | "duration": 1 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | ] -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResult/scenarioDetails.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONSupportPlugin.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.Plugin; 27 | 28 | 29 | /** 30 | * Plugin that understands Cucumbers JSON notation and will report it as standard test metrics. 31 | */ 32 | public class CucumberJSONSupportPlugin extends Plugin { 33 | 34 | public CucumberJSONSupportPlugin() { 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberPluginException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2014 James Nord 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 | /** 27 | * A generic exception indicating that something bad has happened. 28 | * 29 | * @author James Nord 30 | */ 31 | public class CucumberPluginException extends RuntimeException { 32 | 33 | private static final long serialVersionUID = 1L; 34 | 35 | public CucumberPluginException(String message) { 36 | super(message); 37 | } 38 | 39 | public CucumberPluginException(String message, Exception cause) { 40 | super(message, cause); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberModelException.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 | /** 27 | * An exception caused by failing to parse the Cucumber JSON file. Most likely caused by a bug in the code 28 | * with some strange/unseen JSON output. 29 | * 30 | * @author James Nord 31 | */ 32 | public class CucumberModelException extends RuntimeException { 33 | 34 | private static final long serialVersionUID = 1L; 35 | 36 | public CucumberModelException(String message) { 37 | super(message); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__CucumberTestResultAction/index.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | [Test result trend chart] 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResult/summary.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 |

${%Error Details}

33 |
34 |
35 | 36 | 37 |

${%Stack Trace}

38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResultArchiver/config.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/EmbeddedItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2014, 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 java.io.Serializable; 27 | 28 | /** 29 | * An EmbeddedItem represents an item that has been embedded in a test report. 30 | * The actual copying of the item from the JSON (parsed on the slave) to the master happens in 31 | * {@linnk CucumberJSONParser.parse(String, AbstractBuild, Launcher, TaskListener)} 32 | * @author James Nord 33 | * 34 | */ 35 | public class EmbeddedItem implements Serializable { 36 | 37 | private static final long serialVersionUID = 1L; 38 | 39 | /** The mimetype of the object */ 40 | private String mimetype; 41 | 42 | /** The name if the embedded file on disk */ 43 | private String filename; 44 | 45 | public EmbeddedItem(String mimetype, String filename) { 46 | this.mimetype = mimetype; 47 | this.filename = filename; 48 | } 49 | 50 | protected String getFilename() { 51 | return filename; 52 | } 53 | 54 | protected String getMimetype() { 55 | return mimetype; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__FeatureResult/body.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 |

${%All Tests}

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 51 | 52 | 53 | 54 |
${%Test name}${%Duration}${%Status}
39 | 40 | 41 | 42 | 43 | ${p.durationString} 46 | 47 | 48 | ${pst.message} 49 | 50 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__CucumberTestResultAction/body.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 |

${%All Tests}

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 51 | 52 | 53 | 54 |
${%Test name}${%Duration}${%Status}
39 | 40 | 41 | 42 | 43 | ${p.durationString} 46 | 47 | 48 | ${pst.message} 49 | 50 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__CucumberTestResultAction/floatingBox.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 |
37 | ${%Test Result Trend} 38 |
39 |
40 | [Test result trend chart] 41 |
42 | 52 |
53 |
54 |
-------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResult/_list.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 |
${%Build}${%Test Description}${%Test Duration}${%Test Result}
44 | ${b.fullDisplayName} 45 | 46 | 47 | 48 | ${test.durationString} 52 | 53 | 54 | ${pst.message} 55 | 56 |
62 |
63 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__FeatureResult/list.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
${%Build}${%Description}${%Duration}${%Fail}${%Skip}${%Total}
43 | ${b.fullDisplayName} 44 | 45 | 46 | 47 | ${p.durationString}${p.failCount}${p.skipCount}${p.totalCount}
58 |
59 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__CucumberTestResultAction/list.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
${%Build}${%Description}${%Duration}${%Fail}${%Skip}${%Total}
43 | ${b.fullDisplayName} 44 | 45 | 46 | 47 | ${p.durationString}${p.failCount}${p.skipCount}${p.totalCount}
58 |
59 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/TagResult/__list.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
${%Build} - JAMES WAS HERE${%Description}${%Duration}${%Fail}${%Skip}${%Total}
43 | ${b.fullDisplayName} 44 | 45 | 46 | 47 | ${p.durationString}${p.failCount}${p.skipCount}${p.totalCount}
58 |
59 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberTestResult/__list.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
${%Build} - JAMES WAS HERE${%Description}${%Duration}${%Fail}${%Skip}${%Total}
43 | ${b.fullDisplayName} 44 | 45 | 46 | 47 | ${p.durationString}${p.failCount}${p.skipCount}${p.totalCount}
58 |
59 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResultTest/cucumber-jvm_examples_java-calculator__cucumber-report.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dates-with-different-date-formats", 4 | "description": "This feature shows you can have different date formats, as long as you annotate the\ncorresponding step definition method accordingly.", 5 | "name": "Dates with different date formats", 6 | "keyword": "Feature", 7 | "line": 1, 8 | "elements": [ 9 | { 10 | "after": [ 11 | { 12 | "result": { 13 | "duration": 10658, 14 | "status": "passed" 15 | }, 16 | "match": { 17 | "location": "RpnCalculatorStepdefs.after(Scenario)" 18 | } 19 | } 20 | ], 21 | "id": "dates-with-different-date-formats;determine-past-date", 22 | "before": [ 23 | { 24 | "result": { 25 | "duration": 536444, 26 | "status": "passed" 27 | }, 28 | "match": { 29 | "location": "RpnCalculatorStepdefs.before()" 30 | } 31 | } 32 | ], 33 | "description": "", 34 | "name": "Determine past date", 35 | "keyword": "Scenario", 36 | "line": 5, 37 | "steps": [ 38 | { 39 | "result": { 40 | "duration": 328814, 41 | "status": "passed" 42 | }, 43 | "name": "today is 2011-01-20", 44 | "keyword": "Given ", 45 | "line": 6, 46 | "match": { 47 | "arguments": [ 48 | { 49 | "val": "2011-01-20", 50 | "offset": 9 51 | } 52 | ], 53 | "location": "DateStepdefs.today_is(Date)" 54 | } 55 | }, 56 | { 57 | "result": { 58 | "duration": 177236, 59 | "status": "passed" 60 | }, 61 | "name": "I ask if Jan 19, 2011 is in the past", 62 | "keyword": "When ", 63 | "line": 7, 64 | "match": { 65 | "arguments": [ 66 | { 67 | "val": "Jan 19, 2011", 68 | "offset": 9 69 | } 70 | ], 71 | "location": "DateStepdefs.I_ask_if_date_is_in_the_past(Date)" 72 | } 73 | }, 74 | { 75 | "result": { 76 | "duration": 36315, 77 | "status": "passed" 78 | }, 79 | "name": "the result should be yes", 80 | "keyword": "Then ", 81 | "line": 8, 82 | "match": { 83 | "arguments": [ 84 | { 85 | "val": "yes", 86 | "offset": 21 87 | } 88 | ], 89 | "location": "DateStepdefs.the_result_should_be(String)" 90 | } 91 | } 92 | ], 93 | "type": "scenario" 94 | } 95 | ], 96 | "uri": "cucumber/examples/java/calculator/date_calculator.feature" 97 | } 98 | ] -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResultTest/pending.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "connectivity", 4 | "description": "As a user I want to be able to connect to the Oracle instance via JDBC", 5 | "name": "Connectivity", 6 | "keyword": "Feature", 7 | "line": 1, 8 | "elements": [ 9 | { 10 | "id": "connectivity;i-can-connect-to-the-database-instance", 11 | "description": "", 12 | "name": "I can connect to the database instance", 13 | "keyword": "Scenario", 14 | "line": 4, 15 | "steps": [ 16 | { 17 | "result": { 18 | "duration": 98840000, 19 | "status": "passed" 20 | }, 21 | "name": "a service instance exists", 22 | "keyword": "Given ", 23 | "line": 5, 24 | "match": { 25 | "location": "JDBCStepDefs.a_service_instance_exists()" 26 | } 27 | }, 28 | { 29 | "result": { 30 | "duration": 53000, 31 | "status": "passed" 32 | }, 33 | "name": "I have a JDBC connection URL", 34 | "keyword": "And ", 35 | "line": 6, 36 | "match": { 37 | "location": "JDBCStepDefs.i_have_a_JDBC_connection_URL()" 38 | } 39 | }, 40 | { 41 | "result": { 42 | "duration": 1920000, 43 | "status": "pending", 44 | "error_message": "cucumber.api.PendingException: TODO: implement me\n\tat com.cisco.vci.service.oracle.aat.glue.JDBCStepDefs.i_have_a_username(JDBCStepDefs.java:55)\n\tat ✽.And I have a username(features/Conectivity.feature:7)\n" 45 | }, 46 | "name": "I have a username", 47 | "keyword": "And ", 48 | "line": 7, 49 | "match": { 50 | "location": "JDBCStepDefs.i_have_a_username()" 51 | } 52 | }, 53 | { 54 | "result": { 55 | "status": "skipped" 56 | }, 57 | "name": "I have a password", 58 | "keyword": "And ", 59 | "line": 8, 60 | "match": { 61 | "location": "JDBCStepDefs.i_have_a_password()" 62 | } 63 | }, 64 | { 65 | "result": { 66 | "status": "skipped" 67 | }, 68 | "name": "I have configure the JDBC driver", 69 | "keyword": "When ", 70 | "line": 9, 71 | "match": { 72 | "location": "JDBCStepDefs.i_have_configure_the_JDBC_driver()" 73 | } 74 | }, 75 | { 76 | "result": { 77 | "status": "skipped" 78 | }, 79 | "name": "I should be able to connect via JDBC without issues", 80 | "keyword": "Then ", 81 | "line": 10, 82 | "match": { 83 | "location": "JDBCStepDefs.i_should_be_able_to_connect_via_JDBC_without_issues()" 84 | } 85 | } 86 | ], 87 | "type": "scenario" 88 | } 89 | ], 90 | "uri": "features/Conectivity.feature" 91 | } 92 | ] -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/__CucumberTestResultAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 40 | 41 | 42 | 43 | ${it.displayName} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
    52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
  • 66 | 67 | 68 | 69 | 70 | 71 |
  • 72 |
    73 |
74 | 75 | 76 | 77 | ${%Show all failed tests} ${">>>"} 80 | 81 |
82 | 83 |
84 |
85 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResult/index.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 |

33 | 34 |

35 |

36 | 37 | 38 | 39 | (from ) 40 |

41 | 42 |
43 | 44 | 45 | ${%skippedFor(it.age)} 46 | 47 | 48 | ${%failingFor(it.age)} 49 | 50 | 51 | (${%since.before}${%since.after}) 52 |
53 |
54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 | 71 | 72 |

${%Skip Message}

73 |
74 |
75 | 76 | 77 |

${%Error Message}

78 |
79 |
80 | 81 | 82 |

${%Stacktrace}

83 |
84 |
85 | 86 | 87 |

${%Standard Output}

88 |
89 |
90 | 91 | 92 |

${%Standard Error}

93 |
94 |
95 |
96 |
97 |
98 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/StepResult.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.Match; 27 | import gherkin.formatter.model.Result; 28 | import gherkin.formatter.model.Step; 29 | import hudson.model.Run; 30 | import hudson.tasks.test.TestResult; 31 | 32 | /** 33 | * Represents a Step belonging to a Scenario from Cucumber. 34 | * 35 | * @author James Nord 36 | */ 37 | public class StepResult extends TestResult { 38 | 39 | private static final long serialVersionUID = 1L; 40 | 41 | private Step step; 42 | private Match match; 43 | private Result result; 44 | 45 | private ScenarioResult parent; 46 | private transient Run owner; 47 | 48 | 49 | StepResult(Step step, Match match, Result result) { 50 | this.step = step; 51 | this.match = match; 52 | this.result = result; 53 | } 54 | 55 | 56 | public String getDisplayName() { 57 | return "Cucumber Step result"; 58 | } 59 | 60 | 61 | @Override 62 | public Run getRun() { 63 | return owner; 64 | } 65 | 66 | 67 | void setOwner(Run owner) { 68 | this.owner = owner; 69 | } 70 | 71 | 72 | @Override 73 | public ScenarioResult getParent() { 74 | return parent; 75 | } 76 | 77 | 78 | protected void setParent(ScenarioResult parent) { 79 | this.parent = parent; 80 | } 81 | 82 | 83 | @Override 84 | public TestResult findCorrespondingResult(String id) { 85 | // TODO Auto-generated method stub 86 | return null; 87 | } 88 | 89 | 90 | @Override 91 | public float getDuration() { 92 | return CucumberUtils.durationFromResult(result); 93 | } 94 | 95 | 96 | /** 97 | * Gets the total number of passed tests. 98 | */ 99 | public int getPassCount() { 100 | return CucumberUtils.PASSED_TEST_STRING.equals(result.getStatus()) ? 1 : 0; 101 | } 102 | 103 | 104 | /** 105 | * Gets the total number of failed tests. 106 | */ 107 | public int getFailCount() { 108 | if (CucumberUtils.FAILED_TEST_STRING.equals(result.getStatus()) 109 | || CucumberUtils.UNDEFINED_TEST_STRING.equals(result.getStatus())) { 110 | return 1; 111 | } 112 | return 0; 113 | } 114 | 115 | 116 | /** 117 | * Gets the total number of skipped tests. 118 | */ 119 | public int getSkipCount() { 120 | if (CucumberUtils.SKIPPED_TEST_STRING.equals(result.getStatus()) 121 | || CucumberUtils.PENDING_TEST_STRING.equals(result.getStatus())) { 122 | return 1; 123 | } 124 | return 0; 125 | } 126 | 127 | 128 | Step getStep() { 129 | return step; 130 | } 131 | 132 | 133 | Match getMatch() { 134 | return match; 135 | } 136 | 137 | 138 | Result getResult() { 139 | return result; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/BeforeAfterResult.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.Match; 27 | import gherkin.formatter.model.Result; 28 | import hudson.model.Run; 29 | import hudson.tasks.test.TestObject; 30 | import hudson.tasks.test.TestResult; 31 | 32 | /** 33 | * Represents a Before or After belonging to a Scenario. Although this is a test Object as it is a background 34 | * it is not intended for individual Display. 35 | * 36 | * @author James Nord 37 | */ 38 | public class BeforeAfterResult extends TestResult { 39 | 40 | private static final long serialVersionUID = 1L; 41 | 42 | private Match macth; 43 | private Result result; 44 | 45 | private transient Run owner; 46 | 47 | 48 | public BeforeAfterResult(Match match, Result result) { 49 | this.macth = match; 50 | this.result = result; 51 | } 52 | 53 | 54 | @Override 55 | public String getName() { 56 | return "Cucumber Background"; 57 | } 58 | 59 | 60 | /** 61 | * Gets the total number of passed tests. 62 | */ 63 | @Override 64 | public int getPassCount() { 65 | return CucumberUtils.PASSED_TEST_STRING.equals(result.getStatus()) ? 1 : 0; 66 | } 67 | 68 | 69 | /** 70 | * Gets the total number of failed tests. 71 | */ 72 | @Override 73 | public int getFailCount() { 74 | if (CucumberUtils.FAILED_TEST_STRING.equals(result.getStatus()) 75 | || CucumberUtils.UNDEFINED_TEST_STRING.equals(result.getStatus())) { 76 | return 1; 77 | } 78 | return 0; 79 | } 80 | 81 | 82 | /** 83 | * Gets the total number of skipped tests. 84 | */ 85 | @Override 86 | public int getSkipCount() { 87 | if (CucumberUtils.SKIPPED_TEST_STRING.equals(result.getStatus()) 88 | || CucumberUtils.PENDING_TEST_STRING.equals(result.getStatus())) { 89 | return 1; 90 | } 91 | return 0; 92 | } 93 | 94 | 95 | @Override 96 | public Run getRun() { 97 | return owner; 98 | } 99 | 100 | 101 | void setOwner(Run owner) { 102 | this.owner = owner; 103 | } 104 | 105 | 106 | @Override 107 | public TestObject getParent() { 108 | // TODO Auto-generated method stub 109 | return null; 110 | } 111 | 112 | 113 | @Override 114 | public TestResult findCorrespondingResult(String id) { 115 | // TODO Auto-generated method stub 116 | return null; 117 | } 118 | 119 | 120 | public String getDisplayName() { 121 | // TODO Auto-generated method stub 122 | return null; 123 | } 124 | 125 | 126 | @Override 127 | public float getDuration() { 128 | return CucumberUtils.durationFromResult(result); 129 | } 130 | 131 | 132 | Match getMatch() { 133 | return macth; 134 | } 135 | 136 | 137 | Result getResult() { 138 | return result; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberUtils.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.Result; 27 | import gherkin.formatter.model.TagStatement; 28 | 29 | import java.io.File; 30 | import java.io.FileOutputStream; 31 | import java.io.IOException; 32 | import java.lang.reflect.Field; 33 | import java.util.logging.Level; 34 | import java.util.logging.Logger; 35 | 36 | 37 | public class CucumberUtils { 38 | 39 | private static final Logger LOG = Logger.getLogger(CucumberUtils.class.getName()); 40 | 41 | public static final String FAILED_TEST_STRING = "failed"; 42 | public static final String PASSED_TEST_STRING = "passed"; 43 | public static final String UNDEFINED_TEST_STRING = "undefined"; 44 | public static final String PENDING_TEST_STRING = "pending"; 45 | public static final String SKIPPED_TEST_STRING = "skipped"; 46 | 47 | 48 | /** Get the duration (in seconds) that the result took. */ 49 | static float durationFromResult(Result result) { 50 | // internally this is in nanosecodes 51 | Long l = result.getDuration(); 52 | if (l == null) { 53 | return 0.0f; 54 | } 55 | return l.floatValue() / 1000000000.0f; 56 | } 57 | 58 | 59 | /** 60 | * Get the ID from the TagStatement. For some reason the authors of cucumber jvm thought the ID should be 61 | * private with no getter... TODO - create a patch for cucumber-jvm so we do not need this hack. 62 | * 63 | * @param stmt the {@link TagStatement} with the ID> 64 | * @return the ID of the {@link TagStatement} - possibly null 65 | */ 66 | public static String getId(TagStatement stmt) { 67 | try { 68 | Field f = TagStatement.class.getField("id"); 69 | f.setAccessible(true); 70 | return (String) f.get(stmt); 71 | } 72 | catch (NoSuchFieldException e) { 73 | LOG.log(Level.WARNING, "Could not get ID from statement: " + stmt.getName(), e); 74 | } 75 | catch (SecurityException e) { 76 | LOG.log(Level.WARNING, "Could not get ID from statement: " + stmt.getName(), e); 77 | } 78 | catch (IllegalArgumentException e) { 79 | LOG.log(Level.WARNING, "Could not get ID from statement: " + stmt.getName(), e); 80 | } 81 | catch (IllegalAccessException e) { 82 | LOG.log(Level.WARNING, "Could not get ID from statement: " + stmt.getName(), e); 83 | } 84 | return null; 85 | } 86 | 87 | 88 | /** 89 | * Create a temporary file on the slave to store the embedded content 90 | * 91 | * @throws IOException if we couldn't create a temporary file 92 | */ 93 | public static File createEmbedFile(byte[] data) throws IOException { 94 | File f = File.createTempFile("cuke_", ".embed"); 95 | { 96 | FileOutputStream fos = new FileOutputStream(f); 97 | try { 98 | fos.write(data); 99 | fos.flush(); 100 | } 101 | finally { 102 | fos.close(); 103 | } 104 | } 105 | return f; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONParser.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.JSONParser; 27 | import hudson.AbortException; 28 | import hudson.Extension; 29 | import hudson.FilePath; 30 | import hudson.Launcher; 31 | import hudson.model.Run; 32 | import hudson.model.TaskListener; 33 | 34 | import java.io.File; 35 | import java.io.IOException; 36 | import java.util.List; 37 | 38 | import org.apache.commons.io.FileUtils; 39 | 40 | /** 41 | * Parser that understands Cucumbers JSON notation and will 42 | * generate {@link hudson.tasks.test.TestResult} so that Jenkins will display the results. 43 | */ 44 | @Extension 45 | public class CucumberJSONParser extends DefaultTestResultParserImpl { 46 | 47 | private static final long serialVersionUID = -296964473181541824L; 48 | private boolean ignoreBadSteps; 49 | 50 | public CucumberJSONParser() { 51 | } 52 | 53 | public CucumberJSONParser(boolean ignoreBadSteps){ 54 | this.ignoreBadSteps = ignoreBadSteps; 55 | } 56 | 57 | @Override 58 | public String getDisplayName() { 59 | return "Cucumber JSON parser"; 60 | } 61 | 62 | @Override 63 | protected CucumberTestResult parse(List reportFiles, TaskListener listener) throws InterruptedException, IOException { 64 | 65 | CucumberTestResult result = new CucumberTestResult(); 66 | GherkinCallback callback = new GherkinCallback(result, listener, ignoreBadSteps); 67 | listener.getLogger().println("[Cucumber Tests] Parsing results."); 68 | JSONParser jsonParser = new JSONParser(callback, callback); 69 | 70 | try { 71 | for (File f : reportFiles) { 72 | String s = FileUtils.readFileToString(f, "UTF-8"); 73 | // if no scenarios where executed for a feature then a json file may still exist. 74 | if (s.isEmpty()) { 75 | listener.getLogger().println("[Cucumber Tests] ignoring empty file (" + f.getName() + ")"); 76 | } 77 | else {listener.getLogger().println("[Cucumber Tests] parsing " + f.getName()); 78 | jsonParser.parse(s); 79 | } 80 | } 81 | } 82 | catch (CucumberModelException ccm) { 83 | throw new AbortException("Failed to parse Cucumber JSON: " + ccm.getMessage()); 84 | } 85 | finally { 86 | // even though this is a noop prevent an eclipse warning. 87 | callback.close(); 88 | } 89 | result.tally(); 90 | return result; 91 | } 92 | 93 | 94 | @Override 95 | public CucumberTestResult parseResult(final String testResultLocations, 96 | final Run build, 97 | final FilePath workspace, 98 | final Launcher launcher, 99 | final TaskListener listener) throws InterruptedException, IOException { 100 | // overridden so we tally and set the owner on the master. 101 | CucumberTestResult result = (CucumberTestResult) super.parseResult(testResultLocations, build, workspace, launcher, listener); 102 | result.tally(); 103 | result.setOwner(build); 104 | return result; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/BackgroundResult.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 java.util.ArrayList; 27 | import java.util.Collection; 28 | 29 | import gherkin.formatter.model.Background; 30 | import hudson.model.Run; 31 | import hudson.tasks.test.TestResult; 32 | 33 | /** 34 | * Represents a Background belonging to a Scenario. 35 | * Although this is a test Object as it is a background it is not intended for individual Display. 36 | * @author James Nord 37 | */ 38 | public class BackgroundResult extends TestResult { 39 | 40 | private static final long serialVersionUID = 1L; 41 | 42 | private Background background; 43 | private ArrayList stepResults = new ArrayList(); 44 | 45 | private ScenarioResult parent; 46 | 47 | private transient Run owner; 48 | 49 | /* Recomputed by a call to {@link CucumberTestResult#tally()} */ 50 | // true if this test failed 51 | private transient boolean failed; 52 | private transient boolean skipped; 53 | 54 | private transient float duration; 55 | 56 | BackgroundResult(Background background) { 57 | this.background = background; 58 | } 59 | 60 | @Override 61 | public Run getRun() { 62 | return owner; 63 | } 64 | 65 | void setOwner(Run owner) { 66 | this.owner = owner; 67 | for (StepResult sr : stepResults) { 68 | sr.setOwner(owner); 69 | } 70 | } 71 | 72 | @Override 73 | public String getName() { 74 | return "Cucumber Background"; 75 | } 76 | 77 | @Override 78 | public int getFailCount() { 79 | return failed ? 1 : 0; 80 | } 81 | 82 | @Override 83 | public int getSkipCount() { 84 | return skipped ? 1 : 0; 85 | } 86 | 87 | @Override 88 | public int getPassCount() { 89 | return (failed || skipped) ? 0 : 1; 90 | } 91 | 92 | 93 | 94 | 95 | @Override 96 | public ScenarioResult getParent() { 97 | return parent; 98 | } 99 | 100 | void setParent(ScenarioResult parent) { 101 | this.parent = parent; 102 | } 103 | 104 | @Override 105 | public float getDuration() { 106 | return duration; 107 | } 108 | 109 | @Override 110 | public TestResult findCorrespondingResult(String id) { 111 | // TODO Auto-generated method stub 112 | return null; 113 | } 114 | 115 | public String getDisplayName() { 116 | return "Background Result"; 117 | } 118 | 119 | public Background getBackground() { 120 | return this.background; 121 | } 122 | 123 | void addStepResult(StepResult stepResult) { 124 | stepResults.add(stepResult); 125 | } 126 | 127 | 128 | Collection getStepResults() { 129 | return stepResults; 130 | } 131 | 132 | @Override 133 | public void tally() { 134 | duration = 0.0f; 135 | failed = false; 136 | skipped = false; 137 | 138 | for (StepResult sr : stepResults) { 139 | sr.tally(); 140 | if (!failed) { 141 | // only change the flags if we haven't had a failure 142 | if (sr.getFailCount() != 0) { 143 | failed = true; 144 | skipped = false; 145 | } 146 | else if (sr.getSkipCount() != 0) { 147 | failed = false; 148 | skipped = true; 149 | } 150 | } 151 | duration += sr.getDuration(); 152 | } 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/TagResult.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.model.Run; 27 | import hudson.tasks.test.MetaTabulatedResult; 28 | import hudson.tasks.test.TestObject; 29 | import hudson.tasks.test.TestResult; 30 | 31 | import java.util.ArrayList; 32 | import java.util.Collection; 33 | import java.util.HashSet; 34 | import java.util.List; 35 | import java.util.Set; 36 | 37 | import org.kohsuke.stapler.StaplerRequest; 38 | import org.kohsuke.stapler.StaplerResponse; 39 | 40 | /** 41 | * A TagResult is a pseudo result to link scenarios with the same tag. 42 | * 43 | * @author James Nord 44 | */ 45 | public class TagResult extends MetaTabulatedResult { 46 | 47 | private static final long serialVersionUID = -5418078481483188238L; 48 | 49 | private transient Run owner; 50 | private transient String safeName; 51 | 52 | private Set scenarioResults = new HashSet(); 53 | private transient List failedScenarioResults; 54 | 55 | private String tagName; 56 | 57 | private int passCount; 58 | private int failCount; 59 | private int skipCount; 60 | private float duration; 61 | 62 | private CucumberTestResult parent; 63 | 64 | 65 | TagResult(String tagName) { 66 | this.tagName = tagName; 67 | } 68 | 69 | 70 | public String getDisplayName() { 71 | return getName(); 72 | } 73 | 74 | 75 | public String getName() { 76 | return tagName; 77 | } 78 | 79 | 80 | @Override 81 | public Collection getChildren() { 82 | return scenarioResults; 83 | } 84 | 85 | 86 | public Collection getScenarioResults() { 87 | return scenarioResults; 88 | } 89 | 90 | 91 | @Override 92 | public String getChildTitle() { 93 | return "Cucumber Scenario"; 94 | } 95 | 96 | 97 | @Override 98 | public boolean hasChildren() { 99 | return !scenarioResults.isEmpty(); 100 | } 101 | 102 | 103 | @Override 104 | public Run getRun() { 105 | return owner; 106 | } 107 | 108 | 109 | public void setOwner(Run owner) { 110 | this.owner = owner; 111 | } 112 | 113 | 114 | @Override 115 | public TestObject getParent() { 116 | return parent; 117 | } 118 | 119 | 120 | protected void setParent(CucumberTestResult parent) { 121 | this.parent = parent; 122 | } 123 | 124 | 125 | @Override 126 | public TestResult findCorrespondingResult(String id) { 127 | // TODO Auto-generated method stub 128 | return null; 129 | } 130 | 131 | 132 | @Override 133 | public Collection getFailedTests() { 134 | return failedScenarioResults; 135 | } 136 | 137 | 138 | public String getTagName() { 139 | return tagName; 140 | } 141 | 142 | 143 | void addScenarioResult(ScenarioResult scenarioResult) { 144 | scenarioResults.add(scenarioResult); 145 | } 146 | 147 | 148 | @Override 149 | public synchronized String getSafeName() { 150 | // no need to make unique as tags are shared! 151 | if (safeName != null) { 152 | return safeName; 153 | } 154 | safeName = safe(getName()); 155 | return safeName; 156 | } 157 | 158 | 159 | @Override 160 | public void tally() { 161 | if (failedScenarioResults == null) { 162 | failedScenarioResults = new ArrayList(); 163 | } 164 | else { 165 | failedScenarioResults.clear(); 166 | } 167 | passCount = 0; 168 | failCount = 0; 169 | skipCount = 0; 170 | duration = 0.0f; 171 | 172 | for (ScenarioResult sr : scenarioResults) { 173 | // ScenarioResult will have already been tallyed 174 | passCount += sr.getPassCount(); 175 | failCount += sr.getFailCount(); 176 | skipCount += sr.getSkipCount(); 177 | duration += sr.getDuration(); 178 | if (!sr.isPassed()) { 179 | failedScenarioResults.add(sr); 180 | } 181 | } 182 | } 183 | 184 | 185 | @Override 186 | public int getFailCount() { 187 | return failCount; 188 | } 189 | 190 | 191 | @Override 192 | public int getPassCount() { 193 | return passCount; 194 | } 195 | 196 | 197 | @Override 198 | public float getDuration() { 199 | return duration; 200 | } 201 | 202 | 203 | @Override 204 | public int getSkipCount() { 205 | return skipCount; 206 | } 207 | 208 | @Override 209 | public int getTotalCount() { 210 | int retVal = super.getTotalCount(); 211 | return retVal; 212 | } 213 | 214 | @Override 215 | public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { 216 | 217 | // if (token.equals(getId())) { 218 | // return this; 219 | // } 220 | // ScenarioResult result = scenariosByID.get(token); 221 | // if (result != null) { 222 | // return result; 223 | // } 224 | // else { 225 | // return super.getDynamic(token, req, rsp); 226 | // } 227 | return super.getDynamic(token, req, rsp); 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/cucumber/jsontestsupport/FeatureResult.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.Feature; 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.List; 35 | import java.util.Map; 36 | import java.util.TreeMap; 37 | 38 | import org.kohsuke.stapler.StaplerRequest; 39 | import org.kohsuke.stapler.StaplerResponse; 40 | import org.kohsuke.stapler.export.Exported; 41 | import org.kohsuke.stapler.export.ExportedBean; 42 | 43 | /** 44 | * Represents a single Feature in Cucumber. 45 | * 46 | * @author James Nord 47 | */ 48 | @ExportedBean 49 | public class FeatureResult extends MetaTabulatedResult { 50 | 51 | private static final long serialVersionUID = 995206500596875310L; 52 | 53 | private Feature feature; 54 | private String uri; 55 | private transient Run owner; 56 | private transient String safeName; 57 | 58 | private List scenarioResults = new ArrayList(); 59 | 60 | private transient List failedScenarioResults; 61 | /** 62 | * Map of scenarios keyed by scenario name. 63 | * Recomputed by a call to {@link CucumberTestResult#tally()} 64 | */ 65 | private transient Map scenariosByID = new TreeMap(); 66 | 67 | // XXX do we need to store these or should they be transient and recomputed on load. 68 | private int passCount; 69 | private int failCount; 70 | private int skipCount; 71 | private float duration; 72 | 73 | 74 | // TODO should this be reset on loading from xStream 75 | private CucumberTestResult parent; 76 | 77 | FeatureResult(String uri, Feature feature) { 78 | this.uri = uri; 79 | this.feature = feature; 80 | } 81 | 82 | 83 | public String getDisplayName() { 84 | return getName(); 85 | } 86 | 87 | @Exported(visibility=9) 88 | public String getName() { 89 | return feature.getName(); 90 | } 91 | 92 | 93 | @Override 94 | public Collection getChildren() { 95 | return scenarioResults; 96 | } 97 | 98 | @Exported(visibility=9) 99 | public Collection getScenarioResults() { 100 | return scenarioResults; 101 | } 102 | 103 | @Override 104 | public String getChildTitle() { 105 | return "Cucumber Scenarios"; 106 | } 107 | 108 | 109 | @Override 110 | public boolean hasChildren() { 111 | return !scenarioResults.isEmpty(); 112 | } 113 | 114 | 115 | @Override 116 | public Run getRun() { 117 | return owner; 118 | } 119 | 120 | public void setOwner(Run owner) { 121 | this.owner = owner; 122 | for (ScenarioResult sr : scenarioResults) { 123 | sr.setOwner(owner); 124 | } 125 | } 126 | 127 | @Override 128 | public TestObject getParent() { 129 | return parent; 130 | } 131 | 132 | 133 | protected void setParent(CucumberTestResult parent) { 134 | this.parent = parent; 135 | } 136 | 137 | 138 | @Override 139 | public TestResult findCorrespondingResult(String id) { 140 | return scenariosByID.get(id); 141 | } 142 | 143 | 144 | @Override 145 | public Collection getFailedTests() { 146 | return failedScenarioResults; 147 | } 148 | 149 | 150 | public String getURI() { 151 | return uri; 152 | } 153 | 154 | public Feature getFeature() { 155 | return feature; 156 | } 157 | 158 | void addScenarioResult(ScenarioResult scenarioResult) { 159 | scenarioResults.add(scenarioResult); 160 | scenarioResult.setParent(this); 161 | } 162 | 163 | @Override 164 | public synchronized String getSafeName() { 165 | if (safeName != null) { 166 | return safeName; 167 | } 168 | safeName = uniquifyName(parent.getChildren(), safe(feature.getId())); 169 | return safeName; 170 | } 171 | 172 | @Override 173 | public void tally() { 174 | if (scenariosByID == null) { 175 | scenariosByID = new TreeMap(); 176 | } 177 | else { 178 | scenariosByID.clear(); 179 | } 180 | if (failedScenarioResults == null) { 181 | failedScenarioResults = new ArrayList(); 182 | } 183 | else { 184 | failedScenarioResults.clear(); 185 | } 186 | passCount = 0; 187 | failCount = 0; 188 | skipCount = 0; 189 | duration = 0.0f; 190 | 191 | for (ScenarioResult sr : scenarioResults) { 192 | sr.tally(); 193 | // XXX scenarious may be duplicated!??! 194 | scenariosByID.put(sr.getSafeName(), sr); 195 | passCount += sr.getPassCount(); 196 | failCount += sr.getFailCount(); 197 | skipCount += sr.getSkipCount(); 198 | duration += sr.getDuration(); 199 | if (!sr.isPassed()) { 200 | failedScenarioResults.add(sr); 201 | } 202 | } 203 | } 204 | 205 | 206 | @Override 207 | public int getFailCount() { 208 | return failCount; 209 | } 210 | 211 | 212 | @Override 213 | public int getPassCount() { 214 | return passCount; 215 | } 216 | 217 | 218 | @Override 219 | public float getDuration() { 220 | return duration; 221 | } 222 | 223 | 224 | @Override 225 | public int getSkipCount() { 226 | return skipCount; 227 | } 228 | 229 | 230 | @Override 231 | public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { 232 | if (token.equals(getId())) { 233 | return this; 234 | } 235 | ScenarioResult result = scenariosByID.get(token); 236 | if (result != null) { 237 | return result; 238 | } 239 | else { 240 | return super.getDynamic(token, req, rsp); 241 | } 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONSupportPluginIT/passWithEmbeddedItem.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "elements": [ 5 | { 6 | "line": 4, 7 | "name": "I can connect to the database instance", 8 | "description": "", 9 | "id": "connectivity;i-can-connect-to-the-database-instance", 10 | "after": [ 11 | { 12 | "result": { 13 | "duration": 9738454, 14 | "status": "passed" 15 | }, 16 | "match": { 17 | "location": "JDBCStepDefs.embedScreenshot(Scenario)" 18 | } 19 | } 20 | ], 21 | "type": "scenario", 22 | "keyword": "Scenario", 23 | "steps": [ 24 | { 25 | "result": { 26 | "duration": 1355975209, 27 | "status": "passed" 28 | }, 29 | "line": 5, 30 | "name": "I have a server", 31 | "match": { 32 | "location": "JDBCStepDefs.i_have_a_server()" 33 | }, 34 | "keyword": "Given " 35 | }, 36 | { 37 | "result": { 38 | "duration": 71051, 39 | "status": "passed" 40 | }, 41 | "line": 6, 42 | "name": "I have a username", 43 | "match": { 44 | "location": "JDBCStepDefs.i_have_a_username()" 45 | }, 46 | "keyword": "And " 47 | }, 48 | { 49 | "result": { 50 | "duration": 30789, 51 | "status": "passed" 52 | }, 53 | "line": 7, 54 | "name": "I have a password", 55 | "match": { 56 | "location": "JDBCStepDefs.i_have_a_password()" 57 | }, 58 | "keyword": "And " 59 | }, 60 | { 61 | "result": { 62 | "duration": 87074513, 63 | "status": "passed" 64 | }, 65 | "line": 8, 66 | "name": "I have registered the Oracle JDBC driver", 67 | "match": { 68 | "location": "JDBCStepDefs.i_register_the_Oracle_JDBC_driver()" 69 | }, 70 | "keyword": "When " 71 | }, 72 | { 73 | "result": { 74 | "duration": 28625433269, 75 | "status": "passed" 76 | }, 77 | "embeddings": [ 78 | { 79 | "data": "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\u003d", 80 | "mime_type": "image/png" 81 | } 82 | ], 83 | "line": 9, 84 | "name": "I should be able to connect via JDBC without issues", 85 | "match": { 86 | "location": "JDBCStepDefs.i_should_be_able_to_connect_via_JDBC_without_issues()" 87 | }, 88 | "keyword": "Then " 89 | } 90 | ] 91 | } 92 | ], 93 | "name": "Connectivity", 94 | "description": "As a user I want to be able to connect to the Oracle instance via JDBC", 95 | "id": "connectivity", 96 | "keyword": "Feature", 97 | "uri": "features/Conectivity.feature" 98 | } 99 | ] -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 4.0.0 29 | 30 | org.jenkins-ci.plugins 31 | plugin 32 | 2.14 33 | 34 | 35 | 36 | org.jenkins-ci.plugins 37 | cucumber-testresult-plugin-customised 38 | 0.10-SNAPSHOT 39 | hpi 40 | 41 | Cucumber json test reporting. 42 | This plugin understands cucumber json files and converts them to Jenkins TestCase so they can be seen in the standard test reports. 43 | 44 | http://wiki.jenkins-ci.org/display/JENKINS/Cucumber+Test+Result+Plugin 45 | 2013 46 | 47 | 48 | 1.651 49 | 2.14 50 | 51 | 52 | 53 | 54 | teilo 55 | James Nord 56 | 57 | 58 | 59 | 60 | scm:git:ssh://git@github.com/jenkinsci/cucumber-testresult-plugin.git 61 | scm:git:ssh://git@github.com/jenkinsci/cucumber-testresult-plugin.git 62 | https://github.com/jenkinsci/cucumber-testresult-plugin/ 63 | HEAD 64 | 65 | 66 | 67 | 68 | MIT 69 | https://github.com/jenkinsci/cucumber-testresult-plugin/blob/master/LICENCE.txt 70 | repo 71 | 72 | 73 | 74 | 75 | 76 | org.jenkins-ci.plugins 77 | junit 78 | 1.2 79 | 80 | 81 | org.jenkins-ci.plugins.workflow 82 | workflow-aggregator 83 | 2.3 84 | test 85 | 86 | 87 | org.jenkins-ci.plugins.workflow 88 | workflow-support 89 | 2.5 90 | test-jar 91 | test 92 | 93 | 94 | org.jenkins-ci.plugins.workflow 95 | workflow-step-api 96 | 2.3 97 | test 98 | 99 | 100 | org.jenkins-ci.plugins 101 | matrix-project 102 | 1.4 103 | 104 | 105 | 106 | info.cukes 107 | gherkin 108 | 2.12.2 109 | 110 | 111 | com.google.guava 112 | guava 113 | 114 | 11.0.1 115 | 116 | 117 | org.jenkins-ci.plugins 118 | structs 119 | 1.5 120 | 121 | 122 | junit 123 | junit 124 | 4.11 125 | test 126 | 127 | 128 | org.hamcrest 129 | hamcrest-library 130 | 1.3 131 | test 132 | 133 | 134 | org.mockito 135 | mockito-core 136 | 1.9.5 137 | test 138 | 139 | 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-failsafe-plugin 146 | 2.18.1 147 | 148 | 149 | 150 | integration-test 151 | verify 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-javadoc-plugin 162 | 163 | true 164 | 165 | 166 | 167 | org.codehaus.mojo 168 | findbugs-maven-plugin 169 | 170 | ${basedir}/src/build/findbugs/findbugs-exclude.xml 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | repo.jenkins-ci.org 181 | 182 | true 183 | 184 | http://repo.jenkins-ci.org/public/ 185 | 186 | 187 | 188 | 189 | repo.jenkins-ci.org 190 | http://repo.jenkins-ci.org/public/ 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/test/resources/org/jenkinsci/plugins/cucumber/jsontestsupport/ScenarioResultTest/cucumber-embedded-item.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "line": 1, 4 | "elements": [ 5 | { 6 | "line": 4, 7 | "name": "I can connect to the database instance", 8 | "description": "", 9 | "id": "connectivity;i-can-connect-to-the-database-instance", 10 | "after": [ 11 | { 12 | "result": { 13 | "duration": 9738454, 14 | "status": "passed" 15 | }, 16 | "match": { 17 | "location": "JDBCStepDefs.embedScreenshot(Scenario)" 18 | } 19 | } 20 | ], 21 | "type": "scenario", 22 | "keyword": "Scenario", 23 | "steps": [ 24 | { 25 | "result": { 26 | "duration": 1355975209, 27 | "status": "passed" 28 | }, 29 | "line": 5, 30 | "name": "I have a server", 31 | "match": { 32 | "location": "JDBCStepDefs.i_have_a_server()" 33 | }, 34 | "keyword": "Given " 35 | }, 36 | { 37 | "result": { 38 | "duration": 71051, 39 | "status": "passed" 40 | }, 41 | "line": 6, 42 | "name": "I have a username", 43 | "match": { 44 | "location": "JDBCStepDefs.i_have_a_username()" 45 | }, 46 | "keyword": "And " 47 | }, 48 | { 49 | "result": { 50 | "duration": 30789, 51 | "status": "passed" 52 | }, 53 | "line": 7, 54 | "name": "I have a password", 55 | "match": { 56 | "location": "JDBCStepDefs.i_have_a_password()" 57 | }, 58 | "keyword": "And " 59 | }, 60 | { 61 | "result": { 62 | "duration": 87074513, 63 | "status": "passed" 64 | }, 65 | "line": 8, 66 | "name": "I have registered the Oracle JDBC driver", 67 | "match": { 68 | "location": "JDBCStepDefs.i_register_the_Oracle_JDBC_driver()" 69 | }, 70 | "keyword": "When " 71 | }, 72 | { 73 | "result": { 74 | "duration": 28625433269, 75 | "error_message": "java.lang.AssertionError: Connection failed with IO Error: The Network Adapter could not establish the connection\r\n\tat org.junit.Assert.fail(Assert.java:88)\r\n\tat com.cisco.vci.service.oracle.aat.glue.JDBCStepDefs.i_should_be_able_to_connect_via_JDBC_without_issues(JDBCStepDefs.java:97)\r\n\tat ✽.Then I should be able to connect via JDBC without issues(features/Conectivity.feature:9)\r\n", 76 | "status": "failed" 77 | }, 78 | "embeddings": [ 79 | { 80 | "data": "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\u003d", 81 | "mime_type": "image/png" 82 | } 83 | ], 84 | "line": 9, 85 | "name": "I should be able to connect via JDBC without issues", 86 | "match": { 87 | "location": "JDBCStepDefs.i_should_be_able_to_connect_via_JDBC_without_issues()" 88 | }, 89 | "keyword": "Then " 90 | } 91 | ] 92 | } 93 | ], 94 | "name": "Connectivity", 95 | "description": "As a user I want to be able to connect to the Oracle instance via JDBC", 96 | "id": "connectivity", 97 | "keyword": "Feature", 98 | "uri": "features/Conectivity.feature" 99 | } 100 | ] -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/cucumber/jsontestsupport/CucumberJSONParserTest.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.model.TaskListener; 27 | 28 | import java.io.File; 29 | import java.net.URL; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | import org.junit.Assert; 34 | import org.junit.Test; 35 | import org.mockito.Mockito; 36 | 37 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 38 | import static org.hamcrest.core.Is.is; 39 | import static org.junit.Assert.assertThat; 40 | 41 | public class CucumberJSONParserTest { 42 | 43 | 44 | @Test 45 | public void testParsing() throws Exception { 46 | CucumberJSONParser parser = new CucumberJSONParser(); 47 | 48 | File f = getResourceAsFile("ScenarioResultTest/cucumber-jvm_examples_java-calculator__cucumber-report.json"); 49 | 50 | List files = new ArrayList(); 51 | files.add(f); 52 | 53 | TaskListener mockListener = Mockito.mock(TaskListener.class); 54 | Mockito.when(mockListener.getLogger()).thenReturn(System.out); 55 | 56 | CucumberTestResult testresult = parser.parse(files, mockListener); 57 | 58 | assertThat("Tests passed", testresult.isPassed(), is(true)); 59 | assertThat("Correct # of passing tests", testresult.getPassCount(), is(1)); 60 | assertThat("Correct # of failing tests", testresult.getFailCount(), is(0)); 61 | assertThat("Correct # of skipped tests", testresult.getSkipCount(), is(0)); 62 | assertThat("Duration is correct", testresult.getDuration(), is(0.0010894671F)); 63 | assertThat("Duration string is correct", testresult.getDurationString(), is("1 ms")); 64 | assertThat("Correct # of children", testresult.getChildren(), hasSize(1)); 65 | assertThat("Correct # of features", testresult.getFeatures(), hasSize(1)); 66 | 67 | // Get the individual Features and check their scenarios. 68 | } 69 | 70 | @Test 71 | public void testBackgroundFailure() throws Exception { 72 | CucumberJSONParser parser = new CucumberJSONParser(); 73 | 74 | File f = getResourceAsFile("ScenarioResultTest/backgroundFailure.json"); 75 | 76 | List files = new ArrayList(); 77 | files.add(f); 78 | 79 | TaskListener mockListener = Mockito.mock(TaskListener.class); 80 | Mockito.when(mockListener.getLogger()).thenReturn(System.out); 81 | 82 | CucumberTestResult testresult = parser.parse(files, mockListener); 83 | 84 | assertThat("Test should failed", testresult.isPassed(), is(false)); 85 | 86 | assertThat("Correct # of passing tests", testresult.getPassCount(), is(7)); 87 | assertThat("Correct # of failing tests", testresult.getFailCount(), is(1)); 88 | assertThat("Correct # of skipped tests", testresult.getSkipCount(), is(0)); 89 | assertThat("Duration is correct", testresult.getDuration(), is(0.33427134F)); 90 | assertThat("Duration string is correct", testresult.getDurationString(), is("0.33 sec")); 91 | assertThat("Correct # of children", testresult.getChildren(), hasSize(3)); 92 | assertThat("Correct # of features", testresult.getFeatures(), hasSize(3)); 93 | 94 | // Get the individual Features and check their scenarios. 95 | } 96 | 97 | @Test 98 | public void testPendingStep() throws Exception { 99 | CucumberJSONParser parser = new CucumberJSONParser(); 100 | 101 | File f = getResourceAsFile("ScenarioResultTest/pending.json"); 102 | 103 | List files = new ArrayList(); 104 | files.add(f); 105 | 106 | TaskListener mockListener = Mockito.mock(TaskListener.class); 107 | Mockito.when(mockListener.getLogger()).thenReturn(System.out); 108 | 109 | CucumberTestResult testresult = parser.parse(files, mockListener); 110 | 111 | assertThat("result should be pass", testresult.isPassed(), is(true)); 112 | 113 | assertThat("Correct # of passing tests", testresult.getPassCount(), is(0)); 114 | assertThat("Correct # of failing tests", testresult.getFailCount(), is(0)); 115 | assertThat("Correct # of skipped tests", testresult.getSkipCount(), is(1)); 116 | assertThat("Duration is correct", testresult.getDuration(), is(0.100813F)); 117 | assertThat("Duration string is correct", testresult.getDurationString(), is("0.1 sec")); 118 | assertThat("Correct # of children", testresult.getChildren(), hasSize(1)); 119 | assertThat("Correct # of features", testresult.getFeatures(), hasSize(1)); 120 | 121 | // Get the individual Features and check their scenarios. 122 | } 123 | 124 | @Test 125 | public void testUndefinedStep() throws Exception { 126 | CucumberJSONParser parser = new CucumberJSONParser(); 127 | 128 | File f = getResourceAsFile("ScenarioResultTest/undefinedStep.json"); 129 | 130 | List files = new ArrayList(); 131 | files.add(f); 132 | 133 | TaskListener mockListener = Mockito.mock(TaskListener.class); 134 | Mockito.when(mockListener.getLogger()).thenReturn(System.out); 135 | 136 | CucumberTestResult testresult = parser.parse(files, mockListener); 137 | 138 | assertThat("result should be failure", testresult.isPassed(), is(false)); 139 | 140 | assertThat("Correct # of passing tests", testresult.getPassCount(), is(7)); 141 | assertThat("Correct # of failing tests", testresult.getFailCount(), is(1)); 142 | assertThat("Correct # of skipped tests", testresult.getSkipCount(), is(0)); 143 | assertThat("Duration is correct", testresult.getDuration(), is(0.023931958F)); 144 | assertThat("Duration string is correct", testresult.getDurationString(), is("23 ms")); 145 | assertThat("Correct # of children", testresult.getChildren(), hasSize(3)); 146 | assertThat("Correct # of features", testresult.getFeatures(), hasSize(3)); 147 | 148 | // Get the individual Features and check their scenarios. 149 | } 150 | 151 | @Test 152 | public void testEmbededItem() throws Exception { 153 | CucumberJSONParser parser = new CucumberJSONParser(); 154 | 155 | File f = getResourceAsFile("ScenarioResultTest/cucumber-embedded-item.json"); 156 | 157 | List files = new ArrayList(); 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 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 | 81 | 82 | 85 | 88 | 89 | 90 |
${%Test Name}${%Duration}${%Age}
66 | >>> 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 83 | ${f.durationString} 84 | 86 | ${f.age} 87 |
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 | 143 | 144 | 145 | 148 | 149 | 152 | 153 | 156 | 157 | 158 | 159 |
${it.childTitle}${%Duration}${%Fail}(${%diff})${%Skip}(${%diff})${%Total}(${%diff})
Total 113 | ${it.durationString} 114 | ${it.failCount}${h.getDiffString2(it.failCount-prevAll.failCount)} 118 | ${it.skipCount}${h.getDiffString2(it.skipCount-prevAll.skipCount)} 122 | ${it.totalCount}${h.getDiffString2(it.totalCount-prevAll.totalCount)} 126 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ${p.durationString}${p.failCount} 146 | ${h.getDiffString2(p.failCount-prev.failCount)} 147 | ${p.skipCount} 150 | ${h.getDiffString2(p.skipCount-prev.skipCount)} 151 | ${p.totalCount} 154 | ${h.getDiffString2(p.totalCount-prev.totalCount)} 155 |
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 | 218 | 219 | 220 | 223 | 224 | 227 | 228 | 231 | 232 | 233 | 234 |
Tag name${%Duration}${%Fail}(${%diff})${%Skip}(${%diff})${%Total}(${%diff})
Total 186 | ${it.durationString} 187 | ${it.failCount}${h.getDiffString2(it.failCount-prevAll.failCount)} 191 | ${it.skipCount}${h.getDiffString2(it.skipCount-prevAll.skipCount)} 195 | ${it.totalCount}${h.getDiffString2(it.totalCount-prevAll.totalCount)} 199 |
209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | ${p.durationString}${p.failCount} 221 | ${h.getDiffString2(p.failCount-prev.failCount)} 222 | ${p.skipCount} 225 | ${h.getDiffString2(p.skipCount-prev.skipCount)} 226 | ${p.totalCount} 229 | ${h.getDiffString2(p.totalCount-prev.totalCount)} 230 |
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 | 76 | 77 | 80 | 83 | 84 | 85 |
${%Test Name}${%Duration}${%Age}
63 | >>> 65 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 78 | ${f.durationString} 79 | 81 | ${f.age} 82 |
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 | 126 | 127 | 128 | 131 | 132 | 135 | 136 | 139 | 140 | 141 | 142 |
${it.childTitle}${%Duration}${%Fail}(${%diff})${%Skip}(${%diff})${%Total}(${%diff})
Total${it.durationString}${it.failCount}${h.getDiffString2(it.failCount-prevAll.failCount)}${it.skipCount}${h.getDiffString2(it.skipCount-prevAll.skipCount)}${it.totalCount}${h.getDiffString2(it.totalCount-prevAll.totalCount)}
121 | 122 | 123 | 124 | 125 | ${p.durationString}${p.failCount} 129 | ${h.getDiffString2(p.failCount-prev.failCount)} 130 | ${p.skipCount} 133 | ${h.getDiffString2(p.skipCount-prev.skipCount)} 134 | ${p.totalCount} 137 | ${h.getDiffString2(p.totalCount-prev.totalCount)} 138 |
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 | 189 | 190 | 191 | 194 | 195 | 198 | 199 | 202 | 203 | 204 | 205 |
Tag name${%Duration}${%Fail}(${%diff})${%Skip}(${%diff})${%Total}(${%diff})
Total${it.durationString}${it.failCount}${h.getDiffString2(it.failCount-prevAll.failCount)}${it.skipCount}${h.getDiffString2(it.skipCount-prevAll.skipCount)}${it.totalCount}${h.getDiffString2(it.totalCount-prevAll.totalCount)}
184 | 185 | 186 | 187 | 188 | ${p.durationString}${p.failCount} 192 | ${h.getDiffString2(p.failCount-prev.failCount)} 193 | ${p.skipCount} 196 | ${h.getDiffString2(p.skipCount-prev.skipCount)} 197 | ${p.totalCount} 200 | ${h.getDiffString2(p.totalCount-prev.totalCount)} 201 |
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(""); 307 | continue; 308 | } 309 | sb.append(""); 312 | } 313 | sb.append("
    "); 305 | sb.append(cell); 306 | sb.append(""); 310 | sb.append(cell); 311 | 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 jobType) { 343 | return true; 344 | } 345 | } 346 | 347 | } 348 | --------------------------------------------------------------------------------