├── .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 "" (name (:tag e)) ">")))
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 | [](https://github.com/lambdaisland/kaocha-junit-xml/actions/workflows/main.yml) [](https://cljdoc.org/d/lambdaisland/kaocha-junit-xml) [](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 |
--------------------------------------------------------------------------------