├── .VERSION_PREFIX ├── .dir-locals.el ├── bin ├── kaocha └── proj ├── .gitignore ├── bb.edn ├── .github └── workflows │ ├── add_to_project_board.yml │ └── main.yml ├── tests.edn ├── deps.edn ├── fixtures └── kaocha-demo │ └── demo │ └── test.clj ├── test └── unit │ └── kaocha │ └── plugin │ ├── junit_xml │ └── xml_test.clj │ └── junit_xml_test.clj ├── CHANGELOG.md ├── src └── kaocha │ └── plugin │ ├── junit_xml │ └── xml.clj │ └── junit_xml.clj ├── pom.xml ├── README.md ├── resources └── kaocha │ └── junit_xml │ └── JUnit.xsd └── LICENSE.txt /.VERSION_PREFIX: -------------------------------------------------------------------------------- 1 | 1.17 -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test")))) 2 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec clojure -A:test:dev -M -m kaocha.runner "$@" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .nrepl-port 3 | target 4 | repl 5 | scratch.clj 6 | .store 7 | .clj-kondo/.cache 8 | .lsp 9 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" 3 | :git/sha "e0e234aea52aeafac6ebb06c4a5149d83977e6a0" 4 | #_#_:local/root "../open-source"}}} 5 | -------------------------------------------------------------------------------- /.github/workflows/add_to_project_board.yml: -------------------------------------------------------------------------------- 1 | name: Add new pr or issue to project board 2 | 3 | on: [issues] 4 | 5 | jobs: 6 | add-to-project: 7 | uses: lambdaisland/open-source/.github/workflows/add-to-project-board.yml@main 8 | secrets: inherit 9 | 10 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:test-paths ["test/unit"]}] 3 | 4 | ;; This test suite is a bit strange as it invokes Kaocha within kaocha. For 5 | ;; some reason the color flag for the inner config isn't honored, working 6 | ;; around it by disabling color for the outer test as well. 7 | :color? false} 8 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {} 3 | 4 | :aliases 5 | {:dev 6 | {} 7 | 8 | :test 9 | {:extra-deps {lambdaisland/kaocha {:mvn/version "1.69.1069"} 10 | nubank/matcher-combinators {:mvn/version "3.5.0"} 11 | org.clojure/data.xml {:mvn/version "0.0.8"} 12 | lambdaisland/kaocha-cloverage {:mvn/version "1.0.75"}}}}} 13 | -------------------------------------------------------------------------------- /bin/proj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns proj (:require [lioss.main :as lioss])) 4 | 5 | (lioss/main 6 | {:license :epl 7 | :inception-year 2018 8 | :group-id "lambdaisland" 9 | :description "Code coverage plugin for Kaocha based on Cloverage" 10 | :aliases-as-optional-deps [:test]}) 11 | 12 | ;; Local Variables: 13 | ;; mode:clojure 14 | ;; End: 15 | -------------------------------------------------------------------------------- /fixtures/kaocha-demo/demo/test.clj: -------------------------------------------------------------------------------- 1 | (ns demo.test 2 | (:require [clojure.test :refer :all])) 3 | 4 | (deftest basic-test 5 | (is (= {:foo 1} {:foo 1}) "at least one that passes")) 6 | 7 | (deftest output-test 8 | (println "this is on stdout") 9 | 10 | (binding [*out* *err*] 11 | (println "this is on stderr")) 12 | 13 | (is (= {:foo 1} {:foo 2}) "oops")) 14 | 15 | (deftest exception-in-is-test 16 | (is 17 | (throw (Exception. "Inside assertion")))) 18 | 19 | (deftest exception-outside-is-test 20 | (throw (Exception. "outside assertion"))) 21 | 22 | (deftest ^:kaocha/skip skip-test 23 | (println "this test does not run.") 24 | (is false)) 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Delivery 2 | 3 | on: push 4 | 5 | jobs: 6 | Kaocha: 7 | runs-on: ${{matrix.sys.os}} 8 | 9 | strategy: 10 | matrix: 11 | sys: 12 | # - { os: macos-latest, shell: bash } 13 | - { os: ubuntu-latest, shell: bash } 14 | # - { os: windows-latest, shell: powershell } 15 | 16 | defaults: 17 | run: 18 | shell: ${{matrix.sys.shell}} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: 🔧 Install java 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: '25' 27 | 28 | - name: 🔧 Install clojure 29 | uses: DeLaGuardo/setup-clojure@master 30 | with: 31 | cli: '1.12.3.1577' 32 | 33 | - name: 🗝 maven cache 34 | uses: actions/cache@v4 35 | with: 36 | path: | 37 | ~/.m2 38 | ~/.gitlibs 39 | key: ${{ runner.os }}-maven-${{ github.sha }} 40 | restore-keys: | 41 | ${{ runner.os }}-maven- 42 | 43 | - name: 🧪 Run tests 44 | run: bin/kaocha 45 | -------------------------------------------------------------------------------- /test/unit/kaocha/plugin/junit_xml/xml_test.clj: -------------------------------------------------------------------------------- 1 | (ns kaocha.plugin.junit-xml.xml-test 2 | (:require [clojure.java.io :as io] 3 | [clojure.test :refer :all] 4 | [kaocha.plugin.junit-xml.xml :as xml] 5 | [matcher-combinators.test] 6 | [matcher-combinators.matchers :as m]) 7 | (:import java.io.FileInputStream)) 8 | 9 | (deftest emit-test 10 | (testing "escapes text" 11 | (is (= "\n\n<hello> & <world> are "great", aren't they?\n\n" 12 | (xml/emit-str {:tag :foo 13 | :content [" & are \"great\", aren't they?"]})))) 14 | 15 | (testing "escapes attributes" 16 | (is (= "\n\n" 17 | (xml/emit-str {:tag :foo 18 | :attrs {:hello "'twas brillig & the \"slithy\" toves"}}))))) 19 | 20 | (deftest validate-test 21 | (is (match? {:valid? false, 22 | :line-number 1, 23 | :column-number 6, 24 | :public-id nil, 25 | :system-id nil, 26 | :message (m/regex #"Cannot find the declaration of element 'foo'")} 27 | (xml/validate "" 28 | (io/resource "kaocha/junit_xml/JUnit.xsd"))))) 29 | 30 | (deftest coercions-test 31 | (let [schema-file (io/file (io/resource "kaocha/junit_xml/JUnit.xsd")) 32 | schema (xml/xml-schema schema-file)] 33 | (is (= (xml/as-schema schema) schema)) 34 | 35 | (is (instance? javax.xml.transform.stream.StreamSource (xml/as-source (FileInputStream. schema-file)))))) 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## Added 4 | 5 | ## Fixed 6 | 7 | ## Changed 8 | 9 | # 1.17.101 (2022-11-09 / 95067b2) 10 | 11 | ## Fixed 12 | 13 | * Fix Cljdoc build. 14 | 15 | # 1.16.98 (2022-07-26 / d50528d) 16 | 17 | ## Fixed 18 | 19 | - Fix `--junit-xml-add-location-metadata` command line flag 20 | 21 | # 1.15.95 (2022-07-26 / de6d134) 22 | 23 | ## Added 24 | 25 | - Added a flag to include file location metadata as attributes on testcases, à la 26 | pytest. 27 | 28 | # 0.0.76 (2020-07-21 / 397a3c1) 29 | 30 | ## Added 31 | 32 | - Added a flag to omit `system-out` from the generated junit.xml file, (thanks @ondrs) 33 | 34 | # 0.0-70 (2019-03-30 / 0377e39) 35 | 36 | ## Fixed 37 | 38 | - `<` and `>` are now escaped in XML attributes 39 | 40 | # 0.0-63 (2019-02-15 / 5243781) 41 | 42 | ## Fixed 43 | 44 | - Fix potential NullPointerException 45 | 46 | # 0.0-57 (2019-02-15 / 3804cb7) 47 | 48 | ## Fixed 49 | 50 | - Addressed a warning. 51 | 52 | # 0.0-53 (2019-01-28 / 69d2e2f) 53 | 54 | ## Fixed 55 | 56 | - Render non-leaf test types (e.g. clojure.test / ns) if they contain failures 57 | or errors (e.g. load errors). 58 | 59 | # 0.0-50 (2018-12-28 / d44f155) 60 | 61 | ## Changed 62 | 63 | - The rendering of errors and failures has been made more in line with what 64 | consumers expect, with a one-line message attribute, and with full multiline 65 | output as a text element. 66 | - Each failure and error is now output as a separate XML element, rather than 67 | being grouped into a single element for each type. 68 | - Rendering of errors (exceptions) will look for a 69 | `:kaocha.report/error-message` and `:kaocha.report/error-type`, before falling 70 | back to calling `.getMessage` / `.getClass` on the Exception. This is for 71 | cases like ClojureScript where the error is originating and captured outside 72 | the JVM. 73 | 74 | ## Fixed 75 | 76 | - Fixed an issue in the code that strips out ANSI escape sequences, to prevent 77 | it from eating up some of the input. 78 | 79 | # 0.0-47 (2018-12-07 / db418fa) 80 | 81 | ## Fixed 82 | 83 | - Detect "leaf" test types through Kaocha's hierarchy functions, this fixes 84 | compatibility with `kaocha-cucumber` 85 | 86 | # 0.0-43 (2018-12-05 / 311587e) 87 | 88 | ## Fixed 89 | 90 | - Address cljdoc analysis error preventing the docs to build 91 | 92 | # 0.0-39 (2018-12-05 / 1507bab) 93 | 94 | ## Fixed 95 | 96 | - Automatically create parent directory of output file if it doesn't exist 97 | 98 | ## Changed 99 | 100 | - Encode single quote as the more widely understood `'` rather than `` 101 | 102 | # 0.0-31 (2018-11-20 / 060108f) 103 | 104 | ## Fixed 105 | 106 | - Make XML output strictly conform to the JUnit XML schema ([#2](https://github.com/lambdaisland/kaocha-junit-xml/issues/2)) 107 | 108 | ## Changed 109 | 110 | - Strip escape characters in text node, they are not valid XML 111 | - Strip ANSI color codes 112 | - Number of skipped tests and number of assertions are no longer reported. While 113 | some sources seem to suggest they are part of the JUnit XML format, they are 114 | not part of the schema, and so hinder validation. 115 | 116 | # 0.0-27 (2018-11-17 / a7f8432) 117 | 118 | ## Fixed 119 | 120 | - Fix entity escaping of text nodes and attribute values in output XML ([#1](https://github.com/lambdaisland/kaocha-junit-xml/issues/1)) 121 | 122 | # 0.0-18 (2018-11-05 / 83a953b) 123 | 124 | ## Changed 125 | 126 | - error elements now contain the full stack trace as a child element, and only 127 | the short message as a message attribute 128 | 129 | # 0.0-13 (2018-11-01 / a22889b) 130 | 131 | ## Fixed 132 | 133 | - Make target file configurable in tests.edn 134 | 135 | # 0.0-7 (2018-10-31 / 163d219) 136 | 137 | First release. -------------------------------------------------------------------------------- /src/kaocha/plugin/junit_xml/xml.clj: -------------------------------------------------------------------------------- 1 | (ns kaocha.plugin.junit-xml.xml 2 | (:require [clojure.string :as str] 3 | [clojure.java.io :as io]) 4 | (:import javax.xml.validation.SchemaFactory 5 | java.io.File 6 | javax.xml.validation.Schema 7 | javax.xml.validation.Validator 8 | javax.xml.transform.Source 9 | javax.xml.transform.stream.StreamSource 10 | java.io.StringReader)) 11 | 12 | (def entities 13 | {"&" "&" 14 | "'" "'" 15 | "\"" """ 16 | "<" "<" 17 | ">" ">"}) 18 | 19 | (defn escape-text [s] 20 | (-> s 21 | (or "") 22 | (str/replace #"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD]" "") 23 | (str/replace #"[&'\"<>]" entities))) 24 | 25 | (defn escape-attr [s] 26 | (-> s 27 | (or "") 28 | (str/replace #"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD]" "") 29 | (str/replace #"[&\"<>]" entities))) 30 | 31 | (defn emit-attr [[k v]] 32 | (print (str " " (name k) "=\"" (escape-attr v) "\""))) 33 | 34 | (defn emit-element 35 | "Like xml/emit-element, but correctly escapes entities." 36 | [e] 37 | (if (instance? String e) 38 | (println (escape-text e)) 39 | (do 40 | (print (str "<" (name (:tag e)))) 41 | (run! emit-attr (:attrs e)) 42 | (if (:content e) 43 | (do 44 | (println ">") 45 | (run! emit-element (:content e)) 46 | (println (str ""))) 47 | (println "/>"))))) 48 | 49 | (defn emit 50 | "Like xml/emit, but correctly escapes entities." 51 | [x] 52 | (println "") 53 | (emit-element x)) 54 | 55 | (defn emit-str [x] 56 | (with-out-str 57 | (emit x))) 58 | 59 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 60 | ;; validation 61 | 62 | (def ^SchemaFactory schema-factory 63 | (memoize #(SchemaFactory/newInstance "http://www.w3.org/2001/XMLSchema"))) 64 | 65 | (defn ^Schema xml-schema [^File schema-file] 66 | (.newSchema (schema-factory) schema-file)) 67 | 68 | (defn ^Validator schema-validator [^Schema schema] 69 | (.newValidator schema)) 70 | 71 | (defprotocol SchemaCoercions 72 | (^Schema as-schema [_] "Coerce to javax.xml.validation.Schema") 73 | (^Validator as-validator [_] "Coerce to java.xml.validation.Validator")) 74 | 75 | (defprotocol SourceCoercions 76 | (^Source as-source [_] "Coerce to java.xml.transform.Source")) 77 | 78 | (extend-protocol SchemaCoercions 79 | Schema 80 | (as-schema [this] this) 81 | (as-validator [this] (schema-validator this)) 82 | 83 | Object 84 | (as-schema [this] (xml-schema (io/as-file this))) 85 | (as-validator [this] (as-validator (as-schema this)))) 86 | 87 | (extend-protocol SourceCoercions 88 | Source 89 | (as-source [this] this) 90 | 91 | java.io.Reader 92 | (as-source [this] (as-source (StreamSource. this))) 93 | 94 | ;; xml source as string 95 | String 96 | (as-source [this] (as-source (StringReader. this))) 97 | 98 | ;; clojure.xml format 99 | clojure.lang.APersistentMap 100 | (as-source [this] 101 | (as-source (with-out-str (emit this)))) 102 | 103 | ;; Anything that's coerible to a Reader 104 | Object 105 | (as-source [this] (as-source (io/reader this)))) 106 | 107 | (defn validate! [xml xsd] 108 | (.validate (as-validator xsd) (as-source xml))) 109 | 110 | (defn validate [xml xsd] 111 | (try 112 | (validate! xml xsd) 113 | {:valid? true} 114 | (catch org.xml.sax.SAXParseException e 115 | {:valid? false 116 | :line-number (.getLineNumber e) 117 | :column-number (.getColumnNumber e) 118 | :public-id (.getPublicId e) 119 | :system-id (.getSystemId e) 120 | :message (.getMessage e)}))) 121 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | lambdaisland 5 | kaocha-junit-xml 6 | 1.17.101 7 | kaocha-junit-xml 8 | Code coverage plugin for Kaocha based on Cloverage 9 | https://github.com/lambdaisland/kaocha-junit-xml 10 | 2018 11 | 12 | Lambda Island 13 | https://lambdaisland.com 14 | 15 | 16 | UTF-8 17 | 18 | 19 | 20 | Eclipse Public License 1.0 21 | https://www.eclipse.org/legal/epl-v10.html 22 | 23 | 24 | 25 | https://github.com/lambdaisland/kaocha-junit-xml 26 | scm:git:git://github.com/lambdaisland/kaocha-junit-xml.git 27 | scm:git:ssh://git@github.com/lambdaisland/kaocha-junit-xml.git 28 | 485bee4c388e536adfcb343fc8d4066e8625f937 29 | 30 | 31 | 32 | lambdaisland 33 | kaocha 34 | 1.69.1069 35 | true 36 | 37 | 38 | nubank 39 | matcher-combinators 40 | 3.5.0 41 | true 42 | 43 | 44 | org.clojure 45 | data.xml 46 | 0.0.8 47 | true 48 | 49 | 50 | lambdaisland 51 | kaocha-cloverage 52 | 1.0.75 53 | true 54 | 55 | 56 | 57 | src 58 | 59 | 60 | src 61 | 62 | 63 | resources 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | 3.8.1 71 | 72 | 1.8 73 | 1.8 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-jar-plugin 79 | 3.2.0 80 | 81 | 82 | 83 | 485bee4c388e536adfcb343fc8d4066e8625f937 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-gpg-plugin 91 | 1.6 92 | 93 | 94 | sign-artifacts 95 | verify 96 | 97 | sign 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | clojars 107 | https://repo.clojars.org/ 108 | 109 | 110 | 111 | 112 | clojars 113 | Clojars repository 114 | https://clojars.org/repo 115 | 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambdaisland/kaocha-junit-xml 2 | 3 | 4 | [![GitHub Actions](https://github.com/lambdaisland/kaocha-junit-xml/actions/workflows/main.yml/badge.svg)](https://github.com/lambdaisland/kaocha-junit-xml/actions/workflows/main.yml) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/kaocha-junit-xml)](https://cljdoc.org/d/lambdaisland/kaocha-junit-xml) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/kaocha-junit-xml.svg)](https://clojars.org/lambdaisland/kaocha-junit-xml) 5 | 6 | 7 | [Kaocha](https://github.com/lambdaisland/kaocha) plugin to generate a JUnit XML version of the test results. 8 | 9 | 10 | ## Lambda Island Open Source 11 | 12 | Thank you! kaocha-junit-xml is made possible thanks to our generous backers. [Become a 13 | backer on OpenCollective](https://opencollective.com/lambda-island) so that we 14 | can continue to make kaocha-junit-xml better. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |   23 | 24 | kaocha-junit-xml is part of a growing collection of quality Clojure libraries created and maintained 25 | by the fine folks at [Gaiwan](https://gaiwan.co). 26 | 27 | Pay it forward by [becoming a backer on our OpenCollective](http://opencollective.com/lambda-island), 28 | so that we continue to enjoy a thriving Clojure ecosystem. 29 | 30 | You can find an overview of all our different projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source). 31 | 32 |   33 | 34 |   35 | 36 | 37 | ## Usage 38 | 39 | - Add kaocha-junit-xml as a dependency 40 | 41 | ``` clojure 42 | ;; deps.edn 43 | {:aliases 44 | {:test 45 | {:extra-deps {lambdaisland/kaocha {...} 46 | lambdaisland/kaocha-junit-xml {:mvn/version "1.17.101"}}}}} 47 | ``` 48 | 49 | or 50 | 51 | ``` clojure 52 | ;; project.clj 53 | (defproject ,,, 54 | :dependencies [,,, 55 | [lambdaisland/kaocha-junit-xml "1.17.101"]]) 56 | ``` 57 | 58 | - Enable the plugin and set an output file 59 | 60 | ``` clojure 61 | ;; tests.edn 62 | #kaocha/v1 63 | {:plugins [:kaocha.plugin/junit-xml] 64 | :kaocha.plugin.junit-xml/target-file "junit.xml"} 65 | ``` 66 | 67 | Or from the CLI 68 | 69 | ``` shell 70 | bin/kaocha --plugin kaocha.plugin/junit-xml --junit-xml-file junit.xml 71 | ``` 72 | 73 | Optionally you can omit captured output from junit.xml 74 | 75 | ``` clojure 76 | ;; tests.edn 77 | #kaocha/v1 78 | {:plugins [:kaocha.plugin/junit-xml] 79 | :kaocha.plugin.junit-xml/target-file "junit.xml" 80 | :kaocha.plugin.junit-xml/omit-system-out? true} 81 | ``` 82 | 83 | Or from the CLI 84 | 85 | ``` shell 86 | bin/kaocha --plugin kaocha.plugin/junit-xml --junit-xml-file junit.xml --junit-xml-omit-system-out 87 | ``` 88 | 89 | ## Requirements 90 | 91 | Requires at least Kaocha 0.0-306 and Clojure 1.9. 92 | 93 | ## CI Integration 94 | 95 | Some CI tooling supports the `junit` `xml` output in various flavours. 96 | 97 | ### CircleCI 98 | 99 | One of the services that can use this output is CircleCI. Your 100 | `.circleci/config.yml` could look like this: 101 | 102 | ``` yml 103 | version: 2 104 | jobs: 105 | build: 106 | docker: 107 | - image: circleci/clojure:tools-deps-1.9.0.394 108 | steps: 109 | - checkout 110 | - run: mkdir -p test-results/kaocha 111 | - run: bin/kaocha --plugin kaocha.plugin/junit-xml --junit-xml-file test-results/kaocha/results.xml --junit-xml-add-location-metadata --junit-xml-use-relative-path-in-location 112 | - store_test_results: 113 | path: test-results 114 | ``` 115 | 116 | ### GitHub Actions 117 | 118 | The following configuration will create annotations for test failures on files of 119 | the relevant commit/PR. First enable the plugin with the `add-location-metadata?` 120 | flag in your `tests.edn`: 121 | 122 | ```edn 123 | #kaocha/v1 124 | {:plugins [:kaocha.plugin/junit-xml] 125 | :kaocha.plugin.junit-xml/target-file "junit.xml" 126 | :kaocha.plugin.junit-xml/add-location-metadata? true} 127 | ``` 128 | 129 | Then, an example `.github/workflows/build.yml` may look like: 130 | 131 | ```yml 132 | name: Build 133 | on: [push] 134 | 135 | jobs: 136 | build: 137 | runs-on: ubuntu-latest 138 | container: 139 | image: clojure:openjdk-8-tools-deps-1.11.1.1113 140 | steps: 141 | - uses: actions/checkout@v2 142 | - name: test 143 | run: | 144 | bin/kaocha 145 | - name: Annotate failure 146 | if: failure() 147 | uses: mikepenz/action-junit-report@41a3188dde10229782fd78cd72fc574884dd7686 148 | with: 149 | report_paths: junit.xml 150 | ``` 151 | 152 | ### Gitlab 153 | 154 | Configuring Gitlab to parse JUnit XML is easy; just add a `report` artifact that 155 | points to the XML file: 156 | 157 | ```yaml 158 | test: 159 | only: 160 | -tags 161 | script: 162 | - make test 163 | artifacts: 164 | reports: 165 | junit: junit.xml 166 | ``` 167 | 168 | See the [Gitlab documentation on reports using 169 | JUnit](https://docs.gitlab.com/ce/ci/junit_test_reports.html) for more information. 170 | 171 | ## Caveats 172 | 173 | For timing information (timestamp and running time) this plugin relies on the 174 | `kaocha.plugin/profiling` plugin. If the plugin is not present then a running 175 | time of 0 will be reported. 176 | 177 | For output capturing the `kaocha.plugin/capture-output` must be present. If it 178 | is not present `` will always be empty. 179 | 180 | ## Resources 181 | 182 | It was hard to find a definitive source of the Ant Junit XML format. I mostly 183 | went with [this page](http://llg.cubic.org/docs/junit/) for documentation. 184 | 185 | For information on how to configure CircleCI to use this information, see 186 | [store_test_results](https://circleci.com/docs/2.0/configuration-reference/#store_test_results). 187 | 188 | After reports that the output was not compatible with Azure Devops Pipeline the 189 | output was changed to adhere to [this schema](https://github.com/windyroad/JUnit-Schema/blob/49e95a79cc0bfba7961aaf779710a43a4d3f96bd/JUnit.xsd). 190 | 191 | The `--junit-xml-add-location-metadata` flag was added to enhance `testcase` 192 | output with test location metadata à la 193 | [pytest](https://docs.pytest.org/en/latest/how-to/output.html?highlight=junit#creating-junitxml-format-files). 194 | This allows for integration with various tools on GitHub Actions for producing 195 | annotations on files in commits/PRs with test failure data. For example, the 196 | [JUnit Report Action](https://github.com/marketplace/actions/junit-report-action). 197 | 198 | The `--junit-xml-use-relative-path-in-location` flasg was added to enhance the `--junit-xml-add-location-metadata` 199 | output for test ecosystems that rely on relative file-pathing. 200 | Notably, CircleCI's [parallelization](https://circleci.com/docs/use-the-circleci-cli-to-split-tests/#junit-xml-reports) assumes file paths in JUnit files are relative to the runner's working directory. 201 | 202 | 203 | ## Contributing 204 | 205 | We warmly welcome patches to kaocha-junit-xml. Please keep in mind the following: 206 | 207 | - adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide) 208 | - write patches that solve a problem 209 | - start by stating the problem, then supply a minimal solution `*` 210 | - by contributing you agree to license your contributions as EPL 1.0 211 | - don't break the contract with downstream consumers `**` 212 | - don't break the tests 213 | 214 | We would very much appreciate it if you also 215 | 216 | - update the CHANGELOG and README 217 | - add tests for new functionality 218 | 219 | We recommend opening an issue first, before opening a pull request. That way we 220 | can make sure we agree what the problem is, and discuss how best to solve it. 221 | This is especially true if you add new dependencies, or significantly increase 222 | the API surface. In cases like these we need to decide if these changes are in 223 | line with the project's goals. 224 | 225 | `*` This goes for features too, a feature needs to solve a problem. State the problem it solves first, only then move on to solving it. 226 | 227 | `**` Projects that have a version that starts with `0.` may still see breaking changes, although we also consider the level of community adoption. The more widespread a project is, the less likely we're willing to introduce breakage. See [LambdaIsland-flavored Versioning](https://github.com/lambdaisland/open-source#lambdaisland-flavored-versioning) for more info. 228 | 229 | 230 | 231 | ## License 232 | 233 | Copyright © 2018-2025 Arne Brasseur and contributors 234 | 235 | Available under the terms of the Eclipse Public License 1.0, see LICENSE.txt 236 | -------------------------------------------------------------------------------- /src/kaocha/plugin/junit_xml.clj: -------------------------------------------------------------------------------- 1 | (ns kaocha.plugin.junit-xml 2 | (:refer-clojure :exclude [symbol]) 3 | (:require [clojure.java.io :as io] 4 | [kaocha.core-ext :refer :all] 5 | [kaocha.hierarchy :as hierarchy] 6 | [kaocha.plugin :as plugin :refer [defplugin]] 7 | [kaocha.plugin.junit-xml.xml :as xml] 8 | [kaocha.report :as report] 9 | [kaocha.result :as result] 10 | [kaocha.testable :as testable] 11 | [clojure.string :as str]) 12 | (:import java.time.Instant 13 | java.nio.file.Files 14 | java.nio.file.Paths 15 | java.nio.file.attribute.FileAttribute)) 16 | 17 | (defn inst->iso8601 [inst] 18 | (.. java.time.format.DateTimeFormatter/ISO_LOCAL_DATE_TIME 19 | (withZone (java.time.ZoneId/systemDefault)) 20 | (format (.truncatedTo inst java.time.temporal.ChronoUnit/SECONDS)))) 21 | 22 | ;; The ESC [ is followed by any number (including none) of "parameter bytes" in 23 | ;; the range 0x30–0x3F (ASCII 0–9:;<=>?), then by any number of "intermediate 24 | ;; bytes" in the range 0x20–0x2F (ASCII space and !"#$%&'()*+,-./), then finally 25 | ;; by a single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). 26 | (defn strip-ansi-sequences [s] 27 | (str/replace s #"\x1b\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]" "")) 28 | 29 | (defn test-seq [testable] 30 | (cons testable (mapcat test-seq (::result/tests testable)))) 31 | 32 | (defn leaf-tests [testable] 33 | (filter #(or (hierarchy/leaf? %) 34 | (result/failed-one? %)) 35 | (test-seq testable))) 36 | 37 | (defn time-stat [testable] 38 | (let [duration (:kaocha.plugin.profiling/duration testable 0)] 39 | (when duration 40 | {:time (format "%.6f" (/ duration 1e9))}))) 41 | 42 | (defn stats [testable] 43 | (let [tests (test-seq testable) 44 | totals (result/testable-totals testable) 45 | start-time (:kaocha.plugin.profiling/start testable (Instant/now))] 46 | {:errors (::result/error totals) 47 | :failures (::result/fail totals) 48 | :tests (::result/count totals) 49 | :timestamp (inst->iso8601 start-time)})) 50 | 51 | (defn test-name [test] 52 | (let [id (::testable/id test)] 53 | (str (if-let [n (and (qualified-ident? id) (namespace id))] 54 | (str n "/")) 55 | (name id)))) 56 | 57 | (defn failure-message [m] 58 | (str/trim 59 | (strip-ansi-sequences 60 | (with-out-str 61 | (report/fail-summary m))))) 62 | 63 | (defn classname [obj] 64 | (.getName (class obj))) 65 | 66 | (defn failure->xml [m] 67 | (let [assertion-type (report/assertion-type m)] 68 | {:tag :failure 69 | :attrs {:message (format "[%s] expected: %s. actual: %s" 70 | (:type m) 71 | (:expected m) 72 | (:actual m)) 73 | :type (str "assertion failure" 74 | (when-not (= :default assertion-type) 75 | (str ": " assertion-type)))} 76 | :content [(failure-message m)]})) 77 | 78 | (defn error->xml [m] 79 | (let [exception (when (throwable? (:actual m)) 80 | (:actual m))] 81 | {:tag :error 82 | :attrs {:message (or (:message m) 83 | (when exception 84 | (.getMessage exception))) 85 | :type (if exception 86 | (classname exception) 87 | ":error")} 88 | :content [(failure-message m)]})) 89 | 90 | (defn absolute-path 91 | [test-file] 92 | (some-> test-file 93 | ;; TODO: 94 | ;; This works, but it is preferable to fix this upstream in `kaocha` so 95 | ;; we don't have to supply the classloader explicitly. 96 | ;; See the discussion on the following PR: 97 | ;; https://github.com/lambdaisland/kaocha-junit-xml/pull/13 98 | (io/resource (deref Compiler/LOADER)) 99 | io/file 100 | .getAbsolutePath)) 101 | 102 | (defn relative-path 103 | [test-file] 104 | (when-let [absolute-path (absolute-path test-file)] 105 | (let [empty-string-array (make-array String 0) 106 | current-path ^java.nio.file.Path (Paths/get (System/getProperty "user.dir") empty-string-array) 107 | file-path ^java.nio.file.Path (Paths/get absolute-path empty-string-array)] 108 | (str (.relativize current-path file-path))))) 109 | 110 | (defn set-file-path 111 | [m result] 112 | (if (::use-relative-path-in-location? result) 113 | (relative-path m) 114 | (absolute-path m))) 115 | 116 | (defn test-location-metadata 117 | [m result] 118 | (-> m 119 | (get ::testable/meta) 120 | (select-keys [:line :column :file]) 121 | (update :file set-file-path result))) 122 | 123 | (defn testcase->xml [result test] 124 | (let [{::testable/keys [id skip events] 125 | ::result/keys [pass fail error] 126 | :or {pass 0 fail 0 error 0}} test] 127 | {:tag :testcase 128 | :attrs (merge {:name (test-name test) 129 | :classname (namespace id)} 130 | (time-stat test) 131 | (when (some-> result ::add-location-metadata?) 132 | (test-location-metadata test result))) 133 | :content (keep (fn [m] 134 | (cond 135 | (hierarchy/error-type? m) (error->xml m) 136 | (hierarchy/fail-type? m) (failure->xml m))) 137 | events)})) 138 | 139 | (defn suite->xml [{::keys [omit-system-out?] :as result} suite index] 140 | (let [id (::testable/id suite)] 141 | {:tag :testsuite 142 | :attrs (-> {:name (test-name suite) 143 | :id index 144 | :hostname "localhost"} 145 | (merge (stats suite) (time-stat suite)) 146 | (assoc :package (if (qualified-ident? id) 147 | (namespace id) 148 | ""))) 149 | :content (concat 150 | [{:tag :properties}] 151 | (map (partial testcase->xml result) (leaf-tests suite)) 152 | [(cond-> {:tag :system-out} 153 | (not omit-system-out?) 154 | (assoc :content (->> suite 155 | test-seq 156 | (keep :kaocha.plugin.capture-output/output) 157 | (remove #{""}) 158 | (map strip-ansi-sequences)))) 159 | {:tag :system-err}])})) 160 | 161 | (defn result->xml [result] 162 | (let [suites (::result/tests result)] 163 | {:tag :testsuites 164 | :attrs {} 165 | :content (map (partial suite->xml result) 166 | suites 167 | (range))})) 168 | 169 | (defn mkparent [path] 170 | (when-let [parent (.getParent path) ] 171 | (let [ppath (.toPath (io/file parent))] 172 | (Files/createDirectories ppath (into-array FileAttribute []))))) 173 | 174 | (defn write-junit-xml [filename result] 175 | (let [file (io/file filename)] 176 | (mkparent file) 177 | (with-open [f (io/writer file)] 178 | (binding [*out* f] 179 | (xml/emit (result->xml result)))))) 180 | 181 | (defplugin kaocha.plugin/junit-xml 182 | "Write test results to junit.xml" 183 | 184 | (cli-options [opts] 185 | (conj opts 186 | [nil 187 | "--junit-xml-file FILENAME" 188 | "Save the test results to a Ant JUnit XML file."] 189 | [nil 190 | "--junit-xml-omit-system-out" 191 | "Do not add captured output to junit.xml"] 192 | [nil 193 | "--junit-xml-add-location-metadata" 194 | "Add line, column, and file attributes to tests in junit.xml"] 195 | [nil 196 | "--junit-xml-use-relative-path-in-location" 197 | "Updates the file attributes printed by --junit-xml-add-location-metadata to use pathing relative to where kaocha was executed from"])) 198 | 199 | (config [config] 200 | (let [target-file (get-in config [:kaocha/cli-options :junit-xml-file]) 201 | omit-system-out? (get-in config [:kaocha/cli-options :junit-xml-omit-system-out]) 202 | add-location-metadata? (get-in config [:kaocha/cli-options :junit-xml-add-location-metadata]) 203 | use-relative-path-in-location? (get-in config [:kaocha/cli-options :junit-xml-use-relative-path-in-location])] 204 | (cond-> config 205 | 206 | target-file 207 | (assoc ::target-file target-file) 208 | 209 | omit-system-out? 210 | (assoc ::omit-system-out? true) 211 | 212 | add-location-metadata? 213 | (assoc ::add-location-metadata? true) 214 | 215 | use-relative-path-in-location? 216 | (assoc ::use-relative-path-in-location? true)))) 217 | 218 | (post-run [result] 219 | (when-let [filename (::target-file result)] 220 | (write-junit-xml filename result)) 221 | result)) 222 | -------------------------------------------------------------------------------- /resources/kaocha/junit_xml/JUnit.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | JUnit test result schema for the Apache Ant JUnit and JUnitReport tasks 10 | Copyright © 2011, Windy Road Technology Pty. Limited 11 | The Apache Ant JUnit XML Schema is distributed under the terms of the Apache License Version 2.0 http://www.apache.org/licenses/ 12 | Permission to waive conditions of this license may be requested from Windy Road Support (http://windyroad.org/support). 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Contains an aggregation of testsuite results 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Derived from testsuite/@name in the non-aggregated documents 33 | 34 | 35 | 36 | 37 | Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Contains the results of exexuting a testsuite 50 | 51 | 52 | 53 | 54 | Properties (e.g., environment settings) set during test execution 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace 79 | 80 | 81 | 82 | 83 | 84 | 85 | The error message. e.g., if a java exception is thrown, the return value of getMessage() 86 | 87 | 88 | 89 | 90 | The type of error that occured. e.g., if a java execption is thrown the full class name of the exception. 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace 100 | 101 | 102 | 103 | 104 | 105 | 106 | The message specified in the assert 107 | 108 | 109 | 110 | 111 | The type of the assert. 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Name of the test method 122 | 123 | 124 | 125 | 126 | Full class name for the class the test method is in. 127 | 128 | 129 | 130 | 131 | Time taken (in seconds) to execute the test 132 | 133 | 134 | 135 | 136 | 137 | 138 | Data that was written to standard out while the test was executed 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Data that was written to standard error while the test was executed 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | when the test was executed. Timezone may not be specified. 170 | 171 | 172 | 173 | 174 | Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined. 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | The total number of tests in the suite 185 | 186 | 187 | 188 | 189 | The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals 190 | 191 | 192 | 193 | 194 | The total number of tests in the suite that errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. 195 | 196 | 197 | 198 | 199 | Time taken (in seconds) to execute the tests in the suite 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' from 19 | a Contributor if it was added to the Program by such Contributor itself or 20 | anyone acting on such Contributor's behalf. Contributions do not include 21 | additions to the Program which: (i) are separate modules of software 22 | distributed in conjunction with the Program under their own license 23 | agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement, 34 | including all Contributors. 35 | 36 | 2. GRANT OF RIGHTS 37 | a) Subject to the terms of this Agreement, each Contributor hereby grants 38 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 39 | reproduce, prepare derivative works of, publicly display, publicly perform, 40 | distribute and sublicense the Contribution of such Contributor, if any, and 41 | such derivative works, in source code and object code form. 42 | b) Subject to the terms of this Agreement, each Contributor hereby grants 43 | Recipient a non-exclusive, worldwide, royalty-free patent license under 44 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 45 | transfer the Contribution of such Contributor, if any, in source code and 46 | object code form. This patent license shall apply to the combination of the 47 | Contribution and the Program if, at the time the Contribution is added by 48 | the Contributor, such addition of the Contribution causes such combination 49 | to be covered by the Licensed Patents. The patent license shall not apply 50 | to any other combinations which include the Contribution. No hardware per 51 | se is licensed hereunder. 52 | c) Recipient understands that although each Contributor grants the licenses to 53 | its Contributions set forth herein, no assurances are provided by any 54 | Contributor that the Program does not infringe the patent or other 55 | intellectual property rights of any other entity. Each Contributor 56 | disclaims any liability to Recipient for claims brought by any other entity 57 | based on infringement of intellectual property rights or otherwise. As a 58 | condition to exercising the rights and licenses granted hereunder, each 59 | Recipient hereby assumes sole responsibility to secure any other 60 | intellectual property rights needed, if any. For example, if a third party 61 | patent license is required to allow Recipient to distribute the Program, it 62 | is Recipient's responsibility to acquire that license before distributing 63 | the Program. 64 | d) Each Contributor represents that to its knowledge it has sufficient 65 | copyright rights in its Contribution, if any, to grant the copyright 66 | license set forth in this Agreement. 67 | 68 | 3. REQUIREMENTS 69 | 70 | A Contributor may choose to distribute the Program in object code form under its 71 | own license agreement, provided that: 72 | 73 | a) it complies with the terms and conditions of this Agreement; and 74 | b) its license agreement: 75 | i) effectively disclaims on behalf of all Contributors all warranties and 76 | conditions, express and implied, including warranties or conditions of 77 | title and non-infringement, and implied warranties or conditions of 78 | merchantability and fitness for a particular purpose; 79 | ii) effectively excludes on behalf of all Contributors all liability for 80 | damages, including direct, indirect, special, incidental and 81 | consequential damages, such as lost profits; 82 | iii) states that any provisions which differ from this Agreement are offered 83 | by that Contributor alone and not by any other party; and 84 | iv) states that source code for the Program is available from such 85 | Contributor, and informs licensees how to obtain it in a reasonable 86 | manner on or through a medium customarily used for software exchange. 87 | 88 | When the Program is made available in source code form: 89 | 90 | a) it must be made available under this Agreement; and 91 | b) a copy of this Agreement must be included with each copy of the Program. 92 | Contributors may not remove or alter any copyright notices contained within 93 | the Program. 94 | 95 | Each Contributor must identify itself as the originator of its Contribution, if 96 | any, in a manner that reasonably allows subsequent Recipients to identify the 97 | originator of the Contribution. 98 | 99 | 4. COMMERCIAL DISTRIBUTION 100 | 101 | Commercial distributors of software may accept certain responsibilities with 102 | respect to end users, business partners and the like. While this license is 103 | intended to facilitate the commercial use of the Program, the Contributor who 104 | includes the Program in a commercial product offering should do so in a manner 105 | which does not create potential liability for other Contributors. Therefore, if 106 | a Contributor includes the Program in a commercial product offering, such 107 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 108 | every other Contributor ("Indemnified Contributor") against any losses, damages 109 | and costs (collectively "Losses") arising from claims, lawsuits and other legal 110 | actions brought by a third party against the Indemnified Contributor to the 111 | extent caused by the acts or omissions of such Commercial Contributor in 112 | connection with its distribution of the Program in a commercial product 113 | offering. The obligations in this section do not apply to any claims or Losses 114 | relating to any actual or alleged intellectual property infringement. In order 115 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 116 | Contributor in writing of such claim, and b) allow the Commercial Contributor to 117 | control, and cooperate with the Commercial Contributor in, the defense and any 118 | related settlement negotiations. The Indemnified Contributor may participate in 119 | any such claim at its own expense. 120 | 121 | For example, a Contributor might include the Program in a commercial product 122 | offering, Product X. That Contributor is then a Commercial Contributor. If that 123 | Commercial Contributor then makes performance claims, or offers warranties 124 | related to Product X, those performance claims and warranties are such 125 | Commercial Contributor's responsibility alone. Under this section, the 126 | Commercial Contributor would have to defend claims against the other 127 | Contributors related to those performance claims and warranties, and if a court 128 | requires any other Contributor to pay any damages as a result, the Commercial 129 | Contributor must pay those damages. 130 | 131 | 5. NO WARRANTY 132 | 133 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 134 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 135 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 136 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 137 | Recipient is solely responsible for determining the appropriateness of using and 138 | distributing the Program and assumes all risks associated with its exercise of 139 | rights under this Agreement , including but not limited to the risks and costs 140 | of program errors, compliance with applicable laws, damage to or loss of data, 141 | programs or equipment, and unavailability or interruption of operations. 142 | 143 | 6. DISCLAIMER OF LIABILITY 144 | 145 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 146 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 147 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 148 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 149 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 150 | OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 151 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 152 | 153 | 7. GENERAL 154 | 155 | If any provision of this Agreement is invalid or unenforceable under applicable 156 | law, it shall not affect the validity or enforceability of the remainder of the 157 | terms of this Agreement, and without further action by the parties hereto, such 158 | provision shall be reformed to the minimum extent necessary to make such 159 | provision valid and enforceable. 160 | 161 | If Recipient institutes patent litigation against any entity (including a 162 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 163 | (excluding combinations of the Program with other software or hardware) 164 | infringes such Recipient's patent(s), then such Recipient's rights granted under 165 | Section 2(b) shall terminate as of the date such litigation is filed. 166 | 167 | All Recipient's rights under this Agreement shall terminate if it fails to 168 | comply with any of the material terms or conditions of this Agreement and does 169 | not cure such failure in a reasonable period of time after becoming aware of 170 | such noncompliance. If all Recipient's rights under this Agreement terminate, 171 | Recipient agrees to cease use and distribution of the Program as soon as 172 | reasonably practicable. However, Recipient's obligations under this Agreement 173 | and any licenses granted by Recipient relating to the Program shall continue and 174 | survive. 175 | 176 | Everyone is permitted to copy and distribute copies of this Agreement, but in 177 | order to avoid inconsistency the Agreement is copyrighted and may only be 178 | modified in the following manner. The Agreement Steward reserves the right to 179 | publish new versions (including revisions) of this Agreement from time to time. 180 | No one other than the Agreement Steward has the right to modify this Agreement. 181 | The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation 182 | may assign the responsibility to serve as the Agreement Steward to a suitable 183 | separate entity. Each new version of the Agreement will be given a 184 | distinguishing version number. The Program (including Contributions) may always 185 | be distributed subject to the version of the Agreement under which it was 186 | received. In addition, after a new version of the Agreement is published, 187 | Contributor may elect to distribute the Program (including its Contributions) 188 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 189 | above, Recipient receives no rights or licenses to the intellectual property of 190 | any Contributor under this Agreement, whether expressly, by implication, 191 | estoppel or otherwise. All rights in the Program not expressly granted under 192 | this Agreement are reserved. 193 | 194 | This Agreement is governed by the laws of the State of New York and the 195 | intellectual property laws of the United States of America. No party to this 196 | Agreement will bring a legal action under this Agreement more than one year 197 | after the cause of action arose. Each party waives its rights to a jury trial in 198 | any resulting litigation. 199 | -------------------------------------------------------------------------------- /test/unit/kaocha/plugin/junit_xml_test.clj: -------------------------------------------------------------------------------- 1 | (ns kaocha.plugin.junit-xml-test 2 | (:require [kaocha.plugin.junit-xml :as junit-xml] 3 | [kaocha.repl :as repl] 4 | [kaocha.api :as api] 5 | [clojure.test :refer :all] 6 | [matcher-combinators.test] 7 | [clojure.string :as str] 8 | [kaocha.plugin :as plugin] 9 | [kaocha.plugin.junit-xml.xml :as xml] 10 | [clojure.java.io :as io] 11 | [clojure.xml]) 12 | (:import [java.nio.file Files] 13 | [java.nio.file.attribute FileAttribute] 14 | [javax.xml.parsers SAXParser SAXParserFactory])) 15 | 16 | (java.util.TimeZone/setDefault (java.util.TimeZone/getTimeZone "UTC")) 17 | 18 | (defn xml->hiccup [xml] 19 | (if (map? xml) 20 | (let [{:keys [tag attrs content]} xml] 21 | `[~tag ~@(when (seq attrs) [attrs]) ~@(map xml->hiccup content)]) 22 | xml)) 23 | 24 | (defn abs-path [test-path test-file] 25 | (.. (io/file test-path test-file) 26 | getAbsolutePath 27 | toString)) 28 | 29 | (deftest xml-output-test 30 | (is (match? 31 | [:testsuites 32 | [:testsuite {:id 0 33 | :tests 4 34 | :failures 1 35 | :errors 2 36 | :package "" 37 | :name "unit" 38 | :hostname "localhost" 39 | :timestamp string? 40 | :time "0.000000"} 41 | [:properties] 42 | [:testcase {:name "demo.test/basic-test" :classname "demo.test" :time "0.000000"}] 43 | [:testcase {:name "demo.test/exception-in-is-test" 44 | :classname "demo.test" 45 | :time "0.000000"} 46 | [:error {:message "Inside assertion" :type "java.lang.Exception"} 47 | #(str/starts-with? % (str "ERROR in demo.test/exception-in-is-test (test.clj:15)\n" 48 | "Exception: java.lang.Exception: Inside assertion\n" 49 | " at demo.test"))]] 50 | [:testcase {:name "demo.test/exception-outside-is-test" 51 | :classname "demo.test" 52 | :time "0.000000"} 53 | [:error {:message "Uncaught exception, not in assertion." 54 | :type "java.lang.Exception"} 55 | #(str/starts-with? % (str "ERROR in demo.test/exception-outside-is-test (test.clj:19)\n" 56 | "Uncaught exception, not in assertion.\n" 57 | "Exception: java.lang.Exception: outside assertion\n" 58 | " at demo.test"))]] 59 | [:testcase {:name "demo.test/output-test" 60 | :classname "demo.test" 61 | :time "0.000000"} 62 | [:failure {:message "[:fail] expected: (= {:foo 1} {:foo 2}). actual: (not (= {:foo 1} {:foo 2}))" 63 | :type "assertion failure: ="} 64 | (str "FAIL in demo.test/output-test (test.clj:13)\n" 65 | "oops\n" 66 | "Expected:\n" 67 | " {:foo 1}\n" 68 | "Actual:\n" 69 | " {:foo -1 +2}\n" 70 | "╭───── Test output ───────────────────────────────────────────────────────\n" 71 | "│ this is on stdout\n" 72 | "│ this is on stderr\n" 73 | "╰─────────────────────────────────────────────────────────────────────────")]] 74 | [:testcase {:name "demo.test/skip-test" 75 | :classname "demo.test" 76 | :time "0.000000"}] 77 | [:system-out "this is on stdout\nthis is on stderr\n"] 78 | [:system-err]]] 79 | 80 | (-> {:tests [{:test-paths ["fixtures/kaocha-demo"] 81 | :ns-patterns [".*"]}] 82 | :kaocha.plugin.randomize/randomize? false 83 | :color? false 84 | :reporter identity} 85 | repl/config 86 | api/run 87 | junit-xml/result->xml 88 | xml->hiccup))) 89 | 90 | (testing "it outputs location metadata when configured" 91 | (is (match? 92 | [:testsuites 93 | [:testsuite {:id 0 94 | :tests 4 95 | :failures 1 96 | :errors 2 97 | :package "" 98 | :name "unit" 99 | :hostname "localhost" 100 | :timestamp string? 101 | :time "0.000000"} 102 | [:properties] 103 | [:testcase {:name "demo.test/basic-test" :classname "demo.test" :time "0.000000"}] 104 | [:testcase {:name "demo.test/exception-in-is-test" 105 | :classname "demo.test" 106 | :time "0.000000" 107 | :column 1 108 | :file (abs-path "fixtures/kaocha-demo" "demo/test.clj") 109 | :line 15} 110 | [:error {:message "Inside assertion" :type "java.lang.Exception"} 111 | #(str/starts-with? % (str "ERROR in demo.test/exception-in-is-test (test.clj:15)\n" 112 | "it outputs location metadata when configured\n" 113 | "Exception: java.lang.Exception: Inside assertion\n" 114 | " at demo.test"))]] 115 | [:testcase {:name "demo.test/exception-outside-is-test" 116 | :classname "demo.test" 117 | :time "0.000000" 118 | :column 1 119 | :file (abs-path "fixtures/kaocha-demo" "demo/test.clj") 120 | :line 19} 121 | [:error {:message "Uncaught exception, not in assertion." 122 | :type "java.lang.Exception"} 123 | #(str/starts-with? % (str "ERROR in demo.test/exception-outside-is-test (test.clj:19)\n" 124 | "it outputs location metadata when configured\n" 125 | "Uncaught exception, not in assertion.\n" 126 | "Exception: java.lang.Exception: outside assertion\n" 127 | " at demo.test"))]] 128 | [:testcase {:name "demo.test/output-test" 129 | :classname "demo.test" 130 | :time "0.000000" 131 | :column 1 132 | :line 7 133 | :file (abs-path "fixtures/kaocha-demo" "demo/test.clj")} 134 | [:failure {:message "[:fail] expected: (= {:foo 1} {:foo 2}). actual: (not (= {:foo 1} {:foo 2}))" 135 | :type "assertion failure: ="} 136 | (str "FAIL in demo.test/output-test (test.clj:13)\n" 137 | "it outputs location metadata when configured\n" 138 | "oops\n" 139 | "Expected:\n" 140 | " {:foo 1}\n" 141 | "Actual:\n" 142 | " {:foo -1 +2}\n" 143 | "╭───── Test output ───────────────────────────────────────────────────────\n" 144 | "│ this is on stdout\n" 145 | "│ this is on stderr\n" 146 | "╰─────────────────────────────────────────────────────────────────────────")]] 147 | [:testcase {:name "demo.test/skip-test" 148 | :classname "demo.test" 149 | :time "0.000000"}] 150 | [:system-out "this is on stdout\nthis is on stderr\n"] 151 | [:system-err]]] 152 | 153 | (-> {:tests [{:test-paths ["fixtures/kaocha-demo"] 154 | :ns-patterns [".*"]}] 155 | :kaocha.plugin.randomize/randomize? false 156 | :kaocha.plugin.junit-xml/add-location-metadata? true 157 | :color? false 158 | :reporter identity} 159 | repl/config 160 | api/run 161 | junit-xml/result->xml 162 | xml->hiccup)))) 163 | (testing "it outputs location metadata when configured" 164 | (is (match? 165 | [:testsuites 166 | [:testsuite {:id 0 167 | :tests 4 168 | :failures 1 169 | :errors 2 170 | :package "" 171 | :name "unit" 172 | :hostname "localhost" 173 | :timestamp string? 174 | :time "0.000000"} 175 | [:properties] 176 | [:testcase {:name "demo.test/basic-test" :classname "demo.test" :time "0.000000"}] 177 | [:testcase {:name "demo.test/exception-in-is-test" 178 | :classname "demo.test" 179 | :time "0.000000" 180 | :column 1 181 | :file "fixtures/kaocha-demo/demo/test.clj" 182 | :line 15} 183 | [:error {:message "Inside assertion" :type "java.lang.Exception"} 184 | #(str/starts-with? % (str "ERROR in demo.test/exception-in-is-test (test.clj:15)\n" 185 | "it outputs location metadata when configured\n" 186 | "Exception: java.lang.Exception: Inside assertion\n" 187 | " at demo.test"))]] 188 | [:testcase {:name "demo.test/exception-outside-is-test" 189 | :classname "demo.test" 190 | :time "0.000000" 191 | :column 1 192 | :file "fixtures/kaocha-demo/demo/test.clj" 193 | :line 19} 194 | [:error {:message "Uncaught exception, not in assertion." 195 | :type "java.lang.Exception"} 196 | #(str/starts-with? % (str "ERROR in demo.test/exception-outside-is-test (test.clj:19)\n" 197 | "it outputs location metadata when configured\n" 198 | "Uncaught exception, not in assertion.\n" 199 | "Exception: java.lang.Exception: outside assertion\n" 200 | " at demo.test"))]] 201 | [:testcase {:name "demo.test/output-test" 202 | :classname "demo.test" 203 | :time "0.000000" 204 | :column 1 205 | :line 7 206 | :file "fixtures/kaocha-demo/demo/test.clj"} 207 | [:failure {:message "[:fail] expected: (= {:foo 1} {:foo 2}). actual: (not (= {:foo 1} {:foo 2}))" 208 | :type "assertion failure: ="} 209 | (str "FAIL in demo.test/output-test (test.clj:13)\n" 210 | "it outputs location metadata when configured\n" 211 | "oops\n" 212 | "Expected:\n" 213 | " {:foo 1}\n" 214 | "Actual:\n" 215 | " {:foo -1 +2}\n" 216 | "╭───── Test output ───────────────────────────────────────────────────────\n" 217 | "│ this is on stdout\n" 218 | "│ this is on stderr\n" 219 | "╰─────────────────────────────────────────────────────────────────────────")]] 220 | [:testcase {:name "demo.test/skip-test" 221 | :classname "demo.test" 222 | :time "0.000000"}] 223 | [:system-out "this is on stdout\nthis is on stderr\n"] 224 | [:system-err]]] 225 | 226 | (-> {:tests [{:test-paths ["fixtures/kaocha-demo"] 227 | :ns-patterns [".*"]}] 228 | :kaocha.plugin.randomize/randomize? false 229 | :kaocha.plugin.junit-xml/add-location-metadata? true 230 | :kaocha.plugin.junit-xml/use-relative-path-in-location? true 231 | :color? false 232 | :reporter identity} 233 | repl/config 234 | api/run 235 | junit-xml/result->xml 236 | xml->hiccup)))) 237 | (testing "it renders start-time when present" 238 | (is (= [:testsuites 239 | [:testsuite {:name "my-test-type" :id 0 :hostname "localhost" 240 | :package "" 241 | :errors 0 :failures 1 :tests 1 242 | :timestamp "2007-12-03T10:15:30" :time "0.000012"} 243 | [:properties] 244 | [:testcase {:classname nil 245 | :name "my-test-type" 246 | :time "0.000012"}] 247 | [:system-out] 248 | [:system-err]]] 249 | (-> {:kaocha.result/tests [{:kaocha.testable/id :my-test-type 250 | :kaocha.testable/type ::foo 251 | :kaocha.result/count 1 252 | :kaocha.result/fail 1 253 | :kaocha.result/pass 1 254 | :kaocha.result/error 0 255 | :kaocha.plugin.profiling/start (java.time.Instant/parse "2007-12-03T10:15:30.00Z") 256 | :kaocha.plugin.profiling/duration 12345}]} 257 | junit-xml/result->xml 258 | xml->hiccup))))) 259 | 260 | 261 | (deftest makeparent-test 262 | (junit-xml/mkparent (io/file "target/foo/bar/baz.xml")) 263 | (is (.isDirectory (io/file "target/foo/bar"))) 264 | (.delete (io/file "target/foo/bar")) 265 | (.delete (io/file "target/foo"))) 266 | 267 | (deftest testcase->xml-test 268 | (is (match? 269 | [:testcase 270 | {:name "my.app/my-test-case", :classname "my.app", :time "0.000000"} 271 | [:failure 272 | {:message 273 | "[:fail] expected: (= {:foo 4} {:foo 5}). actual: (not (= {:foo 4} {:foo 5}))", 274 | :type "assertion failure: ="} 275 | "FAIL in (bar.clj:108)\nExpected:\n {:foo 4}\nActual:\n {:foo -4 +5}"] 276 | [:error 277 | {:message "oh no", :type "java.lang.Exception"} 278 | #(str/starts-with? % "ERROR in (foo.clj:42)\nException: java.lang.Exception: oh no\n at kaocha.plugin.junit_xml_test")]] 279 | 280 | (->> {:kaocha.testable/id :my.app/my-test-case 281 | :kaocha.result/error 1 282 | :kaocha.testable/events [{:file "bar.clj" 283 | :line 108 284 | :type :fail 285 | :expected '(= {:foo 4} {:foo 5}), 286 | :actual '(not (= {:foo 4} {:foo 5})) 287 | :message nil} 288 | {:file "foo.clj" 289 | :line 42 290 | :type :error 291 | :actual (Exception. "oh no")}]} 292 | (junit-xml/testcase->xml {}) 293 | xml->hiccup)))) 294 | 295 | (deftest suite->xml-test 296 | (is (match? {:tag :testsuite 297 | :attrs {:errors 0 298 | :package "foo" 299 | :tests 0 300 | :name "foo/bar" 301 | :time "0.000000" 302 | :hostname "localhost" 303 | :id 0 304 | :failures 0} 305 | :content 306 | [{:tag :properties} 307 | {:tag :system-out :content ()} 308 | {:tag :system-err}]} 309 | (junit-xml/suite->xml {} {:kaocha.testable/id :foo/bar} 0))) 310 | 311 | (is (match? {:tag :testsuite 312 | :attrs {:errors 0 313 | :package "" 314 | :tests 0 315 | :name "foo" 316 | :time "0.000000" 317 | :hostname "localhost" 318 | :id 0 319 | :failures 0} 320 | :content 321 | [{:tag :properties} 322 | {:tag :system-out :content ()} 323 | {:tag :system-err}]} 324 | (junit-xml/suite->xml {} {:kaocha.testable/id :foo} 0))) 325 | 326 | (is (match? {:tag :testsuite 327 | :attrs {:errors 0 328 | :package "" 329 | :tests 0 330 | :name "foo" 331 | :time "0.000000" 332 | :hostname "localhost" 333 | :id 0 334 | :failures 0} 335 | :content 336 | [{:tag :properties} 337 | {:tag :system-out} 338 | {:tag :system-err}]} 339 | (junit-xml/suite->xml {:no-system-out? true} {:kaocha.testable/id :foo} 0)))) 340 | 341 | (deftest junit-xml-plugin 342 | (testing "config" 343 | (is (= {:kaocha/cli-options {:junit-xml-file "my-out-file.xml"} 344 | :kaocha.plugin.junit-xml/target-file "my-out-file.xml"} 345 | (let [chain (plugin/load-all [:kaocha.plugin/junit-xml])] 346 | (plugin/run-hook* chain :kaocha.hooks/config {:kaocha/cli-options {:junit-xml-file "my-out-file.xml"}})))) 347 | 348 | (testing "does nothing if junit-xml-file is not present" 349 | (is (= {:foo :bar} 350 | (let [chain (plugin/load-all [:kaocha.plugin/junit-xml])] 351 | (plugin/run-hook* chain :kaocha.hooks/config {:foo :bar})))))) 352 | 353 | (testing "post-run" 354 | (let [outfile (Files/createTempFile (namespace `_) ".xml" (make-array FileAttribute 0)) 355 | chain (plugin/load-all [:kaocha.plugin/junit-xml]) 356 | result {:kaocha.plugin.junit-xml/target-file (str outfile) 357 | :kaocha.result/tests [{:kaocha.testable/id :my-test-type 358 | :kaocha.testable/type ::foo 359 | :kaocha.result/count 1 360 | :kaocha.result/fail 0 361 | :kaocha.result/pass 1 362 | :kaocha.result/error 0 363 | :kaocha.plugin.profiling/start (java.time.Instant/parse "2007-12-03T10:15:30.00Z") 364 | :kaocha.plugin.profiling/duration 12345}]}] 365 | (plugin/run-hook* chain :kaocha.hooks/post-run result) 366 | (is (= [:testsuites 367 | [:testsuite {:errors "0" 368 | :tests "1" 369 | :name "my-test-type" 370 | :package "" 371 | :time "0.000012" 372 | :hostname "localhost" 373 | :id "0" 374 | :timestamp "2007-12-03T10:15:30" 375 | :failures "0"} 376 | [:properties] 377 | [:system-out] 378 | [:system-err]]] 379 | (xml->hiccup (clojure.xml/parse (io/file (str outfile)) 380 | (fn [s ch] 381 | (let [parser (.. SAXParserFactory (newInstance) (newSAXParser))] 382 | ;; avoid reflection warnings 383 | (.parse ^SAXParser parser 384 | ^java.io.File s 385 | ^org.xml.sax.helpers.DefaultHandler ch)))))))))) 386 | 387 | (deftest valid-xml-test 388 | (testing "the output conforms to the JUnit.xsd schema" 389 | (is (= {:valid? true} 390 | (-> {:tests [{:test-paths ["fixtures/kaocha-demo"] 391 | :ns-patterns [".*"]}] 392 | :kaocha.plugin.randomize/randomize? false 393 | :color? false 394 | :reporter identity} 395 | repl/config 396 | api/run 397 | junit-xml/result->xml 398 | (xml/validate (io/resource "kaocha/junit_xml/JUnit.xsd"))))))) 399 | --------------------------------------------------------------------------------