├── src ├── main │ ├── resources │ │ ├── org │ │ │ └── tap4j │ │ │ │ └── plugin │ │ │ │ ├── tags │ │ │ │ ├── taglib │ │ │ │ ├── comments.jelly │ │ │ │ ├── status.jelly │ │ │ │ ├── directive.jelly │ │ │ │ ├── yaml.jelly │ │ │ │ └── line.jelly │ │ │ │ ├── TapProjectAction │ │ │ │ ├── sidepanel.properties │ │ │ │ ├── sidepanel_es.properties │ │ │ │ ├── sidepanel_pt.properties │ │ │ │ ├── nodata.properties │ │ │ │ ├── nodata_es.properties │ │ │ │ ├── nodata_pt.properties │ │ │ │ ├── sidepanel.jelly │ │ │ │ ├── floatingBox.jelly │ │ │ │ └── nodata.jelly │ │ │ │ ├── TapResult │ │ │ │ ├── contents.jelly │ │ │ │ ├── _index.groovy │ │ │ │ └── index.jelly │ │ │ │ ├── TapPublisher │ │ │ │ ├── help-name.html │ │ │ │ └── config.jelly │ │ │ │ ├── TapBuildAction │ │ │ │ └── summary.jelly │ │ │ │ ├── model │ │ │ │ ├── TapTestResultResult │ │ │ │ │ └── index.jelly │ │ │ │ └── TapStreamResult │ │ │ │ │ └── body.jelly │ │ │ │ └── TapTestResultAction │ │ │ │ ├── index.jelly │ │ │ │ └── sidepanel.jelly │ │ ├── index.jelly │ │ └── META-INF │ │ │ └── hudson.remoting.ClassFilter │ ├── webapp │ │ ├── icons │ │ │ ├── tap-24.png │ │ │ ├── tap-48.png │ │ │ └── exclamation.png │ │ ├── help │ │ │ └── TapPublisher │ │ │ │ ├── help-skipIfBuildNotOk.html │ │ │ │ └── help-failIfNoResults.html │ │ ├── help-globalConfig.html │ │ └── css │ │ │ └── tap.css │ └── java │ │ └── org │ │ └── tap4j │ │ └── plugin │ │ ├── util │ │ ├── Constants.java │ │ ├── Util.java │ │ ├── DiagnosticUtil.java │ │ └── GraphHelper.java │ │ ├── model │ │ ├── ParseErrorTestSetMap.java │ │ ├── TestSetMap.java │ │ ├── TapAttachment.java │ │ ├── TapStreamResult.java │ │ └── TapTestResultResult.java │ │ ├── AbstractTapProjectAction.java │ │ ├── TapBuildAction.java │ │ ├── TapTestResultAction.java │ │ ├── TapParser.java │ │ └── TapProjectAction.java └── test │ ├── java │ └── org │ │ └── tap4j │ │ └── plugin │ │ ├── issue16647 │ │ ├── package-info.java │ │ └── TestIssue16647.java │ │ ├── issue16964 │ │ ├── package-info.java │ │ └── TestIssue16964.java │ │ ├── issue17947 │ │ ├── package-info.java │ │ └── TestIssue17947.java │ │ ├── issue21456 │ │ ├── package-info.java │ │ └── TestIssue21456.java │ │ ├── stripsingleparent │ │ ├── package-info.java │ │ └── TestStripSingleParent.java │ │ ├── flattentapfeature │ │ ├── package-info.java │ │ └── TestFlattenTapResult.java │ │ ├── removeyamlifcorrupted │ │ ├── package-info.java │ │ └── TestRemoveYamlIfCorrupted.java │ │ ├── util │ │ ├── GraphHelperTest.java │ │ └── TestUtil.java │ │ ├── PublishersCombinationTest.java │ │ ├── jenkins_cert_3190 │ │ └── TestXssTapFile.java │ │ └── TapPublisherTest.java │ └── resources │ └── org │ └── tap4j │ └── plugin │ ├── TapPublisherTest.zip │ ├── tap-master-files │ ├── sample.tap │ └── subtest-sample.tap │ ├── util │ └── GraphHelperTest.zip │ └── PublishersCombinationTest.zip ├── docs └── images │ ├── 001.png │ ├── 002.png │ ├── 003.png │ ├── logo1.png │ └── JCertif_Conf2011_2.png ├── .gitignore ├── TODO.txt ├── Jenkinsfile ├── .github └── workflows │ └── jenkins-security-scan.yml ├── LICENSE.txt ├── pom.xml ├── README.md └── CHANGES.md /src/main/resources/org/tap4j/plugin/tags/taglib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/docs/images/001.png -------------------------------------------------------------------------------- /docs/images/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/docs/images/002.png -------------------------------------------------------------------------------- /docs/images/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/docs/images/003.png -------------------------------------------------------------------------------- /docs/images/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/docs/images/logo1.png -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/sidepanel.properties: -------------------------------------------------------------------------------- 1 | Back \to \Dashboard=Back to Dashboard 2 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/sidepanel_es.properties: -------------------------------------------------------------------------------- 1 | Back \to \Dashboard=Regresar al Dashboard 2 | -------------------------------------------------------------------------------- /src/main/webapp/icons/tap-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/main/webapp/icons/tap-24.png -------------------------------------------------------------------------------- /src/main/webapp/icons/tap-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/main/webapp/icons/tap-48.png -------------------------------------------------------------------------------- /docs/images/JCertif_Conf2011_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/docs/images/JCertif_Conf2011_2.png -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/sidepanel_pt.properties: -------------------------------------------------------------------------------- 1 | Back \to \Dashboard=Voltar para o Dashboard 2 | -------------------------------------------------------------------------------- /src/main/webapp/icons/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/main/webapp/icons/exclamation.png -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin publishes TAP test results. 4 |
5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | work/ 2 | target/ 3 | test-output/ 4 | .classpath 5 | .settings/ 6 | .project 7 | **/*~ 8 | .idea 9 | *.iml 10 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue16647/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for issue JENKINS-16647. 3 | */ 4 | package org.tap4j.plugin.issue16647; -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue16964/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for issue JENKINS-16964. 3 | */ 4 | package org.tap4j.plugin.issue16964; -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue17947/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for issue JENKINS-17947. 3 | */ 4 | package org.tap4j.plugin.issue17947; -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue21456/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for issue JENKINS-21456. 3 | */ 4 | package org.tap4j.plugin.issue21456; -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/stripsingleparent/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests stripping single parent feature. 3 | */ 4 | package org.tap4j.plugin.stripsingleparent; -------------------------------------------------------------------------------- /src/test/resources/org/tap4j/plugin/TapPublisherTest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/test/resources/org/tap4j/plugin/TapPublisherTest.zip -------------------------------------------------------------------------------- /src/test/resources/org/tap4j/plugin/tap-master-files/sample.tap: -------------------------------------------------------------------------------- 1 | 1..3 2 | ok 1 3 | not ok 2 4 | # some IO error 5 | # and more text here 6 | ok 3 # SKIP error in test 2 -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/flattentapfeature/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests flatten TAP result feature feature. 3 | */ 4 | package org.tap4j.plugin.flattentapfeature; -------------------------------------------------------------------------------- /src/test/resources/org/tap4j/plugin/util/GraphHelperTest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/test/resources/org/tap4j/plugin/util/GraphHelperTest.zip -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/removeyamlifcorrupted/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests corrupted YAML removal feature feature. 3 | */ 4 | package org.tap4j.plugin.removeyamlifcorrupted; -------------------------------------------------------------------------------- /src/test/resources/org/tap4j/plugin/PublishersCombinationTest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judovana/tap-plugin/master/src/test/resources/org/tap4j/plugin/PublishersCombinationTest.zip -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - fix performance issues 2 | - TapTestResultResult and TapResult could be merged? 3 | - look for code duplication 4 | - remove TapStreamResult#getFailedTests2() 5 | - look for open tasks -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/nodata.properties: -------------------------------------------------------------------------------- 1 | header=TAP Results 2 | description=No TAP results available yet! 3 | content=Run a build with TAP plugin enabled to see results here 4 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapResult/contents.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |
${it.getContents(request.getParameter("f"))}
4 |
-------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // Build the plugin using https://github.com/jenkins-infra/pipeline-library 2 | buildPlugin(useContainerAgent: true, failFast: false, forkCount: '1C', configurations: [ 3 | [platform: 'linux', jdk: 17], 4 | ]) 5 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/nodata_es.properties: -------------------------------------------------------------------------------- 1 | header=Resultados TAP 2 | description=Todavia no hay resultados TAP disponibles! 3 | content=Ejecute una build con el plugin TAP habilitado para ver los resultados ac\ufffd 4 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/nodata_pt.properties: -------------------------------------------------------------------------------- 1 | header=Resultados TAP 2 | description=Sem resultados TAP dispon\ufffdveis ainda. 3 | content=Execute uma constru\ufffd\ufffdo com o plugin TAP habilitado para ver os resultados aqui 4 | -------------------------------------------------------------------------------- /src/main/webapp/help/TapPublisher/help-skipIfBuildNotOk.html: -------------------------------------------------------------------------------- 1 |
2 | If checked, the TAP post build step will be skipped if the build result status 3 | is not equal or better than UNSTABLE. i.e. if checked and a build step failed, 4 | the TAP publisher will be skipped. 5 |
6 | -------------------------------------------------------------------------------- /src/main/webapp/help-globalConfig.html: -------------------------------------------------------------------------------- 1 |
2 | This HTML fragment will be injected into the configuration screen 3 | when the user clicks the 'help' icon. See global.jelly for how the 4 | form decides which page to load. 5 | You can have any HTML fragment here. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/sidepanel.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapPublisher/help-name.html: -------------------------------------------------------------------------------- 1 |
2 | Help file for fields are discovered through a file name convention. This file is 3 | help for the "name" field. You can have arbitrary HTML here. You can write 4 | this file as a Jelly script if you need a dynamic content (but if you do so, change 5 | the extension to .jelly). 6 |
7 | -------------------------------------------------------------------------------- /src/main/webapp/help/TapPublisher/help-failIfNoResults.html: -------------------------------------------------------------------------------- 1 |
2 | This option fails the build when no test results (files) are found. So if your job is 3 | configured to look for *.tap files, but when the plug-in looks at the workspace it 4 | cannot find any .tap file, and this option is checked, then your build will be marked 5 | as failure. If you have empty .tap, they still count, and the plug-in will not fail the build. 6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/floatingBox.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | ${from.graphName} 6 |
7 | 8 | [Test result trend chart] 9 |
10 |
-------------------------------------------------------------------------------- /src/main/resources/META-INF/hudson.remoting.ClassFilter: -------------------------------------------------------------------------------- 1 | # Whitelists safe classes from https://github.com/tupilabs/tap4j 2 | # TODO: patch the library? 3 | org.tap4j.model.BailOut 4 | org.tap4j.model.Comment 5 | org.tap4j.model.Directive 6 | org.tap4j.model.Footer 7 | org.tap4j.model.Header 8 | org.tap4j.model.Plan 9 | org.tap4j.model.SkipPlan 10 | org.tap4j.model.TapElement 11 | org.tap4j.model.TapResult 12 | org.tap4j.model.TestResult 13 | org.tap4j.model.TestSet 14 | org.tap4j.model.Text 15 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapBuildAction/summary.jelly: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |

TAP Extended Test Results

10 |

This build contains ${it.result.testSets.size()} TAP test set(s), and ${it.result.parseErrorTestSets.size()} parse error(s).

11 |
12 |
-------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapProjectAction/nodata.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

${%header}

10 |

${%description}

11 |

${%content}

12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/tags/comments.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | # ${entry.text}
15 |
16 | 17 | 18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue17947/TestIssue17947.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.issue17947; 2 | 3 | import junit.framework.TestCase; 4 | 5 | import org.tap4j.model.TestSet; 6 | import org.tap4j.parser.Tap13Parser; 7 | 8 | public class TestIssue17947 extends TestCase { 9 | 10 | public void testSubtestsIssue17947() { 11 | // tap stream provided by issue reporter 12 | String tap = "1..3\n" + 13 | " 1..1\n" + 14 | " ok 1 - subtest 1\n" + 15 | "ok 1 - test 1\n" + 16 | " 1..4\n" + 17 | " ok 1 - subtest 1\n" + 18 | " ok 2 - subtest 2\n" + 19 | " ok 3 - subtest 3\n" + 20 | " ok 4 - subtest 4\n" + 21 | "ok 2 - test 2\n" + 22 | " 1..15\n" + 23 | " Bail out!\n" + 24 | " not ok 1 - test 3"; 25 | 26 | Tap13Parser parser = new Tap13Parser(true); 27 | TestSet ts = parser.parseTapStream(tap); 28 | System.out.println(ts.getNumberOfTestResults()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2023, Bruno P. Kinoshita 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/tags/status.jelly: -------------------------------------------------------------------------------- 1 | 2 | 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 | -------------------------------------------------------------------------------- /src/main/webapp/css/tap.css: -------------------------------------------------------------------------------- 1 | /* TAP Plug-in CSS styles */ 2 | 3 | .cyan { background-color: #00FFFF; } 4 | .cyan_text { color: #00FFFF; } 5 | .green { background-color: #97f477; } 6 | .green_text { color: #97f477; } 7 | .red { background-color: #f88676; } 8 | .red_text { color: #B40404; } 9 | .yellow { background-color: #FFBF00 } 10 | .yellow_text { color: #FFBF00 } 11 | .center { text-align: center; } 12 | .underline { text-decoration: underline; } 13 | .bold {font-weight: 700;} 14 | 15 | table.tap 16 | { 17 | border: 0px solid; 18 | border-collapse:collapse; 19 | } 20 | 21 | table.tap th 22 | { 23 | border: 1px solid #999; 24 | padding: 2px 4px; 25 | background-color: #eee; 26 | color: #000; 27 | } 28 | 29 | table.tap td 30 | { 31 | border: 1px solid #999; 32 | padding: 2px 4px; 33 | } 34 | 35 | table.tap td.yaml 36 | { 37 | border: 1px solid #999; 38 | padding: 0px 0px; 39 | } 40 | 41 | table.yaml 42 | { 43 | border: 0px solid; 44 | border-collapse:collapse; 45 | } 46 | 47 | table.yaml td.hidden 48 | { 49 | background-color: white; 50 | border: 0px solid; 51 | color: white; 52 | padding: 0px; 53 | } 54 | 55 | table.yaml td 56 | { 57 | border: 1px solid #ccc; 58 | padding: 2px 4px; 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/util/GraphHelperTest.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.util; 2 | 3 | 4 | import hudson.model.TopLevelItem; 5 | import org.htmlunit.html.HtmlPage; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.jvnet.hudson.test.Issue; 9 | import org.jvnet.hudson.test.JenkinsRule; 10 | import org.jvnet.hudson.test.recipes.LocalData; 11 | 12 | import static org.junit.Assert.assertTrue; 13 | 14 | 15 | public class GraphHelperTest { 16 | 17 | @Rule 18 | public JenkinsRule rule = new JenkinsRule(); 19 | 20 | @Issue("JENKINS-37623") 21 | @LocalData 22 | @Test 23 | public void renderTooltipsWithFailedBuilds() throws Exception { 24 | 25 | TopLevelItem project = rule.jenkins.getItem("testPipeline-randomly-no-data"); 26 | try (JenkinsRule.WebClient wc = rule.createWebClient()) { 27 | HtmlPage page = wc.getPage(project); 28 | 29 | // there should be a TAP result trend graph 30 | rule.assertXPath(page, "//img[@src='tapResults/graph']"); 31 | 32 | // check that tooltip is rendered for the last build 33 | rule.assertXPath(page, "//area[@title='1 Failure(s)' and @href='16/tapResults/']"); 34 | 35 | // check that build without TAP action recorded is excluded from graph 36 | assertTrue(page.getByXPath("//area[@href='7/tapResults/']").isEmpty()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/util/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2013 Bruno P. Kinoshita 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.tap4j.plugin.util; 25 | 26 | public final class Constants { 27 | 28 | public static final String TAP_DIR_NAME = "tap-master-files"; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/tags/directive.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | TODO 13 | 14 | ${directive.getReason()} 15 | 16 | 17 | 18 | 19 | 20 | SKIP 21 | 22 | ${directive.getReason()} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/model/TapTestResultResult/index.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

31 |
${it.toString()}
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/tags/yaml.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/model/ParseErrorTestSetMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012 Bruno P. Kinoshita 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.tap4j.plugin.model; 25 | 26 | /** 27 | * A test set map that failed to parse. 28 | * 29 | * @since 1.2.8 30 | */ 31 | public class ParseErrorTestSetMap extends TestSetMap { 32 | 33 | private static final long serialVersionUID = 6433486962160499201L; 34 | 35 | private final Throwable cause; 36 | 37 | /** 38 | * @param fileName TAP file name 39 | * @param cause {@link Throwable} that caused the TAP parse error 40 | */ 41 | public ParseErrorTestSetMap(String fileName, Throwable cause) { 42 | super(fileName, null); 43 | this.cause = cause; 44 | } 45 | 46 | /** 47 | * @return the cause 48 | */ 49 | public Throwable getCause() { 50 | return cause; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapResult/_index.groovy: -------------------------------------------------------------------------------- 1 | // Namespaces 2 | l = namespace("/lib/layout") 3 | tap = namespace("/org/tap4j/plugin/tags") 4 | st = namespace("jelly:stapler") 5 | j = namespace("jelly:core") 6 | i = namespace("jelly:fmt") 7 | t = namespace("/lib/hudson") 8 | f = namespace("/lib/form") 9 | d = namespace("jelly:define") 10 | 11 | 12 | l.layout(norefresh: "true", css: "/plugin/tap/css/tap.css") { 13 | st.include(it: it.owner, page: "sidepanel.jelly") 14 | l.main-panel() { 15 | h1("TAP Test Results") 16 | if(it.isEmptyTestSet()) 17 | else{ 18 | table(width: "100%") { 19 | tr() { 20 | td(width: "5%", "${it.testSets.size()} files") 21 | td("${it.getTotal()} tests, ${it.passed} ok, ${it.failed} not ok, ${it.skipped} skipped, ${it.toDo} ToDo, ${it.bailOuts} Bail Out!.") 22 | } 23 | } 24 | it.testSets.each() { map -> 25 | if(map.getTestSet().getPlan().isSkip()) { 26 | p("File:") { 27 | span(class: "underline") { 28 | a(href: "contents?f=${map.fileName}", map.fileName) 29 | } 30 | } 31 | } 32 | else{ 33 | p("File:") { 34 | span(class: "underline") { 35 | a(href: "contents?f=${map.fileName}", map.fileName) 36 | } 37 | } 38 | } 39 | table(width: "100%", class: "tap") { 40 | tr() { 41 | th() 42 | th("Number") 43 | th("Description") 44 | th("Directive") 45 | } 46 | map.testSet.tapLines.each() { tapLine -> 47 | tap.line(tapFile: map.fileName, tapLine: tapLine) 48 | } 49 | } 50 | br() 51 | } 52 | } 53 | if(it.hasParseErrors() == false) 54 | else{ 55 | h3("Parse errors") 56 | table(width: "100%", class: "tap") { 57 | tr() { 58 | th("File name") 59 | th("Cause") 60 | } 61 | it.parseErrorTestSets.each() { testSet -> 62 | tr() { 63 | td(testSet.fileName) 64 | td(testSet.cause) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/model/TestSetMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011 Bruno P. Kinoshita 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.tap4j.plugin.model; 25 | 26 | import java.io.Serializable; 27 | 28 | import org.tap4j.model.TestSet; 29 | 30 | 31 | /** 32 | * A map for TestSet with the file name, as there is no file name in the 33 | * original TestSet class. 34 | * 35 | * @since 1.0 36 | */ 37 | public class TestSetMap implements Serializable { 38 | 39 | private static final long serialVersionUID = 7300386936718557088L; 40 | 41 | private final String fileName; 42 | private final TestSet testSet; 43 | 44 | public TestSetMap( String fileName, TestSet testSet ) 45 | { 46 | this.fileName = fileName; 47 | this.testSet = testSet; 48 | } 49 | 50 | public String getFileName() 51 | { 52 | return this.fileName; 53 | } 54 | 55 | public TestSet getTestSet() 56 | { 57 | return this.testSet; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapTestResultAction/index.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapTestResultAction/sidepanel.jelly: -------------------------------------------------------------------------------- 1 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2013 Bruno P. Kinoshita 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.tap4j.plugin.util; 25 | 26 | import static org.junit.Assert.assertEquals; 27 | 28 | import org.junit.Test; 29 | 30 | 31 | /** 32 | * Tests for Util class. 33 | * @see Util 34 | */ 35 | public class TestUtil { 36 | 37 | private static final String UNIX_WS = "/home/workspace"; 38 | private static final String UNIX_FOLDER_1 = "/home/workspace/test/subdirectory/another/1.txt"; 39 | private static final String UNIX_FOLDER_2 = "/home/anotherfolder/test.txt"; 40 | 41 | private static final String WIN_WS = "c:\\home\\workspace"; 42 | private static final String WIN_FOLDER_1 = "c:\\home\\workspace\\test\\subdirectory\\another\\1.txt"; 43 | private static final String WIN_FOLDER_2 = "c:\\home\\anotherfolder\\test.txt"; 44 | 45 | @Test 46 | public void testNormalizeFolders() { 47 | assertEquals("Wrong normalization", "test/subdirectory/another/1.txt", Util.normalizeFolders(UNIX_WS, UNIX_FOLDER_1)); 48 | assertEquals("Wrong normalization", "/home/anotherfolder/test.txt", Util.normalizeFolders(UNIX_WS, UNIX_FOLDER_2)); 49 | assertEquals("Wrong normalization", "test/subdirectory/another/1.txt", Util.normalizeFolders(WIN_WS, WIN_FOLDER_1)); 50 | assertEquals("Wrong normalization", "c:/home/anotherfolder/test.txt", Util.normalizeFolders(WIN_WS, WIN_FOLDER_2)); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/tags/line.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ${tapLine.testNumber} 26 | ${tapLine.description} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Bail out! ${tapLine.reason} 38 | 39 | Bail out! ${tapLine.reason} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | # ${it.escapeHTML(tapLine.text)}
48 | 49 | 50 |
51 |
52 |
53 | 54 |
55 | 56 |
57 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue16647/TestIssue16647.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.issue16647; 2 | 3 | import hudson.Launcher; 4 | import hudson.model.BuildListener; 5 | import hudson.model.FreeStyleBuild; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.FreeStyleProject; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.ExecutionException; 13 | 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | import org.jvnet.hudson.test.TestBuilder; 18 | import org.tap4j.plugin.TapPublisher; 19 | import org.tap4j.plugin.TapTestResultAction; 20 | import org.tap4j.plugin.model.TapStreamResult; 21 | import org.tap4j.plugin.model.TapTestResultResult; 22 | 23 | public class TestIssue16647 { 24 | 25 | @Rule 26 | public JenkinsRule jenkins = new JenkinsRule(); 27 | 28 | @Test 29 | public void testDurationMs() throws IOException, InterruptedException, ExecutionException { 30 | FreeStyleProject project = jenkins.createProject(FreeStyleProject.class, "tap-bug-16647"); 31 | 32 | final String tap = "1..2\n" + 33 | "ok 1 - Input file opened\n" + 34 | "not ok 2 - First line of the input valid\n" + 35 | " ---\n" + 36 | " duration_ms: 100660.00\n" + 37 | " ...\n"; 38 | 39 | project.getBuildersList().add(new TestBuilder() { 40 | @Override 41 | public boolean perform(AbstractBuild build, Launcher arg1, 42 | BuildListener arg2) throws InterruptedException, IOException { 43 | build.getWorkspace().child("result.tap").write(tap,"UTF-8"); 44 | return true; 45 | } 46 | }); 47 | 48 | TapPublisher publisher = new TapPublisher( 49 | "result.tap", 50 | true, 51 | true, 52 | true, 53 | true, 54 | true, 55 | true, 56 | true, 57 | true, 58 | false, 59 | true, 60 | true, 61 | true, 62 | true, 63 | true, 64 | false); 65 | project.getPublishersList().add(publisher); 66 | project.save(); 67 | FreeStyleBuild build = (FreeStyleBuild) project.scheduleBuild2(0).get(); 68 | 69 | TapTestResultAction action = build.getAction(TapTestResultAction.class); 70 | TapStreamResult result = (TapStreamResult) action.getResult(); 71 | 72 | TapTestResultResult[] results = result.getChildren().toArray(new TapTestResultResult[0]); 73 | assertEquals(100.66f, results[1].getDuration(), /* delta */ 0.0001f); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/AbstractTapProjectAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011 Bruno P. Kinoshita 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.tap4j.plugin; 25 | 26 | import hudson.model.AbstractProject; 27 | import hudson.model.Action; 28 | import hudson.model.Job; 29 | 30 | /** 31 | * Base class for TAP Project action. 32 | * 33 | * @since 1.0 34 | */ 35 | public class AbstractTapProjectAction implements Action { 36 | 37 | public final Job job; 38 | 39 | @Deprecated 40 | public final AbstractProject project; 41 | 42 | public AbstractTapProjectAction(Job job) { 43 | this.job = job; 44 | project = job instanceof AbstractProject ? (AbstractProject) job : null; 45 | } 46 | 47 | public AbstractTapProjectAction(AbstractProject project) { 48 | this((Job) project); 49 | } 50 | 51 | public static final String URL_NAME = "tapResults"; 52 | public static final String ICON_NAME = "/plugin/tap/icons/tap-24.png"; 53 | 54 | /* (non-Javadoc) 55 | * @see hudson.model.Action#getDisplayName() 56 | */ 57 | public String getDisplayName() { 58 | return "TAP Extended Test Results"; 59 | } 60 | 61 | /* (non-Javadoc) 62 | * @see hudson.model.Action#getIconFileName() 63 | */ 64 | public String getIconFileName() { 65 | return ICON_NAME; 66 | } 67 | 68 | /* (non-Javadoc) 69 | * @see hudson.model.Action#getUrlName() 70 | */ 71 | public String getUrlName() { 72 | return URL_NAME; 73 | } 74 | 75 | public String getSearchUrl() { 76 | return URL_NAME; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue16964/TestIssue16964.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.issue16964; 2 | 3 | import hudson.Launcher; 4 | 5 | import hudson.model.BuildListener; 6 | import hudson.model.FreeStyleBuild; 7 | import hudson.model.AbstractBuild; 8 | import hudson.model.FreeStyleProject; 9 | 10 | import static org.junit.Assert.assertTrue; 11 | 12 | import java.io.IOException; 13 | import java.util.concurrent.ExecutionException; 14 | 15 | import javax.servlet.ServletException; 16 | 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import org.jvnet.hudson.test.JenkinsRule; 20 | import org.jvnet.hudson.test.TestBuilder; 21 | import org.tap4j.plugin.TapPublisher; 22 | import org.tap4j.plugin.TapResult; 23 | import org.tap4j.plugin.TapTestResultAction; 24 | 25 | 26 | public class TestIssue16964 { 27 | 28 | @Rule 29 | public JenkinsRule jenkins = new JenkinsRule(); 30 | 31 | @Test 32 | public void testFailTestEmptyResultsAndOldReports() throws IOException, ServletException, InterruptedException, ExecutionException { 33 | FreeStyleProject project = jenkins.createProject(FreeStyleProject.class, "tap-bug-16964"); 34 | 35 | final String tap = "1..4\n" + 36 | "ok 1 - Input file opened\n" + 37 | "not ok 2 - First line of the input valid.\n" + 38 | "More output from test 2. There can be\n" + 39 | "arbitrary number of lines for any output\n" + 40 | "so long as there is at least some kind\n" + 41 | "of whitespace at beginning of line.\n" + 42 | "ok 3 - Read the rest of the file\n" + 43 | "#TAP meta information\n" + 44 | "not ok 4 - Summarized correctly # TODO Not written yet\n" + 45 | "EOF"; 46 | 47 | project.getBuildersList().add(new TestBuilder() { 48 | @Override 49 | public boolean perform(AbstractBuild build, Launcher arg1, 50 | BuildListener arg2) throws InterruptedException, IOException { 51 | build.getWorkspace().child("result.tap").write(tap,"UTF-8"); 52 | return true; 53 | } 54 | }); 55 | 56 | TapPublisher publisher = new TapPublisher( 57 | "result.tap", 58 | true, 59 | true, 60 | true, 61 | true, 62 | true, 63 | true, 64 | true, 65 | true, 66 | false, 67 | true, 68 | false, 69 | false, 70 | false, 71 | false, 72 | false); 73 | project.getPublishersList().add(publisher); 74 | project.save(); 75 | FreeStyleBuild build = (FreeStyleBuild) project.scheduleBuild2(0).get(); 76 | 77 | TapTestResultAction action = build.getAction(TapTestResultAction.class); 78 | TapResult testResult = action.getTapResult(); 79 | 80 | assertTrue(testResult.getFailed() == 2); 81 | assertTrue(testResult.getPassed() == 2); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapResult/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

TAP Extended Test Results

18 | 19 | 20 | 21 |

Empty test set

22 |
23 | 24 | ${it.updateStats()} 25 | 26 | 27 | 28 | 29 | 30 |
${it.testSets.size()} files${it.getTotal()} tests, ${it.passed} ok, ${it.failed} not ok, ${it.skipped} skipped, ${it.toDo} ToDo, ${it.bailOuts} Bail Out!
31 | 32 | 33 |

Note: Displaying only failures

34 |
35 | 36 | 37 | 38 | 39 |

File: ${map.fileName} (Skipped)

40 |
41 | 42 |

File: ${map.fileName}

43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
NumberDescriptionDirective
57 |
58 |
59 | 60 |
61 |
62 | 63 |

Parse errors

64 | 65 | 66 | 67 |

No parse errors found

68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
File nameCause
${map.fileName}${map.cause}
82 |
83 |
84 | 85 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/TapPublisher/config.jelly: -------------------------------------------------------------------------------- 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/util/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2013 Bruno P. Kinoshita 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.tap4j.plugin.util; 25 | 26 | import org.tap4j.model.Directive; 27 | import org.tap4j.model.TestResult; 28 | import org.tap4j.util.DirectiveValues; 29 | import org.tap4j.util.StatusValues; 30 | 31 | /** 32 | * Utility methods used by tap-plugin. 33 | */ 34 | public final class Util { 35 | 36 | private Util() {} 37 | 38 | /** 39 | * Normalizes a folder path in relation to the workspace path. 40 | *

41 | * A folder that is subdirectory of workspace will return only the difference. 42 | * It means that if the workspace is /home/workspace and the folder we want 43 | * to normalize is /home/workspace/job-1/test.txt, then the return will be 44 | * job-1/test.txt. 45 | * 46 | * @param workspace workspace path 47 | * @param relative relative path 48 | * @return normalized path 49 | */ 50 | public static String normalizeFolders(String workspace, String relative) { 51 | workspace = workspace.replaceAll("\\\\", "\\/"); 52 | relative = relative.replaceAll("\\\\", "\\/"); 53 | if (relative.length() > workspace.length() && relative.contains(workspace)) { 54 | String temp = relative.substring(workspace.length()); 55 | if (temp.startsWith("/") || temp.startsWith("\\")) 56 | temp = temp.substring(1); 57 | return temp; 58 | } 59 | return relative; 60 | } 61 | 62 | public static boolean isSkipped(TestResult testResult) { 63 | boolean r = false; 64 | Directive directive = testResult.getDirective(); 65 | if (directive != null 66 | && directive.getDirectiveValue() == DirectiveValues.SKIP) { 67 | r = true; 68 | } 69 | return r; 70 | } 71 | 72 | public static boolean isTodo(TestResult testResult) { 73 | boolean r = false; 74 | Directive directive = testResult.getDirective(); 75 | if (directive != null 76 | && directive.getDirectiveValue() == DirectiveValues.TODO) { 77 | r = true; 78 | } 79 | return r; 80 | } 81 | 82 | public static boolean isFailure(TestResult testResult, Boolean todoIsFailure) { 83 | boolean r = false; 84 | Directive directive = testResult.getDirective(); 85 | StatusValues status = testResult.getStatus(); 86 | if (directive != null) { 87 | if(directive.getDirectiveValue() == DirectiveValues.TODO && todoIsFailure != null && todoIsFailure) { 88 | r = true; 89 | } 90 | } else if (status == StatusValues.NOT_OK) { 91 | r = true; 92 | } 93 | return r; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/model/TapAttachment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012 Bruno P. Kinoshita 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.tap4j.plugin.model; 25 | 26 | import java.util.Map; 27 | 28 | /** 29 | * @since 0.1 30 | */ 31 | public class TapAttachment { 32 | 33 | private final String fileName; 34 | private final byte[] content; 35 | private final int size; 36 | private final String fileType; 37 | 38 | /** 39 | * @param fileName TAP file name 40 | * @param content byte content 41 | * @param size attachment size 42 | * @param fileType file mime type 43 | */ 44 | public TapAttachment(String fileName, byte[] content, int size, String fileType) { 45 | super(); 46 | this.fileName = fileName; 47 | this.content = content; 48 | this.size = size; 49 | this.fileType = fileType; 50 | } 51 | 52 | /** 53 | * @param content byte content 54 | * @param diagnostics TAP diagnostics 55 | */ 56 | public TapAttachment(byte[] content, Map diagnostics) { 57 | super(); 58 | this.content = content; 59 | int size = -1; 60 | String fileType = ""; 61 | String fileName = "tapAttachment"; 62 | for (Map.Entry entry : diagnostics.entrySet()) { 63 | final String key = entry.getKey(); 64 | final Object value = entry.getValue(); 65 | if (value instanceof Map == Boolean.FALSE) { 66 | if (key.equalsIgnoreCase("file-size")) { 67 | try { 68 | size = (int) Long.parseLong(diagnostics.get(key).toString()); 69 | } catch (NumberFormatException nfe) { 70 | // Do nothing 71 | } 72 | } else if (key.equalsIgnoreCase("file-type")) { 73 | fileType = (String) diagnostics.get(key); 74 | } else if (key.equalsIgnoreCase("file-name")) { 75 | fileName = (String) diagnostics.get(key); 76 | } 77 | } 78 | } 79 | this.size = size; 80 | this.fileType = fileType; 81 | this.fileName = fileName; 82 | } 83 | 84 | /** 85 | * @return the fileName 86 | */ 87 | public String getFileName() { 88 | return fileName; 89 | } 90 | 91 | /** 92 | * @return the content 93 | */ 94 | public byte[] getContent() { 95 | return content; 96 | } 97 | 98 | /** 99 | * @return the size 100 | */ 101 | public int getSize() { 102 | return size; 103 | } 104 | 105 | /** 106 | * @return the fileType 107 | */ 108 | public String getFileType() { 109 | return fileType; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/stripsingleparent/TestStripSingleParent.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.stripsingleparent; 2 | 3 | import hudson.Launcher; 4 | import hudson.model.BuildListener; 5 | import hudson.model.FreeStyleBuild; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.FreeStyleProject; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.ExecutionException; 13 | 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | import org.jvnet.hudson.test.TestBuilder; 18 | import org.tap4j.plugin.TapPublisher; 19 | import org.tap4j.plugin.TapResult; 20 | import org.tap4j.plugin.TapTestResultAction; 21 | 22 | /** 23 | * At least basic tests for strip single parent configuration option. 24 | * 25 | * @author Jakub Podlesak 26 | */ 27 | public class TestStripSingleParent { 28 | 29 | @Rule 30 | public JenkinsRule jenkins = new JenkinsRule(); 31 | 32 | @Test 33 | public void testNoEffect() throws IOException, InterruptedException, ExecutionException { 34 | 35 | final String tap = "1..2\n" + 36 | " 1..3\n" + 37 | " ok 1 1.1\n" + 38 | " ok 2 1.2\n" + 39 | " ok 3 1.3\n" + 40 | "ok 1 - 1\n" + 41 | "ok 2 - 1\n"; 42 | 43 | _test(tap, 2); 44 | } 45 | 46 | @Test 47 | public void testStripFirstLevel() throws IOException, InterruptedException, ExecutionException { 48 | 49 | final String tap = "1..1\n" + 50 | " 1..3\n" + 51 | " ok 1 1.1\n" + 52 | " ok 2 1.2\n" + 53 | " ok 3 1.3\n" + 54 | "ok 1 - 1\n"; 55 | 56 | _test(tap, 3); 57 | } 58 | 59 | @Test 60 | public void testStripSecondLevel() throws IOException, InterruptedException, ExecutionException { 61 | 62 | final String tap = 63 | "1..1\n" + 64 | " 1..1\n" + 65 | " 1..3\n" + 66 | " ok 1 1.1.1\n" + 67 | " ok 2 1.1.2\n" + 68 | " ok 3 1.1.3\n" + 69 | " ok 1.1 - 1\n" + 70 | "ok 1 - 1\n"; 71 | 72 | _test(tap, 3); 73 | } 74 | 75 | private void _test(final String tap, int expectedTotal) throws IOException, InterruptedException, ExecutionException { 76 | FreeStyleProject project = jenkins.createProject(FreeStyleProject.class, "strip-single-parents"); 77 | 78 | project.getBuildersList().add(new TestBuilder() { 79 | @Override 80 | public boolean perform(AbstractBuild build, Launcher arg1, 81 | BuildListener arg2) throws InterruptedException, IOException { 82 | build.getWorkspace().child("result.tap").write(tap,"UTF-8"); 83 | return true; 84 | } 85 | }); 86 | 87 | TapPublisher publisher = new TapPublisher( 88 | "result.tap", // test results 89 | true, // failIfNoResults 90 | true, // failedTestsMarkBuildAsFailure 91 | false, // outputTapToConsole 92 | true, // enableSubtests 93 | true, // discardOldReports 94 | true, // todoIsFailure 95 | true, // includeCommentDiagnostics 96 | true, // validateNumberOfTests 97 | true, // planRequired 98 | false, // verbose 99 | true, // showOnlyFailures 100 | true, // stripSingleParents 101 | false, // flattenTapResult 102 | false, 103 | false); 104 | 105 | project.getPublishersList().add(publisher); 106 | project.save(); 107 | FreeStyleBuild build = (FreeStyleBuild) project.scheduleBuild2(0).get(); 108 | 109 | TapTestResultAction action = build.getAction(TapTestResultAction.class); 110 | TapResult testResult = action.getTapResult(); 111 | 112 | assertEquals(expectedTotal, testResult.getPassed()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/TapBuildAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011 Bruno P. Kinoshita 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.tap4j.plugin; 25 | 26 | import hudson.model.Action; 27 | import hudson.model.Run; 28 | import org.kohsuke.stapler.StaplerProxy; 29 | 30 | import javax.annotation.Nullable; 31 | import java.io.Serializable; 32 | 33 | /** 34 | * TAP Build action with TAP results. 35 | * 36 | * @since 1.0 37 | */ 38 | public class TapBuildAction implements Action, Serializable, StaplerProxy { 39 | 40 | private static final long serialVersionUID = 520981690971849654L; 41 | public static final String URL_NAME = "tapResults"; 42 | public static final String ICON_NAME = "/plugin/tap/icons/tap-24.png"; 43 | public static final String DISPLAY_NAME = "TAP Extended Test Results"; 44 | 45 | private final transient Run build; 46 | 47 | private TapResult result; 48 | 49 | public TapBuildAction(Run build, TapResult result) { 50 | super(); 51 | this.build = build; 52 | this.result = result; 53 | } 54 | 55 | /* 56 | * (non-Javadoc) 57 | * 58 | * @see org.kohsuke.stapler.StaplerProxy#getTarget() 59 | */ 60 | public Object getTarget() { 61 | return this.result; 62 | } 63 | 64 | /* 65 | * (non-Javadoc) 66 | * 67 | * @see hudson.model.Action#getDisplayName() 68 | */ 69 | public String getDisplayName() { 70 | return DISPLAY_NAME; 71 | } 72 | 73 | /* 74 | * (non-Javadoc) 75 | * 76 | * @see hudson.model.Action#getIconFileName() 77 | */ 78 | public String getIconFileName() { 79 | return ICON_NAME; 80 | } 81 | 82 | /* 83 | * (non-Javadoc) 84 | * 85 | * @see hudson.model.Action#getUrlName() 86 | */ 87 | public String getUrlName() { 88 | return URL_NAME; 89 | } 90 | 91 | /** 92 | * @return the build 93 | */ 94 | @Nullable 95 | public Run getBuild() { 96 | return this.build; 97 | } 98 | 99 | public TapResult getResult() { 100 | return this.result; 101 | } 102 | 103 | public TapResult getPreviousResult() { 104 | TapResult previousResult = null; 105 | 106 | TapBuildAction previousAction = this.getPreviousAction(); 107 | 108 | if (previousAction != null) { 109 | previousResult = previousAction.getResult(); 110 | } 111 | 112 | return previousResult; 113 | } 114 | 115 | public TapBuildAction getPreviousAction() { 116 | TapBuildAction previousAction = null; 117 | 118 | if (this.build != null) { 119 | Run previousBuild = this.build.getPreviousBuild(); 120 | if (previousBuild != null) { 121 | previousAction = previousBuild.getAction(TapBuildAction.class); 122 | } 123 | } 124 | 125 | return previousAction; 126 | } 127 | 128 | public void mergeResult(TapResult other) { 129 | result = result.copyWithExtraTestSets(other.getTestSets()); 130 | result.tally(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/PublishersCombinationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2009, Yahoo!, 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.tap4j.plugin; 25 | 26 | import hudson.model.Project; 27 | import hudson.model.TopLevelItem; 28 | import org.htmlunit.html.HtmlPage; 29 | import org.junit.Rule; 30 | import org.junit.Test; 31 | import org.junit.jupiter.api.Disabled; 32 | import org.jvnet.hudson.test.Issue; 33 | import org.jvnet.hudson.test.JenkinsRule; 34 | import org.jvnet.hudson.test.recipes.LocalData; 35 | 36 | @Disabled("Failing on newer versions of Jenkins, 2.414+, but working when ran locally. TODO: fix it.") 37 | public class PublishersCombinationTest { 38 | 39 | // @Rule 40 | public JenkinsRule rule = new JenkinsRule(); 41 | 42 | // @Issue("JENKINS-29649") 43 | // @LocalData 44 | // @Test 45 | public void combinedWithJunitBasic() throws Exception { 46 | 47 | Project project = (Project) rule.jenkins.getItem("multiPublish"); 48 | 49 | // Validate that there are test results where I expect them to be: 50 | try (JenkinsRule.WebClient wc = rule.createWebClient()) { 51 | // On the project page: 52 | HtmlPage projectPage = wc.getPage(project); 53 | 54 | assertJunitPart(projectPage, 3, 4); 55 | assertTapPart(projectPage, 3); 56 | } 57 | } 58 | 59 | // @Issue("JENKINS-29649") 60 | // @LocalData 61 | // @Test 62 | public void combinedWithJunitPipeline() throws Exception { 63 | 64 | TopLevelItem project = rule.jenkins.getItem("testPipeline"); 65 | 66 | // Validate that there are test results where I expect them to be: 67 | try (JenkinsRule.WebClient wc = rule.createWebClient()) { 68 | // On the project page: 69 | HtmlPage projectPage = wc.getPage(project); 70 | 71 | assertJunitPart(projectPage, 15, 7); 72 | assertTapPart(projectPage, 15); 73 | } 74 | } 75 | 76 | private void assertJunitPart(HtmlPage page, int buildNumber, int testsTotal) { 77 | 78 | // we should have a link that reads "Latest Test Result" 79 | // that link should go to http://localhost:8080/job/breakable/lastBuild/testReport/ 80 | rule.assertXPath(page, "//a[@href='lastCompletedBuild/testReport/']"); 81 | rule.assertXPathValue(page, "//a[@href='lastCompletedBuild/testReport/']", "Latest Test Result"); 82 | rule.assertXPathValueContains(page, "//a[@href='lastCompletedBuild/testReport/']", "Latest Test Result"); 83 | 84 | // there should be a test result trend graph 85 | rule.assertXPath(page, "//img[@src='test/trend']"); 86 | 87 | // superficially assert that the number of tests was correct 88 | rule.assertXPath( 89 | page, 90 | String.format("//area[@title='#%1$s: %2$s tests' and @href='%1$s/testReport/']", buildNumber, testsTotal) 91 | ); 92 | } 93 | 94 | private void assertTapPart(HtmlPage page, int buildNumber) { 95 | 96 | // there should be a TAP result trend graph 97 | rule.assertXPath(page, "//img[@src='tapResults/graph']"); 98 | 99 | // superficially assert that the number of tests was correct 100 | rule.assertXPath(page, String.format("//area[@title='1 Skip(s)' and @href='%s/tapResults/']", buildNumber)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/issue21456/TestIssue21456.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010-2023 Bruno P. Kinoshita 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.tap4j.plugin.issue21456; 25 | 26 | import hudson.Launcher; 27 | import hudson.model.AbstractBuild; 28 | import hudson.model.BuildListener; 29 | import hudson.model.FreeStyleProject; 30 | import hudson.tasks.Shell; 31 | import org.junit.Rule; 32 | import org.junit.Test; 33 | import org.jvnet.hudson.test.JenkinsRule; 34 | import org.jvnet.hudson.test.TestBuilder; 35 | import org.tap4j.plugin.TapPublisher; 36 | import org.tap4j.plugin.TapTestResultAction; 37 | 38 | import java.io.IOException; 39 | import java.util.Objects; 40 | import java.util.concurrent.ExecutionException; 41 | 42 | import static org.junit.Assert.assertNull; 43 | 44 | /** 45 | * Tests that Jenkins can be configured to skip using the TAP Plug-in 46 | * when the build fails. 47 | */ 48 | public class TestIssue21456 { 49 | 50 | public @Rule JenkinsRule jenkins = new JenkinsRule(); 51 | 52 | @Test 53 | public void testDurationMs() throws IOException, InterruptedException, ExecutionException { 54 | final FreeStyleProject project = jenkins.createFreeStyleProject("tap-bug-21456"); 55 | 56 | final String tap = String.join("\n", 57 | "1..2", 58 | "ok 1 - Input file opened", 59 | "not ok 2 - First line of the input valid", 60 | " ---", 61 | " duration_ms: 100660.00", 62 | " ..."); 63 | 64 | project 65 | .getBuildersList() 66 | .add(new TestBuilder() { 67 | @Override 68 | public boolean perform(AbstractBuild build, Launcher arg1, 69 | BuildListener arg2) throws InterruptedException, IOException { 70 | Objects.requireNonNull(build.getWorkspace()).child("result.tap").write(tap, "UTF-8"); 71 | return true; 72 | } 73 | }); 74 | 75 | project.getBuildersList().add(new Shell("exit 255")); 76 | 77 | final TapPublisher publisher = new TapPublisher( 78 | "result.tap", // testResults 79 | true, // failIfNoResults 80 | true, // failedTestsMarkBuildAsFailure 81 | true, // outputTapToConsole 82 | true, // enableSubtests 83 | true, // discardOldReports 84 | true, // todoIsFailure 85 | true, // includeCommentDiagnostics 86 | true, // validateNumberOfTests 87 | false, // planRequired 88 | true, // verbose 89 | false, // showOnlyFailures 90 | false, // stripSingleParents 91 | false, // flattenTapResult 92 | true, // removeYamlIfCorrupted 93 | true); //skipIfBuildNotOk <-- that's the one we are testing here 94 | project.getPublishersList().add(publisher); 95 | project.save(); 96 | 97 | TapTestResultAction action = project 98 | .scheduleBuild2(0) 99 | .get() 100 | .getAction(TapTestResultAction.class); 101 | 102 | assertNull( 103 | "Not supposed to have a TAP action. Should have skipped a failed build!", 104 | action); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/jenkins_cert_3190/TestXssTapFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2023 Bruno P. Kinoshita 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.tap4j.plugin.jenkins_cert_3190; 25 | 26 | import hudson.model.FreeStyleProject; 27 | import hudson.model.Run; 28 | import hudson.tasks.Shell; 29 | import org.htmlunit.CollectingAlertHandler; 30 | import org.junit.Rule; 31 | import org.junit.Test; 32 | import org.jvnet.hudson.test.Issue; 33 | import org.jvnet.hudson.test.JenkinsRule; 34 | import org.tap4j.plugin.TapPublisher; 35 | import org.xml.sax.SAXException; 36 | 37 | import java.io.IOException; 38 | import java.util.List; 39 | import java.util.concurrent.ExecutionException; 40 | import java.util.concurrent.Future; 41 | 42 | import static org.junit.Assert.assertEquals; 43 | 44 | /** 45 | * Prevent a case where TAP files with JavaScript code are 46 | * evaluated by the plug-in. 47 | * 48 | * @since 2.4.1 49 | */ 50 | @Issue("3190") 51 | public class TestXssTapFile { 52 | 53 | @Rule 54 | public JenkinsRule j = new JenkinsRule(); 55 | 56 | @Test 57 | public void testTapFileXss() throws IOException, SAXException, ExecutionException, InterruptedException { 58 | final FreeStyleProject project = j.createFreeStyleProject(); 59 | 60 | // We can add more scenarios where XSS must be prevented in 61 | // the TAP stream. Just modify the file below, trying to use 62 | // the next N in `alert(N)` so that it is easier to detect 63 | // where it is coming from. 64 | final Shell shell = new Shell("echo \"\n" + 65 | "1..4 # \n" + 66 | "ok 1 - OK # \n" + 67 | " ---\n" + 68 | " extensions:\n" + 69 | " injected\n" + 70 | " ...\n" + 71 | "# \n" + 72 | "not ok 2 - failed! \n" + 73 | "ok 3 - # SKIP \n" + 74 | "ok 4 # TODO \n" + 75 | "\" > payload.tap\n"); 76 | project.getBuildersList().add(shell); 77 | 78 | final TapPublisher tapPublisher = new TapPublisher( 79 | "**/*.tap", 80 | true, 81 | true, 82 | true, 83 | true, 84 | true, 85 | true, 86 | true, 87 | true, 88 | true, 89 | true, 90 | false, 91 | true, 92 | true, 93 | true, 94 | true 95 | ); 96 | project.getPublishersList().add(tapPublisher); 97 | 98 | project.save(); 99 | 100 | final CollectingAlertHandler alertHandler = new CollectingAlertHandler(); 101 | try (final JenkinsRule.WebClient wc = j.createWebClient()) { 102 | wc.setThrowExceptionOnFailingStatusCode(false); 103 | wc.setAlertHandler(alertHandler); 104 | 105 | Future f = project.scheduleBuild2(0); 106 | Run build = (Run) f.get(); 107 | 108 | wc.goTo("job/" + project.getName() + "/" + build.getNumber() + "/tapResults/"); 109 | 110 | final List alerts = alertHandler.getCollectedAlerts(); 111 | 112 | assertEquals("You got a JS alert, look out for XSS!", 0, alerts.size()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/resources/org/tap4j/plugin/tap-master-files/subtest-sample.tap: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # Subtest: test/tap/access.js 3 | # Subtest: setup 4 | ok 1 - /workdir/npm-module/test/tap/access made successfully 5 | ok 2 - registry mocked successfully 6 | ok 3 - wrote package.json 7 | 1..3 8 | ok 1 - setup # time=68.43ms 9 | 10 | # Subtest: npm access public on current package 11 | ok 1 - npm access 12 | ok 2 - exited OK 13 | ok 3 - no error output 14 | 1..3 15 | ok 2 - npm access public on current package # time=543.472ms 16 | 17 | # Subtest: npm access public when no package passed and no package.json 18 | ok 1 - npm access 19 | ok 2 - should match pattern provided 20 | 1..2 21 | ok 3 - npm access public when no package passed and no package.json # time=457.195ms 22 | 23 | # Subtest: npm access public when no package passed and invalid package.json 24 | ok 1 - npm access 25 | ok 2 - should match pattern provided 26 | 1..2 27 | ok 4 - npm access public when no package passed and invalid package.json # time=458.715ms 28 | 29 | # Subtest: npm access restricted on current package 30 | ok 1 - npm access 31 | ok 2 - exited OK 32 | ok 3 - no error output 33 | 1..3 34 | ok 5 - npm access restricted on current package # time=548.049ms 35 | 36 | # Subtest: npm access on named package 37 | ok 1 - npm access 38 | ok 2 - exited OK 39 | ok 3 - no error output 40 | 1..3 41 | ok 6 - npm access on named package # time=519.159ms 42 | 43 | # Subtest: npm change access on unscoped package 44 | ok 1 - exited with Error 45 | ok 2 - should match pattern provided 46 | 1..2 47 | ok 7 - npm change access on unscoped package # time=461.458ms 48 | 49 | # Subtest: npm access grant read-only 50 | ok 1 - npm access grant 51 | ok 2 - exited with Error 52 | 1..2 53 | ok 8 - npm access grant read-only # time=517.935ms 54 | 55 | # Subtest: npm access grant read-write 56 | ok 1 - npm access grant 57 | ok 2 - exited with Error 58 | 1..2 59 | ok 9 - npm access grant read-write # time=525.574ms 60 | 61 | # Subtest: npm access grant others 62 | ok 1 - exited with Error 63 | ok 2 - should match pattern provided 64 | ok 3 - should match pattern provided 65 | 1..3 66 | ok 10 - npm access grant others # time=473.668ms 67 | 68 | # Subtest: npm access revoke 69 | ok 1 - npm access grant 70 | ok 2 - exited with Error 71 | 1..2 72 | ok 11 - npm access revoke # time=529.943ms 73 | 74 | # Subtest: npm access ls-packages with no team 75 | ok 1 - npm access ls-packages 76 | ok 2 - should be equivalent 77 | 1..2 78 | ok 12 - npm access ls-packages with no team # time=512.118ms 79 | 80 | # Subtest: npm access ls-packages on team 81 | ok 1 - npm access ls-packages 82 | ok 2 - should be equivalent 83 | 1..2 84 | ok 13 - npm access ls-packages on team # time=530.445ms 85 | 86 | # Subtest: npm access ls-packages on org 87 | ok 1 - npm access ls-packages 88 | ok 2 - should be equivalent 89 | 1..2 90 | ok 14 - npm access ls-packages on org # time=538.885ms 91 | 92 | # Subtest: npm access ls-packages on user 93 | ok 1 - npm access ls-packages 94 | ok 2 - should be equivalent 95 | 1..2 96 | ok 15 - npm access ls-packages on user # time=509.704ms 97 | 98 | # Subtest: npm access ls-packages with no package specified or package.json 99 | ok 1 - npm access ls-packages 100 | ok 2 - should be equivalent 101 | 1..2 102 | ok 16 - npm access ls-packages with no package specified or package.json # time=506.721ms 103 | 104 | # Subtest: npm access ls-collaborators on current 105 | ok 1 - npm access ls-collaborators 106 | ok 2 - should be equivalent 107 | 1..2 108 | ok 17 - npm access ls-collaborators on current # time=516.851ms 109 | 110 | # Subtest: npm access ls-collaborators on package 111 | ok 1 - npm access ls-collaborators 112 | ok 2 - should be equivalent 113 | 1..2 114 | ok 18 - npm access ls-collaborators on package # time=508.724ms 115 | 116 | # Subtest: npm access ls-collaborators on current w/user filter 117 | ok 1 - npm access ls-collaborators 118 | ok 2 - should be equivalent 119 | 1..2 120 | ok 19 - npm access ls-collaborators on current w/user filter # time=501.361ms 121 | 122 | # Subtest: npm access edit 123 | ok 1 - exited with Error 124 | ok 2 - should match pattern provided 125 | 1..2 126 | ok 20 - npm access edit # time=444.677ms 127 | 128 | # Subtest: npm access blerg 129 | ok 1 - exited with Error 130 | ok 2 - should match pattern provided 131 | 1..2 132 | ok 21 - npm access blerg # time=443.858ms 133 | 134 | # Subtest: cleanup 135 | ok 1 - cleaned up 136 | 1..1 137 | ok 22 - cleanup # time=3.574ms 138 | 139 | 1..22 140 | # time=10166.406ms 141 | ok 1 - test/tap/access.js # time=10398.064ms 142 | 143 | 1..1 144 | # time=10431.957ms -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/util/DiagnosticUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010 Bruno P. Kinoshita 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.tap4j.plugin.util; 25 | 26 | import hudson.Functions; 27 | import org.apache.commons.lang.StringEscapeUtils; 28 | 29 | import java.util.Arrays; 30 | import java.util.List; 31 | import java.util.Locale; 32 | import java.util.Map; 33 | import java.util.Map.Entry; 34 | import java.util.Set; 35 | 36 | /** 37 | * Used to create YAML view. FIXME: Figure out another way to write HTML (send JSON to Stapler/Groovy/etc.?). 38 | * 39 | * @since 1.0 40 | * @deprecated To be soon removed by something easier to maintain (return JSON to JS?). 41 | */ 42 | public class DiagnosticUtil { 43 | 44 | private enum RENDER_TYPE { 45 | TEXT, IMAGE 46 | } 47 | 48 | private static final String INNER_TABLE_HEADER = "\n\n"; 49 | 50 | private static final String INNER_TABLE_FOOTER = "
\n\n"; 51 | 52 | private static final int MAX_DEPTH = 3; 53 | 54 | private DiagnosticUtil() { 55 | super(); 56 | } 57 | 58 | public static String createDiagnosticTable(String tapFile, Map diagnostic) { 59 | StringBuilder sb = new StringBuilder(); 60 | createDiagnosticTableRecursively(tapFile, null, diagnostic, sb, 1); 61 | return sb.toString(); 62 | } 63 | 64 | @SuppressWarnings({ "rawtypes", "unchecked" }) 65 | private static void createDiagnosticTableRecursively( 66 | String tapFile, 67 | String parentKey, 68 | Map diagnostic, 69 | StringBuilder sb, 70 | int depth) { 71 | 72 | sb.append(INNER_TABLE_HEADER); 73 | 74 | final RENDER_TYPE renderType = getMapEntriesRenderType(diagnostic); 75 | final List parentKeys = Arrays.asList("files", "extensions"); 76 | 77 | for (Entry entry : diagnostic.entrySet()) { 78 | final String key = entry.getKey(); 79 | final Object value = entry.getValue(); 80 | 81 | sb.append(""); 82 | 83 | sb.append(" ".repeat(Math.max(0, depth))); 84 | sb.append("").append(StringEscapeUtils.escapeHtml(key)).append(""); 85 | 86 | if(renderType == RENDER_TYPE.IMAGE && key.equals("File-Content")) { 87 | final Object o = diagnostic.get("File-Name"); 88 | final String fileName = (o instanceof String) ? (String) o : "attachment"; 89 | final boolean useParentKey = (parentKey != null && depth > MAX_DEPTH && !parentKeys.contains(parentKey.trim().toLowerCase(Locale.ROOT))); 90 | final String downloadKey = useParentKey ? parentKey : fileName; 91 | Arrays.asList( 92 | "", 97 | StringEscapeUtils.escapeHtml(fileName), 98 | "" 99 | ).forEach(sb::append); 100 | } else if (renderType == RENDER_TYPE.TEXT && value instanceof java.util.Map) { 101 | sb.append(" "); 102 | createDiagnosticTableRecursively(tapFile, key, (java.util.Map) value, sb, (depth + 1)); 103 | } else { 104 | sb.append("

").append(org.apache.commons.lang.StringEscapeUtils.escapeHtml(value.toString())).append("
"); 105 | } 106 | sb.append(""); 107 | } 108 | 109 | sb.append(INNER_TABLE_FOOTER); 110 | } 111 | 112 | private static RENDER_TYPE getMapEntriesRenderType(Map diagnostic) { 113 | if (diagnostic.containsKey("File-Type")) { 114 | if (diagnostic.containsKey("File-Location") || diagnostic.containsKey("File-Content")) { 115 | return RENDER_TYPE.IMAGE; 116 | } 117 | } 118 | return RENDER_TYPE.TEXT; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/removeyamlifcorrupted/TestRemoveYamlIfCorrupted.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.removeyamlifcorrupted; 2 | 3 | import hudson.Launcher; 4 | import hudson.model.BuildListener; 5 | import hudson.model.FreeStyleBuild; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.FreeStyleProject; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.ExecutionException; 13 | 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.jvnet.hudson.test.JenkinsRule; 17 | import org.jvnet.hudson.test.TestBuilder; 18 | import org.tap4j.plugin.TapPublisher; 19 | import org.tap4j.plugin.TapResult; 20 | import org.tap4j.plugin.TapTestResultAction; 21 | 22 | /** 23 | * Tests for remove corrupted yaml configuration option. 24 | * 25 | * @author Jakub Podlesak 26 | */ 27 | public class TestRemoveYamlIfCorrupted { 28 | 29 | @Rule 30 | public JenkinsRule jenkins = new JenkinsRule(); 31 | 32 | @Test 33 | public void testYamlStripped() throws IOException, InterruptedException, ExecutionException { 34 | 35 | final String tap = "1..1\n" 36 | + "not ok 1 '0' should not be in window anymore\n" 37 | + " ---\n" 38 | + " type: AssertionError\n" 39 | + " message: '0' should not be in window anymore\n" 40 | + " code: ~\n" 41 | + " errno: ~\n" 42 | + " file: /workdir/npm-module/test/window/frame.js\n" 43 | + " line: 416\n" 44 | + " column: 10\n" 45 | + " stack:\n" 46 | + " - |\n" 47 | + " Object.remove frame (/workdir/npm-module/test/window/frame.js:416:10)\n" 48 | + " - |\n" 49 | + " Object. (/workdir/npm-module/node_modules/nodeunit/lib/core.js:236:16)\n" 50 | + " - |\n" 51 | + " Object. (/workdir/npm-module/node_modules/nodeunit/lib/core.js:236:16)\n" 52 | + " - |\n" 53 | + " /workdir/npm-module/node_modules/nodeunit/lib/core.js:236:16\n" 54 | + " - |\n" 55 | + " Object.runTest (/workdir/npm-module/node_modules/nodeunit/lib/core.js:70:9)\n" 56 | + " - |\n" 57 | + " /workdir/npm-module/node_modules/nodeunit/lib/core.js:118:25\n" 58 | + " - |\n" 59 | + " /workdir/npm-module/node_modules/nodeunit/deps/async.js:513:13\n" 60 | + " - |\n" 61 | + " iterate (/workdir/npm-module/node_modules/nodeunit/deps/async.js:123:13)\n" 62 | + " - |\n" 63 | + " /workdir/npm-module/node_modules/nodeunit/deps/async.js:134:25\n" 64 | + " - |\n" 65 | + " /workdir/npm-module/node_modules/nodeunit/deps/async.js:515:17\n" 66 | + " - |\n" 67 | + " Immediate. (/workdir/npm-module/node_modules/nodeunit/lib/types.js:146:17)\n" 68 | + " - |\n" 69 | + " runCallback (timers.js:693:18)\n" 70 | + " - |\n" 71 | + " tryOnImmediate (timers.js:664:5)\n" 72 | + " - |\n" 73 | + " process.processImmediate (timers.js:646:5)\n" 74 | + " wanted: true\n" 75 | + " found: false\n" 76 | + " ..."; 77 | 78 | _test("do-not-remove-corrupted-yaml", false, tap, 0); 79 | _test("remove-corrupted-yaml", true, tap, 1); 80 | } 81 | 82 | private void _test(String projectName, boolean removeYamlIfCorrupted, final String tap, int expectedTotal) throws IOException, InterruptedException, ExecutionException { 83 | FreeStyleProject project = jenkins.createProject(FreeStyleProject.class, projectName); 84 | 85 | project.getBuildersList().add(new TestBuilder() { 86 | @Override 87 | public boolean perform(AbstractBuild build, Launcher arg1, 88 | BuildListener arg2) throws InterruptedException, IOException { 89 | build.getWorkspace().child("result.tap").write(tap, "UTF-8"); 90 | return true; 91 | } 92 | }); 93 | 94 | TapPublisher publisher = new TapPublisher( 95 | "result.tap", // test results 96 | true, // failIfNoResults 97 | true, // failedTestsMarkBuildAsFailure 98 | false, // outputTapToConsole 99 | true, // enableSubtests 100 | true, // discardOldReports 101 | true, // todoIsFailure 102 | true, // includeCommentDiagnostics 103 | true, // validateNumberOfTests 104 | true, // planRequired 105 | false, // verbose 106 | true, // showOnlyFailures 107 | false, // stripSingleParents 108 | true, // flattenTapResult 109 | removeYamlIfCorrupted, 110 | false); //skipIfBuildNotOk 111 | 112 | project.getPublishersList().add(publisher); 113 | project.save(); 114 | FreeStyleBuild build = (FreeStyleBuild) project.scheduleBuild2(0).get(); 115 | 116 | TapTestResultAction action = build.getAction(TapTestResultAction.class); 117 | TapResult testResult = action.getTapResult(); 118 | 119 | assertEquals(expectedTotal, testResult.getTotal()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 4.0.0 28 | 29 | 30 | org.jenkins-ci.plugins 31 | plugin 32 | 4.76 33 | 34 | 35 | 36 | 2011 37 | 38 | org.tap4j 39 | tap 40 | 2.4.4-SNAPSHOT 41 | hpi 42 | 43 | Jenkins TAP Plugin 44 | This plugin publishes TAP test results. 45 | https://github.com/jenkinsci/tap-plugin 46 | 47 | 48 | 49 | MIT License 50 | https://opensource.org/licenses/MIT 51 | 52 | 53 | 54 | 55 | JIRA 56 | https://issues.jenkins.io/issues/?jql=project%20%3D%20JENKINS%20AND%20component%20%3D%20tap-plugin 57 | 58 | 59 | 60 | 2.4.0 61 | -SNAPSHOT 62 | 2.426.1 63 | jenkinsci/tap-plugin 64 | 4.4.2 65 | 66 | 67 | 68 | scm:git:https://github.com/${gitHubRepo}.git 69 | scm:git:git@github.com:${gitHubRepo}.git 70 | https://github.com/${gitHubRepo} 71 | ${scmTag} 72 | 73 | 74 | 75 | 76 | kinow 77 | Bruno P. Kinoshita 78 | Europe/Madrid 79 | 80 | 81 | 82 | 83 | 84 | 85 | io.jenkins.tools.bom 86 | bom-2.426.x 87 | 2598.v49e2b_e68d413 88 | import 89 | pom 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.tap4j 98 | tap4j 99 | ${tap4j.version} 100 | 101 | 102 | org.yaml 103 | snakeyaml 104 | 105 | 106 | 107 | 108 | org.yaml 109 | snakeyaml 110 | 2.2 111 | 112 | 113 | 114 | org.jenkins-ci.plugins 115 | matrix-project 116 | 117 | 118 | 119 | org.jenkins-ci.plugins 120 | junit 121 | 122 | 123 | 124 | org.jenkins-ci.plugins.workflow 125 | workflow-job 126 | test 127 | 128 | 129 | org.jenkins-ci.plugins.workflow 130 | workflow-cps 131 | test 132 | 133 | 134 | 135 | 136 | clean test install 137 | 138 | 139 | org.jenkins-ci.tools 140 | maven-hpi-plugin 141 | true 142 | 143 | 2.0 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | repo.jenkins-ci.org 152 | https://repo.jenkins-ci.org/public/ 153 | 154 | 155 | 156 | 157 | 158 | repo.jenkins-ci.org 159 | https://repo.jenkins-ci.org/public/ 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/resources/org/tap4j/plugin/model/TapStreamResult/body.jelly: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 49 | 50 | 51 |

${%All Failed Tests}

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 75 | 76 | 79 | 82 | 83 | 84 |
${%Test Name}${%Duration}${%Age}
61 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 74 | 77 | ${f.durationString} 78 | 80 | ${f.age} 81 |
85 |
86 | 87 | 88 |

${%All Tests}

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
${it.childTitle}${%Duration}${%Status}${%Skip}${%Todo}
102 | 103 | 104 | 105 | 106 | 107 | ${p.durationString}${p.status}${p.skip}${p.todo}
125 |
126 |
127 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/TapTestResultAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012-2023 Bruno P. Kinoshita 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.tap4j.plugin; 25 | 26 | import hudson.Util; 27 | import hudson.model.AbstractBuild; 28 | import hudson.model.Action; 29 | import hudson.model.HealthReport; 30 | import hudson.model.HealthReportingAction; 31 | import hudson.model.Job; 32 | import hudson.model.Run; 33 | import jenkins.model.RunAction2; 34 | import jenkins.tasks.SimpleBuildStep; 35 | import jenkins.util.NonLocalizable; 36 | import org.kohsuke.stapler.StaplerProxy; 37 | import org.kohsuke.stapler.export.Exported; 38 | import org.tap4j.plugin.model.TapStreamResult; 39 | 40 | import java.util.Collection; 41 | import java.util.Collections; 42 | 43 | 44 | /** 45 | * @since 0.1 46 | */ 47 | public class TapTestResultAction 48 | implements StaplerProxy, SimpleBuildStep.LastBuildAction, HealthReportingAction, RunAction2 { 49 | 50 | public transient Run run; 51 | @Deprecated 52 | public transient AbstractBuild owner; 53 | 54 | private TapResult tapResult; 55 | 56 | protected TapTestResultAction(Run r, TapResult tapResult) { 57 | setRunAndOwner(r); 58 | 59 | this.tapResult = tapResult; 60 | } 61 | 62 | /** 63 | * @return the tapResult 64 | */ 65 | public TapResult getTapResult() { 66 | return tapResult; 67 | } 68 | 69 | /* (non-Javadoc) 70 | * @see hudson.tasks.test.AbstractTestResultAction#getFailCount() 71 | */ 72 | @Exported(visibility = 2) 73 | public int getFailCount() { 74 | return tapResult.getFailed(); 75 | } 76 | 77 | /* (non-Javadoc) 78 | * @see hudson.tasks.test.AbstractTestResultAction#getTotalCount() 79 | */ 80 | @Exported(visibility = 2) 81 | public int getTotalCount() { 82 | return tapResult.getTotal(); 83 | } 84 | 85 | /* (non-Javadoc) 86 | * @see hudson.tasks.test.AbstractTestResultAction#getSkipCount() 87 | */ 88 | @Exported(visibility = 2) 89 | public int getSkipCount() { 90 | return tapResult.getSkipped(); 91 | } 92 | 93 | /* 94 | * (non-Javadoc) 95 | * @see org.kohsuke.stapler.StaplerProxy#getTarget() 96 | */ 97 | public Object getTarget() { 98 | return getResult(); 99 | } 100 | 101 | public TapStreamResult getResult() { 102 | return new TapStreamResult(owner, tapResult); 103 | } 104 | 105 | /* (non-Javadoc) 106 | * @see hudson.tasks.test.AbstractTestResultAction#getUrlName() 107 | */ 108 | @Override 109 | @Exported(visibility = 2) 110 | public String getUrlName() { 111 | return "tapTestReport"; 112 | } 113 | 114 | 115 | @Override 116 | public String getIconFileName() { 117 | return "clipboard.png"; 118 | } 119 | 120 | /* (non-Javadoc) 121 | * @see hudson.tasks.test.AbstractTestResultAction#getDisplayName() 122 | */ 123 | @Override 124 | public String getDisplayName() { 125 | return "TAP Test Results"; 126 | } 127 | 128 | /* 129 | * (non-Javadoc) 130 | * @see jenkins.tasks.SimpleBuildStep#getProjectActions() 131 | */ 132 | @Override 133 | public Collection getProjectActions() { 134 | Job job = run.getParent(); 135 | if (!Util.filter(job.getActions(), TapProjectAction.class).isEmpty()) { 136 | return Collections.emptySet(); 137 | } 138 | return Collections.singleton(new TapProjectAction(job)); 139 | } 140 | 141 | @Override 142 | public HealthReport getBuildHealth() { 143 | final double scaleFactor = 1.0; 144 | final int totalCount = getTotalCount(); 145 | final int failCount = getFailCount(); 146 | int score = (totalCount == 0) 147 | ? 100 148 | : (int) (100.0 * Math.max(0.0, Math.min(1.0, 1.0 - (scaleFactor * failCount) / totalCount))); 149 | // TODO: we used to use the hudson.tasks.test.Messages localized entries 150 | // here, https://github.com/jenkinsci/junit-plugin/blob/master/src/main/resources/hudson/tasks/test/Messages.properties, 151 | // but now `mvn` fails in an injected test. 152 | String description, displayName = "Test Result"; 153 | if (totalCount == 0) { 154 | description = String.format("%s: 0 tests in total", displayName); 155 | } else { 156 | description = String.format("%s: %d test(s) failing out of a total of %d test(s).", 157 | displayName, failCount, totalCount); 158 | } 159 | return new HealthReport(score, new NonLocalizable(description)); 160 | } 161 | 162 | @Override 163 | public void onAttached(Run r) { 164 | setRunAndOwner(r); 165 | } 166 | 167 | @Override 168 | public void onLoad(Run r) { 169 | setRunAndOwner(r); 170 | } 171 | 172 | private void setRunAndOwner(Run r) { 173 | this.run = r; 174 | this.owner = r instanceof AbstractBuild ? (AbstractBuild) r : null; 175 | } 176 | 177 | void mergeResult(TapResult additionalResult) { 178 | TapStreamResult original = getResult(); 179 | original.merge(additionalResult); 180 | setFromTapStreamResult(original); 181 | } 182 | 183 | private void setFromTapStreamResult(TapStreamResult result) { 184 | this.tapResult = result.getTapResult(); 185 | } 186 | } -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/flattentapfeature/TestFlattenTapResult.java: -------------------------------------------------------------------------------- 1 | package org.tap4j.plugin.flattentapfeature; 2 | 3 | import hudson.Launcher; 4 | import hudson.model.BuildListener; 5 | import hudson.model.FreeStyleBuild; 6 | import hudson.model.AbstractBuild; 7 | import hudson.model.FreeStyleProject; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.concurrent.ExecutionException; 16 | 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import org.jvnet.hudson.test.JenkinsRule; 20 | import org.jvnet.hudson.test.TestBuilder; 21 | import org.tap4j.model.TestResult; 22 | import org.tap4j.model.TestSet; 23 | import org.tap4j.plugin.TapPublisher; 24 | import org.tap4j.plugin.TapResult; 25 | import org.tap4j.plugin.TapTestResultAction; 26 | 27 | /** 28 | * Tests for flatten TAP result configuration option. 29 | * 30 | * @author Jakub Podlesak 31 | */ 32 | public class TestFlattenTapResult { 33 | 34 | @Rule 35 | public JenkinsRule jenkins = new JenkinsRule(); 36 | 37 | @Test 38 | public void testMixedLevels() throws IOException, InterruptedException, ExecutionException { 39 | 40 | final String tap = "1..2\n" + 41 | " 1..3\n" + 42 | " ok 1 1.1\n" + 43 | " ok 2 1.2\n" + 44 | " ok 3 1.3\n" + 45 | "ok 1 1\n" + 46 | "ok 2 2\n"; 47 | 48 | _test(tap, 4, null, false); 49 | } 50 | 51 | @Test 52 | public void testStripFirstLevel() throws IOException, InterruptedException, ExecutionException { 53 | 54 | final String tap = "1..2\n" + 55 | " 1..2\n" + 56 | " ok 1 .1\n" + 57 | " ok 2 .2\n" + 58 | "ok 1 1\n" + 59 | " 1..3\n" + 60 | " ok 1 .1\n" + 61 | " ok 2 .2\n" + 62 | " ok 3 .3\n" + 63 | "ok 2 2\n"; 64 | 65 | _test(tap, 5, new String[] { 66 | "1.1", "1.2", 67 | "2.1", "2.2", "2.3"}, false); 68 | } 69 | 70 | @Test 71 | public void testStripSecondLevel() throws IOException, InterruptedException, ExecutionException { 72 | 73 | final String tap = 74 | "1..1\n" + 75 | " 1..2\n" + 76 | " 1..4\n" + 77 | " ok 1 .1\n" + 78 | " ok 2 .2\n" + 79 | " ok 3 .3\n" + 80 | " ok 4 .4\n" + 81 | " ok 1 .1\n" + 82 | " 1..3\n" + 83 | " ok 1 .1\n" + 84 | " ok 2 .2\n" + 85 | " ok 3 .3\n" + 86 | " ok 2 .2\n" + 87 | "ok 1 1\n"; 88 | 89 | _test(tap, 7, 90 | new String[] { 91 | "1.1.1", "1.1.2", "1.1.3", "1.1.4", 92 | "1.2.1", "1.2.2", "1.2.3"}, false); 93 | } 94 | 95 | @Test 96 | public void testStripSecondLevelIncompleteResult1() throws IOException, InterruptedException, ExecutionException { 97 | 98 | final String tap = 99 | "1..1\n" + 100 | " 1..2\n" + 101 | " 1..4\n" + 102 | " ok 1 .1\n" + 103 | " ok 2 .2\n" + 104 | " ok 3 .3\n" + 105 | " ok 1 .1\n" + 106 | " 1..3\n" + 107 | " ok 1 .1\n" + 108 | " ok 2 .2\n" + 109 | " ok 3 .3\n" + 110 | " ok 2 .2\n" + 111 | "ok 1 1\n"; 112 | 113 | _test(tap, 7, 114 | new String[] { 115 | "1.1.1", "1.1.2", "1.1.3", "1.1 failed: 1 subtest(s) missing", 116 | "1.2.1", "1.2.2", "1.2.3"}, true); 117 | } 118 | 119 | @Test 120 | public void testStripSecondLevelIncompleteResult2() throws IOException, InterruptedException, ExecutionException { 121 | final String tap2 = 122 | "1..1\n" + 123 | " 1..2\n" + 124 | " 1..4\n" + 125 | " ok 1 .1\n" + 126 | " ok 2 .2\n" + 127 | " ok 3 .3\n" + 128 | " ok 1 .1\n" + 129 | " 1..3\n" + 130 | " ok 1 .1\n" + 131 | " ok 2 .2\n" + 132 | "ok 1 1\n"; 133 | 134 | _test(tap2, 6, 135 | new String[] { 136 | "1.1.1", "1.1.2", "1.1.3", "1.1 failed: 1 subtest(s) missing", 137 | "1.2.1", "1.2 failed: 2 subtest(s) missing"}, true); 138 | } 139 | 140 | @Test 141 | public void testARealTapOuptut() throws Exception { 142 | final String tap = _is2String(TestFlattenTapResult.class.getResourceAsStream("/org/tap4j/plugin/tap-master-files/subtest-sample.tap")); 143 | _test(tap, 48, null, true); 144 | } 145 | 146 | private String _is2String(InputStream is) throws IOException { 147 | ByteArrayOutputStream result = new ByteArrayOutputStream(); 148 | byte[] buffer = new byte[1024]; 149 | int length; 150 | while ((length = is.read(buffer)) != -1) { 151 | result.write(buffer, 0, length); 152 | } 153 | return result.toString("UTF-8"); 154 | } 155 | 156 | private void _test(final String tap, int expectedTotal, String[] expectedDescriptions, boolean printDescriptions) throws IOException, InterruptedException, ExecutionException { 157 | FreeStyleProject project = jenkins.createProject(FreeStyleProject.class, "flatten-the-file"); 158 | 159 | project.getBuildersList().add(new TestBuilder() { 160 | @Override 161 | public boolean perform(AbstractBuild build, Launcher arg1, 162 | BuildListener arg2) throws InterruptedException, IOException { 163 | build.getWorkspace().child("result.tap").write(tap,"UTF-8"); 164 | return true; 165 | } 166 | }); 167 | 168 | TapPublisher publisher = new TapPublisher( 169 | "result.tap", // test results 170 | true, // failIfNoResults 171 | true, // failedTestsMarkBuildAsFailure 172 | false, // outputTapToConsole 173 | true, // enableSubtests 174 | true, // discardOldReports 175 | true, // todoIsFailure 176 | true, // includeCommentDiagnostics 177 | true, // validateNumberOfTests 178 | true, // planRequired 179 | false, // verbose 180 | true, // showOnlyFailures 181 | false, // stripSingleParents 182 | true, // flattenTapResult 183 | false, //skipIfBuildNotOk 184 | false); 185 | 186 | project.getPublishersList().add(publisher); 187 | project.save(); 188 | FreeStyleBuild build = (FreeStyleBuild) project.scheduleBuild2(0).get(); 189 | 190 | TapTestResultAction action = build.getAction(TapTestResultAction.class); 191 | TapResult testResult = action.getTapResult(); 192 | 193 | assertEquals(expectedTotal, testResult.getTotal()); 194 | 195 | final TestSet testSet = testResult.getTestSets().get(0).getTestSet(); 196 | int testIndex = 0; 197 | for (TestResult result : testSet.getTestResults()) { 198 | 199 | final String description = result.getDescription(); 200 | final int testNumber = result.getTestNumber(); 201 | 202 | int expectedTestNumber = testIndex +1; 203 | 204 | if (printDescriptions) { 205 | System.out.printf("%d: %s\n", testNumber, description); 206 | } 207 | 208 | assertEquals(expectedTestNumber, testNumber); 209 | 210 | if (expectedDescriptions != null) { 211 | assertEquals(expectedDescriptions[testIndex], description); 212 | } 213 | 214 | 215 | testIndex ++; 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/model/TapStreamResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012 Bruno P. Kinoshita 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.tap4j.plugin.model; 25 | 26 | import hudson.model.Run; 27 | import hudson.tasks.junit.CaseResult; 28 | import hudson.tasks.test.TabulatedResult; 29 | import hudson.tasks.test.TestObject; 30 | import hudson.tasks.test.TestResult; 31 | import org.kohsuke.stapler.StaplerRequest; 32 | import org.kohsuke.stapler.StaplerResponse; 33 | import org.kohsuke.stapler.export.Exported; 34 | import org.tap4j.model.TestSet; 35 | import org.tap4j.plugin.TapResult; 36 | import org.tap4j.util.StatusValues; 37 | 38 | import javax.annotation.Nullable; 39 | import java.util.ArrayList; 40 | import java.util.Collection; 41 | import java.util.Collections; 42 | import java.util.List; 43 | 44 | /** 45 | * A tabulated TAP Stream result. 46 | * 47 | * @since 0.1 48 | */ 49 | public class TapStreamResult extends TabulatedResult { 50 | 51 | private static final long serialVersionUID = 8337146933697574082L; 52 | private final transient Run owner; 53 | private List children = new ArrayList<>(); 54 | private TapResult tapResult; 55 | 56 | public TapStreamResult(Run owner, TapResult tapResult) { 57 | this.owner = owner; 58 | this.tapResult = tapResult; 59 | setChildrenInfo(); 60 | } 61 | 62 | /* (non-Javadoc) 63 | * @see hudson.model.ModelObject#getDisplayName() 64 | */ 65 | public String getDisplayName() { 66 | return "TAP Stream Results"; 67 | } 68 | 69 | /* (non-Javadoc) 70 | * @see hudson.tasks.test.TestObject#getOwner() 71 | */ 72 | @Nullable 73 | @Override 74 | public Run getRun() { 75 | return owner; 76 | } 77 | 78 | /* (non-Javadoc) 79 | * @see hudson.tasks.test.TestObject#getParent() 80 | */ 81 | @Override 82 | public TestObject getParent() { 83 | return null; 84 | } 85 | 86 | /* (non-Javadoc) 87 | * @see hudson.tasks.test.TestObject#findCorrespondingResult(java.lang.String) 88 | */ 89 | @Override 90 | public TestResult findCorrespondingResult(String id) { 91 | return null; 92 | } 93 | 94 | /* (non-Javadoc) 95 | * @see hudson.tasks.test.TabulatedResult#getChildren() 96 | */ 97 | @Override 98 | public Collection getChildren() { 99 | return children; 100 | } 101 | 102 | /* (non-Javadoc) 103 | * @see hudson.tasks.test.TabulatedResult#hasChildren() 104 | */ 105 | @Override 106 | public boolean hasChildren() { 107 | return children.size() > 0; 108 | } 109 | 110 | /* (non-Javadoc) 111 | * @see hudson.tasks.test.AbstractTestResultAction#getFailCount() 112 | */ 113 | @Override 114 | @Exported(visibility = 2) 115 | public int getFailCount() { 116 | return tapResult.getFailed(); 117 | } 118 | 119 | /* (non-Javadoc) 120 | * @see hudson.tasks.test.AbstractTestResultAction#getTotalCount() 121 | */ 122 | @Override 123 | @Exported(visibility = 2) 124 | public int getTotalCount() { 125 | return tapResult.getTotal(); 126 | } 127 | 128 | /* (non-Javadoc) 129 | * @see hudson.tasks.test.AbstractTestResultAction#getSkipCount() 130 | */ 131 | @Override 132 | @Exported(visibility = 2) 133 | public int getSkipCount() { 134 | return tapResult.getSkipped(); 135 | } 136 | 137 | public int getToDoCount() { 138 | return tapResult.getToDo(); 139 | } 140 | 141 | /* (non-Javadoc) 142 | * @see hudson.tasks.test.AbstractTestResultAction#getFailedTests() 143 | */ 144 | @Override 145 | public List getFailedTests() { 146 | //throw new AssertionError("Not supposed to be called"); 147 | return Collections.emptyList(); 148 | } 149 | 150 | // FIXME: use the getFailedTests, or explain why it's not used 151 | public List getFailedTests2() { 152 | List failedTests = new ArrayList<>(); 153 | if(tapResult != null && tapResult.getTestSets().size() > 0) { 154 | for(TestSetMap tsm : tapResult.getTestSets()) { 155 | TestSet ts = tsm.getTestSet(); 156 | for(org.tap4j.model.TestResult tr : ts.getTestResults()) { 157 | if(tr.getStatus() == StatusValues.NOT_OK) { 158 | failedTests.add(new TapTestResultResult(owner, tsm, tr, this.tapResult.getTodoIsFailure(), tapResult.getIncludeCommentDiagnostics(), tapResult.getValidateNumberOfTests())); 159 | } 160 | } 161 | } 162 | } 163 | return failedTests; 164 | } 165 | 166 | public float getDuration() { 167 | return this.tapResult.getDuration(); 168 | } 169 | 170 | @Override 171 | public Object getDynamic(String name, StaplerRequest req, StaplerResponse rsp) { 172 | TapTestResultResult tr = getTapTestResultResult(name); 173 | if (tr != null) { 174 | return tr; 175 | } else { 176 | return super.getDynamic(name, req, rsp); 177 | } 178 | } 179 | 180 | public TapResult getTapResult() { 181 | return this.tapResult; 182 | } 183 | 184 | private TapTestResultResult getTapTestResultResult(String name) { 185 | if (name == null) 186 | return null; // we don't allow null, nay! 187 | if (name.lastIndexOf("-") <= 0) 188 | return null; // ops, where's the - mate? 189 | 190 | name = name.trim(); 191 | 192 | int rightIndex = name.length(); 193 | while (name.charAt(rightIndex-1) == '/') { 194 | rightIndex -= 1; 195 | } 196 | int leftIndex = name.lastIndexOf('/') +1; 197 | 198 | String testResultName = name.substring(leftIndex, rightIndex); // but we want the test result name (testSet1.tap) 199 | if (testResultName.indexOf('-') <= 0) // plus the number (testSet1.tap-2) 200 | return null; 201 | String testNumber = testResultName.substring(testResultName.lastIndexOf('-')+1); 202 | String fileName = name.substring(0, name.lastIndexOf('-')); 203 | 204 | for(TestSetMap tsm : tapResult.getTestSets()) { 205 | if(tsm.getFileName().equals(fileName)) { 206 | TestSet ts = tsm.getTestSet(); 207 | org.tap4j.model.TestResult desired = ts.getTestResult(Integer.parseInt(testNumber)); 208 | return new TapTestResultResult(owner, tsm, desired, this.tapResult.getTodoIsFailure(), tapResult.getIncludeCommentDiagnostics(), tapResult.getValidateNumberOfTests()); 209 | } 210 | } 211 | 212 | return null; // ops, something went wrong 213 | } 214 | 215 | public void merge(TapResult other) { 216 | 217 | tapResult = tapResult.copyWithExtraTestSets(other.getTestSets()); 218 | tapResult.tally(); 219 | children = new ArrayList<>(); 220 | 221 | setChildrenInfo(); 222 | } 223 | 224 | private void setChildrenInfo() { 225 | for(TestSetMap tsm : tapResult.getTestSets()) { 226 | TestSet ts = tsm.getTestSet(); 227 | for(org.tap4j.model.TestResult tr : ts.getTestResults()) { 228 | this.children.add(new TapTestResultResult(owner, tsm, tr, tapResult.getTodoIsFailure(), tapResult.getIncludeCommentDiagnostics(), tapResult.getValidateNumberOfTests())); 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TAP Plug-in 2 | 3 | [![Jenkins Plugin](https://img.shields.io/jenkins/plugin/v/tap.svg)](https://plugins.jenkins.io/tap) 4 | [![GitHub release](https://img.shields.io/github/release/jenkinsci/tap.svg?label=changelog)](https://github.com/jenkinsci/tap-plugin/blob/master/CHANGES.md) 5 | [![Jenkins Plugin Installs](https://img.shields.io/jenkins/plugin/i/tap.svg?color=blue)](https://plugins.jenkins.io/tap) 6 | [![Jenkins](https://ci.jenkins.io/job/Plugins/job/tap-plugin/job/master/badge/icon?subject=Jenkins%20CI)](https://ci.jenkins.io/job/Plugins/job/tap-plugin/job/master/) 7 | [![JIRA issues](https://img.shields.io/static/v1?label=Issue%20tracking&message=component:%20tap-plugin&color=blue)](https://issues.jenkins.io/browse/JENKINS-64962?jql=component%20%3D%20%27tap-plugin%27%20AND%20resolution%20IS%20EMPTY%20ORDER%20BY%20updated%20DESC) 8 | 9 | ## Overview 10 | 11 | This plug-in adds support to [TAP](https://testanything.org/) test result files to Jenkins. 12 | It lets you specify an ant-like pattern for a directory that contains your TAP files and 13 | scans and creates views for your test results in Jenkins. 14 | 15 | TAP Plug-in depends on [tap4j](https://github.com/tupilabs/tap4j) - a TAP implementation 16 | for Java, and on the [Jenkins JUnit Plug-in](https://plugins.jenkins.io/junit/). 17 | 18 | > NOTE: You may get errors if the JUnit Plug-in is not active in your Jenkins instance 19 | (see JENKINS-27227 for more). 20 | 21 | ## Overview 22 | 23 | The plug-in looks for TAP files like the following one. 24 | 25 | ```tap 26 | 1..2 27 | ok 1 - Yahoo! 28 | not ok 2 - org.tap4j.Error... 29 | ``` 30 | 31 | When a TAP stream like the above is found, the plug-in delegates the 32 | parsing the tap4j. The results of tap4j parsing are then analysed, 33 | organized and displayed to the user as graphs and custom pages. 34 | 35 | [comment]: <> (TODO: check if instantTAP is still the best way to validate it?) 36 | 37 | You can test your TAP streams with tap4j, using [InstantTAP](http://instanttap.appspot.com/). 38 | 39 | ### Understanding TAP streams in 2 minutes 40 | 41 | One of the easiest ways to learn something new is by examples. So here 42 | we will show some examples of TAP streams. For being human-friendly, it 43 | shouldn't confuse you. We will use comments to explain each line. 44 | 45 | ```tap 46 | 1..2 # a plan stating that we have two tests cases, from 1 to 2. 47 | ok 1 - Yahoo! # the first test result was executed successfully and has a description of ' - Yahoo!'. 48 | not ok 2 - org.tap4j.Error... # unfortunately, the second test result failed. The description here was used to display some nasty Exception. 49 | ``` 50 | 51 | ### Running Perl tests with prove 52 | 53 | The plug-in cannot handle prove's default output (since it includes more 54 | information than simply TAP, and causes tap4j parser to fail). The best 55 | way to handle the prove output is by using Perl module 56 | [TAP::Harness::Archive](http://search.cpan.org/~wonko/TAP-Harness-Archive-0.14/lib/TAP/Harness/Archive.pm). 57 | Supposing you have your tests under t/ directory, you can create another 58 | directory (say, output) and archive your TAP tests with prove by using a 59 | command line similar to the below: 60 | 61 | ```bash 62 | prove t/ --archive output 63 | ``` 64 | 65 | The result files will be stored under *output/t/*. You can use a pattern 66 | in the plug-in configuration like *t/\*****/****.t*. 67 | 68 | ### Using attachments 69 | 70 | The following is a TAP with attachments, using YAMLish. If you are 71 | familiar with YAML, this example should be very easy to read. 72 | 73 | ```yaml 74 | 1..2 75 | ok 1 76 | not ok 2 - br.eti.kinoshita.selenium.TestListVeterinarians#testGoogle 77 | --- 78 | extensions: 79 | Files: 80 | my_message.txt: 81 | File-Title: my_message.txt 82 | File-Description: Sample message 83 | File-Size: 31 84 | File-Name: message.txt 85 | File-Content: TuNvIGNvbnRhdmFtIGNvbSBtaW5oYSBhc3T6Y2lhIQ== 86 | File-Type: image/png 87 | ... 88 | ``` 89 | 90 | The Files entry has an array of files. You have to Base64 encode your 91 | content data. 92 | 93 | ### Subtests (grouping or test suites). 94 | 95 | Subtests let you group several TAP streams unto a single one. This way, 96 | you can organize your tests in similar fashion to JUnit or TestNG test 97 | suites. Indentation is important for TAP subtests. 98 | 99 | Subtests and YAMLish are not officially in TAP 13 specification 100 | 101 | ```yaml 102 | 1..3 103 | ok 1 - First test 104 | 1..2 105 | ok 1 - This is a subtest 106 | ok 2 - So is this 107 | 1..2 108 | ok 1 - This is a subtest 109 | ok 2 - So is this 110 | ok 2 - An example subtest 111 | ok 3 - Third test 112 | ``` 113 | 114 | ## Configuration 115 | 116 | 1. Install the Jenkins TAP Plug-in using the Plug-in Manager or manually by copying the `.hpi` (or `.jpi`) file 117 | 2. Check the option to publish TAP, configure a pattern (and other settings) 118 | 3. Execute your build and analyze the results 119 | 120 | ## Screenshots 121 | 122 | #### Jenkins JUnit compatible reports and graphs 123 | 124 | ![](docs/images/001.png) 125 | 126 | #### Custom actions for TAP too 127 | 128 | ![](docs/images/002.png) 129 | 130 | #### YAMLish support 131 | 132 | ![](docs/images/003.png) 133 | 134 | ## Languages Supported 135 | 136 | 1. English (American) 137 | 2. Portuguese (Brazil)) Work in Progress 138 | 3. Spanish (Thanks to César Fernandes de Almeida) Work in Progress 139 | 140 | ## Known Limitations 141 | 142 | 1. If the file type of the TAP report is considered as binary by the Jenkins webserver then the TAP plugin does not 143 | consider this file for inclusion in TAP reports (see [#15813](https://issues.jenkins-ci.org/browse/JENKINS-15813) 144 | for further details). To make sure your TAP report is considered for inclusion use e.g. the file name suffix `.tap` ( 145 | so instead of a file named `report` use `report.tap`). 146 | 147 | ## Sponsors 148 | 149 | [![](docs/images/logo1.png){width="300"}](http://www.tupilabs.com/) 150 | 151 | For commercial support, please get contact us 152 | via [@tupilabs](https://twitter.com/tupilabs) 153 | 154 | ## Resources 155 | 156 | 1. This plug-in is going to be part of "Make your tests speak TAP" presentation in [JCertif](http://www.jcertif.com/) 157 | by [Bruno P. Kinoshita](http://www.kinoshita.eti.br/), in September 2011. In this presentation will also be presented 158 | the tap4j project, how to enable TAP in JUnit and TestNG, integrate [Perl](http://www.perl.org/) and 159 | [Java](http://www.oracle.com/java) tests and an [Eclipse TAP editor](https://github.com/kinow/tap-editor). 160 | 2. Gábor Szabó (2009), Test Reporting system: Smolder 161 | wish-list . 162 | 3. Gábor Szabó (2009), Reporting Test Results . 163 | 4. [tap4j](http://www.tap4j.org/) - The TAP implementation for Java. 164 | 5. [Test Anything Protocol](http://www.testanything.org/) (official webpage). 165 | 6. [Performance tests with phantomjs and yslow](http://www.slideshare.net/guest94ab56d/2013-0717continuous-performancemonitoringwithjenkins) 166 | (uses the plug-in for plotting the TAP results) 167 | 7. [TAP Plugin for Matlab](http://www.mathworks.com/help/matlab/ref/matlab.unittest.plugins.tapplugin-class.html) 168 | 169 | ### JCertif 2011 170 | 171 | [JCertif](http://www.jcertif.com/) - Make your tests speak TAP - 172 | Speaker: [Bruno P. Kinoshita](http://www.kinoshita.eti.br/) 173 | September, 2011 - Brazzaville, Congo 174 | 175 | ![](docs/images/JCertif_Conf2011_2.png) 176 | 177 | ## Release Notes 178 | 179 | Moved to [CHANGES.md](./CHANGES.md). 180 | 181 | ## Roadmap (wish list) 182 | 183 | ### Version 2.x 184 | 185 | 1. Update Jenkins API 186 | 2. Update tap4j to match the latest protocol specification (13 and 14) 187 | 3. Remove deprecated code 188 | 4. Simplify code (it's old!) 189 | 190 | ### Version 1.x 191 | 192 | 1. Add configurations like validate number of tests with test plan, a TODO causes a test to fail 193 | 2. Diagnostics image gallery Done! Fixed as 194 | a [new plug-in](https://wiki.jenkins-ci.org/display/JENKINS/Image+Gallery+Plugin) 195 | 3. Diagnostics exception code formatting 196 | 4. Add a link to open the file in the Build workspace (think about remote and local issues) Done! 197 | 198 | ## History 199 | 200 | The idea of the plug-in surged after tap4j was created. After learning 201 | about Smolder, it became evident that Jenkins could be used as a 202 | replacement for it. All that was needed was just adding TAP support to 203 | Jenkins and implementing a nice UI to display the test results. After 204 | some messages in jenkins-dev-list, Max and Nick commented about their 205 | need to show test results in a different manner than how Jenkins was 206 | doing at that moment. Soon after that Max, Nick, Bruno (tap4j) and 207 | Cesar (tap4j) started to work together, exchanging mail messages and 208 | discussing an initial design for this plug-in. 209 | 210 | In July 2011 the first version of the plug-in was ready to be released. 211 | The graph code used here was adapted from TestNG Plugin (big thanks to 212 | the development team, great work). The diagnostic (YAMLish) was 213 | implemented in Jelly + Java + CSS. And the road map was incremented 214 | based on what Gábor Szabó posted about Smolder and testing reports in 215 | his blog (see resources for links). 216 | 217 | ## Simulating TAP streams with a Shell build step 218 | 219 | You can use a [heredoc](https://en.wikipedia.org/wiki/Here_document) 220 | to write a TAP file with Shell, and use it with the plug-in. This is 221 | useful for testing. 222 | 223 | ```bash 224 | #!/bin/bash 225 | 226 | for x in {1..100}; do 227 | 228 | cat > $x.tap < 103 | 104 | ## Version 1.17 105 | 106 | 1. [JENKINS-22047: Add option to reduce noise in logs](https://issues.jenkins-ci.org/browse/JENKINS-22047) 107 | 2. [JENKINS-22036: NullPointer when there is no Test Plan](https://issues.jenkins-ci.org/browse/JENKINS-22036) 108 | 3. [JENKINS-17960: Indicate if tests don't go to plan](https://issues.jenkins-ci.org/browse/JENKINS-17960) 109 | 4. [JENKINS-21917: TAP results graph causes null pointer exception](https://issues.jenkins-ci.org/browse/JENKINS-21917) 110 | 5. [Pull request \#4](https://github.com/jenkinsci/tap-plugin/pull/4) 111 | 112 | ## Version 1.17 113 | 114 | 1. [JENKINS-20924: Make plans optional in TAP via a configuration](https://issues.jenkins-ci.org/browse/JENKINS-20924) 115 | 116 | ## Version 1.16 117 | 118 | 1. Updated to tap4j-4.0.5 (better subtests handling) 119 | 120 | ## Version 1.15 121 | 122 | 1. [JENKINS-16325: TAP Parser can't handle the output from prove](https://issues.jenkins-ci.org/browse/JENKINS-16325) 123 | 124 | ## Version 1.14 125 | 126 | 1. Security bug reported by Kees J. via e-mail. This issue is related to exposing files that the user running Jenkins 127 | has access via the plug-in. 128 | 129 | ## Version 1.13 130 | 131 | 1. [JENKINS-17960: Indicate if tests don't go to plan](https://issues.jenkins-ci.org/browse/JENKINS-17960) 132 | 133 | ## Version 1.12 134 | 135 | 1. [JENKINS-18885: Parse errors with Git's TAP test suite, part 2](https://issues.jenkins-ci.org/browse/JENKINS-18885) 136 | 2. [JENKINS-17878: HTML test output in tapResults not escaped](https://issues.jenkins-ci.org/browse/JENKINS-17878) 137 | 3. [JENKINS-17855: TAP Stream results summary page contains links that fail](https://issues.jenkins-ci.org/browse/JENKINS-17855) 138 | 4. [JENKINS-17504: TAP Plugin generates bad detail links on "tapTestReport" page](https://issues.jenkins-ci.org/browse/JENKINS-17504) 139 | 140 | ## Version 1.11 141 | 142 | 1. [JENKINS-17859: TAP report table show failed test message on ALL tests after the failed one.](https://issues.jenkins-ci.org/browse/JENKINS-17859) 143 | 2. [JENKINS-17855: TAP Stream results summary page contains links that fail](https://issues.jenkins-ci.org/browse/JENKINS-17855) 144 | 3. [JENKINS-17941: Parse errors with Git's TAP test suite](https://issues.jenkins-ci.org/browse/JENKINS-17941) 145 | 4. [JENKINS-17947: Nested TAP not parsed correctly](https://issues.jenkins-ci.org/browse/JENKINS-17947) 146 | 5. [JENKINS-17504: TAP Plugin generates bad detail links on "tapTestReport" page](https://issues.jenkins-ci.org/browse/JENKINS-17504) 147 | 148 | ## Version 1.10 149 | 150 | 1. [JENKINS-17245: Tap plug-in can't find TAP attachments](https://issues.jenkins-ci.org/browse/JENKINS-17245) 151 | 152 | ## Version 1.9 153 | 154 | 1. [JENKINS-16262: Tap plug-in can't find TAP attachments](https://issues.jenkins-ci.org/browse/JENKINS-16262) 155 | 156 | ## Version 1.8 157 | 158 | 1. [JENKINS-15914: TAP results table misses first comment line](https://issues.jenkins-ci.org/browse/JENKINS-15914) 159 | 2. [JENKINS-15322: NOTESTS in TAP response gives parse error and stack trace from plugin](https://issues.jenkins-ci.org/browse/JENKINS-15322) 160 | 3. [JENKINS-15401: support TODO directive to not fail such tests](https://issues.jenkins-ci.org/browse/JENKINS-15401) 161 | 4. [JENKINS-15907: When multiple TAP files with same basename match pattern, only one is processed](https://issues.jenkins-ci.org/browse/JENKINS-15907) 162 | 163 | ## Version 1.7 164 | 165 | 1. [JENKINS-15586: TAP plug-in is ignoring given file extension and looking for .tap files](https://issues.jenkins-ci.org/browse/JENKINS-15586) 166 | 167 | ## Version 1.6 168 | 169 | 1. [JENKINS-15419: TAP published results hide JUnit published results](https://issues.jenkins-ci.org/browse/JENKINS-15419) 170 | 2. [JENKINS-15497: Display link to download TAP attachment](https://issues.jenkins-ci.org/browse/JENKINS-15497) 171 | 172 | ## Version 1.2.7 173 | 174 | 1. Removed requirement to have the TAP Plan at start or at the end of the TAP Stream. This way, TAP Streams generated 175 | using Perl done\_testing() now works well with the plug-in 176 | 177 | ## Version 1.2.6 178 | 179 | 1. Support JSON within YAMLish data 180 | 181 | ## Version 1.1 182 | 183 | 1. Support to Bail out!'s 184 | 2. JENKINS-10562 TAP Plugin fails on slave 185 | 186 | ## Version 1.0 187 | 188 | 1. Initial design of the plug-in 189 | 2. Custom UI for TAP test results 190 | 3. JUnit-like graph that displays the test results per build (actually the graph was modeled using TestNG Plug-in as 191 | basis) 192 | -------------------------------------------------------------------------------- /src/test/java/org/tap4j/plugin/TapPublisherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2009, Yahoo!, 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.tap4j.plugin; 25 | 26 | import hudson.FilePath; 27 | import hudson.model.FreeStyleBuild; 28 | import hudson.model.FreeStyleProject; 29 | import hudson.model.Result; 30 | import hudson.slaves.DumbSlave; 31 | import hudson.tasks.test.TestResult; 32 | import org.junit.Before; 33 | import org.junit.Rule; 34 | import org.junit.Test; 35 | import org.jvnet.hudson.test.JenkinsRule; 36 | import org.jvnet.hudson.test.TouchBuilder; 37 | import org.jvnet.hudson.test.recipes.LocalData; 38 | import org.tap4j.plugin.model.TapStreamResult; 39 | import org.tap4j.plugin.model.TapTestResultResult; 40 | 41 | import java.util.Iterator; 42 | import java.util.concurrent.TimeUnit; 43 | 44 | import static org.junit.Assert.assertEquals; 45 | import static org.junit.Assert.assertNotNull; 46 | 47 | 48 | public class TapPublisherTest { 49 | 50 | @Rule 51 | public JenkinsRule j = new JenkinsRule(); 52 | private FreeStyleProject project; 53 | private TapPublisher archiver1; 54 | private TapPublisher archiver2; 55 | 56 | @Before 57 | public void setUp() throws Exception { 58 | project = j.createFreeStyleProject("tap"); 59 | archiver1 = new TapPublisher( 60 | "**/sample.tap", 61 | true, 62 | true, 63 | false, 64 | true, 65 | true, 66 | false, 67 | true, 68 | true, 69 | true, 70 | false, 71 | false, 72 | false, 73 | false, 74 | false, 75 | false 76 | ); 77 | archiver2 = new TapPublisher( 78 | "**/more.tap", 79 | true, 80 | true, 81 | false, 82 | true, 83 | true, 84 | false, 85 | true, 86 | true, 87 | true, 88 | false, 89 | false, 90 | false, 91 | false, 92 | false, 93 | false 94 | ); 95 | project.getPublishersList().add(archiver1); 96 | 97 | project.getBuildersList().add(new TouchBuilder()); 98 | } 99 | 100 | @LocalData 101 | @Test 102 | // getPage uses deprecation to tell users about a possibility to use relative pages (shrugs) 103 | @SuppressWarnings("deprecation") 104 | public void basic() throws Exception { 105 | FreeStyleBuild build = project.scheduleBuild2(0).get(1000, TimeUnit.SECONDS); 106 | 107 | assertTestResultsBasic(build); 108 | 109 | try (JenkinsRule.WebClient wc = j.new WebClient()) { 110 | // Check that we can access project page. 111 | wc.getPage(project); 112 | 113 | // Check that we can access current build page. 114 | wc.getPage(build); 115 | 116 | // Check that we can access TAP report page. 117 | wc.getPage(build, "tapTestReport"); 118 | 119 | // Check that we can access green and red test links from "All Tests" table. 120 | wc.getPage(build, "tapTestReport/" + getNthResultPathFromAllTestsTable(build, 0)); // green 121 | wc.getPage(build, "tapTestReport/" + getNthResultPathFromAllTestsTable(build, 1)); // red 122 | 123 | // Check that we can access link from "Failed Tests" table. 124 | wc.getPage(getNthResultPathFromFailedTestsTable(build)); 125 | } 126 | } 127 | 128 | @LocalData 129 | @Test 130 | // getPage uses deprecation to tell users about a possibility to use relative pages (shrugs) 131 | @SuppressWarnings("deprecation") 132 | public void merged() throws Exception { 133 | 134 | project.getPublishersList().add(archiver2); 135 | 136 | FreeStyleBuild build = project.scheduleBuild2(0).get(1000, TimeUnit.SECONDS); 137 | 138 | assertEquals(1, build.getActions(TapTestResultAction.class).size()); 139 | assertEquals(1, build.getActions(TapBuildAction.class).size()); 140 | 141 | assertTestResultsMerged(build); 142 | 143 | try (JenkinsRule.WebClient wc = j.new WebClient()) { 144 | // Check that we can access project page. 145 | wc.getPage(project); 146 | 147 | // Check that we can access current build page. 148 | wc.getPage(build); 149 | 150 | // Check that we can access TAP report page. 151 | wc.getPage(build, "tapTestReport"); 152 | 153 | // Check that we can access green and red test links from "All Tests" table. 154 | wc.getPage(build, "tapTestReport/" + getNthResultPathFromAllTestsTable(build, 0)); // green 155 | wc.getPage(build, "tapTestReport/" + getNthResultPathFromAllTestsTable(build, 1)); // red 156 | 157 | // Check that we can access link from "Failed Tests" table. 158 | wc.getPage(getNthResultPathFromFailedTestsTable(build)); 159 | } 160 | } 161 | 162 | @SuppressWarnings("unchecked") 163 | private String getNthResultPathFromAllTestsTable(FreeStyleBuild build, int testNumber) { 164 | Iterator it = (Iterator) build 165 | .getActions(TapTestResultAction.class) 166 | .get(0) 167 | .getResult().getChildren().iterator(); 168 | 169 | int count = 0; 170 | TapTestResultResult result = null; 171 | while (count++ <= testNumber) { 172 | result = it.next(); 173 | } 174 | assert result != null; 175 | return result.getSafeName(); 176 | } 177 | 178 | /** 179 | * This method unfortunately returns absolute URL of the 180 | * testNumberth link in "Failed Tests" table. 181 | * 182 | * @param build build object. 183 | * @return absolute URL. 184 | */ 185 | private String getNthResultPathFromFailedTestsTable(FreeStyleBuild build) { 186 | 187 | TapStreamResult testObject = build 188 | .getActions(TapTestResultAction.class) 189 | .get(0) 190 | .getResult(); 191 | 192 | int count = 0; 193 | TestResult result = null; 194 | Iterator it = testObject.getFailedTests2().iterator(); 195 | while (count++ <= 0) { 196 | result = it.next(); 197 | } 198 | assert result != null; 199 | return result.getRelativePathFrom(testObject); 200 | } 201 | 202 | @LocalData 203 | @Test 204 | public void slave() throws Exception { 205 | DumbSlave s = j.createOnlineSlave(); 206 | project.setAssignedLabel(s.getSelfLabel()); 207 | 208 | FilePath src = new FilePath(j.jenkins.getRootPath(), "jobs/tap/workspace/"); 209 | assertNotNull(src); 210 | FilePath dest = s.getWorkspaceFor(project); 211 | assertNotNull(dest); 212 | src.copyRecursiveTo("*.tap", dest); 213 | 214 | basic(); 215 | } 216 | 217 | private void assertTestResultsBasic(FreeStyleBuild build) { 218 | assertTestResults(build, 3, 1); 219 | } 220 | 221 | private void assertTestResultsMerged(FreeStyleBuild build) { 222 | assertTestResults(build, 5, 2); 223 | } 224 | 225 | private void assertTestResults(FreeStyleBuild build, int total, int failed) { 226 | TapTestResultAction testResultAction = build.getAction(TapTestResultAction.class); 227 | assertNotNull("no TestResultAction", testResultAction); 228 | 229 | TestResult result = testResultAction.getResult(); 230 | assertNotNull("no TestResult", result); 231 | 232 | assertEquals(String.format("should have %d failing test", failed), failed, testResultAction.getFailCount()); 233 | assertEquals(String.format("should have %d failing test", failed), failed, result.getFailCount()); 234 | 235 | assertEquals(String.format("should have %d total tests", total), total, testResultAction.getTotalCount()); 236 | assertEquals(String.format("should have %d total tests", total), total, result.getTotalCount()); 237 | 238 | assertEquals(String.format("should have %d skipped test", 1), 1, testResultAction.getSkipCount()); 239 | assertEquals(String.format("should have %d skipped test", 1), 1, result.getSkipCount()); 240 | } 241 | 242 | @LocalData 243 | @Test 244 | public void persistence() throws Exception { 245 | project.scheduleBuild2(0).get(60, TimeUnit.SECONDS); 246 | 247 | reloadJenkins(); 248 | 249 | FreeStyleBuild build = project.getBuildByNumber(1); 250 | 251 | assertTestResultsBasic(build); 252 | } 253 | 254 | private void reloadJenkins() throws Exception { 255 | j.jenkins.reload(); 256 | project = (FreeStyleProject) j.jenkins.getItem("tap"); 257 | } 258 | 259 | @Test 260 | public void emptyDirectory() throws Exception { 261 | FreeStyleProject freeStyleProject = j.createFreeStyleProject(); 262 | freeStyleProject.getPublishersList().add(archiver1); 263 | j.assertBuildStatus(Result.FAILURE, freeStyleProject.scheduleBuild2(0).get()); 264 | } 265 | } -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/TapParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2010-2016 Bruno P. Kinoshita 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.tap4j.plugin; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.io.PrintStream; 29 | import java.util.LinkedList; 30 | import java.util.List; 31 | import java.util.logging.Logger; 32 | 33 | import org.apache.commons.io.FileUtils; 34 | import org.tap4j.model.Plan; 35 | import org.tap4j.model.TestResult; 36 | import org.tap4j.model.TestSet; 37 | import org.tap4j.parser.ParserException; 38 | import org.tap4j.parser.Tap13Parser; 39 | import org.tap4j.plugin.model.ParseErrorTestSetMap; 40 | import org.tap4j.plugin.model.TestSetMap; 41 | import org.tap4j.util.DirectiveValues; 42 | import org.tap4j.util.StatusValues; 43 | 44 | import hudson.FilePath; 45 | import hudson.model.Run; 46 | 47 | /** 48 | * Executes remote TAP Stream retrieval and execution. 49 | * 50 | * @since 1.1 51 | */ 52 | public class TapParser { 53 | 54 | /** Prints the logs to the web server's console / log files */ 55 | private static final Logger log = Logger.getLogger(TapParser.class.getName()); 56 | private final Boolean outputTapToConsole; 57 | private final Boolean enableSubtests; 58 | private final Boolean todoIsFailure; 59 | 60 | /** Build's logger to print logs as part of build's console output */ 61 | private final PrintStream logger; 62 | private final Boolean includeCommentDiagnostics; 63 | private final Boolean validateNumberOfTests; 64 | private final Boolean planRequired; 65 | private final Boolean verbose; 66 | private final Boolean stripSingleParents; 67 | private final Boolean flattenTheTap; 68 | private final Boolean removeYamlIfCorrupted; 69 | 70 | private boolean hasFailedTests; 71 | private boolean parserErrors; 72 | 73 | public TapParser(Boolean outputTapToConsole, Boolean enableSubtests, Boolean todoIsFailure, 74 | Boolean includeCommentDiagnostics, Boolean validateNumberOfTests, Boolean planRequired, Boolean verbose, 75 | Boolean stripSingleParents, Boolean flattenTheTap, Boolean removeYamlIfCorrupted, 76 | PrintStream logger) { 77 | this.outputTapToConsole = outputTapToConsole; 78 | this.enableSubtests = enableSubtests; 79 | this.todoIsFailure = todoIsFailure; 80 | this.parserErrors = false; 81 | this.includeCommentDiagnostics = includeCommentDiagnostics; 82 | this.validateNumberOfTests = validateNumberOfTests; 83 | this.planRequired = planRequired; 84 | this.verbose = verbose; 85 | this.stripSingleParents = stripSingleParents; 86 | this.flattenTheTap = flattenTheTap; 87 | this.removeYamlIfCorrupted = removeYamlIfCorrupted; 88 | this.logger = logger; 89 | } 90 | 91 | public Boolean hasParserErrors() { 92 | return this.parserErrors; 93 | } 94 | 95 | public Boolean getOutputTapToConsole() { 96 | return outputTapToConsole; 97 | } 98 | 99 | public Boolean getTodoIsFailure() { 100 | return todoIsFailure; 101 | } 102 | 103 | public boolean getParserErrors() { 104 | return parserErrors; 105 | } 106 | 107 | public boolean getStripSingleParents() { 108 | return stripSingleParents; 109 | } 110 | 111 | public Boolean getIncludeCommentDiagnostics() { 112 | return includeCommentDiagnostics; 113 | } 114 | 115 | public Boolean getValidateNumberOfTests() { 116 | return validateNumberOfTests; 117 | } 118 | 119 | public Boolean getPlanRequired() { 120 | return planRequired; 121 | } 122 | 123 | public Boolean getEnableSubtests() { 124 | return enableSubtests; 125 | } 126 | 127 | public boolean hasFailedTests() { 128 | return this.hasFailedTests; 129 | } 130 | 131 | public Boolean getVerbose() { 132 | return verbose; 133 | } 134 | 135 | public boolean getFlattenTheTap() { 136 | return flattenTheTap; 137 | } 138 | 139 | public Boolean getRemoveYamlIfCorrupted() { 140 | return enableSubtests; 141 | } 142 | 143 | private boolean containsNotOk(TestSet testSet) { 144 | for (TestResult testResult : testSet.getTestResults()) { 145 | if (testResult.getStatus().equals(StatusValues.NOT_OK) && !(testResult.getDirective() != null 146 | && DirectiveValues.SKIP == testResult.getDirective().getDirectiveValue())) { 147 | return true; 148 | } 149 | } 150 | return false; 151 | } 152 | 153 | public TapResult parse(FilePath[] results, Run build) { 154 | this.parserErrors = Boolean.FALSE; 155 | this.hasFailedTests = Boolean.FALSE; 156 | final List testSets = new LinkedList<>(); 157 | if (null == results) { 158 | log("File paths not specified. paths var is null. Returning empty test results."); 159 | } else { 160 | for (FilePath path : results) { 161 | File tapFile = new File(path.getRemote()); 162 | if (!tapFile.isFile()) { 163 | log("'" + tapFile.getAbsolutePath() + "' points to an invalid test report"); 164 | continue; // move to next file 165 | } else { 166 | log("Processing '" + tapFile.getAbsolutePath() + "'"); 167 | } 168 | try { 169 | log("Parsing TAP test result [" + tapFile + "]."); 170 | 171 | final Tap13Parser parser = new Tap13Parser("UTF-8", enableSubtests, planRequired, removeYamlIfCorrupted); 172 | final TestSet testSet = flattenTheSetAsRequired(stripSingleParentsAsRequired(parser.parseFile(tapFile))); 173 | 174 | if (containsNotOk(testSet) || testSet.containsBailOut()) { 175 | this.hasFailedTests = Boolean.TRUE; 176 | } 177 | 178 | final TestSetMap map = new TestSetMap(tapFile.getAbsolutePath(), testSet); 179 | testSets.add(map); 180 | 181 | if (this.outputTapToConsole) { 182 | try { 183 | log(FileUtils.readFileToString(tapFile)); 184 | } catch (RuntimeException | IOException re) { 185 | log(re); 186 | } 187 | } 188 | } catch (ParserException pe) { 189 | testSets.add(new ParseErrorTestSetMap(tapFile.getAbsolutePath(), pe)); 190 | this.parserErrors = Boolean.TRUE; 191 | log(pe); 192 | } 193 | } 194 | } 195 | return new TapResult("TAP Test Results", build, testSets, this.todoIsFailure, 196 | this.includeCommentDiagnostics, this.validateNumberOfTests); 197 | } 198 | 199 | private TestSet stripSingleParentsAsRequired(TestSet originalSet) { 200 | if (!stripSingleParents) { 201 | return originalSet; 202 | } else { 203 | TestSet result = originalSet; 204 | while (hasSingleParent(result)) { 205 | result = result.getTestResults().get(0).getSubtest(); 206 | } 207 | return result; 208 | } 209 | } 210 | 211 | private TestSet flattenTheSetAsRequired(TestSet originalSet) { 212 | if (!flattenTheTap) { 213 | return originalSet; 214 | } else { 215 | TestSet result = new TestSet(); 216 | final List resultsToProcess = originalSet.getTestResults(); 217 | int testIndex = 1; 218 | while (!resultsToProcess.isEmpty()) { 219 | final TestResult actualTestResult = resultsToProcess.remove(0); 220 | TestSet subtests = actualTestResult.getSubtest(); 221 | if (subtests == null || subtests.getNumberOfTestResults() == 0) { 222 | actualTestResult.setTestNumber(testIndex++); 223 | result.addTestResult(actualTestResult); 224 | } else { 225 | final List subtestResults = subtests.getTestResults(); 226 | for (TestResult subtestResult : subtestResults) { 227 | subtestResult.setDescription(actualTestResult.getDescription() + subtestResult.getDescription()); 228 | resultsToProcess.add(subtestResult); 229 | } 230 | 231 | final Plan subtestPlan = subtests.getPlan(); 232 | final boolean planIsPresent = subtestPlan != null; 233 | final int subtestCountAsPlanned = planIsPresent ? 234 | subtestPlan.getLastTestNumber() - subtestPlan.getInitialTestNumber() + 1 235 | : -1; 236 | 237 | final boolean subtestCountDiffersFromPlan = planIsPresent && subtestCountAsPlanned != subtestResults.size(); 238 | 239 | if (subtestCountDiffersFromPlan) { 240 | 241 | final int missingTestCount = 242 | subtestCountAsPlanned - subtestResults.size(); 243 | 244 | final TestResult timeoutTestResult = new TestResult(); 245 | timeoutTestResult.setStatus(StatusValues.NOT_OK); 246 | timeoutTestResult.setDescription( 247 | String.format("%s %s %d %s", 248 | actualTestResult.getDescription(), "failed:", missingTestCount, "subtest(s) missing")); 249 | 250 | resultsToProcess.add(timeoutTestResult); 251 | } 252 | } 253 | } 254 | return result; 255 | } 256 | } 257 | 258 | private boolean hasSingleParent(TestSet testSet) { 259 | 260 | if (testSet == null) { 261 | return false; 262 | } 263 | 264 | if (testSet.getNumberOfTestResults() != 1) { 265 | return false; // not a single test result 266 | } 267 | 268 | int planSpan = testSet.getPlan() != null ? (testSet.getPlan().getLastTestNumber() - testSet.getPlan().getInitialTestNumber()) : 0; 269 | 270 | if (planSpan == 0) { // exactly one test 271 | return testSet.getTestResults().get(0).getSubtest() != null; // which has a child(ern) 272 | } else { 273 | return false; 274 | } 275 | } 276 | 277 | private void log(String str) { 278 | if (verbose && logger != null) { 279 | logger.println(str); 280 | } else { 281 | log.fine(str); 282 | } 283 | } 284 | 285 | private void log(Exception ex) { 286 | if (logger != null) { 287 | ex.printStackTrace(logger); 288 | } else { 289 | log.severe(ex.toString()); 290 | } 291 | } 292 | 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/TapProjectAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011 Bruno P. Kinoshita 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.tap4j.plugin; 25 | 26 | import hudson.matrix.MatrixProject; 27 | import hudson.model.AbstractProject; 28 | import hudson.model.Job; 29 | import hudson.model.Run; 30 | import hudson.util.ChartUtil; 31 | import hudson.util.DataSetBuilder; 32 | import hudson.util.RunList; 33 | import org.jfree.chart.JFreeChart; 34 | import org.kohsuke.stapler.StaplerRequest; 35 | import org.kohsuke.stapler.StaplerResponse; 36 | import org.tap4j.plugin.util.GraphHelper; 37 | 38 | import java.io.IOException; 39 | import java.util.Calendar; 40 | import java.util.HashMap; 41 | import java.util.Map; 42 | 43 | /** 44 | * A TAP Project action, with a graph and a list of builds. 45 | * 46 | * @since 1.0 47 | */ 48 | public class TapProjectAction extends AbstractTapProjectAction { 49 | 50 | protected static class Result { 51 | public int numPassed; 52 | public int numFailed; 53 | public int numSkipped; 54 | public int numToDo; 55 | 56 | public Result() { 57 | numPassed = 0; 58 | numFailed = 0; 59 | numSkipped = 0; 60 | numToDo = 0; 61 | } 62 | 63 | public void add(Result r) { 64 | numPassed += r.numPassed; 65 | numFailed += r.numFailed; 66 | numSkipped += r.numSkipped; 67 | numToDo += r.numToDo; 68 | } 69 | } 70 | 71 | /** 72 | * Used to figure out if we need to regenerate the graphs or not. Only used 73 | * in newGraphNotNeeded() method. Key is the request URI and value is the 74 | * number of builds for the project. 75 | */ 76 | private transient final Map requestMap = new HashMap<>(); 77 | 78 | public TapProjectAction(AbstractProject project) { 79 | super(project); 80 | } 81 | 82 | public TapProjectAction(Job job) { 83 | super(job); 84 | } 85 | 86 | public AbstractProject getProject() { 87 | return this.project; 88 | } 89 | 90 | protected Class getBuildActionClass() { 91 | return TapBuildAction.class; 92 | } 93 | 94 | public TapBuildAction getLastBuildAction() { 95 | TapBuildAction action = null; 96 | final Run lastBuild = this.getLastBuildWithTap(); 97 | 98 | if (lastBuild != null) { 99 | action = lastBuild.getAction(TapBuildAction.class); 100 | } 101 | 102 | return action; 103 | } 104 | 105 | private Run getLastBuildWithTap() { 106 | Run lastBuild = this.job.getLastBuild(); 107 | while (lastBuild != null && lastBuild.getAction(TapBuildAction.class) == null) { 108 | lastBuild = lastBuild.getPreviousBuild(); 109 | } 110 | return lastBuild; 111 | } 112 | 113 | public void doIndex( final StaplerRequest request, 114 | final StaplerResponse response ) throws IOException { 115 | Run lastBuild = this.getLastBuildWithTap(); 116 | if (lastBuild == null) { 117 | response.sendRedirect2("nodata"); 118 | } else { 119 | int buildNumber = lastBuild.getNumber(); 120 | response.sendRedirect2(String.format("../%d/%s", buildNumber, 121 | TapBuildAction.URL_NAME)); 122 | } 123 | } 124 | 125 | /** 126 | * Generates the graph that shows test pass/fail ratio. 127 | * 128 | * @param req Stapler request 129 | * @param rsp Stapler response 130 | * @throws IOException if it fails to create the graph image and serve it 131 | */ 132 | public void doGraph( final StaplerRequest req, StaplerResponse rsp ) throws IOException { 133 | if (newGraphNotNeeded(req, rsp)) { 134 | return; 135 | } 136 | 137 | final DataSetBuilder dataSetBuilder = new DataSetBuilder<>(); 138 | 139 | populateDataSetBuilder(dataSetBuilder); 140 | new hudson.util.Graph(-1, getGraphWidth(), getGraphHeight()) { 141 | protected JFreeChart createGraph() { 142 | return GraphHelper.createChart(req, dataSetBuilder.build()); 143 | } 144 | }.doPng(req, rsp); 145 | } 146 | 147 | public void doGraphMap( final StaplerRequest req, StaplerResponse rsp ) throws IOException { 148 | if (newGraphNotNeeded(req, rsp)) { 149 | return; 150 | } 151 | 152 | final DataSetBuilder dataSetBuilder = new DataSetBuilder<>(); 153 | 154 | // TODO: optimize by using cache 155 | populateDataSetBuilder(dataSetBuilder); 156 | new hudson.util.Graph(-1, getGraphWidth(), getGraphHeight()) { 157 | protected JFreeChart createGraph() { 158 | return GraphHelper.createChart(req, dataSetBuilder.build()); 159 | } 160 | }.doMap(req, rsp); 161 | } 162 | 163 | /** 164 | * Returns true if there is a graph to plot. 165 | * 166 | * @return value for property 'graphAvailable' 167 | */ 168 | public boolean isGraphActive() { 169 | Run build = this.job.getLastBuild(); 170 | // in order to have a graph, we must have at least two points. 171 | int numPoints = 0; 172 | while (numPoints < 2) { 173 | if (build == null) { 174 | return false; 175 | } 176 | if( this.job instanceof MatrixProject ) 177 | { 178 | MatrixProject mp = (MatrixProject) this.job; 179 | 180 | for (Job j : mp.getAllJobs()) { 181 | if (j != mp) { //getAllJobs includes the parent job too, so skip that 182 | Run sub = j.getBuild(build.getId()); 183 | if(sub != null) { 184 | // Not all builds are on all subprojects 185 | if (sub.getAction(getBuildActionClass()) != null) { 186 | //data for at least 1 sub-job on this build 187 | numPoints++; 188 | break; // go look at the next build now 189 | } 190 | } 191 | } 192 | } 193 | } 194 | else 195 | { 196 | if (build.getAction(getBuildActionClass()) != null) { 197 | numPoints++; 198 | } 199 | } 200 | build = build.getPreviousBuild(); 201 | } 202 | return true; 203 | } 204 | 205 | /** 206 | * If number of builds hasn't changed and if checkIfModified() returns true, 207 | * no need to regenerate the graph. Browser should reuse it's cached image 208 | * 209 | * @param req Stapler request 210 | * @param rsp Stapler response 211 | * @return true, if new image does NOT need to be generated, false otherwise 212 | */ 213 | private boolean newGraphNotNeeded( final StaplerRequest req, StaplerResponse rsp ) { 214 | final Calendar t = this.job.getLastCompletedBuild().getTimestamp(); 215 | final int prevNumBuilds = requestMap.getOrDefault(req.getRequestURI(), 0); 216 | final int numBuilds = (int) this.job.getBuilds().stream().count(); 217 | 218 | if (prevNumBuilds != numBuilds) { 219 | requestMap.put(req.getRequestURI(), numBuilds); 220 | } 221 | 222 | if (requestMap.keySet().size() > 10) { 223 | // keep map size in check 224 | requestMap.clear(); 225 | } 226 | 227 | /* 228 | * checkIfModified() is after '&&' because we want it evaluated only 229 | * if number of builds is different 230 | */ 231 | return prevNumBuilds == numBuilds && req.checkIfModified(t, rsp); 232 | } 233 | 234 | protected void populateDataSetBuilder(DataSetBuilder dataset ) { 235 | 236 | Job p = this.job; 237 | 238 | for (Run build = this.job.getLastBuild(); build != null; build = build.getPreviousBuild()) { 239 | 240 | /* 241 | * The build has most likely failed before any TAP data was recorded. 242 | * 243 | * If we don't exclude such builds, we'd have to account for that in GraphHelper. Besides, that, it's not 244 | * consistent with JUnit graph behaviour where builds without test results are not included in graph. 245 | */ 246 | if (build.getAction(TapBuildAction.class) == null) { 247 | continue; 248 | } 249 | 250 | ChartUtil.NumberOnlyBuildLabel label = new ChartUtil.NumberOnlyBuildLabel(build); 251 | 252 | Result r = new Result(); 253 | 254 | if( p instanceof MatrixProject ) 255 | { 256 | MatrixProject mp = (MatrixProject) p; 257 | 258 | for (Job j : mp.getAllJobs()) { 259 | if (j != mp) { //getAllJobs includes the parent job too, so skip that 260 | Run sub = j.getBuild(build.getId()); 261 | if(sub != null) { 262 | // Not all builds are on all subprojects 263 | r.add(summarizeBuild(sub)); 264 | } 265 | } 266 | } 267 | } 268 | else 269 | { 270 | r = summarizeBuild(build); 271 | } 272 | 273 | dataset.add(r.numPassed, "Passed", label); 274 | dataset.add(r.numFailed, "Failed", label); 275 | dataset.add(r.numSkipped, "Skipped", label); 276 | dataset.add(r.numToDo, "ToDo", label); 277 | } 278 | } 279 | 280 | protected Result summarizeBuild(Run b) 281 | { 282 | Result r = new Result(); 283 | 284 | TapBuildAction action = b.getAction(getBuildActionClass()); 285 | if (action != null) { 286 | TapResult report = action.getResult(); 287 | report.tally(); 288 | 289 | r.numPassed = report.getPassed(); 290 | r.numFailed = report.getFailed(); 291 | r.numSkipped = report.getSkipped(); 292 | r.numToDo = report.getToDo(); 293 | } 294 | 295 | return r; 296 | } 297 | /** 298 | * Getter for property 'graphWidth'. 299 | * 300 | * @return Value for property 'graphWidth'. 301 | */ 302 | public int getGraphWidth() { 303 | return 500; 304 | } 305 | 306 | /** 307 | * Getter for property 'graphHeight'. 308 | * 309 | * @return Value for property 'graphHeight'. 310 | */ 311 | public int getGraphHeight() { 312 | return 200; 313 | } 314 | 315 | } 316 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/util/GraphHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2013 Bruno P. Kinoshita 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.tap4j.plugin.util; 25 | 26 | import hudson.util.ChartUtil.NumberOnlyBuildLabel; 27 | import hudson.util.ColorPalette; 28 | import hudson.util.ShiftedCategoryAxis; 29 | import hudson.util.StackedAreaRenderer2; 30 | import org.jfree.chart.ChartFactory; 31 | import org.jfree.chart.JFreeChart; 32 | import org.jfree.chart.axis.CategoryAxis; 33 | import org.jfree.chart.axis.CategoryLabelPositions; 34 | import org.jfree.chart.axis.NumberAxis; 35 | import org.jfree.chart.plot.CategoryPlot; 36 | import org.jfree.chart.plot.PlotOrientation; 37 | import org.jfree.chart.renderer.category.BarRenderer; 38 | import org.jfree.chart.renderer.category.StackedAreaRenderer; 39 | import org.jfree.chart.title.LegendTitle; 40 | import org.jfree.data.category.CategoryDataset; 41 | import org.jfree.ui.RectangleEdge; 42 | import org.jfree.ui.RectangleInsets; 43 | import org.kohsuke.stapler.StaplerRequest; 44 | import org.kohsuke.stapler.StaplerResponse; 45 | import org.tap4j.plugin.AbstractTapProjectAction; 46 | import org.tap4j.plugin.TapBuildAction; 47 | import org.tap4j.plugin.TapResult; 48 | 49 | import java.awt.*; 50 | import java.io.IOException; 51 | import java.util.HashMap; 52 | import java.util.Map; 53 | 54 | /** 55 | * Helper class for trend graph generation. 56 | * 57 | * @author TestNG plug-in 58 | * @since 1.0 59 | */ 60 | public class GraphHelper 61 | { 62 | 63 | /** 64 | * Do not instantiate GraphHelper. 65 | */ 66 | private GraphHelper() 67 | { 68 | super(); 69 | } 70 | 71 | public static void redirectWhenGraphUnsupported( StaplerResponse rsp, 72 | StaplerRequest req ) throws IOException 73 | { 74 | // not available. send out error message 75 | rsp.sendRedirect2(req.getContextPath() + "/images/headless.png"); 76 | } 77 | 78 | public static JFreeChart createChart(StaplerRequest req, CategoryDataset dataset) { 79 | 80 | final JFreeChart chart = ChartFactory.createStackedAreaChart( 81 | "TAP Tests", // chart title 82 | null, // unused 83 | "TAP Tests Count", // range axis label 84 | dataset, // data 85 | PlotOrientation.VERTICAL, // orientation 86 | true, // include legend 87 | true, // tooltips 88 | false // urls 89 | ); 90 | 91 | // NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART... 92 | final LegendTitle legend = chart.getLegend(); 93 | legend.setPosition(RectangleEdge.RIGHT); 94 | 95 | chart.setBackgroundPaint(Color.white); 96 | 97 | final CategoryPlot plot = chart.getCategoryPlot(); 98 | plot.setBackgroundPaint(Color.WHITE); 99 | plot.setOutlinePaint(null); 100 | plot.setForegroundAlpha(0.8f); 101 | plot.setDomainGridlinesVisible(true); 102 | plot.setDomainGridlinePaint(Color.white); 103 | plot.setRangeGridlinesVisible(true); 104 | plot.setRangeGridlinePaint(Color.black); 105 | 106 | CategoryAxis domainAxis = new ShiftedCategoryAxis(null); 107 | plot.setDomainAxis(domainAxis); 108 | domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); 109 | domainAxis.setLowerMargin(0.0); 110 | domainAxis.setUpperMargin(0.0); 111 | domainAxis.setCategoryMargin(0.0); 112 | 113 | final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); 114 | rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 115 | 116 | StackedAreaRenderer ar = new StackedAreaRenderer2() { 117 | private static final long serialVersionUID = 331915263367089058L; 118 | 119 | @Override 120 | public String generateURL(CategoryDataset dataset, int row, int column) { 121 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset.getColumnKey(column); 122 | return label.getRun().getNumber() + "/" + AbstractTapProjectAction.URL_NAME + "/"; 123 | } 124 | 125 | @Override 126 | public String generateToolTip(CategoryDataset dataset, int row, int column) 127 | { 128 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset.getColumnKey(column); 129 | TapBuildAction build = label.getRun().getAction(TapBuildAction.class); 130 | TapResult report = build.getResult(); 131 | report.tally(); 132 | 133 | switch (row) { 134 | case 0: 135 | return report.getFailed() + " Failure(s)"; 136 | case 1: 137 | return report.getPassed() + " Pass"; 138 | case 2: 139 | return report.getSkipped() + " Skip(s)"; 140 | case 3: 141 | return report.getToDo() + " ToDo(s)"; 142 | default: 143 | return ""; 144 | } 145 | } 146 | 147 | @Override 148 | public boolean equals(Object obj) { 149 | return super.equals(obj); 150 | } 151 | }; 152 | 153 | plot.setRenderer(ar); 154 | ar.setSeriesPaint(0, ColorPalette.RED); // Failures 155 | ar.setSeriesPaint(1, ColorPalette.BLUE); // Pass 156 | ar.setSeriesPaint(2, ColorPalette.YELLOW); // Skips 157 | ar.setSeriesPaint(3, Color.CYAN); // ToDo 158 | 159 | // crop extra space around the graph 160 | plot.setInsets(new RectangleInsets(0, 0, 0, 5.0)); 161 | 162 | return chart; 163 | } 164 | 165 | /** 166 | * Creates the graph displayed on Method results page to compare execution 167 | * duration and status of a test method across builds. 168 | * 169 | * At max, 9 older builds are displayed. 170 | * 171 | * @param req 172 | * request 173 | * @param dataset 174 | * data set to be displayed on the graph 175 | * @param statusMap 176 | * a map with build as key and the test method's execution status 177 | * (result) as the value 178 | * @param methodUrl 179 | * URL to get to the method from a build test result page 180 | * @return the chart 181 | */ 182 | public static JFreeChart createMethodChart( StaplerRequest req, 183 | final CategoryDataset dataset, 184 | final Map statusMap, 185 | final String methodUrl ) 186 | { 187 | 188 | final JFreeChart chart = ChartFactory.createBarChart(null, // chart 189 | // title 190 | null, // unused 191 | "� Duration (secs)",// range axis label 192 | dataset, // data 193 | PlotOrientation.VERTICAL, // orientation 194 | true, // include legend 195 | true, // tooltips 196 | true // urls 197 | ); 198 | 199 | // NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART... 200 | chart.setBackgroundPaint(Color.white); 201 | chart.removeLegend(); 202 | 203 | final CategoryPlot plot = chart.getCategoryPlot(); 204 | plot.setBackgroundPaint(Color.WHITE); 205 | plot.setOutlinePaint(null); 206 | plot.setForegroundAlpha(0.8f); 207 | plot.setDomainGridlinesVisible(true); 208 | plot.setDomainGridlinePaint(Color.white); 209 | plot.setRangeGridlinesVisible(true); 210 | plot.setRangeGridlinePaint(Color.black); 211 | 212 | CategoryAxis domainAxis = new ShiftedCategoryAxis(null); 213 | plot.setDomainAxis(domainAxis); 214 | domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); 215 | domainAxis.setLowerMargin(0.0); 216 | domainAxis.setUpperMargin(0.0); 217 | domainAxis.setCategoryMargin(0.0); 218 | 219 | final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); 220 | rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 221 | 222 | BarRenderer br = new BarRenderer() 223 | { 224 | 225 | private static final long serialVersionUID = 961671076462240008L; 226 | final Map statusPaintMap = new HashMap<>(); 227 | 228 | { 229 | statusPaintMap.put("PASS", ColorPalette.BLUE); 230 | statusPaintMap.put("SKIP", ColorPalette.YELLOW); 231 | statusPaintMap.put("FAIL", ColorPalette.RED); 232 | statusPaintMap.put("TODO", ColorPalette.GREY); 233 | } 234 | 235 | /** 236 | * Returns the paint for an item. Overrides the default behavior 237 | * inherited from AbstractSeriesRenderer. 238 | * 239 | * @param row 240 | * the series. 241 | * @param column 242 | * the category. 243 | * 244 | * @return The item color. 245 | */ 246 | public Paint getItemPaint( final int row, final int column ) 247 | { 248 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset 249 | .getColumnKey(column); 250 | Paint paint = statusPaintMap.get(statusMap.get(label)); 251 | // when the status of test method is unknown, use gray color 252 | return paint == null ? Color.gray : paint; 253 | } 254 | 255 | @Override 256 | public boolean equals(Object obj) { 257 | return super.equals(obj); 258 | } 259 | 260 | @Override 261 | public int hashCode() { 262 | return super.hashCode(); 263 | } 264 | }; 265 | 266 | br.setBaseToolTipGenerator((dataset1, row, column) -> { 267 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset1 268 | .getColumnKey(column); 269 | if ("UNKNOWN".equals(statusMap.get(label))) 270 | { 271 | return "unknown"; 272 | } 273 | // values are in seconds 274 | return dataset1.getValue(row, column) + " secs"; 275 | }); 276 | 277 | br.setBaseItemURLGenerator((dataset12, series, category) -> { 278 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset12 279 | .getColumnKey(category); 280 | if ("UNKNOWN".equals(statusMap.get(label))) 281 | { 282 | // no link when method result doesn't exist 283 | return null; 284 | } 285 | // return label.build.getUpUrl() + label.build.getNumber() + "/" + PluginImpl.URL + "/" + methodUrl; 286 | return label.build.getUpUrl() + label.getRun().getNumber() + "/tap/" + methodUrl; 287 | }); 288 | 289 | br.setItemMargin(0.0); 290 | br.setMinimumBarLength(5); 291 | // set the base to be 1/100th of the maximum value displayed in the 292 | // graph 293 | br.setBase(br.findRangeBounds(dataset).getUpperBound() / 100); 294 | plot.setRenderer(br); 295 | 296 | // crop extra space around the graph 297 | plot.setInsets(new RectangleInsets(0, 0, 0, 5.0)); 298 | return chart; 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/org/tap4j/plugin/model/TapTestResultResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2012 Bruno P. Kinoshita 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.tap4j.plugin.model; 25 | 26 | import hudson.Functions; 27 | import hudson.model.AbstractBuild; 28 | import hudson.model.Item; 29 | import hudson.model.Run; 30 | import hudson.tasks.test.TestObject; 31 | import hudson.tasks.test.TestResult; 32 | import jenkins.model.Jenkins; 33 | import org.apache.commons.lang.StringUtils; 34 | import org.kohsuke.stapler.Stapler; 35 | import org.kohsuke.stapler.StaplerRequest; 36 | import org.tap4j.model.Comment; 37 | import org.tap4j.model.Directive; 38 | import org.tap4j.model.TestSet; 39 | import org.tap4j.plugin.TapResult; 40 | import org.tap4j.plugin.TapTestResultAction; 41 | import org.tap4j.plugin.util.Util; 42 | import org.tap4j.util.DirectiveValues; 43 | 44 | import javax.annotation.CheckForNull; 45 | import javax.annotation.Nullable; 46 | import java.io.StringWriter; 47 | import java.io.UnsupportedEncodingException; 48 | import java.net.URLEncoder; 49 | import java.util.ArrayList; 50 | import java.util.List; 51 | import java.util.Map; 52 | import java.util.logging.Logger; 53 | 54 | /** 55 | * 56 | * @since 0.1 57 | */ 58 | public class TapTestResultResult extends TestResult { 59 | 60 | private static final String DURATION_KEY = "duration_ms"; 61 | 62 | private static final long serialVersionUID = -4499261655602135921L; 63 | private static final Logger LOGGER = Logger.getLogger(TapTestResultResult.class.getName()); 64 | 65 | private final transient Run owner; 66 | private final org.tap4j.model.TestResult tapTestResult; 67 | private final TestSetMap testSetMap; 68 | private final Boolean todoIsFailure; 69 | private final Boolean includeCommentDiagnostics; 70 | private final Boolean validateNumberOfTests; 71 | 72 | public TapTestResultResult(Run owner, TestSetMap testSetMap, org.tap4j.model.TestResult tapTestResult, 73 | Boolean todoIsFailure, Boolean includeCommentDiagnostics, Boolean validateNumberOfTests) { 74 | this.owner = owner; 75 | this.testSetMap = testSetMap; 76 | this.tapTestResult = tapTestResult; 77 | this.todoIsFailure = todoIsFailure; 78 | this.includeCommentDiagnostics = includeCommentDiagnostics; 79 | this.validateNumberOfTests = validateNumberOfTests; 80 | } 81 | 82 | /* (non-Javadoc) 83 | * @see hudson.model.ModelObject#getDisplayName() 84 | */ 85 | public String getDisplayName() { 86 | return getName(); 87 | } 88 | 89 | /* (non-Javadoc) 90 | * @see hudson.tasks.test.TestObject#getOwner() 91 | */ 92 | @Nullable 93 | @Override 94 | public Run getRun() { 95 | return owner; 96 | } 97 | 98 | /* (non-Javadoc) 99 | * @see hudson.tasks.test.TestObject#getParent() 100 | */ 101 | @Override 102 | public TestObject getParent() { 103 | TapStreamResult parent = null; 104 | TestSet testSet = this.tapTestResult.getSubtest(); 105 | if(testSet != null) { 106 | TestSetMap subTest = new TestSetMap(testSetMap.getFileName(), testSet); 107 | List list = new ArrayList<>(); 108 | list.add(subTest); 109 | parent = new TapStreamResult(owner, new TapResult("TAP Test Results", owner, list, todoIsFailure, includeCommentDiagnostics, validateNumberOfTests)); 110 | } 111 | return parent; 112 | } 113 | 114 | /* (non-Javadoc) 115 | * @see hudson.tasks.test.TestObject#findCorrespondingResult(java.lang.String) 116 | */ 117 | @Override 118 | public TestResult findCorrespondingResult(String id) { 119 | if(this.getDisplayName().equals(id)) { 120 | return this; 121 | } 122 | return null; 123 | } 124 | 125 | /* (non-Javadoc) 126 | * @see hudson.tasks.test.TestObject#getName() 127 | */ 128 | @Override 129 | public String getName() { 130 | StringBuilder buf = new StringBuilder(); 131 | buf.append(tapTestResult.getTestNumber()); 132 | String tapTestResultDescription = tapTestResult.getDescription(); 133 | if (StringUtils.isNotBlank(tapTestResultDescription)) { 134 | buf.append(" - "); 135 | buf.append(tapTestResultDescription); 136 | } 137 | return buf.toString(); 138 | } 139 | 140 | public String getStatus() { 141 | boolean failure = Util.isFailure(this.tapTestResult, todoIsFailure); 142 | return failure ? "NOT OK" : "OK"; 143 | } 144 | 145 | public String getSkip() { 146 | boolean skip = Util.isSkipped(this.tapTestResult); 147 | return skip ? "Yes" : "No"; 148 | } 149 | 150 | public String getTodo() { 151 | String todo = "No"; 152 | // TODO: not consistent with the other methods in TapResult 153 | Directive directive = this.tapTestResult.getDirective(); 154 | if(directive != null) { 155 | if(directive.getDirectiveValue() == DirectiveValues.TODO) { 156 | todo = "Yes"; 157 | } 158 | } 159 | return todo; 160 | } 161 | 162 | public String getFullName() { 163 | return getName(); 164 | } 165 | 166 | public String getRelativePathFrom(TestObject it) { 167 | // if (it is one of my ancestors) { 168 | // return a relative path from it 169 | // } else { 170 | // return a complete path starting with "/" 171 | // } 172 | if (it==this) { 173 | return "."; 174 | } 175 | 176 | StringBuilder buf = new StringBuilder(); 177 | TestObject next = this; 178 | TestObject cur = this; 179 | // Walk up my ancestors from leaf to root, looking for "it" 180 | // and accumulating a relative url as I go 181 | while (next!=null && it!=next) { 182 | cur = next; 183 | String safeName = cur.getSafeName(); 184 | 185 | if (!"(empty)".equals(safeName)) { 186 | buf.insert(0, '/'); 187 | buf.insert(0, safeName); 188 | } 189 | next = cur.getParent(); 190 | } 191 | if (it != next) { 192 | // Keep adding on to the string we've built so far 193 | 194 | // Start with the test result action 195 | TapTestResultAction action = getTestResultActionDiverged(); 196 | if (action == null) { 197 | //LOGGER.warning("trying to get relative path, but we can't determine the action that owns this result."); 198 | return ""; // this won't take us to the right place, but it also won't 404. 199 | } 200 | buf.insert(0, '/'); 201 | buf.insert(0, action.getUrlName()); 202 | 203 | // Now the build 204 | AbstractBuild myBuild = cur.getOwner(); 205 | if (myBuild == null) { 206 | //LOGGER.warning("trying to get relative path, but we can't determine the build that owns this result."); 207 | return ""; // this won't take us to the right place, but it also won't 404. 208 | } 209 | //buf.insert(0,'/'); 210 | buf.insert(0, myBuild.getUrl()); 211 | 212 | // If we're inside a stapler request, just delegate to Hudson.Functions to get the relative path! 213 | StaplerRequest req = Stapler.getCurrentRequest(); 214 | if (req != null && myBuild instanceof Item) { 215 | buf.insert(0, '/'); 216 | // Ugly but I don't see how else to convince the compiler that myBuild is an Item 217 | Item myBuildAsItem = (Item) myBuild; 218 | buf.insert(0, Functions.getRelativeLinkTo(myBuildAsItem)); 219 | } else { 220 | // We're not in a stapler request. Okay, give up. 221 | //LOGGER.info("trying to get relative path, but it is not my ancestor, and we're not in a stapler request. Trying absolute hudson url..."); 222 | String hudsonRootUrl = Jenkins.getInstance().getRootUrl(); 223 | if (hudsonRootUrl == null || hudsonRootUrl.length() == 0) { 224 | //LOGGER.warning("Can't find anything like a decent hudson url. Punting, returning empty string."); 225 | return ""; 226 | 227 | } 228 | //buf.insert(0, '/'); 229 | buf.insert(0, hudsonRootUrl); 230 | } 231 | 232 | //LOGGER.info("Here's our relative path: " + buf.toString()); 233 | } 234 | return buf.toString(); 235 | 236 | } 237 | 238 | /* (non-Javadoc) 239 | * @see hudson.tasks.test.TestObject#getSafeName() 240 | */ 241 | @Override 242 | public String getSafeName() { 243 | String safeName = testSetMap.getFileName() + "-" + tapTestResult.getTestNumber(); 244 | try { 245 | safeName = URLEncoder.encode(safeName, "UTF-8"); 246 | } catch (UnsupportedEncodingException uee) { 247 | LOGGER.warning(uee.getMessage()); 248 | } 249 | return safeName; 250 | } 251 | 252 | /* (non-Javadoc) 253 | * @see hudson.tasks.test.TestResult#getTitle() 254 | */ 255 | public String getTitle() { 256 | return getName(); 257 | } 258 | 259 | public float getDuration() { 260 | Map diagnostic = this.tapTestResult.getDiagnostic(); 261 | // FIXME: code duplication. Refactor it and TapResult 262 | if (diagnostic != null && ! diagnostic.isEmpty()) { 263 | Object duration = diagnostic.get(DURATION_KEY); 264 | if (duration != null) { 265 | float durationMS = Float.parseFloat(duration.toString()); 266 | return durationMS / 1000; 267 | } 268 | } 269 | return super.getDuration(); 270 | } 271 | 272 | /* (non-Javadoc) 273 | * @see java.lang.Object#toString() 274 | */ 275 | @Override 276 | public String toString() { 277 | StringWriter pw = new StringWriter(); 278 | pw.append(tapTestResult.getStatus().toString()); 279 | if (tapTestResult.getTestNumber() != null) { 280 | pw.append(' ').append(Integer.toString(tapTestResult.getTestNumber())); 281 | } 282 | if (StringUtils.isNotBlank(tapTestResult.getDescription())) { 283 | pw.append(' ').append(tapTestResult.getDescription()); 284 | } 285 | if (tapTestResult.getDirective() != null) { 286 | pw.append(" # ").append( 287 | tapTestResult.getDirective().getDirectiveValue().toString()); 288 | if (StringUtils.isNotBlank(tapTestResult.getDirective().getReason())) { 289 | pw.append(' ').append(tapTestResult.getDirective().getReason()); 290 | } 291 | } 292 | List comments = tapTestResult.getComments(); 293 | if (comments.size() > 0) { 294 | for(Comment comment : comments) { 295 | if(comment.isInline()) { 296 | pw.append(' '); 297 | pw.append("# ").append(comment.getText()); 298 | } else { 299 | pw.append("\n"); 300 | pw.append("# ").append(comment.getText()); 301 | } 302 | } 303 | } 304 | return pw.toString(); 305 | } 306 | 307 | /** 308 | * This is mostly a verbatim from {@link TestObject#getTestResultAction()}, with the only difference in a return 309 | * type. 310 | * 311 | * @return associated TAP test result action object 312 | */ 313 | @CheckForNull 314 | private TapTestResultAction getTestResultActionDiverged() { 315 | Run owner = getRun(); 316 | if (owner != null) { 317 | return owner.getAction(TapTestResultAction.class); 318 | } else { 319 | LOGGER.warning("owner is null when trying to getTestResultActionDiverged."); 320 | return null; 321 | } 322 | } 323 | } 324 | --------------------------------------------------------------------------------