diff --git a/.github/actions/install-deps/action.yml b/.github/actions/install-deps/action.yml index 62c3fa5ab4..7ed4a1a77f 100644 --- a/.github/actions/install-deps/action.yml +++ b/.github/actions/install-deps/action.yml @@ -30,4 +30,4 @@ runs: # export npm_config_keytar_binary_host_mirror=${{ inputs.keytar-host-mirror }} # export npm_config_node_sqlite3_binary_host_mirror=${{ inputs.sqlite3-host-mirror }} - yarn install + yarn install --frozen-lockfile --network-timeout 1000000 diff --git a/.github/e2e-results.js b/.github/e2e-results.js new file mode 100644 index 0000000000..51c722fc90 --- /dev/null +++ b/.github/e2e-results.js @@ -0,0 +1,58 @@ +const fs = require('fs'); + +let parallelNodeInfo = ''; +// const totalNodes = parseInt(process.env.NODE_TOTAL, 10); +const totalNodes = 4; +if (totalNodes > 1) { + parallelNodeInfo = ` (node: ${parseInt(process.env.NODE_INDEX, 10) + 1}/${totalNodes})` +} + +const file = 'tests/e2e/results/e2e.results.json' +const appBuildType = process.env.APP_BUILD_TYPE || 'Web' +const results = { + message: { + text: `*E2ETest - ${appBuildType}${parallelNodeInfo}* (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\n`, + attachments: [], + }, +}; + +const result = JSON.parse(fs.readFileSync(file, 'utf-8')) +const testRunResult = { + color: '#36a64f', + title: `Started at: *${result.startTime}`, + text: `Executed ${result.total} in ${(new Date(result.endTime) - new Date(result.startTime)) / 1000}s`, + fields: [ + { + title: 'Passed', + value: result.passed, + short: true, + }, + { + title: 'Skipped', + value: result.skipped, + short: true, + }, + ], +}; +const failed = result.total - result.passed; +if (failed) { + results.passed = false; + testRunResult.color = '#cc0000'; + testRunResult.fields.push({ + title: 'Failed', + value: failed, + short: true, + }); +} + +results.message.attachments.push(testRunResult); + +if (results.passed === false) { + results.message.text = ' ' + results.message.text; +} + +fs.writeFileSync('e2e.report.json', JSON.stringify({ + channel: process.env.SLACK_TEST_REPORT_CHANNEL, + ...results.message, +})); diff --git a/.github/e2e/test.app-image.sh b/.github/e2e/test.app-image.sh new file mode 100755 index 0000000000..9d2b9b2f39 --- /dev/null +++ b/.github/e2e/test.app-image.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +yarn --cwd tests/e2e install + +# mount app resources +chmod +x ./release/*.AppImage +./release/*.AppImage --appimage-mount >> apppath & + +# create folder before tests run to prevent permissions issue +mkdir -p tests/e2e/remote +mkdir -p tests/e2e/rdi + +# run rte +docker compose -f tests/e2e/rte.docker-compose.yml build +docker compose -f tests/e2e/rte.docker-compose.yml up --force-recreate -d -V +./tests/e2e/wait-for-redis.sh localhost 12000 && \ + +# run tests +COMMON_URL=$(tail -n 1 apppath)/resources/app.asar/dist/renderer/index.html \ +ELECTRON_PATH=$(tail -n 1 apppath)/redisinsight \ +RI_SOCKETS_CORS=true \ +yarn --cwd tests/e2e dotenv -e .desktop.env yarn --cwd tests/e2e test:desktop:ci diff --git a/.github/itest-results.js b/.github/itest-results.js new file mode 100644 index 0000000000..082254a902 --- /dev/null +++ b/.github/itest-results.js @@ -0,0 +1,51 @@ +const fs = require('fs'); + +const file = 'redisinsight/api/test/test-runs/coverage/test-run-result.json' + +const results = { + message: { + text: `*ITest - ${process.env.ITEST_NAME}* (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\n`, + attachments: [], + }, +}; + +const result = JSON.parse(fs.readFileSync(file, 'utf-8')) +const testRunResult = { + color: '#36a64f', + title: `Started at: ${result.stats.start}`, + text: `Executed ${result.stats.tests} in ${result.stats.duration / 1000}s`, + fields: [ + { + title: 'Passed', + value: result.stats.passes, + short: true, + }, + { + title: 'Skipped', + value: result.stats.pending, + short: true, + }, + ], +}; + +if (result.stats.failures) { + results.passed = false; + testRunResult.color = '#cc0000'; + testRunResult.fields.push({ + title: 'Failed', + value: result.stats.failures, + short: true, + }); +} + +results.message.attachments.push(testRunResult); + +if (results.passed === false) { + results.message.text = ' ' + results.message.text; +} + +fs.writeFileSync('itests.report.json', JSON.stringify({ + channel: process.env.SLACK_TEST_REPORT_CHANNEL, + ...results.message, +})); diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 008e24d9b0..6278730871 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build +name: 🚀 Build on: # Manual trigger build diff --git a/.github/workflows/compress-images.yml b/.github/workflows/compress-images.yml index f27ce65e3f..ad2211d751 100644 --- a/.github/workflows/compress-images.yml +++ b/.github/workflows/compress-images.yml @@ -16,12 +16,12 @@ jobs: permissions: write-all runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@v4 + - name: Checkout Repo + uses: actions/checkout@v4 - - name: Compress Images - uses: calibreapp/image-actions@main - with: - # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. - # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions - githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Compress Images + uses: calibreapp/image-actions@main + with: + # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. + # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions + githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..05004431c1 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,33 @@ +name: Nightly jobs +on: + schedule: + - cron: 0 0 * * * + +jobs: + # Integration tests + build-docker: + uses: ./.github/workflows/pipeline-build-docker.yml + secrets: inherit + + integration-tests-nightly: + needs: build-docker + uses: ./.github/workflows/tests-integration.yml + secrets: inherit + with: + build: 'docker' + report: true + short_rte_list: false + + # E2E tests + build-appimage: + uses: ./.github/workflows/pipeline-build-linux.yml + secrets: inherit + with: + target: 'linux:appimage:x64' + + e2e-appimage-nightly: + uses: ./.github/workflows/tests-e2e-appimage.yml + needs: build-appimage + secrets: inherit + with: + report: true diff --git a/.github/workflows/pipeline-build-docker.yml b/.github/workflows/pipeline-build-docker.yml index f27c42bc8d..9ce369577a 100644 --- a/.github/workflows/pipeline-build-docker.yml +++ b/.github/workflows/pipeline-build-docker.yml @@ -7,6 +7,11 @@ on: required: false default: 'staging' type: string + only_docker: + description: Build only docker + required: false + default: false + type: boolean jobs: build: @@ -28,7 +33,7 @@ jobs: - name: Build sources run: ./.circleci/build/build.sh - # todo: matrix + # todo: matrix - name: Build web archives run: | unset npm_config_keytar_binary_host_mirror @@ -82,15 +87,17 @@ jobs: - uses: actions/upload-artifact@v4 name: Upload web-mini artifacts + if: ${{ !inputs.only_docker }} with: - if-no-files-found: error + if-no-files-found: warn name: web-mini path: ./release/web-mini - uses: actions/upload-artifact@v4 name: Upload web artifacts + if: ${{ !inputs.only_docker }} with: - if-no-files-found: error + if-no-files-found: warn name: web path: ./release/web diff --git a/.github/workflows/pipeline-build-linux.yml b/.github/workflows/pipeline-build-linux.yml index d7fe413497..f7dfde6008 100644 --- a/.github/workflows/pipeline-build-linux.yml +++ b/.github/workflows/pipeline-build-linux.yml @@ -28,7 +28,7 @@ jobs: # ssh # - run: sudo apt-get update -qy && sudo apt-get install -qy tmux; # - name: Setup upterm session - # - uses: mxschmitt/action-tmate #1 better + # - uses: mxschmitt/action-tmate@v3 #1 better # uses: lhotari/action-upterm@v1 #2 # with: # limit-access-to-actor: true @@ -83,6 +83,7 @@ jobs: name: linux-appimage-build path: | ./release/Redis-Insight*.AppImage + ./release/latest-linux.yml - uses: actions/upload-artifact@v4 name: Upload Deb artifact @@ -104,7 +105,6 @@ jobs: name: linux-snap-builds path: | ./release/Redis-Insight*.snap - ./release/latest-linux.yml env: RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }} diff --git a/.github/workflows/pull-request-created.yml b/.github/workflows/pull-request-created.yml deleted file mode 100644 index 3d479b82d1..0000000000 --- a/.github/workflows/pull-request-created.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Pull request created - -on: - pull_request_target: - -permissions: - pull-requests: write - -jobs: - assign-author: - runs-on: ubuntu-latest - name: Assign author pr - steps: - - uses: toshimaru/auto-author-assign@v2.1.1 diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml index bb24782ff6..11c27a421e 100644 --- a/.github/workflows/release-prod.yml +++ b/.github/workflows/release-prod.yml @@ -1,4 +1,4 @@ -name: Release (prod) +name: ❗ Release (prod) on: push: @@ -11,7 +11,8 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit with: - all_tests: true + group_tests: 'without_e2e' + short_rte_list: false builds-prod: name: Create all builds for release @@ -22,6 +23,18 @@ jobs: environment: 'production' target: 'all' + e2e-docker-tests: + name: E2E Docker tests + needs: builds-prod + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit + + e2e-appimage-tests: + name: E2E AppImage tests + needs: builds-prod + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit + virustotal-prod: name: Virustotal uses: ./.github/workflows/virustotal.yml diff --git a/.github/workflows/release-stage.yml b/.github/workflows/release-stage.yml index d163c5e192..7e8009b9e7 100644 --- a/.github/workflows/release-stage.yml +++ b/.github/workflows/release-stage.yml @@ -1,4 +1,4 @@ -name: Release (stage) +name: 📖 Release (stage) on: push: @@ -11,7 +11,8 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit with: - all_tests: true + group_tests: 'without_e2e' + short_rte_list: false builds: name: Release stage builds @@ -22,16 +23,15 @@ jobs: environment: 'staging' target: 'all' + e2e-docker-tests: + needs: builds + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit - # todo: e2e tests for release stage - # e2e-linux: - # uses: ./.github/workflows/build.yml - # needs: builds - # secrets: inherit - # with: - # environment: 'staging' - # target: 'all' - + e2e-appimage-tests: + needs: builds + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit diff --git a/.github/workflows/tests-backend.yml b/.github/workflows/tests-backend.yml index ddd7a41005..2f9cfed34f 100644 --- a/.github/workflows/tests-backend.yml +++ b/.github/workflows/tests-backend.yml @@ -13,7 +13,7 @@ env: jobs: unit-tests: - name: Backend tests + name: Unit tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -49,4 +49,16 @@ jobs: curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: Unit tests API - run: yarn --cwd redisinsight/api/ test:cov --ci + run: yarn --cwd redisinsight/api/ test:cov --ci --silent + + - name: Generate test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: BE unit tests' + path: redisinsight/api/reports/jest-*.xml + reporter: jest-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + diff --git a/.github/workflows/tests-e2e-appimage.yml b/.github/workflows/tests-e2e-appimage.yml new file mode 100644 index 0000000000..1bce4c2f78 --- /dev/null +++ b/.github/workflows/tests-e2e-appimage.yml @@ -0,0 +1,82 @@ +name: Tests E2E AppImage +on: + workflow_call: + inputs: + report: + description: Send report to Slack + required: false + default: false + type: boolean + +env: + E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }} + E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }} + E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }} + E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }} + E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }} + E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }} + E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }} + E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }} + RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }} + RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }} + RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} + SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + DBUS_SESSION_BUS_ADDRESS: ${{ vars.DBUS_SESSION_BUS_ADDRESS }} + DISPLAY: ${{ vars.DISPLAY }} + APPIMAGE_PATH: ${{ vars.APPIMAGE_PATH }} + +jobs: + e2e-tests-appimage: + runs-on: ubuntu-latest + name: E2E AppImage tests + steps: + - uses: actions/checkout@v4 + + - name: Install WM + run: | + sudo apt-get update -y + sudo apt-get install kmod libfuse2 xvfb net-tools fluxbox netcat -y + # sudo apt-get install kmod libfuse2 xvfb net-tools fluxbox netcat appimagelauncher -y + + - name: Download AppImage Artifacts + uses: actions/download-artifact@v4 + with: + name: linux-appimage-build + path: ./release + + - name: Run X11 + run: | + Xvfb :99 -screen 0 1920x1080x24 & + sleep 3 + fluxbox & + + - name: Run tests + run: | + .github/e2e/test.app-image.sh + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: report-appimage + path: tests/e2e/report + + - name: Send report to Slack + if: inputs.report && always() + run: | + APP_BUILD_TYPE="Electron (Linux)" node ./.github/e2e-results.js + curl -H "Content-type: application/json" --data @e2e.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage + + - name: Generate test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: E2E (AppImage)' + path: tests/e2e/results/results.xml + reporter: java-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + + diff --git a/.github/workflows/tests-e2e-docker.yml b/.github/workflows/tests-e2e-docker.yml new file mode 100644 index 0000000000..1322c14119 --- /dev/null +++ b/.github/workflows/tests-e2e-docker.yml @@ -0,0 +1,105 @@ +name: Tests E2E Docker +on: + workflow_call: + inputs: + report: + description: Send report to Slack + required: false + default: false + type: boolean + +env: + E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }} + E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }} + E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }} + E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }} + E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }} + E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }} + E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }} + E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }} + RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }} + RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }} + RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} + SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + E2E_VOLUME_PATH: "/usr/src/app" + +jobs: + e2e-docker-tests: + runs-on: ubuntu-latest + name: E2E Docker tests + container: + image: docker:latest + options: --privileged + volumes: + - /usr/src/app/results:/usr/src/app/results + - /usr/src/app/report:/usr/src/app/report + strategy: + fail-fast: false + matrix: + # Number of threads to run tests + parallel: [0, 1, 2, 3] + + steps: + - uses: actions/checkout@v4 + + - name: Setup repository + run: git config --global --add safe.directory /__w/RedisInsight/RedisInsight + + - name: Download Docker Artifacts + uses: actions/download-artifact@v4 + with: + name: docker + path: ./release + + - name: Load built docker image from workspace + run: | + docker image load -i ./release/docker-linux-alpine.amd64.tar + # docker pull redis/redisinsight + + - name: Generate short list of the test files + working-directory: ./tests/e2e + run: | + testFiles=$(find tests/web -type f -name '*.e2e.ts' | sort | awk "NR % 4 == ${{ matrix.parallel }}") + echo $testFiles + + # Multi-Line value + echo "TEST_FILES<> $GITHUB_ENV + echo "$testFiles" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Run tests + run: | + export NODE_INDEX=${{ matrix.parallel }} + + TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ + RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ + RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ + docker compose \ + -f tests/e2e/rte.docker-compose.yml \ + -f tests/e2e/docker.web.docker-compose.yml \ + up --abort-on-container-exit --force-recreate + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: report-docker-node-${{ matrix.parallel }} + path: /usr/src/app/report + + - name: Send report to Slack + if: inputs.report && always() + run: | + node ./.github/e2e-results.js + curl -H "Content-type: application/json" --data @e2e.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage + + - name: Generate test results for ${{ matrix.parallel }}th node + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: E2E (docker) ${{ matrix.parallel }}th node' + path: /usr/src/app/results/results.xml + reporter: java-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' diff --git a/.github/workflows/tests-frontend.yml b/.github/workflows/tests-frontend.yml index c8969414d4..13af73dd59 100644 --- a/.github/workflows/tests-frontend.yml +++ b/.github/workflows/tests-frontend.yml @@ -48,3 +48,14 @@ jobs: - name: Unit tests UI run: yarn test:cov --ci --silent + + - name: Generate test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: FE unit tests' + path: reports/jest-*.xml + reporter: jest-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' diff --git a/.github/workflows/tests-integration.yml b/.github/workflows/tests-integration.yml new file mode 100644 index 0000000000..e47750d149 --- /dev/null +++ b/.github/workflows/tests-integration.yml @@ -0,0 +1,176 @@ +name: Integration tests +on: + workflow_call: + inputs: + build: + description: Backend build to run tests over + type: string + default: 'local' + redis_client: + description: Library to use for redis connection + type: string + default: 'ioredis' + report: + description: Send report for test run to slack + type: boolean + default: false + short_rte_list: + description: Use short rte list + type: boolean + default: false + +env: + SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }} + TEST_MEDIUM_DB_DUMP: ${{ secrets.TEST_MEDIUM_DB_DUMP }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + ITESTS_NAMES: | + { + "oss-st-5": "OSS Standalone v5", + "oss-st-5-pass": "OSS Standalone v5 with admin pass required", + "oss-st-6": "OSS Standalone v6 and all modules", + "oss-st-big": "OSS Standalone v6 and all modules and predefined amount of data inside (~3-4M)", + "mods-preview": "OSS Standalone and all preview modules", + "oss-st-6-tls": "OSS Standalone v6 with TLS enabled", + "oss-st-6-tls-auth": "OSS Standalone v6 with TLS auth required", + "oss-clu": "OSS Cluster", + "oss-clu-tls": "OSS Cluster with TLS enabled", + "oss-sent": "OSS Sentinel", + "oss-sent-tls-auth": "OSS Sentinel with TLS auth", + "re-st": "Redis Enterprise with Standalone inside", + "re-clu": "Redis Enterprise with Cluster inside", + "re-crdt": "Redis Enterprise with active-active database inside" + } + ITESTS_NAMES_SHORT: | + { + "oss-st-5-pass": "OSS Standalone v5 with admin pass required", + "oss-st-6-tls-auth": "OSS Standalone v6 with TLS auth required", + "oss-clu-tls": "OSS Cluster with TLS enabled", + "re-crdt": "Redis Enterprise with active-active database inside", + "oss-sent-tls-auth": "OSS Sentinel with TLS auth" + } + +jobs: + set-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.parse-matrix.outputs.matrix }} + steps: + - name: Create JSON array for run-tests matrix + id: parse-matrix + run: | + # Extract the JSON object from the environment variable + MATRIX_JSON="$ITESTS_NAMES_SHORT" + + if [ "${{ inputs.short_rte_list }}" == "false" ]; then + MATRIX_JSON="$ITESTS_NAMES" + fi + + MATRIX_ARRAY=$(echo "$MATRIX_JSON" | jq -c 'keys') + + # Output the formed JSON array for use in other jobs + echo "matrix=$MATRIX_ARRAY" >> $GITHUB_OUTPUT + + - name: Verify the formed matrix array + run: | + echo "Formed matrix array:" + echo "${{ steps.parse-matrix.outputs.matrix }}" + + run-tests: + name: ITest + runs-on: ubuntu-latest + needs: set-matrix + strategy: + fail-fast: false + matrix: + rte: ${{ fromJson(needs.set-matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + - name: Download Docker Artifacts + if: inputs.build == 'docker' + uses: actions/download-artifact@v4 + with: + name: docker + path: ./release + + - name: Load built docker image from workspace + if: inputs.build == 'docker' + run: | + docker image load -i ./release/docker-linux-alpine.amd64.tar + + - name: Run tests + run: | + if [ ${{ inputs.redis_client }} == "node-redis" ]; then + export RI_REDIS_CLIENTS_FORCE_STRATEGY=${{ inputs.redis_client }} + fi + + ./redisinsight/api/test/test-runs/start-test-run.sh -r ${{ matrix.rte }} -t ${{ inputs.build }} + mkdir -p mkdir itest/coverages && mkdir -p itest/results + + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/${{ matrix.rte }}.result.json + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/${{ matrix.rte }}.result.xml + cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/${{ matrix.rte }}.coverage.json + + - name: Upload coverage files as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverages-${{ matrix.rte }} + path: itest/coverages + + - name: Send report to Slack + if: inputs.report && always() + run: | + ITEST_NAME=${{ matrix.rte }} node ./.github/itest-results.js + curl -H "Content-type: application/json" --data @itests.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage + + - name: Generate test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: IT (${{ matrix.rte }}) tests' + path: itest/results/*.result.xml + reporter: jest-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + + coverage: + runs-on: ubuntu-latest + name: Final coverage + needs: run-tests + steps: + - uses: actions/checkout@v4 + + - name: Merge coverage artifacts + id: merge-artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: coverages-artifacts + pattern: coverages-* + delete-merged: true + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + pattern: coverages-artifacts + path: ./coverages + + - name: Calculate coverage across all tests runs + run: | + npx nyc report -t ./coverages -r text -r text-summary + sudo mkdir -p /usr/src/app + sudo cp -a ./redisinsight/api/. /usr/src/app/ + sudo cp -R ./coverages /usr/src/app && sudo chmod 777 -R /usr/src/app + cd /usr/src/app && npx nyc report -t ./coverages -r text -r text-summary + + - name: Delete Artifact + uses: actions/github-script@v7 + with: + script: | + github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: ${{ steps.merge-artifacts.outputs.artifact-id }} + }); + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8fae373c51..87dd75d06a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,28 +1,41 @@ -name: Tests +name: ✅ Tests on: push: branches: - - 'fe/*' - - 'be/*' - # branches-ignore: - # - main + - 'fe/**' + - 'be/**' + - 'e2e/**' workflow_dispatch: inputs: - all_tests: - description: Run all tests (FE, BE, IT, E2E) - type: boolean - required: false - default: false + group_tests: + description: Run group of tests + default: 'all' + type: choice + options: + - all + - without_e2e + + redis_client: + description: Library to use for redis connection + default: 'ioredis' + type: choice + options: + - ioredis + - node-redis workflow_call: inputs: - all_tests: - description: Run all tests (FE, BE, IT, E2E) + group_tests: + description: Run group of tests + type: string + default: 'all' + short_rte_list: + description: Use short rte list type: boolean - required: false - default: false + default: true + jobs: changes: @@ -48,18 +61,63 @@ jobs: e2e: - 'tests/e2e/**' + # TODO: concurrency + frontend-tests: - # TODO: concurrency - # concurrency: build needs: changes - if: needs.changes.outputs.frontend == 'true' || inputs.all_tests || startsWith(github.ref_name, 'fe/') + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'fe/') uses: ./.github/workflows/tests-frontend.yml secrets: inherit backend-tests: - # TODO: concurrency - # concurrency: build needs: changes - if: needs.changes.outputs.backend == 'true' || inputs.all_tests || startsWith(github.ref_name, 'be/') + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') uses: ./.github/workflows/tests-backend.yml secrets: inherit + + integration-tests: + needs: changes + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') + uses: ./.github/workflows/tests-integration.yml + secrets: inherit + with: + short_rte_list: ${{ inputs.short_rte_list || true }} + redis_client: ${{ inputs.redis_client }} + + # E2E Approve + e2e-approve: + runs-on: ubuntu-latest + needs: changes + timeout-minutes: 60 + if: inputs.group_tests == 'all' || startsWith(github.ref_name, 'e2e/') + environment: ${{ startsWith(github.ref_name, 'e2e/') && 'e2e-approve' || 'staging' }} + name: Approve E2E tests + steps: + - uses: actions/checkout@v4 + + # E2E Docker + build-docker: + uses: ./.github/workflows/pipeline-build-docker.yml + needs: e2e-approve + secrets: inherit + with: + only_docker: true + + e2e-docker-tests: + needs: build-docker + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit + + + # E2E AppImage + build-appimage: + uses: ./.github/workflows/pipeline-build-linux.yml + needs: e2e-approve + secrets: inherit + with: + target: linux:appimage:x64 + + e2e-appimage-tests: + needs: build-appimage + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit diff --git a/.github/workflows/virustotal.yml b/.github/workflows/virustotal.yml index cb5fe09497..c67f520cae 100644 --- a/.github/workflows/virustotal.yml +++ b/.github/workflows/virustotal.yml @@ -22,39 +22,39 @@ jobs: artifact_names: ${{ steps.list_artifacts.outputs.artifact_names }} artifact_exists: ${{ steps.list_artifacts.outputs.artifact_exists }} steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Download All Artifacts - uses: actions/download-artifact@v4 - with: - path: ./release - # TODO: enable pattern filter after fix: - # https://github.com/nektos/act/issues/2433 - # pattern: '*-build' - # merge-multiple: true - - - run: ls -R ./release - - - name: List Artifact Files - id: list_artifacts - run: | - # If artifacts don't exist put array of app names for url check - if [ ! -d "./release" ]; then - echo "NO REALEASE FOLDER ${VIRUSTOTAL_FILE_NAMES}" - echo "artifact_exists=false" >> $GITHUB_OUTPUT - echo "artifact_names=$VIRUSTOTAL_FILE_NAMES" >> $GITHUB_OUTPUT - exit 0; - fi + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download All Artifacts + uses: actions/download-artifact@v4 + with: + path: ./release + # TODO: enable pattern filter after fix: + # https://github.com/nektos/act/issues/2433 + # pattern: '*-build' + # merge-multiple: true + + - run: ls -R ./release + + - name: List Artifact Files + id: list_artifacts + run: | + # If artifacts don't exist put array of app names for url check + if [ ! -d "./release" ]; then + echo "NO REALEASE FOLDER ${VIRUSTOTAL_FILE_NAMES}" + echo "artifact_exists=false" >> $GITHUB_OUTPUT + echo "artifact_names=$VIRUSTOTAL_FILE_NAMES" >> $GITHUB_OUTPUT + exit 0; + fi - # Get list of artifacts - ARTIFACTS=$(ls ./release) + # Get list of artifacts + ARTIFACTS=$(ls ./release) - # Conver list to json - ARTIFACTS_JSON=$(echo "$ARTIFACTS" | jq -R -s -c 'split("\n")[:-1]') + # Conver list to json + ARTIFACTS_JSON=$(echo "$ARTIFACTS" | jq -R -s -c 'split("\n")[:-1]') - echo "artifact_exists=true" >> $GITHUB_OUTPUT - echo "artifact_names=$ARTIFACTS_JSON" >> $GITHUB_OUTPUT + echo "artifact_exists=true" >> $GITHUB_OUTPUT + echo "artifact_names=$ARTIFACTS_JSON" >> $GITHUB_OUTPUT analyze: name: Analyze file @@ -62,6 +62,7 @@ jobs: needs: download_artifacts strategy: + fail-fast: false matrix: artifact: ${{ fromJson(needs.download_artifacts.outputs.artifact_names) }} diff --git a/.gitignore b/.gitignore index 232acd8c53..c3499c5456 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,12 @@ redisinsight/ui/style.css.map redisinsight/ui/dist redisinsight/api/commands redisinsight/api/guides +redisinsight/api/reports redisinsight/api/dist-minified redisinsight/api/tutorials redisinsight/api/content redisinsight/ui/dist-stats.html +reports dist distWeb dll diff --git a/jest.config.cjs b/jest.config.cjs index a6817a1303..aec5ae9f16 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,7 +1,7 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { testEnvironmentOptions: { - url: 'http://localhost/' + url: 'http://localhost/', }, moduleNameMapper: { '\\.(jpg|jpeg|png|ico|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': @@ -18,7 +18,8 @@ module.exports = { 'remark-rehype': '/redisinsight/__mocks__/remarkRehype.js', 'rehype-stringify': '/redisinsight/__mocks__/rehypeStringify.js', 'unist-util-visit': '/redisinsight/__mocks__/unistUtilsVisit.js', - 'react-children-utilities': '/redisinsight/__mocks__/react-children-utilities.js', + 'react-children-utilities': + '/redisinsight/__mocks__/react-children-utilities.js', d3: '/node_modules/d3/dist/d3.min.js', '^uuid$': require.resolve('uuid'), msgpackr: require.resolve('msgpackr'), @@ -27,20 +28,9 @@ module.exports = { 'construct-style-sheets-polyfill', '/redisinsight/ui/src/setup-env.ts', ], - setupFilesAfterEnv: [ - '/redisinsight/ui/src/setup-tests.ts', - ], - moduleDirectories: [ - 'node_modules', - 'redisinsight/node_modules', - ], - moduleFileExtensions: [ - 'js', - 'jsx', - 'ts', - 'tsx', - 'json', - ], + setupFilesAfterEnv: ['/redisinsight/ui/src/setup-tests.ts'], + moduleDirectories: ['node_modules', 'redisinsight/node_modules'], + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'], testEnvironment: 'jest-environment-jsdom', transformIgnorePatterns: [ 'node_modules/(?!(monaco-editor|react-monaco-editor)/)', @@ -57,6 +47,21 @@ module.exports = { '/redisinsight/ui/src/packages', ], resolver: '/jest-resolver.js', + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'reports', + outputName: 'jest-junit.xml', + ancestorSeparator: ' › ', + uniqueOutputName: 'false', + suiteNameTemplate: '{filepath}', + classNameTemplate: '{classname}', + titleTemplate: '{title}', + }, + ], + ], coverageThreshold: { global: { statements: 80, @@ -65,4 +70,4 @@ module.exports = { lines: 80, }, }, -} +}; diff --git a/package.json b/package.json index 73ece58055..8f73245cba 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "ioredis-mock": "^5.5.4", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-junit": "^16.0.0", "jest-runner-groups": "^2.2.0", "jest-when": "^3.2.1", "license-checker": "^25.0.1", diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 91933ab7f7..1436a0f627 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -123,6 +123,7 @@ "eslint-plugin-sonarjs": "^0.9.1", "ioredis-mock": "^8.2.2", "jest": "^29.7.0", + "jest-junit": "^16.0.0", "jest-when": "^3.2.1", "joi": "^17.4.0", "mocha": "^8.4.0", @@ -167,6 +168,21 @@ "src/(.*)": "/$1", "apiSrc/(.*)": "/$1", "tests/(.*)": "/__tests__/$1" - } + }, + "reporters": [ + "default", + [ + "jest-junit", + { + "outputDirectory": "reports", + "outputName": "jest-junit.xml", + "ancestorSeparator": " › ", + "uniqueOutputName": "false", + "suiteNameTemplate": "{filepath}", + "classNameTemplate": "{classname}", + "titleTemplate": "{title}" + } + ] + ] } } diff --git a/redisinsight/api/test/test-runs/start-test-run.sh b/redisinsight/api/test/test-runs/start-test-run.sh index 49724ba3b6..4da56042f5 100755 --- a/redisinsight/api/test/test-runs/start-test-run.sh +++ b/redisinsight/api/test/test-runs/start-test-run.sh @@ -43,31 +43,31 @@ if test -f "$PRESTART"; then fi echo "Pulling RTE... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose \ +eval "ID=$ID RTE=$RTE docker compose \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env pull redis" echo "Building RTE... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose \ +eval "ID=$ID RTE=$RTE docker compose \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env build --no-cache redis" echo "Test run is starting... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env run --use-aliases test" echo "Stop all containers... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env stop" echo "Remove containers with anonymous volumes... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env rm -v -f" diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index 548556ca5f..0c857742c6 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -5390,6 +5390,16 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" +jest-junit@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-16.0.0.tgz#d838e8c561cf9fdd7eb54f63020777eee4136785" + integrity sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ== + dependencies: + mkdirp "^1.0.4" + strip-ansi "^6.0.1" + uuid "^8.3.2" + xml "^1.0.1" + jest-leak-detector@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index 24496ae6ed..32c6c78d90 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -7,8 +7,8 @@ services: dockerfile: e2e.Dockerfile tty: true volumes: - - ./results:/usr/src/app/results - - ./report:/usr/src/app/report + - ${E2E_VOLUME_PATH:-.}/results:/usr/src/app/results + - ${E2E_VOLUME_PATH:-.}/report:/usr/src/app/report - ./plugins:/usr/src/app/plugins - rihomedir:/root/.redis-insight - tmp:/tmp diff --git a/yarn.lock b/yarn.lock index 7791decbaa..fbf694def2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8464,6 +8464,16 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" +jest-junit@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-16.0.0.tgz#d838e8c561cf9fdd7eb54f63020777eee4136785" + integrity sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ== + dependencies: + mkdirp "^1.0.4" + strip-ansi "^6.0.1" + uuid "^8.3.2" + xml "^1.0.1" + jest-leak-detector@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" @@ -14144,6 +14154,11 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== + xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1: version "15.1.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"