diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d009edc..ac166ae 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -18,5 +18,11 @@ jobs: run: npm install - name: Build run: npx tsc - - name: Convert - run: npx junit-to-ctrf test.xml + - name: Run against junit + run: npm run run:junit + - name: Run against minitest + run: npm run run:minitest + - name: Run against surefire + run: npm run run:surefire + - name: Run against glob + run: npm run run:glob diff --git a/README.md b/README.md index c05d14b..48a98d0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ Explore more integrations npx junit-to-ctrf path/to/junit.xml ``` +or glob pattern: + +```sh +npx junit-to-ctrf "test-results/**/*.xml" +``` + ## Options `-o`, `--output` : Output directory and filename for the CTRF report. If not provided, defaults to ctrf/ctrf-report.json. @@ -49,7 +55,7 @@ npx junit-to-ctrf path/to/junit.xml Convert a JUnit XML report to the default CTRF report location (ctrf/ctrf-report.json): ```sh -npx junit-to-ctrf path/to/junit.xml +npx junit-to-ctrf "test-results/**/*.xml" ``` ### Specify Output File @@ -57,7 +63,7 @@ npx junit-to-ctrf path/to/junit.xml Convert a JUnit XML report to a specified output file: ```sh -npx junit-to-ctrf path/to/junit.xml -o path/to/output/ctrf-report.json +npx junit-to-ctrf "test-results/**/*.xml" -o ctrf/combined-report.json ``` ### Include Tool Name diff --git a/package-lock.json b/package-lock.json index d1a47f3..eeb1cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "junit-to-ctrf", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "junit-to-ctrf", - "version": "0.0.8", + "version": "0.0.9", "license": "MIT", "dependencies": { + "ctrf": "^0.0.12", "fs-extra": "^11.2.0", + "glob": "^11.0.1", "typescript": "^5.4.5", "xml2js": "^0.6.2", "yargs": "^17.7.2" @@ -25,6 +27,95 @@ "typescript": "^5.4.5" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -99,6 +190,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -128,6 +232,37 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ctrf": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/ctrf/-/ctrf-0.0.12.tgz", + "integrity": "sha512-FLGO/5C6f4tKd5qH/8SF9nvgaqt8I57LzDCJBWuBzj/3c4cDlh4Kt3eA89ZqJxgNt9nHQ1jYAwo6BK19ThzQwQ==", + "dependencies": { + "glob": "^11.0.1", + "typescript": "^5.4.5", + "yargs": "^17.7.2" + }, + "bin": { + "ctrf": "dist/cli.js" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -141,6 +276,21 @@ "node": ">=6" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -162,6 +312,28 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -175,6 +347,25 @@ "node": ">=8" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -186,6 +377,64 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -199,6 +448,36 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -212,6 +491,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -223,11 +516,22 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -250,6 +554,20 @@ "node": ">= 10.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -266,6 +584,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -321,6 +656,64 @@ } }, "dependencies": { + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@types/fs-extra": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", @@ -386,6 +779,19 @@ "color-convert": "^2.0.1" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -409,6 +815,31 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "ctrf": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/ctrf/-/ctrf-0.0.12.tgz", + "integrity": "sha512-FLGO/5C6f4tKd5qH/8SF9nvgaqt8I57LzDCJBWuBzj/3c4cDlh4Kt3eA89ZqJxgNt9nHQ1jYAwo6BK19ThzQwQ==", + "requires": { + "glob": "^11.0.1", + "typescript": "^5.4.5", + "yargs": "^17.7.2" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -419,6 +850,15 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, "fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -434,6 +874,19 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -444,6 +897,19 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -453,6 +919,43 @@ "universalify": "^2.0.0" } }, + "lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==" + }, + "minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -463,6 +966,24 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -473,6 +994,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -481,11 +1012,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" }, "undici-types": { "version": "5.26.5", @@ -498,6 +1036,14 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -508,6 +1054,16 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index bc7ca02..c055bd2 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,17 @@ { "name": "junit-to-ctrf", - "version": "0.0.8", + "version": "0.0.9", "description": "Convert JUnit XML reports to CTRF JSON", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "npx tsc", - "run:junit": "node dist/cli.js test-junit.xml", - "run:minitest": "node dist/cli.js test-minitest-junit.xml", - "run:surefire": "node dist/cli.js test-surefire.xml", - "test": "echo \"Error: no test specified\" && exit 1" + "run:junit": "node dist/cli.js reports/test-junit.xml --output reports/test-junit-ctrf.json", + "run:minitest": "node dist/cli.js reports/test-minitest-junit.xml --output reports/test-minitest-junit-ctrf.json", + "run:surefire": "node dist/cli.js reports/test-surefire.xml --output reports/test-surefire-ctrf.json", + "run:glob": "node dist/cli.js \"reports/*.xml\" --output reports/test-glob-ctrf.json", + "test": "jest", + "test:e2e": "jest --config jest.config.js" }, "bin": { "junit-to-ctrf": "./dist/cli.js" @@ -22,7 +25,9 @@ "homepage": "https://ctrf.io", "license": "MIT", "dependencies": { + "ctrf": "^0.0.12", "fs-extra": "^11.2.0", + "glob": "^11.0.1", "typescript": "^5.4.5", "xml2js": "^0.6.2", "yargs": "^17.7.2" diff --git a/reports/not-junit.xml b/reports/not-junit.xml new file mode 100644 index 0000000..830ff28 --- /dev/null +++ b/reports/not-junit.xml @@ -0,0 +1,19 @@ + + + + Stephen King + The Shining + Horror + 9.99 + 1977-01-28 + A family becomes caretakers of an isolated hotel for the winter where a sinister presence influences the father. + + + Jane Austen + Pride and Prejudice + Romance + 7.99 + 1813-01-28 + The story follows the main character Elizabeth Bennet as she deals with issues of manners, upbringing, morality, education, and marriage. + + \ No newline at end of file diff --git a/test-junit.xml b/reports/test-junit.xml similarity index 100% rename from test-junit.xml rename to reports/test-junit.xml diff --git a/test-minitest-junit.xml b/reports/test-minitest-junit.xml similarity index 100% rename from test-minitest-junit.xml rename to reports/test-minitest-junit.xml diff --git a/test-surefire.xml b/reports/test-surefire.xml similarity index 100% rename from test-surefire.xml rename to reports/test-surefire.xml diff --git a/src/cli.ts b/src/cli.ts index 395600d..b86a092 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,23 +2,24 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { convertJUnitToCTRF } from './convert'; +import { convertJUnitToCTRFReport } from './convert'; yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') .command( - '$0 ', - 'Convert JUnit XML report to CTRF', + '$0 ', + 'Convert JUnit XML report(s) to CTRF', (yargs) => { return yargs - .positional('path', { - describe: 'Path to the JUnit XML file', - type: 'string', - demandOption: true, - }) + .positional('pattern', { + describe: 'Glob pattern to match JUnit XML files (e.g., "test-results/**/*.xml")', + type: 'string', + demandOption: true, + }) .option('output', { alias: 'o', type: 'string', + default: 'ctrf/ctrf-report.json', description: 'Output directory and filename for the CTRF report', }) .option('tool', { @@ -40,8 +41,8 @@ yargs(hideBin(process.argv)) }, async (argv) => { try { - const { path, output, tool, env, useSuiteName } = argv; - await convertJUnitToCTRF(path as string, output as string, tool as string, env as string[], useSuiteName); + const { pattern, output, tool, env, useSuiteName } = argv; + await convertJUnitToCTRFReport(pattern as string, { outputPath: output as string, toolName: tool as string, envProps: env as string[], useSuiteName: useSuiteName as boolean, log: true }); console.log('Conversion completed successfully.'); } catch (error: any) { console.error('Error:', error.message); diff --git a/src/convert.ts b/src/convert.ts index 3630b88..32cff40 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -1,89 +1,48 @@ import fs from 'fs-extra'; -import path from 'path'; -import xml2js from 'xml2js'; import { CtrfReport, CtrfTest, Tool } from '../types/ctrf'; +import { readJUnitReportsFromGlob } from './read'; +import path from 'path'; -interface JUnitTestCase { - suite: string; - classname: string; - name: string; - time: string; - hasFailure: boolean; - failureTrace: string | undefined, - failureMessage: string | undefined, - failureType: string | undefined, - hasError: boolean, - errorTrace: string | undefined, - errorMessage: string | undefined, - errorType: string | undefined, - file?: string, - lineno?: string, - skipped?: boolean; +interface ConvertOptions { + outputPath?: string; + toolName?: string; + envProps?: string[]; + useSuiteName?: boolean; + log?: boolean; } -async function parseJUnitReport(filePath: string): Promise { - console.log('Reading JUnit report file:', filePath); - const xml = await fs.readFile(filePath, 'utf-8'); - const result = await xml2js.parseStringPromise(xml); - const testCases: JUnitTestCase[] = []; - - const parseTestSuite = (suite: any, suiteName: string) => { - if (suite.testcase) { - suite.testcase.forEach((testCase: any) => { - const { classname, file, lineno, name, time } = testCase.$; - - const hasFailure = testCase.failure !== undefined; - const failureTrace = hasFailure ? (testCase.failure[0]?._ || '') : undefined; - const failureMessage = hasFailure ? (testCase.failure[0]?.$?.message || '') : undefined; - const failureType = hasFailure ? (testCase.failure[0]?.$?.type || '') : undefined; - - const hasError = testCase.error !== undefined; - const errorTrace = hasError ? (testCase.error[0]?._ || '') : undefined; - const errorMessage = hasError ? (testCase.error[0]?.$?.message || '') : undefined; - const errorType = hasError ? (testCase.error[0]?.$?.type || '') : undefined; - - const skipped = testCase.skipped !== undefined; - testCases.push({ - suite: suiteName, - classname, - name, - time, - file, - lineno, - hasFailure, - failureTrace, - failureMessage, - failureType, - hasError, - errorTrace, - errorMessage, - errorType, - skipped, - }); - }); - } - if (suite.testsuite) { - suite.testsuite.forEach((nestedSuite: any) => { - const nestedSuiteName = nestedSuite.$.name || suiteName; - parseTestSuite(nestedSuite, nestedSuiteName); - }); - } - }; +/** + * Convert JUnit XML report(s) to CTRF + * @param pattern - Path to JUnit XML file or glob pattern + * @param options - Optional options for the conversion + * @returns Promise that resolves when the conversion is complete + */ +export async function convertJUnitToCTRFReport( + pattern: string, + options: ConvertOptions = {} +): Promise { + const { outputPath, toolName, envProps, useSuiteName } = options; + const testCases = await readJUnitReportsFromGlob(pattern, { log: options.log }); + const envPropsObj = envProps ? Object.fromEntries(envProps.map(prop => prop.split('='))) : {}; - if (result.testsuites && result.testsuites.testsuite) { - result.testsuites.testsuite.forEach((suite: any) => { - const suiteName = suite.$.name; - parseTestSuite(suite, suiteName); - }); - } else if (result.testsuite) { - const suite = result.testsuite; - const suiteName = suite.$.name; - parseTestSuite(suite, suiteName); - } else { - console.warn('No test suites found in the provided file.'); + if (testCases.length === 0) { + console.warn('No test cases found in the provided path. No CTRF report generated.'); + return null; } + + if (options.log) console.log(`Converting ${testCases.length} test cases to CTRF format`); + const ctrfReport = createCTRFReport(testCases, toolName, envPropsObj, useSuiteName); + + if (outputPath) { + const finalOutputPath = path.resolve(outputPath) + const outputDir = path.dirname(finalOutputPath); + await fs.ensureDir(outputDir); - return testCases; + if (options.log) console.log('Writing CTRF report to:', finalOutputPath); + await fs.outputJson(finalOutputPath, ctrfReport, { spaces: 2 }); + if (options.log) console.log(`CTRF report written to ${outputPath}`); + } + return ctrfReport; } function convertToCTRFTest(testCase: JUnitTestCase, useSuiteName: boolean): CtrfTest { @@ -160,25 +119,4 @@ function createCTRFReport( } return report; -} - -export async function convertJUnitToCTRF( - junitPath: string, - outputPath?: string, - toolName?: string, - envProps?: string[], - useSuiteName?: boolean -): Promise { - const testCases = await parseJUnitReport(junitPath); - const envPropsObj = envProps ? Object.fromEntries(envProps.map(prop => prop.split('='))) : {}; - const ctrfReport = createCTRFReport(testCases, toolName, envPropsObj, useSuiteName); - - const defaultOutputPath = path.join('ctrf', 'ctrf-report.json'); - const finalOutputPath = path.resolve(outputPath || defaultOutputPath); - - const outputDir = path.dirname(finalOutputPath); - await fs.ensureDir(outputDir); - - console.log('Writing CTRF report to:', finalOutputPath); - await fs.outputJson(finalOutputPath, ctrfReport, { spaces: 2 }); -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..66452f9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { convertJUnitToCTRFReport } from './convert'; diff --git a/src/read.ts b/src/read.ts new file mode 100644 index 0000000..a934cac --- /dev/null +++ b/src/read.ts @@ -0,0 +1,96 @@ +import fs from 'fs-extra'; +import xml2js from 'xml2js'; +import { glob } from 'glob'; + +/** + * Read JUnit report files matching a glob pattern + * @param globPattern - The glob pattern to match JUnit XML files + * @returns Promise resolving to an array of all test cases from all matching files + */ +export async function readJUnitReportsFromGlob(globPattern: string, options: { log?: boolean } = {}): Promise { + if (options.log) console.log('Searching for JUnit reports matching pattern:', globPattern); + + const files = await glob(globPattern); + + if (files.length === 0) { + if (options.log) console.warn('No files found matching the pattern:', globPattern); + return []; + } + + if (options.log) console.log(`Found ${files.length} JUnit report files`); + + const allTestCasesPromises = files.map(file => parseJUnitReport(file, options)); + const testCasesArrays = await Promise.all(allTestCasesPromises); + + return testCasesArrays.flat(); +} + +/** + * Parse a JUnit XML report file and extract test cases + * @param filePath - Path to the JUnit XML file + * @returns Promise resolving to an array of test cases + */ +export async function parseJUnitReport(filePath: string, options: { log?: boolean } = {}): Promise { + if (options.log) console.log('Reading JUnit report file:', filePath); + const xml = await fs.readFile(filePath, 'utf-8'); + const result = await xml2js.parseStringPromise(xml); + const testCases: JUnitTestCase[] = []; + + const parseTestSuite = (suite: any, suiteName: string) => { + if (suite.testcase) { + suite.testcase.forEach((testCase: any) => { + const { classname, file, lineno, name, time } = testCase.$; + + const hasFailure = testCase.failure !== undefined; + const failureTrace = hasFailure ? (testCase.failure[0]?._ || '') : undefined; + const failureMessage = hasFailure ? (testCase.failure[0]?.$?.message || '') : undefined; + const failureType = hasFailure ? (testCase.failure[0]?.$?.type || '') : undefined; + + const hasError = testCase.error !== undefined; + const errorTrace = hasError ? (testCase.error[0]?._ || '') : undefined; + const errorMessage = hasError ? (testCase.error[0]?.$?.message || '') : undefined; + const errorType = hasError ? (testCase.error[0]?.$?.type || '') : undefined; + + const skipped = testCase.skipped !== undefined; + testCases.push({ + suite: suiteName, + classname, + name, + time, + file, + lineno, + hasFailure, + failureTrace, + failureMessage, + failureType, + hasError, + errorTrace, + errorMessage, + errorType, + skipped, + }); + }); + } + if (suite.testsuite) { + suite.testsuite.forEach((nestedSuite: any) => { + const nestedSuiteName = nestedSuite.$.name || suiteName; + parseTestSuite(nestedSuite, nestedSuiteName); + }); + } + }; + + if (result.testsuites && result.testsuites.testsuite) { + result.testsuites.testsuite.forEach((suite: any) => { + const suiteName = suite.$.name; + parseTestSuite(suite, suiteName); + }); + } else if (result.testsuite) { + const suite = result.testsuite; + const suiteName = suite.$.name; + parseTestSuite(suite, suiteName); + } else { + if (options.log) console.warn('No test suites found in the provided file.'); + } + + return testCases; +} \ No newline at end of file diff --git a/types/junit.d.ts b/types/junit.d.ts new file mode 100644 index 0000000..fa712ed --- /dev/null +++ b/types/junit.d.ts @@ -0,0 +1,17 @@ +interface JUnitTestCase { + suite: string; + classname: string; + name: string; + time: string; + hasFailure: boolean; + failureTrace: string | undefined, + failureMessage: string | undefined, + failureType: string | undefined, + hasError: boolean, + errorTrace: string | undefined, + errorMessage: string | undefined, + errorType: string | undefined, + file?: string, + lineno?: string, + skipped?: boolean; + } \ No newline at end of file