diff --git a/.github/workflows/collect-deps.yml b/.github/workflows/collect-deps.yml new file mode 100644 index 000000000..8ae9cb11b --- /dev/null +++ b/.github/workflows/collect-deps.yml @@ -0,0 +1,58 @@ +name: Collect ubuntu browser dependencies +on: + schedule: + - cron: 0 0 1 * * +permissions: + pull-requests: write +jobs: + collect: + name: Collect browser dependencies + runs-on: ${{ matrix.os }} + env: + BRANCH_NAME: resolve-ubuntu-dependencies-${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + token: ${{ secrets.GH_ACCESS_TOKEN }} + - name: Setup Node JS + uses: actions/setup-node@v2 + with: + node-version: 18 + registry-url: https://registry.npmjs.org + - name: Prepare Ubuntu + run: sudo apt-get update && sudo apt-get install -y apt-file && sudo apt-file update + - name: Install npm dependencies + run: npm ci + - name: Config git + run: git config --global user.name "y-infra" && git config --global user.email "y-infra@yandex.ru" + - name: Checkout to branch + run: | + if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then + git fetch origin ${{ env.BRANCH_NAME }} + git checkout ${{ env.BRANCH_NAME }} + git pull + else + git checkout -b ${{ env.BRANCH_NAME }} + fi + - name: Resolve Ubuntu dependencies + run: npm run resolve-ubuntu-dependencies + - name: Commit changes + run: | + git add src + git status + if git diff-index --quiet HEAD src; then + echo 'No changes' + else + echo 'Committing changes' + git commit src -m 'chore: update local browser dependencies for ${{ matrix.os }}' + git push origin ${{ env.BRANCH_NAME }} + gh pr create -B master -H ${{ env.BRANCH_NAME }} --title "Auto update local browser deps for ${{ matrix.os }}" --body "Created by Github action" || echo "Could not create PR. Seems like it already exists" + fi + env: + GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} diff --git a/package-lock.json b/package-lock.json index 7d93e9efb..1613870ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,7 @@ "@types/debug": "4.1.12", "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", + "@types/js-levenshtein": "1.1.3", "@types/lodash": "4.14.191", "@types/micromatch": "4.0.9", "@types/mocha": "10.0.1", @@ -111,8 +112,10 @@ "eslint": "8.25.0", "eslint-config-gemini-testing": "2.8.0", "eslint-config-prettier": "8.7.0", + "execa": "5.1.1", "glob-extra": "5.0.2", "husky": "0.11.4", + "js-levenshtein": "1.1.6", "jsdom": "^24.0.0", "jsdom-global": "3.0.2", "onchange": "7.1.0", @@ -812,6 +815,128 @@ "node": ">=v18" } }, + "node_modules/@commitlint/cli/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@commitlint/cli/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@commitlint/cli/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/cli/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@commitlint/cli/node_modules/yargs": { "version": "17.5.1", "dev": true, @@ -1039,6 +1164,128 @@ "node": ">=v18" } }, + "node_modules/@commitlint/rules/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@commitlint/rules/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@commitlint/rules/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/rules/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@commitlint/to-lines": { "version": "19.0.0", "dev": true, @@ -2290,6 +2537,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-levenshtein": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", + "integrity": "sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -3485,17 +3738,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/archiver-utils/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4714,17 +4956,6 @@ "node": ">=0.8.x" } }, - "node_modules/compress-commons/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/compress-commons/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -7223,38 +7454,46 @@ } }, "node_modules/execa": { - "version": "8.0.1", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=16.17" + "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/execa/node_modules/get-stream": { - "version": "8.0.1", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "license": "MIT", "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/expand-template": { "version": "2.0.3", "license": "(MIT OR WTFPL)", @@ -8432,11 +8671,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/human-signals": { - "version": "5.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": ">=16.17.0" + "node": ">=10.17.0" } }, "node_modules/husky": { @@ -8756,11 +8996,11 @@ "license": "MIT" }, "node_modules/is-stream": { - "version": "3.0.0", - "dev": true, - "license": "MIT", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8957,6 +9197,15 @@ "js-graphs": "src/jsgraphs.js" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-sdsl": { "version": "4.1.5", "dev": true, @@ -9942,14 +10191,12 @@ } }, "node_modules/mimic-fn": { - "version": "4.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/mimic-response": { @@ -10495,28 +10742,15 @@ } }, "node_modules/npm-run-path": { - "version": "5.3.0", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "license": "MIT", "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "path-key": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "dev": true, - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/nwsapi": { @@ -10564,14 +10798,15 @@ } }, "node_modules/onetime": { - "version": "6.0.0", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, - "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=12" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12707,14 +12942,12 @@ } }, "node_modules/strip-final-newline": { - "version": "3.0.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/strip-indent": { @@ -15174,6 +15407,77 @@ "yargs": "^17.0.0" }, "dependencies": { + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, "yargs": { "version": "17.5.1", "dev": true, @@ -15329,6 +15633,79 @@ "@commitlint/to-lines": "^19.0.0", "@commitlint/types": "^19.0.3", "execa": "^8.0.1" + }, + "dependencies": { + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } } }, "@commitlint/to-lines": { @@ -16154,6 +16531,12 @@ "@types/istanbul-lib-report": "*" } }, + "@types/js-levenshtein": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", + "integrity": "sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==", + "dev": true + }, "@types/json-schema": { "version": "7.0.15", "dev": true @@ -17013,11 +17396,6 @@ "path-scurry": "^1.11.1" } }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, "minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -17780,11 +18158,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, "readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -19412,22 +19785,32 @@ } }, "execa": { - "version": "8.0.1", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "requires": { "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "dependencies": { "get-stream": { - "version": "8.0.1", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true } } @@ -20185,7 +20568,9 @@ } }, "human-signals": { - "version": "5.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, "husky": { @@ -20378,8 +20763,9 @@ "dev": true }, "is-stream": { - "version": "3.0.0", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "is-text-path": { "version": "1.0.1", @@ -20503,6 +20889,12 @@ "js-graph-algorithms": { "version": "1.0.18" }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, "js-sdsl": { "version": "4.1.5", "dev": true @@ -21154,7 +21546,9 @@ } }, "mimic-fn": { - "version": "4.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "mimic-response": { @@ -21497,16 +21891,12 @@ "dev": true }, "npm-run-path": { - "version": "5.3.0", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "path-key": "^4.0.0" - }, - "dependencies": { - "path-key": { - "version": "4.0.0", - "dev": true - } + "path-key": "^3.0.0" } }, "nwsapi": { @@ -21540,10 +21930,12 @@ } }, "onetime": { - "version": "6.0.0", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { - "mimic-fn": "^4.0.0" + "mimic-fn": "^2.1.0" } }, "os-browserify": { @@ -22929,7 +23321,9 @@ } }, "strip-final-newline": { - "version": "3.0.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "strip-indent": { diff --git a/package.json b/package.json index 1b2c92e7a..a99018bd9 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,11 @@ ], "scripts": { "build": "tsc --build && npm run copy-static && npm run build-bundles", - "copy-static": "copyfiles 'src/browser/client-scripts/*' build", + "copy-static": "copyfiles 'src/browser/client-scripts/*' 'src/**/[!cache]*/autogenerated/**/*.json' build", "build-node-bundle": "esbuild ./src/bundle/cjs/index.ts --outdir=./build/src/bundle/cjs --bundle --format=cjs --platform=node --target=ES2021", "build-browser-bundle": "node ./src/browser/client-scripts/build.js", "build-bundles": "concurrently -c 'auto' 'npm:build-browser-bundle' 'npm:build-node-bundle --minify'", + "resolve-ubuntu-dependencies": "ts-node ./src/browser-installer/ubuntu-packages/collect-dependencies", "check-types": "tsc --project tsconfig.spec.json", "clean": "rimraf build/ *.tsbuildinfo", "lint": "eslint --cache . && prettier --check .", @@ -122,6 +123,7 @@ "@types/debug": "4.1.12", "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", + "@types/js-levenshtein": "1.1.3", "@types/lodash": "4.14.191", "@types/micromatch": "4.0.9", "@types/mocha": "10.0.1", @@ -150,7 +152,9 @@ "eslint-config-gemini-testing": "2.8.0", "eslint-config-prettier": "8.7.0", "glob-extra": "5.0.2", + "execa": "5.1.1", "husky": "0.11.4", + "js-levenshtein": "1.1.6", "jsdom": "^24.0.0", "jsdom-global": "3.0.2", "onchange": "7.1.0", diff --git a/src/browser-installer/chrome/browser.ts b/src/browser-installer/chrome/browser.ts index 91f85e01b..bc980839d 100644 --- a/src/browser-installer/chrome/browser.ts +++ b/src/browser-installer/chrome/browser.ts @@ -8,10 +8,12 @@ import { Browser, type DownloadProgressCallback, } from "../utils"; -import { getBinaryPath, getMatchedBrowserVersion, installBinary } from "../registry"; +import registry from "../registry"; import { normalizeChromeVersion } from "../utils"; +import { installUbuntuPackageDependencies } from "../ubuntu-packages"; +import { installChromeDriver } from "./driver"; -export const installChrome = async (version: string, { force = false } = {}): Promise => { +const installChromeBrowser = async (version: string, { force = false } = {}): Promise => { const milestone = getMilestone(version); if (Number(milestone) < MIN_CHROME_FOR_TESTING_VERSION) { @@ -23,12 +25,12 @@ export const installChrome = async (version: string, { force = false } = {}): Pr } const platform = getBrowserPlatform(); - const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.CHROME, platform, version); + const existingLocallyBrowserVersion = registry.getMatchedBrowserVersion(Browser.CHROME, platform, version); if (existingLocallyBrowserVersion && !force) { browserInstallerDebug(`A locally installed chrome@${version} browser was found. Skipping the installation`); - return getBinaryPath(Browser.CHROME, platform, existingLocallyBrowserVersion); + return registry.getBinaryPath(Browser.CHROME, platform, existingLocallyBrowserVersion); } const normalizedVersion = normalizeChromeVersion(version); @@ -57,5 +59,18 @@ export const installChrome = async (version: string, { force = false } = {}): Pr unpack: true, }).then(result => result.executablePath); - return installBinary(Browser.CHROME, platform, buildId, installFn); + return registry.installBinary(Browser.CHROME, platform, buildId, installFn); +}; + +export const installChrome = async ( + version: string, + { force = false, needWebDriver = false, needUbuntuPackages = false } = {}, +): Promise => { + const [browserPath] = await Promise.all([ + installChromeBrowser(version, { force }), + needWebDriver && installChromeDriver(version, { force }), + needUbuntuPackages && installUbuntuPackageDependencies(), + ]); + + return browserPath; }; diff --git a/src/browser-installer/chrome/driver.ts b/src/browser-installer/chrome/driver.ts index 85f642d53..efb1ede45 100644 --- a/src/browser-installer/chrome/driver.ts +++ b/src/browser-installer/chrome/driver.ts @@ -8,18 +8,18 @@ import { Driver, type DownloadProgressCallback, } from "../utils"; -import { getBinaryPath, getMatchedDriverVersion, installBinary } from "../registry"; +import registry from "../registry"; export const installChromeDriver = async (chromeVersion: string, { force = false } = {}): Promise => { const platform = getBrowserPlatform(); - const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.CHROMEDRIVER, platform, chromeVersion); + const existingLocallyDriverVersion = registry.getMatchedDriverVersion(Driver.CHROMEDRIVER, platform, chromeVersion); if (existingLocallyDriverVersion && !force) { browserInstallerDebug( `A locally installed chromedriver for chrome@${chromeVersion} was found. Skipping the installation`, ); - return getBinaryPath(Driver.CHROMEDRIVER, platform, existingLocallyDriverVersion); + return registry.getBinaryPath(Driver.CHROMEDRIVER, platform, existingLocallyDriverVersion); } const milestone = getMilestone(chromeVersion); @@ -59,5 +59,5 @@ export const installChromeDriver = async (chromeVersion: string, { force = false downloadProgressCallback, }).then(result => result.executablePath); - return installBinary(Driver.CHROMEDRIVER, platform, buildId, installFn); + return registry.installBinary(Driver.CHROMEDRIVER, platform, buildId, installFn); }; diff --git a/src/browser-installer/chrome/index.ts b/src/browser-installer/chrome/index.ts index 9ebb4061d..5ec7456c0 100644 --- a/src/browser-installer/chrome/index.ts +++ b/src/browser-installer/chrome/index.ts @@ -6,6 +6,7 @@ import { DRIVER_WAIT_TIMEOUT } from "../constants"; import { getMilestone } from "../utils"; import { installChrome } from "./browser"; import { installChromeDriver } from "./driver"; +import { isUbuntu, getUbuntuLinkerEnv } from "../ubuntu-packages"; export { installChrome, installChromeDriver }; @@ -13,18 +14,22 @@ export const runChromeDriver = async ( chromeVersion: string, { debug = false } = {}, ): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { - const [chromeDriverPath] = await Promise.all([installChromeDriver(chromeVersion), installChrome(chromeVersion)]); - - const milestone = getMilestone(chromeVersion); - const randomPort = await getPort(); + const [chromeDriverPath, randomPort, chromeDriverEnv] = await Promise.all([ + installChromeDriver(chromeVersion), + getPort(), + isUbuntu() + .then(isUbuntu => (isUbuntu ? getUbuntuLinkerEnv() : null)) + .then(extraEnv => (extraEnv ? { ...process.env, ...extraEnv } : process.env)), + ]); const chromeDriver = spawn(chromeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { windowsHide: true, detached: false, + env: chromeDriverEnv, }); if (debug) { - pipeLogsWithPrefix(chromeDriver, `[chromedriver@${milestone}] `); + pipeLogsWithPrefix(chromeDriver, `[chromedriver@${getMilestone(chromeVersion)}] `); } const gridUrl = `http://127.0.0.1:${randomPort}`; diff --git a/src/browser-installer/chromium/browser.ts b/src/browser-installer/chromium/browser.ts index 55b92d3a1..029947a24 100644 --- a/src/browser-installer/chromium/browser.ts +++ b/src/browser-installer/chromium/browser.ts @@ -1,5 +1,5 @@ import { install as puppeteerInstall, canDownload } from "@puppeteer/browsers"; -import { installBinary, getBinaryPath, getMatchedBrowserVersion } from "../registry"; +import registry from "../registry"; import { getMilestone, browserInstallerDebug, getBrowsersDir, Browser, type DownloadProgressCallback } from "../utils"; import { getChromiumBuildId } from "./utils"; import { getChromePlatform } from "../utils"; @@ -18,12 +18,12 @@ export const installChromium = async (version: string, { force = false } = {}): } const platform = getChromePlatform(version); - const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.CHROMIUM, platform, version); + const existingLocallyBrowserVersion = registry.getMatchedBrowserVersion(Browser.CHROMIUM, platform, version); if (existingLocallyBrowserVersion && !force) { browserInstallerDebug(`A locally installed chromium@${version} browser was found. Skipping the installation`); - return getBinaryPath(Browser.CHROMIUM, platform, existingLocallyBrowserVersion); + return registry.getBinaryPath(Browser.CHROMIUM, platform, existingLocallyBrowserVersion); } const buildId = await getChromiumBuildId(platform, milestone); @@ -52,5 +52,5 @@ export const installChromium = async (version: string, { force = false } = {}): unpack: true, }).then(result => result.executablePath); - return installBinary(Browser.CHROMIUM, platform, milestone, installFn); + return registry.installBinary(Browser.CHROMIUM, platform, milestone, installFn); }; diff --git a/src/browser-installer/chromium/driver.ts b/src/browser-installer/chromium/driver.ts index 5804e0b77..e53dd73b6 100644 --- a/src/browser-installer/chromium/driver.ts +++ b/src/browser-installer/chromium/driver.ts @@ -2,7 +2,7 @@ import fs from "fs-extra"; import path from "path"; import { noop } from "lodash"; import { CHROMEDRIVER_STORAGE_API, MIN_CHROMIUM_VERSION } from "../constants"; -import { installBinary } from "../registry"; +import registry from "../registry"; import { downloadFile, getChromiumDriverDir, @@ -50,5 +50,5 @@ export const installChromeDriverManually = async (milestone: string): Promise; diff --git a/src/browser-installer/chromium/revisions/mac.ts b/src/browser-installer/chromium/revisions/mac.ts deleted file mode 100644 index 400c85312..000000000 --- a/src/browser-installer/chromium/revisions/mac.ts +++ /dev/null @@ -1,42 +0,0 @@ -export default { - 73: 625980, - 74: 638898, - 75: 652451, - 76: 665035, - 77: 681144, - 78: 694594, - 79: 707225, - 80: 722372, - 81: 737194, - 82: 749986, - 83: 756141, - 84: 769122, - 85: 782819, - 86: 800433, - 87: 813052, - 88: 827138, - 89: 843934, - 90: 858007, - 91: 870831, - 92: 885361, - 93: 902294, - 94: 911610, - 95: 920094, - 96: 929514, - 97: 938636, - 98: 950414, - 99: 961780, - 100: 972801, - 101: 982575, - 102: 992180, - 103: 1002972, - 104: 1012821, - 105: 1027082, - 106: 1036918, - 107: 1047818, - 108: 1059080, - 109: 1070155, - 110: 1084167, - 111: 1097778, - 112: 1107206, -} as Record; diff --git a/src/browser-installer/chromium/revisions/mac_arm.ts b/src/browser-installer/chromium/revisions/mac_arm.ts deleted file mode 100644 index 69c5e1267..000000000 --- a/src/browser-installer/chromium/revisions/mac_arm.ts +++ /dev/null @@ -1,22 +0,0 @@ -export default { - 93: 902292, - 94: 911612, - 95: 920092, - 96: 929514, - 97: 938625, - 98: 950409, - 99: 961774, - 100: 972803, - 101: 982572, - 102: 992815, - 103: 1002973, - 104: 1012821, - 105: 1027072, - 106: 1036918, - 107: 1047805, - 108: 1059080, - 109: 1070155, - 110: 1084160, - 111: 1097774, - 112: 1107206, -} as Record; diff --git a/src/browser-installer/chromium/revisions/win32.ts b/src/browser-installer/chromium/revisions/win32.ts deleted file mode 100644 index 763cc6f2a..000000000 --- a/src/browser-installer/chromium/revisions/win32.ts +++ /dev/null @@ -1,42 +0,0 @@ -export default { - 73: 625980, - 74: 638903, - 75: 652459, - 76: 665037, - 77: 681141, - 78: 694594, - 79: 707225, - 80: 722370, - 81: 737194, - 82: 750023, - 83: 756143, - 84: 769121, - 85: 782817, - 86: 800429, - 87: 813051, - 88: 827139, - 89: 843930, - 90: 858016, - 91: 870819, - 92: 885359, - 93: 902289, - 94: 911612, - 95: 920068, - 96: 929514, - 97: 938622, - 98: 950413, - 99: 961752, - 100: 972803, - 101: 982576, - 102: 992804, - 103: 1002976, - 104: 1012819, - 105: 1027031, - 106: 1036918, - 107: 1047790, - 108: 1059072, - 109: 1070143, - 110: 1084163, - 111: 1097773, - 112: 1107205, -} as Record; diff --git a/src/browser-installer/chromium/revisions/win64.ts b/src/browser-installer/chromium/revisions/win64.ts deleted file mode 100644 index 7bf9e27e7..000000000 --- a/src/browser-installer/chromium/revisions/win64.ts +++ /dev/null @@ -1,42 +0,0 @@ -export default { - 73: 625982, - 74: 638903, - 75: 652459, - 76: 665038, - 77: 681145, - 78: 694594, - 79: 707229, - 80: 722374, - 81: 737198, - 82: 750000, - 83: 756141, - 84: 769133, - 85: 782823, - 86: 800433, - 87: 813059, - 88: 827103, - 89: 843930, - 90: 858016, - 91: 870818, - 92: 885361, - 93: 902299, - 94: 911616, - 95: 920069, - 96: 929514, - 97: 938627, - 98: 950416, - 99: 961781, - 100: 972790, - 101: 982567, - 102: 992822, - 103: 1002974, - 104: 1012814, - 105: 1027071, - 106: 1036912, - 107: 1047802, - 108: 1059068, - 109: 1070151, - 110: 1084157, - 111: 1097757, - 112: 1107206, -} as Record; diff --git a/src/browser-installer/chromium/utils.ts b/src/browser-installer/chromium/utils.ts index 20a28375b..e637bfdc8 100644 --- a/src/browser-installer/chromium/utils.ts +++ b/src/browser-installer/chromium/utils.ts @@ -1,11 +1,12 @@ import os from "os"; import path from "path"; +import fs from "fs-extra"; import { BrowserPlatform } from "@puppeteer/browsers"; import { getChromePlatform, getMilestone } from "../utils"; import { CHROMEDRIVER_STORAGE_API, MIN_CHROMEDRIVER_MAC_ARM_NEW_ARCHIVE_NAME } from "../constants"; export const getChromiumBuildId = async (platform: BrowserPlatform, milestone: string | number): Promise => { - const { default: revisions } = await import(`./revisions/${platform}`); + const revisions = await fs.readJSON(require.resolve(`./revisions/autogenerated/${platform}.json`)); return String(revisions[milestone]); }; diff --git a/src/browser-installer/constants.ts b/src/browser-installer/constants.ts index 68cc14b3b..dc3d4b86c 100644 --- a/src/browser-installer/constants.ts +++ b/src/browser-installer/constants.ts @@ -7,7 +7,9 @@ export const MIN_CHROMEDRIVER_FOR_TESTING_VERSION = 115; export const MIN_CHROMEDRIVER_MAC_ARM_NEW_ARCHIVE_NAME = 106; export const MIN_CHROMIUM_MAC_ARM_VERSION = 93; export const MIN_CHROMIUM_VERSION = 73; +export const MIN_FIREFOX_VERSION = 60; export const MIN_EDGEDRIVER_VERSION = 94; export const DRIVER_WAIT_TIMEOUT = 10 * 1000; // 10s -export const BYTES_PER_KILOBYTE = 1 << 10; // eslint-disable-line no-bitwise -export const BYTES_PER_MEGABYTE = BYTES_PER_KILOBYTE << 10; // eslint-disable-line no-bitwise +export const LINUX_UBUNTU_RELEASE_ID = "ubuntu"; +export const LINUX_RUNTIME_LIBRARIES_PATH_ENV_NAME = "LD_LIBRARY_PATH"; +export const MANDATORY_UBUNTU_PACKAGES_TO_BE_INSTALLED = ["fontconfig"]; diff --git a/src/browser-installer/edge/driver.ts b/src/browser-installer/edge/driver.ts index e18ea3b06..0d198e05a 100644 --- a/src/browser-installer/edge/driver.ts +++ b/src/browser-installer/edge/driver.ts @@ -7,7 +7,7 @@ import { getMilestone, retryFetch, } from "../utils"; -import { getBinaryPath, getMatchedDriverVersion, installBinary } from "../registry"; +import registry from "../registry"; import { MIN_EDGEDRIVER_VERSION, MSEDGEDRIVER_API } from "../constants"; const getLatestMajorEdgeDriverVersion = async (milestone: string): Promise => { @@ -29,14 +29,14 @@ const getLatestMajorEdgeDriverVersion = async (milestone: string): Promise => { const platform = getBrowserPlatform(); - const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.EDGEDRIVER, platform, edgeVersion); + const existingLocallyDriverVersion = registry.getMatchedDriverVersion(Driver.EDGEDRIVER, platform, edgeVersion); if (existingLocallyDriverVersion && !force) { browserInstallerDebug( `A locally installed edgedriver for edge@${edgeVersion} browser was found. Skipping the installation`, ); - return getBinaryPath(Driver.EDGEDRIVER, platform, existingLocallyDriverVersion); + return registry.getBinaryPath(Driver.EDGEDRIVER, platform, existingLocallyDriverVersion); } const milestone = getMilestone(edgeVersion); @@ -49,5 +49,5 @@ export const installEdgeDriver = async (edgeVersion: string, { force = false } = const installFn = (): Promise => downloadEdgeDriver(driverVersion, getEdgeDriverDir(driverVersion)); - return installBinary(Driver.EDGEDRIVER, platform, driverVersion, installFn); + return registry.installBinary(Driver.EDGEDRIVER, platform, driverVersion, installFn); }; diff --git a/src/browser-installer/edge/index.ts b/src/browser-installer/edge/index.ts index 1aefbe84e..5860f5b2b 100644 --- a/src/browser-installer/edge/index.ts +++ b/src/browser-installer/edge/index.ts @@ -11,8 +11,7 @@ export const runEdgeDriver = async ( edgeVersion: string, { debug = false }: { debug?: boolean } = {}, ): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { - const edgeDriverPath = await installEdgeDriver(edgeVersion); - const randomPort = await getPort(); + const [edgeDriverPath, randomPort] = await Promise.all([installEdgeDriver(edgeVersion), getPort()]); const edgeDriver = spawn(edgeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { windowsHide: true, diff --git a/src/browser-installer/firefox/browser.ts b/src/browser-installer/firefox/browser.ts index b582b05dd..b366d4cf9 100644 --- a/src/browser-installer/firefox/browser.ts +++ b/src/browser-installer/firefox/browser.ts @@ -6,17 +6,19 @@ import { getBrowsersDir, type DownloadProgressCallback, } from "../utils"; -import { installBinary, getBinaryPath, getMatchedBrowserVersion } from "../registry"; +import registry from "../registry"; import { getFirefoxBuildId, normalizeFirefoxVersion } from "./utils"; +import { installLatestGeckoDriver } from "./driver"; +import { installUbuntuPackageDependencies } from "../ubuntu-packages"; -export const installFirefox = async (version: string, { force = false } = {}): Promise => { +const installFirefoxBrowser = async (version: string, { force = false } = {}): Promise => { const platform = getBrowserPlatform(); - const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.FIREFOX, platform, version); + const existingLocallyBrowserVersion = registry.getMatchedBrowserVersion(Browser.FIREFOX, platform, version); if (existingLocallyBrowserVersion && !force) { browserInstallerDebug(`A locally installed firefox@${version} browser was found. Skipping the installation`); - return getBinaryPath(Browser.FIREFOX, platform, existingLocallyBrowserVersion); + return registry.getBinaryPath(Browser.FIREFOX, platform, existingLocallyBrowserVersion); } const normalizedVersion = normalizeFirefoxVersion(version); @@ -47,5 +49,18 @@ export const installFirefox = async (version: string, { force = false } = {}): P unpack: true, }).then(result => result.executablePath); - return installBinary(Browser.FIREFOX, platform, buildId, installFn); + return registry.installBinary(Browser.FIREFOX, platform, buildId, installFn); +}; + +export const installFirefox = async ( + version: string, + { force = false, needWebDriver = false, needUbuntuPackages = false } = {}, +): Promise => { + const [browserPath] = await Promise.all([ + installFirefoxBrowser(version, { force }), + needWebDriver && installLatestGeckoDriver(version, { force }), + needUbuntuPackages && installUbuntuPackageDependencies(), + ]); + + return browserPath; }; diff --git a/src/browser-installer/firefox/driver.ts b/src/browser-installer/firefox/driver.ts index 558cd2e78..2843c709a 100644 --- a/src/browser-installer/firefox/driver.ts +++ b/src/browser-installer/firefox/driver.ts @@ -1,6 +1,6 @@ import { download as downloadGeckoDriver } from "geckodriver"; import { GECKODRIVER_CARGO_TOML } from "../constants"; -import { installBinary, getBinaryPath, getMatchedDriverVersion } from "../registry"; +import registry from "../registry"; import { Driver, browserInstallerDebug, getBrowserPlatform, getGeckoDriverDir, retryFetch } from "../utils"; const getLatestGeckoDriverVersion = async (): Promise => { @@ -20,19 +20,19 @@ const getLatestGeckoDriverVersion = async (): Promise => { export const installLatestGeckoDriver = async (firefoxVersion: string, { force = false } = {}): Promise => { const platform = getBrowserPlatform(); - const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.GECKODRIVER, platform, firefoxVersion); + const existingLocallyDriverVersion = registry.getMatchedDriverVersion(Driver.GECKODRIVER, platform, firefoxVersion); if (existingLocallyDriverVersion && !force) { browserInstallerDebug( `A locally installed geckodriver for firefox@${firefoxVersion} browser was found. Skipping the installation`, ); - return getBinaryPath(Driver.GECKODRIVER, platform, existingLocallyDriverVersion); + return registry.getBinaryPath(Driver.GECKODRIVER, platform, existingLocallyDriverVersion); } const latestVersion = await getLatestGeckoDriverVersion(); const installFn = (): Promise => downloadGeckoDriver(latestVersion, getGeckoDriverDir(latestVersion)); - return installBinary(Driver.GECKODRIVER, platform, latestVersion, installFn); + return registry.installBinary(Driver.GECKODRIVER, platform, latestVersion, installFn); }; diff --git a/src/browser-installer/firefox/index.ts b/src/browser-installer/firefox/index.ts index 7d6d42bf9..682ec2373 100644 --- a/src/browser-installer/firefox/index.ts +++ b/src/browser-installer/firefox/index.ts @@ -6,6 +6,7 @@ import { installFirefox } from "./browser"; import { installLatestGeckoDriver } from "./driver"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; +import { getUbuntuLinkerEnv, isUbuntu } from "../ubuntu-packages"; export { installFirefox, installLatestGeckoDriver }; @@ -13,13 +14,14 @@ export const runGeckoDriver = async ( firefoxVersion: string, { debug = false } = {}, ): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { - const [geckoDriverPath] = await Promise.all([ + const [geckoDriverPath, randomPort, geckoDriverEnv] = await Promise.all([ installLatestGeckoDriver(firefoxVersion), - installFirefox(firefoxVersion), + getPort(), + isUbuntu() + .then(isUbuntu => (isUbuntu ? getUbuntuLinkerEnv() : null)) + .then(extraEnv => (extraEnv ? { ...process.env, ...extraEnv } : process.env)), ]); - const randomPort = await getPort(); - const geckoDriver = await startGeckoDriver({ customGeckoDriverPath: geckoDriverPath, port: randomPort, @@ -27,6 +29,7 @@ export const runGeckoDriver = async ( spawnOpts: { windowsHide: true, detached: false, + env: geckoDriverEnv, }, }); diff --git a/src/browser-installer/index.ts b/src/browser-installer/index.ts index 939b0976e..e3b21c33f 100644 --- a/src/browser-installer/index.ts +++ b/src/browser-installer/index.ts @@ -1,4 +1,4 @@ export { installBrowser, installBrowsersWithDrivers, BrowserInstallStatus } from "./install"; export { runBrowserDriver } from "./run"; -export { getDriverNameForBrowserName } from "./utils"; +export { getNormalizedBrowserName } from "./utils"; export type { SupportedBrowser, SupportedDriver } from "./utils"; diff --git a/src/browser-installer/install.ts b/src/browser-installer/install.ts index b6f101d98..d2552801d 100644 --- a/src/browser-installer/install.ts +++ b/src/browser-installer/install.ts @@ -1,61 +1,61 @@ import _ from "lodash"; +import { Browser, browserInstallerDebug, getNormalizedBrowserName, type SupportedBrowser } from "./utils"; /** - * @returns path to browser binary + * @returns path to installed browser binary */ export const installBrowser = async ( - browserName?: string, + browserName: SupportedBrowser, browserVersion?: string, - { force = false, installWebDriver = false } = {}, + { force = false, shouldInstallWebDriver = false, shouldInstallUbuntuPackages = false } = {}, ): Promise => { - const unsupportedBrowserError = new Error( - [ - `Couldn't install browser '${browserName}', as it is not supported`, - `Currently supported for installation browsers: 'chrome', 'firefox`, - ].join("\n"), - ); - - if (!browserName) { - throw unsupportedBrowserError; - } - if (!browserVersion) { throw new Error( `Couldn't install browser '${browserName}' because it has invalid version: '${browserVersion}'`, ); } - if (/chrome/i.test(browserName)) { - const { installChrome, installChromeDriver } = await import("./chrome"); - - return installWebDriver - ? await Promise.all([ - installChrome(browserVersion, { force }), - installChromeDriver(browserVersion, { force }), - ]).then(binaries => binaries[0]) - : installChrome(browserVersion, { force }); - } else if (/firefox/i.test(browserName)) { - const { installFirefox, installLatestGeckoDriver } = await import("./firefox"); - - return installWebDriver - ? await Promise.all([ - installFirefox(browserVersion, { force }), - installLatestGeckoDriver(browserVersion, { force }), - ]).then(binaries => binaries[0]) - : installFirefox(browserVersion, { force }); - } else if (/edge/i.test(browserName)) { - const { installEdgeDriver } = await import("./edge"); - - if (installWebDriver) { - await installEdgeDriver(browserVersion, { force }); + const { isUbuntu } = await import("./ubuntu-packages"); + + const needUbuntuPackages = shouldInstallUbuntuPackages && (await isUbuntu()); + + browserInstallerDebug( + [ + `install ${browserName}@${browserVersion}`, + `shouldInstallWebDriver:${shouldInstallWebDriver}`, + `shouldInstallUbuntuPackages:${shouldInstallUbuntuPackages}`, + `needUbuntuPackages:${needUbuntuPackages}`, + ].join(", "), + ); + + switch (browserName) { + case Browser.CHROME: + case Browser.CHROMIUM: { + const { installChrome } = await import("./chrome"); + + return installChrome(browserVersion, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); } - return null; - } else if (/safari/i.test(browserName)) { - return null; - } + case Browser.FIREFOX: { + const { installFirefox } = await import("./firefox"); + + return installFirefox(browserVersion, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); + } - throw unsupportedBrowserError; + case Browser.EDGE: { + const { installEdgeDriver } = await import("./edge"); + + if (shouldInstallWebDriver) { + await installEdgeDriver(browserVersion, { force }); + } + + return null; + } + + case Browser.SAFARI: { + return null; + } + } }; export const BrowserInstallStatus = { @@ -79,7 +79,17 @@ const forceInstallBinaries = async ( browserName?: string, browserVersion?: string, ): ForceInstallBinaryResult => { - return installFn(browserName, browserVersion, { force: true, installWebDriver: true }) + const normalizedBrowserName = getNormalizedBrowserName(browserName); + const installOpts = { force: true, shouldInstallWebDriver: true, shouldInstallUbuntuPackages: true }; + + if (!normalizedBrowserName) { + return { + status: BrowserInstallStatus.Error, + reason: `Installing ${browserName} is unsupported. Supported browsers: "chrome", "firefox", "safari", "edge"`, + }; + } + + return installFn(normalizedBrowserName, browserVersion, installOpts) .then(successResult => { return successResult ? { status: BrowserInstallStatus.Ok } diff --git a/src/browser-installer/registry/cli-progress-bar.ts b/src/browser-installer/registry/cli-progress-bar.ts index 61a278d49..b9e0fa0ae 100644 --- a/src/browser-installer/registry/cli-progress-bar.ts +++ b/src/browser-installer/registry/cli-progress-bar.ts @@ -1,35 +1,40 @@ import { MultiBar, type SingleBar } from "cli-progress"; import type { DownloadProgressCallback } from "../utils"; -import { BYTES_PER_MEGABYTE } from "../constants"; export type RegisterProgressBarFn = (browserName: string, browserVersion: string) => DownloadProgressCallback; -export const createBrowserDownloadProgressBar = (): { register: RegisterProgressBarFn } => { +export const createBrowserDownloadProgressBar = (): { register: RegisterProgressBarFn; stop: () => void } => { const progressBar = new MultiBar({ stopOnComplete: true, forceRedraw: true, autopadding: true, hideCursor: true, + clearOnComplete: true, fps: 5, - format: " [{bar}] | {filename} | {value}/{total} MB", + format: " [{bar}] | {filename} | {value}%", }); const register: RegisterProgressBarFn = (browserName, browserVersion) => { let bar: SingleBar; - const downloadProgressCallback: DownloadProgressCallback = (downloadedBytes, totalBytes) => { + const downloadProgressCallback: DownloadProgressCallback = (done, total = 100) => { if (!bar) { - const totalMB = Math.round((totalBytes / BYTES_PER_MEGABYTE) * 100) / 100; - bar = progressBar.create(totalMB, 0, { filename: `${browserName}@${browserVersion}` }); + bar = progressBar.create(100, 0, { filename: `${browserName}@${browserVersion}` }); } - const downloadedMB = Math.round((downloadedBytes / BYTES_PER_MEGABYTE) * 100) / 100; + const downloadedPercents = Math.floor((done / total) * 100); - bar.update(downloadedMB); + bar.update(downloadedPercents); }; return downloadProgressCallback; }; - return { register }; + const stop = (): void => { + progressBar.stop(); + }; + + process.once("exit", stop); + + return { register, stop }; }; diff --git a/src/browser-installer/registry/index.ts b/src/browser-installer/registry/index.ts index e5b600e04..33e29158c 100644 --- a/src/browser-installer/registry/index.ts +++ b/src/browser-installer/registry/index.ts @@ -1,5 +1,6 @@ import type { BrowserPlatform } from "@puppeteer/browsers"; -import { readJsonSync, outputJSONSync, existsSync } from "fs-extra"; +import _ from "lodash"; +import fs from "fs-extra"; import path from "path"; import { getRegistryPath, @@ -15,201 +16,304 @@ import { } from "../utils"; import { getFirefoxBuildId } from "../firefox/utils"; import logger from "../../utils/logger"; -import type { createBrowserDownloadProgressBar } from "./cli-progress-bar"; type VersionToPathMap = Record>; type BinaryName = Exclude; -type RegistryKey = `${BinaryName}_${BrowserPlatform}`; -type Registry = Record; +type BinaryKey = `${BinaryName}_${BrowserPlatform}`; +type OsName = string; +type OsVersion = string; +type OsPackagesKey = `${OsName}_${OsVersion}`; +export type RegistryFileContents = { + binaries: Record; + osPackages: Record>; + meta: { version: number }; +}; -const registryPath = getRegistryPath(); -const registry: Registry = existsSync(registryPath) ? readJsonSync(registryPath) : {}; +const getRegistryBinaryKey = (name: BinaryName, platform: BrowserPlatform): BinaryKey => `${name}_${platform}`; +const getRegistryOsPackagesKey = (name: OsName, version: OsVersion): OsPackagesKey => `${name}_${version}`; -let cliProgressBar: ReturnType | null = null; -let warnedFirstTimeInstall = false; +const getCliProgressBar = _.once(async () => { + const { createBrowserDownloadProgressBar } = await import("./cli-progress-bar"); -const getRegistryKey = (name: BinaryName, platform: BrowserPlatform): RegistryKey => `${name}_${platform}`; + return createBrowserDownloadProgressBar(); +}); -export const getBinaryPath = async (name: BinaryName, platform: BrowserPlatform, version: string): Promise => { - const registryKey = getRegistryKey(name, platform); +const logDownloadingOsPackagesWarningOnce = _.once((osName: string) => { + logger.warn(`Downloading extra ${osName} packages`); +}); - if (!registry[registryKey]) { - throw new Error(`Binary '${name}' on '${platform}' is not installed`); - } +const logDownloadingBrowsersWarningOnce = _.once(() => { + logger.warn("Downloading Testplane browsers"); + logger.warn("Note: this is one-time action. It may take a while..."); +}); - if (!registry[registryKey][version]) { - throw new Error(`Version '${version}' of driver '${name}' on '${platform}' is not installed`); - } +class Registry { + private registryPath = getRegistryPath(); + private registry = this.readRegistry(); + + public async getBinaryPath(name: BinaryName, platform: BrowserPlatform, version: string): Promise { + const registryKey = getRegistryBinaryKey(name, platform); - const binaryRelativePath = await registry[registryKey][version]; + if (!this.registry.binaries[registryKey]) { + throw new Error(`Binary '${name}' on '${platform}' is not installed`); + } + + if (!this.registry.binaries[registryKey][version]) { + throw new Error(`Version '${version}' of driver '${name}' on '${platform}' is not installed`); + } - browserInstallerDebug(`resolved '${name}@${version}' on ${platform} to ${binaryRelativePath}`); + const binaryRelativePath = await this.registry.binaries[registryKey][version]; - return path.resolve(registryPath, binaryRelativePath); -}; + browserInstallerDebug(`resolved '${name}@${version}' on ${platform} to ${binaryRelativePath}`); -const addBinaryToRegistry = ( - name: BinaryName, - platform: BrowserPlatform, - version: string, - absoluteBinaryPath: string, -): void => { - const registryKey = getRegistryKey(name, platform); - const relativePath = path.relative(registryPath, absoluteBinaryPath); - - registry[registryKey] ||= {}; - registry[registryKey][version] = relativePath; - - const replacer = (_: string, value: unknown): unknown | undefined => { - if ((value as Promise).then) { - return; + return path.resolve(this.registryPath, binaryRelativePath); + } + + public async getOsPackagesPath(name: OsName, version: OsVersion): Promise { + const registryKey = getRegistryOsPackagesKey(name, version); + + if (!this.registry.osPackages[registryKey]) { + throw new Error(`Packages for ${name}@${version} are not installed`); } - return value; - }; + const osPackagesRelativePath = await this.registry.osPackages[registryKey]; - browserInstallerDebug(`adding '${name}@${version}' on '${platform}' to registry at ${relativePath}`); - outputJSONSync(registryPath, registry, { replacer }); -}; + browserInstallerDebug(`resolved os packages for '${name}@${version}' to ${osPackagesRelativePath}`); -const getBinaryVersions = (name: BinaryName, platform: BrowserPlatform): string[] => { - const registryKey = getRegistryKey(name, platform); + return path.resolve(this.registryPath, osPackagesRelativePath); + } - if (!registry[registryKey]) { - return []; + public hasOsPackages(name: OsName, version: OsVersion): boolean { + return Boolean(this.registry.osPackages[getRegistryOsPackagesKey(name, version)]); } - return Object.keys(registry[registryKey]); -}; + public getMatchedDriverVersion( + driverName: SupportedDriver, + platform: BrowserPlatform, + browserVersion: string, + ): string | null { + const registryKey = getRegistryBinaryKey(driverName, platform); + + if (!this.registry.binaries[registryKey]) { + return null; + } + + if (driverName === Driver.CHROMEDRIVER || driverName === Driver.EDGEDRIVER) { + const milestone = getMilestone(browserVersion); + const buildIds = this.getBinaryVersions(driverName, platform); + const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(milestone)); + + if (!suitableBuildIds.length) { + return null; + } + + return suitableBuildIds.sort(semverVersionsComparator).pop() as string; + } -const hasBinaryVersion = (name: BinaryName, platform: BrowserPlatform, version: string): boolean => - getBinaryVersions(name, platform).includes(version); + if (driverName === Driver.GECKODRIVER) { + const buildIds = Object.keys(this.registry.binaries[registryKey]); + const buildIdsSorted = buildIds.sort(semverVersionsComparator); -export const getMatchedDriverVersion = ( - driverName: SupportedDriver, - platform: BrowserPlatform, - browserVersion: string, -): string | null => { - const registryKey = getRegistryKey(driverName, platform); + return buildIdsSorted.length ? buildIdsSorted[buildIdsSorted.length - 1] : null; + } - if (!registry[registryKey]) { return null; } - if (driverName === Driver.CHROMEDRIVER || driverName === Driver.EDGEDRIVER) { - const milestone = getMilestone(browserVersion); - const buildIds = getBinaryVersions(driverName, platform); - const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(milestone)); + public getMatchedBrowserVersion( + browserName: SupportedBrowser, + platform: BrowserPlatform, + browserVersion: string, + ): string | null { + const registryKey = getRegistryBinaryKey(browserName, platform); + + if (!this.registry.binaries[registryKey]) { + return null; + } + + let buildPrefix: string; + + switch (browserName) { + case Browser.CHROME: + buildPrefix = normalizeChromeVersion(browserVersion); + break; + + case Browser.CHROMIUM: + buildPrefix = getMilestone(browserVersion); + break; + + case Browser.FIREFOX: + buildPrefix = getFirefoxBuildId(browserVersion); + break; + + default: + return null; + } + + const buildIds = this.getBinaryVersions(browserName, platform); + const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(buildPrefix)); if (!suitableBuildIds.length) { return null; } - return suitableBuildIds.sort(semverVersionsComparator).pop() as string; - } + const firefoxVersionComparator = (a: string, b: string): number => { + a = a.slice(a.indexOf("_") + 1); + b = b.slice(b.indexOf("_") + 1); + + // Firefox has versions like "stable_131.0a1" and "stable_129.0b9" + // Parsing raw numbers as hex values is needed in order to distinguish "129.0b9" and "129.0b7" for example + return parseInt(a.replace(".", ""), 16) - parseInt(b.replace(".", ""), 16); + }; - if (driverName === Driver.GECKODRIVER) { - const buildIds = Object.keys(registry[registryKey]); - const buildIdsSorted = buildIds.sort(semverVersionsComparator); + const comparator = browserName === Browser.FIREFOX ? firefoxVersionComparator : semverVersionsComparator; + const suitableBuildIdsSorted = suitableBuildIds.sort(comparator); - return buildIdsSorted.length ? buildIdsSorted[buildIdsSorted.length - 1] : null; + return suitableBuildIdsSorted[suitableBuildIdsSorted.length - 1]; } - return null; -}; + public async installBinary( + name: BinaryName, + platform: BrowserPlatform, + version: string, + installFn: (downloadProgressCallback: DownloadProgressCallback) => Promise, + ): Promise { + const registryKey = getRegistryBinaryKey(name, platform); + + if (this.hasBinaryVersion(name, platform, version)) { + return this.getBinaryPath(name, platform, version); + } -export const getMatchedBrowserVersion = ( - browserName: SupportedBrowser, - platform: BrowserPlatform, - browserVersion: string, -): string | null => { - const registryKey = getRegistryKey(browserName, platform); + browserInstallerDebug(`installing '${name}@${version}' on '${platform}'`); - if (!registry[registryKey]) { - return null; - } + const progressBar = await getCliProgressBar(); - let buildPrefix: string; + const originalDownloadProgressCallback = progressBar.register(name, version); + const downloadProgressCallback: DownloadProgressCallback = (...args) => { + logDownloadingBrowsersWarningOnce(); - switch (browserName) { - case Browser.CHROME: - buildPrefix = normalizeChromeVersion(browserVersion); - break; + return originalDownloadProgressCallback(...args); + }; - case Browser.CHROMIUM: - buildPrefix = getMilestone(browserVersion); - break; + const installPromise = installFn(downloadProgressCallback) + .then(executablePath => { + this.addBinaryToRegistry(name, platform, version, executablePath); - case Browser.FIREFOX: - buildPrefix = getFirefoxBuildId(browserVersion); - break; + return executablePath; + }) + .catch(err => { + progressBar?.stop(); - default: - return null; + throw err; + }); + + this.registry.binaries[registryKey] ||= {}; + this.registry.binaries[registryKey][version] = installPromise; + + return installPromise; } - const buildIds = getBinaryVersions(browserName, platform); - const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(buildPrefix)); + public async installOsPackages( + osName: OsName, + version: OsVersion, + installFn: (downloadProgressCallback: DownloadProgressCallback) => Promise, + ): Promise { + const registryKey = getRegistryOsPackagesKey(osName, version); - if (!suitableBuildIds.length) { - return null; + if (this.hasOsPackages(osName, version)) { + return this.getOsPackagesPath(osName, version); + } + + browserInstallerDebug(`installing os packages for '${osName}@${version}'`); + + logDownloadingOsPackagesWarningOnce(osName); + + const progressBar = await getCliProgressBar(); + + const downloadProgressCallback = progressBar.register(`extra packages for ${osName}`, version); + + const installPromise = installFn(downloadProgressCallback) + .then(packagesPath => { + this.addOsPackageToRegistry(osName, version, packagesPath); + + return packagesPath; + }) + .catch(err => { + progressBar.stop(); + + throw err; + }); + + this.registry.osPackages[registryKey] = installPromise; + + return installPromise; } - const firefoxVersionComparator = (a: string, b: string): number => { - a = a.slice(a.indexOf("_") + 1); - b = b.slice(b.indexOf("_") + 1); + private readRegistry(): RegistryFileContents { + const registry: RegistryFileContents = fs.existsSync(this.registryPath) + ? fs.readJSONSync(this.registryPath) + : {}; - // Firefox has versions like "stable_131.0a1" and "stable_129.0b9" - // Parsing raw numbers as hex values is needed in order to distinguish "129.0b9" and "129.0b7" for example - return parseInt(a.replace(".", ""), 16) - parseInt(b.replace(".", ""), 16); - }; + registry.binaries ||= {} as Record; + registry.osPackages ||= {} as Record; + registry.meta ||= { version: 1 }; - const comparator = browserName === Browser.FIREFOX ? firefoxVersionComparator : semverVersionsComparator; - const suitableBuildIdsSorted = suitableBuildIds.sort(comparator); + return registry; + } - return suitableBuildIdsSorted[suitableBuildIdsSorted.length - 1]; -}; + private writeRegistry(): void { + const replacer = (_: string, value: unknown): unknown | undefined => { + if ((value as Promise).then) { + return; + } -export const installBinary = async ( - name: BinaryName, - platform: BrowserPlatform, - version: string, - installFn: (downloadProgressCallback: DownloadProgressCallback) => Promise, -): Promise => { - const registryKey = getRegistryKey(name, platform); + return value; + }; - if (hasBinaryVersion(name, platform, version)) { - return getBinaryPath(name, platform, version); + fs.outputJSONSync(this.registryPath, this.registry, { replacer }); } - browserInstallerDebug(`installing '${name}@${version}' on '${platform}'`); + private addBinaryToRegistry( + name: BinaryName, + platform: BrowserPlatform, + version: string, + absoluteBinaryPath: string, + ): void { + const registryKey = getRegistryBinaryKey(name, platform); + const relativePath = path.relative(this.registryPath, absoluteBinaryPath); + + this.registry.binaries[registryKey] ||= {}; + this.registry.binaries[registryKey][version] = relativePath; - if (!cliProgressBar) { - const { createBrowserDownloadProgressBar } = await import("./cli-progress-bar"); + browserInstallerDebug(`adding '${name}@${version}' on '${platform}' to registry at ${relativePath}`); - cliProgressBar = createBrowserDownloadProgressBar(); + this.writeRegistry(); } - const originalDownloadProgressCallback = cliProgressBar.register(name, version); - const downloadProgressCallback: DownloadProgressCallback = (...args) => { - if (!warnedFirstTimeInstall) { - logger.warn("Downloading Testplane browsers"); - logger.warn("Note: this is one-time action. It may take a while..."); + private addOsPackageToRegistry(name: OsName, version: OsVersion, absolutePackagesDirPath: string): void { + const registryKey = getRegistryOsPackagesKey(name, version); + const relativePath = path.relative(this.registryPath, absolutePackagesDirPath); - warnedFirstTimeInstall = true; - } + this.registry.osPackages[registryKey] = relativePath; - return originalDownloadProgressCallback(...args); - }; + browserInstallerDebug(`adding os packages for '${name}@${version}' to registry at ${relativePath}`); - const installPromise = installFn(downloadProgressCallback).then(executablePath => { - addBinaryToRegistry(name, platform, version, executablePath); + this.writeRegistry(); + } - return executablePath; - }); + private getBinaryVersions(name: BinaryName, platform: BrowserPlatform): string[] { + const registryKey = getRegistryBinaryKey(name, platform); - registry[registryKey] ||= {}; - registry[registryKey][version] = installPromise; + if (!this.registry.binaries[registryKey]) { + return []; + } - return installPromise; -}; + return Object.keys(this.registry.binaries[registryKey]); + } + + private hasBinaryVersion(name: BinaryName, platform: BrowserPlatform, version: string): boolean { + return this.getBinaryVersions(name, platform).includes(version); + } +} + +export default new Registry(); diff --git a/src/browser-installer/run.ts b/src/browser-installer/run.ts index 9e3adc45c..c2dde8ebb 100644 --- a/src/browser-installer/run.ts +++ b/src/browser-installer/run.ts @@ -1,21 +1,27 @@ import type { ChildProcess } from "child_process"; -import { Driver, type SupportedDriver } from "./utils"; +import { installBrowser } from "./install"; +import { Browser, type SupportedBrowser } from "./utils"; export const runBrowserDriver = async ( - driverName: SupportedDriver, + browserName: SupportedBrowser, browserVersion: string, { debug = false } = {}, ): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { - switch (driverName) { - case Driver.CHROMEDRIVER: + const installBrowserOpts = { shouldInstallWebDriver: true, shouldInstallUbuntuPackages: true }; + + await installBrowser(browserName, browserVersion, installBrowserOpts); + + switch (browserName) { + case Browser.CHROME: + case Browser.CHROMIUM: return import("./chrome").then(module => module.runChromeDriver(browserVersion, { debug })); - case Driver.EDGEDRIVER: - return import("./edge").then(module => module.runEdgeDriver(browserVersion, { debug })); - case Driver.GECKODRIVER: + case Browser.FIREFOX: return import("./firefox").then(module => module.runGeckoDriver(browserVersion, { debug })); - case Driver.SAFARIDRIVER: + case Browser.EDGE: + return import("./edge").then(module => module.runEdgeDriver(browserVersion, { debug })); + case Browser.SAFARI: return import("./safari").then(module => module.runSafariDriver({ debug })); default: - throw new Error(`Invalid driver name: ${driverName}. Expected one of: ${Object.values(Driver).join(", ")}`); + throw new Error(`Invalid browser: ${browserName}. Expected one of: ${Object.values(Browser).join(", ")}`); } }; diff --git a/src/browser-installer/ubuntu-packages/apt.ts b/src/browser-installer/ubuntu-packages/apt.ts new file mode 100644 index 000000000..64c88239c --- /dev/null +++ b/src/browser-installer/ubuntu-packages/apt.ts @@ -0,0 +1,162 @@ +import _ from "lodash"; +import os from "os"; +import path from "path"; +import fs from "fs-extra"; +import { exec } from "child_process"; +import { ensureUnixBinaryExists } from "./utils"; +import { browserInstallerDebug, type DownloadProgressCallback } from "../utils"; +import { MANDATORY_UBUNTU_PACKAGES_TO_BE_INSTALLED } from "../constants"; + +/** @link https://manpages.org/apt-cache/8 */ +const resolveTransitiveDependencies = async (directDependencies: string[]): Promise => { + await Promise.all(["apt-cache", "grep", "sort"].map(ensureUnixBinaryExists)); + + const aptDependsArgs = [ + "recurse", + "no-recommends", + "no-suggests", + "no-conflicts", + "no-breaks", + "no-replaces", + "no-enhances", + ] + .map(arg => `--${arg}`) + .join(" "); + + const listDependencies = (dependencyName: string): Promise => + new Promise((resolve, reject) => { + exec(`apt-cache depends ${aptDependsArgs} "${dependencyName}" | grep "^\\w" | sort -u`, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result.split(/\s+/).filter(Boolean)); + } + }); + }); + + const fullDependencies = await Promise.all(directDependencies.map(listDependencies)); + + const rawDependencies = _.flatten(fullDependencies); + + return _.uniq(rawDependencies); +}; + +/** @link https://manpages.org/apt/8 */ +const filterNotExistingDependencies = async (dependencies: string[]): Promise => { + if (!dependencies.length) { + return []; + } + + await ensureUnixBinaryExists("apt"); + + const existingDependencies = await new Promise((resolve, reject) => { + exec(`apt list ${dependencies.join(" ")} --installed`, (err, result) => { + if (err) { + reject(err); + } else { + const lines = result.split("\n"); + const existingDependencies = lines + .map(line => { + const slashIndex = line.indexOf("/"); + + if (slashIndex === -1) { + return ""; + } + + return line.slice(0, slashIndex); + }) + .filter(Boolean); + + resolve(existingDependencies); + } + }); + }); + + return _.difference(dependencies, existingDependencies); +}; + +/** @link https://manpages.org/apt-get/8 */ +const downloadUbuntuPackages = async (dependencies: string[], targetDir: string): Promise => { + if (!dependencies.length) { + return; + } + + await ensureUnixBinaryExists("apt-get"); + + return new Promise((resolve, reject) => { + exec(`apt-get download ${dependencies.join(" ")}`, { cwd: targetDir }, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +}; + +/** @link https://manpages.org/dpkg */ +const unpackUbuntuPackages = async (packagesDir: string, destination: string): Promise => { + await Promise.all([ensureUnixBinaryExists("dpkg"), fs.ensureDir(destination)]); + + return new Promise((resolve, reject) => { + exec(`for pkg in *.deb; do dpkg -x $pkg ${destination}; done`, { cwd: packagesDir }, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +}; + +export const installUbuntuPackages = async ( + packages: string[], + destination: string, + { downloadProgressCallback }: { downloadProgressCallback: DownloadProgressCallback }, +): Promise => { + if (!packages) { + browserInstallerDebug(`There are no ubuntu packages to install`); + + return; + } + + const withRecursiveDependencies = await resolveTransitiveDependencies(packages); + + downloadProgressCallback(40); + + browserInstallerDebug(`Resolved direct packages to ${withRecursiveDependencies.length} dependencies`); + + const dependenciesToDownload = await filterNotExistingDependencies(withRecursiveDependencies); + + downloadProgressCallback(70); + + const missingPkgs = MANDATORY_UBUNTU_PACKAGES_TO_BE_INSTALLED.filter(pkg => dependenciesToDownload.includes(pkg)); + + if (missingPkgs.length) { + throw new Error( + [ + "Missing some packages, which needs to be installed manually", + `Use \`apt-get install ${missingPkgs.join(" ")}\` to install them`, + `Then run "testplane install-deps" again\n`, + ].join("\n"), + ); + } + + browserInstallerDebug(`There are ${dependenciesToDownload.length} deb packages to download`); + + if (!dependenciesToDownload.length) { + return; + } + + const tmpPackagesDir = await fs.mkdtemp(path.join(os.tmpdir(), "testplane-ubuntu-apt-packages")); + + await downloadUbuntuPackages(dependenciesToDownload, tmpPackagesDir); + + downloadProgressCallback(100); + + browserInstallerDebug(`Downloaded ${dependenciesToDownload.length} deb packages`); + + await unpackUbuntuPackages(tmpPackagesDir, destination); + + browserInstallerDebug(`Unpacked ${dependenciesToDownload.length} deb packages`); +}; diff --git a/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-20-dependencies.json b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-20-dependencies.json new file mode 100644 index 000000000..3df0d3b05 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-20-dependencies.json @@ -0,0 +1,42 @@ +[ + "libasound2", + "libatk-bridge2.0-0", + "libatk1.0-0", + "libatspi2.0-0", + "libc6", + "libcairo2", + "libcups2", + "libdbus-1-3", + "libdbus-glib-1-2", + "libdrm2", + "libexpat1", + "libgbm1", + "libgcc-s1", + "libgdk-pixbuf2.0-0", + "libglib2.0-0", + "libgtk-3-0", + "libnspr4", + "libnss3", + "libpango-1.0-0", + "libpangocairo-1.0-0", + "libstdc++6", + "libudev1", + "libuuid1", + "libx11-6", + "libx11-xcb1", + "libxcb-dri3-0", + "libxcb1", + "libxcomposite1", + "libxcursor1", + "libxdamage1", + "libxext6", + "libxfixes3", + "libxi6", + "libxkbcommon0", + "libxrandr2", + "libxrender1", + "libxshmfence1", + "libxss1", + "libxt6", + "libxtst6" +] diff --git a/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-22-dependencies.json b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-22-dependencies.json new file mode 100644 index 000000000..20f5d7e60 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-22-dependencies.json @@ -0,0 +1,42 @@ +[ + "libasound2", + "libatk-bridge2.0-0", + "libatk1.0-0", + "libatspi2.0-0", + "libc6", + "libcairo2", + "libcups2", + "libdbus-1-3", + "libdbus-glib-1-2", + "libdrm2", + "libexpat1", + "libgbm1", + "libgcc-s1", + "libgdk-pixbuf-2.0-0", + "libglib2.0-0", + "libgtk-3-0", + "libnspr4", + "libnss3", + "libpango-1.0-0", + "libpangocairo-1.0-0", + "libstdc++6", + "libudev1", + "libuuid1", + "libx11-6", + "libx11-xcb1", + "libxcb-dri3-0", + "libxcb1", + "libxcomposite1", + "libxcursor1", + "libxdamage1", + "libxext6", + "libxfixes3", + "libxi6", + "libxkbcommon0", + "libxrandr2", + "libxrender1", + "libxshmfence1", + "libxss1", + "libxt6", + "libxtst6" +] diff --git a/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-24-dependencies.json b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-24-dependencies.json new file mode 100644 index 000000000..19f37be03 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/autogenerated/ubuntu-24-dependencies.json @@ -0,0 +1,42 @@ +[ + "libasound2t64", + "libatk-bridge2.0-0t64", + "libatk1.0-0t64", + "libatspi2.0-0t64", + "libc6", + "libcairo2", + "libcups2t64", + "libdbus-1-3", + "libdbus-glib-1-2", + "libdrm2", + "libexpat1", + "libgbm1", + "libgcc-s1", + "libgdk-pixbuf-2.0-0", + "libglib2.0-0t64", + "libgtk-3-0t64", + "libnspr4", + "libnss3", + "libpango-1.0-0", + "libpangocairo-1.0-0", + "libstdc++6", + "libudev1", + "libuuid1", + "libx11-6", + "libx11-xcb1", + "libxcb-dri3-0", + "libxcb1", + "libxcomposite1", + "libxcursor1", + "libxdamage1", + "libxext6", + "libxfixes3", + "libxi6", + "libxkbcommon0", + "libxrandr2", + "libxrender1", + "libxshmfence1", + "libxss1", + "libxt6t64", + "libxtst6" +] diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-downloader.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-downloader.ts new file mode 100644 index 000000000..74318d0c9 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-downloader.ts @@ -0,0 +1,37 @@ +import path from "path"; +import fs from "fs"; +import _ from "lodash"; +import { installBrowser } from "../.."; +import { getRegistryPath } from "../../utils"; +import type { RegistryFileContents } from "../../registry"; +import type { BrowserWithVersion } from "./utils"; + +const getRegistryBinaryPaths = (registry: RegistryFileContents): string[] => { + const versionToPathMap = Object.values(registry.binaries); + const binaryPaths = _.flatMap(versionToPathMap, Object.values); + const registryPath = getRegistryPath(); + + return binaryPaths.map(relativePath => path.resolve(registryPath, relativePath)); +}; + +/** @returns array of binary absolute paths */ +export const downloadBrowserVersions = async (browsers: BrowserWithVersion[]): Promise => { + if (!browsers.length) { + return []; + } + + const registryPath = getRegistryPath(); + + const installBinaries = ({ browserName, browserVersion }: BrowserWithVersion): Promise => + installBrowser(browserName, browserVersion, { + shouldInstallWebDriver: true, + shouldInstallUbuntuPackages: false, + }); + + await Promise.all(browsers.map(installBinaries)); + + const registryJson = await fs.promises.readFile(registryPath, { encoding: "utf8" }); + const registry = JSON.parse(registryJson); + + return getRegistryBinaryPaths(registry); +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts new file mode 100644 index 000000000..9507f727b --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts @@ -0,0 +1,20 @@ +import { retryFetch } from "../../../utils"; +import { CHROME_FOR_TESTING_VERSIONS_API_URL } from "../constants"; + +type ChromeVersionInfo = { + milestone: `${number}`; + version: `${number}.${number}.${number}.${number}`; + revision: `${number}`; +}; + +type ChromeVersionsApiResponse = { milestones: Record<`${number}`, ChromeVersionInfo> }; + +export const fetchChromeMilestoneVersions = async (): Promise => { + try { + const response = await retryFetch(CHROME_FOR_TESTING_VERSIONS_API_URL); + const data = (await response.json()) as ChromeVersionsApiResponse; + return Object.values(data.milestones).map(({ version }) => version); + } catch (err) { + throw new Error(`Couldn't get chrome versions: ${err}`); + } +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chromium.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chromium.ts new file mode 100644 index 000000000..f996cb322 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chromium.ts @@ -0,0 +1,15 @@ +import fs from "fs-extra"; +import { getBrowserPlatform } from "../../../utils"; + +export const fetchChromiumMilestoneVersions = async (): Promise => { + try { + const platform = getBrowserPlatform(); + + const revisionsPath = require.resolve(`../../../chromium/revisions/autogenerated/${platform}`); + const versions = await fs.readJSON(revisionsPath); + + return Object.keys(versions); + } catch (err) { + throw new Error(`Couldn't get chromium versions: ${err}`); + } +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts new file mode 100644 index 000000000..ad6e16552 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts @@ -0,0 +1,34 @@ +import _ from "lodash"; +import { getMilestone, retryFetch } from "../../../utils"; +import { FIREFOX_VERSIONS_API_URL } from "../constants"; +import { MIN_FIREFOX_VERSION } from "../../../constants"; + +type FirefoxVersionInfo = { + category: "major" | "esr" | "stability" | "dev"; + date: `${number}-${number}-${number}`; + version: string; +}; + +type FirefoxVersionsApiResponse = { releases: Record }; + +export const fetchFirefoxMilestoneVersions = async (): Promise => { + try { + const response = await retryFetch(FIREFOX_VERSIONS_API_URL); + const data = (await response.json()) as FirefoxVersionsApiResponse; + const stableVersions = Object.values(data.releases) + .filter(data => ["stability", "esr"].includes(data.category)) + .filter(data => Number(getMilestone(data.version)) >= MIN_FIREFOX_VERSION); + + const majorGrouped = _.groupBy(stableVersions, data => data.version.split(".")[0]); + + return Object.keys(majorGrouped).map(groupName => { + const versionsSorted = majorGrouped[groupName].sort((a, b) => { + return parseInt(a.version.replace(".", ""), 16) - parseInt(b.version.replace(".", ""), 16); + }); + + return versionsSorted.pop()?.version as string; + }); + } catch (err) { + throw new Error(`Couldn't get firefox versions: ${err}`); + } +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/index.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/index.ts new file mode 100644 index 000000000..43360df23 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/index.ts @@ -0,0 +1,18 @@ +import { fetchChromiumMilestoneVersions } from "./chromium"; +import { fetchChromeMilestoneVersions } from "./chrome"; +import { fetchFirefoxMilestoneVersions } from "./firefox"; +import type { BrowserWithVersion } from "../utils"; +import { Browser, type SupportedBrowser } from "../../../utils"; + +export const fetchBrowsersMilestones = async (): Promise => { + const createMapToBrowser = (browserName: SupportedBrowser) => (data: string[]) => + data.map(browserVersion => ({ browserName, browserVersion })); + + const [chromiumVersions, chromeVersions, firefoxVersions] = await Promise.all([ + fetchChromiumMilestoneVersions().then(createMapToBrowser(Browser.CHROME)), + fetchChromeMilestoneVersions().then(createMapToBrowser(Browser.CHROME)), + fetchFirefoxMilestoneVersions().then(createMapToBrowser(Browser.FIREFOX)), + ]); + + return [...chromiumVersions, ...chromeVersions, ...firefoxVersions]; +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/processed-browsers-linux.json b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/processed-browsers-linux.json new file mode 100644 index 000000000..b066ab001 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/processed-browsers-linux.json @@ -0,0 +1,192 @@ +{ + "downloadedBrowsers": { + "chrome": [ + "100", + "101", + "102", + "103", + "104", + "105", + "106", + "107", + "108", + "109", + "110", + "111", + "112", + "113", + "114", + "115", + "116", + "117", + "118", + "119", + "120", + "121", + "122", + "123", + "124", + "125", + "126", + "127", + "128", + "129", + "130", + "131", + "132", + "133", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "80", + "81", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99" + ], + "firefox": [ + "100", + "101", + "102", + "103", + "104", + "105", + "106", + "107", + "108", + "109", + "110", + "111", + "112", + "113", + "114", + "115", + "115", + "116", + "117", + "118", + "119", + "120", + "121", + "122", + "123", + "124", + "125", + "126", + "127", + "128", + "128", + "129", + "130", + "131", + "132", + "57", + "58", + "59", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "70", + "72", + "73", + "74", + "76", + "77", + "78", + "80", + "81", + "82", + "84", + "85", + "86", + "88", + "89", + "90", + "91", + "92", + "94", + "95", + "96", + "97", + "98", + "99" + ] + }, + "sharedObjects": [ + "ld-linux-x86-64.so.2", + "libX11-xcb.so.1", + "libX11.so.6", + "libXcomposite.so.1", + "libXcursor.so.1", + "libXdamage.so.1", + "libXext.so.6", + "libXfixes.so.3", + "libXi.so.6", + "libXrandr.so.2", + "libXrender.so.1", + "libXss.so.1", + "libXt.so.6", + "libXtst.so.6", + "libasound.so.2", + "libatk-1.0.so.0", + "libatk-bridge-2.0.so.0", + "libatspi.so.0", + "libc.so.6", + "libcairo.so.2", + "libcups.so.2", + "libdbus-1.so.3", + "libdbus-glib-1.so.2", + "libdl.so.2", + "libdrm.so.2", + "libexpat.so.1", + "libgbm.so.1", + "libgcc_s.so.1", + "libgdk-3.so.0", + "libgdk_pixbuf-2.0.so.0", + "libgio-2.0.so.0", + "libglib-2.0.so.0", + "libgobject-2.0.so.0", + "libgtk-3.so.0", + "libm.so.6", + "libnspr4.so", + "libnss3.so", + "libnssutil3.so", + "libpango-1.0.so.0", + "libpangocairo-1.0.so.0", + "libpthread.so.0", + "librt.so.1", + "libsmime3.so", + "libstdc++.so.6", + "libudev.so.1", + "libuuid.so.1", + "libxcb-dri3.so.0", + "libxcb.so.1", + "libxkbcommon.so.0", + "libxshmfence.so.1" + ] +} diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-20.json b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-20.json new file mode 100644 index 000000000..98a01b903 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-20.json @@ -0,0 +1,52 @@ +{ + "ld-linux-x86-64.so.2": "libc6", + "libX11-xcb.so.1": "libx11-xcb1", + "libX11.so.6": "libx11-6", + "libXcomposite.so.1": "libxcomposite1", + "libXcursor.so.1": "libxcursor1", + "libXdamage.so.1": "libxdamage1", + "libXext.so.6": "libxext6", + "libXfixes.so.3": "libxfixes3", + "libXi.so.6": "libxi6", + "libXrandr.so.2": "libxrandr2", + "libXrender.so.1": "libxrender1", + "libXss.so.1": "libxss1", + "libXt.so.6": "libxt6", + "libXtst.so.6": "libxtst6", + "libasound.so.2": "libasound2", + "libatk-1.0.so.0": "libatk1.0-0", + "libatk-bridge-2.0.so.0": "libatk-bridge2.0-0", + "libatspi.so.0": "libatspi2.0-0", + "libc.so.6": "libc6", + "libcairo.so.2": "libcairo2", + "libcups.so.2": "libcups2", + "libdbus-1.so.3": "libdbus-1-3", + "libdbus-glib-1.so.2": "libdbus-glib-1-2", + "libdl.so.2": "libc6", + "libdrm.so.2": "libdrm2", + "libexpat.so.1": "libexpat1", + "libgbm.so.1": "libgbm1", + "libgcc_s.so.1": "libgcc-s1", + "libgdk-3.so.0": "libgtk-3-0", + "libgdk_pixbuf-2.0.so.0": "libgdk-pixbuf2.0-0", + "libgio-2.0.so.0": "libglib2.0-0", + "libglib-2.0.so.0": "libglib2.0-0", + "libgobject-2.0.so.0": "libglib2.0-0", + "libgtk-3.so.0": "libgtk-3-0", + "libm.so.6": "libc6", + "libnspr4.so": "libnspr4", + "libnss3.so": "libnss3", + "libnssutil3.so": "libnss3", + "libpango-1.0.so.0": "libpango-1.0-0", + "libpangocairo-1.0.so.0": "libpangocairo-1.0-0", + "libpthread.so.0": "libc6", + "librt.so.1": "libc6", + "libsmime3.so": "libnss3", + "libstdc++.so.6": "libstdc++6", + "libudev.so.1": "libudev1", + "libuuid.so.1": "libuuid1", + "libxcb-dri3.so.0": "libxcb-dri3-0", + "libxcb.so.1": "libxcb1", + "libxkbcommon.so.0": "libxkbcommon0", + "libxshmfence.so.1": "libxshmfence1" +} diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-22.json b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-22.json new file mode 100644 index 000000000..f4d5b4268 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-22.json @@ -0,0 +1,52 @@ +{ + "ld-linux-x86-64.so.2": "libc6", + "libX11-xcb.so.1": "libx11-xcb1", + "libX11.so.6": "libx11-6", + "libXcomposite.so.1": "libxcomposite1", + "libXcursor.so.1": "libxcursor1", + "libXdamage.so.1": "libxdamage1", + "libXext.so.6": "libxext6", + "libXfixes.so.3": "libxfixes3", + "libXi.so.6": "libxi6", + "libXrandr.so.2": "libxrandr2", + "libXrender.so.1": "libxrender1", + "libXss.so.1": "libxss1", + "libXt.so.6": "libxt6", + "libXtst.so.6": "libxtst6", + "libasound.so.2": "libasound2", + "libatk-1.0.so.0": "libatk1.0-0", + "libatk-bridge-2.0.so.0": "libatk-bridge2.0-0", + "libatspi.so.0": "libatspi2.0-0", + "libc.so.6": "libc6", + "libcairo.so.2": "libcairo2", + "libcups.so.2": "libcups2", + "libdbus-1.so.3": "libdbus-1-3", + "libdbus-glib-1.so.2": "libdbus-glib-1-2", + "libdl.so.2": "libc6", + "libdrm.so.2": "libdrm2", + "libexpat.so.1": "libexpat1", + "libgbm.so.1": "libgbm1", + "libgcc_s.so.1": "libgcc-s1", + "libgdk-3.so.0": "libgtk-3-0", + "libgdk_pixbuf-2.0.so.0": "libgdk-pixbuf-2.0-0", + "libgio-2.0.so.0": "libglib2.0-0", + "libglib-2.0.so.0": "libglib2.0-0", + "libgobject-2.0.so.0": "libglib2.0-0", + "libgtk-3.so.0": "libgtk-3-0", + "libm.so.6": "libc6", + "libnspr4.so": "libnspr4", + "libnss3.so": "libnss3", + "libnssutil3.so": "libnss3", + "libpango-1.0.so.0": "libpango-1.0-0", + "libpangocairo-1.0.so.0": "libpangocairo-1.0-0", + "libpthread.so.0": "libc6", + "librt.so.1": "libc6", + "libsmime3.so": "libnss3", + "libstdc++.so.6": "libstdc++6", + "libudev.so.1": "libudev1", + "libuuid.so.1": "libuuid1", + "libxcb-dri3.so.0": "libxcb-dri3-0", + "libxcb.so.1": "libxcb1", + "libxkbcommon.so.0": "libxkbcommon0", + "libxshmfence.so.1": "libxshmfence1" +} diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-24.json b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-24.json new file mode 100644 index 000000000..32d0cfcc5 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/autogenerated/shared-objects-map-ubuntu-24.json @@ -0,0 +1,52 @@ +{ + "ld-linux-x86-64.so.2": "libc6", + "libX11-xcb.so.1": "libx11-xcb1", + "libX11.so.6": "libx11-6", + "libXcomposite.so.1": "libxcomposite1", + "libXcursor.so.1": "libxcursor1", + "libXdamage.so.1": "libxdamage1", + "libXext.so.6": "libxext6", + "libXfixes.so.3": "libxfixes3", + "libXi.so.6": "libxi6", + "libXrandr.so.2": "libxrandr2", + "libXrender.so.1": "libxrender1", + "libXss.so.1": "libxss1", + "libXt.so.6": "libxt6t64", + "libXtst.so.6": "libxtst6", + "libasound.so.2": "libasound2t64", + "libatk-1.0.so.0": "libatk1.0-0t64", + "libatk-bridge-2.0.so.0": "libatk-bridge2.0-0t64", + "libatspi.so.0": "libatspi2.0-0t64", + "libc.so.6": "libc6", + "libcairo.so.2": "libcairo2", + "libcups.so.2": "libcups2t64", + "libdbus-1.so.3": "libdbus-1-3", + "libdbus-glib-1.so.2": "libdbus-glib-1-2", + "libdl.so.2": "libc6", + "libdrm.so.2": "libdrm2", + "libexpat.so.1": "libexpat1", + "libgbm.so.1": "libgbm1", + "libgcc_s.so.1": "libgcc-s1", + "libgdk-3.so.0": "libgtk-3-0t64", + "libgdk_pixbuf-2.0.so.0": "libgdk-pixbuf-2.0-0", + "libgio-2.0.so.0": "libglib2.0-0t64", + "libglib-2.0.so.0": "libglib2.0-0t64", + "libgobject-2.0.so.0": "libglib2.0-0t64", + "libgtk-3.so.0": "libgtk-3-0t64", + "libm.so.6": "libc6", + "libnspr4.so": "libnspr4", + "libnss3.so": "libnss3", + "libnssutil3.so": "libnss3", + "libpango-1.0.so.0": "libpango-1.0-0", + "libpangocairo-1.0.so.0": "libpangocairo-1.0-0", + "libpthread.so.0": "libc6", + "librt.so.1": "libc6", + "libsmime3.so": "libnss3", + "libstdc++.so.6": "libstdc++6", + "libudev.so.1": "libudev1", + "libuuid.so.1": "libuuid1", + "libxcb-dri3.so.0": "libxcb-dri3-0", + "libxcb.so.1": "libxcb1", + "libxkbcommon.so.0": "libxkbcommon0", + "libxshmfence.so.1": "libxshmfence1" +} diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/cache/index.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/index.ts new file mode 100644 index 000000000..9e5efa206 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/cache/index.ts @@ -0,0 +1,133 @@ +import fs from "fs-extra"; +import _ from "lodash"; +import path from "path"; +import { getMilestone } from "../../../utils"; +import type { BrowserWithVersion } from "../utils"; + +type SharedObjectName = string; +type PackageName = string; + +type BrowserName = string; +type BrowserVersion = string; + +export type CacheData = { + /** Different for each ubuntu version */ + sharedObjectsMap: Record; + + /** Mutual for all linux versions */ + processedBrowsers: { + downloadedBrowsers: Record; + sharedObjects: string[]; + }; +}; + +const sortObject = (obj: T): T => { + if (_.isArray(obj)) { + return obj.sort(); + } + + if (!_.isPlainObject(obj)) { + return obj; + } + + const sourceObj = obj as T & Record; + const result = {} as T; + + const sortedKeys = Object.keys(sourceObj).sort() as Array; + + for (const key of sortedKeys) { + result[key] = sortObject(sourceObj[key]); + } + + return result; +}; + +export class Cache { + private _sharedObjectsMapPath: string; + private _processedBrowsersCachePath: string; + private _cache: CacheData = { + sharedObjectsMap: {}, + processedBrowsers: { downloadedBrowsers: {}, sharedObjects: [] }, + }; + + constructor(osVersion: string) { + const autoGeneratedDirPath = path.join(__dirname, "autogenerated"); + + this._sharedObjectsMapPath = path.join(autoGeneratedDirPath, `shared-objects-map-ubuntu-${osVersion}.json`); + this._processedBrowsersCachePath = path.join(autoGeneratedDirPath, "processed-browsers-linux.json"); + } + + async read(): Promise { + const [sharedObjectsMap, processedBrowsers] = await Promise.all([ + fs.readJSON(this._sharedObjectsMapPath).catch(() => null), + fs.readJSON(this._processedBrowsersCachePath).catch(() => null), + ]); + + if (sharedObjectsMap) { + this._cache.sharedObjectsMap = sharedObjectsMap; + } + + if (processedBrowsers) { + this._cache.processedBrowsers = processedBrowsers; + } + + return this; + } + + async write(): Promise { + const { sharedObjectsMap, processedBrowsers } = this._cache; + + const uniqSharedObjects = _.uniq(Object.keys(sharedObjectsMap).concat(processedBrowsers.sharedObjects)); + + processedBrowsers.sharedObjects = uniqSharedObjects; + + await fs.outputJSON(this._sharedObjectsMapPath, sortObject(sharedObjectsMap), { spaces: 4 }); + await fs.outputJSON(this._processedBrowsersCachePath, sortObject(processedBrowsers), { spaces: 4 }); + } + + private hasProcessedBrowser({ browserName, browserVersion }: BrowserWithVersion): boolean { + const processedBrowserVersions = this._cache.processedBrowsers.downloadedBrowsers[browserName]; + const milestone = getMilestone(browserVersion); + + return Boolean(processedBrowserVersions && processedBrowserVersions.includes(milestone)); + } + + filterProcessedBrowsers(browsers: BrowserWithVersion[]): BrowserWithVersion[] { + return browsers.filter(browser => !this.hasProcessedBrowser(browser)); + } + + private saveProcessedBrowser({ browserName, browserVersion }: BrowserWithVersion): void { + const browserCache = (this._cache.processedBrowsers.downloadedBrowsers[browserName] ||= []); + const milestone = getMilestone(browserVersion); + + if (!browserCache.includes(milestone)) { + browserCache.push(milestone); + } + } + + saveProcessedBrowsers(browsers: BrowserWithVersion[]): void { + browsers.forEach(browser => this.saveProcessedBrowser(browser)); + } + + hasResolvedPackageName(sharedObjectName: string): boolean { + return Boolean(this._cache.sharedObjectsMap[sharedObjectName]); + } + + getResolvedPackageName(sharedObjectName: string): string { + if (!this.hasResolvedPackageName(sharedObjectName)) { + throw new Error(`shared object [${sharedObjectName}] is not cached`); + } + + return this._cache.sharedObjectsMap[sharedObjectName]; + } + + savePackageName(sharedObjectName: string, packageName: string): void { + this._cache.sharedObjectsMap[sharedObjectName] = packageName; + } + + getUnresolvedSharedObjects(): string[] { + const resolvedSharedObjects = Object.keys(this._cache.sharedObjectsMap); + + return _.difference(this._cache.processedBrowsers.sharedObjects, resolvedSharedObjects); + } +} diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts new file mode 100644 index 000000000..56d8b0d49 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts @@ -0,0 +1,5 @@ +export const FIREFOX_VERSIONS_API_URL = "https://product-details.mozilla.org/1.0/firefox.json"; +export const CHROME_FOR_TESTING_VERSIONS_API_URL = + "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json"; +// Those are couldn't be seen with readelf -d +export const EXTRA_FIREFOX_SHARED_OBJECTS = ["libdbus-glib-1.so.2", "libXt.so.6"]; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts new file mode 100644 index 000000000..e70107a79 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/index.ts @@ -0,0 +1,69 @@ +import _ from "lodash"; +import { EXTRA_FIREFOX_SHARED_OBJECTS } from "./constants"; +import { getBinarySharedObjectDependencies, searchSharedObjectPackage } from "./shared-object"; +import { Cache } from "./cache"; +import { fetchBrowsersMilestones } from "./browser-versions/index"; +import { downloadBrowserVersions } from "./browser-downloader"; +import { getUbuntuMilestone, writeUbuntuPackageDependencies } from ".."; +import logger from "../../../utils/logger"; + +const createResolveSharedObjectToPackageName = + (cache: Cache) => + async (sharedObject: string): Promise => { + if (cache.hasResolvedPackageName(sharedObject)) { + return cache.getResolvedPackageName(sharedObject); + } + + const packageName = await searchSharedObjectPackage(sharedObject); + + cache.savePackageName(sharedObject, packageName); + + return packageName; + }; + +async function main(): Promise { + const ubuntuMilestone = await getUbuntuMilestone(); + + logger.log(`Detected ubuntu release: "${ubuntuMilestone}"`); + + const cache = await new Cache(ubuntuMilestone).read(); + + const browserVersions = await fetchBrowsersMilestones(); + + logger.log(`Fetched ${browserVersions.length} browser milestones`); + + const browsersToDownload = cache.filterProcessedBrowsers(browserVersions); + + logger.log(`There are ${browsersToDownload.length} browsers to download`); + + const binaryPaths = await downloadBrowserVersions(browsersToDownload); + + logger.log(`There are ${binaryPaths.length} binaries in registry (browsers with drivers)`); + + const downloadedBinarySharedObjectsArrays = await Promise.all(binaryPaths.map(getBinarySharedObjectDependencies)); + const downloadedBinarySharedObjects = _.flatten(downloadedBinarySharedObjectsArrays); + + const extraBinarySharedObjects = cache.getUnresolvedSharedObjects().concat(EXTRA_FIREFOX_SHARED_OBJECTS); + + const uniqSharedObjects = _.uniq(downloadedBinarySharedObjects.concat(extraBinarySharedObjects)); + + logger.log(`There are ${uniqSharedObjects.length} shared objects to resolve`); + + const resolveSharedObjectToPackageName = createResolveSharedObjectToPackageName(cache); + const ubuntuPackages = await Promise.all(uniqSharedObjects.map(resolveSharedObjectToPackageName)); + const uniqUbuntuPackages = _.uniq(ubuntuPackages).filter(Boolean); + + logger.log(`Resolved ${uniqSharedObjects.length} shared objects to ${uniqUbuntuPackages.length} packages`); + + cache.saveProcessedBrowsers(browsersToDownload); + + await cache.write(); + + logger.log("Saved cache to file system"); + + await writeUbuntuPackageDependencies(ubuntuMilestone, uniqUbuntuPackages); + + logger.log(`Saved ubuntu package direct dependencies for Ubuntu@${ubuntuMilestone}`); +} + +main(); diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts new file mode 100644 index 000000000..6feb9cf62 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts @@ -0,0 +1,34 @@ +import _ from "lodash"; +import calcLevenshtein from "js-levenshtein"; +import { readElf, aptFileSearch } from "./ubuntu"; + +export const searchSharedObjectPackage = async (sharedObject: string): Promise => { + const aptFileResult = await aptFileSearch(sharedObject); + + const packages = aptFileResult.split("\n").filter(Boolean); + + if (packages.includes("libc6")) { + return "libc6"; + } + + const relevantPackageName = _.minBy(packages, packageName => calcLevenshtein(sharedObject, packageName)) as string; + + return relevantPackageName; +}; + +export const getBinarySharedObjectDependencies = async (binaryPath: string): Promise => { + const sharedObjectRegExp = /^\s*\dx\d+\s\(NEEDED\)\s*Shared library: \[(.*)\]/gm; + + const readElfResult = await readElf(binaryPath, { dynamic: true }); + + let regExpResult = sharedObjectRegExp.exec(readElfResult); + const sharedObjectDependencies: string[] = []; + + while (regExpResult && regExpResult[1]) { + sharedObjectDependencies.push(regExpResult[1]); + + regExpResult = sharedObjectRegExp.exec(readElfResult); + } + + return sharedObjectDependencies; +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/apt-file.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/apt-file.ts new file mode 100644 index 000000000..9d4ebf034 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/apt-file.ts @@ -0,0 +1,23 @@ +import execa from "execa"; +import { getCliArgs } from "../utils"; +import { throwIfFailed } from "./utils"; +import { ensureUnixBinaryExists } from "../.."; + +const APT_FILE_BINARY_NAME = "apt-file"; + +/** + * @summary search in which package a file is included + * @returns name of the library, which can be downloaded via apt + * @link https://manpages.org/apt-file + */ +export const aptFileSearch = async (fileToSearch: string): Promise => { + await ensureUnixBinaryExists(APT_FILE_BINARY_NAME); + + const args = getCliArgs({ "package-only": true }); + + const result = await execa(APT_FILE_BINARY_NAME, ["search", fileToSearch, ...args]); + + throwIfFailed(result); + + return result.stdout; +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/index.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/index.ts new file mode 100644 index 000000000..2490649ea --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/index.ts @@ -0,0 +1,2 @@ +export * from "./readelf"; +export * from "./apt-file"; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/readelf.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/readelf.ts new file mode 100644 index 000000000..8462bab89 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/readelf.ts @@ -0,0 +1,21 @@ +import execa from "execa"; +import { getCliArgs } from "../utils"; +import { throwIfFailed } from "./utils"; +import { ensureUnixBinaryExists } from "../.."; + +const BINARY_NAME = "readelf"; +/** + * @summary get information about ELF files + * @link https://manpages.org/readelf + */ +export const readElf = async (filePath: string, opts?: { dynamic?: boolean }): Promise => { + await ensureUnixBinaryExists(BINARY_NAME); + + const args = getCliArgs({ ...opts, wide: true }); + + const result = await execa(BINARY_NAME, [filePath, ...args]); + + throwIfFailed(result); + + return result.stdout; +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/utils.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/utils.ts new file mode 100644 index 000000000..1bf8ac1c4 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/ubuntu/utils.ts @@ -0,0 +1,9 @@ +import type { ExecaReturnValue } from "execa"; + +export const throwIfFailed = (execaResult: ExecaReturnValue): void => { + const { exitCode, failed, command, stderr } = execaResult; + + if (failed) { + throw new Error(`Command "${command}" failed with exit code "${exitCode}". stderr:\n${stderr}`); + } +}; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts new file mode 100644 index 000000000..52fefd993 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts @@ -0,0 +1,14 @@ +import type { SupportedBrowser } from "../../utils"; + +export type BrowserWithVersion = { browserName: SupportedBrowser; browserVersion: string }; + +export const getCliArgs = >(flags?: T): string[] => { + if (!flags) { + return []; + } + + const keys = Object.keys(flags).filter(Boolean); + const enabledFlags = keys.filter(key => Boolean(flags[key])); + + return enabledFlags.map(flag => (flag.length === 1 ? `-${flag}` : `--${flag}`)); +}; diff --git a/src/browser-installer/ubuntu-packages/index.ts b/src/browser-installer/ubuntu-packages/index.ts new file mode 100644 index 000000000..ce2a95d8a --- /dev/null +++ b/src/browser-installer/ubuntu-packages/index.ts @@ -0,0 +1,108 @@ +import _ from "lodash"; +import fs from "fs-extra"; +import path from "path"; +import { getOsPackagesDir, type DownloadProgressCallback, browserInstallerDebug } from "../utils"; +import { installUbuntuPackages } from "./apt"; +import { getUbuntuMilestone } from "./utils"; +import logger from "../../utils/logger"; +import { LINUX_RUNTIME_LIBRARIES_PATH_ENV_NAME, LINUX_UBUNTU_RELEASE_ID } from "../constants"; +import registry from "../registry"; + +export { isUbuntu, getUbuntuMilestone, ensureUnixBinaryExists } from "./utils"; + +const getDependenciesArrayFilePath = (ubuntuMilestone: string): string => + path.join(__dirname, "autogenerated", `ubuntu-${ubuntuMilestone}-dependencies.json`); + +const readUbuntuPackageDependencies = async (ubuntuMilestone: string): Promise => { + try { + return await fs.readJSON(getDependenciesArrayFilePath(ubuntuMilestone)); + } catch (_) { + logger.warn( + [ + `Unable to read ubuntu dependencies for Ubuntu@${ubuntuMilestone}, as this version currently not supported`, + `Assuming all necessary packages are installed already`, + ].join("\n"), + ); + + return []; + } +}; + +export const writeUbuntuPackageDependencies = async (ubuntuMilestone: string, deps: string[]): Promise => { + const currentPackages = await readUbuntuPackageDependencies(ubuntuMilestone).catch(() => [] as string[]); + + const packagesToWrite = _.uniq(currentPackages.concat(deps)).sort(); + + await fs.outputJSON(getDependenciesArrayFilePath(ubuntuMilestone), packagesToWrite, { spaces: 4 }); +}; + +export const installUbuntuPackageDependencies = async (): Promise => { + const ubuntuMilestone = await getUbuntuMilestone(); + + browserInstallerDebug(`installing ubuntu${ubuntuMilestone} dependencies`); + + if (registry.hasOsPackages(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone)) { + browserInstallerDebug(`installing ubuntu${ubuntuMilestone} dependencies`); + + return registry.getOsPackagesPath(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone); + } + + const downloadFn = async (downloadProgressCallback: DownloadProgressCallback): Promise => { + const ubuntuPackageDependencies = await readUbuntuPackageDependencies(ubuntuMilestone); + const ubuntuPackagesDir = getOsPackagesDir(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone); + + await installUbuntuPackages(ubuntuPackageDependencies, ubuntuPackagesDir, { downloadProgressCallback }); + + return ubuntuPackagesDir; + }; + + return registry.installOsPackages(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone, downloadFn); +}; + +const listDirsAbsolutePath = async (dirBasePath: string, ...prefix: string[]): Promise => { + const fullDirPath = path.join(dirBasePath, ...prefix); + + if (!fs.existsSync(fullDirPath)) { + return []; + } + + const dirContents = await fs.readdir(fullDirPath); + const dirContentsAbsPaths = dirContents.map(obj => path.join(fullDirPath, obj)); + + const directories = [] as string[]; + + await Promise.all( + dirContentsAbsPaths.map(obj => + fs.stat(obj).then(stat => { + if (stat.isDirectory()) { + directories.push(obj); + } + }), + ), + ); + + return directories; +}; + +const getUbuntuLinkerEnvRaw = async (): Promise> => { + const ubuntuMilestone = await getUbuntuMilestone(); + + if (!registry.hasOsPackages(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone)) { + return {}; + } + + const ubuntuPackagesDir = await registry.getOsPackagesPath(LINUX_UBUNTU_RELEASE_ID, ubuntuMilestone); + + const currentRuntimeLibrariesEnvValue = process.env[LINUX_RUNTIME_LIBRARIES_PATH_ENV_NAME]; + + const [libDirs, usrLibDirs] = await Promise.all([ + listDirsAbsolutePath(ubuntuPackagesDir, "lib"), + listDirsAbsolutePath(ubuntuPackagesDir, "usr", "lib"), + ]); + + const libraryPaths = [...libDirs, ...usrLibDirs, currentRuntimeLibrariesEnvValue].filter(Boolean); + + return { [LINUX_RUNTIME_LIBRARIES_PATH_ENV_NAME]: libraryPaths.join(":") }; +}; + +export const getUbuntuLinkerEnv = _.once(getUbuntuLinkerEnvRaw); diff --git a/src/browser-installer/ubuntu-packages/utils.ts b/src/browser-installer/ubuntu-packages/utils.ts new file mode 100644 index 000000000..74c5889d2 --- /dev/null +++ b/src/browser-installer/ubuntu-packages/utils.ts @@ -0,0 +1,82 @@ +import _ from "lodash"; +import { exec } from "child_process"; +import fs from "fs"; +import { browserInstallerDebug } from "../utils"; +import { LINUX_UBUNTU_RELEASE_ID } from "../constants"; + +/** @link https://manpages.org/os-release/5 */ +const OS_RELEASE_PATH = "/etc/os-release"; + +type OsRelease = { + // General OS identification + NAME: string; + ID: string; + PRETTY_NAME: string; + ID_LIKE?: string; + CPE_NAME?: string; + VARIANT?: string; + VARIANT_ID?: string; + // Version identification + VERSION?: string; + VERSION_ID?: string; + VERSION_CODENAME?: string; + BUILD_ID?: string; + IMAGE_ID?: string; +}; + +/** @link https://manpages.org/which */ +export const ensureUnixBinaryExists = (binaryName: string): Promise => + new Promise((resolve, reject) => + exec(`which "${binaryName}"`, err => { + browserInstallerDebug(`Checking binary "${binaryName}" is installed: ${!err}`); + + if (err) { + reject(new Error(`Binary "${binaryName}" does not exist`)); + } else { + resolve(); + } + }), + ); + +/** @link https://manpages.org/os-release/5 */ +const osRelease = async (): Promise => { + if (!fs.existsSync(OS_RELEASE_PATH)) { + throw new Error(`"${OS_RELEASE_PATH}" is missing. Probably its not Linux`); + } + + const fileContents = await fs.promises.readFile(OS_RELEASE_PATH, "utf8"); + const result = {} as OsRelease; + + for (const line of fileContents.split("\n")) { + if (!line.includes("=")) { + continue; + } + + const splitPosition = line.indexOf("="); + const key = line.slice(0, splitPosition) as keyof OsRelease; + const value = line.slice(splitPosition + 1); + const valueIsWrappedWithQuotes = value.startsWith('"') && value.endsWith('"'); + + result[key] = valueIsWrappedWithQuotes ? value.slice(1, -1) : value; + } + + return result; +}; + +const osReleaseCached = _.once(osRelease); + +export const isUbuntu = async (): Promise => { + return osReleaseCached() + .then(release => release.ID === LINUX_UBUNTU_RELEASE_ID) + .catch(() => false); +}; + +export const getUbuntuMilestone = async (): Promise => { + const release = await osReleaseCached(); + + if (!release.VERSION_ID) { + throw new Error(`VERSION_ID is missing in ${OS_RELEASE_PATH}. Probably its not Ubuntu`); + } + + return release.VERSION_ID.split(".")[0] as string; +}; diff --git a/src/browser-installer/utils.ts b/src/browser-installer/utils.ts index c15947fa6..850a7209e 100644 --- a/src/browser-installer/utils.ts +++ b/src/browser-installer/utils.ts @@ -2,12 +2,12 @@ import { detectBrowserPlatform, BrowserPlatform, Browser as PuppeteerBrowser } f import extractZip from "extract-zip"; import os from "os"; import path from "path"; -import { createWriteStream } from "fs"; +import fs from "fs-extra"; import { Readable } from "stream"; import debug from "debug"; import { MIN_CHROMIUM_MAC_ARM_VERSION } from "./constants"; -export type DownloadProgressCallback = (downloadedBytes: number, totalBytes: number) => void; +export type DownloadProgressCallback = (done: number, total?: number) => void; export const browserInstallerDebug = debug("testplane:browser-installer"); @@ -29,21 +29,27 @@ export const Driver = { export type SupportedBrowser = (typeof Browser)[keyof typeof Browser]; export type SupportedDriver = (typeof Driver)[keyof typeof Driver]; -export const getDriverNameForBrowserName = (browserName: SupportedBrowser): SupportedDriver | null => { - if (browserName === Browser.CHROME || browserName === Browser.CHROMIUM) { - return Driver.CHROMEDRIVER; +export const getNormalizedBrowserName = ( + browserName?: string, +): Exclude | null => { + if (!browserName) { + return null; } - if (browserName === Browser.FIREFOX) { - return Driver.GECKODRIVER; + if (/chrome/i.test(browserName)) { + return Browser.CHROME; } - if (browserName === Browser.SAFARI) { - return Driver.SAFARIDRIVER; + if (/firefox/i.test(browserName)) { + return Browser.FIREFOX; } - if (browserName === Browser.EDGE) { - return Driver.EDGEDRIVER; + if (/edge/i.test(browserName)) { + return Browser.EDGE; + } + + if (/safari/i.test(browserName)) { + return Browser.SAFARI; } return null; @@ -129,6 +135,9 @@ const getDriversDir = (): string => path.join(getCacheDir(), "drivers"); const getDriverDir = (driverName: string, driverVersion: string): string => path.join(getDriversDir(), driverName, driverVersion); +export const getOsPackagesDir = (osName: string, osVersion: string): string => + path.join(getCacheDir(), "packages", osName, osVersion); + export const getGeckoDriverDir = (driverVersion: string): string => getDriverDir("geckodriver", getBrowserPlatform() + "-" + driverVersion); export const getEdgeDriverDir = (driverVersion: string): string => @@ -161,7 +170,7 @@ export const retryFetch = async ( }; export const downloadFile = async (url: string, filePath: string): Promise => { - const writeStream = createWriteStream(filePath); + const writeStream = fs.createWriteStream(filePath); const response = await fetch(url); if (!response.ok || !response.body) { diff --git a/src/browser-pool/webdriver-pool.ts b/src/browser-pool/webdriver-pool.ts index 715acb6b3..e102f49c1 100644 --- a/src/browser-pool/webdriver-pool.ts +++ b/src/browser-pool/webdriver-pool.ts @@ -1,14 +1,14 @@ import type { ChildProcess } from "child_process"; -import { runBrowserDriver, getDriverNameForBrowserName } from "../browser-installer"; -import type { SupportedBrowser, SupportedDriver } from "../browser-installer"; +import { runBrowserDriver, getNormalizedBrowserName } from "../browser-installer"; +import type { SupportedBrowser } from "../browser-installer"; -type DriverVersion = string; +type BrowserVersion = string; type Port = string; type ChildProcessWithStatus = { process: ChildProcess; gridUrl: string; isBusy: boolean }; export type WdProcess = { gridUrl: string; free: () => void; kill: () => void }; export class WebdriverPool { - private driverProcess: Map>>; + private driverProcess: Map>>; private portToDriverProcess: Map; constructor() { @@ -17,13 +17,13 @@ export class WebdriverPool { } async getWebdriver( - browserName: SupportedBrowser, - browserVersion: string, + browserName?: string, + browserVersion?: string, { debug = false } = {}, ): ReturnType { - const driverName = getDriverNameForBrowserName(browserName); + const browserNameNormalized = getNormalizedBrowserName(browserName); - if (!driverName) { + if (!browserNameNormalized) { throw new Error( [ `Couldn't run browser driver for "${browserName}", as this browser is not supported`, @@ -36,7 +36,7 @@ export class WebdriverPool { throw new Error(`Couldn't run browser driver for "${browserName}" because its version is undefined`); } - const wdProcesses = this.driverProcess.get(driverName)?.get(browserVersion) ?? {}; + const wdProcesses = this.driverProcess.get(browserNameNormalized)?.get(browserVersion) ?? {}; for (const port in wdProcesses) { if (!wdProcesses[port].isBusy) { @@ -45,12 +45,12 @@ export class WebdriverPool { return { gridUrl: wdProcesses[port].gridUrl, free: () => this.freeWebdriver(port), - kill: () => this.killWebdriver(driverName, browserVersion, port), + kill: () => this.killWebdriver(browserNameNormalized, browserVersion, port), }; } } - return this.createWebdriverProcess(driverName, browserVersion, { debug }); + return this.createWebdriverProcess(browserNameNormalized, browserVersion, { debug }); } private freeWebdriver(port: Port): void { @@ -61,9 +61,9 @@ export class WebdriverPool { } } - private killWebdriver(driverName: SupportedDriver, browserVersion: string, port: Port): void { + private killWebdriver(browserName: SupportedBrowser, browserVersion: string, port: Port): void { const wdProcess = this.portToDriverProcess.get(port); - const nodes = this.driverProcess.get(driverName)?.get(browserVersion); + const nodes = this.driverProcess.get(browserName)?.get(browserVersion); if (wdProcess && nodes) { wdProcess.process.kill(); @@ -73,21 +73,21 @@ export class WebdriverPool { } private async createWebdriverProcess( - driverName: SupportedDriver, + browserName: SupportedBrowser, browserVersion: string, { debug = false } = {}, ): Promise { - const driver = await runBrowserDriver(driverName, browserVersion, { debug }); + const driver = await runBrowserDriver(browserName, browserVersion, { debug }); - if (!this.driverProcess.has(driverName)) { - this.driverProcess.set(driverName, new Map()); + if (!this.driverProcess.has(browserName)) { + this.driverProcess.set(browserName, new Map()); } - if (!this.driverProcess.get(driverName)?.has(browserVersion)) { - this.driverProcess.get(driverName)?.set(browserVersion, {}); + if (!this.driverProcess.get(browserName)?.has(browserVersion)) { + this.driverProcess.get(browserName)?.set(browserVersion, {}); } - const nodes = this.driverProcess.get(driverName)?.get(browserVersion) as Record; + const nodes = this.driverProcess.get(browserName)?.get(browserVersion) as Record; const node = { process: driver.process, gridUrl: driver.gridUrl, isBusy: true }; nodes[driver.port] = node; @@ -97,7 +97,7 @@ export class WebdriverPool { return { gridUrl: driver.gridUrl, free: () => this.freeWebdriver(String(driver.port)), - kill: () => this.killWebdriver(driverName, browserVersion, String(driver.port)), + kill: () => this.killWebdriver(browserName, browserVersion, String(driver.port)), }; } } diff --git a/src/browser/new-browser.ts b/src/browser/new-browser.ts index 58f27c8ff..44b47a330 100644 --- a/src/browser/new-browser.ts +++ b/src/browser/new-browser.ts @@ -13,7 +13,6 @@ import { DEVTOOLS_PROTOCOL, WEBDRIVER_PROTOCOL, LOCAL_GRID_URL } from "../consta import { Config } from "../config"; import { BrowserConfig } from "../config/browser-config"; import { gridUrl as DEFAULT_GRID_URL } from "../config/defaults"; -import { installBrowser, type SupportedBrowser } from "../browser-installer"; export type CapabilityName = "goog:chromeOptions" | "moz:firefoxOptions" | "ms:edgeOptions"; export type HeadlessBrowserOptions = Record< @@ -214,8 +213,8 @@ export class NewBrowser extends Browser { } this._wdProcess = await this._wdPool.getWebdriver( - this._config.desiredCapabilities?.browserName as SupportedBrowser, - this._config.desiredCapabilities?.browserVersion as string, + this._config.desiredCapabilities?.browserName, + this._config.desiredCapabilities?.browserVersion, { debug: this._config.system.debug }, ); @@ -226,13 +225,26 @@ export class NewBrowser extends Browser { config: BrowserConfig, capabilities: WebdriverIO.Capabilities, ): Promise { - const browserNameLowerCase = config.desiredCapabilities?.browserName?.toLowerCase() as string; + const { getNormalizedBrowserName, installBrowser } = await import("../browser-installer"); + const normalizedBrowserName = getNormalizedBrowserName(this._config.desiredCapabilities?.browserName); + + if (!normalizedBrowserName) { + throw new Error( + [ + `Running auto local "${this._config.desiredCapabilities?.browserName}" is unsupported`, + `Supported browsers: "chrome", "firefox", "safari", "edge"`, + ].join("\n"), + ); + } + const executablePath = await installBrowser( - this._config.desiredCapabilities?.browserName as SupportedBrowser, - this._config.desiredCapabilities?.browserVersion as string, + normalizedBrowserName, + this._config.desiredCapabilities?.browserVersion, + { shouldInstallWebDriver: false, shouldInstallUbuntuPackages: true }, ); if (executablePath) { + const browserNameLowerCase = config.desiredCapabilities?.browserName?.toLowerCase() as string; const { capabilityName } = headlessBrowserOptions[browserNameLowerCase]; capabilities[capabilityName] ||= {}; capabilities[capabilityName]!.binary ||= executablePath; diff --git a/test/src/browser-installer/chrome/browser.ts b/test/src/browser-installer/chrome/browser.ts index 31a4c6f57..2192c5683 100644 --- a/test/src/browser-installer/chrome/browser.ts +++ b/test/src/browser-installer/chrome/browser.ts @@ -18,6 +18,9 @@ describe("browser-installer/chrome/browser", () => { let getMatchedBrowserVersionStub: SinonStub; let installBinaryStub: SinonStub; + let installChromeDriverStub: SinonStub; + let installUbuntuPackageDependenciesStub: SinonStub; + beforeEach(() => { installChromiumStub = sandbox.stub().resolves("/chromium/browser/path"); @@ -29,17 +32,24 @@ describe("browser-installer/chrome/browser", () => { getMatchedBrowserVersionStub = sandbox.stub().returns(null); installBinaryStub = sandbox.stub(); + installChromeDriverStub = sandbox.stub(); + installUbuntuPackageDependenciesStub = sandbox.stub(); + installChrome = proxyquire("../../../../src/browser-installer/chrome/browser", { + "./driver": { installChromeDriver: installChromeDriverStub }, "../chromium": { installChromium: installChromiumStub }, + "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, "@puppeteer/browsers": { resolveBuildId: resolveBuildIdStub, install: puppeteerInstallStub, canDownload: canDownloadStub, }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedBrowserVersion: getMatchedBrowserVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, }, }).installChrome; }); @@ -108,4 +118,16 @@ describe("browser-installer/chrome/browser", () => { ].join("\n"), ); }); + + it("should try to install chromedriver if 'needWebDriver' is set", async () => { + await installChrome("115", { needWebDriver: true }); + + assert.calledOnceWith(installChromeDriverStub, "115", { force: false }); + }); + + it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { + await installChrome("115", { needUbuntuPackages: true }); + + assert.calledOnceWith(installUbuntuPackageDependenciesStub); + }); }); diff --git a/test/src/browser-installer/chrome/driver.ts b/test/src/browser-installer/chrome/driver.ts index ff0464d8a..11e8673d7 100644 --- a/test/src/browser-installer/chrome/driver.ts +++ b/test/src/browser-installer/chrome/driver.ts @@ -37,9 +37,11 @@ describe("browser-installer/chrome/driver", () => { canDownload: canDownloadStub, }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedDriverVersion: getMatchedDriverVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, }, }).installChromeDriver; }); diff --git a/test/src/browser-installer/chrome/index.ts b/test/src/browser-installer/chrome/index.ts index 4944f884c..3244f2edb 100644 --- a/test/src/browser-installer/chrome/index.ts +++ b/test/src/browser-installer/chrome/index.ts @@ -16,6 +16,10 @@ describe("browser-installer/chrome", () => { let getPortStub: SinonStub; let waitPortStub: SinonStub; + let isUbuntuStub: SinonStub; + let getUbuntuLinkerEnvStub: SinonStub; + let installUbuntuPackageDependenciesStub: SinonStub; + beforeEach(() => { pipeLogsWithPrefixStub = sandbox.stub(); installChromeStub = sandbox.stub().resolves("/browser/path"); @@ -24,10 +28,19 @@ describe("browser-installer/chrome", () => { getPortStub = sandbox.stub().resolves(12345); waitPortStub = sandbox.stub().resolves(); + isUbuntuStub = sandbox.stub().resolves(false); + getUbuntuLinkerEnvStub = sandbox.stub().resolves({ LD_LINKER_PATH: "foobar" }); + installUbuntuPackageDependenciesStub = sandbox.stub().resolves(); + runChromeDriver = proxyquire("../../../../src/browser-installer/chrome", { "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, "./driver": { installChromeDriver: installChromeDriverStub }, "./browser": { installChrome: installChromeStub }, + "../ubuntu-packages": { + isUbuntu: isUbuntuStub, + getUbuntuLinkerEnv: getUbuntuLinkerEnvStub, + installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub, + }, child_process: { spawn: spawnStub }, // eslint-disable-line camelcase "wait-port": waitPortStub, "get-port": getPortStub, @@ -84,4 +97,45 @@ describe("browser-installer/chrome", () => { assert.notCalled(pipeLogsWithPrefixStub); }); + + describe("ubuntu", () => { + it(`should not try to install ubuntu packages if its not ubuntu`, async () => { + isUbuntuStub.resolves(false); + + await runChromeDriver("130"); + + assert.notCalled(installUbuntuPackageDependenciesStub); + }); + + it(`should not set ubuntu linker env variables if its not ubuntu`, async () => { + installChromeDriverStub.resolves("/driver/path"); + getPortStub.resolves(10050); + isUbuntuStub.resolves(false); + + await runChromeDriver("130"); + + assert.notCalled(getUbuntuLinkerEnvStub); + assert.calledOnceWith(spawnStub, sinon.match.string, sinon.match.array, { + windowsHide: true, + detached: false, + env: process.env, + }); + }); + + it(`should set ubuntu linker env variables if its ubuntu`, async () => { + isUbuntuStub.resolves(true); + getUbuntuLinkerEnvStub.resolves({ foo: "bar" }); + + await runChromeDriver("130"); + + assert.calledOnceWith(spawnStub, sinon.match.string, sinon.match.array, { + windowsHide: true, + detached: false, + env: { + ...process.env, + foo: "bar", + }, + }); + }); + }); }); diff --git a/test/src/browser-installer/chromium/browser.ts b/test/src/browser-installer/chromium/browser.ts index 0084e3d36..fdf9d2f22 100644 --- a/test/src/browser-installer/chromium/browser.ts +++ b/test/src/browser-installer/chromium/browser.ts @@ -32,9 +32,11 @@ describe("browser-installer/chromium/browser", () => { }, "./utils": { getChromiumBuildId: getChromiumBuildIdStub }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedBrowserVersion: getMatchedBrowserVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, }, }).installChromium; }); diff --git a/test/src/browser-installer/chromium/driver.ts b/test/src/browser-installer/chromium/driver.ts index 969a8c982..394e65b7c 100644 --- a/test/src/browser-installer/chromium/driver.ts +++ b/test/src/browser-installer/chromium/driver.ts @@ -20,7 +20,7 @@ describe("browser-installer/chromium/driver", () => { ...require("../../../../src/browser-installer/utils"), retryFetch: retryFetchStub, }, - "../registry": { installBinary: installBinaryStub }, + "../registry": { default: { installBinary: installBinaryStub } }, }).installChromeDriverManually; }); diff --git a/test/src/browser-installer/edge/driver.ts b/test/src/browser-installer/edge/driver.ts index 6eea60c69..f7f9c4b39 100644 --- a/test/src/browser-installer/edge/driver.ts +++ b/test/src/browser-installer/edge/driver.ts @@ -28,9 +28,11 @@ describe("browser-installer/edge/driver", () => { retryFetch: retryFetchStub, }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedDriverVersion: getMatchedDriverVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, }, }).installEdgeDriver; }); diff --git a/test/src/browser-installer/firefox/browser.ts b/test/src/browser-installer/firefox/browser.ts index 5bfe0eeaa..9b64b2cb1 100644 --- a/test/src/browser-installer/firefox/browser.ts +++ b/test/src/browser-installer/firefox/browser.ts @@ -15,6 +15,9 @@ describe("browser-installer/firefox/browser", () => { let getMatchedBrowserVersionStub: SinonStub; let installBinaryStub: SinonStub; + let installLatestGeckoDriverStub: SinonStub; + let installUbuntuPackageDependenciesStub: SinonStub; + beforeEach(() => { puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/firefox/browser/path" }); canDownloadStub = sandbox.stub().resolves(true); @@ -23,15 +26,22 @@ describe("browser-installer/firefox/browser", () => { getMatchedBrowserVersionStub = sandbox.stub().returns(null); installBinaryStub = sandbox.stub(); + installLatestGeckoDriverStub = sandbox.stub(); + installUbuntuPackageDependenciesStub = sandbox.stub(); + installFirefox = proxyquire("../../../../src/browser-installer/firefox/browser", { + "./driver": { installLatestGeckoDriver: installLatestGeckoDriverStub }, + "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, "@puppeteer/browsers": { install: puppeteerInstallStub, canDownload: canDownloadStub, }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedBrowserVersion: getMatchedBrowserVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, }, }).installFirefox; }); @@ -84,4 +94,16 @@ describe("browser-installer/firefox/browser", () => { ].join("\n"), ); }); + + it("should try to install geckodriver if 'needWebDriver' is set", async () => { + await installFirefox("115", { needWebDriver: true }); + + assert.calledOnceWith(installLatestGeckoDriverStub, "115", { force: false }); + }); + + it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { + await installFirefox("115", { needUbuntuPackages: true }); + + assert.calledOnceWith(installUbuntuPackageDependenciesStub); + }); }); diff --git a/test/src/browser-installer/firefox/driver.ts b/test/src/browser-installer/firefox/driver.ts index 221f2a90e..4fbd8c4fe 100644 --- a/test/src/browser-installer/firefox/driver.ts +++ b/test/src/browser-installer/firefox/driver.ts @@ -28,9 +28,11 @@ describe("browser-installer/firefox/driver", () => { retryFetch: retryFetchStub, }, "../registry": { - getBinaryPath: getBinaryPathStub, - getMatchedDriverVersion: getMatchedDriverVersionStub, - installBinary: installBinaryStub, + default: { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, }, }).installLatestGeckoDriver; }); diff --git a/test/src/browser-installer/firefox/index.ts b/test/src/browser-installer/firefox/index.ts index 2c457bc97..d48179d12 100644 --- a/test/src/browser-installer/firefox/index.ts +++ b/test/src/browser-installer/firefox/index.ts @@ -16,6 +16,10 @@ describe("browser-installer/firefox", () => { let getPortStub: SinonStub; let waitPortStub: SinonStub; + let isUbuntuStub: SinonStub; + let getUbuntuLinkerEnvStub: SinonStub; + let installUbuntuPackageDependenciesStub: SinonStub; + beforeEach(() => { pipeLogsWithPrefixStub = sandbox.stub(); installFirefoxStub = sandbox.stub().resolves("/browser/path"); @@ -24,10 +28,19 @@ describe("browser-installer/firefox", () => { getPortStub = sandbox.stub().resolves(12345); waitPortStub = sandbox.stub().resolves(); + isUbuntuStub = sandbox.stub().resolves(false); + getUbuntuLinkerEnvStub = sandbox.stub().resolves({ LD_LINKER_PATH: "foobar" }); + installUbuntuPackageDependenciesStub = sandbox.stub().resolves(); + runGeckoDriver = proxyquire("../../../../src/browser-installer/firefox", { "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, "./browser": { installFirefox: installFirefoxStub }, "./driver": { installLatestGeckoDriver: installLatestGeckoDriverStub }, + "../ubuntu-packages": { + isUbuntu: isUbuntuStub, + getUbuntuLinkerEnv: getUbuntuLinkerEnvStub, + installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub, + }, geckodriver: { start: startGeckoDriverStub }, "wait-port": waitPortStub, "get-port": getPortStub, @@ -49,6 +62,7 @@ describe("browser-installer/firefox", () => { spawnOpts: { windowsHide: true, detached: false, + env: process.env, }, }); }); @@ -86,7 +100,7 @@ describe("browser-installer/firefox", () => { customGeckoDriverPath: "/driver/path", port: 12345, log: "debug", - spawnOpts: { windowsHide: true, detached: false }, + spawnOpts: { windowsHide: true, detached: false, env: process.env }, }); assert.calledOnceWith(pipeLogsWithPrefixStub, result.process, "[geckodriver@130] "); }); @@ -96,4 +110,53 @@ describe("browser-installer/firefox", () => { assert.notCalled(pipeLogsWithPrefixStub); }); + + describe("ubuntu", () => { + it(`should not try to install ubuntu packages if its not ubuntu`, async () => { + isUbuntuStub.resolves(false); + + await runGeckoDriver("130"); + + assert.notCalled(installUbuntuPackageDependenciesStub); + }); + + it(`should not set ubuntu linker env variables if its not ubuntu`, async () => { + installLatestGeckoDriverStub.resolves("/driver/path"); + getPortStub.resolves(10050); + isUbuntuStub.resolves(false); + + await runGeckoDriver("130"); + + assert.notCalled(getUbuntuLinkerEnvStub); + assert.calledOnceWith(startGeckoDriverStub, { + customGeckoDriverPath: "/driver/path", + port: 10050, + log: "fatal", + spawnOpts: { + windowsHide: true, + detached: false, + env: process.env, + }, + }); + }); + + it(`should set ubuntu linker env variables if its ubuntu`, async () => { + isUbuntuStub.resolves(true); + getUbuntuLinkerEnvStub.resolves({ foo: "bar" }); + + await runGeckoDriver("130"); + + assert.calledOnceWith( + startGeckoDriverStub, + sinon.match({ + spawnOpts: { + env: { + ...process.env, + foo: "bar", + }, + }, + }), + ); + }); + }); }); diff --git a/test/src/browser-installer/install.ts b/test/src/browser-installer/install.ts index cff6f5435..8c2cf00b3 100644 --- a/test/src/browser-installer/install.ts +++ b/test/src/browser-installer/install.ts @@ -4,6 +4,7 @@ import type { installBrowser as InstallBrowser, installBrowsersWithDrivers as InstallBrowsersWithDrivers, } from "../../../src/browser-installer/install"; +import { Browser } from "../../../src/browser-installer/utils"; describe("browser-installer/install", () => { const sandbox = sinon.createSandbox(); @@ -17,6 +18,9 @@ describe("browser-installer/install", () => { let installLatestGeckoDriverStub: SinonStub; let installEdgeDriverStub: SinonStub; + let isUbuntuStub: SinonStub; + let installUbuntuPackageDependenciesStub: SinonStub; + beforeEach(() => { installChromeStub = sandbox.stub(); installChromeDriverStub = sandbox.stub(); @@ -24,10 +28,17 @@ describe("browser-installer/install", () => { installLatestGeckoDriverStub = sandbox.stub(); installEdgeDriverStub = sandbox.stub(); + isUbuntuStub = sandbox.stub().resolves(false); + installUbuntuPackageDependenciesStub = sandbox.stub().resolves(); + const installer = proxyquire("../../../src/browser-installer/install", { "./chrome": { installChrome: installChromeStub, installChromeDriver: installChromeDriverStub }, "./edge": { installEdgeDriver: installEdgeDriverStub }, "./firefox": { installFirefox: installFirefoxStub, installLatestGeckoDriver: installLatestGeckoDriverStub }, + "./ubuntu-packages": { + isUbuntu: isUbuntuStub, + installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub, + }, }); installBrowser = installer.installBrowser; @@ -36,98 +47,140 @@ describe("browser-installer/install", () => { afterEach(() => sandbox.restore()); - [true, false].forEach(force => { - describe(`installBrowser, force: ${force}`, () => { - describe("chrome", () => { - it("should install browser", async () => { - installChromeStub.withArgs("115").resolves("/browser/path"); - - const binaryPath = await installBrowser("chrome", "115", { force }); - - assert.equal(binaryPath, "/browser/path"); - assert.calledOnceWith(installChromeStub, "115", { force }); - assert.notCalled(installChromeDriverStub); + describe(`installBrowser`, () => { + [true, false].forEach(force => { + describe(`force: ${force}`, () => { + describe("chrome", () => { + it("should install browser", async () => { + installChromeStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser(Browser.CHROME, "115", { force }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installChromeStub, "115", { + force, + needUbuntuPackages: false, + needWebDriver: false, + }); + }); + + it("should install browser with webdriver", async () => { + installChromeStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser(Browser.CHROME, "115", { + force, + shouldInstallWebDriver: true, + }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installChromeStub, "115", { + force, + needUbuntuPackages: false, + needWebDriver: true, + }); + }); }); - it("should install browser with webdriver", async () => { - installChromeStub.withArgs("115").resolves("/browser/path"); - - const binaryPath = await installBrowser("chrome", "115", { force, installWebDriver: true }); - - assert.equal(binaryPath, "/browser/path"); - assert.calledOnceWith(installChromeStub, "115", { force }); - assert.calledOnceWith(installChromeDriverStub, "115", { force }); + describe("firefox", () => { + it("should install browser", async () => { + installFirefoxStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser(Browser.FIREFOX, "115", { force }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installFirefoxStub, "115", { + force, + needUbuntuPackages: false, + needWebDriver: false, + }); + }); + + it("should install browser with webdriver", async () => { + installFirefoxStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser(Browser.FIREFOX, "115", { + force, + shouldInstallWebDriver: true, + }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installFirefoxStub, "115", { + force, + needUbuntuPackages: false, + needWebDriver: true, + }); + }); }); - }); - describe("firefox", () => { - it("should install browser", async () => { - installFirefoxStub.withArgs("115").resolves("/browser/path"); + describe("edge", () => { + it("should return null", async () => { + const binaryPath = await installBrowser("MicrosoftEdge", "115", { force }); - const binaryPath = await installBrowser("firefox", "115", { force }); + assert.equal(binaryPath, null); + assert.notCalled(installEdgeDriverStub); + }); - assert.equal(binaryPath, "/browser/path"); - assert.calledOnceWith(installFirefoxStub, "115", { force }); - assert.notCalled(installLatestGeckoDriverStub); - }); - - it("should install browser with webdriver", async () => { - installFirefoxStub.withArgs("115").resolves("/browser/path"); - - const binaryPath = await installBrowser("firefox", "115", { force, installWebDriver: true }); + it("should install webdriver", async () => { + const binaryPath = await installBrowser("MicrosoftEdge", "115", { + force, + shouldInstallWebDriver: true, + }); - assert.equal(binaryPath, "/browser/path"); - assert.calledOnceWith(installFirefoxStub, "115", { force }); - assert.calledOnceWith(installLatestGeckoDriverStub, "115", { force }); + assert.equal(binaryPath, null); + assert.calledOnceWith(installEdgeDriverStub, "115", { force }); + }); }); - }); - describe("edge", () => { - it("should return null", async () => { - const binaryPath = await installBrowser("MicrosoftEdge", "115", { force }); + describe("safari", () => { + it("should return null", async () => { + const binaryPath = await installBrowser("safari", "115", { + force, + shouldInstallWebDriver: true, + }); - assert.equal(binaryPath, null); - assert.notCalled(installEdgeDriverStub); + assert.equal(binaryPath, null); + }); }); - it("should install webdriver", async () => { - const binaryPath = await installBrowser("MicrosoftEdge", "115", { force, installWebDriver: true }); - - assert.equal(binaryPath, null); - assert.calledOnceWith(installEdgeDriverStub, "115", { force }); + it("should throw exception on empty browser version", async () => { + await assert.isRejected( + installBrowser(Browser.CHROME, "", { force }), + /Couldn't install browser 'chrome' because it has invalid version: ''/, + ); }); }); + }); + }); - describe("safari", () => { - it("should return null", async () => { - const binaryPath = await installBrowser("safari", "115", { force, installWebDriver: true }); + describe("installBrowsersWithDrivers", () => { + it("should force install browser with driver", async () => { + await installBrowsersWithDrivers([{ browserName: "chrome", browserVersion: "115" }]); - assert.equal(binaryPath, null); - }); + assert.calledOnceWith(installChromeStub, "115", { + force: true, + needUbuntuPackages: false, + needWebDriver: true, }); + }); - it("should throw exception on unsupported browser name", async () => { - await assert.isRejected( - installBrowser("foobar", "115", { force }), - /Couldn't install browser 'foobar', as it is not supported/, - ); - }); + it("should install ubuntu packages on ubuntu", async () => { + isUbuntuStub.resolves(true); - it("should throw exception on empty browser version", async () => { - await assert.isRejected( - installBrowser("chrome", "", { force }), - /Couldn't install browser 'chrome' because it has invalid version: ''/, - ); + await installBrowsersWithDrivers([{ browserName: "chrome", browserVersion: "115" }]); + + assert.calledOnceWith(installChromeStub, "115", { + force: true, + needWebDriver: true, + needUbuntuPackages: true, }); }); - }); - describe("installBrowsersWithDrivers", () => { - it("should force install browser with driver", async () => { + it("should not install ubuntu packages if its not ubuntu", async () => { + isUbuntuStub.resolves(false); + await installBrowsersWithDrivers([{ browserName: "chrome", browserVersion: "115" }]); - assert.calledOnceWith(installChromeStub, "115", { force: true }); - assert.calledOnceWith(installChromeDriverStub, "115", { force: true }); + assert.notCalled(installUbuntuPackageDependenciesStub); }); it("should return result with browsers install status", async () => { diff --git a/test/src/browser-installer/registry.ts b/test/src/browser-installer/registry.ts index db900d5c3..2a49eef9f 100644 --- a/test/src/browser-installer/registry.ts +++ b/test/src/browser-installer/registry.ts @@ -1,32 +1,47 @@ import proxyquire from "proxyquire"; import sinon, { type SinonStub } from "sinon"; -import type * as Registry from "../../../src/browser-installer/registry"; +import type RegistryType from "../../../src/browser-installer/registry"; +import type { RegistryFileContents } from "../../../src/browser-installer/registry"; import { Browser, Driver, type DownloadProgressCallback } from "../../../src/browser-installer/utils"; import { BrowserPlatform } from "@puppeteer/browsers"; +import type { PartialDeep } from "type-fest"; describe("browser-installer/registry", () => { const sandbox = sinon.createSandbox(); - let registry: typeof Registry; + let registry: typeof RegistryType; - let readJsonSyncStub: SinonStub; - let outputJSONSyncStub: SinonStub; let existsSyncStub: SinonStub; + let readJSONSyncStub: SinonStub; + let outputJSONSyncStub: SinonStub; let progressBarRegisterStub: SinonStub; let loggerWarnStub: SinonStub; - const createRegistry_ = (contents: Record> = {}): typeof Registry => { + const createRegistry_ = ( + contents: PartialDeep = {} as RegistryFileContents, + ): typeof RegistryType => { + contents.binaries ||= {}; + contents.osPackages ||= {}; + contents.meta ||= { version: 1 }; + + existsSyncStub.returns(true); + readJSONSyncStub.returns(contents); + return proxyquire("../../../src/browser-installer/registry", { - "../utils": { getRegistryPath: () => "/testplane/registry/registry.json" }, - "fs-extra": { readJsonSync: () => contents, existsSync: () => true }, "../../utils/logger": { warn: loggerWarnStub }, - }); + "../utils": { getRegistryPath: () => "/testplane/registry/registry.json" }, + "fs-extra": { + existsSync: existsSyncStub, + readJSONSync: readJSONSyncStub, + outputJSONSync: outputJSONSyncStub, + }, + }).default; }; beforeEach(() => { - readJsonSyncStub = sandbox.stub().returns({}); - outputJSONSyncStub = sandbox.stub(); existsSyncStub = sandbox.stub().returns(false); + readJSONSyncStub = sandbox.stub().returns({ binaries: {}, osPackages: {}, meta: { version: 1 } }); + outputJSONSyncStub = sandbox.stub(); progressBarRegisterStub = sandbox.stub(); loggerWarnStub = sandbox.stub(); @@ -35,11 +50,11 @@ describe("browser-installer/registry", () => { "../utils": { getRegistryPath: () => "/testplane/registry/registry.json" }, "../../utils/logger": { warn: loggerWarnStub }, "fs-extra": { - readJsonSync: readJsonSyncStub, - outputJSONSync: outputJSONSyncStub, existsSync: existsSyncStub, + readJSONSync: readJSONSyncStub, + outputJSONSync: outputJSONSyncStub, }, - }); + }).default; }); afterEach(() => sandbox.restore()); @@ -47,9 +62,11 @@ describe("browser-installer/registry", () => { describe("getBinaryPath", () => { it("should return binary path", async () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chrome_mac_arm: { - "115.0.5790.170": "../browsers/chrome", + binaries: { + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome", + }, }, }); @@ -68,7 +85,7 @@ describe("browser-installer/registry", () => { it("should throw an error if browser version is not installed", async () => { // eslint-disable-next-line camelcase - registry = createRegistry_({ chrome_mac_arm: {} }); + registry = createRegistry_({ binaries: { chrome_mac_arm: {} } }); const fn = (): Promise => registry.getBinaryPath(Browser.CHROME, BrowserPlatform.MAC_ARM, "120"); @@ -79,11 +96,13 @@ describe("browser-installer/registry", () => { describe("getMatchedBrowserVersion", () => { it("should return matching latest chrome browser version", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chrome_mac_arm: { - "115.0.5790.170": "../browsers/chrome-115-0-5790-170", - "114.0.6980.170": "../browsers/chrome-114-0-6980-170", - "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + binaries: { + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome-115-0-5790-170", + "114.0.6980.170": "../browsers/chrome-114-0-6980-170", + "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + }, }, }); @@ -96,11 +115,13 @@ describe("browser-installer/registry", () => { it("should return matching latest firefox browser version", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - firefox_mac_arm: { - "stable_117.0b2": "../browsers/chrome-117-0b2", - "stable_118.0": "../browsers/firefox-118-0", - "stable_117.0b9": "../browsers/firefox-117-0b9", + binaries: { + // eslint-disable-next-line camelcase + firefox_mac_arm: { + "stable_117.0b2": "../browsers/chrome-117-0b2", + "stable_118.0": "../browsers/firefox-118-0", + "stable_117.0b9": "../browsers/firefox-117-0b9", + }, }, }); @@ -113,11 +134,13 @@ describe("browser-installer/registry", () => { it("should return null if no installed browser matching requirements", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chrome_mac_arm: { - "115.0.5790.170": "../browsers/chrome-115-0-5790-170", - "114.0.6980.170": "../browsers/chrome-114-0-6980-170", - "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + binaries: { + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome-115-0-5790-170", + "114.0.6980.170": "../browsers/chrome-114-0-6980-170", + "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + }, }, }); @@ -132,11 +155,13 @@ describe("browser-installer/registry", () => { describe("getMatchedDriverVersion", () => { it("should return matching chromedriver version", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chromedriver_mac_arm: { - "115.0.5790.170": "../drivers/chromedriver-115-0-5790-170", - "114.0.6980.170": "../drivers/chromedriver-114-0-6980-170", - "115.0.5320.180": "../drivers/chromedriver-115-0-5230-180", + binaries: { + // eslint-disable-next-line camelcase + chromedriver_mac_arm: { + "115.0.5790.170": "../drivers/chromedriver-115-0-5790-170", + "114.0.6980.170": "../drivers/chromedriver-114-0-6980-170", + "115.0.5320.180": "../drivers/chromedriver-115-0-5230-180", + }, }, }); @@ -149,11 +174,13 @@ describe("browser-installer/registry", () => { it("should return matching chromedriver version", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - edgedriver_mac_arm: { - "115.0.5790.170": "../drivers/edgedriver-115-0-5790-170", - "114.0.6980.170": "../drivers/edgedriver-114-0-6980-170", - "115.0.5320.180": "../drivers/edgedriver-115-0-5230-180", + binaries: { + // eslint-disable-next-line camelcase + edgedriver_mac_arm: { + "115.0.5790.170": "../drivers/edgedriver-115-0-5790-170", + "114.0.6980.170": "../drivers/edgedriver-114-0-6980-170", + "115.0.5320.180": "../drivers/edgedriver-115-0-5230-180", + }, }, }); @@ -166,11 +193,13 @@ describe("browser-installer/registry", () => { it("should return latest version for geckodriver", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - geckodriver_mac_arm: { - "0.33.0": "../drivers/geckodriver-33", - "0.35.0": "../drivers/geckodriver-35", - "0.34.0": "../drivers/geckodriver-34", + binaries: { + // eslint-disable-next-line camelcase + geckodriver_mac_arm: { + "0.33.0": "../drivers/geckodriver-33", + "0.35.0": "../drivers/geckodriver-35", + "0.34.0": "../drivers/geckodriver-34", + }, }, }); @@ -183,8 +212,10 @@ describe("browser-installer/registry", () => { it("should return null if matching version is not found", () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chromedriver_mac_arm: {}, + binaries: { + // eslint-disable-next-line camelcase + chromedriver_mac_arm: {}, + }, }); const version = registry.getMatchedDriverVersion(Driver.GECKODRIVER, BrowserPlatform.MAC_ARM, "115"); @@ -206,9 +237,11 @@ describe("browser-installer/registry", () => { it("should not install binary if it is already installed", async () => { registry = createRegistry_({ - // eslint-disable-next-line camelcase - chrome_mac_arm: { - "115.0.5320.180": "../browser/path", + binaries: { + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5320.180": "../browser/path", + }, }, }); @@ -235,8 +268,12 @@ describe("browser-installer/registry", () => { outputJSONSyncStub, "/testplane/registry/registry.json", { - // eslint-disable-next-line camelcase - chrome_mac_arm: { "115.0.5320.180": "../browser/path" }, + binaries: { + // eslint-disable-next-line camelcase + chrome_mac_arm: { "115.0.5320.180": "../browser/path" }, + }, + osPackages: {}, + meta: { version: 1 }, }, { replacer: sinon.match.func }, ); diff --git a/test/src/browser-installer/run.ts b/test/src/browser-installer/run.ts index 9f61eac71..522463315 100644 --- a/test/src/browser-installer/run.ts +++ b/test/src/browser-installer/run.ts @@ -1,19 +1,26 @@ import proxyquire from "proxyquire"; import sinon, { type SinonStub } from "sinon"; import type { runBrowserDriver as RunBrowserDriver } from "../../../src/browser-installer/run"; -import { Driver } from "../../../src/browser-installer/utils"; +import { Browser } from "../../../src/browser-installer/utils"; describe("browser-installer/run", () => { const sandbox = sinon.createSandbox(); let runBrowserDriver: typeof RunBrowserDriver; + + let installBrowserStub: SinonStub; let runChromeDriverStub: SinonStub; + let runGeckoDriverStub: SinonStub; beforeEach(() => { + installBrowserStub = sandbox.stub(); runChromeDriverStub = sandbox.stub(); + runGeckoDriverStub = sandbox.stub(); runBrowserDriver = proxyquire.noCallThru()("../../../src/browser-installer/run", { + "./install": { installBrowser: installBrowserStub }, "./chrome": { runChromeDriver: runChromeDriverStub }, + "./firefox": { runGeckoDriver: runGeckoDriverStub }, }).runBrowserDriver; }); @@ -21,9 +28,29 @@ describe("browser-installer/run", () => { [true, false, undefined].forEach(debug => { it(`should run chrome driver with debug: ${debug}`, async () => { - await runBrowserDriver(Driver.CHROMEDRIVER, "some-version", { debug }); + await runBrowserDriver(Browser.CHROME, "some-version", { debug }); assert.calledOnceWith(runChromeDriverStub, "some-version", { debug: Boolean(debug) }); }); }); + + it(`should try to install chrome before running its driver`, async () => { + await runBrowserDriver(Browser.CHROME, "some-version"); + + assert.calledOnceWith(installBrowserStub, Browser.CHROME, "some-version", { + shouldInstallWebDriver: true, + shouldInstallUbuntuPackages: true, + }); + assert.callOrder(installBrowserStub, runChromeDriverStub); + }); + + it(`should try to install firefox before running its driver`, async () => { + await runBrowserDriver(Browser.FIREFOX, "some-version"); + + assert.calledOnceWith(installBrowserStub, Browser.FIREFOX, "some-version", { + shouldInstallWebDriver: true, + shouldInstallUbuntuPackages: true, + }); + assert.callOrder(installBrowserStub, runGeckoDriverStub); + }); }); diff --git a/test/src/browser-installer/ubuntu-packages/collect-dependencies/cache.ts b/test/src/browser-installer/ubuntu-packages/collect-dependencies/cache.ts new file mode 100644 index 000000000..64d7bbe64 --- /dev/null +++ b/test/src/browser-installer/ubuntu-packages/collect-dependencies/cache.ts @@ -0,0 +1,99 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { + Cache as CacheType, + CacheData, +} from "../../../../../src/browser-installer/ubuntu-packages/collect-dependencies/cache"; +import { Browser } from "../../../../../src/browser-installer/utils"; + +describe("browser-installer/ubuntu-packages/collect-dependencies/shared-object", () => { + const sandbox = sinon.createSandbox(); + + let cache: CacheType; + + let fsStub: Record; + + const setCache_ = async (data: CacheData): Promise => { + fsStub.readJSON.withArgs(sinon.match("processed-browsers-linux.json")).resolves(data.processedBrowsers); + fsStub.readJSON.withArgs(sinon.match("ubuntu-os-version.json")).resolves(data.sharedObjectsMap); + + await cache.read(); + }; + + const getCache_ = async (): Promise => { + await cache.write(); + + const processedBrowsersCache = fsStub.outputJSON.withArgs(sinon.match("processed-browsers-linux.json")).args; + const sharedObjectsMapPath = fsStub.outputJSON.withArgs(sinon.match("ubuntu-os-version.json")).args; + + const result: CacheData = { + sharedObjectsMap: sharedObjectsMapPath[sharedObjectsMapPath.length - 1][1], + processedBrowsers: processedBrowsersCache[processedBrowsersCache.length - 1][1], + }; + + return result; + }; + + beforeEach(() => { + fsStub = { + readJSON: sinon.stub().resolves({}), + outputJSON: sinon.stub().resolves({}), + existsSync: sinon.stub().returns(false), + readdir: sinon.stub().resolves([]), + stat: sinon.stub().resolves({ isDirectory: false }), + } as Record; + + const Cache = proxyquire("../../../../../src/browser-installer/ubuntu-packages/collect-dependencies/cache", { + "fs-extra": fsStub, + }).Cache; + + cache = new Cache("os-version"); + }); + + afterEach(() => sandbox.restore()); + + it("should filter processed browsers", async () => { + await setCache_({ + processedBrowsers: { downloadedBrowsers: { chrome: ["80"] }, sharedObjects: ["libc.so.6"] }, + sharedObjectsMap: {}, + }); + + const filteredBrowsers = cache.filterProcessedBrowsers([ + { browserName: Browser.CHROME, browserVersion: "80.0.123.17" }, + { browserName: Browser.CHROME, browserVersion: "82.0.123.17" }, + ]); + + assert.deepEqual(filteredBrowsers, [{ browserName: "chrome", browserVersion: "82.0.123.17" }]); + }); + + it("should save processed browsers", async () => { + cache.saveProcessedBrowsers([ + { browserName: Browser.CHROME, browserVersion: "80.0.123.17" }, + { browserName: Browser.CHROME, browserVersion: "82.0.123.17" }, + ]); + + const cacheData = await getCache_(); + + assert.deepEqual(cacheData.processedBrowsers.downloadedBrowsers, { chrome: ["80", "82"] }); + }); + + it("should save resolved shared objects", async () => { + cache.savePackageName("libc.so.6", "libc6"); + + const cacheData = await getCache_(); + + assert.deepEqual(cacheData.processedBrowsers.sharedObjects, ["libc.so.6"]); + assert.deepEqual(cacheData.sharedObjectsMap, { "libc.so.6": "libc6" }); + }); + + it("should get unresolved shared objects", async () => { + await setCache_({ + processedBrowsers: { downloadedBrowsers: { chrome: ["80"] }, sharedObjects: ["libc.so.6", "libnss3.so"] }, + sharedObjectsMap: { "libc.so.6": "libc6" }, + }); + + const unresolvedSharedObjects = cache.getUnresolvedSharedObjects(); + + assert.deepEqual(unresolvedSharedObjects, ["libnss3.so"]); + }); +}); diff --git a/test/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts b/test/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts new file mode 100644 index 000000000..cded1cdd1 --- /dev/null +++ b/test/src/browser-installer/ubuntu-packages/collect-dependencies/shared-object.ts @@ -0,0 +1,84 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { + searchSharedObjectPackage as SearchSharedObjectPackage, + getBinarySharedObjectDependencies as GetBinarySharedObjectDependencies, +} from "../../../../../src/browser-installer/ubuntu-packages/collect-dependencies/shared-object"; + +describe("browser-installer/ubuntu-packages/collect-dependencies/shared-object", () => { + const sandbox = sinon.createSandbox(); + + let searchSharedObjectPackage: typeof SearchSharedObjectPackage; + let getBinarySharedObjectDependencies: typeof GetBinarySharedObjectDependencies; + + let readElfStub: SinonStub; + let aptFileSearchStub: SinonStub; + + beforeEach(() => { + readElfStub = sandbox.stub().resolves(); + aptFileSearchStub = sandbox.stub().resolves(); + + const sharedObject = proxyquire( + "../../../../../src/browser-installer/ubuntu-packages/collect-dependencies/shared-object", + { + "./ubuntu": { + readElf: readElfStub, + aptFileSearch: aptFileSearchStub, + }, + }, + ); + + ({ searchSharedObjectPackage, getBinarySharedObjectDependencies } = sharedObject); + }); + + afterEach(() => sandbox.restore()); + + describe("searchSharedObjectPackage", () => { + it("should return package name closest to shared object name", async () => { + aptFileSearchStub.withArgs("libnss3.so").resolves(`firefox\nlibnss3\n`); + + const packageName = await searchSharedObjectPackage("libnss3.so"); + + assert.equal(packageName, "libnss3"); + }); + }); + + describe("getBinarySharedObjectDependencies", () => { + it("should return binary direct shared object deps", async () => { + readElfStub.resolves(` +Dynamic section at offset 0xb00 contains 26 entries: + Tag Type Name/Value + 0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0] + 0x0000000000000001 (NEEDED) Shared library: [libdl.so.2] + 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] + 0x0000000000000015 (DEBUG) 0x0 + 0x0000000000000007 (RELA) 0x2005b0 + 0x0000000000000008 (RELASZ) 48 (bytes) + 0x0000000000000009 (RELAENT) 24 (bytes) + 0x0000000000000017 (JMPREL) 0x2005e0 + 0x0000000000000002 (PLTRELSZ) 192 (bytes) + 0x0000000000000003 (PLTGOT) 0x203cc0 + 0x0000000000000014 (PLTREL) RELA + 0x0000000000000006 (SYMTAB) 0x200308 + 0x000000000000000b (SYMENT) 24 (bytes) + 0x0000000000000005 (STRTAB) 0x2004c0 + 0x000000000000000a (STRSZ) 238 (bytes) + 0x000000006ffffef5 (GNU_HASH) 0x2004a0 + 0x0000000000000019 (INIT_ARRAY) 0x202af8 + 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) + 0x000000000000001a (FINI_ARRAY) 0x202af0 + 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) + 0x000000000000000c (INIT) 0x201a34 + 0x000000000000000d (FINI) 0x201a50 + 0x000000006ffffff0 (VERSYM) 0x200428 + 0x000000006ffffffe (VERNEED) 0x200440 + 0x000000006fffffff (VERNEEDNUM) 2 + 0x0000000000000000 (NULL) 0x0 + `); + + const deps = await getBinarySharedObjectDependencies("binary/path"); + + assert.deepEqual(deps, ["libpthread.so.0", "libdl.so.2", "libc.so.6"]); + }); + }); +}); diff --git a/test/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts b/test/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts new file mode 100644 index 000000000..939538850 --- /dev/null +++ b/test/src/browser-installer/ubuntu-packages/collect-dependencies/utils.ts @@ -0,0 +1,17 @@ +import { getCliArgs } from "../../../../../src/browser-installer/ubuntu-packages/collect-dependencies/utils"; + +describe("browser-installer/ubuntu-packages/collect-dependencies/utils", () => { + describe("getCliArgs", () => { + it("should support long cli keys", () => { + assert.deepEqual(getCliArgs({ foo: true }), ["--foo"]); + }); + + it("should support short cli keys", () => { + assert.deepEqual(getCliArgs({ f: true }), ["-f"]); + }); + + it("should not return disabled cli keys", () => { + assert.deepEqual(getCliArgs({ f: false, foo: false }), []); + }); + }); +}); diff --git a/test/src/browser-installer/ubuntu-packages/index.ts b/test/src/browser-installer/ubuntu-packages/index.ts new file mode 100644 index 000000000..49afcb2c5 --- /dev/null +++ b/test/src/browser-installer/ubuntu-packages/index.ts @@ -0,0 +1,163 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { + writeUbuntuPackageDependencies as WriteUbuntuPackageDependencies, + installUbuntuPackageDependencies as InstallUbuntuPackageDependencies, + getUbuntuLinkerEnv as GetUbuntuLinkerEnv, +} from "../../../../src/browser-installer/ubuntu-packages"; +import type { DownloadProgressCallback } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/ubuntu-packages", () => { + const sandbox = sinon.createSandbox(); + + let writeUbuntuPackageDependencies: typeof WriteUbuntuPackageDependencies; + let installUbuntuPackageDependencies: typeof InstallUbuntuPackageDependencies; + let getUbuntuLinkerEnv: typeof GetUbuntuLinkerEnv; + + let fsStub: Record; + let loggerLogStub: SinonStub; + let loggerWarnStub: SinonStub; + let installUbuntuPackagesStub: SinonStub; + let getUbuntuMilestoneStub: SinonStub; + let hasOsPackagesStub: SinonStub; + let getOsPackagesPathStub: SinonStub; + let installOsPackagesStub: SinonStub; + + beforeEach(() => { + fsStub = { + readJSON: sinon.stub().resolves({}), + existsSync: sinon.stub().returns(false), + readdir: sinon.stub().resolves([]), + stat: sinon.stub().resolves({ isDirectory: () => true }), + outputJSON: sinon.stub().resolves({}), + } as Record; + + loggerLogStub = sandbox.stub(); + loggerWarnStub = sandbox.stub(); + installUbuntuPackagesStub = sandbox.stub(); + getUbuntuMilestoneStub = sandbox.stub().resolves("20"); + hasOsPackagesStub = sandbox.stub().returns(false); + getOsPackagesPathStub = sandbox.stub().resolves("/.testplane/packages/ubuntu/20"); + installOsPackagesStub = sandbox + .stub() + .callsFake( + async ( + _, + __, + installFn: (downloadProgressCallback: DownloadProgressCallback) => Promise, + ): Promise => { + return installFn(sinon.stub()); + }, + ); + + const ubuntuPackages = proxyquire("../../../../src/browser-installer/ubuntu-packages", { + "fs-extra": fsStub, + "./apt": { installUbuntuPackages: installUbuntuPackagesStub }, + "./utils": { getUbuntuMilestone: getUbuntuMilestoneStub }, + "../registry": { + default: { + hasOsPackages: hasOsPackagesStub, + getOsPackagesPath: getOsPackagesPathStub, + installOsPackages: installOsPackagesStub, + }, + }, + "../../utils/logger": { log: loggerLogStub, warn: loggerWarnStub }, + }); + + ({ writeUbuntuPackageDependencies, installUbuntuPackageDependencies, getUbuntuLinkerEnv } = ubuntuPackages); + }); + + afterEach(() => sandbox.restore()); + + describe("writeUbuntuPackageDependencies", () => { + it("should write sorted dependencies if file does not exist", async () => { + getUbuntuMilestoneStub.resolves("20"); + fsStub.readJSON.withArgs(sinon.match("ubuntu-20-dependencies.json")).rejects(new Error("No such file")); + + await writeUbuntuPackageDependencies("20", ["b", "a", "c"]); + + assert.calledOnceWith(fsStub.outputJSON, sinon.match.string, ["a", "b", "c"]); + }); + + it("should write uniq sorted dependencies with existing deps from file", async () => { + fsStub.readJSON.resolves(["a", "b", "d"]); + + await writeUbuntuPackageDependencies("20", ["e", "c", "d"]); + + assert.calledOnceWith(fsStub.outputJSON, sinon.match.string, ["a", "b", "c", "d", "e"]); + }); + }); + + describe("installUbuntuPackageDependencies", () => { + it("should install deps for current milestone", async () => { + getUbuntuMilestoneStub.resolves("20"); + fsStub.existsSync.withArgs(sinon.match("packages")).returns(false); + fsStub.readJSON.withArgs(sinon.match("ubuntu-20-dependencies.json")).resolves(["foo", "bar"]); + + await installUbuntuPackageDependencies(); + + assert.calledOnceWith(installUbuntuPackagesStub, ["foo", "bar"], sinon.match("packages")); + }); + + it("should log warning if current ubuntu version is not supported", async () => { + getUbuntuMilestoneStub.resolves("100500"); + fsStub.readJSON.withArgs(sinon.match("ubuntu-100500-dependencies.json")).rejects(new Error("No such file")); + + await installUbuntuPackageDependencies(); + + assert.calledOnceWith( + loggerWarnStub, + [ + `Unable to read ubuntu dependencies for Ubuntu@100500, as this version currently not supported`, + `Assuming all necessary packages are installed already`, + ].join("\n"), + ); + assert.calledOnceWith(installUbuntuPackagesStub, []); + }); + }); + + describe("getUbuntuLinkerEnv", () => { + beforeEach(() => { + hasOsPackagesStub.returns(true); + fsStub.existsSync.withArgs(sinon.match("packages")).returns(true); + fsStub.readdir.withArgs(sinon.match("/lib")).resolves(["foo", "bar"]); + fsStub.readdir.withArgs(sinon.match("/usr/lib")).resolves(["baz", "qux"]); + fsStub.stat.resolves({ isDirectory: () => true }); + }); + + it("should resolve ubuntu linker env", async () => { + const env = await getUbuntuLinkerEnv(); + + assert.match(env.LD_LIBRARY_PATH, "/packages/ubuntu/20/lib/foo"); + assert.match(env.LD_LIBRARY_PATH, "/packages/ubuntu/20/lib/bar"); + assert.match(env.LD_LIBRARY_PATH, "/packages/ubuntu/20/usr/lib/baz"); + assert.match(env.LD_LIBRARY_PATH, "/packages/ubuntu/20/usr/lib/qux"); + }); + + it("should concat existing LD_LIBRARY_PATH", async () => { + const envBack = process.env.LD_LIBRARY_PATH; + process.env.LD_LIBRARY_PATH = "foo/bar/baz"; + + const env = await getUbuntuLinkerEnv(); + + process.env.LD_LIBRARY_PATH = envBack; + assert.match(env.LD_LIBRARY_PATH, "foo/bar/baz"); + }); + + it("should cache env value", async () => { + await getUbuntuLinkerEnv(); + + const existsSyncCallCount = fsStub.existsSync.callCount; + const readDirCallCount = fsStub.readdir.callCount; + const statCallCount = fsStub.stat.callCount; + + await getUbuntuLinkerEnv(); + await getUbuntuLinkerEnv(); + await getUbuntuLinkerEnv(); + + assert.callCount(fsStub.existsSync, existsSyncCallCount); + assert.callCount(fsStub.readdir, readDirCallCount); + assert.callCount(fsStub.stat, statCallCount); + }); + }); +}); diff --git a/test/src/browser-installer/utils.ts b/test/src/browser-installer/utils.ts index 18b420a76..abce0e804 100644 --- a/test/src/browser-installer/utils.ts +++ b/test/src/browser-installer/utils.ts @@ -1,29 +1,31 @@ -import { Browser, Driver } from "../../../src/browser-installer/utils"; +import { Browser } from "../../../src/browser-installer/utils"; import * as utils from "../../../src/browser-installer/utils"; describe("browser-installer/utils", () => { - describe("getDriverNameForBrowserName", () => { - it("CHROMEDRIVER", () => { - assert.equal(utils.getDriverNameForBrowserName(Browser.CHROME), Driver.CHROMEDRIVER); - assert.equal(utils.getDriverNameForBrowserName(Browser.CHROMIUM), Driver.CHROMEDRIVER); + describe("getNormalizedBrowserName", () => { + it("CHROME", () => { + assert.equal(utils.getNormalizedBrowserName("chrome"), Browser.CHROME); }); - it("GECKODRIVER", () => { - assert.equal(utils.getDriverNameForBrowserName(Browser.FIREFOX), Driver.GECKODRIVER); + it("FIREFOX", () => { + assert.equal(utils.getNormalizedBrowserName("firefox"), Browser.FIREFOX); }); - it("SAFARIDRIVER", () => { - assert.equal(utils.getDriverNameForBrowserName(Browser.SAFARI), Driver.SAFARIDRIVER); + it("EDGE", () => { + assert.equal(utils.getNormalizedBrowserName("edge"), Browser.EDGE); + assert.equal(utils.getNormalizedBrowserName("MicrosoftEdge"), Browser.EDGE); + assert.equal(utils.getNormalizedBrowserName("msedge"), Browser.EDGE); }); - it("EDGEDRIVER", () => { - assert.equal(utils.getDriverNameForBrowserName(Browser.EDGE), Driver.EDGEDRIVER); + it("SAFARI", () => { + assert.equal(utils.getNormalizedBrowserName("safari"), Browser.SAFARI); }); it("null", () => { const invalidValue = "unknown" as (typeof Browser)[keyof typeof Browser]; - assert.equal(utils.getDriverNameForBrowserName(invalidValue), null); + assert.equal(utils.getNormalizedBrowserName(invalidValue), null); + assert.equal(utils.getNormalizedBrowserName(), null); }); }); diff --git a/test/src/browser-pool/webdriver-pool.ts b/test/src/browser-pool/webdriver-pool.ts index 7fa2aaa74..226211364 100644 --- a/test/src/browser-pool/webdriver-pool.ts +++ b/test/src/browser-pool/webdriver-pool.ts @@ -41,7 +41,7 @@ describe("browser-pool/webdriver-pool", () => { const driver = await wdPool.getWebdriver("MicrosoftEdge", "135.0"); assert.equal(driver.gridUrl, "http://localhost:100500"); - assert.calledOnceWith(runBrowserDriverStub, "edgedriver", "135.0", { debug: false }); + assert.calledOnceWith(runBrowserDriverStub, "MicrosoftEdge", "135.0", { debug: false }); }); it("should run browser driver with debug mode", async () => { diff --git a/tsconfig.json b/tsconfig.json index ebbbcd72b..24406c2df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "src/**/*.test.ts", "src/browser/client-scripts", "src/bundle/cjs", + "src/browser-installer/ubuntu-packages/collect-dependencies", "src/runner/browser-env/vite/browser-modules" ], "compilerOptions": {