├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── news.md ├── releases └── Release_Notes.txt └── src ├── main ├── groovy │ └── com │ │ └── athaydes │ │ └── spockframework │ │ └── report │ │ ├── IReportCreator.groovy │ │ ├── SpockReportExtension.groovy │ │ ├── extension │ │ └── SpockReportsSpecificationExtension.groovy │ │ ├── internal │ │ ├── AbstractHtmlCreator.groovy │ │ ├── ConfigLoader.groovy │ │ ├── CssResource.groovy │ │ ├── HtmlReportAggregator.groovy │ │ ├── HtmlReportCreator.groovy │ │ ├── KnowsWhenAndWhoRanTest.groovy │ │ ├── MultiReportCreator.groovy │ │ ├── ProblemBlockWriter.groovy │ │ ├── ReportDataAggregator.groovy │ │ ├── SpecData.groovy │ │ ├── SpecSummaryNameOption.groovy │ │ ├── SpockReportsConfiguration.groovy │ │ ├── StringFormatHelper.groovy │ │ └── StringTemplateProcessor.groovy │ │ ├── template │ │ ├── TemplateReportAggregator.groovy │ │ └── TemplateReportCreator.groovy │ │ ├── util │ │ ├── Hasher.groovy │ │ └── Utils.groovy │ │ └── vivid │ │ ├── SpecSourceCode.groovy │ │ ├── SpecSourceCodeCollector.groovy │ │ ├── SpecSourceCodeReader.groovy │ │ └── VividAstInspector.groovy └── resources │ ├── META-INF │ ├── groovy │ │ └── org.codehaus.groovy.runtime.ExtensionModule │ └── services │ │ └── org.spockframework.runtime.extension.IGlobalExtension │ ├── com │ └── athaydes │ │ └── spockframework │ │ └── report │ │ └── internal │ │ └── config.properties │ ├── spock-feature-report.css │ ├── spock-summary-report.css │ └── templateReportCreator │ ├── spec-template.md │ └── summary-template.md └── test ├── groovy └── com │ └── athaydes │ └── spockframework │ └── report │ ├── FakeTest.groovy │ ├── FullyIgnoredSpec.groovy │ ├── ReportSpec.groovy │ ├── SpecIncludingExtraInfo.groovy │ ├── SpockReportExtensionSpec.groovy │ ├── UnrolledSpec.groovy │ ├── VividFakeTest.groovy │ ├── engine │ └── CanRunSpockSpecs.groovy │ ├── hierarchy_tests │ ├── ChildSpec.groovy │ └── ParentSpec.groovy │ ├── internal │ ├── ConfigLoaderSpec.groovy │ ├── HtmlReportAggregatorSpec.groovy │ ├── HtmlReportCreatorSpec.groovy │ ├── ProblemBlockWriterSpec.groovy │ ├── SimulatedReportWriter.groovy │ ├── StringFormatHelperSpec.groovy │ ├── StringTemplateProcessorSpec.groovy │ ├── TestHelper.groovy │ └── VividAstInspectorSpec.groovy │ ├── template │ └── TemplateReportCreatorSpec.groovy │ ├── util │ └── UtilsSpec.groovy │ └── vivid │ └── SpecSourceCodeSpec.groovy └── resources ├── FakeTest.md ├── VividFakeTest.md └── com └── athaydes └── spockframework └── report └── internal ├── FakeTestReport.html ├── FullyIgnoredSpecReport.html ├── SingleTestSummaryReport.html ├── SpecIncludingExtraInfoReport.html ├── TestSummaryReport.html ├── UnrolledSpecReport.html └── VividFakeTestReport.html /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build And Test on All OSs 2 | on: [push, pull_request] 3 | jobs: 4 | gradle: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest, windows-latest] 8 | java: [1.8, 11, 17] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-java@v1 13 | with: 14 | java-version: ${{ matrix.java }} 15 | - uses: eskatos/gradle-command-action@v1 16 | with: 17 | arguments: check 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | lib/ 4 | out/ 5 | target/ 6 | .gradle/ 7 | .idea/ 8 | *.class 9 | *.iml 10 | *.ipr 11 | *.iws 12 | 13 | !gradlew-wrapper.jar -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 --- Renato Athaydes --- 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'groovy' 4 | id 'idea' 5 | id 'maven-publish' 6 | id 'signing' 7 | } 8 | 9 | defaultTasks 'check' 10 | 11 | group = 'com.athaydes' 12 | version = "2.5.1-groovy-4.0" 13 | description = 'This project is a global extension for Spock to create test (or, in Spock terms, Specifications) reports.' 14 | 15 | sourceCompatibility = '1.8' 16 | targetCompatibility = '1.8' 17 | 18 | def groovyVersion = '4.0.13' 19 | def spockVersion = '2.3-groovy-4.0' 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | api "org.apache.groovy:groovy:${groovyVersion}" 27 | implementation "org.apache.groovy:groovy-xml:${groovyVersion}" 28 | implementation "org.apache.groovy:groovy-json:${groovyVersion}" 29 | implementation "org.apache.groovy:groovy-templates:${groovyVersion}" 30 | implementation platform( "org.spockframework:spock-bom:$spockVersion" ) 31 | api "org.spockframework:spock-core", { 32 | exclude group: 'org.codehaus.groovy' 33 | } 34 | implementation 'org.slf4j:slf4j-api:1.7.36' 35 | testImplementation platform( 'org.junit:junit-bom:5.9.1' ) 36 | testImplementation 'org.junit.platform:junit-platform-testkit' 37 | testImplementation 'org.junit.jupiter:junit-jupiter' 38 | testImplementation "cglib:cglib-nodep:3.3.0" 39 | testImplementation "org.slf4j:slf4j-simple:1.7.36" 40 | } 41 | 42 | test { 43 | useJUnitPlatform() 44 | 45 | maxParallelForks = 4 46 | exclude '**/*FakeTest.class', '**/UnrolledSpec.class', '**/SpecIncludingExtraInfo.class' 47 | systemProperty 'project.buildDir', project.buildDir 48 | systemProperty 'org.slf4j.simpleLogger.defaultLogLevel', 'debug' 49 | testLogging.showStandardStreams = true 50 | beforeTest { descriptor -> 51 | logger.lifecycle( "Running test [{}]: {}", Thread.currentThread().id, descriptor ) 52 | } 53 | testLogging { 54 | events "failed" 55 | exceptionFormat "full" 56 | showStackTraces = true 57 | } 58 | } 59 | 60 | jar { 61 | manifest { 62 | attributes( 63 | "Implementation-Title": "Athaydes-Spock-Reports", 64 | "Implementation-Version": project.version, 65 | "Automatic-Module-Name": 'com.athaydes.spock.reports' ) 66 | } 67 | } 68 | 69 | java { 70 | withJavadocJar() 71 | withSourcesJar() 72 | } 73 | 74 | javadoc { 75 | if ( JavaVersion.current().isJava9Compatible() ) { 76 | options.addBooleanOption( 'html5', true ) 77 | } 78 | } 79 | 80 | def getProjectProperty = { String propertyName -> 81 | project.properties[ propertyName ] 82 | } 83 | 84 | publishing { 85 | publications { 86 | mavenJava( MavenPublication ) { 87 | artifactId = 'spock-reports' 88 | from components.java 89 | versionMapping { 90 | usage( 'java-api' ) { 91 | fromResolutionOf( 'runtimeClasspath' ) 92 | } 93 | usage( 'java-runtime' ) { 94 | fromResolutionResult() 95 | } 96 | } 97 | pom { 98 | inceptionYear = '2013' 99 | name = project.name 100 | packaging = 'jar' 101 | description = project.description 102 | 103 | url = 'https://github.com/renatoathaydes/spock-reports' 104 | 105 | scm { 106 | connection = 'git@github.com:renatoathaydes/spock-reports.git' 107 | developerConnection = 'git@github.com:renatoathaydes/spock-reports.git' 108 | url = 'https://github.com/renatoathaydes/spock-reports' 109 | } 110 | 111 | licenses { 112 | license { 113 | name = 'The Apache License, Version 2.0' 114 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 115 | } 116 | } 117 | 118 | developers { 119 | developer { 120 | id = 'renatoathaydes' 121 | name = 'Renato Athaydes' 122 | email = 'renato@athaydes.com' 123 | } 124 | } 125 | } 126 | } 127 | } 128 | repositories { 129 | maven { 130 | url "https://oss.sonatype.org/service/local/staging/deploy/maven2" 131 | credentials { 132 | username getProjectProperty( 'ossrhUsername' )?.toString() 133 | password getProjectProperty( 'ossrhPassword' )?.toString() 134 | } 135 | } 136 | } 137 | } 138 | 139 | signing { 140 | if ( project.hasProperty( 'sign' ) ) { 141 | sign publishing.publications.mavenJava 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renatoathaydes/spock-reports/b2bc0f85f5ee5023a5d69128497dfa90567df1de/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /releases/Release_Notes.txt: -------------------------------------------------------------------------------- 1 | 2.5.1-groovy-4 - 2023 August 21 2 | 3 | * Fixed bug in default template report. 4 | 5 | 2.5.0-groovy-4 - 2023 May 03 6 | 7 | * Fixed how Unrolled feature is determined (changed in Spock 2 to be the default). 8 | * Fixed #244 `reportInfo` added to the correct iteration on `@RollUp` test. 9 | * Fixed feature header to stop showing iteration values, as values as shown in a table already. 10 | * Made `Utils` class almost entirely `@CompileStatic` for better performance and safety. 11 | * Changed `FAILURE` to `FAIL` in template reports for consistency. 12 | * Improved unrolled spec features display names. 13 | 14 | 2.4.0-groovy-4 - 2022 Oct 30 15 | 16 | * Upgrade Groovy version to 4.0.6. 17 | * Minor performance improvements and quieter logging. 18 | 19 | 2.3.2-groovy-3.0 - 2022 Nov 14 20 | 21 | * #235 fixed indentation of source code in vivid reports. 22 | * #237 keep `where` block source code available for inclusion in template reports (thanks to @Gleethos). 23 | 24 | 2.3.1-groovy-3.0 - 2022 Aug 25 25 | 26 | * #231 fixed disappearing blocks in vivid reports when spec uses annotations (thanks to @ArkiMargust). 27 | * #228 more efficient generation of aggregated report. 28 | * upgraded Spock to 2.1-groovy-3.0. 29 | 30 | 2.3.0-groovy-3.0 - 2022 Feb 06 31 | 32 | * #222 fixed null-pointer when executing child specification (bug introduced in version `2.1-groovy-3.0`). 33 | * execution times were added to every feature and iteration. 34 | * styles for TOC-Return link were moved from HTML to CSS (`return-toc` CSS class). 35 | 36 | 2.2.0-groovy-3.0 - 2022 Jan 30 37 | 38 | * #219 concurrent test execution caused reportInfo to put information in wrong feature/iteration. 39 | * #219 **breaking change** on report templates. Please see this [commit](https://github.com/renatoathaydes/spock-reports/commit/d59834e3917725f70c63871d86975702b9b496ab) if your template breaks. 40 | * #220 allow `testSourceRoots` to take not only String, but also File and Iterable or Closure providing String/File's. 41 | * update Groovy version to `3.0.9` to support running on JDK 17. 42 | 43 | 2.1.1-groovy-3.0 - 2021 Nov 06 44 | 45 | * #217 Fixed concurrency bug when executing parallel tests. 46 | * #218 Fixed regression #212, null-pointer error on setup test failure. 47 | 48 | 2.1-groovy-3.0 - 2021 Sep 02 49 | 50 | * #215 Support parallel execution of features. 51 | 52 | 2.0-groovy-3.0 - 2021 May 18 53 | 54 | * Upgrade to Spock 2.0-groovy-3.0. 55 | 56 | 2.0-RC4 - 2021 May 03 57 | 58 | * Upgrade to Spock 2.0-M5-groovy-3.0. 59 | * Fixed issue #212 - error can happen when iteration fails before spec even gets initiated. 60 | * Improved reporting of cleanupSpec errors. 61 | * Better computation of errors and failures counts, as well as success rate. 62 | 63 | 2.0.1-RC3 - 2021 February 13 64 | 65 | * Fixes the POM published to Maven Central, which was incorrect in the previous version. 66 | 67 | 2.0-RC3 - 2021 January 05 68 | 69 | * Merged 1.8.0 fixes to the 2.0 branch. 70 | * Upgrade to Spock 2.0-M4-groovy-3.0. 71 | 72 | 1.8.0 - 2020 October 04 73 | 74 | * #193 added more statistics to reports. 75 | * Fixed Java 9+ module loading issue #197. 76 | 77 | 2.0-RC2 - 2020 May 15 78 | 79 | * Upgrade to Spock 2 and Groovy 3. 80 | 81 | 1.7.1 - 2020 March 10 82 | 83 | * Publishing same code as 1.7.0 but compiled with Java 8. Use this version if Java 8 is required. 84 | 85 | 1.7.0 - 2020 March 10 86 | 87 | * Upgraded Spock and Groovy versions to 1.2-groovy-2.5 and 2.5.10 respectively to support both Java 8 and Java 9+. 88 | 89 | 1.6.3 - 2020 January 04 90 | 91 | * #144 include tests originating from a jar rather than the project's source code into the reports. 92 | * #173 report correct number of failures when multiple failures are generated from same iteration, using `verifyAll`. 93 | * #174 #187 include features declared in abstract base-class in all concrete sub-types. 94 | 95 | 1.6.2 - 2019 April 10 96 | 97 | * #167 use UTF-8 to write all reports. 98 | * #169 include line number of failure in report in more cases. 99 | * #170 include ignored Specification in reports. 100 | * Added new `totalFeatures` statistic to report results and improved accuracy of feature count. 101 | 102 | 1.6.1 - 2018 September 23 103 | 104 | * #154 collect time each feature iteration takes to run, so information can be used in template reports. 105 | * #154 added feature iteration time to markdown default template report. 106 | * #155 allow location of aggregated json report file to be customized. 107 | * #157 show extra reportInfo even when feature is ignored. 108 | * tiny improvement in the appearance of example tables in HTML reports. 109 | 110 | 1.6.0 - 2018 May 30 111 | 112 | * changed log level of message showing spock-reports config from warning to info. 113 | * #147 vivid reports can include test methods which have parameters. 114 | * #146 added specification's title and narrative to aggregated data. 115 | * added spec's title to summary report. Allow configuring whether to show both name and title (default), 116 | only class name, or only title (if available). 117 | 118 | 1.5.0 - 2018 March 27 119 | 120 | * #76 support Spock's @Issue configuration (issueUrlPrefix and issueNamePrefix). 121 | * #131 Fixed bug causing OverlappingFileLockException when writing reports from multiple JVMs/Threads. 122 | * #139 removed println statements. 123 | * #138 added support for Specification parent class' code to be included in Vivid Reports. 124 | * #132 added link to the summary report (index.html) in Spec HTML reports. 125 | * #141 support for Spock's SpockConfig.groovy file to configure spock-reports. 126 | * Added Automatic-Module-Name to Manifest to stabilize Java 9's module name. 127 | 128 | 1.4.0 - 2017 December 13 129 | 130 | * #85 show data table with a single row in unrolled specifications. 131 | * #79 support multiple report creators to be configured. 132 | * #123 added extension methods to Specification class that allow information to be added to reports programmatically. 133 | 134 | 1.3.2 - 2017 September 26 135 | 136 | * #109 any config file property can now be set as a system property as well 137 | * #112 support for Spock's @PendingFeature annotation 138 | * #118 show line of code responsible for test failure in vivid reports 139 | * Java 7 support no longer tested as Gradle and Travis CI dropped support for running tests 140 | * Spock version 1.1+ now required 141 | 142 | 1.3.1 - 2017 May 18 143 | 144 | * #103 fixed NullPointerException when vivid report cannot find test sources 145 | * documented and added default property for test sources root folder 146 | 147 | 1.3.0 - 2017 April 21 148 | 149 | * #90 @Unrolled class should cause all features to unroll. 150 | * #91 better dependency declaration on Groovy (only required modules). 151 | * #92 new flag to allow disabling the generation of reports. 152 | * #88 improved the display of @Title and @Narrative in HTML reports. 153 | * Modernized the looks of the reports. 154 | * #78 show project name and version in reports. 155 | * #96 show Specification source code for blocks (vivid report) if enabled. 156 | 157 | 1.2.13 - 2016 October 02 158 | 159 | * #70 template report showed wrong index for unrolled features. 160 | * #73 ArrayIndexOutOfBoundsException for reports which faile at initialization. 161 | * #75 show error in reports even when the Spec fields fail to initialize. 162 | * #82 do not execute Runnables in the where block examples. 163 | 164 | 1.2.12 - 2016 June 09 165 | 166 | * #68 fixed NPE caused by Spec setup method throwing an Exception. 167 | * clearly indicate initialization error on specifications affected. 168 | * small performance improvements in HTML report creator. 169 | * #70 fixed index of unrolled feature items. 170 | * ToC link to unrolled features now works correctly. 171 | * #71 report shows features in execution order consistently (by @tilmanginzel). 172 | 173 | 1.2.11 - 2016 May 19 174 | 175 | * #63 #67 fixed template reports aggregator NPE 176 | * #65 fixed logging in report aggregator classes 177 | 178 | 1.2.10 - 2016 March 08 179 | 180 | * Improved contents of the aggregated_data file so it may be used by external tools. 181 | 182 | 1.2.9 - 2016 February 14 183 | 184 | * #54 optionally create separate css resources instead of inlining them in the html files. 185 | * improved Maven pom with much more information and correct dependencies declarations. 186 | * first version to be published on Maven Central as well as JCenter. 187 | * all artifacts are now signed with gpg by the Bintray public key 188 | 189 | 1.2.8 - 2015 January 6 190 | 191 | * #51 Replaced usage of @Log with @Slf4j. 192 | * new option to include the stack-trace of a Throwable that caused a test failure in HTML reports. 193 | * #49 Only make Strings given by @See and @Issue hyperlinks if they seem to be URLs. 194 | 195 | 1.2.7 - 2015 August 15 196 | 197 | * #46 Added support for showing comment on Spock's @Ignore, @Issue, @See, @Title, @Narrative annotations to reports. 198 | * Made it easier to write template reports by including more variables in the scope (see README). 199 | * Fixed misplacement of "Return" link in html reports. 200 | 201 | 1.2.6 - 2015 August 12 202 | 203 | * #43 Summary report now contains all specifications even when build runs in parallel (forked) mode. 204 | * #36 Log much more information about config and errors (use the --info flag when running a build). 205 | * Small bug fixes when logging errors. 206 | * Try to create report dir more strongly if it does not already exist when writing the aggregated report 207 | (related to parallel builds). 208 | * Upgraded Gradle version used in the build to 2.2. 209 | * Due to above - re-added transitive dependencies on Spock and Groovy as Gradle no longer supports 210 | "provided" dependency hack. Users must exclude dependencies if they wish to use different versions and avoid conflicts. 211 | 212 | 1.2.5 - 2015 March 8 213 | 214 | * #33 Support for Spock 1.0, but this version also works with Spock 0.7 215 | * #27 Added new report creator - TemplateReportCreator 216 | 217 | 1.2.4 - 2015 February 14 218 | 219 | * #18 Added character encoding for HTML reports 220 | * Support for configuring some properties using environment variables 221 | * Stopped using `println` in favour of using java.util.logging 222 | * Added a Table Of Contents to HTML reports (can be turned off via config file) 223 | 224 | 1.2.3 - 2014 November 08 225 | 226 | * #15 Downgraded minimum Groovy version required to 2.0.8 (benefits some Grails users). 227 | * Ensured successRate in reports gets bounded between 0 and 100% (some test problems count both as failure and error). 228 | 229 | 1.2.2 - 2014 November 03 230 | 231 | * #14 support for @Unroll-ing variables not only in Spec names, but also in block texts 232 | 233 | 1.2.1 - 2014 November 01 234 | 235 | * #12 stopped throwing Exception when a Specification setup method fails 236 | * #12 appropriate reporting of Specification which did not run due to setup failing 237 | * #13 removed usage of Groovy features that break with some JDK versions 238 | (notably VerifyError on Java 1.7_67 and 1.8_11) 239 | 240 | 1.2 - 2014 August 03 241 | 242 | * #4 Support for @Unroll. Each unrolled iteration now shown as individual spec and placeholders are correctly resolved. 243 | Statistics also respect @Unroll semantics (each iteration failure is treated as a spec failure). 244 | * #5 numbers shown in reports are internationalized 245 | * #6 default config properties are merged with custom config (not replaced by it) 246 | * #7 new config option to hide empty blocks (which do not have a description) 247 | 248 | 1.1 - 2013 Sep 14 249 | 250 | * Fixed HTML footer alignment in summary report 251 | * Added "when" and "who" ran tests to HTML reports 252 | * Fixed bug with showing text for WHERE blocks in HTML reports 253 | * Blocks with empty Strings or no Strings now show in HTML reports 254 | * Text to the left of example tables in HTML reports are now called "Examples", not "Where" 255 | 256 | 1.0 - INITIAL RELEASE - 2013 August 06 257 | 258 | * creates HTML summary reports for all specs run by Spock 259 | * creates HTML feature reports for each individual spec -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/IReportCreator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import com.athaydes.spockframework.report.internal.SpecData 4 | 5 | /** 6 | * A spock-reports report creator. 7 | */ 8 | interface IReportCreator { 9 | 10 | void createReportFor( SpecData data ) 11 | 12 | void setOutputDir( String path ) 13 | 14 | void setAggregatedJsonReportDir( String path ) 15 | 16 | void setHideEmptyBlocks( boolean hide ) 17 | 18 | void setShowCodeBlocks( boolean show ) 19 | 20 | void setTestSourceRoots( roots ) 21 | 22 | void setProjectName( String projectName ) 23 | 24 | void setProjectVersion( String projectVersion ) 25 | 26 | void done() 27 | 28 | } -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/SpockReportExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | 4 | import com.athaydes.spockframework.report.internal.ConfigLoader 5 | import com.athaydes.spockframework.report.internal.FeatureRun 6 | import com.athaydes.spockframework.report.internal.MultiReportCreator 7 | import com.athaydes.spockframework.report.internal.SpecData 8 | import com.athaydes.spockframework.report.internal.SpecProblem 9 | import com.athaydes.spockframework.report.internal.SpockReportsConfiguration 10 | import com.athaydes.spockframework.report.util.Utils 11 | import groovy.transform.CompileDynamic 12 | import groovy.transform.CompileStatic 13 | import groovy.util.logging.Slf4j 14 | import org.spockframework.runtime.IRunListener 15 | import org.spockframework.runtime.extension.IGlobalExtension 16 | import org.spockframework.runtime.model.ErrorInfo 17 | import org.spockframework.runtime.model.FeatureInfo 18 | import org.spockframework.runtime.model.IterationInfo 19 | import org.spockframework.runtime.model.MethodKind 20 | import org.spockframework.runtime.model.SpecInfo 21 | 22 | import java.util.concurrent.atomic.AtomicBoolean 23 | 24 | /** 25 | * 26 | * User: Renato 27 | */ 28 | @Slf4j 29 | class SpockReportExtension implements IGlobalExtension { 30 | 31 | static final PROJECT_URL = 'https://github.com/renatoathaydes/spock-reports' 32 | 33 | private final AtomicBoolean initialized = new AtomicBoolean( false ) 34 | 35 | //@Injected 36 | private SpockReportsConfiguration configuration 37 | 38 | protected ConfigLoader configLoader = new ConfigLoader() 39 | 40 | IReportCreator reportCreator 41 | 42 | @CompileStatic 43 | @Override 44 | void start() { 45 | if ( !initialized.getAndSet( true ) ) { 46 | log.debug( "Got configuration from Spock: {}", configuration ) 47 | log.debug 'Configuring {}', this.class.name 48 | def config = configLoader.loadConfig( configuration ) 49 | 50 | // Read the class report property and exit if its not set 51 | String commaListOfReportClasses = config.remove( IReportCreator.name ) 52 | if ( !commaListOfReportClasses ) { 53 | log.warn( 'Missing property: {} - no report classes defined', IReportCreator.name ) 54 | return 55 | } 56 | 57 | // Create the IReportCreator instance(s) - skipping those that fail 58 | def reportCreators = commaListOfReportClasses.tokenize( ',' ) 59 | .collect { it.trim() } 60 | .collect { instantiateReportCreatorAndApplyConfig( it, config ) } 61 | .findAll { it != null } 62 | 63 | // If none were successfully created then exit 64 | if ( reportCreators.isEmpty() ) return 65 | 66 | // Assign the IReportCreator(s) - use the multi report creator only if necessary 67 | reportCreator = ( reportCreators.size() == 1 ) ? reportCreators[ 0 ] : new MultiReportCreator( reportCreators ) 68 | } 69 | } 70 | 71 | @Override 72 | void stop() { 73 | reportCreator?.done() 74 | } 75 | 76 | @Override 77 | void visitSpec( SpecInfo specInfo ) { 78 | if ( reportCreator != null ) { 79 | specInfo.addListener createListener() 80 | } else { 81 | log.warn 'Not creating report for {} as reportCreator is null', specInfo.name 82 | } 83 | } 84 | 85 | @CompileStatic 86 | IReportCreator instantiateReportCreator( String reportCreatorClassName ) { 87 | def reportCreatorClass = Class.forName( reportCreatorClassName ) 88 | reportCreatorClass 89 | .asSubclass( IReportCreator ) 90 | .getDeclaredConstructor() 91 | .newInstance() 92 | } 93 | 94 | @CompileStatic 95 | IReportCreator instantiateReportCreatorAndApplyConfig( String reportCreatorClassName, Properties config ) { 96 | // Given the IReportCreator class name then create it and apply config properties 97 | try { 98 | def reportCreator = instantiateReportCreator( reportCreatorClassName ) 99 | configLoader.apply( reportCreator, config ) 100 | return reportCreator 101 | } catch ( e ) { 102 | log.warn( "Failed to create instance of $reportCreatorClassName", e ) 103 | return null 104 | } 105 | } 106 | 107 | // this method is patched by the UseTemplateReportCreator category and others 108 | SpecInfoListener createListener() { 109 | new SpecInfoListener( reportCreator ) 110 | } 111 | 112 | } 113 | 114 | @Slf4j 115 | @CompileStatic 116 | class SpecInfoListener implements IRunListener { 117 | 118 | private final IReportCreator reportCreator 119 | 120 | // synchronization is done on access 121 | private final Map specs = [ : ] 122 | 123 | // no iteration required, so this is enough 124 | private final Map features = [ : ].asSynchronized() 125 | 126 | SpecInfoListener( IReportCreator reportCreator ) { 127 | this.reportCreator = reportCreator 128 | } 129 | 130 | @Override 131 | void beforeSpec( SpecInfo spec ) { 132 | synchronized ( specs ) { 133 | specs[ spec ] = new SpecData( spec ) 134 | } 135 | log.debug( "Before spec: {}", Utils.getSpecClassName( spec ) ) 136 | } 137 | 138 | @Override 139 | void beforeFeature( FeatureInfo feature ) { 140 | log.debug( "Before feature: {}", feature.name ) 141 | SpecData specData = specFor( feature ) 142 | if ( specData ) { 143 | specData.withFeatureRuns { it << new FeatureRun( feature ) } 144 | } else { 145 | log.warn( "Unable to find feature" ) 146 | } 147 | } 148 | 149 | @Override 150 | void beforeIteration( IterationInfo iteration ) { 151 | log.debug( "Before iteration: {}", iteration.name ) 152 | featureRunFor( iteration ).with { 153 | failuresByIteration[ iteration ] = [ ] 154 | timeByIteration[ iteration ] = System.nanoTime() 155 | } 156 | } 157 | 158 | @Override 159 | void afterIteration( IterationInfo iteration ) { 160 | log.debug( "After iteration: {}", iteration.name ) 161 | featureRunFor( iteration ).with { 162 | Long startTime = timeByIteration[ iteration ] 163 | if ( startTime == null ) { 164 | log.info( "Could not find startTime for iteration, times in report may be misleading." ) 165 | timeByIteration[ iteration ] = 0L 166 | } else { 167 | long totalTime = ( ( System.nanoTime() - startTime ) / 1_000_000L ).toLong() 168 | log.debug( "Iteration totalTime: {}", totalTime ) 169 | timeByIteration[ iteration ] = totalTime 170 | } 171 | } 172 | } 173 | 174 | @Override 175 | void afterFeature( FeatureInfo feature ) { 176 | log.debug( "After feature: {}", feature.name ) 177 | features.remove( feature ) 178 | } 179 | 180 | @Override 181 | void afterSpec( SpecInfo spec ) { 182 | // we don't need the spec anymore 183 | SpecData specData 184 | synchronized ( specs ) { 185 | specData = specs.remove( spec ) 186 | } 187 | if ( specData == null ) { 188 | // we already handled this spec 189 | return 190 | } 191 | assert specData.info == spec 192 | log.debug( "After spec: {}", Utils.getSpecClassName( specData ) ) 193 | specData.totalTime = System.currentTimeMillis() - specData.startTime 194 | createReport( specData ) 195 | } 196 | 197 | @Override 198 | void error( ErrorInfo errorInfo ) { 199 | def feature = errorInfo.method?.feature 200 | SpecData specData = feature == null ? null : specFor( feature ) 201 | try { 202 | log.debug( "Error on spec {}, feature {}", specData == null 203 | ? "" 204 | : Utils.getSpecClassName( specData ), 205 | errorInfo.method?.feature?.name ?: '' 206 | ) 207 | 208 | def noSpecData = specData == null 209 | def setupSpecError = !noSpecData && errorInfo.method?.kind == MethodKind.SETUP_SPEC 210 | 211 | if ( setupSpecError ) { 212 | log.debug( 'Error in setupSpec method' ) 213 | specData.initializationError = errorInfo 214 | } else if ( noSpecData ) { 215 | log.debug( 'Error before Spec could be instantiated' ) 216 | } else { 217 | def iteration = errorInfo.method?.iteration 218 | if ( iteration != null ) { 219 | featureRunFor( iteration ).failuresByIteration[ iteration ] << new SpecProblem( errorInfo ) 220 | } else { 221 | log.debug( "Error in cleanupSpec method: {}", errorInfo.exception?.toString() ) 222 | specData?.cleanupSpecError = errorInfo 223 | } 224 | } 225 | } catch ( Throwable e ) { 226 | // nothing we can do here 227 | e.printStackTrace() 228 | } 229 | } 230 | 231 | @Override 232 | void specSkipped( SpecInfo spec ) { 233 | beforeSpec( spec ) 234 | log.debug( "Skipping specification {}", Utils.getSpecClassName( spec ) ) 235 | 236 | def specFeatures = spec.features 237 | synchronized ( specFeatures ) { 238 | specFeatures.each { feature -> 239 | feature.skipped = true 240 | beforeFeature( feature ) 241 | afterFeature( feature ) 242 | } 243 | } 244 | 245 | afterSpec( spec ) 246 | } 247 | 248 | @Override 249 | void featureSkipped( FeatureInfo feature ) { 250 | // feature already knows if it's skipped 251 | } 252 | 253 | private SpecData specFor( FeatureInfo feature ) { 254 | synchronized ( specs ) { 255 | SpecData result = specs[ feature.spec ] 256 | if ( result ) return result 257 | 258 | // check the cache 259 | result = features[ feature ] 260 | if ( result ) { 261 | log.debug( "Found spec data in cache for feature: {}", feature.name ) 262 | return result 263 | } 264 | 265 | // try the hard way... Spock won't always give us the "right" spec as it seems... 266 | // inherited spec feature methods come with the "wrong" spec, for example. 267 | log.debug( "Unable to find spec data for feature, falling back on exhaustive search: '{}'", feature.name ) 268 | for ( candidate in specs.values() ) { 269 | def spec = candidate.info.specsTopToBottom.find { it == feature.spec } 270 | if ( spec ) { 271 | log.debug( 'Found spec the hard way: {}', spec.name ) 272 | features[ feature ] = candidate 273 | return candidate 274 | } 275 | } 276 | } 277 | return null 278 | } 279 | 280 | // allow test categories to mock functionality 281 | @CompileDynamic 282 | private void createReport( SpecData specData ) { 283 | reportCreator.createReportFor specData 284 | } 285 | 286 | private FeatureRun featureRunFor( IterationInfo iteration ) { 287 | def targetFeature = iteration.feature 288 | def specData = specFor( targetFeature ) 289 | def run = specData?.withFeatureRuns { it.find { it.feature == targetFeature } } 290 | 291 | if ( run == null ) { 292 | log.warn( "Could not find feature for current iteration, iteration will not appear in reports: {}", 293 | targetFeature.name ) 294 | return new FeatureRun( specData?.info?.features?.first() ?: dummyFeature() ) 295 | } 296 | return run 297 | } 298 | 299 | private static FeatureInfo dummyFeature() { 300 | new FeatureInfo( name: '' ) 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/extension/SpockReportsSpecificationExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.extension 2 | 3 | import com.athaydes.spockframework.report.util.Utils 4 | import groovy.transform.CompileStatic 5 | import groovy.transform.PackageScope 6 | import groovy.util.logging.Slf4j 7 | import org.spockframework.runtime.model.FeatureInfo 8 | import org.spockframework.runtime.model.IterationInfo 9 | import spock.lang.Specification 10 | 11 | @CompileStatic 12 | class SpockReportsSpecificationExtension { 13 | 14 | /** 15 | * Add information to the Spock report. 16 | *

17 | * The provided object's String representation will be included in the feature report, 18 | * so this method should only be called from a feature. 19 | *

20 | * This method may be called several times for each feature. 21 | * 22 | * @param self receiver 23 | * @param info to include in the feature report 24 | */ 25 | static void reportInfo( Specification self, info ) { 26 | InfoContainer.add self, info 27 | } 28 | 29 | /** 30 | * Add header to the Spock report. 31 | *

32 | * The provided object's String representation will be included in the Specification's report header, 33 | * so this method should normally be called from the {@code setupSpec} method. 34 | * 35 | * @param self 36 | * @param header to include in the Specification report 37 | */ 38 | static void reportHeader( Specification self, header ) { 39 | InfoContainer.addHeader self, header 40 | } 41 | 42 | } 43 | 44 | @Slf4j 45 | @CompileStatic 46 | class InfoContainer { 47 | 48 | private static final Map headerBySpecName = [ : ].asSynchronized() 49 | private static final Map infoBySpecName = [ : ].asSynchronized() 50 | 51 | private static String keyFor( String specName, 52 | FeatureInfo feature, 53 | IterationInfo iteration ) { 54 | def index = iteration && Utils.isUnrolled( feature ) ? iteration.iterationIndex : -1 55 | "$specName${feature?.name}$index" 56 | } 57 | 58 | @PackageScope 59 | static void addHeader( Specification spec, item ) { 60 | headerBySpecName.get( spec.class.name, [ ] ) << item 61 | } 62 | 63 | @PackageScope 64 | static void add( Specification spec, item ) { 65 | try { 66 | def key = keyFor( spec.class.name, 67 | spec.specificationContext.currentFeature, 68 | spec.specificationContext.currentIteration ) 69 | infoBySpecName.get( key, [ ] ) << item 70 | } catch ( e ) { 71 | log.debug( "Unable to add info to report, will add it as header instead: {}. " + 72 | "Problem: {}", item, e ) 73 | addHeader( spec, item ) 74 | } 75 | } 76 | 77 | static List getHeadersFor( String specName ) { 78 | headerBySpecName.remove( specName ) ?: [ ] 79 | } 80 | 81 | static List getNextInfoFor( String specName, 82 | FeatureInfo feature, 83 | IterationInfo iteration ) { 84 | def key = keyFor( specName, feature, iteration ) 85 | infoBySpecName.remove( key ) ?: [ ] 86 | } 87 | 88 | static void resetSpecData( String specName, List headers ) { 89 | headerBySpecName[ specName ] = headers 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/AbstractHtmlCreator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.SpockReportExtension 4 | import groovy.util.logging.Slf4j 5 | import groovy.xml.MarkupBuilder 6 | 7 | /** 8 | * 9 | * User: Renato 10 | */ 11 | @Slf4j 12 | abstract class AbstractHtmlCreator { 13 | 14 | String css 15 | boolean doInlineCss = true 16 | String outputDirectory = '' 17 | boolean hideEmptyBlocks = false 18 | KnowsWhenAndWhoRanTest whenAndWho = new KnowsWhenAndWhoRanTest() 19 | String excludeToc = "false" 20 | private String resolvedCss 21 | 22 | abstract String cssDefaultName() 23 | 24 | String getCss() { 25 | if ( resolvedCss ) { 26 | return resolvedCss 27 | } 28 | if ( !css || css.trim().empty ) return '' 29 | resolvedCss = new CssResource( css, doInlineCss, new File( outputDirectory, cssDefaultName() ) ).text 30 | } 31 | 32 | String reportFor( T data ) { 33 | def writer = new StringWriter( 4096 ) 34 | def builder = new MarkupBuilder( new IndentPrinter( new PrintWriter( writer ), "" ) ) 35 | builder.expandEmptyElements = true 36 | builder.html { 37 | head { 38 | meta( 'http-equiv': 'Content-Type', content: 'text/html; charset=utf-8' ) 39 | if ( css && doInlineCss ) style css 40 | else if ( css ) link( rel: 'stylesheet', type: 'text/css', href: css ) 41 | } 42 | body { 43 | h2 reportHeader( data ) 44 | hr() 45 | writeSummary( builder, data ) 46 | writeDetails( builder, data ) 47 | hr() 48 | writeFooter( builder ) 49 | } 50 | } 51 | '' + writer.toString() 52 | } 53 | 54 | protected void writeFooter( MarkupBuilder builder ) { 55 | builder.div( 'class': 'footer' ) { 56 | mkp.yieldUnescaped( 57 | "Generated by Athaydes Spock Reports" ) 58 | } 59 | } 60 | 61 | abstract protected String reportHeader( T data ) 62 | 63 | abstract protected void writeSummary( MarkupBuilder builder, T data ) 64 | 65 | abstract protected void writeDetails( MarkupBuilder builder, T data ) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/ConfigLoader.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.IReportCreator 4 | import com.athaydes.spockframework.report.util.Utils 5 | import groovy.transform.CompileDynamic 6 | import groovy.transform.CompileStatic 7 | import groovy.util.logging.Slf4j 8 | import org.spockframework.runtime.RunContext 9 | import org.spockframework.util.Nullable 10 | 11 | /** 12 | * 13 | * User: Renato 14 | */ 15 | @Slf4j 16 | @CompileStatic 17 | class ConfigLoader { 18 | 19 | static final String SYS_PROPERTY_PREFIX = 'com.athaydes.spockframework.report.' 20 | 21 | static final String CUSTOM_CONFIG = "META-INF/services/${IReportCreator.class.name}.properties" 22 | 23 | Properties loadConfig( @Nullable SpockReportsConfiguration spockConfig = null ) { 24 | def props = loadSystemProperties( 25 | loadSpockConfig( spockConfig, 26 | loadCustomProperties( 27 | loadDefaultProperties() ) ) ) 28 | 29 | log.debug( "SpockReports config loaded: {}", props ) 30 | 31 | props 32 | } 33 | 34 | void apply( IReportCreator reportCreator, Properties config ) { 35 | 36 | config.each { keyObject, value -> 37 | String key = keyObject.toString() 38 | int lastDotIndex = key.lastIndexOf( '.' ) 39 | 40 | if ( lastDotIndex > 0 && lastDotIndex + 1 < key.size() ) { 41 | final prefix = key[ 0.. 74 | def key = entry.key 75 | key instanceof String && key.startsWith( SYS_PROPERTY_PREFIX ) 76 | } 77 | 78 | def reportClassName = props[ IReportCreator.name ] 79 | 80 | // add also the properties starting with the report's class name 81 | if ( reportClassName instanceof String ) { 82 | def reportClassPrefix = reportClassName + '.' 83 | System.properties.findAll { entry -> 84 | def key = entry.key 85 | key instanceof String && key.startsWith( reportClassPrefix ) 86 | }.each { key, value -> 87 | filteredProps[ key ] = value 88 | } 89 | } 90 | 91 | filteredProps.each { key, value -> 92 | if ( props.containsKey( key ) ) { 93 | log.debug( "Overriding property [$key] with System property's value: $value" ) 94 | } 95 | props[ key ] = value 96 | } 97 | 98 | props 99 | } 100 | 101 | private Properties loadDefaultProperties() { 102 | def defaultProperties = new Properties() 103 | ConfigLoader.class.getResource( 'config.properties' )?.withInputStream { 104 | defaultProperties.load it 105 | } 106 | defaultProperties 107 | } 108 | 109 | private Properties loadCustomProperties( Properties properties ) { 110 | def resources = RunContext.classLoader.getResources( CUSTOM_CONFIG ) 111 | for ( URL url in resources ) { 112 | try { 113 | log.debug( "Trying to load custom configuration at $url" ) 114 | url.withInputStream { properties.load it } 115 | } catch ( IOException | IllegalArgumentException e ) { 116 | log.warn( "Unable to read config from ${url.path}", e ) 117 | } 118 | } 119 | properties 120 | } 121 | 122 | private Properties loadSpockConfig( @Nullable SpockReportsConfiguration config, 123 | Properties properties ) { 124 | if ( config && config.properties ) { 125 | properties.putAll( config.properties ) 126 | } 127 | return properties 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/CssResource.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.util.Utils 4 | import groovy.util.logging.Slf4j 5 | import org.spockframework.util.Nullable 6 | 7 | @Slf4j 8 | class CssResource { 9 | 10 | final String css 11 | final boolean inlineCss 12 | final File cssFile 13 | 14 | CssResource( String css, boolean inlineCss, File cssFile ) { 15 | this.css = css 16 | this.inlineCss = inlineCss 17 | this.cssFile = cssFile 18 | } 19 | 20 | String getText() { 21 | def text = resource?.text 22 | if ( !text ) { 23 | log.warn( 'css resource seems to be empty: {}', resource ) 24 | return '' 25 | } 26 | 27 | log.debug( 'Found css resource ({} characters long)', text.size() ) 28 | 29 | if ( inlineCss ) { 30 | log.debug( "Inlining css in HTML report" ) 31 | return text 32 | } else { 33 | log.debug( "Writing css file to {}", cssFile.absolutePath ) 34 | try { 35 | cssFile.write text 36 | } catch ( e ) { 37 | log.warn( 'Unable to write CSS file to {} due to {}', text, e ) 38 | } 39 | return cssFile.name 40 | } 41 | } 42 | 43 | @Nullable 44 | private URL getResource() { 45 | if ( Utils.isUrl( css ) ) urlResource 46 | else classPathResource 47 | } 48 | 49 | @Nullable 50 | private URL getClassPathResource() { 51 | log.debug 'Getting classpath resource text: {}', css 52 | def cssResource = this.class.getResource( "/$css" ) 53 | if ( cssResource ) 54 | try { 55 | return cssResource 56 | } catch ( e ) { 57 | log.warn( "Failed to set CSS resource to {} due to {}", css, e ) 58 | } 59 | else 60 | log.info "The CSS classpath resource does not exist: {}", css 61 | null 62 | } 63 | 64 | @Nullable 65 | private URL getUrlResource() { 66 | log.debug 'Getting URL resource text: {}', css 67 | try { 68 | return new URL( css ) 69 | } catch ( e ) { 70 | log.warn( "Failed to set CSS resource as the URL {} could not be read due to {}", css, e ) 71 | } 72 | null 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/HtmlReportAggregator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.util.Utils 4 | import groovy.util.logging.Slf4j 5 | import groovy.xml.MarkupBuilder 6 | import spock.lang.Title 7 | 8 | import static com.athaydes.spockframework.report.internal.ReportDataAggregator.getAllAggregatedDataAndPersistLocalData 9 | import static com.athaydes.spockframework.report.internal.SpecSummaryNameOption.* 10 | 11 | /** 12 | * 13 | * User: Renato 14 | */ 15 | @Slf4j 16 | class HtmlReportAggregator extends AbstractHtmlCreator { 17 | 18 | final Map aggregatedData = [ : ] 19 | 20 | def stringFormatter = new StringFormatHelper() 21 | 22 | String projectName 23 | String projectVersion 24 | String aggregatedJsonReportDir 25 | SpecSummaryNameOption specSummaryNameOption = CLASS_NAME_AND_TITLE 26 | 27 | protected HtmlReportAggregator() { 28 | // provided for testing only (need to Mock it) 29 | } 30 | 31 | @Override 32 | String cssDefaultName() { 'summary-report.css' } 33 | 34 | void aggregateReport( SpecData data, Map stats ) { 35 | def specName = Utils.getSpecClassName( data ) 36 | def allFeatures = data.info.allFeaturesInExecutionOrder.groupBy { feature -> Utils.isSkipped( feature ) } 37 | 38 | def specTitle = Utils.specAnnotation( data, Title )?.value() ?: '' 39 | def narrative = data.info.narrative ?: '' 40 | 41 | aggregatedData[ specName ] = Utils.createAggregatedData( 42 | allFeatures[ false ], allFeatures[ true ], stats, specTitle, narrative ) 43 | } 44 | 45 | void writeOut() { 46 | final reportsDir = outputDirectory as File // try to force it into being a File! 47 | if ( existsOrCanCreate( reportsDir ) ) { 48 | final aggregatedReport = new File( reportsDir, 'index.html' ) 49 | final jsonDir = aggregatedJsonReportDir ? new File( aggregatedJsonReportDir ) : reportsDir 50 | 51 | try { 52 | def allData = getAllAggregatedDataAndPersistLocalData( jsonDir, aggregatedData ) 53 | aggregatedData.clear() 54 | aggregatedReport.write( reportFor( allData ), 'UTF-8' ) 55 | } catch ( e ) { 56 | log.warn( "Failed to create aggregated report", e ) 57 | } 58 | } else { 59 | log.warn "Cannot create output directory: {}", reportsDir?.absolutePath 60 | } 61 | } 62 | 63 | static boolean existsOrCanCreate( File reportsDir ) { 64 | reportsDir?.exists() || reportsDir?.mkdirs() 65 | } 66 | 67 | @Override 68 | protected String reportHeader( Map data ) { 69 | 'Specification run results' 70 | } 71 | 72 | @Override 73 | protected void writeSummary( MarkupBuilder builder, Map json ) { 74 | def stats = Utils.aggregateStats( json ) 75 | def cssClassIfTrue = { isTrue, String cssClass -> 76 | if ( isTrue ) [ 'class': cssClass ] else Collections.emptyMap() 77 | } 78 | 79 | if ( projectName && projectVersion ) { 80 | builder.div( 'class': 'project-header' ) { 81 | span( 'class': 'project-name', "Project: ${projectName}" ) 82 | span( 'class': 'project-version', "Version: ${projectVersion}" ) 83 | } 84 | } 85 | 86 | builder.div( 'class': 'summary-report' ) { 87 | h3 'Specifications summary:' 88 | builder.div( 'class': 'date-test-ran', whenAndWho.whenAndWhoRanTest( stringFormatter ) ) 89 | table( 'class': 'summary-table' ) { 90 | thead { 91 | tr { 92 | th 'Total' 93 | th 'Passed' 94 | th 'Failed' 95 | th 'Skipped' 96 | th 'Ft Total' 97 | th 'Ft Passed' 98 | th 'Ft Failed' 99 | th 'Ft Skipped' 100 | th 'Success rate' 101 | th 'Total time' 102 | } 103 | } 104 | tbody { 105 | tr { 106 | td stats.total 107 | td stats.passed 108 | td( cssClassIfTrue( stats.failed, 'failure' ), stats.failed ) 109 | td stats.skipped 110 | td stats.fTotal 111 | td stats.fPassed 112 | td( cssClassIfTrue( stats.fFails + stats.fErrors, 'failure' ), stats.fFails + stats.fErrors ) 113 | td stats.fSkipped 114 | td( cssClassIfTrue( stats.failed, 'failure' ), stringFormatter 115 | .toPercentage( stats.successRate as double ) ) 116 | td stringFormatter.toTimeDuration( stats.time ) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | @Override 124 | protected void writeDetails( MarkupBuilder builder, Map data ) { 125 | builder.h3 'Specifications:' 126 | builder.table( 'class': 'summary-table' ) { 127 | thead { 128 | tr { 129 | th 'Name' 130 | th 'Features' 131 | th 'Iterations' 132 | th 'Failed' 133 | th 'Errors' 134 | th 'Skipped' 135 | th 'Success rate' 136 | th 'Time' 137 | } 138 | } 139 | tbody { 140 | data.keySet().sort().each { String specName -> 141 | def stats = data[ specName ].stats 142 | def title = data[ specName ].title 143 | writeSpecSummary( builder, stats, specName, title ) 144 | } 145 | } 146 | } 147 | 148 | } 149 | 150 | protected void writeSpecSummary( MarkupBuilder builder, Map stats, String specName, String title ) { 151 | def cssClasses = [ ] 152 | if ( stats.failures ) cssClasses << 'failure' 153 | if ( stats.errors ) cssClasses << 'error' 154 | if ( !stats.errors && !stats.failures && stats.totalRuns == 0 ) cssClasses << 'ignored' 155 | builder.tr( cssClasses ? [ 'class': cssClasses.join( ' ' ) ] : null ) { 156 | td { 157 | switch ( specSummaryNameOption ) { 158 | case CLASS_NAME_AND_TITLE: 159 | a( href: "${specName}.html", specName ) 160 | if ( title ) { 161 | div( 'class': 'spec-title', title ) 162 | } 163 | break 164 | case CLASS_NAME: 165 | a( href: "${specName}.html", specName ) 166 | break 167 | case TITLE: 168 | if ( title ) { 169 | a( href: "${specName}.html" ) { 170 | div( 'class': 'spec-title', title ) 171 | } 172 | } else { 173 | a( href: "${specName}.html", specName ) 174 | } 175 | break 176 | } 177 | } 178 | td stats.totalFeatures 179 | td stats.totalRuns 180 | td stats.failures 181 | td stats.errors 182 | td stats.skipped 183 | td stringFormatter.toPercentage( stats.successRate ) 184 | td stringFormatter.toTimeDuration( stats.time ) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/KnowsWhenAndWhoRanTest.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | /** 3 | * 4 | * User: Renato 5 | */ 6 | class KnowsWhenAndWhoRanTest { 7 | 8 | String whenAndWhoRanTest( StringFormatHelper stringFormatter ) { 9 | "Created on ${stringFormatter.toDateString( new Date() )}" + 10 | " by ${getUserName()}" 11 | } 12 | 13 | private String getUserName() { 14 | System.getProperty( 'user.name' ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/MultiReportCreator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.IReportCreator 4 | import com.athaydes.spockframework.report.extension.InfoContainer 5 | 6 | import javax.naming.OperationNotSupportedException 7 | 8 | import static com.athaydes.spockframework.report.util.Utils.getSpecClassName 9 | 10 | /** 11 | * Container for multiple IReportCreators 12 | */ 13 | class MultiReportCreator implements IReportCreator { 14 | private final List reportCreators = [ ] 15 | 16 | MultiReportCreator( List reportCreators ) { 17 | this.reportCreators.addAll( reportCreators ) 18 | } 19 | 20 | @Override 21 | void createReportFor( SpecData data ) { 22 | def specName = getSpecClassName( data ) 23 | def headers = InfoContainer.getHeadersFor( specName ).asImmutable() 24 | reportCreators.each { 25 | InfoContainer.resetSpecData( specName, headers.collect() ) 26 | it.createReportFor( data ) 27 | } 28 | } 29 | 30 | @Override 31 | void setOutputDir( String path ) { 32 | throw new OperationNotSupportedException( "No modifications after construction" ) 33 | } 34 | 35 | @Override 36 | void setAggregatedJsonReportDir( String path ) { 37 | throw new OperationNotSupportedException( "No modifications after construction" ) 38 | } 39 | 40 | @Override 41 | void setHideEmptyBlocks( boolean hide ) { 42 | throw new OperationNotSupportedException( "No modifications after construction" ) 43 | } 44 | 45 | @Override 46 | void setShowCodeBlocks( boolean show ) { 47 | throw new OperationNotSupportedException( "No modifications after construction" ) 48 | } 49 | 50 | @Override 51 | void setTestSourceRoots( roots ) { 52 | throw new OperationNotSupportedException( "No modifications after construction" ) 53 | } 54 | 55 | @Override 56 | void setProjectName( String projectName ) { 57 | throw new OperationNotSupportedException( "No modifications after construction" ) 58 | } 59 | 60 | @Override 61 | void setProjectVersion( String projectVersion ) { 62 | throw new OperationNotSupportedException( "No modifications after construction" ) 63 | } 64 | 65 | @Override 66 | void done() { 67 | reportCreators.each { it.done() } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/ProblemBlockWriter.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.util.Utils 4 | import groovy.xml.MarkupBuilder 5 | import org.spockframework.runtime.model.IterationInfo 6 | 7 | /** 8 | * 9 | * User: Renato 10 | */ 11 | class ProblemBlockWriter { 12 | 13 | StringFormatHelper stringFormatter 14 | boolean printThrowableStackTrace = false 15 | 16 | void writeProblemBlockForAllIterations( MarkupBuilder builder, FeatureRun run, boolean isError, boolean isFailure ) { 17 | if ( isError || isFailure ) { 18 | problemsContainer( builder ) { 19 | writeProblems( builder, problemsByIteration( run.copyFailuresByIteration(), run.copyTimeByIteration() ) ) 20 | } 21 | } 22 | } 23 | 24 | void writeProblemBlockForIteration( MarkupBuilder builder, IterationInfo iteration, 25 | List problems, Long time ) { 26 | if ( problems ) { 27 | problemsContainer( builder ) { 28 | def problemsByIteration = problemsByIteration( [ ( iteration ): problems ], [ ( iteration ): time ] ) 29 | problemsByIteration.each { it.dataValues = null } // do not show data values in the report 30 | writeProblems( builder, problemsByIteration ) 31 | } 32 | } 33 | } 34 | 35 | void problemsContainer( MarkupBuilder builder, Runnable createProblemList ) { 36 | problemsContainer( builder, 'The following problems occurred:', createProblemList ) 37 | } 38 | 39 | void problemsContainer( MarkupBuilder builder, String header, Runnable createProblemList ) { 40 | builder.tr { 41 | td( colspan: '10' ) { 42 | div( 'class': 'problem-description' ) { 43 | div( 'class': 'problem-header', header ) 44 | div( 'class': 'problem-list' ) { 45 | createProblemList.run() 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | private void writeProblems( MarkupBuilder builder, List problems ) { 53 | problems.each { Map problem -> 54 | if ( !problem.messages ) { 55 | return 56 | } 57 | if ( problem.dataValues ) { 58 | builder.ul { 59 | li { 60 | div problem.dataValues.toString() 61 | writeProblemMsgs( builder, problem.messages ) 62 | } 63 | } 64 | } else { 65 | writeProblemMsgs( builder, problem.messages ) 66 | } 67 | } 68 | } 69 | 70 | void writeProblemMsgs( MarkupBuilder builder, List msgs ) { 71 | builder.ul { 72 | msgs.each { msg -> 73 | li { 74 | pre { 75 | mkp.yieldUnescaped( 76 | stringFormatter.formatToHtml( 77 | stringFormatter.escapeXml( formatProblemMessage( msg ) ) ) ) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | private static List problemsByIteration( Map> failures, 85 | Map times ) { 86 | Utils.iterationData( failures, times ).collect { Map entry -> 87 | entry + [ messages: entry.errors ] 88 | } 89 | } 90 | 91 | protected String formatProblemMessage( message ) { 92 | if ( printThrowableStackTrace && message instanceof Throwable ) { 93 | def writer = new StringWriter() 94 | message.printStackTrace( new PrintWriter( writer ) ) 95 | return writer.toString() 96 | } else if ( message == null ) { 97 | return 'null' 98 | } else { 99 | return message.toString() 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/ReportDataAggregator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.json.JsonOutput 4 | import groovy.json.JsonSlurper 5 | import groovy.transform.CompileStatic 6 | import groovy.transform.PackageScope 7 | import groovy.util.logging.Slf4j 8 | 9 | import java.nio.channels.FileLock 10 | import java.nio.charset.Charset 11 | import java.util.concurrent.Callable 12 | import java.util.regex.Pattern 13 | 14 | /** 15 | * Static functions for aggregating report data even across different Java processes. 16 | * 17 | * This is necessary to support parallel builds. 18 | */ 19 | @Slf4j 20 | @CompileStatic 21 | class ReportDataAggregator { 22 | 23 | static final String AGGREGATED_DATA_FILE = 'aggregated_report.json' 24 | 25 | static final Charset charset = Charset.forName( 'utf-8' ) 26 | static final JsonSlurper jsonParser = new JsonSlurper() 27 | 28 | static Map getAllAggregatedDataAndPersistLocalData( File dir, Map localData ) { 29 | if ( dir != null && !dir.exists() ) { 30 | dir.mkdirs() 31 | } 32 | 33 | if ( !dir?.isDirectory() ) { 34 | log.warn( "Cannot store aggregated JSON report in the 'aggregatedJsonReportDir' as it does not exist " + 35 | "or is not a directory: {}", dir ) 36 | return localData 37 | } 38 | 39 | final rawFile = new File( dir, AGGREGATED_DATA_FILE ) 40 | rawFile.createNewFile() // ensure file exists before locking it 41 | 42 | final dataFile = new RandomAccessFile( rawFile, 'rw' ) 43 | 44 | return withFileLock( dataFile ) { 45 | def persistedData = readTextFrom( dataFile ) ?: '{}' 46 | Map allData = jsonParser.parseText( persistedData ) as Map + localData 47 | appendDataToFile( dataFile, localData ) 48 | allData.asImmutable() as Map 49 | } 50 | } 51 | 52 | static T withFileLock( RandomAccessFile file, Callable action ) { 53 | FileLock lock = null 54 | Exception error = null 55 | def desistTime = System.currentTimeMillis() + 5_000L 56 | while ( lock == null && System.currentTimeMillis() < desistTime ) { 57 | try { 58 | lock = file.channel.tryLock() 59 | } catch ( e ) { 60 | error = e 61 | sleep 25L 62 | } 63 | } 64 | if ( lock == null ) { 65 | throw new RuntimeException( 'Unable to get lock on report file', error ) 66 | } else try { 67 | return action.call() 68 | } finally { 69 | lock.channel().force( true ) // forces flushing before releasing the lock 70 | lock.release() 71 | file.close() 72 | } 73 | } 74 | 75 | private static void appendDataToFile( RandomAccessFile file, Map localData ) { 76 | if ( localData.isEmpty() ) { 77 | return; 78 | } 79 | def toWrite = JsonOutput.toJson( localData ) 80 | def pointer = file.filePointer 81 | if ( pointer > 1 ) { 82 | // move back by one byte so we can remove the last '}' character 83 | file.seek( pointer - 1 ) 84 | // and replace the opening '{' of the appended json object with a ',' 85 | toWrite = toWrite.replaceFirst( Pattern.quote( '{' ), ',' ) 86 | } 87 | file.write( toWrite.getBytes( charset ) ) 88 | } 89 | 90 | @PackageScope 91 | static String readTextFrom( RandomAccessFile file ) { 92 | def buffer = new byte[file.length()] 93 | file.readFully( buffer ) 94 | new String( buffer, charset ) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/SpecData.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.transform.CompileStatic 4 | import org.spockframework.runtime.model.ErrorInfo 5 | import org.spockframework.runtime.model.FeatureInfo 6 | import org.spockframework.runtime.model.IterationInfo 7 | import org.spockframework.runtime.model.SpecInfo 8 | 9 | import java.util.function.Function 10 | 11 | /** 12 | * Data collected for a Spock Specification. 13 | */ 14 | @CompileStatic 15 | class SpecData { 16 | private final List featureRuns = [ ].asSynchronized() as List 17 | final SpecInfo info 18 | final long startTime 19 | long totalTime 20 | ErrorInfo initializationError 21 | ErrorInfo cleanupSpecError 22 | 23 | SpecData( SpecInfo info ) { 24 | this.info = info 25 | this.startTime = System.currentTimeMillis() 26 | } 27 | 28 | def T withFeatureRuns( Function, T> action ) { 29 | synchronized ( featureRuns ) { 30 | return action.apply( featureRuns ) 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Data related to a single feature run in a Specification. 37 | */ 38 | @CompileStatic 39 | class FeatureRun { 40 | final FeatureInfo feature 41 | final Map> failuresByIteration = [ : ].asSynchronized() 42 | final Map timeByIteration = [ : ].asSynchronized() 43 | 44 | FeatureRun( FeatureInfo feature ) { 45 | this.feature = feature 46 | } 47 | 48 | int iterationCount() { 49 | failuresByIteration.size() 50 | } 51 | 52 | /** 53 | * Copy the failuresByIteration Map. 54 | * Use this method to be able to safely iterate over the Map. 55 | * @return a copy of the failuresByIteration. 56 | */ 57 | Map> copyFailuresByIteration() { 58 | synchronized ( failuresByIteration ) { 59 | return new LinkedHashMap<>( failuresByIteration ) 60 | } 61 | } 62 | 63 | /** 64 | * Copy the timeByIteration Map. 65 | * Use this method to be able to safely iterate over the Map. 66 | * @return a copy of the timeByIteration. 67 | */ 68 | Map copyTimeByIteration() { 69 | synchronized ( timeByIteration ) { 70 | return new LinkedHashMap<>( timeByIteration ) 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Information about an error that occurred within a feature run. 77 | */ 78 | @CompileStatic 79 | class SpecProblem { 80 | 81 | final ErrorInfo failure 82 | 83 | SpecProblem( ErrorInfo failure ) { 84 | this.failure = failure 85 | } 86 | 87 | FailureKind getKind() { 88 | failure.exception instanceof AssertionError ? FailureKind.FAILURE : FailureKind.ERROR 89 | } 90 | 91 | } 92 | 93 | /** 94 | * Kind of failure for a feature run. 95 | * 96 | * An ERROR means an unexpected {@link Throwable} was thrown, while FAILURE means a test assertion failure. 97 | */ 98 | enum FailureKind { 99 | FAILURE, ERROR 100 | } 101 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/SpecSummaryNameOption.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | enum SpecSummaryNameOption { 4 | CLASS_NAME_AND_TITLE, 5 | CLASS_NAME, 6 | TITLE 7 | } 8 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/SpockReportsConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.transform.ToString 4 | import spock.config.ConfigurationObject 5 | 6 | @ConfigurationObject( 'spockReports' ) 7 | @ToString 8 | class SpockReportsConfiguration { 9 | 10 | Map properties = [ : ] 11 | 12 | void addSet( map ) { 13 | if ( map instanceof Map ) { 14 | map.each { k, value -> 15 | properties[ k.toString() ] = value 16 | } 17 | } else { 18 | throw new IllegalArgumentException( "Expected map entry (e.g. 'set \"some.property\": \"value\"'), found $map" ) 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/StringFormatHelper.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.time.TimeDuration 4 | 5 | import java.text.DecimalFormat 6 | 7 | /** 8 | * 9 | * User: Renato 10 | */ 11 | class StringFormatHelper { 12 | 13 | private final MINUTE = 60 * 1000 14 | private final HOUR = 60 * MINUTE 15 | static final ds = new DecimalFormat().decimalFormatSymbols.decimalSeparator 16 | 17 | String toTimeDuration( timeInMs ) { 18 | long t = timeInMs?.toLong() ?: 0L 19 | int hours = ( t / HOUR ).toInteger() 20 | int mins = ( ( t - HOUR * hours ) / MINUTE ).toInteger() 21 | int secs = ( ( t - HOUR * hours - mins * MINUTE ) / 1000 ).toInteger() 22 | int millis = ( t % 1000 ).toInteger() 23 | internationalizeTimeDuration( new TimeDuration( hours, mins, secs, millis ) ) 24 | } 25 | 26 | private String internationalizeTimeDuration( TimeDuration timeDuration ) { 27 | ( ds == '.' ) ? timeDuration.toString() : timeDuration.toString().replace( '.', ds.toString() ) 28 | } 29 | 30 | String toPercentage( double rate ) { 31 | String.format( '%.2f%%', rate * 100 ).replace( "${ds}00", "${ds}0" ) 32 | } 33 | 34 | String formatToHtml( String text ) { 35 | text.replaceAll( /[\t\n]/, '
' ) 36 | } 37 | 38 | String toDateString( Date date ) { 39 | date.toString() 40 | } 41 | 42 | String escapeXml( String str ) { 43 | str 44 | .replaceAll( '&', '&' ) 45 | .replaceAll( '<', '<' ) 46 | .replaceAll( '>', '>' ) 47 | .replaceAll( '"', '"' ) 48 | .replaceAll( '\'', ''' ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/internal/StringTemplateProcessor.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.transform.CompileStatic 4 | import org.spockframework.runtime.extension.builtin.UnrollIterationNameProvider 5 | import org.spockframework.runtime.model.FeatureInfo 6 | import org.spockframework.runtime.model.IterationInfo 7 | 8 | /** 9 | * 10 | */ 11 | class StringTemplateProcessor { 12 | 13 | @CompileStatic 14 | String process( String input, List dataVariables, IterationInfo iteration ) { 15 | def tempFeature = new FeatureInfo() 16 | tempFeature.name = input 17 | for ( variable in dataVariables ) { 18 | tempFeature.addParameterName( variable ) 19 | } 20 | tempFeature.iterationNameProvider = new UnrollIterationNameProvider( tempFeature, input, false ) 21 | tempFeature.iterationNameProvider.getName( iteration ) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/template/TemplateReportAggregator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.template 2 | 3 | import com.athaydes.spockframework.report.internal.SpecData 4 | import com.athaydes.spockframework.report.internal.StringFormatHelper 5 | import com.athaydes.spockframework.report.util.Utils 6 | import groovy.text.GStringTemplateEngine 7 | import groovy.util.logging.Slf4j 8 | import spock.lang.Title 9 | 10 | import static com.athaydes.spockframework.report.internal.ReportDataAggregator.getAllAggregatedDataAndPersistLocalData 11 | 12 | @Slf4j 13 | class TemplateReportAggregator { 14 | 15 | private final Map aggregatedData = [ : ] 16 | 17 | volatile String projectName 18 | volatile String projectVersion 19 | volatile String aggregatedJsonReportDir 20 | 21 | void addData( SpecData data ) { 22 | def specName = Utils.getSpecClassName( data ) 23 | log.debug( "Adding data to report {}", specName ) 24 | 25 | def stats = Utils.stats( data ) 26 | def allFeatures = data.info.allFeaturesInExecutionOrder.groupBy { feature -> Utils.isSkipped( feature ) } 27 | def specTitle = Utils.specAnnotation( data, Title )?.value() ?: '' 28 | def narrative = data.info.narrative ?: '' 29 | 30 | aggregatedData[ specName ] = Utils.createAggregatedData( 31 | allFeatures[ false ], allFeatures[ true ], stats, specTitle, narrative ) 32 | } 33 | 34 | private String summary( String templateLocation, Map allData ) { 35 | def template = this.class.getResource( templateLocation ) 36 | if ( !template ) { 37 | log.warn( "Summary template location does not exist: $templateLocation" ) 38 | throw new RuntimeException( 'SpockReports: TemplateReportAggregator extension could not create ' + 39 | 'report as template could not be found at ' + templateLocation ) 40 | } 41 | 42 | def engine = new GStringTemplateEngine() 43 | 44 | engine.createTemplate( template ) 45 | .make( [ data : allData, 46 | 'utils' : Utils, 47 | 'fmt' : new StringFormatHelper(), 48 | projectName : projectName, 49 | projectVersion: projectVersion ] ) 50 | .toString() 51 | } 52 | 53 | void writeOut( File summaryFile, String templateLocation ) { 54 | final reportsDir = summaryFile.parentFile 55 | final jsonDir = aggregatedJsonReportDir ? new File( aggregatedJsonReportDir ) : reportsDir 56 | log.info( "Writing summary report to {}", summaryFile.absolutePath ) 57 | 58 | try { 59 | def allData = getAllAggregatedDataAndPersistLocalData( jsonDir, aggregatedData ) 60 | aggregatedData.clear() 61 | summaryFile.write summary( templateLocation, allData ) 62 | } catch ( e ) { 63 | log.warn( "${this.class.name} failed to create aggregated report", e ) 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/template/TemplateReportCreator.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.template 2 | 3 | import com.athaydes.spockframework.report.IReportCreator 4 | import com.athaydes.spockframework.report.internal.FeatureRun 5 | import com.athaydes.spockframework.report.internal.SpecData 6 | import com.athaydes.spockframework.report.internal.StringFormatHelper 7 | import com.athaydes.spockframework.report.internal.StringTemplateProcessor 8 | import com.athaydes.spockframework.report.util.Utils 9 | import com.athaydes.spockframework.report.vivid.BlockCode 10 | import com.athaydes.spockframework.report.vivid.SpecSourceCodeReader 11 | import groovy.text.GStringTemplateEngine 12 | import groovy.util.logging.Slf4j 13 | import org.spockframework.runtime.model.BlockInfo 14 | import org.spockframework.runtime.model.FeatureInfo 15 | import org.spockframework.runtime.model.IterationInfo 16 | 17 | import static java.util.Collections.emptyList 18 | import static java.util.Collections.emptyMap 19 | 20 | /** 21 | * IReportCreator which uses a user-provided template to generate spock-reports. 22 | */ 23 | @Slf4j 24 | class TemplateReportCreator implements IReportCreator { 25 | 26 | static final reportAggregator = new TemplateReportAggregator() 27 | 28 | final stringProcessor = new StringTemplateProcessor() 29 | 30 | // IReportCreator shared properties 31 | String outputDir 32 | boolean hideEmptyBlocks 33 | boolean showCodeBlocks 34 | 35 | // TemplateReportCreator properties 36 | String specTemplateFile 37 | String reportFileExtension 38 | String summaryTemplateFile 39 | String summaryFileName 40 | boolean enabled = true 41 | 42 | private final SpecSourceCodeReader codeReader = new SpecSourceCodeReader() 43 | 44 | @Override 45 | void setTestSourceRoots( roots ) { 46 | if ( roots ) { 47 | codeReader.testSourceRoots = roots 48 | } 49 | } 50 | 51 | void setEnabled( String enabled ) { 52 | try { 53 | this.@enabled = Boolean.parseBoolean( enabled ) 54 | } catch ( e ) { 55 | log.warn( "Problem parsing 'enabled' property, invalid value: $enabled", e ) 56 | } 57 | } 58 | 59 | @Override 60 | void setProjectName( String projectName ) { 61 | reportAggregator?.projectName = projectName 62 | } 63 | 64 | @Override 65 | void setProjectVersion( String projectVersion ) { 66 | reportAggregator?.projectVersion = projectVersion 67 | } 68 | 69 | @Override 70 | void setAggregatedJsonReportDir( String path ) { 71 | reportAggregator?.aggregatedJsonReportDir = path 72 | } 73 | 74 | void done() { 75 | if ( !enabled ) { 76 | return 77 | } 78 | 79 | def reportsDir = Utils.createDir( outputDir ) 80 | 81 | reportAggregator?.writeOut( 82 | new File( reportsDir, summaryFileName ), 83 | summaryTemplateFile ) 84 | } 85 | 86 | @Override 87 | void createReportFor( SpecData data ) { 88 | if ( !enabled ) { 89 | return 90 | } 91 | 92 | def specClassName = Utils.getSpecClassName( data ) 93 | def reportsDir = Utils.createDir( outputDir ) 94 | def reportFile = new File( reportsDir, specClassName + '.' + reportFileExtension ) 95 | reportFile.delete() 96 | try { 97 | if ( reportsDir.isDirectory() ) { 98 | log.debug( "Writing report to file: {}", reportFile ) 99 | reportFile.write( reportFor( data ), 'UTF-8' ) 100 | reportAggregator.addData( data ) 101 | } else { 102 | log.warn "${this.class.name} cannot create output directory: ${reportsDir.absolutePath}" 103 | } 104 | } catch ( e ) { 105 | log.warn "Unexpected error creating report", e 106 | } 107 | } 108 | 109 | String reportFor( SpecData data ) { 110 | def templateFileUrl = this.class.getResource( specTemplateFile ) 111 | if ( !templateFileUrl ) { 112 | throw new RuntimeException( "Template File does not exist: $specTemplateFile" ) 113 | } 114 | 115 | def engine = new GStringTemplateEngine() 116 | 117 | def featuresCallback = createFeaturesCallback data 118 | 119 | if ( showCodeBlocks ) { 120 | codeReader.read( data ) 121 | } 122 | 123 | engine.createTemplate( templateFileUrl ) 124 | .make( [ reportCreator: this, 125 | 'utils' : Utils, 126 | 'fmt' : new StringFormatHelper(), 127 | data : data, 128 | features : featuresCallback ] ) 129 | .toString() 130 | } 131 | 132 | def createFeaturesCallback( SpecData data ) { 133 | return [ eachFeature: { Closure callback -> 134 | for ( feature in data.info.allFeaturesInExecutionOrder ) { 135 | callback.delegate = feature 136 | FeatureRun run = data.withFeatureRuns { it.find { it.feature == feature } } 137 | if ( run && Utils.isUnrolled( feature ) ) { 138 | handleUnrolledFeature( run, feature, callback ) 139 | } else { 140 | handleRegularFeature( run, callback, feature ) 141 | } 142 | } 143 | } ] 144 | } 145 | 146 | protected void handleRegularFeature( FeatureRun run, Closure callback, FeatureInfo feature ) { 147 | final failures = run ? Utils.countProblems( [ run ], Utils.&isFailure ) : 0 148 | final errors = run ? Utils.countProblems( [ run ], Utils.&isError ) : 0 149 | final isSkipped = !run || Utils.isSkipped( feature ) 150 | final result = errors ? 'ERROR' : failures ? 'FAIL' : isSkipped ? 'IGNORED' : 'PASS' 151 | final problemsByIteration = run ? Utils.iterationData( run.copyFailuresByIteration(), run.copyTimeByIteration() ) : [ : ] 152 | callback.call( feature.name, result, processedBlocks( feature ), problemsByIteration, feature.parameterNames ) 153 | } 154 | 155 | protected void handleUnrolledFeature( FeatureRun run, FeatureInfo feature, Closure callback ) { 156 | def iterations = run.copyFailuresByIteration() 157 | def multipleIterations = iterations.size() > 1 158 | iterations.eachWithIndex { iteration, problems, index -> 159 | final name = Utils.featureNameFrom( feature, iteration, index, multipleIterations ) 160 | final result = problems.any( Utils.&isError ) ? 'ERROR' : 161 | problems.any( Utils.&isFailure ) ? 'FAIL' : 162 | Utils.isSkipped( feature ) ? 'IGNORED' : 'PASS' 163 | final time = run.timeByIteration.get( iteration, 0L ) 164 | final problemsByIteration = Utils.iterationData( [ ( iteration ): problems ], [ ( iteration ): time ] ) 165 | callback.call( name, result, processedBlocks( feature, iteration ), problemsByIteration, feature.parameterNames ) 166 | } 167 | } 168 | 169 | protected List processedBlocks( FeatureInfo feature, IterationInfo iteration = null ) { 170 | if ( showCodeBlocks ) { 171 | def result = processedBlocksFromCode( feature, iteration ) 172 | if ( result ) { // only return if we found something, otherwise, run the conventional procedure 173 | return result 174 | } 175 | } 176 | 177 | // as we don't have the AST, we need to use the old way to get the block text from Spock's API 178 | feature.blocks.collect { BlockInfo block -> 179 | List blockTexts = block.texts 180 | 181 | if ( !Utils.isEmptyOrContainsOnlyEmptyStrings( blockTexts ) ) { 182 | int index = 0 183 | blockTexts.collect { blockText -> 184 | if ( iteration ) { 185 | blockText = stringProcessor.process( blockText, feature.dataVariables, iteration ) 186 | } 187 | [ kind : Utils.block2String[ ( index++ ) == 0 ? block.kind : 'and' ], 188 | text : blockText, 189 | sourceCode: emptyList() ] 190 | } 191 | } else if ( !hideEmptyBlocks ) { 192 | [ [ kind : Utils.block2String[ block.kind ], 193 | text : '----', 194 | sourceCode: emptyList() ] ] 195 | } else { 196 | [ [ : ] ] 197 | } 198 | }.flatten().findAll { Map item -> !item.isEmpty() } 199 | } 200 | 201 | private List processedBlocksFromCode( FeatureInfo feature, IterationInfo iteration ) { 202 | def blocks = codeReader.getBlocks( feature ) 203 | 204 | blocks.collect { BlockCode block -> 205 | def blockText = iteration && block.text ? 206 | stringProcessor.process( block.text, feature.dataVariables, iteration ) : 207 | ( block.text ?: '' ) 208 | def blockKind = Utils.block2String[ block.label ] ?: 'Block:' // use an emergency label if something goes wrong 209 | 210 | if ( blockText || block.statements || !hideEmptyBlocks ) { 211 | [ kind: blockKind, text: blockText, sourceCode: block.statements ] 212 | } else { 213 | emptyMap() 214 | } 215 | }.findAll { Map map -> !map.isEmpty() } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/util/Hasher.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.util 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @Singleton 6 | @CompileStatic 7 | class Hasher { 8 | 9 | String hash( String text ) { 10 | text.hashCode().toString() 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/vivid/SpecSourceCode.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.vivid 2 | 3 | import groovy.transform.Canonical 4 | import groovy.transform.CompileStatic 5 | import groovy.transform.PackageScope 6 | import groovy.transform.ToString 7 | import groovy.util.logging.Slf4j 8 | import org.codehaus.groovy.ast.MethodNode 9 | import org.spockframework.util.Nullable 10 | 11 | @ToString 12 | @Slf4j 13 | @CompileStatic 14 | class SpecSourceCode { 15 | 16 | private final Map sourceCodeByFeatureName = [ : ] 17 | private final LinkedHashSet parents = [ ] 18 | 19 | void addParent( SpecSourceCode parent ) { 20 | parents << parent 21 | } 22 | 23 | void startBlock( MethodNode feature, String label, @Nullable String text ) { 24 | sourceCodeByFeatureName.get( feature.name, new FeatureSourceCode() ).startBlock( label, text ) 25 | } 26 | 27 | void addStatement( MethodNode feature, String statement, int lineNumber ) { 28 | def currentFeature = sourceCodeByFeatureName[ feature.name ] 29 | if ( currentFeature ) { 30 | statement = removeIndent( statement ) 31 | currentFeature.addStatement( statement, lineNumber ) 32 | } else { 33 | log.debug( "Skipping statement on method {}, not a test method?", feature?.name ) 34 | } 35 | } 36 | 37 | List getBlocks( String featureName ) { 38 | List result = sourceCodeByFeatureName[ featureName ]?.blocks 39 | if ( result == null ) { 40 | log.debug( 'Unable to find code for feature "{}", will try in parent Specs: {}', featureName, parents ) 41 | 42 | Iterator parentIterator = parents.iterator() 43 | while ( result == null ) { 44 | if ( parentIterator.hasNext() ) { 45 | def parent = parentIterator.next() 46 | result = parent.getBlocks( featureName ) 47 | } else { 48 | break 49 | } 50 | } 51 | } 52 | return result ?: [ ] 53 | } 54 | 55 | static String removeIndent( String code ) { 56 | def lines = code.readLines() 57 | 58 | def firstTextIndexes = lines.collect { String line -> line.findIndexOf { it != ' ' } } 59 | def minIndent = firstTextIndexes.min() 60 | 61 | if ( minIndent > 0 ) { 62 | def resultLines = lines.collect { String line -> trimLine( line, minIndent ) } 63 | return resultLines.join( '\n' ) 64 | } else { 65 | return code 66 | } 67 | } 68 | 69 | private static String trimLine( String line, int minIndent ) { 70 | def lastIndex = line.findLastIndexOf { it != ' ' } 71 | line[minIndent..lastIndex] 72 | } 73 | 74 | } 75 | 76 | @ToString 77 | @CompileStatic 78 | @PackageScope 79 | class FeatureSourceCode { 80 | private final List blocks = [ ] 81 | 82 | void startBlock( String label, @Nullable String text ) { 83 | blocks << new BlockCode( label, text, [ ], [ ] ) 84 | } 85 | 86 | void addStatement( String statement, int lineNumber ) { 87 | def block = blocks.last() 88 | statement.split( '\n' ).eachWithIndex { String line, int index -> 89 | block.statements.add( line ) 90 | block.lineNumbers.add( lineNumber + index ) 91 | } 92 | } 93 | 94 | List getBlocks() { 95 | return this.@blocks.asImmutable() 96 | } 97 | } 98 | 99 | @Canonical 100 | class BlockCode { 101 | final String label 102 | @Nullable 103 | final String text 104 | final List statements 105 | final List lineNumbers 106 | } 107 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/vivid/SpecSourceCodeCollector.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.vivid 2 | 3 | import com.athaydes.spockframework.report.util.Utils 4 | import groovy.transform.CompileStatic 5 | import groovy.util.logging.Slf4j 6 | import org.codehaus.groovy.ast.MethodNode 7 | import org.codehaus.groovy.ast.ModuleNode 8 | import org.codehaus.groovy.ast.expr.ConstantExpression 9 | import org.codehaus.groovy.ast.stmt.ExpressionStatement 10 | import org.codehaus.groovy.ast.stmt.Statement 11 | import org.codehaus.groovy.control.Janitor 12 | import org.codehaus.groovy.control.SourceUnit 13 | import org.spockframework.util.Nullable 14 | 15 | import java.util.concurrent.ConcurrentHashMap 16 | 17 | @Slf4j 18 | @CompileStatic 19 | class SpecSourceCodeCollector implements AutoCloseable { 20 | 21 | private final SourceUnit sourceUnit 22 | private final Janitor janitor = new Janitor() 23 | 24 | final ModuleNode module 25 | 26 | private static final Map specSourceCodeByClassName = [ : ] as ConcurrentHashMap 27 | 28 | @Nullable 29 | private String className 30 | @Nullable 31 | private MethodNode method 32 | 33 | SpecSourceCodeCollector( SourceUnit sourceUnit ) { 34 | this.sourceUnit = sourceUnit 35 | this.module = sourceUnit.AST 36 | } 37 | 38 | void setClassName( String className ) { 39 | log.debug( "Collecting code for class {}", className ) 40 | this.@className = className 41 | specSourceCodeByClassName[ className ] = new SpecSourceCode() 42 | } 43 | 44 | void setMethod( MethodNode methodNode ) { 45 | this.@method = methodNode 46 | } 47 | 48 | @Nullable 49 | SpecSourceCode getResultFor( String className ) { 50 | def sourceCode = specSourceCodeByClassName[ className ] 51 | if ( sourceCode ) { 52 | def parentSpecs = Utils.getParentSpecNames( className ) 53 | for ( parentSpec in parentSpecs ) { 54 | def parentCode = specSourceCodeByClassName[ parentSpec ] 55 | if ( parentCode ) { 56 | sourceCode.addParent( parentCode ) 57 | } 58 | } 59 | } 60 | sourceCode 61 | } 62 | 63 | void add( Statement statement ) { 64 | assert className && method 65 | 66 | def label = statement.statementLabel 67 | def code = lookupCode( statement ) 68 | def specCode = specSourceCodeByClassName[ className ] 69 | 70 | if ( !specCode ) { 71 | log.warn( "Ignoring statement, class has not been initialized: $className" ) 72 | return 73 | } 74 | 75 | log.debug( "LABEL: $label -> $code" ) 76 | if ( label ) { 77 | def labelText = stringConstant( statement ) 78 | specCode.startBlock( method, label, labelText ) 79 | 80 | if ( labelText ) { 81 | return // don't add the text to the code 82 | } 83 | } 84 | // All statements must be added to the code in the report 85 | specCode.addStatement( method, code, statement.lineNumber ) 86 | } 87 | 88 | private String lookupCode( Statement statement) { 89 | def text = new StringBuilder() 90 | for (int i = statement.getLineNumber(); i <= statement.getLastLineNumber(); i++) { 91 | def line = sourceUnit.getSample( i, 0, janitor ) 92 | text.append( line ).append( '\n' ) 93 | } 94 | text.toString() 95 | } 96 | 97 | @Nullable 98 | private static String stringConstant( Statement statement ) { 99 | if ( statement instanceof ExpressionStatement ) { 100 | def expr = ( statement as ExpressionStatement ).expression 101 | if ( expr instanceof ConstantExpression && expr.type.name == 'java.lang.String' ) { 102 | return ( expr as ConstantExpression ).value as String 103 | } 104 | } 105 | 106 | null 107 | } 108 | 109 | @Override 110 | void close() { 111 | janitor.cleanup() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/vivid/SpecSourceCodeReader.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.vivid 2 | 3 | import com.athaydes.spockframework.report.internal.SpecData 4 | import com.athaydes.spockframework.report.util.Utils 5 | import groovy.transform.CompileStatic 6 | import groovy.util.logging.Slf4j 7 | import org.spockframework.runtime.model.FeatureInfo 8 | import org.spockframework.util.Nullable 9 | 10 | @Slf4j 11 | @CompileStatic 12 | class SpecSourceCodeReader { 13 | 14 | def testSourceRoots = ['src/test/groovy'] 15 | 16 | @Nullable 17 | private SpecSourceCode specSourceCode 18 | 19 | private final VividAstInspector inspector = new VividAstInspector() 20 | 21 | void read( SpecData data ) { 22 | try { 23 | File file = Utils.getSpecFile( testSourceRoots, data ) 24 | specSourceCode = inspector.load( file, Utils.getSpecClassName( data ) ) 25 | } catch ( Exception e ) { 26 | log.error( "Cannot create SpecSourceCode: ${e.message ?: e}", e ) 27 | } 28 | } 29 | 30 | List getBlocks( FeatureInfo feature ) { 31 | return specSourceCode?.getBlocks( feature.name ) ?: [ ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/groovy/com/athaydes/spockframework/report/vivid/VividAstInspector.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.vivid 2 | 3 | import groovy.transform.CompileStatic 4 | import groovy.util.logging.Slf4j 5 | import org.codehaus.groovy.ast.AnnotatedNode 6 | import org.codehaus.groovy.ast.ClassCodeVisitorSupport 7 | import org.codehaus.groovy.ast.ClassNode 8 | import org.codehaus.groovy.ast.MethodNode 9 | import org.codehaus.groovy.ast.stmt.BlockStatement 10 | import org.codehaus.groovy.ast.stmt.Statement 11 | import org.codehaus.groovy.control.CompilationFailedException 12 | import org.codehaus.groovy.control.CompilationUnit 13 | import org.codehaus.groovy.control.CompilePhase 14 | import org.codehaus.groovy.control.CompilerConfiguration 15 | import org.codehaus.groovy.control.SourceUnit 16 | import org.spockframework.util.Nullable 17 | import org.spockframework.util.inspector.AstInspectorException 18 | 19 | import java.security.CodeSource 20 | import java.util.concurrent.ConcurrentHashMap 21 | 22 | /** 23 | * Based on org.spockframework.util.inspector.AstInspector by Peter Niederwieser 24 | */ 25 | @CompileStatic 26 | @Slf4j 27 | class VividAstInspector { 28 | 29 | private CompilePhase compilePhase = CompilePhase.CONVERSION 30 | private final VividClassLoader classLoader 31 | private final Map specCodesByFile = [ : ] as ConcurrentHashMap 32 | 33 | VividAstInspector() { 34 | classLoader = new VividClassLoader( this.class.classLoader ) 35 | } 36 | 37 | @Nullable 38 | SpecSourceCode load( @Nullable File sourceFile, String className ) { 39 | // first, check if a previous spec file contained this class 40 | for ( codeCollector in specCodesByFile.values() ) { 41 | def code = codeCollector.getResultFor( className ) 42 | if ( code ) { 43 | log.debug( "Found source code for $className in previously parsed file" ) 44 | return code 45 | } 46 | } 47 | 48 | if ( sourceFile == null ) { 49 | log.warn( "Cannot find source code for spec $className" ) 50 | log.info( "Perhaps you need to set the 'com.athaydes.spockframework.report.testSourceRoots' property? " + 51 | "(the default is src/test/groovy)" ) 52 | return null 53 | } 54 | 55 | boolean alreadyVisited = specCodesByFile.containsKey( sourceFile ) 56 | 57 | if ( alreadyVisited ) { 58 | log.debug( "Cancelling visit to source file, already seen it: $sourceFile" ) 59 | return null 60 | } 61 | 62 | log.debug "Trying to read source file $sourceFile" 63 | 64 | try { 65 | classLoader.parseClass( sourceFile ) 66 | } catch ( IOException e ) { 67 | throw new AstInspectorException( "cannot read source file", e ) 68 | } catch ( AstSuccessfullyCaptured captured ) { 69 | def source = getSpecSource( captured.codeCollector ) 70 | specCodesByFile[ sourceFile ] = source 71 | return source.getResultFor( className ) 72 | } 73 | 74 | throw new AstInspectorException( "internal error" ) 75 | } 76 | 77 | private SpecSourceCodeCollector getSpecSource( SpecSourceCodeCollector codeCollector ) { 78 | final visitor = new VividASTVisitor( codeCollector ) 79 | final module = codeCollector.module 80 | 81 | codeCollector.withCloseable { 82 | visitor.visitBlockStatement( module.statementBlock ) 83 | 84 | for ( MethodNode method in module.methods ) { 85 | visitor.visitMethod( method ) 86 | } 87 | 88 | for ( ClassNode clazz in module.classes ) { 89 | visitor.visitClass( clazz ) 90 | } 91 | 92 | codeCollector 93 | } 94 | } 95 | 96 | private class VividClassLoader extends GroovyClassLoader { 97 | VividClassLoader( ClassLoader parent ) { 98 | super( parent, null ) 99 | } 100 | 101 | @Override 102 | protected CompilationUnit createCompilationUnit( CompilerConfiguration config, CodeSource source ) { 103 | CompilationUnit unit = super.createCompilationUnit( config, source ) 104 | 105 | unit.addPhaseOperation( new CompilationUnit.SourceUnitOperation() { 106 | @Override 107 | void call( SourceUnit sourceUnit ) throws CompilationFailedException { 108 | throw new AstSuccessfullyCaptured( new SpecSourceCodeCollector( sourceUnit ) ) 109 | } 110 | }, compilePhase.phaseNumber ) 111 | return unit 112 | } 113 | } 114 | 115 | private static class AstSuccessfullyCaptured extends Error { 116 | final SpecSourceCodeCollector codeCollector 117 | 118 | AstSuccessfullyCaptured( SpecSourceCodeCollector codeCollector ) { 119 | super() 120 | this.codeCollector = codeCollector 121 | } 122 | 123 | } 124 | 125 | } 126 | 127 | @CompileStatic 128 | class VividASTVisitor extends ClassCodeVisitorSupport { 129 | 130 | private final SpecSourceCodeCollector codeCollector 131 | private boolean visitStatements = false 132 | 133 | @Nullable 134 | private String currentLabel = null 135 | 136 | VividASTVisitor( SpecSourceCodeCollector codeCollector ) { 137 | this.codeCollector = codeCollector 138 | } 139 | 140 | @Override 141 | void visitClass( ClassNode node ) { 142 | codeCollector.className = node.name 143 | super.visitClass( node ) 144 | } 145 | 146 | @Override 147 | void visitMethod( MethodNode node ) { 148 | def previousIsTestMethod = visitStatements 149 | visitStatements = node.isPublic() && !node.isStatic() 150 | 151 | if ( visitStatements ) { 152 | currentLabel = null 153 | codeCollector.method = node 154 | } 155 | 156 | super.visitMethod( node ) 157 | 158 | codeCollector.method = null 159 | 160 | visitStatements = previousIsTestMethod 161 | } 162 | 163 | // This is overridden to avoid visiting annotations. 164 | @Override 165 | void visitAnnotations( AnnotatedNode node ) { 166 | // do nothing - we don't want to visit annotations (see #231) 167 | } 168 | 169 | @Override 170 | void visitStatement( Statement node ) { 171 | if ( visitStatements && node instanceof BlockStatement ) { 172 | def stmts = ( node as BlockStatement ).statements 173 | if ( stmts ) 174 | for ( statement in stmts ) { 175 | codeCollector.add( statement ) 176 | } 177 | visitStatements = false 178 | } 179 | super.visitStatement( node ) 180 | } 181 | 182 | @Override 183 | protected SourceUnit getSourceUnit() { 184 | throw new AstInspectorException( "internal error" ) 185 | } 186 | } 187 | 188 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule: -------------------------------------------------------------------------------- 1 | moduleName=Spock Reports Extension Module 2 | moduleVersion=1.0 3 | extensionClasses=com.athaydes.spockframework.report.extension.SpockReportsSpecificationExtension -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension: -------------------------------------------------------------------------------- 1 | com.athaydes.spockframework.report.SpockReportExtension -------------------------------------------------------------------------------- /src/main/resources/com/athaydes/spockframework/report/internal/config.properties: -------------------------------------------------------------------------------- 1 | # Name of the implementation class(es) of report creator(s) to enable (separate multiple entries with commas) 2 | # Currently supported classes are: 3 | # 1. com.athaydes.spockframework.report.internal.HtmlReportCreator 4 | # 2. com.athaydes.spockframework.report.template.TemplateReportCreator 5 | com.athaydes.spockframework.report.IReportCreator=com.athaydes.spockframework.report.internal.HtmlReportCreator 6 | 7 | # Set properties of the report creator 8 | # For the HtmlReportCreator, the only properties available are 9 | # (the location of the css files is relative to the classpath): 10 | com.athaydes.spockframework.report.internal.HtmlReportCreator.featureReportCss=spock-feature-report.css 11 | com.athaydes.spockframework.report.internal.HtmlReportCreator.summaryReportCss=spock-summary-report.css 12 | com.athaydes.spockframework.report.internal.HtmlReportCreator.printThrowableStackTrace=false 13 | com.athaydes.spockframework.report.internal.HtmlReportCreator.inlineCss=true 14 | com.athaydes.spockframework.report.internal.HtmlReportCreator.enabled=true 15 | # options are: "class_name_and_title", "class_name", "title" 16 | com.athaydes.spockframework.report.internal.HtmlReportCreator.specSummaryNameOption=class_name_and_title 17 | 18 | # exclude Specs Table of Contents 19 | com.athaydes.spockframework.report.internal.HtmlReportCreator.excludeToc=false 20 | 21 | # Output directory (where the spock reports will be created) - relative to working directory 22 | com.athaydes.spockframework.report.outputDir=build/spock-reports 23 | 24 | # Output directory where to store the aggregated JSON report (used to support parallel builds) 25 | com.athaydes.spockframework.report.aggregatedJsonReportDir= 26 | 27 | # If set to true, hides blocks which do not have any description 28 | com.athaydes.spockframework.report.hideEmptyBlocks=false 29 | 30 | # Set the name of the project under test so it can be displayed in the report 31 | com.athaydes.spockframework.report.projectName= 32 | 33 | # Set the version of the project under test so it can be displayed in the report 34 | com.athaydes.spockframework.report.projectVersion=Unknown 35 | 36 | # Show the source code for each block 37 | com.athaydes.spockframework.report.showCodeBlocks=false 38 | 39 | # Set the root location of the Spock test source code (only used if showCodeBlocks is 'true') 40 | com.athaydes.spockframework.report.testSourceRoots=src/test/groovy 41 | 42 | # Set properties specific to the TemplateReportCreator 43 | com.athaydes.spockframework.report.template.TemplateReportCreator.specTemplateFile=/templateReportCreator/spec-template.md 44 | com.athaydes.spockframework.report.template.TemplateReportCreator.reportFileExtension=md 45 | com.athaydes.spockframework.report.template.TemplateReportCreator.summaryTemplateFile=/templateReportCreator/summary-template.md 46 | com.athaydes.spockframework.report.template.TemplateReportCreator.summaryFileName=summary.md 47 | com.athaydes.spockframework.report.template.TemplateReportCreator.enabled=true 48 | -------------------------------------------------------------------------------- /src/main/resources/spock-feature-report.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-weight: 300; 4 | } 5 | 6 | h2 { 7 | font-weight: 400; 8 | } 9 | 10 | h3 { 11 | font-weight: 200; 12 | } 13 | 14 | .back-link { 15 | font-size: small; 16 | } 17 | 18 | table { 19 | margin: 5px; 20 | } 21 | 22 | div.date-test-ran { 23 | font-size: small; 24 | font-style: italic; 25 | } 26 | 27 | .return-toc { 28 | float: right; font-size: 60%; 29 | } 30 | 31 | table.features-table { 32 | width: 800px; 33 | } 34 | 35 | table.summary-table { 36 | width: 800px; 37 | text-align: left; 38 | font-weight: bold; 39 | font-size: small; 40 | } 41 | 42 | table.summary-table th { 43 | background: lightblue; 44 | padding: 6px; 45 | } 46 | 47 | table.summary-table td { 48 | background: #E0E0E0; 49 | padding: 6px; 50 | } 51 | 52 | pre.title { 53 | font-family: inherit; 54 | font-size: 24px; 55 | line-height: 28px; 56 | letter-spacing: -1px; 57 | color: #333; 58 | } 59 | 60 | pre.narrative { 61 | font-family: inherit; 62 | font-size: 18px; 63 | font-style: italic; 64 | line-height: 23px; 65 | letter-spacing: -1px; 66 | color: #333; 67 | } 68 | 69 | .feature-description { 70 | font-size: large; 71 | background: lightblue; 72 | padding: 12px; 73 | } 74 | 75 | .feature-toc-error { 76 | color: #F89A4F; 77 | } 78 | 79 | .feature-toc-failure { 80 | color: #FF8080; 81 | } 82 | 83 | .feature-toc-ignored { 84 | color: lightgray; 85 | } 86 | 87 | .feature-toc-pass { 88 | color: green; 89 | } 90 | 91 | .feature-description.error { 92 | background: #F89A4F; 93 | } 94 | 95 | .feature-description.failure { 96 | background: #FF8080; 97 | } 98 | 99 | .feature-description.ignored { 100 | background: lightgray; 101 | } 102 | 103 | .feature-description.ignored .reason { 104 | color: black; 105 | font-style: italic; 106 | font-size: small; 107 | } 108 | 109 | div.extra-info { 110 | font-size: small; 111 | } 112 | 113 | div.spec-headers { 114 | margin: 4px; 115 | font-style: italic; 116 | } 117 | 118 | div.spec-header { 119 | } 120 | 121 | div.issues { 122 | margin-top: 6px; 123 | padding: 10px 5px 5px 5px; 124 | background-color: lemonchiffon; 125 | color: black; 126 | font-weight: 500; 127 | font-size: small; 128 | max-width: 50%; 129 | } 130 | 131 | div.pending-feature { 132 | background-color: dodgerblue; 133 | color: white; 134 | margin-top: 6px; 135 | padding: 5px; 136 | text-align: center; 137 | font-size: small; 138 | max-width: 120px; 139 | } 140 | 141 | div.problem-description { 142 | padding: 10px; 143 | background: pink; 144 | border-radius: 10px; 145 | } 146 | 147 | div.problem-header { 148 | font-weight: bold; 149 | color: red; 150 | } 151 | 152 | div.problem-list { 153 | 154 | } 155 | 156 | table.ex-table th { 157 | background: lightblue; 158 | padding: 5px; 159 | } 160 | 161 | table.ex-table td { 162 | background: #E0E0E0; 163 | padding: 2px 5px 2px 5px; 164 | } 165 | 166 | table td { 167 | min-width: 50px; 168 | } 169 | 170 | col.block-kind-col { 171 | width: 70px; 172 | } 173 | 174 | span.spec-header { 175 | font-weight: bold; 176 | } 177 | 178 | div.spec-text { 179 | /*color: green;*/ 180 | } 181 | 182 | div.spec-status { 183 | font-style: italic; 184 | } 185 | 186 | .ignored { 187 | color: gray; 188 | } 189 | 190 | td.ex-result { 191 | text-align: center; 192 | background: white !important; 193 | } 194 | 195 | .ex-time { 196 | font-style: italic; 197 | font-size: smaller; 198 | } 199 | 200 | .ex-pass { 201 | color: darkgreen; 202 | } 203 | 204 | .ex-fail { 205 | color: red; 206 | font-weight: bold; 207 | } 208 | 209 | div.block-kind { 210 | margin: 2px; 211 | font-style: italic; 212 | } 213 | 214 | div.block-text { 215 | 216 | } 217 | 218 | pre.block-source { 219 | background-color: whitesmoke; 220 | padding: 10px; 221 | } 222 | 223 | pre.block-source.error { 224 | background-color: pink; 225 | color: red; 226 | font-weight: bold; 227 | } 228 | 229 | pre.block-source.pre-error { 230 | 231 | } 232 | 233 | pre.block-source.before-error { 234 | margin-bottom: -14px; 235 | } 236 | 237 | pre.block-source.after-error { 238 | color: gray; 239 | margin-top: -14px; 240 | } 241 | 242 | pre.block-source.post-error { 243 | color: gray; 244 | } 245 | 246 | div.footer { 247 | text-align: center; 248 | font-size: small; 249 | } 250 | -------------------------------------------------------------------------------- /src/main/resources/spock-summary-report.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-weight: 300; 4 | } 5 | 6 | h2 { 7 | font-weight: 400; 8 | } 9 | 10 | h3 { 11 | font-weight: 200; 12 | } 13 | 14 | table { 15 | margin: 5px; 16 | } 17 | 18 | .ignored { 19 | color: gray; 20 | } 21 | 22 | div.project-header { 23 | margin-bottom: 10px; 24 | font-size: large; 25 | } 26 | 27 | div.project-header > span.project-name { 28 | 29 | } 30 | 31 | div.project-header > span.project-version { 32 | padding-left: 20px; 33 | } 34 | 35 | div.date-test-ran { 36 | font-size: small; 37 | font-style: italic; 38 | } 39 | 40 | div.spec-title { 41 | padding: 10px 0px 5px 0px; 42 | } 43 | 44 | table.summary-table { 45 | width: 800px; 46 | text-align: left; 47 | font-weight: 500; 48 | font-size: small; 49 | } 50 | 51 | table.summary-table th { 52 | background: lightblue; 53 | padding: 6px; 54 | } 55 | 56 | table.summary-table td { 57 | background: #E0E0E0; 58 | padding: 6px; 59 | } 60 | 61 | tr.error td, td.error { 62 | background-color: #F89A4F !important; 63 | } 64 | 65 | tr.failure td, td.failure { 66 | color: red; 67 | } 68 | 69 | div.footer { 70 | text-align: center; 71 | font-size: small; 72 | } 73 | -------------------------------------------------------------------------------- /src/main/resources/templateReportCreator/spec-template.md: -------------------------------------------------------------------------------- 1 | <% 2 | def stats = utils.stats( data ) 3 | %># Report for ${utils.getSpecClassName( data )} 4 | 5 | ##Summary 6 | 7 | * Total Runs: ${stats.totalRuns} 8 | * Success Rate: ${fmt.toPercentage(stats.successRate)} 9 | * Failures: ${stats.failures} 10 | * Errors: ${stats.errors} 11 | * Skipped: ${stats.skipped} 12 | * Total time: ${fmt.toTimeDuration(stats.time)} 13 | 14 | <% 15 | def specTitle = utils.specAnnotation( data, spock.lang.Title )?.value() 16 | if ( specTitle ) { 17 | specTitle.split('\n').each { out << '##' << it << '\n' } 18 | } 19 | if ( data.info.narrative ) { 20 | if ( specTitle ) { out << '\n' } 21 | out << '

\n' << data.info.narrative << '\n
' 22 | } 23 | 24 | def writeTagOrAttachment = { feature -> 25 | def tagsByKey = feature.tags.groupBy( { t -> t.key } ) 26 | tagsByKey.each { key, values -> 27 | out << '\n#### ' << key.capitalize() << 's:\n\n' 28 | values.each { tag -> 29 | out << '* ' << tag.url << '\n' 30 | } 31 | } 32 | if ( feature.attachments.size() > 0 ) { 33 | out << '\n#### ' << 'See:' << '\n\n' 34 | feature.attachments.each { value -> 35 | out << '* ' << value.url << '\n' 36 | } 37 | } 38 | } 39 | def writePendingFeature = { pendingFeature -> 40 | if ( pendingFeature ) { 41 | out << '\n> Pending Feature\n' 42 | } 43 | } 44 | def writeHeaders = { headers -> 45 | if ( headers ) { 46 | headers.each { h -> 47 | out << '> ' << h << '\n' 48 | } 49 | } 50 | } 51 | def writeExtraInfo = { extraInfo -> 52 | if ( extraInfo ) { 53 | extraInfo.each { info -> 54 | out << '* ' << info << '\n' 55 | } 56 | } 57 | } 58 | writeHeaders( utils.specHeaders( data ) ) 59 | writeTagOrAttachment data.info 60 | %> 61 | 62 | ## Features 63 | <% 64 | features.eachFeature { name, result, blocks, iterations, params -> 65 | %> 66 | ### $name 67 | <% 68 | writePendingFeature( featureMethod.getAnnotation( spock.lang.PendingFeature ) ) 69 | def feature = delegate 70 | writeTagOrAttachment( feature ) 71 | if (result != "IGNORED") { 72 | if ( utils.isUnrolled( feature ) ) { 73 | iterations.each { iter -> 74 | writeExtraInfo( utils.nextSpecExtraInfo( data, feature, iter.info ) ) 75 | } 76 | } else { 77 | writeExtraInfo( utils.nextSpecExtraInfo( data, feature ) ) 78 | } 79 | } 80 | def iterationTimes = iterations.collect { it.time ?: 0L } 81 | def totalTime = fmt.toTimeDuration( iterationTimes.sum() ) 82 | %> 83 | Result: **$result** 84 | Time: $totalTime 85 | <% 86 | for ( block in blocks ) { 87 | %> 88 | * ${block.kind} ${block.text} 89 | <% 90 | if ( block.sourceCode && block.kind != 'Where:' ) { 91 | out << "\n```\n" 92 | block.sourceCode.each { codeLine -> 93 | out << codeLine << '\n' 94 | } 95 | out << "```\n" 96 | } 97 | } 98 | def executedIterations = iterations.findAll { it.dataValues || it.errors } 99 | 100 | if ( params && executedIterations ) { 101 | def iterationReportedTimes = executedIterations.collect { it.time ?: 0L } 102 | .collect { fmt.toTimeDuration( it ) } 103 | def maxTimeLength = iterationReportedTimes.collect { it.size() }.sort().last() 104 | %> 105 | | ${params.join( ' | ' )} | ${' ' * maxTimeLength} | 106 | |${params.collect { ( '-' * ( it.size() + 2 ) ) + '|' }.join()}${'-' * ( maxTimeLength + 2 )}| 107 | <% 108 | executedIterations.eachWithIndex { iteration, i -> 109 | %> | ${( iteration.dataValues + [ iterationReportedTimes[ i ] ] ).join( ' | ' )} | ${iteration.errors ? '(FAIL)' : '(PASS)'} 110 | <% } 111 | } 112 | def problems = executedIterations.findAll { it.errors } 113 | if ( problems ) { 114 | out << "\nThe following problems occurred:\n\n" 115 | for ( badIteration in problems ) { 116 | if ( badIteration.dataValues ) { 117 | out << '* ' << badIteration.dataValues << '\n' 118 | } 119 | for ( error in badIteration.errors ) { 120 | out << '```\n' << error << '\n```\n' 121 | } 122 | } 123 | } 124 | } 125 | %> 126 | 127 | Generated by Athaydes Spock Reports -------------------------------------------------------------------------------- /src/main/resources/templateReportCreator/summary-template.md: -------------------------------------------------------------------------------- 1 | <% def stats = utils.aggregateStats( data ) 2 | %># Specification run results<% if (projectName && projectVersion) { 3 | %> 4 | 5 | ## Project: ${projectName}, Version: ${projectVersion} <% 6 | } 7 | %> 8 | 9 | ## Specifications summary 10 | 11 | Created on ${new Date()} by ${System.properties['user.name']} 12 | 13 | | Total | Passed | Failed | Feature failures | Feature errors | Success rate | Total time (ms) | 14 | |----------------|-----------------|-----------------|------------------|------------------|---------------------|-----------------| 15 | | ${stats.total} | ${stats.passed} | ${stats.failed} | ${stats.fFails} | ${stats.fErrors} | ${stats.successRate}| ${stats.time} | 16 | 17 | ## Specifications 18 | 19 | |Name | Features | Failed | Errors | Skipped | Success rate | Time | 20 | |------|----------|--------|--------|---------|--------------|------| 21 | <% data.each { name, map -> 22 | def s = map.stats 23 | %>| $name | ${s.totalFeatures} | ${s.failures} | ${s.errors} | ${s.skipped} | ${s.successRate} | ${s.time} | 24 | <% } 25 | %> 26 | 27 | Generated by Athaydes Spock Reports -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/FakeTest.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import spock.lang.* 4 | 5 | /** 6 | * 7 | * User: Renato 8 | */ 9 | @Title( 'This is just a Fake test to test spock-reports' ) 10 | @Narrative( """ 11 | As a user 12 | I want foo 13 | So that bar""" ) 14 | class FakeTest extends Specification { 15 | 16 | @Rollup 17 | def "A first test"() { 18 | given: 19 | "we have x and y" 20 | 21 | and: 22 | "some more things" 23 | 24 | when: 25 | "I do crazy things" 26 | 27 | then: 28 | "I get one iteration pass and another fail" 29 | x == y 30 | 31 | where: 32 | "The examples below are used" 33 | x | y 34 | 'a' | 'a' 35 | 'b' | 'c' 36 | 37 | } 38 | 39 | def "Another feature!!!!"() { 40 | setup: 41 | "Setup block here" 42 | expect: 43 | "Expecting something ??" 44 | } 45 | 46 | def "A when then spec"() { 47 | when: 48 | "This is the when" 49 | then: 50 | "This is the then" 51 | } 52 | 53 | @Ignore( "Feature not implemented yet" ) 54 | def "Please ignore me"() { 55 | given: 56 | "Nothing" 57 | when: 58 | "Do nothing" 59 | then: 60 | "Nothing happens" 61 | } 62 | 63 | @Issue( [ "http://myhost.com/issues/995", "https://myhost.com/issues/973" ] ) 64 | def "A test with an error"() { 65 | when: 66 | "An Exception is thrown" 67 | throw new RuntimeException( 'As expected' ) 68 | 69 | then: 70 | "Will never succeed" 71 | } 72 | 73 | @See( [ "http://myhost.com/features/feature-234" ] ) 74 | def "A test with a failure"() { 75 | when: 76 | "Do nothing" 77 | then: 78 | "Test fails" 79 | verifyAll { 80 | 3 == 2 81 | 5 == 1 82 | } 83 | } 84 | 85 | def """An incredibly long feature description that unfortunately will popup in some cases where business 86 | analysts write these too detailed overviews of what the test should be all about when what they really 87 | should do is to let the details go in the body of the test using the Gherkin language which underlies BDD 88 | and is proven to make it easier for all involved to understand what the test is doing, what the inputs are 89 | and what the expected outcomes are in such a way that the best possible common understanding is reached"""() { 90 | expect: 91 | "The long description above to look good in the report" 92 | } 93 | 94 | def "A Spec with empty block Strings"() { 95 | given: 96 | def a = 0 97 | 98 | and: 99 | def b = 1 100 | 101 | when: 102 | def c = a + b 103 | 104 | then: 105 | "" 106 | c == 1 107 | 108 | and: 109 | " " 110 | c > 0 111 | } 112 | 113 | @Unroll 114 | def "An @Unrolled spec with x=#x and y=#y"() { 115 | setup: 116 | "nothing" 117 | expect: 118 | "#x to be 0" 119 | x == 0 120 | and: 121 | "An error if y is 5" 122 | if ( y == 5 ) throw new RuntimeException( 'y is 5' ) 123 | where: 124 | x | y 125 | 0 | 1 126 | 2 | 3 127 | 0 | 5 128 | } 129 | 130 | @PendingFeature 131 | def "Future feature"() { 132 | when: 133 | 'the feature is ready' 134 | then: 135 | 'the annotation will be removed' 136 | throw new RuntimeException( 'Not ready' ) 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/FullyIgnoredSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import spock.lang.Ignore 4 | import spock.lang.Specification 5 | 6 | @Ignore 7 | class FullyIgnoredSpec extends Specification { 8 | def 'feature1'() { 9 | expect: true 10 | } 11 | 12 | def 'feature2'() { 13 | expect: true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/ReportSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import com.athaydes.spockframework.report.internal.KnowsWhenAndWhoRanTest 4 | import groovy.text.SimpleTemplateEngine 5 | import spock.lang.Specification 6 | 7 | /** 8 | * 9 | * User: Renato 10 | */ 11 | abstract class ReportSpec extends Specification { 12 | static final String TEST_USER_NAME = 'me' 13 | static final String DATE_TEST_RAN = 'today' 14 | 15 | KnowsWhenAndWhoRanTest mockKnowsWhenAndWhoRanTest() { 16 | def mockWhenAndWho = Mock( KnowsWhenAndWhoRanTest ) 17 | mockWhenAndWho.whenAndWhoRanTest( _ ) >> "Created on $DATE_TEST_RAN by $TEST_USER_NAME" 18 | return mockWhenAndWho 19 | } 20 | 21 | String replacePlaceholdersInRawHtml( String rawHtml, Map binding ) { 22 | def templateEngine = new SimpleTemplateEngine() 23 | templateEngine.createTemplate( rawHtml ).make( binding ).toString() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/SpecIncludingExtraInfo.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import org.spockframework.runtime.model.parallel.ExecutionMode 4 | import spock.lang.Execution 5 | import spock.lang.Rollup 6 | import spock.lang.Specification 7 | import spock.lang.Unroll 8 | 9 | @Execution( ExecutionMode.CONCURRENT ) 10 | class SpecIncludingExtraInfo extends Specification { 11 | 12 | def setupSpec() { 13 | reportHeader '
Report environment: currentOS
' 14 | } 15 | 16 | @Execution( ExecutionMode.CONCURRENT ) 17 | def "Simple feature adding a list to the report"() { 18 | when: 'The report adds something to the report' 19 | sleep 5 20 | reportInfo( [ 1, 2, 3 ] ) 21 | 22 | then: 'Show it in the report' 23 | } 24 | 25 | @Execution( ExecutionMode.CONCURRENT ) 26 | def "Feature adding several items to the report"() { 27 | given: 'Some info is added to the report' 28 | reportInfo( 'Hello world' ) 29 | 30 | when: 'More info is added' 31 | sleep 10 32 | reportInfo( 'More information here' ) 33 | reportInfo( 'Even more now' ) 34 | 35 | then: 'All of that info is shown' 36 | } 37 | 38 | @Execution( ExecutionMode.CONCURRENT ) 39 | @Rollup 40 | def "Non-unrolled example-based feature adding info"() { 41 | when: 'Info is added on each iteration' 42 | sleep 5 43 | reportInfo( "The current iteration is $iteration (not-unrolled)" ) 44 | 45 | then: 'It works' 46 | 47 | where: 48 | iteration << [ 0, 1, 2 ] 49 | } 50 | 51 | @Execution( ExecutionMode.CONCURRENT ) 52 | @Unroll 53 | def "Unrolled example-based feature adding info"() { 54 | when: 'Info is added on each iteration' 55 | sleep 10 56 | reportInfo( "The current iteration is $iteration (unrolled)" ) 57 | 58 | then: 'It works' 59 | 60 | where: 61 | iteration << [ 0, 1, 2 ] 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/SpockReportExtensionSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import com.athaydes.spockframework.report.internal.ConfigLoader 4 | import com.athaydes.spockframework.report.internal.HtmlReportCreator 5 | import com.athaydes.spockframework.report.internal.SpockReportsConfiguration 6 | import com.athaydes.spockframework.report.template.TemplateReportCreator 7 | import org.spockframework.runtime.model.SpecInfo 8 | import spock.lang.Specification 9 | import spock.lang.Unroll 10 | 11 | /** 12 | * 13 | * User: Renato 14 | */ 15 | class SpockReportExtensionSpec extends Specification { 16 | 17 | def "The settings found in the config_properties file are read only once"() { 18 | given: 19 | "An instance of SpockReportExtension with a mocked out config loader" 20 | def mockReportCreator = Mock IReportCreator 21 | def extension = new SpockReportExtension() { 22 | @Override 23 | IReportCreator instantiateReportCreator( String reportCreatorClassName ) { mockReportCreator } 24 | } 25 | extension.configLoader = Mock ConfigLoader 26 | 27 | when: 28 | "Spock visits 10 spec" 29 | 10.times { 30 | extension.start() 31 | extension.visitSpec( Mock( SpecInfo ) ) 32 | extension.stop() 33 | } 34 | 35 | then: 36 | "The config is read once" 37 | 1 * extension.configLoader.loadConfig( _ ) >> new Properties() 38 | } 39 | 40 | def "The settings found in the config_properties file are used to configure the report framework"() { 41 | given: 42 | "A set of valid and invalid properties emulating the properties file" 43 | def className = 'MockReportCreator' 44 | 45 | def props = new Properties() 46 | props.setProperty( IReportCreator.class.name, className ) 47 | props.setProperty( className + '.customProp', 'customValue' ) 48 | props.setProperty( "com.athaydes.spockframework.report.outputDir", "the-output-dir" ) 49 | props.setProperty( "some.invalid.property", "invalid-value" ) 50 | props.setProperty( IReportCreator.name, HtmlReportCreator.name ) 51 | 52 | and: 53 | "A real ConfigLoader that uses the properties file" 54 | def configLoader = new ConfigLoader() { 55 | @Override 56 | Properties loadConfig( SpockReportsConfiguration config ) { props } 57 | } 58 | 59 | and: 60 | "A mock ReportCreator" 61 | def mockReportCreator = GroovyMock( IReportCreator ) 62 | mockReportCreator.getClass() >> [ name: className ] 63 | mockReportCreator.hasProperty( _ as String ) >> { String name -> 64 | name in [ 'customProp', 'outputDir' ] 65 | } 66 | 67 | and: 68 | "A mock Spec" 69 | def mockSpecInfo = Mock( SpecInfo ) 70 | 71 | and: 72 | "An instance of SpockReportExtension with mocked out config loader and reportCreator" 73 | def extension = new SpockReportExtension() { 74 | @Override 75 | IReportCreator instantiateReportCreator( String reportCreatorClassName ) { mockReportCreator } 76 | } 77 | extension.configLoader = configLoader 78 | 79 | when: 80 | "This extension framework is initiated by Spock visiting the mock spec" 81 | extension.start() 82 | extension.visitSpec( mockSpecInfo ) 83 | extension.stop() 84 | 85 | then: 86 | "This extension added a SpecInfoListener to Spock's SpecInfo" 87 | 1 * mockSpecInfo.addListener( _ as SpecInfoListener ) 88 | 89 | and: 90 | "The ReportCreator was configured with the valid properties" 91 | 1 * mockReportCreator.setOutputDir( "the-output-dir" ) 92 | 93 | // this property does not exist in the report type, hence cannot be set 94 | 0 * mockReportCreator.setCustomProp( "customValue" ) 95 | 96 | and: 97 | "The ReportCreator is done" 98 | 1 * mockReportCreator.done() 99 | } 100 | 101 | @Unroll( "More than one report creator can be specified in the properties when there are #description" ) 102 | def "More than one report creator can be specified in the properties"() { 103 | given: 104 | "Properties specifying two report creators in a comma separated property value" 105 | def props = new Properties() 106 | props.setProperty( IReportCreator.name, iReportCreatorPropertyValue ) 107 | 108 | and: 109 | "A real ConfigLoader that uses the properties file" 110 | def configLoader = new ConfigLoader() { 111 | @Override 112 | Properties loadConfig( SpockReportsConfiguration config ) { props } 113 | } 114 | 115 | and: 116 | "A couple of mock ReportCreators are created" 117 | def htmlReportCreator = Mock( HtmlReportCreator ) 118 | def templateReportCreator = Mock( TemplateReportCreator ) 119 | 120 | and: 121 | "A mock Spec" 122 | def mockSpecInfo = Mock( SpecInfo ) 123 | 124 | and: 125 | "An instance of SpockReportExtension with mocked out config loader and reportCreators" 126 | def extension = new SpockReportExtension() { 127 | @Override 128 | IReportCreator instantiateReportCreator( String reportCreatorClassName ) { 129 | switch ( reportCreatorClassName ) { 130 | case HtmlReportCreator.class.name: return htmlReportCreator 131 | case TemplateReportCreator.class.name: return templateReportCreator 132 | default: throw new IllegalArgumentException( "Unexpected creator requested" ) 133 | } 134 | } 135 | } 136 | extension.configLoader = configLoader 137 | 138 | when: 139 | "This extension framework is initiated by Spock visiting the mock spec" 140 | extension.start() 141 | extension.visitSpec( mockSpecInfo ) 142 | extension.stop() 143 | 144 | then: 145 | "This extension added a SpecInfoListener to Spock's SpecInfo" 146 | 1 * mockSpecInfo.addListener( _ as SpecInfoListener ) 147 | 148 | and: 149 | "The ReportCreators are both called" 150 | 1 * htmlReportCreator.done() 151 | 1 * templateReportCreator.done() 152 | 153 | where: 154 | iReportCreatorPropertyValue | description 155 | "${HtmlReportCreator.name},${TemplateReportCreator.name}" | "no spaces" 156 | " ${HtmlReportCreator.name},${TemplateReportCreator.name}" | "a space at the start" 157 | "${HtmlReportCreator.name},${TemplateReportCreator.name} " | "a space at the end" 158 | "${HtmlReportCreator.name} ,${TemplateReportCreator.name}" | "a space in the middle" 159 | " ${HtmlReportCreator.name} ,${TemplateReportCreator.name} " | "some spaces" 160 | " ${HtmlReportCreator.name},${TemplateReportCreator.name}" | "some tabs" 161 | " ${HtmlReportCreator.name} , ${TemplateReportCreator.name} " | "lots of spaces and tabs" 162 | 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/UnrolledSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | @Unroll 7 | class UnrolledSpec extends Specification { 8 | 9 | def "Not exampled-based feature"() { 10 | expect: '2 and 2 is 4' 11 | 2 + 2 == 4 12 | } 13 | 14 | def "Example-based feature"() { 15 | expect: '#a + #b == #c' 16 | a + b == c 17 | 18 | where: 19 | a | b | c 20 | 2 | 2 | 4 21 | 1 | 4 | 5 22 | } 23 | 24 | def "Second Example-based feature"() { 25 | expect: '#a and #b is equal to #c' 26 | a + b == c 27 | 28 | where: 'a=#a, b=#b, c=#c' 29 | a | b | c 30 | 1 | 2 | 3 31 | 5 | 3 | 8 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/VividFakeTest.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report 2 | 3 | import spock.lang.* 4 | 5 | @Narrative( """ 6 | As a developer 7 | I want to see my code""" ) 8 | class VividFakeTest extends Specification { 9 | 10 | @Rollup 11 | def "A first test with Then code block"() { 12 | given: 13 | "we have x and y" 14 | 15 | and: 16 | "some more things" 17 | 18 | when: 19 | "I do crazy things" 20 | 21 | then: 22 | verifyAll { 23 | x == y 24 | y == x 25 | } 26 | 27 | where: 28 | "The examples below are used" 29 | x | y 30 | 'a' | 'a' 31 | 'b' | 'c' 32 | } 33 | 34 | def "Another feature without code"() { 35 | setup: 36 | "Setup block here" 37 | expect: 38 | "Expecting something ??" 39 | } 40 | 41 | def "Another feature with method call"() { 42 | expect: 43 | add( 1, 2 ) == 3 44 | } 45 | 46 | private static int add( int a, int b ) { 47 | return a + b 48 | } 49 | 50 | @Issue( [ "http://myhost.com/issues/995", "https://myhost.com/issues/973" ] ) 51 | def "A test with an error"() { 52 | when: 53 | "An Exception is thrown" 54 | throw new RuntimeException( 'As expected' ) 55 | 56 | then: 57 | "Will never succeed" 58 | } 59 | 60 | @See( [ "http://myhost.com/features/feature-234" ] ) 61 | def "A test with a failure"() { 62 | when: 63 | "Do nothing" 64 | then: 65 | "Test fails" 66 | assert 3 == 2 67 | } 68 | 69 | def "A Spec without block Strings"() { 70 | given: 71 | int a = 0 72 | 73 | and: 74 | int b = 1 75 | int c = 2 76 | int d = b + c 77 | 78 | when: 79 | int e = a + b + c + d 80 | 81 | then: 82 | e == 6 83 | a == 0 84 | c == 2 * b 85 | 86 | and: 87 | c > 0 88 | } 89 | 90 | @Unroll 91 | def "An @Unrolled spec with x=#x and y=#y"() { 92 | setup: 93 | "nothing" 94 | expect: 95 | x == 0 96 | and: 97 | "An error if y is 5" 98 | if ( y == 5 ) { 99 | throw new RuntimeException( 'y is 5' ) 100 | } 101 | 102 | where: 103 | x | y 104 | 0 | 1 105 | 2 | 3 106 | 0 | 5 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/engine/CanRunSpockSpecs.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.engine 2 | 3 | import org.junit.platform.engine.DiscoverySelector 4 | import org.junit.platform.testkit.engine.EngineExecutionResults 5 | import org.junit.platform.testkit.engine.EngineTestKit 6 | import org.junit.platform.testkit.engine.EventStatistics 7 | import spock.lang.Specification 8 | 9 | import java.util.function.Consumer 10 | 11 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass 12 | 13 | trait CanRunSpockSpecs { 14 | 15 | void runSpec( Class specClass, Consumer assertions = null ) { 16 | if ( assertions ) execute( selectClass( specClass ), assertions ) 17 | else execute( selectClass( specClass ) ) 18 | } 19 | 20 | private void execute( DiscoverySelector selector, Consumer statisticsConsumer ) { 21 | execute( selector ) 22 | .testEvents() 23 | .debug() 24 | .assertStatistics( statisticsConsumer ) 25 | } 26 | 27 | private EngineExecutionResults execute( DiscoverySelector selector ) { 28 | return EngineTestKit 29 | .engine( "spock" ) 30 | .selectors( selector ) 31 | .execute() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/hierarchy_tests/ChildSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.hierarchy_tests 2 | 3 | class ChildSpec extends ParentSpec { 4 | def 'child feature'() { 5 | expect: 6 | true 7 | } 8 | 9 | def 'override'() { 10 | expect: 11 | true 12 | } 13 | 14 | def 'examples'() { 15 | expect: 16 | true 17 | 18 | where: 19 | a << (1..10) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/hierarchy_tests/ParentSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.hierarchy_tests 2 | 3 | import spock.lang.Specification 4 | 5 | class ParentSpec extends Specification { 6 | def "super feature"() { 7 | expect: 8 | true 9 | } 10 | 11 | def 'override'() { 12 | expect: 13 | true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/ConfigLoaderSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import com.athaydes.spockframework.report.IReportCreator 4 | import groovy.transform.CompileStatic 5 | import groovy.transform.ToString 6 | import spock.lang.Specification 7 | import spock.lang.Unroll 8 | 9 | /** 10 | * 11 | * User: Renato 12 | */ 13 | class ConfigLoaderSpec extends Specification { 14 | 15 | private static final String FEATURE_REPORT_CSS = HtmlReportCreator.class.name + '.featureReportCss' 16 | 17 | static final String PROP_HIDE_EMPTY_BLOCKS = 'com.athaydes.spockframework.report.hideEmptyBlocks' 18 | static final String PROP_ENABLE_VIVID_REPORTS = 'com.athaydes.spockframework.report.showCodeBlocks' 19 | 20 | def "The ConfigLoader should load the default configurations"() { 21 | given: 22 | "A ConfigLoader without any custom configuration" 23 | def configLoader = new ConfigLoader() 24 | 25 | and: 26 | "The configLocation exists" 27 | ( ConfigLoader.CUSTOM_CONFIG as File ).exists() 28 | 29 | when: 30 | "I ask the ConfigLoader to load the configuration" 31 | def result = configLoader.loadConfig() 32 | 33 | then: 34 | "The ConfigLoader to find all of the properties declared in the configLocation" 35 | result.getProperty( FEATURE_REPORT_CSS ) == 'spock-feature-report.css' 36 | result.getProperty( 'com.athaydes.spockframework.report.hideEmptyBlocks' ) == 'false' 37 | } 38 | 39 | def "Custom configurations should override default configurations"() { 40 | given: 41 | "A ConfigLoader in an environment where there is a custom config file" 42 | def configLoader = new ConfigLoader() 43 | File customFile = createFileUnderMetaInf( IReportCreator.class.name + '.properties' ) 44 | customFile.write "${FEATURE_REPORT_CSS}=${expected}" 45 | 46 | and: 47 | "The configLocation exists" 48 | assert customFile.exists() 49 | 50 | when: 51 | "I ask the ConfigLoader to load the configuration" 52 | def result = configLoader.loadConfig() 53 | 54 | then: 55 | "The ConfigLoader to find all of the properties declared in the configLocation" 56 | result.getProperty( FEATURE_REPORT_CSS ) == expected 57 | 58 | and: 59 | "The default properties are also kept" 60 | result.getProperty( 'com.athaydes.spockframework.report.hideEmptyBlocks' ) == 'false' 61 | result.getProperty( 'com.athaydes.spockframework.report.outputDir' ) == 'build/spock-reports' 62 | 63 | cleanup: 64 | assert customFile.delete() 65 | 66 | where: 67 | expected << [ 'example/report.css' ] 68 | } 69 | 70 | def 'System properties should override props files and Groovy Spock config'() { 71 | given: 72 | "A ConfigLoader without any custom configuration" 73 | def configLoader = new ConfigLoader() 74 | 75 | and: 76 | "The configLocation exists" 77 | ( ConfigLoader.CUSTOM_CONFIG as File ).exists() 78 | 79 | and: 80 | "I have specified a few system property overrides" 81 | def originalHideEmptyBlocksProp = System.properties[ PROP_HIDE_EMPTY_BLOCKS ] 82 | System.properties[ PROP_HIDE_EMPTY_BLOCKS ] = hideEmptyBlocksProp 83 | 84 | def originalEnableVividReportsProp = System.properties[ PROP_ENABLE_VIVID_REPORTS ] 85 | System.properties[ PROP_ENABLE_VIVID_REPORTS ] = enableVividReportsProp.toString() 86 | 87 | and: 88 | "There is a Groovy Spock config file specifying some properties" 89 | def groovyConfig = new SpockReportsConfiguration( properties: [ 90 | ( PROP_ENABLE_VIVID_REPORTS ): !enableVividReportsProp, 91 | 'extra.prop' : 'another property' 92 | ] as Map ) 93 | 94 | when: 95 | "I ask the ConfigLoader to load the configuration" 96 | def result = configLoader.loadConfig( groovyConfig ) 97 | 98 | then: 99 | "The ConfigLoader must use the value from the system property overrides" 100 | result.getProperty( PROP_HIDE_EMPTY_BLOCKS ) == hideEmptyBlocksProp 101 | result.getProperty( PROP_ENABLE_VIVID_REPORTS ) == enableVividReportsProp.toString() 102 | 103 | and: 104 | "The extra property added by the Groovy config is found" 105 | result.getProperty( 'extra.prop' ) == 'another property' 106 | 107 | cleanup: 108 | if ( originalHideEmptyBlocksProp ) 109 | System.properties[ PROP_HIDE_EMPTY_BLOCKS ] = originalHideEmptyBlocksProp 110 | else 111 | System.properties.remove PROP_HIDE_EMPTY_BLOCKS 112 | if ( originalEnableVividReportsProp ) 113 | System.properties[ PROP_ENABLE_VIVID_REPORTS ] = originalEnableVividReportsProp 114 | else 115 | System.properties.remove PROP_ENABLE_VIVID_REPORTS 116 | 117 | where: 118 | hideEmptyBlocksProp | enableVividReportsProp 119 | 'custom_value' | true 120 | } 121 | 122 | @Unroll 123 | def "ConfigLoader can apply properties with the correct types"() { 124 | given: 125 | "A ConfigLoader without any custom configuration" 126 | def configLoader = new ConfigLoader() 127 | 128 | and: 129 | 'Properties of several different types' 130 | def properties = new Properties() 131 | ( propertiesMap + methodCalls ).each { k, v -> 132 | properties[ propertyPrefix + k ] = v.toString() // all properties come in Stringified 133 | } 134 | 135 | when: 'The ReportCreatorWithManyProperties instance is populated with the properties' 136 | def reporter = new ReportCreatorWithManyProperties() 137 | configLoader.apply( reporter, properties ) 138 | 139 | then: 'All properties of the report creator are set as expected' 140 | reporter.cool == propertiesMap.cool 141 | reporter.name == propertiesMap.name 142 | reporter.count == propertiesMap.count 143 | 144 | //noinspection GrEqualsBetweenInconvertibleTypes 145 | reporter.percentage == propertiesMap.percentage 146 | 147 | and: 'All method setters are called as expected' 148 | reporter.methodCalls == methodCalls 149 | 150 | where: 151 | propertiesMap << [ 152 | [ cool: true, name: 'nice', count: 4, percentage: 0.33 ], 153 | [ cool: false, name: 'boo', count: -2, percentage: -0.1 ], 154 | [ cool: false, name: 'bar', count: 2, percentage: 0.5, aggregatedJsonReportDir: 'json_dir' ], 155 | [ cool: true, name: 'nice', count: 4, percentage: 0.33 ], 156 | [ cool: false, name: 'boo', count: -2, percentage: -0.1 ], 157 | ] 158 | 159 | propertyPrefix << [ 160 | IReportCreator.package.name + '.', 161 | IReportCreator.package.name + '.', 162 | IReportCreator.package.name + '.', 163 | ReportCreatorWithManyProperties.name + '.', 164 | ReportCreatorWithManyProperties.name + '.', 165 | ] 166 | 167 | methodCalls << [ 168 | [ : ], 169 | [ outputDir: 'hi', projectName: 'hello', projectVersion: '1.0' ], 170 | [ outputDir: 'hi', projectName: 'hello', projectVersion: '1.0', aggregatedJsonReportDir: 'json_dir' ], 171 | [ hideEmptyBlocks: true, showCodeBlocks: true ], 172 | [ showCodeBlocks: false ], 173 | ] 174 | 175 | } 176 | 177 | private createFileUnderMetaInf( String fileName ) { 178 | def globalExtConfig = this.class.getResource( '/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension' ) 179 | def f = new File( globalExtConfig.toURI() ) 180 | new File( f.parentFile, fileName ) 181 | } 182 | 183 | } 184 | 185 | @CompileStatic 186 | @ToString 187 | class ReportCreatorWithManyProperties implements IReportCreator { 188 | 189 | boolean cool 190 | String name 191 | int count 192 | double percentage 193 | 194 | final Map methodCalls = [ : ] 195 | 196 | @Override 197 | void createReportFor( SpecData data ) {} 198 | 199 | @Override 200 | void setOutputDir( String path ) { 201 | methodCalls << [ 'outputDir': path ] 202 | } 203 | 204 | @Override 205 | void setAggregatedJsonReportDir( String path ) { 206 | methodCalls << [ 'aggregatedJsonReportDir': path ] 207 | } 208 | 209 | @Override 210 | void setHideEmptyBlocks( boolean hide ) { 211 | methodCalls << [ 'hideEmptyBlocks': hide ] 212 | } 213 | 214 | @Override 215 | void setShowCodeBlocks( boolean show ) { 216 | methodCalls << [ 'showCodeBlocks': show ] 217 | } 218 | 219 | @Override 220 | void setTestSourceRoots( roots ) { 221 | methodCalls << [ 'testSourceRoots': roots ] 222 | } 223 | 224 | @Override 225 | void setProjectName( String projectName ) { 226 | methodCalls << [ 'projectName': projectName ] 227 | } 228 | 229 | @Override 230 | void setProjectVersion( String projectVersion ) { 231 | methodCalls << [ 'projectVersion': projectVersion ] 232 | } 233 | 234 | @Override 235 | void done() {} 236 | } -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/ProblemBlockWriterSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Subject 5 | 6 | class ProblemBlockWriterSpec extends Specification { 7 | 8 | @Subject 9 | ProblemBlockWriter problemBlockWriter = new ProblemBlockWriter() 10 | 11 | def "Can print any kind of error in report when not printing stacktrace"() { 12 | given: 'A ProblemBlockWriter that does not print stacktraces' 13 | problemBlockWriter.printThrowableStackTrace = false 14 | 15 | when: 'Some error is formatted' 16 | def result = problemBlockWriter.formatProblemMessage error 17 | 18 | then: 'The result is as expected' 19 | result == expectedString 20 | 21 | where: 22 | error | expectedString 23 | '' | '' 24 | 'an error' | 'an error' 25 | 10 | '10' 26 | null | 'null' 27 | throwAndGet( new Exception( 'hi' ) ) | new Exception( 'hi' ).toString() 28 | } 29 | 30 | def "Can print any kind of error in report when printing stacktrace"() { 31 | given: 'A ProblemBlockWriter that prints stacktraces' 32 | problemBlockWriter.printThrowableStackTrace = true 33 | 34 | when: 'Some error that is NOT a Throwable is formatted' 35 | def result = problemBlockWriter.formatProblemMessage error 36 | 37 | then: 'The result is as expected' 38 | result == expectedString 39 | 40 | where: 41 | error | expectedString 42 | '' | '' 43 | 'an error' | 'an error' 44 | 10 | '10' 45 | null | 'null' 46 | } 47 | 48 | 49 | def "Can print stacktrace in report"() { 50 | given: 'An Throwable with a real stacktrace' 51 | def someThrowable = throwAndGet new RuntimeException( "An error" ) 52 | 53 | and: 'A ProblemBlockWriter that prints stacktraces' 54 | problemBlockWriter.printThrowableStackTrace = true 55 | 56 | when: 'The Throwable is printed using ProblemBlockWriter' 57 | def result = problemBlockWriter.formatProblemMessage(someThrowable) 58 | 59 | then: 'The given String contains the stack-trace of the Throwable' 60 | result.startsWith 'java.lang.RuntimeException: An error' 61 | result.readLines().size() > 5 62 | result.readLines().drop( 1 )*.trim().every { it.startsWith( 'at ' ) } 63 | result.contains( "at ${this.class.name}" ) 64 | } 65 | 66 | static Throwable throwAndGet( Throwable throwable ) { 67 | try { 68 | throw throwable 69 | } catch ( Throwable t ) { 70 | return t 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/SimulatedReportWriter.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import java.util.concurrent.CountDownLatch 6 | import java.util.concurrent.TimeUnit 7 | 8 | @CompileStatic 9 | class SimulatedReportWriter { 10 | 11 | static void main( String[] args ) { 12 | assert args.size() == 2, "Expected 2 args (name of file to write to, number of threads) but got $args" 13 | final file = new File( args.first() ) 14 | final threadCount = args.last().toInteger() 15 | assert write( file, threadCount ).await( 10, TimeUnit.SECONDS ), "Did not finish writing within timeout" 16 | } 17 | 18 | static CountDownLatch write( File file, int threadCount ) { 19 | final counter = new CountDownLatch( threadCount ) 20 | 21 | ( 1..threadCount ).each { n -> 22 | sleep 50 // pause quickly to make sure the separate JVMs Threads interleave 23 | Thread.start { 24 | final raf = new RandomAccessFile( file, 'rw' ) 25 | ReportDataAggregator.withFileLock( raf ) { 26 | def contents = ReportDataAggregator.readTextFrom( raf ) 27 | def numbers = contents.split( /\s/ ) 28 | def newNumber = numbers.last().toInteger() + 1 29 | raf.write( " $newNumber".bytes ) 30 | } 31 | counter.countDown() 32 | } 33 | } 34 | 35 | return counter 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/StringFormatHelperSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import spock.lang.Specification 4 | 5 | import static com.athaydes.spockframework.report.internal.StringFormatHelper.ds 6 | 7 | /** 8 | * 9 | * User: Renato 10 | */ 11 | class StringFormatHelperSpec extends Specification { 12 | 13 | def "Percentage values should be adequate for a Test Report"() { 14 | given: 15 | "Doubles representing rate of success for a test" 16 | 17 | when: 18 | "Formatting the Doubles to show them in a report" 19 | def result = new StringFormatHelper().toPercentage( input ) 20 | then: 21 | "The values should look as in the examples, with decimal separator localized" 22 | result == expected 23 | 24 | where: 25 | input | expected 26 | 0 | "0${ds}0%" 27 | 0.1 | "10${ds}0%" 28 | 0.25 | "25${ds}0%" 29 | 0.5 | "50${ds}0%" 30 | 3.0 / 4.0 | "75${ds}0%" 31 | 1.0 / 3.0 | "33${ds}33%" 32 | 1.0 | "100${ds}0%" 33 | } 34 | 35 | def "Time amounts should look adequate for Test Reports"() { 36 | given: 37 | "The example evaluates to a number" 38 | def example = Eval.me timeDurationInMillis 39 | 40 | when: 41 | "I Convert the time duration to a presentable String" 42 | def result = new StringFormatHelper().toTimeDuration( example ) 43 | 44 | then: 45 | "The result is as expected" 46 | result == expected 47 | 48 | where: 49 | timeDurationInMillis | expected 50 | "0" | "0" 51 | "1" | "0${ds}001 seconds" 52 | "250" | "0${ds}250 seconds" 53 | "1000" | "1${ds}000 seconds" 54 | """2 + //ms 55 | 4 * 1000 + // sec 56 | 5 * 1000 * 60 + // min 57 | 8 * 1000 * 60 * 60 // hour""" | "8 hours, 5 minutes, 4${ds}002 seconds" 58 | } 59 | 60 | def "A formatted String should be converted nicely to an equivalent HTML String"() { 61 | when: 62 | "An a formatted String is converted to an HTML String" 63 | def result = new StringFormatHelper().formatToHtml( formattedString ) 64 | 65 | then: 66 | "The result is as expected" 67 | result == expected 68 | 69 | where: 70 | formattedString | expected 71 | '' | '' 72 | 'abc' | 'abc' 73 | 'Hi\tthere' | 'Hi
there' 74 | 'Hi\nHo' | 'Hi
Ho' 75 | } 76 | 77 | def "A date should look as specified in the examples when shown in a report"() { 78 | given: 79 | "The dateParams in the examples are converted to a Gregorian Calendar Date" 80 | def date = new GregorianCalendar( *Eval.me( dateParams ) ).time 81 | 82 | when: 83 | "A Date is converted to a String" 84 | def result = new StringFormatHelper().toDateString( date ) 85 | 86 | then: 87 | "The result is as in the examples" 88 | result.split( ' ' ).toList().containsAll( expectedStringsInResult ) 89 | 90 | where: 91 | dateParams | expectedStringsInResult 92 | '[ 1995, Calendar.SEPTEMBER, 5, 19, 35, 30 ]' | [ 'Tue', 'Sep', '05', '19:35:30', '1995' ] 93 | '[ 2013, Calendar.JANUARY, 31, 0, 0, 0 ]' | [ 'Thu', 'Jan', '31', '00:00:00', '2013' ] 94 | } 95 | 96 | def "Escapes XML in Strings"() { 97 | when: 98 | "Escaping XML in a String" 99 | def result = new StringFormatHelper().escapeXml( input ) 100 | 101 | then: 102 | "The resulting String has encoded XML characters" 103 | result == expected 104 | 105 | where: 106 | input | expected 107 | '' | '' 108 | 'a' | 'a' 109 | 'Hello world' | 'Hello world' 110 | '123' | '123' 111 | '' | '<hi>' 112 | 'Hello world!' | 'Hello <em>world</em>!' 113 | '"Great"' | '"Great"' 114 | 'You&"Me"' | 'You&"Me"' 115 | "The 'good' people" | 'The 'good' people' 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/StringTemplateProcessorSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import org.spockframework.runtime.model.FeatureInfo 4 | import org.spockframework.runtime.model.IterationInfo 5 | import spock.lang.Specification 6 | 7 | class StringTemplateProcessorSpec extends Specification { 8 | 9 | def "Should replace #variableNames with the provided values"() { 10 | given: 11 | "A StringTemplateProcessor" 12 | def processor = new StringTemplateProcessor() 13 | 14 | when: 15 | "A String is processed" 16 | def feature = new FeatureInfo( name: string ) 17 | variableNames.each{ value -> feature.addDataVariable( value?.toString() ) } 18 | def iteration = new IterationInfo( feature, 0, variableValues as Object[], variableValues.size() ) 19 | def result = processor.process( string, variableNames, iteration ) 20 | 21 | then: 22 | "All #variables are replaced with the respective value" 23 | result == expectedString 24 | 25 | where: 26 | string | variableNames | variableValues || expectedString 27 | '' | [ ] | [ ] || '' 28 | '' | [ 'a' ] | [ 0 ] || '' 29 | 'spec 1' | [ ] | [ ] || 'spec 1' 30 | 'spec a' | [ 'a' ] | [ 1 ] || 'spec a' 31 | 'A nice Spec' | [ 'nice', 'Spec' ] | [ 'x', 'y' ] || 'A nice Spec' 32 | 'Value #x == #y' | [ 'x', 'y' ] | [ 1, 2 ] || 'Value 1 == 2' 33 | 'Value #x == #y' | [ 'x' ] | [ 4 ] || 'Value 4 == #Error:y' 34 | 'Value #x == #y' | [ 'z' ] | [ 2 ] || 'Value #Error:x == #Error:y' 35 | 'Value #x1 == #y' | [ 'x', 'y' ] | [ 0, 1 ] || 'Value #Error:x1 == 1' 36 | 'Value #x==#y' | [ 'x', 'y' ] | [ 0, 1 ] || 'Value 0==1' 37 | 'Value #x,#y,#z x, y, z' | [ 'x', 'y', 'z' ] | [ 0, 1, 2 ] || 'Value 0,1,2 x, y, z' 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/groovy/com/athaydes/spockframework/report/internal/TestHelper.groovy: -------------------------------------------------------------------------------- 1 | package com.athaydes.spockframework.report.internal 2 | 3 | import groovy.transform.CompileStatic 4 | import spock.lang.Narrative 5 | import spock.lang.Title 6 | 7 | @Title( "spock-reports TestHelper specification" ) 8 | @Narrative( "This specification ensures the helper works" ) 9 | final class TestHelper { 10 | 11 | static String minify( String xml ) { 12 | xml.replaceAll( /[\t\r\n]\s*/, '' ).replaceAll( />\s+<' ) 13 | } 14 | 15 | static void assertVerySimilar( String actualText, String expectedText ) { 16 | def actualLines = partition( actualText ).iterator() 17 | def expectedLines = partition( expectedText ).iterator() 18 | def currentLine = 1 19 | while ( actualLines.hasNext() ) { 20 | def actual = actualLines.next().trim() 21 | if ( expectedLines.hasNext() ) { 22 | def expected = expectedLines.next().trim() 23 | assert actual == expected: "At index ${actualLines.index}.\n" + 24 | " Expected: $expected\n" + 25 | " Actual : $actual\n" 26 | } else { 27 | assert false: "At index ${actualLines.index}.\n" + 28 | " Expected: \n" + 29 | " Actual : $actual\n" 30 | } 31 | currentLine++ 32 | } 33 | if ( expectedLines.hasNext() ) { 34 | def expected = expectedLines.next().trim() 35 | assert false: "At index ${expectedText.index}.\n" + 36 | " Expected: $expected\n" + 37 | " Actual : \n" 38 | } 39 | } 40 | 41 | def "minimizeXml() Spec"() { 42 | expect: 43 | minify( normalXml ) == result 44 | 45 | where: 46 | normalXml | result 47 | '' | '' 48 | '\t' | '' 49 | '\r' | '' 50 | '\n' | '' 51 | '\t\r\n' | '' 52 | '\n\r\r\n\r\n\r\n' | '' 53 | 'Hi' | 'Hi' 54 | ' ' | '' 55 | ' ' | ' ' 56 | '\t\t\t\t\t\n\r' | '' 57 | '\n\r \n\r \n\r ' | '' 58 | } 59 | 60 | private static Iterable partition( String text ) { 61 | return new Iterable() { 62 | static final int PARTITION_SIZE = 100 63 | 64 | @Override 65 | Iterator iterator() { 66 | return new Iterator() { 67 | int index = 0 68 | 69 | @CompileStatic 70 | @Override 71 | boolean hasNext() { 72 | return index < text.length() 73 | } 74 | 75 | @CompileStatic 76 | @Override 77 | String next() { 78 | def partition = text[ index.. 15 | As a user 16 | I want foo 17 | So that bar 18 | 19 | 20 | ## Features 21 | 22 | ### A first test 23 | 24 | Result: **FAIL** 25 | Time: Unknown 26 | 27 | * Given: we have x and y 28 | 29 | * And: some more things 30 | 31 | * When: I do crazy things 32 | 33 | * Then: I get one iteration pass and another fail 34 | 35 | * Where: The examples below are used 36 | 37 | | x | y | | 38 | |---|---|---------| 39 | | a | a | Unknown | (PASS) 40 | | b | c | Unknown | (FAIL) 41 | 42 | The following problems occurred: 43 | 44 | * [b, c] 45 | ``` 46 | Condition not satisfied: 47 | 48 | x == y 49 | | | | 50 | b | c 51 | false 52 | 1 difference (0% similarity) 53 | (b) 54 | (c) 55 | 56 | ``` 57 | 58 | ### Another feature!!!! 59 | 60 | Result: **PASS** 61 | Time: Unknown 62 | 63 | * Given: Setup block here 64 | 65 | * Expect: Expecting something ?? 66 | 67 | ### A when then spec 68 | 69 | Result: **PASS** 70 | Time: Unknown 71 | 72 | * When: This is the when 73 | 74 | * Then: This is the then 75 | 76 | ### Please ignore me 77 | 78 | Result: **IGNORED** 79 | Time: Unknown 80 | 81 | * Given: Nothing 82 | 83 | * When: Do nothing 84 | 85 | * Then: Nothing happens 86 | 87 | ### A test with an error 88 | 89 | #### Issues: 90 | 91 | * http://myhost.com/issues/995 92 | * https://myhost.com/issues/973 93 | 94 | Result: **ERROR** 95 | Time: Unknown 96 | 97 | * When: An Exception is thrown 98 | 99 | * Then: Will never succeed 100 | 101 | The following problems occurred: 102 | 103 | ``` 104 | java.lang.RuntimeException: As expected 105 | ``` 106 | 107 | ### A test with a failure 108 | 109 | #### See: 110 | 111 | * http://myhost.com/features/feature-234 112 | 113 | Result: **FAIL** 114 | Time: Unknown 115 | 116 | * When: Do nothing 117 | 118 | * Then: Test fails 119 | 120 | The following problems occurred: 121 | 122 | ``` 123 | Condition not satisfied: 124 | 125 | 3 == 2 126 | | 127 | false 128 | 129 | ``` 130 | ``` 131 | Condition not satisfied: 132 | 133 | 5 == 1 134 | | 135 | false 136 | 137 | ``` 138 | 139 | ### An incredibly long feature description that unfortunately will popup in some cases where business 140 | analysts write these too detailed overviews of what the test should be all about when what they really 141 | should do is to let the details go in the body of the test using the Gherkin language which underlies BDD 142 | and is proven to make it easier for all involved to understand what the test is doing, what the inputs are 143 | and what the expected outcomes are in such a way that the best possible common understanding is reached 144 | 145 | Result: **PASS** 146 | Time: Unknown 147 | 148 | * Expect: The long description above to look good in the report 149 | 150 | ### A Spec with empty block Strings 151 | 152 | Result: **PASS** 153 | Time: Unknown 154 | 155 | * Given: ---- 156 | 157 | * When: ---- 158 | 159 | * Then: ---- 160 | 161 | ### An @Unrolled spec with x=0 and y=1 [0] 162 | 163 | Result: **PASS** 164 | Time: Unknown 165 | 166 | * Given: nothing 167 | 168 | * Expect: 0 to be 0 169 | 170 | * And: An error if y is 5 171 | 172 | * Where: ---- 173 | 174 | | x | y | | 175 | |---|---|---------| 176 | | 0 | 1 | Unknown | (PASS) 177 | 178 | ### An @Unrolled spec with x=2 and y=3 [1] 179 | 180 | Result: **FAIL** 181 | Time: Unknown 182 | 183 | * Given: nothing 184 | 185 | * Expect: 2 to be 0 186 | 187 | * And: An error if y is 5 188 | 189 | * Where: ---- 190 | 191 | | x | y | | 192 | |---|---|---------| 193 | | 2 | 3 | Unknown | (FAIL) 194 | 195 | The following problems occurred: 196 | 197 | * [2, 3] 198 | ``` 199 | Condition not satisfied: 200 | 201 | x == 0 202 | | | 203 | 2 false 204 | 205 | ``` 206 | 207 | ### An @Unrolled spec with x=0 and y=5 [2] 208 | 209 | Result: **ERROR** 210 | Time: Unknown 211 | 212 | * Given: nothing 213 | 214 | * Expect: 0 to be 0 215 | 216 | * And: An error if y is 5 217 | 218 | * Where: ---- 219 | 220 | | x | y | | 221 | |---|---|---------| 222 | | 0 | 5 | Unknown | (FAIL) 223 | 224 | The following problems occurred: 225 | 226 | * [0, 5] 227 | ``` 228 | java.lang.RuntimeException: y is 5 229 | ``` 230 | 231 | ### Future feature 232 | 233 | > Pending Feature 234 | 235 | Result: **IGNORED** 236 | Time: Unknown 237 | 238 | * When: the feature is ready 239 | 240 | * Then: the annotation will be removed 241 | 242 | 243 | Generated by Athaydes Spock Reports -------------------------------------------------------------------------------- /src/test/resources/VividFakeTest.md: -------------------------------------------------------------------------------- 1 | # Report for com.athaydes.spockframework.report.VividFakeTest 2 | 3 | ##Summary 4 | 5 | * Total Runs: 9 6 | * Success Rate: 44${ds}44% 7 | * Failures: 3 8 | * Errors: 2 9 | * Skipped: 0 10 | * Total time: Unknown 11 | 12 |
 13 | As a developer
 14 | I want to see my code
 15 | 
16 | 17 | ## Features 18 | 19 | ### A first test with Then code block 20 | 21 | Result: **FAIL** 22 | Time: Unknown 23 | 24 | * Given: we have x and y 25 | 26 | * And: some more things 27 | 28 | * When: I do crazy things 29 | 30 | * Then: 31 | 32 | ``` 33 | verifyAll { 34 | x == y 35 | y == x 36 | } 37 | ``` 38 | 39 | * Where: The examples below are used 40 | 41 | | x | y | | 42 | |---|---|---------| 43 | | a | a | Unknown | (PASS) 44 | | b | c | Unknown | (FAIL) 45 | 46 | The following problems occurred: 47 | 48 | * [b, c] 49 | ``` 50 | Condition not satisfied: 51 | 52 | x == y 53 | | | | 54 | b | c 55 | false 56 | 1 difference (0% similarity) 57 | (b) 58 | (c) 59 | 60 | ``` 61 | ``` 62 | Condition not satisfied: 63 | 64 | y == x 65 | | | | 66 | c | b 67 | false 68 | 1 difference (0% similarity) 69 | (c) 70 | (b) 71 | 72 | ``` 73 | 74 | ### Another feature without code 75 | 76 | Result: **PASS** 77 | Time: Unknown 78 | 79 | * Given: Setup block here 80 | 81 | * Expect: Expecting something ?? 82 | 83 | ### Another feature with method call 84 | 85 | Result: **PASS** 86 | Time: Unknown 87 | 88 | * Expect: 89 | 90 | ``` 91 | add( 1, 2 ) == 3 92 | ``` 93 | 94 | ### A test with an error 95 | 96 | #### Issues: 97 | 98 | * http://myhost.com/issues/995 99 | * https://myhost.com/issues/973 100 | 101 | Result: **ERROR** 102 | Time: Unknown 103 | 104 | * When: An Exception is thrown 105 | 106 | ``` 107 | throw new RuntimeException( 'As expected' ) 108 | ``` 109 | 110 | * Then: Will never succeed 111 | 112 | The following problems occurred: 113 | 114 | ``` 115 | java.lang.RuntimeException: As expected 116 | ``` 117 | 118 | ### A test with a failure 119 | 120 | #### See: 121 | 122 | * http://myhost.com/features/feature-234 123 | 124 | Result: **FAIL** 125 | Time: Unknown 126 | 127 | * When: Do nothing 128 | 129 | * Then: Test fails 130 | 131 | ``` 132 | assert 3 == 2 133 | ``` 134 | 135 | The following problems occurred: 136 | 137 | ``` 138 | Condition not satisfied: 139 | 140 | 3 == 2 141 | | 142 | false 143 | 144 | ``` 145 | 146 | ### A Spec without block Strings 147 | 148 | Result: **PASS** 149 | Time: Unknown 150 | 151 | * Given: 152 | 153 | ``` 154 | int a = 0 155 | ``` 156 | 157 | * And: 158 | 159 | ``` 160 | int b = 1 161 | int c = 2 162 | int d = b + c 163 | ``` 164 | 165 | * When: 166 | 167 | ``` 168 | int e = a + b + c + d 169 | ``` 170 | 171 | * Then: 172 | 173 | ``` 174 | e == 6 175 | a == 0 176 | c == 2 * b 177 | ``` 178 | 179 | * And: 180 | 181 | ``` 182 | c > 0 183 | ``` 184 | 185 | ### An @Unrolled spec with x=0 and y=1 [0] 186 | 187 | Result: **PASS** 188 | Time: Unknown 189 | 190 | * Given: nothing 191 | 192 | * Expect: 193 | 194 | ``` 195 | x == 0 196 | ``` 197 | 198 | * And: An error if y is 5 199 | 200 | ``` 201 | if ( y == 5 ) { 202 | throw new RuntimeException( 'y is 5' ) 203 | } 204 | ``` 205 | 206 | * Where: 207 | 208 | | x | y | | 209 | |---|---|---------| 210 | | 0 | 1 | Unknown | (PASS) 211 | 212 | ### An @Unrolled spec with x=2 and y=3 [1] 213 | 214 | Result: **FAIL** 215 | Time: Unknown 216 | 217 | * Given: nothing 218 | 219 | * Expect: 220 | 221 | ``` 222 | x == 0 223 | ``` 224 | 225 | * And: An error if y is 5 226 | 227 | ``` 228 | if ( y == 5 ) { 229 | throw new RuntimeException( 'y is 5' ) 230 | } 231 | ``` 232 | 233 | * Where: 234 | 235 | | x | y | | 236 | |---|---|---------| 237 | | 2 | 3 | Unknown | (FAIL) 238 | 239 | The following problems occurred: 240 | 241 | * [2, 3] 242 | ``` 243 | Condition not satisfied: 244 | 245 | x == 0 246 | | | 247 | 2 false 248 | 249 | ``` 250 | 251 | ### An @Unrolled spec with x=0 and y=5 [2] 252 | 253 | Result: **ERROR** 254 | Time: Unknown 255 | 256 | * Given: nothing 257 | 258 | * Expect: 259 | 260 | ``` 261 | x == 0 262 | ``` 263 | 264 | * And: An error if y is 5 265 | 266 | ``` 267 | if ( y == 5 ) { 268 | throw new RuntimeException( 'y is 5' ) 269 | } 270 | ``` 271 | 272 | * Where: 273 | 274 | | x | y | | 275 | |---|---|---------| 276 | | 0 | 5 | Unknown | (FAIL) 277 | 278 | The following problems occurred: 279 | 280 | * [0, 5] 281 | ``` 282 | java.lang.RuntimeException: y is 5 283 | ``` 284 | 285 | 286 | Generated by Athaydes Spock Reports -------------------------------------------------------------------------------- /src/test/resources/com/athaydes/spockframework/report/internal/FullyIgnoredSpecReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 |

Report for ${classOnTest}

12 |
13 | 16 |
17 |

Summary:

18 |
Created on ${dateTestRan} by ${username}
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Executed featuresPassedFailuresErrorsSkippedSuccess rateTime
${executedFeatures}${passed}${failures}${errors}${skipped}${successRate}${time}
43 |
44 |

Features:

45 | 46 | 47 | 48 | 49 | 50 | 51 |
TOC
52 | 53 | 58 | 59 | 60 | 63 | 66 | 67 | 68 | 73 | 74 | 75 | 78 | 81 | 82 | 83 |
54 |
feature1 55 | Return 56 |
(Unknown)
57 |
61 |
Expect:
62 |
64 |
65 |
69 |
feature2 70 | Return 71 |
(Unknown)
72 |
76 |
Expect:
77 |
79 |
80 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /src/test/resources/com/athaydes/spockframework/report/internal/SingleTestSummaryReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Specification run results

8 |
9 | ${projectHeader} 10 |
11 |

Specifications summary:

12 |
Created on ${dateTestRan} by ${username}
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 |
TotalPassedFailedSkippedFt TotalFt PassedFt FailedFt SkippedSuccess rateTotal time
1010561225.0%1.0 second
43 |
44 |

Specifications:

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
NameFeaturesIterationsFailedErrorsSkippedSuccess rateTime
Spec15310225%1 sec
71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /src/test/resources/com/athaydes/spockframework/report/internal/TestSummaryReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 |

Specification run results

11 |
12 |
13 |

Specifications summary:

14 |
Created on ${dateTestRan} by ${username}
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 |
TotalPassedFailedSkippedFt TotalFt PassedFt FailedFt SkippedSuccess rateTotal time
${total}${passed}${failed}${skipped}${fTotal}${fPassed}${fFails + fErrors}${fSkipped}${successRate}${time}
45 |
46 |

Specifications:

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
NameFeaturesIterationsFailedErrorsSkippedSuccess rateTime
Spec143002${successRate1}${duration1}
a.Spec264013${successRate2}${duration2}
a.Spec375324${successRate3}${duration3}
a.b.c.Spec486435${successRate4}${duration4}
a.b.c.Spec598546${successRate5}${duration5}
b.c.Spec6107657${successRate6}${duration6}
c.d.Spec6109768${successRate7}${duration7}
133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /src/test/resources/com/athaydes/spockframework/report/internal/UnrolledSpecReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Report for com.athaydes.spockframework.report.UnrolledSpec

8 |
9 | 12 |
13 |

Summary:

14 |
Created on ${dateTestRan} by ${username}
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Executed featuresPassedFailuresErrorsSkippedSuccess rateTime
${executedFeatures}${passed}${failures}${errors}${skipped}${successRate}${time}
39 |
40 |

Features:

41 | 42 | 43 | 44 | 45 | 46 | 47 |
TOC
48 | 49 | 58 | 59 | 60 | 63 | 66 | 67 | 68 | 77 | 78 | 79 | 82 | 85 | 86 | 87 | 90 | 93 | 94 | 95 | 98 | 119 | 120 | 121 | 130 | 131 | 132 | 135 | 138 | 139 | 140 | 143 | 146 | 147 | 148 | 151 | 172 | 173 | 174 | 183 | 184 | 185 | 188 | 191 | 192 | 193 | 196 | 199 | 200 | 201 | 204 | 225 | 226 | 227 | 236 | 237 | 238 | 241 | 244 | 245 | 246 | 249 | 252 | 253 | 254 | 257 | 278 | 279 | 280 |
50 |
51 | Not exampled-based feature 52 | 53 | Return 54 |
(Unknown)
55 |
56 |
57 |
61 |
Expect:
62 |
64 |
2 and 2 is 4
65 |
69 |
70 | Example-based feature [0] 71 | 72 | Return 73 |
(Unknown)
74 |
75 |
76 |
80 |
Expect:
81 |
83 |
2 + 2 == 4
84 |
88 |
Where:
89 |
91 |
----
92 |
96 |
Examples:
97 |
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
abc
224OK(Unknown)
117 |
118 |
122 |
123 | Example-based feature [1] 124 | 125 | Return 126 |
(Unknown)
127 |
128 |
129 |
133 |
Expect:
134 |
136 |
1 + 4 == 5
137 |
141 |
Where:
142 |
144 |
----
145 |
149 |
Examples:
150 |
152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 |
abc
145OK(Unknown)
170 |
171 |
175 |
176 | Second Example-based feature [0] 177 | 178 | Return 179 |
(Unknown)
180 |
181 |
182 |
186 |
Expect:
187 |
189 |
1 and 2 is equal to 3
190 |
194 |
Where:
195 |
197 |
a=1, b=2, c=3
198 |
202 |
Examples:
203 |
205 |
206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
abc
123OK(Unknown)
223 |
224 |
228 |
229 | Second Example-based feature [1] 230 | 231 | Return 232 |
(Unknown)
233 |
234 |
235 |
239 |
Expect:
240 |
242 |
5 and 3 is equal to 8
243 |
247 |
Where:
248 |
250 |
a=5, b=3, c=8
251 |
255 |
Examples:
256 |
258 |
259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 |
abc
538OK(Unknown)
276 |
277 |
281 |
282 | 283 | 284 | --------------------------------------------------------------------------------