diff --git a/README.md b/README.md index efa2dc0..3aed457 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The tool can upload test case results from JUnit XML and Playwright JSON files t ### Requirements -Node.js version 18.0.0 or higher. +Node.js version 20.0.0 or higher. ### Via NPX @@ -59,9 +59,9 @@ QAS_URL=https://qas.eu1.qasphere.com # QAS_URL=https://qas.eu1.qasphere.com ``` -## Commands: `junit-upload`, `playwright-json-upload` +## Commands: `junit-upload`, `playwright-json-upload`, `xcresult-upload` -The `junit-upload` and `playwright-json-upload` commands upload test results from JUnit XML and Playwright JSON reports to QA Sphere respectively. +The `junit-upload`, `playwright-json-upload` and `xcresult-upload` commands upload test results from JUnit XML, Playwright JSON and Xcode reports to QA Sphere respectively. There are two modes for uploading results using the commands: @@ -71,10 +71,10 @@ There are two modes for uploading results using the commands: ### Options - `-r`/`--run-url` - Upload results to an existing test run -- `--project-code`, `--run-name`, `--create-tcases` - Create a new test run and upload results to it +- `--project-code`, `--run-name`, `--create-tcases` - Create a new test run and upload results to it (if `-r`/`--run-url` is not specified) - `--project-code` - Project code for creating new test run. It can also be auto detected from test case markers in the results, but this is not fully reliable, so it is recommended to specify the project code explicitly - `--run-name` - Optional name template for creating new test run. It supports `{env:VAR}`, `{YYYY}`, `{YY}`, `{MM}`, `{MMM}`, `{DD}`, `{HH}`, `{hh}`, `{mm}`, `{ss}`, `{AMPM}` placeholders (default: `Automated test run - {MMM} {DD}, {YYYY}, {hh}:{mm}:{ss} {AMPM}`) - - `--create-tcases` - Automatically create test cases in QA Sphere for results that don't have valid test case markers. A mapping file (`qasphere-automapping-YYYYMMDD-HHmmss.txt`) is generated showing the sequence numbers assigned to each new test case (default: `false`) + - `--create-tcases` - Automatically create test cases in QA Sphere for results that don't have valid test case markers. A mapping file (`qasphere-automapping-YYYYMMDD-HHmmss.txt`) is generated showing the sequence numbers assigned to each new test case, use it to update your test cases to include the markers in the name, for future uploads (default: `false`) - `--attachments` - Try to detect and upload any attachments with the test result - `--force` - Ignore API request errors, invalid test cases, or attachments - `--ignore-unmatched` - Suppress individual unmatched test messages, show summary only @@ -97,13 +97,11 @@ The `--run-name` option supports the following placeholders: - `{mm}` - 2-digit minute - `{ss}` - 2-digit second -**Note:** The `--run-name` option is only used when creating new test runs (i.e., when `--run-url` is not specified). - ### Usage Examples Ensure the required environment variables are defined before running these commands. -**Note:** The following examples use `junit-upload`, but you can replace it with `playwright-json-upload` and adjust the file extension from `.xml` to `.json` to upload Playwright JSON reports instead. +**Note:** The following examples use `junit-upload`, but you can replace it with `playwright-json-upload` and `xcresult-upload` to upload Playwright JSON and Xcode reports. 1. Upload to an existing test run: @@ -218,6 +216,22 @@ Playwright JSON reports support two methods for referencing test cases (checked 2. **Test Case Marker in Name** - Include the `PROJECT-SEQUENCE` marker in the test name (same format as JUnit XML) +### XCode Reports + +Test case names in the XCode reports must include a QA Sphere test case marker in the format `PROJECT_SEQUENCE`: + +- **PROJECT** - Your QA Sphere project code +- **SEQUENCE** - Test case sequence number (minimum 3 digits, zero-padded if needed) + +**Examples:** + +- `PRJ_002_login_with_valid_credentials` +- `login_with_valid_credentials_PRJ_1312` + +## Other Requirements + +The `xcresult-upload` command will automatically invoke `xcrun xcresulttool`, if the SQLite database is not found inside the `.xcresult` bundle. This requires **Xcode Command Line Tools** to be installed. See [Apple Developer documentation](https://developer.apple.com/xcode/resources/) for installation instructions. Alternatively, having the full Xcode application installed also provides these tools. + ## Development (for those who want to contribute to the tool) 1. Install and build: `npm install && npm run build && npm link` @@ -225,4 +239,4 @@ Playwright JSON reports support two methods for referencing test cases (checked 3. Configure `.qaspherecli` with credentials 4. Test with sample reports from [bistro-e2e](https://github.com/Hypersequent/bistro-e2e) -Tests: `npm test` (Vitest) and `cd mnode-test && ./docker-test.sh` (Node.js 18+ compatibility) +Tests: `npm test` (Vitest) and `cd mnode-test && ./docker-test.sh` (Node.js 20+ compatibility) diff --git a/mnode-test/docker-test.sh b/mnode-test/docker-test.sh index 49c2aec..2ca19e2 100755 --- a/mnode-test/docker-test.sh +++ b/mnode-test/docker-test.sh @@ -52,7 +52,7 @@ EXPECTED_VERSION=$(node -p "require('${PROJECT_DIR}/package.json').version") echo "Expected version: ${EXPECTED_VERSION}" echo "" -NODE_VERSIONS=("18" "20" "22" "24") +NODE_VERSIONS=("20" "22" "24") # Get current user ID for fixing permissions on Linux FIX_PERMS="true" # Default no-op command @@ -73,16 +73,16 @@ for VERSION in "${NODE_VERSIONS[@]}"; do set -e echo '→ Installing qas-cli globally...' npm install -g ${PACKAGE_FILE} - + echo '→ Testing qasphere --version' qasphere --version - + VERSION_OUTPUT=\$(qasphere --version) if [ \"\$VERSION_OUTPUT\" != \"${EXPECTED_VERSION}\" ]; then echo \"Error: Version mismatch! Expected ${EXPECTED_VERSION}, got \$VERSION_OUTPUT\" exit 1 fi - + echo '✓ Global installation works correctly' " @@ -94,22 +94,22 @@ for VERSION in "${NODE_VERSIONS[@]}"; do sh -c " set -e echo '→ Testing with npx...' - + # Install the package locally to test npx npm init -y > /dev/null 2>&1 npm install ${PACKAGE_FILE} - + echo '→ Running npx qas-cli --version' npx qas-cli --version - + VERSION_OUTPUT=\$(npx qas-cli --version) if [ \"\$VERSION_OUTPUT\" != \"${EXPECTED_VERSION}\" ]; then echo \"Error: Version mismatch! Expected ${EXPECTED_VERSION}, got \$VERSION_OUTPUT\" exit 1 fi - + echo '✓ npx execution works correctly' - + # Fix ownership on Linux ${FIX_PERMS} || true " @@ -120,4 +120,4 @@ done echo "========================================" echo "All Node versions passed successfully!" -echo "========================================" \ No newline at end of file +echo "========================================" diff --git a/package-lock.json b/package-lock.json index 7674e76..e8c5c6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.4.4", "license": "ISC", "dependencies": { + "better-sqlite3": "^12.6.2", "chalk": "^5.4.1", "dotenv": "^16.5.0", "escape-html": "^1.0.3", + "fzstd": "^0.1.1", "semver": "^7.7.1", "strip-ansi": "^7.1.2", "xml2js": "^0.6.2", @@ -23,6 +25,7 @@ }, "devDependencies": { "@eslint/js": "^9.25.1", + "@types/better-sqlite3": "^7.6.13", "@types/escape-html": "^1.0.4", "@types/node": "^20.17.32", "@types/semver": "^7.7.0", @@ -39,6 +42,9 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.31.1", "vitest": "^3.1.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@bundled-es-modules/cookie": { @@ -470,10 +476,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -497,12 +504,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -511,19 +519,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -567,30 +580,36 @@ } }, "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1072,6 +1091,16 @@ "win32" ] }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1094,7 +1123,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "20.17.32", @@ -1280,10 +1310,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1462,10 +1493,11 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1572,11 +1604,66 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1594,6 +1681,30 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1648,6 +1759,12 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -1844,6 +1961,21 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1853,12 +1985,30 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -1875,6 +2025,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -1960,32 +2119,32 @@ } }, "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2036,10 +2195,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2080,10 +2240,11 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2092,14 +2253,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2109,10 +2271,11 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2137,6 +2300,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2202,6 +2366,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -2278,6 +2451,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2325,6 +2504,12 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2339,6 +2524,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fzstd": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/fzstd/-/fzstd-0.1.1.tgz", + "integrity": "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==", + "license": "MIT" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2373,6 +2564,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2452,6 +2649,26 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2486,6 +2703,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2550,10 +2779,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2932,6 +3162,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2944,6 +3186,21 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3021,12 +3278,30 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -3056,6 +3331,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3235,6 +3519,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3272,6 +3582,16 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3307,6 +3627,44 @@ } ] }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3442,6 +3800,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -3497,6 +3875,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -3576,6 +3999,15 @@ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -3672,6 +4104,34 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3804,6 +4264,18 @@ "typescript": ">=4.8.4" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3897,11 +4369,18 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4166,6 +4645,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/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 432ae80..cde5c3f 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,12 @@ "url": "https://github.com/Hypersequent/qas-cli/issues" }, "homepage": "https://github.com/Hypersequent/qas-cli#readme", + "engines": { + "node": ">=20" + }, "devDependencies": { "@eslint/js": "^9.25.1", + "@types/better-sqlite3": "^7.6.13", "@types/escape-html": "^1.0.4", "@types/node": "^20.17.32", "@types/semver": "^7.7.0", @@ -58,9 +62,11 @@ "vitest": "^3.1.2" }, "dependencies": { + "better-sqlite3": "^12.6.2", "chalk": "^5.4.1", "dotenv": "^16.5.0", "escape-html": "^1.0.3", + "fzstd": "^0.1.1", "semver": "^7.7.1", "strip-ansi": "^7.1.2", "xml2js": "^0.6.2", diff --git a/src/commands/main.ts b/src/commands/main.ts index 98ed8a3..01c5e4f 100644 --- a/src/commands/main.ts +++ b/src/commands/main.ts @@ -13,6 +13,7 @@ Required variables: ${qasEnvs.join(', ')} ) .command(new ResultUploadCommandModule('junit-upload')) .command(new ResultUploadCommandModule('playwright-json-upload')) + .command(new ResultUploadCommandModule('xcresult-upload')) .demandCommand(1, '') .help('h') .alias('h', 'help') diff --git a/src/commands/resultUpload.ts b/src/commands/resultUpload.ts index 78b6687..28d5dde 100644 --- a/src/commands/resultUpload.ts +++ b/src/commands/resultUpload.ts @@ -10,11 +10,13 @@ import { const commandTypeDisplayStrings: Record = { 'junit-upload': 'JUnit XML', 'playwright-json-upload': 'Playwright JSON', + 'xcresult-upload': 'Xcode Result Bundle', } const commandTypeFileExtensions: Record = { 'junit-upload': 'xml', 'playwright-json-upload': 'json', + 'xcresult-upload': 'xcresult', } export class ResultUploadCommandModule implements CommandModule { @@ -25,7 +27,7 @@ export class ResultUploadCommandModule implements CommandModule { @@ -48,7 +50,7 @@ export class ResultUploadCommandModule implements CommandModule { - test('Should parse comprehensive test XML without exceptions', async () => { - const xmlPath = `${xmlBasePath}/comprehensive-test.xml` - const xmlContent = await readFile(xmlPath, 'utf8') + let tempXmlFile: string | null = null + + afterEach(() => { + if (tempXmlFile) { + deleteTempFile(tempXmlFile) + tempXmlFile = null + } + }) + test('Should parse comprehensive test XML without exceptions', async () => { // This should not throw any exceptions - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/comprehensive-test.xml`, xmlBasePath, { skipStdout: 'never', skipStderr: 'never', }) @@ -52,10 +58,7 @@ describe('Junit XML parsing', () => { }) test('Should handle all failure/error/skipped element variations', async () => { - const xmlPath = `${xmlBasePath}/comprehensive-test.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/comprehensive-test.xml`, xmlBasePath, { skipStdout: 'never', skipStderr: 'never', }) @@ -83,10 +86,7 @@ describe('Junit XML parsing', () => { }) test('Should handle empty and similar empty tags', async () => { - const xmlPath = `${xmlBasePath}/empty-system-err.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/empty-system-err.xml`, xmlBasePath, { skipStdout: 'never', skipStderr: 'never', }) @@ -100,13 +100,14 @@ describe('Junit XML parsing', () => { }) test('Should handle Jest failure without type attribute', async () => { - const xmlPath = `${xmlBasePath}/jest-failure-type-missing.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { - skipStdout: 'never', - skipStderr: 'never', - }) + const testcases = await parseJUnitXml( + `${xmlBasePath}/jest-failure-type-missing.xml`, + xmlBasePath, + { + skipStdout: 'never', + skipStderr: 'never', + } + ) expect(testcases).toHaveLength(3) // Verify test result types @@ -129,10 +130,7 @@ describe('Junit XML parsing', () => { }) test('Should extract attachments from failure/error message attributes (WebDriverIO style)', async () => { - const xmlPath = `${xmlBasePath}/webdriverio-real.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/webdriverio-real.xml`, xmlBasePath, { skipStdout: 'never', skipStderr: 'never', }) @@ -151,10 +149,7 @@ describe('Junit XML parsing', () => { }) test('Should include stdout/stderr when skipStdout and skipStderr are set to "never"', async () => { - const xmlPath = `${xmlBasePath}/empty-system-err.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/empty-system-err.xml`, xmlBasePath, { skipStdout: 'never', skipStderr: 'never', }) @@ -166,10 +161,7 @@ describe('Junit XML parsing', () => { }) test('Should skip stdout for passed tests when skipStdout is set to "on-success"', async () => { - const xmlPath = `${xmlBasePath}/empty-system-err.xml` - const xmlContent = await readFile(xmlPath, 'utf8') - - const testcases = await parseJUnitXml(xmlContent, xmlBasePath, { + const testcases = await parseJUnitXml(`${xmlBasePath}/empty-system-err.xml`, xmlBasePath, { skipStdout: 'on-success', skipStderr: 'never', }) @@ -182,7 +174,8 @@ describe('Junit XML parsing', () => { }) test('Should skip stderr for passed tests when skipStderr is set to "on-success"', async () => { - const xml = ` + tempXmlFile = createTempFile( + ` @@ -190,9 +183,11 @@ describe('Junit XML parsing', () => { stderr content -` +`, + 'xml' + ) - const testcases = await parseJUnitXml(xml, xmlBasePath, { + const testcases = await parseJUnitXml(tempXmlFile, xmlBasePath, { skipStdout: 'never', skipStderr: 'on-success', }) @@ -206,7 +201,8 @@ describe('Junit XML parsing', () => { }) test('Should include stdout/stderr for failed tests even when skip options are set to "on-success"', async () => { - const xml = ` + tempXmlFile = createTempFile( + ` @@ -215,9 +211,11 @@ describe('Junit XML parsing', () => { stderr from failed test -` +`, + 'xml' + ) - const testcases = await parseJUnitXml(xml, xmlBasePath, { + const testcases = await parseJUnitXml(tempXmlFile, xmlBasePath, { skipStdout: 'on-success', skipStderr: 'on-success', }) @@ -232,7 +230,8 @@ describe('Junit XML parsing', () => { }) test('Should skip both stdout and stderr for passed tests when both skip options are set to "on-success"', async () => { - const xml = ` + tempXmlFile = createTempFile( + ` @@ -240,9 +239,11 @@ describe('Junit XML parsing', () => { stderr content -` +`, + 'xml' + ) - const testcases = await parseJUnitXml(xml, xmlBasePath, { + const testcases = await parseJUnitXml(tempXmlFile, xmlBasePath, { skipStdout: 'on-success', skipStderr: 'on-success', }) diff --git a/src/tests/playwright-json-parsing.spec.ts b/src/tests/playwright-json-parsing.spec.ts index 89f2184..9b955f2 100644 --- a/src/tests/playwright-json-parsing.spec.ts +++ b/src/tests/playwright-json-parsing.spec.ts @@ -1,19 +1,30 @@ +import { afterEach } from 'node:test' import { expect, test, describe } from 'vitest' import { parsePlaywrightJson } from '../utils/result-upload/playwrightJsonParser' -import { readFile } from 'fs/promises' +import { createTempFile, deleteTempFile } from './utils' const playwrightJsonBasePath = './src/tests/fixtures/playwright-json' describe('Playwright JSON parsing', () => { - test('Should parse comprehensive test JSON without exceptions', async () => { - const jsonPath = `${playwrightJsonBasePath}/comprehensive-test.json` - const jsonContent = await readFile(jsonPath, 'utf8') + let tempJsonFile: string | null = null + + afterEach(() => { + if (tempJsonFile) { + deleteTempFile(tempJsonFile) + tempJsonFile = null + } + }) + test('Should parse comprehensive test JSON without exceptions', async () => { // This should not throw any exceptions - const testcases = await parsePlaywrightJson(jsonContent, '', { - skipStdout: 'never', - skipStderr: 'never', - }) + const testcases = await parsePlaywrightJson( + `${playwrightJsonBasePath}/comprehensive-test.json`, + '', + { + skipStdout: 'never', + skipStderr: 'never', + } + ) // Verify that we got the expected number of test cases expect(testcases).toHaveLength(12) @@ -50,10 +61,7 @@ describe('Playwright JSON parsing', () => { }) test('Should handle empty test suite', async () => { - const jsonPath = `${playwrightJsonBasePath}/empty-tsuite.json` - const jsonContent = await readFile(jsonPath, 'utf8') - - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(`${playwrightJsonBasePath}/empty-tsuite.json`, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -65,50 +73,53 @@ describe('Playwright JSON parsing', () => { }) test('Should use last result when there are retries', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'retry.spec.ts', - specs: [ - { - title: 'Flaky test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'failed', - errors: [{ message: 'First attempt failed' }], - stdout: [], - stderr: [], - retry: 0, - duration: 1300, - attachments: [], - }, - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 1, - duration: 1200, - attachments: [], - }, - ], - status: 'flaky', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'retry.spec.ts', + specs: [ + { + title: 'Flaky test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [{ message: 'First attempt failed' }], + stdout: [], + stderr: [], + retry: 0, + duration: 1300, + attachments: [], + }, + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 1, + duration: 1200, + attachments: [], + }, + ], + status: 'flaky', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -121,71 +132,74 @@ describe('Playwright JSON parsing', () => { }) test('Should handle nested suites correctly', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'parent.spec.ts', - specs: [ - { - title: 'Parent test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [ - { - title: 'Nested Suite', - specs: [ - { - title: 'Nested test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 5100, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'parent.spec.ts', + specs: [ + { + title: 'Parent test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [ + { + title: 'Nested Suite', + specs: [ + { + title: 'Nested test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 5100, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -205,54 +219,57 @@ describe('Playwright JSON parsing', () => { }) test('Should strip ANSI escape codes from errors and output', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'ansi.spec.ts', - specs: [ - { - title: 'Test with ANSI colors in error', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'failed', - errors: [ - { - message: - '\x1b[31mError: Test failed\x1b[0m\n\x1b[90m at Object.test\x1b[0m', - }, - ], - stdout: [ - { - text: '\x1b[32m✓\x1b[0m Test started\n\x1b[33mWarning:\x1b[0m Something happened', - }, - ], - stderr: [ - { - text: '\x1b[31mError output\x1b[0m\n\x1b[90mStack trace\x1b[0m', - }, - ], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'unexpected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'ansi.spec.ts', + specs: [ + { + title: 'Test with ANSI colors in error', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [ + { + message: + '\x1b[31mError: Test failed\x1b[0m\n\x1b[90m at Object.test\x1b[0m', + }, + ], + stdout: [ + { + text: '\x1b[32m✓\x1b[0m Test started\n\x1b[33mWarning:\x1b[0m Something happened', + }, + ], + stderr: [ + { + text: '\x1b[31mError output\x1b[0m\n\x1b[90mStack trace\x1b[0m', + }, + ], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'unexpected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -273,97 +290,100 @@ describe('Playwright JSON parsing', () => { }) test('Should prefix test case marker from annotations to test name', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'annotation.spec.ts', - specs: [ - { - title: 'User login test', - tags: [], - tests: [ - { - annotations: [ - { - type: 'test case', - description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/123', - }, - ], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - { - title: 'Test without annotation', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - { - title: 'PRJ-456: Test with marker in name and annotation', - tags: [], - tests: [ - { - annotations: [ - { - type: 'Test Case', - description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/789', - }, - ], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'annotation.spec.ts', + specs: [ + { + title: 'User login test', + tags: [], + tests: [ + { + annotations: [ + { + type: 'test case', + description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/123', + }, + ], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + { + title: 'Test without annotation', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + { + title: 'PRJ-456: Test with marker in name and annotation', + tags: [], + tests: [ + { + annotations: [ + { + type: 'Test Case', + description: 'https://qas.eu1.qasphere.com/project/PRJ/tcase/789', + }, + ], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -380,125 +400,128 @@ describe('Playwright JSON parsing', () => { }) test('Should map test status correctly', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'status.spec.ts', - specs: [ - { - title: 'Expected test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - { - title: 'Unexpected test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'failed', - errors: [{ message: 'Test failed' }], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [ - { - name: 'screenshot', - contentType: 'image/png', - path: '../test-results/ui.cart-Test-cart-chromium/test-finished-1.png', - }, - ], - }, - ], - status: 'unexpected', - }, - ], - }, - { - title: 'Flaky test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'failed', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - { - status: 'passed', - errors: [], - stdout: [], - stderr: [], - retry: 1, - duration: 1000, - attachments: [], - }, - ], - status: 'flaky', - }, - ], - }, - { - title: 'Skipped test', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'skipped', - projectName: 'chromium', - results: [ - { - status: 'skipped', - errors: [], - stdout: [], - stderr: [], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'skipped', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'status.spec.ts', + specs: [ + { + title: 'Expected test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + { + title: 'Unexpected test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [{ message: 'Test failed' }], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [ + { + name: 'screenshot', + contentType: 'image/png', + path: '../test-results/ui.cart-Test-cart-chromium/test-finished-1.png', + }, + ], + }, + ], + status: 'unexpected', + }, + ], + }, + { + title: 'Flaky test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + { + status: 'passed', + errors: [], + stdout: [], + stderr: [], + retry: 1, + duration: 1000, + attachments: [], + }, + ], + status: 'flaky', + }, + ], + }, + { + title: 'Skipped test', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'skipped', + projectName: 'chromium', + results: [ + { + status: 'skipped', + errors: [], + stdout: [], + stderr: [], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'skipped', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -511,41 +534,44 @@ describe('Playwright JSON parsing', () => { }) test('Should include stdout/stderr when skipStdout and skipStderr are set to "never"', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'test.spec.ts', - specs: [ - { - title: 'Passed test with output', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [{ text: 'stdout content' }], - stderr: [{ text: 'stderr content' }], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'never', }) @@ -557,47 +583,50 @@ describe('Playwright JSON parsing', () => { }) test('Should skip stdout for passed tests when skipStdout is set to "on-success"', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'test.spec.ts', - specs: [ - { - title: 'Passed test with output', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [{ text: 'stdout content' }], - stderr: [{ text: 'stderr content' }], - retry: 0, - duration: 1000, - attachments: [ - { - name: 'screenshot', - contentType: 'image/png', - path: '../test-results/ui.cart-Test-cart-chromium/test-finished-1.png', - }, - ], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + duration: 1000, + attachments: [ + { + name: 'screenshot', + contentType: 'image/png', + path: '../test-results/ui.cart-Test-cart-chromium/test-finished-1.png', + }, + ], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'on-success', skipStderr: 'never', }) @@ -609,41 +638,44 @@ describe('Playwright JSON parsing', () => { }) test('Should skip stderr for passed tests when skipStderr is set to "on-success"', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'test.spec.ts', - specs: [ - { - title: 'Passed test with output', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [{ text: 'stdout content' }], - stderr: [{ text: 'stderr content' }], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'never', skipStderr: 'on-success', }) @@ -655,41 +687,44 @@ describe('Playwright JSON parsing', () => { }) test('Should include stdout/stderr for failed tests even when skip options are set to "on-success"', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'test.spec.ts', - specs: [ - { - title: 'Failed test with output', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'failed', - errors: [{ message: 'Test failed' }], - stdout: [{ text: 'stdout from failed test' }], - stderr: [{ text: 'stderr from failed test' }], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'unexpected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Failed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'failed', + errors: [{ message: 'Test failed' }], + stdout: [{ text: 'stdout from failed test' }], + stderr: [{ text: 'stderr from failed test' }], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'unexpected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'on-success', skipStderr: 'on-success', }) @@ -702,41 +737,44 @@ describe('Playwright JSON parsing', () => { }) test('Should skip both stdout and stderr for passed tests when both skip options are set to "on-success"', async () => { - const jsonContent = JSON.stringify({ - suites: [ - { - title: 'test.spec.ts', - specs: [ - { - title: 'Passed test with output', - tags: [], - tests: [ - { - annotations: [], - expectedStatus: 'passed', - projectName: 'chromium', - results: [ - { - status: 'passed', - errors: [], - stdout: [{ text: 'stdout content' }], - stderr: [{ text: 'stderr content' }], - retry: 0, - duration: 1000, - attachments: [], - }, - ], - status: 'expected', - }, - ], - }, - ], - suites: [], - }, - ], - }) + const tempJsonFile = createTempFile( + JSON.stringify({ + suites: [ + { + title: 'test.spec.ts', + specs: [ + { + title: 'Passed test with output', + tags: [], + tests: [ + { + annotations: [], + expectedStatus: 'passed', + projectName: 'chromium', + results: [ + { + status: 'passed', + errors: [], + stdout: [{ text: 'stdout content' }], + stderr: [{ text: 'stderr content' }], + retry: 0, + duration: 1000, + attachments: [], + }, + ], + status: 'expected', + }, + ], + }, + ], + suites: [], + }, + ], + }), + 'json' + ) - const testcases = await parsePlaywrightJson(jsonContent, '', { + const testcases = await parsePlaywrightJson(tempJsonFile, '', { skipStdout: 'on-success', skipStderr: 'on-success', }) diff --git a/src/tests/result-upload.spec.ts b/src/tests/result-upload.spec.ts index 3aee549..720166b 100644 --- a/src/tests/result-upload.spec.ts +++ b/src/tests/result-upload.spec.ts @@ -504,3 +504,17 @@ fileTypes.forEach((fileType) => { }) }) }) + +describe('Uploading XCode reports', () => { + const xcresultBasePath = './src/tests/fixtures/xcresult' + + test('Should successfully upload xcresult bundle with matching test cases', async () => { + const numFileUploadCalls = countFileUploadApiCalls() + const numResultUploadCalls = countResultUploadApiCalls() + + await run(`xcresult-upload -r ${runURL} ${xcresultBasePath}/Variety.xcresult`) + + expect(numFileUploadCalls()).toBe(0) + expect(numResultUploadCalls()).toBe(1) // 5 results total + }) +}) diff --git a/src/tests/utils.ts b/src/tests/utils.ts index caacea1..b557893 100644 --- a/src/tests/utils.ts +++ b/src/tests/utils.ts @@ -1,4 +1,8 @@ import { SetupServerApi } from 'msw/node' +import { randomBytes } from 'node:crypto' +import { unlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' export const countMockedApiCalls = ( server: SetupServerApi, @@ -21,3 +25,24 @@ export const countMockedApiCalls = ( }) return () => count } + +/** + * Creates a temp file with the provided content in the OS temp directory and returns its path. + * @param content Content to be written to the temp file + * @param extension Extension of the file + * @returns string Path of the created temp file + */ +export function createTempFile(content: string, extension: string) { + const randomName = `tmp-${randomBytes(8).toString('hex')}.${extension}` + const tmpPath = join(tmpdir(), randomName) + writeFileSync(tmpPath, content, { encoding: 'utf-8' }) + return tmpPath +} + +/** + * Deletes the file at the given path. + * @param filePath Path to the file to delete + */ +export function deleteTempFile(filePath: string) { + unlinkSync(filePath) +} diff --git a/src/tests/xcresult-parsing.spec.ts b/src/tests/xcresult-parsing.spec.ts new file mode 100644 index 0000000..68119ca --- /dev/null +++ b/src/tests/xcresult-parsing.spec.ts @@ -0,0 +1,71 @@ +import { expect, test, describe } from 'vitest' +import { parseXCResult } from '../utils/result-upload/xcresultSqliteParser' + +const xcresultBasePath = './src/tests/fixtures/xcresult' + +describe('XCResult parsing', () => { + test('Should correctly parse all test cases from xcresult bundle', async () => { + const testcases = await parseXCResult( + `${xcresultBasePath}/Variety.xcresult`, + xcresultBasePath, + { + skipStdout: 'never', + skipStderr: 'never', + } + ) + + // Verify total count + expect(testcases).toHaveLength(5) + + // Verify each test case has required properties + testcases.forEach((tc) => { + expect(tc).toHaveProperty('name') + expect(tc).toHaveProperty('folder') + expect(tc).toHaveProperty('status') + expect(tc).toHaveProperty('message') + expect(tc).toHaveProperty('timeTaken') + expect(tc).toHaveProperty('attachments') + expect(Array.isArray(tc.attachments)).toBe(true) + }) + + // Test case 1: Passed test (TEST_002) + const test1 = testcases.find((tc) => tc.name === 'test_TEST_002_AppLaunches') + expect(test1).toBeDefined() + expect(test1?.status).toBe('passed') + expect(test1?.timeTaken ? Math.round(test1.timeTaken) : test1?.timeTaken).toBe(9045) + expect(test1?.folder).toContain('BistroAppUITests') + + // Test case 2: Failed test with failure message (TEST_003) + const test2 = testcases.find((tc) => tc.name === 'test_TEST_003_MenuShowsPizzas') + expect(test2).toBeDefined() + expect(test2?.status).toBe('failed') + expect(test2?.timeTaken ? Math.round(test2.timeTaken) : test2?.timeTaken).toBe(9058) + expect(test2?.folder).toContain('BistroAppUITests') + expect(test2?.message).toContain('XCTAssertTrue failed') + + // Test case 3: Skipped test with skip reason (TEST_004) + const test3 = testcases.find((tc) => tc.name === 'test_TEST_004_NavigateToCart') + expect(test3).toBeDefined() + expect(test3?.status).toBe('skipped') + expect(test3?.timeTaken ? Math.round(test3.timeTaken) : test3?.timeTaken).toBe(7260) + expect(test3?.folder).toContain('BistroAppUITests') + expect(test3?.message).toContain('Skipped Reason') + expect(test3?.message).toContain('Test not ready yet') + + // Test case 4: Another passed test (TEST_005) + const test4 = testcases.find((tc) => tc.name === 'test_TEST_005_SwitchBetweenTabs') + expect(test4).toBeDefined() + expect(test4?.status).toBe('passed') + expect(test4?.timeTaken ? Math.round(test4.timeTaken) : test4?.timeTaken).toBe(24564) + expect(test4?.folder).toContain('BistroAppUITests') + + // Test case 5: Expected failure (blocked) with reason (TEST_006) + const test5 = testcases.find((tc) => tc.name === 'test_TEST_006_AddItemAndCheckout') + expect(test5).toBeDefined() + expect(test5?.status).toBe('blocked') + expect(test5?.timeTaken ? Math.round(test5.timeTaken) : test5?.timeTaken).toBe(24576) + expect(test5?.folder).toContain('BistroAppUITests') + expect(test5?.message).toContain('Expected Failure') + expect(test5?.message).toContain('should fail') + }) +}) diff --git a/src/utils/result-upload/ResultUploadCommandHandler.ts b/src/utils/result-upload/ResultUploadCommandHandler.ts index f4b90be..da3bebe 100644 --- a/src/utils/result-upload/ResultUploadCommandHandler.ts +++ b/src/utils/result-upload/ResultUploadCommandHandler.ts @@ -1,16 +1,23 @@ import { Arguments } from 'yargs' import chalk from 'chalk' -import { readFileSync, writeFileSync } from 'node:fs' +import { writeFileSync } from 'node:fs' import { dirname } from 'node:path' -import { getTCaseMarker, parseRunUrl, printErrorThenExit, processTemplate } from '../misc' +import { + getTCaseMarker, + parseRunUrl, + printError, + printErrorThenExit, + processTemplate, +} from '../misc' import { Api, createApi } from '../../api' import { TCase } from '../../api/schemas' import { TestCaseResult } from './types' import { ResultUploader } from './ResultUploader' -import { parseJUnitXml } from './junitXmlParser' -import { parsePlaywrightJson } from './playwrightJsonParser' +import { parseJUnitXml, printJUnitMissingMarkerGuidance } from './junitXmlParser' +import { parsePlaywrightJson, printPlaywrightMissingMarkerGuidance } from './playwrightJsonParser' +import { parseXCResult, printXCResultMissingMarkerGuidance } from './xcresultSqliteParser' -export type UploadCommandType = 'junit-upload' | 'playwright-json-upload' +export type UploadCommandType = 'junit-upload' | 'playwright-json-upload' | 'xcresult-upload' export type SkipOutputOption = 'on-success' | 'never' @@ -20,7 +27,7 @@ export interface ParserOptions { } export type Parser = ( - data: string, + filePath: string, attachmentBaseDirectory: string, options: ParserOptions ) => Promise @@ -59,9 +66,20 @@ const DEFAULT_PAGE_SIZE = 5000 export const DEFAULT_FOLDER_TITLE = 'cli-import' const DEFAULT_TCASE_TAGS = ['cli-import'] const DEFAULT_MAPPING_FILENAME_TEMPLATE = 'qasphere-automapping-{YYYY}{MM}{DD}-{HH}{mm}{ss}.txt' + const commandTypeParsers: Record = { 'junit-upload': parseJUnitXml, 'playwright-json-upload': parsePlaywrightJson, + 'xcresult-upload': parseXCResult, +} + +export const commandTypePrintMissingMarkerGuidance: Record< + UploadCommandType, + (projectCode: string, testCaseName: string) => void +> = { + 'junit-upload': printJUnitMissingMarkerGuidance, + 'playwright-json-upload': printPlaywrightMissingMarkerGuidance, + 'xcresult-upload': printXCResultMissingMarkerGuidance, } export class ResultUploadCommandHandler { @@ -132,12 +150,7 @@ export class ResultUploadCommandHandler { } for (const file of this.args.files) { - const fileData = readFileSync(file).toString() - const fileResults = await commandTypeParsers[this.type]( - fileData, - dirname(file), - parserOptions - ) + const fileResults = await commandTypeParsers[this.type](file, dirname(file), parserOptions) results.push({ file, results: fileResults }) } @@ -145,8 +158,8 @@ export class ResultUploadCommandHandler { } protected detectProjectCodeFromTCaseNames(fileResults: FileResults[]) { - // Look for pattern like PRJ-123 or TEST-456 - const tcaseSeqPattern = String.raw`([A-Za-z0-9]{1,5})-\d{3,}` + // Look for pattern like PRJ-123 or TEST-456 (_ is also allowed as separator) + const tcaseSeqPattern = String.raw`([A-Za-z0-9]{1,5})[-_]\d{3,}` for (const { results } of fileResults) { for (const result of results) { if (result.name) { @@ -248,9 +261,14 @@ export class ResultUploadCommandHandler { } if (shouldFailOnInvalid) { - return printErrorThenExit( - `Test case name "${result.name}" in ${file} does not contain valid sequence number with project code (e.g., ${projectCode}-123)` + printError( + `Test case name "${result.name}" in ${file} does not contain valid test case marker` + ) + commandTypePrintMissingMarkerGuidance[this.type](projectCode, result.name) + console.error( + chalk.yellow('Also ensure that the test cases exist in the QA Sphere project.') ) + return process.exit(1) } } diff --git a/src/utils/result-upload/ResultUploader.ts b/src/utils/result-upload/ResultUploader.ts index bb96596..28bec51 100644 --- a/src/utils/result-upload/ResultUploader.ts +++ b/src/utils/result-upload/ResultUploader.ts @@ -4,7 +4,11 @@ import { RunTCase } from '../../api/schemas' import { getTCaseMarker, parseRunUrl, printError, printErrorThenExit, twirlLoader } from '../misc' import { Api, createApi } from '../../api' import { Attachment, TestCaseResult } from './types' -import { ResultUploadCommandArgs, UploadCommandType } from './ResultUploadCommandHandler' +import { + commandTypePrintMissingMarkerGuidance, + ResultUploadCommandArgs, + UploadCommandType, +} from './ResultUploadCommandHandler' const MAX_CONCURRENT_FILE_UPLOADS = 10 let MAX_RESULTS_IN_REQUEST = 50 // Only updated from tests, otherwise it's a constant @@ -77,55 +81,17 @@ export class ResultUploader { } private printMissingTestCaseGuidance(missing: TestCaseResult[]) { - if (this.type === 'junit-upload') { - this.printJUnitGuidance() - } else if (this.type === 'playwright-json-upload') { - this.printPlaywrightGuidance(missing[0]?.name || 'your test name') - } - console.error( - chalk.yellow( - 'Also ensure that the test cases exist in the QA Sphere project and the test run (if run URL is provided).' - ) - ) - } - - private printJUnitGuidance() { - console.error(` -${chalk.yellow('To fix this issue, include the test case marker in your test names:')} - - Format: ${chalk.green(`${this.project}-: Your test name`)} - Example: ${chalk.green(`${this.project}-002: Login with valid credentials`)} - ${chalk.green(`Login with invalid credentials: ${this.project}-1312`)} - - ${chalk.dim('Where is the test case number (minimum 3 digits, zero-padded if needed)')} -`) - } - - private printPlaywrightGuidance(exampleTestName: string) { - console.error(` -${chalk.yellow('To fix this issue, choose one of the following options:')} + commandTypePrintMissingMarkerGuidance[this.type](this.project, missing[0]?.name) - ${chalk.bold('Option 1: Use Test Annotations (Recommended)')} - Add a test annotation to your Playwright test: - - ${chalk.green(`test('${exampleTestName}', { - annotation: { - type: 'test case', - description: 'https://your-qas-instance.com/project/${this.project}/tcase/123' - } - }, async ({ page }) => { - // your test code - });`)} - - ${chalk.dim('Note: The "type" field is case-insensitive')} - - ${chalk.bold('Option 2: Include Test Case Marker in Name')} - Rename your test to include the marker ${chalk.green(`${this.project}-`)}: - - Format: ${chalk.green(`${this.project}-: Your test name`)} - Example: ${chalk.green(`${this.project}-1024: Login with valid credentials`)} - ${chalk.dim('Where is the test case number (minimum 3 digits, zero-padded if needed)')} -`) + if (!this.args.createTcases) { + console.error( + chalk.yellow( + `Also ensure that the test cases exist in the QA Sphere project${ + this.args.runUrl ? ' and the provided test run' : '' + }.` + ) + ) + } } private validateAndPrintMissingAttachments = (results: TCaseWithResult[]) => { @@ -292,7 +258,9 @@ ${chalk.yellow('To fix this issue, choose one of the following options:')} if (result.name) { const tcase = testcases.find((tcase) => { const tcaseMarker = getTCaseMarker(this.project, tcase.seq) - return result.name.includes(tcaseMarker) + return ( + result.name.includes(tcaseMarker) || result.name.includes(tcaseMarker.replace('-', '_')) + ) }) if (tcase) { diff --git a/src/utils/result-upload/junitXmlParser.ts b/src/utils/result-upload/junitXmlParser.ts index 21d0712..9700915 100644 --- a/src/utils/result-upload/junitXmlParser.ts +++ b/src/utils/result-upload/junitXmlParser.ts @@ -1,4 +1,6 @@ +import chalk from 'chalk' import escapeHtml from 'escape-html' +import { readFileSync } from 'node:fs' import xml from 'xml2js' import z from 'zod' import { Attachment, TestCaseResult } from './types' @@ -73,11 +75,27 @@ const junitXmlSchema = z.object({ }), }) +export const printJUnitMissingMarkerGuidance = ( + projectCode: string, + testCaseName = 'your test name' +) => { + console.error(` +${chalk.yellow('To fix this issue, include the test case marker in your test names:')} + + Format: ${chalk.green(`${projectCode}-`)}, ${chalk.dim( + 'where is the test case number (minimum 3 digits, zero-padded if needed)' + )} + Example: ${chalk.green(`${projectCode}-002: ${testCaseName}`)} + ${chalk.green(`${testCaseName}: ${projectCode}-1312`)} +`) +} + export const parseJUnitXml: Parser = async ( - xmlString: string, + filePath: string, attachmentBaseDirectory: string, options: ParserOptions ): Promise => { + const xmlString = readFileSync(filePath).toString() const xmlData = await xml.parseStringPromise(xmlString, { explicitCharkey: true, includeWhiteChars: true, diff --git a/src/utils/result-upload/playwrightJsonParser.ts b/src/utils/result-upload/playwrightJsonParser.ts index 55ec5a7..171462d 100644 --- a/src/utils/result-upload/playwrightJsonParser.ts +++ b/src/utils/result-upload/playwrightJsonParser.ts @@ -1,6 +1,8 @@ -import z from 'zod' +import chalk from 'chalk' import escapeHtml from 'escape-html' +import { readFileSync } from 'node:fs' import stripAnsi from 'strip-ansi' +import z from 'zod' import { Attachment, TestCaseResult } from './types' import { Parser, ParserOptions } from './ResultUploadCommandHandler' import { ResultStatus } from '../../api/schemas' @@ -79,11 +81,44 @@ const playwrightJsonSchema = z.object({ suites: suiteSchema.array(), }) +export const printPlaywrightMissingMarkerGuidance = ( + projectCode: string, + exampleTestName = 'your test name' +) => { + console.error(` +${chalk.yellow('To fix this issue, choose one of the following options:')} + + ${chalk.bold('Option 1: Use Test Annotations (Recommended)')} + Add a "test case" annotation to your Playwright test with the QA Sphere test case URL: + + ${chalk.green(`test('${exampleTestName}', { + annotation: { + type: 'test case', + description: 'https://your-qas-instance.com/project/${projectCode}/tcase/123' + } + }, async ({ page }) => { + // your test code + });`)} + + ${chalk.dim('Note: The "type" field is case-insensitive')} + + ${chalk.bold('Option 2: Include Test Case Marker in Name')} + Rename your test to include the test case marker: + + Format: ${chalk.green(`${projectCode}-`)}, ${chalk.dim( + 'where is the test case number (minimum 3 digits, zero-padded if needed)' + )} + Example: ${chalk.green(`${projectCode}-002: ${exampleTestName}`)} + ${chalk.green(`${exampleTestName}: ${projectCode}-1312`)} +`) +} + export const parsePlaywrightJson: Parser = async ( - jsonString: string, + filePath: string, attachmentBaseDirectory: string, options: ParserOptions ): Promise => { + const jsonString = readFileSync(filePath).toString() const jsonData = JSON.parse(jsonString) const validated = playwrightJsonSchema.parse(jsonData) const testcases: TestCaseResult[] = [] @@ -185,7 +220,7 @@ const buildMessage = (result: Result, status: ResultStatus, options: ParserOptio let message = '' if (result.retry) { - message += `

Test passed in ${result.retry + 1} attempts

` + message += `

Test passed in ${result.retry + 1} attempts

` } if (result.errors.length > 0) { diff --git a/src/utils/result-upload/xcresultSqliteParser.ts b/src/utils/result-upload/xcresultSqliteParser.ts new file mode 100644 index 0000000..1eee62a --- /dev/null +++ b/src/utils/result-upload/xcresultSqliteParser.ts @@ -0,0 +1,461 @@ +import Database from 'better-sqlite3' +import chalk from 'chalk' +import escapeHtml from 'escape-html' +import { decompress } from 'fzstd' +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { ResultStatus } from '../../api/schemas' +import { Attachment, TestCaseResult } from './types' +import { Parser } from './ResultUploadCommandHandler' + +// Zstandard magic bytes: 0x28 0xB5 0x2F 0xFD +const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]) + +const sqliteFile = 'database.sqlite3' +const dataDir = 'data' // Contains refs and data files +const dataFilePrefix = 'data.' +const ignoredAttachmentsPrefix = 'SynthesizedEvent_' + +interface TestSuiteRow { + rowid: number + name: string | null + parentSuite_fk: number | null +} + +interface TestCaseRow { + rowid: number + testSuite_fk: number | null + name: string | null +} + +interface TestCaseRunRow { + rowid: number + testCase_fk: number | null + result: string | null + duration: number | null // In seconds + skipNotice_fk: number | null +} + +interface AttachmentRow { + rowid: number + filenameOverride: string | null + xcResultKitPayloadRefId: string | null + + // From JOIN with Activities table + testCaseRun_fk: number | null +} + +interface SkipNoticeRow { + rowid: number + message: string | null +} + +interface ExpectedFailureRow { + rowid: number + testCaseRun_fk: number | null + issue_fk: number | null + failureReason: string | null +} + +interface TestIssueRow { + rowid: number + testCaseRun_fk: number | null + compactDescription: string | null + detailedDescription: string | null + sanitizedDescription: string | null + sourceCodeContext_fk: number | null + + // From JOIN with SourceCodeContexts and SourceCodeLocations tables + filePath: string | null + lineNumber: number | null +} + +interface SourceCodeFrameRow { + rowid: number + context_fk: number | null + + // From JOIN with SourceCodeSymbolInfos table + symbolName: string | null + + // From JOIN with SourceCodeLocations table + filePath: string | null + lineNumber: number | null +} + +export const printXCResultMissingMarkerGuidance = ( + projectCode: string, + testCaseName = 'your_test_name' +) => { + console.error(` +${chalk.yellow('To fix this issue, include the test case marker in your test names:')} + + Format: ${chalk.green(`${projectCode}_`)}, ${chalk.dim( + 'where is the test case number (minimum 3 digits, zero-padded if needed)' + )} + Example: ${chalk.green(`${projectCode}_002_${testCaseName}`)} + ${chalk.green(`${testCaseName}_${projectCode}_1312`)} +`) +} + +export const parseXCResult: Parser = async (bundlePath: string): Promise => { + const dbPath = path.join(bundlePath, sqliteFile) + if (!existsSync(dbPath)) { + // Following ensures that the sqlite path exist (is generated on first run) + try { + execSync(`xcrun xcresulttool get test-results summary --path "${bundlePath}"`, { + stdio: 'ignore', + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to get test-results summary for ${bundlePath}: ${errorMessage}`) + } + } + + const db = new Database(dbPath, { readonly: true }) + + try { + const testSuitesIdToPathMap = getTestSuitesIdToPathMap(db) + const testCasesIdToRowMap = getTestCasesIdToRowMap(db) + const skipNoticesIdToMessageMap = getSkipNoticesIdToMessageMap(db) + const testCaseRunIdToExpectedFailuresMap = getTestCaseRunIdToExpectedFailuresMap(db) + const testIssues = getTestIssues(db) + const sourceCodeContextIdToFramesMap = getSourceCodeContextIdToFramesMap(db) + const testCaseRunIdToAttachmentsMap = getTestCaseRunIdToAttachmentsMap(db, bundlePath) + + const testCaseRuns = db + .prepare('SELECT rowid, testCase_fk, result, duration, skipNotice_fk FROM TestCaseRuns') + .all() as TestCaseRunRow[] + + const results: TestCaseResult[] = [] + for (const testCaseRun of testCaseRuns) { + const testCase = testCaseRun.testCase_fk ? testCasesIdToRowMap[testCaseRun.testCase_fk] : null + if (!testCase) { + continue + } + + const folder = testCase.testSuite_fk + ? (testSuitesIdToPathMap[testCase.testSuite_fk] ?? null) + : null + const status = mapResultStatus(testCaseRun.result) + const message = buildMessage( + testCaseRun.rowid, + status, + testCaseRun.skipNotice_fk + ? (skipNoticesIdToMessageMap[testCaseRun.skipNotice_fk] ?? null) + : null, + testCaseRunIdToExpectedFailuresMap[testCaseRun.rowid], + testIssues, + sourceCodeContextIdToFramesMap + ) + + results.push({ + name: (testCase.name ?? 'Unknown Test').split('(')[0], // Names include "()" as well + folder: folder ?? 'Unknown Suite', + status, + message, + timeTaken: testCaseRun.duration ? testCaseRun.duration * 1000 : null, + attachments: testCaseRunIdToAttachmentsMap[testCaseRun.rowid] ?? [], + }) + } + + return results + } finally { + db.close() + } +} + +function getTestSuitesIdToPathMap(db: Database.Database): Record { + const rows = db + .prepare('SELECT rowid, name, parentSuite_fk FROM TestSuites') + .all() as TestSuiteRow[] + + const testSuitesMap: Record = {} + for (const row of rows) { + testSuitesMap[row.rowid] = row + } + + const testSuitesPathMap: Record = {} + + const getTestSuitePath = (testSuite: TestSuiteRow): string => { + if (testSuitesPathMap[testSuite.rowid]) { + return testSuitesPathMap[testSuite.rowid] + } + + const parentSuite = testSuite.parentSuite_fk ? testSuitesMap[testSuite.parentSuite_fk] : null + const parentSuitePath = parentSuite ? getTestSuitePath(parentSuite) : '' + const path = `${parentSuitePath ? `${parentSuitePath} › ` : ''}${testSuite.name ?? ''}` + + // Also store the path in the map + testSuitesPathMap[testSuite.rowid] = path + return path + } + + for (const testSuite of Object.values(testSuitesMap)) { + getTestSuitePath(testSuite) + } + + return testSuitesPathMap +} + +function getTestCasesIdToRowMap(db: Database.Database): Record { + const rows = db.prepare('SELECT rowid, name, testSuite_fk FROM TestCases').all() as TestCaseRow[] + + const map: Record = {} + for (const row of rows) { + map[row.rowid] = row + } + return map +} + +function getSkipNoticesIdToMessageMap(db: Database.Database): Record { + const rows = db.prepare('SELECT rowid, message FROM SkipNotices').all() as SkipNoticeRow[] + + const map: Record = {} + for (const row of rows) { + map[row.rowid] = row.message ?? '' + } + return map +} + +function getTestCaseRunIdToExpectedFailuresMap( + db: Database.Database +): Record { + const rows = db + .prepare( + 'SELECT rowid, issue_fk, testCaseRun_fk, failureReason FROM ExpectedFailures ORDER BY orderInOwner' + ) + .all() as ExpectedFailureRow[] + + const map: Record = {} + for (const row of rows) { + if (!row.testCaseRun_fk) { + continue + } + + const expectedFailures = map[row.testCaseRun_fk] ?? [] + expectedFailures.push(row) + map[row.testCaseRun_fk] = expectedFailures + } + return map +} + +function getTestIssues(db: Database.Database): TestIssueRow[] { + const rows = db + .prepare( + `SELECT + ti.rowid, + ti.testCaseRun_fk, + ti.compactDescription, + ti.detailedDescription, + ti.sanitizedDescription, + ti.sourceCodeContext_fk, + scl.filePath, + scl.lineNumber + FROM TestIssues AS ti + LEFT JOIN SourceCodeContexts AS scc ON scc.rowid = ti.sourceCodeContext_fk + INNER JOIN SourceCodeLocations AS scl ON scl.rowid = scc.location_fk + ORDER BY ti.testCaseRun_fk, ti.orderInOwner` + ) + .all() as TestIssueRow[] + + return rows +} + +function getSourceCodeContextIdToFramesMap( + db: Database.Database +): Record { + const rows = db + .prepare( + `SELECT + scf.rowid, + scf.context_fk, + scsi.symbolName, + scl.filePath, + scl.lineNumber + FROM SourceCodeFrames AS scf + INNER JOIN SourceCodeSymbolInfos AS scsi ON scsi.rowid = scf.symbolinfo_fk + INNER JOIN SourceCodeLocations AS scl ON scl.rowid = scsi.location_fk + WHERE scf.symbolInfo_fk IS NOT NULL + ORDER BY scf.context_fk, scf.orderInContainer` + ) + .all() as SourceCodeFrameRow[] + + const map: Record = {} + for (const row of rows) { + if (!row.context_fk) { + continue + } + + const context = map[row.context_fk] ?? [] + context.push(row) + map[row.context_fk] = context + } + return map +} + +function getTestCaseRunIdToAttachmentsMap( + db: Database.Database, + baseDir: string +): Record { + const rows = db + .prepare( + `SELECT + att.rowid, + att.filenameOverride, + att.xcResultKitPayloadRefId, + act.testCaseRun_fk + FROM Attachments AS att + INNER JOIN Activities AS act ON att.activity_fk = act.rowid` + ) + .all() as AttachmentRow[] + + const map: Record = {} + for (const row of rows) { + if (!row.testCaseRun_fk || !row.filenameOverride || !row.xcResultKitPayloadRefId) { + continue + } + + if (row.filenameOverride.startsWith(ignoredAttachmentsPrefix)) { + continue + } + + const buffer = readDataBlob(baseDir, row.xcResultKitPayloadRefId) + if (!buffer) { + continue + } + + const attachments = map[row.testCaseRun_fk] ?? [] + attachments.push({ + filename: row.filenameOverride, + buffer, + error: null, + }) + map[row.testCaseRun_fk] = attachments + } + return map +} + +function readDataBlob(baseDir: string, refId: string): Buffer | null { + const filename = `${dataFilePrefix}${refId}` + const filepath = path.join(baseDir, dataDir, filename) + + if (!existsSync(filepath)) { + return null + } + + const rawData = readFileSync(filepath) + if (isZstdCompressed(rawData)) { + return Buffer.from(decompress(rawData)) + } + + return rawData +} + +function isZstdCompressed(data: Buffer): boolean { + if (data.length < 4) return false + return data.subarray(0, 4).equals(ZSTD_MAGIC) +} + +function mapResultStatus(result: string | null): ResultStatus { + switch (result?.toLowerCase() ?? null) { + case 'success': + return 'passed' + case 'failure': + return 'failed' + case 'skipped': + return 'skipped' + case 'expected failure': + return 'blocked' + } + + return 'skipped' +} + +function buildMessage( + testCaseRunId: number, + status: ResultStatus, + skipNotice: string | null, + expectedFailures: ExpectedFailureRow[] | null, + allTestIssues: TestIssueRow[], + sourceCodeContextIdToFramesMap: Record +): string { + let message = '' + + if (status === 'skipped' && skipNotice) { + message += `

Skipped Reason: ${escapeHtml(skipNotice)}

` + } + + if (status === 'blocked' && expectedFailures) { + for (let i = 0; i < expectedFailures.length; i++) { + const expectedFailure = expectedFailures[i] + const issue = expectedFailure.issue_fk + ? allTestIssues?.find((ti) => ti.rowid === expectedFailure.issue_fk) + : null + + message += `${i > 0 ? '

' : ''}

Expected Failure: ${escapeHtml( + expectedFailure.failureReason + )}

` + if (issue) { + const issueMessage = getIssueMessage( + issue, + sourceCodeContextIdToFramesMap, + '    ' + ) + if (issueMessage) { + message += issueMessage + } + } + } + } + + if (status === 'failed') { + let addSeparation = false + const issues = allTestIssues.filter((ti) => ti.testCaseRun_fk === testCaseRunId) + + for (const issue of issues) { + const issueMessage = getIssueMessage(issue, sourceCodeContextIdToFramesMap) + if (issueMessage) { + message += `${addSeparation ? '

' : ''}

${issueMessage}

` + addSeparation = true + } + } + } + + return message +} + +function getIssueMessage( + issue: TestIssueRow, + sourceCodeContextIdToFramesMap: Record, + indent = '' +) { + let issueMessage = + issue.detailedDescription || issue.sanitizedDescription || issue.compactDescription || '' + + if (!issueMessage) { + return '' + } + + issueMessage = `${indent}${escapeHtml(issueMessage)}` + if (issue.filePath && issue.lineNumber) { + issueMessage += ` (at ${escapeHtml(issue.filePath)}:${issue.lineNumber})
` + } + + const frames = issue.sourceCodeContext_fk + ? sourceCodeContextIdToFramesMap[issue.sourceCodeContext_fk] + : null + if (frames?.length) { + for (const frame of frames) { + issueMessage += `${indent}    ${escapeHtml( + frame.symbolName ?? '??' + )}` + if (frame.filePath && frame.lineNumber) { + issueMessage += ` (at ${escapeHtml(frame.filePath)}:${frame.lineNumber})` + } + issueMessage += `
` + } + } + + return issueMessage +}