diff --git a/.github/workflows/regression-test-forked-branch.yaml b/.github/workflows/regression-test-forked-branch.yaml new file mode 100644 index 0000000000..2b339a269f --- /dev/null +++ b/.github/workflows/regression-test-forked-branch.yaml @@ -0,0 +1,139 @@ +name: Regression Tests Forked Branch + +on: + workflow_dispatch: + push: + branches: + - remote-regression-test-runs + pull_request: + types: [labeled, opened, reopened, synchronize] + +jobs: + run-tests: + name: Regression Tests - Run tests + runs-on: ubuntu-24.04 + if: > + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + ( + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'Require Test') && + github.event.pull_request.head.repo.fork == true && + github.event.pull_request.base.ref == 'development' + ) + env: + REACT_APP_FUNCTION_CATALOG_URL: ${{ secrets.REACT_APP_FUNCTION_CATALOG_URL }} + REACT_APP_MLRUN_API_URL: ${{ secrets.REACT_APP_MLRUN_API_URL }} + REACT_APP_NUCLIO_API_URL: ${{ secrets.REACT_APP_NUCLIO_API_URL }} + REACT_APP_IGUAZIO_API_URL: ${{ secrets.REACT_APP_IGUAZIO_API_URL }} + outputs: + failed_step: ${{ steps.identify_failure.outputs.FAILED_STEP }} + job_status: ${{ job.status }} + steps: + + - name: Display GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + + - name: Checkout code + id: checkout_code + uses: actions/checkout@v4 + + - name: Set up Node.js + id: setup_node + uses: actions/setup-node@v4 + with: + node-version: '16' + + - name: Install dependencies + id: install_dependencies + run: npm install + + - name: Run parallel tasks + id: run_parallel_tasks + run: | + npm run add-comment-to-http-client & + npm run mock-server & + npm start & + npm run test:regression + + - name: Upload test reports + id: upload_reports + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: tests/reports/ + if-no-files-found: error + + - name: Identify failed step + if: ${{ always() }} + id: identify_failure + run: | + FAILED_STEP="" + if [ "${{ steps.checkout_code.outcome }}" = "failure" ]; then + FAILED_STEP="Checkout code" + elif [ "${{ steps.setup_node.outcome }}" = "failure" ]; then + FAILED_STEP="Set up Node.js" + elif [ "${{ steps.install_dependencies.outcome }}" = "failure" ]; then + FAILED_STEP="Install dependencies" + elif [ "${{ steps.run_parallel_tasks.outcome }}" = "failure" ]; then + FAILED_STEP="Run parallel tasks" + elif [ "${{ steps.upload_reports.outcome }}" = "failure" ]; then + FAILED_STEP="Upload test reports" + fi + echo "failed_step=$FAILED_STEP" >> $GITHUB_OUTPUT + + comment-pr: + name: Regression Test - comment on PR + runs-on: ubuntu-24.04 + needs: run-tests + if: > + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + ( + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'Require Test') && + github.event.pull_request.head.repo.fork == true && + github.event.pull_request.base.ref == 'development' + ) && always() + env: + JOB_STATUS: ${{ needs.run-tests.result }} + FAILED_STEP: ${{ needs.run-tests.outputs.failed_step }} + steps: + - name: Post PR comment (success, failure, or cancelled) + uses: actions/github-script@v6 + with: + script: | + const jobStatus = process.env.JOB_STATUS; + const failedStep = process.env.FAILED_STEP; + + if (jobStatus === 'success') { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "**Regression Test workflow finished successfully.** ✅" + }); + } else if (jobStatus === 'cancelled') { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "**Regression Test workflow was cancelled.** ⚠️" + }); + } else if (jobStatus === 'failure') { + let body = "**Regression Test workflow failed.** ❌"; + if (failedStep) { + body += `\nFailed step: **${failedStep}**`; + } else { + body += "\nFailed step: Unknown"; + } + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + \ No newline at end of file diff --git a/package.json b/package.json index c9588c750f..4d38b09893 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,10 @@ "build-storybook": "build-storybook", "mock-server": "node scripts/mockServer.js", "mock-server:dev": "nodemon --watch tests/mockServer scripts/mockServer.js", + "add-comment-to-http-client": "node scripts/ci-cd-scripts/appendCommentToHttpClient.js", "test:ui": "node scripts/testui.js", "report": "node tests/report.js", - "test:regression": "npm run test:ui && npm run report", + "test:regression": "HEADLESS=true npm run test:ui && sleep 10 && echo 'Finished regression' && sleep 10 && npm run report", "start:regression": "concurrently \"npm:mock-server\" \"npm:start\" \"npm:test:regression\"", "ui-steps": "export BABEL_ENV=test; export NODE_ENV=test; npx -p @babel/core -p @babel/node babel-node --presets @babel/preset-env scripts/collectUITestsSteps.js", "nli": "npm link iguazio.dashboard-react-controls", diff --git a/scripts/ci-cd-scripts/appendCommentToHttpClient.js b/scripts/ci-cd-scripts/appendCommentToHttpClient.js new file mode 100644 index 0000000000..405b872fc4 --- /dev/null +++ b/scripts/ci-cd-scripts/appendCommentToHttpClient.js @@ -0,0 +1,34 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +const fs = require('fs') +const path = require('path') + +const sourceFilePath = path.join(__dirname, 'commentedHttpClient.js') +const targetFilePath = path.join(__dirname, '../../src/httpClient.js') + +try { + const fileContent = fs.readFileSync(sourceFilePath, 'utf-8') + fs.writeFileSync(targetFilePath, fileContent) + + console.log(`Successfully overwritten ${targetFilePath} with content from ${sourceFilePath}`) +} catch (err) { + console.error(`Error occurred: ${err.message}`) + process.exit(1) +} diff --git a/scripts/ci-cd-scripts/commentedHttpClient.js b/scripts/ci-cd-scripts/commentedHttpClient.js new file mode 100644 index 0000000000..42d878cbc2 --- /dev/null +++ b/scripts/ci-cd-scripts/commentedHttpClient.js @@ -0,0 +1,194 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. You may not use this +file except in compliance with the License. You may obtain a copy of +the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +import axios from 'axios' +import qs from 'qs' + +// import { ConfirmDialog } from 'igz-controls/components' +import { + CANCEL_REQUEST_TIMEOUT, + LARGE_REQUEST_CANCELED + // PROJECTS_PAGE_PATH +} from './constants' +// import { openPopUp } from 'igz-controls/utils/common.util' +// import { mlrunUnhealthyErrors } from './components/ProjectsPage/projects.util' + +const headers = { + 'Cache-Control': 'no-cache' +} + +// serialize a param with an array value as a repeated param, for example: +// { label: ['host', 'owner=admin'] } => 'label=host&label=owner%3Dadmin' +const paramsSerializer = params => qs.stringify(params, { arrayFormat: 'repeat' }) + +// const MAX_CONSECUTIVE_ERRORS_COUNT = 2 +// let consecutiveErrorsCount = 0 + +export const mainBaseUrl = `${process.env.PUBLIC_URL}/api/v1` +export const mainBaseUrlV2 = `${process.env.PUBLIC_URL}/api/v2` + +export const mainHttpClient = axios.create({ + baseURL: mainBaseUrl, + headers, + paramsSerializer +}) + +export const mainHttpClientV2 = axios.create({ + baseURL: mainBaseUrlV2, + headers, + paramsSerializer +}) + +export const functionTemplatesHttpClient = axios.create({ + baseURL: `${process.env.PUBLIC_URL}/function-catalog`, + headers +}) + +export const nuclioHttpClient = axios.create({ + baseURL: `${process.env.PUBLIC_URL}/nuclio/api`, + headers +}) + +export const iguazioHttpClient = axios.create({ + baseURL: process.env.NODE_ENV === 'production' ? '/api' : '/iguazio/api', + headers +}) + +const getAbortSignal = (controller, abortCallback, timeoutMs) => { + let timeoutId = null + const newController = new AbortController() + const abortController = controller || newController + + if (timeoutMs) { + timeoutId = setTimeout(() => abortController.abort(LARGE_REQUEST_CANCELED), timeoutMs) + } + + abortController.signal.onabort = event => { + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (abortCallback) { + abortCallback(event) + } + } + + return [abortController.signal, timeoutId] +} + +let requestId = 1 +let requestTimeouts = {} +let largeResponsePopUpIsOpen = false + +const requestLargeDataOnFulfill = config => { + if (config?.ui?.setLargeRequestErrorMessage) { + const [signal, timeoutId] = getAbortSignal( + config.ui?.controller, + abortEvent => { + if (abortEvent.target.reason === LARGE_REQUEST_CANCELED) { + showLargeResponsePopUp(config.ui.setLargeRequestErrorMessage) + } + }, + CANCEL_REQUEST_TIMEOUT + ) + + config.signal = signal + + requestTimeouts[requestId] = timeoutId + config.ui.requestId = requestId + requestId++ + } + + return config +} +const requestLargeDataOnReject = error => { + return Promise.reject(error) +} +const responseFulfillInterceptor = response => { + // consecutiveErrorsCount = 0 + + if (response.config?.ui?.requestId) { + const isLargeResponse = + response.data?.total_size >= 0 + ? response.data.total_size > 10000 + : Object.values(response.data)?.[0]?.length > 10000 + + clearTimeout(requestTimeouts[response.config.ui.requestId]) + delete requestTimeouts[response.config.ui.requestId] + + if (isLargeResponse) { + showLargeResponsePopUp(response.config.ui.setLargeRequestErrorMessage) + + throw new Error(LARGE_REQUEST_CANCELED) + } else { + response.config.ui.setLargeRequestErrorMessage('') + } + } + + return response +} +const responseRejectInterceptor = error => { + if (error.config?.ui?.requestId) { + clearTimeout(requestTimeouts[error.config.ui.requestId]) + delete requestTimeouts[error.config.ui.requestId] + } + + // if (error.config?.method === 'get') { + // if ( + // mlrunUnhealthyErrors.includes(error.response?.status) && + // consecutiveErrorsCount < MAX_CONSECUTIVE_ERRORS_COUNT + // ) { + // consecutiveErrorsCount++ + // + // if ( + // consecutiveErrorsCount === MAX_CONSECUTIVE_ERRORS_COUNT && + // window.location.pathname !== `/${PROJECTS_PAGE_PATH}` + // ) { + // window.location.href = '/projects' + // } + // } + // } + + return Promise.reject(error) +} + +// Request interceptors +mainHttpClient.interceptors.request.use(requestLargeDataOnFulfill, requestLargeDataOnReject) +mainHttpClientV2.interceptors.request.use(requestLargeDataOnFulfill, requestLargeDataOnReject) + +// Response interceptors +mainHttpClient.interceptors.response.use(responseFulfillInterceptor, responseRejectInterceptor) +mainHttpClientV2.interceptors.response.use(responseFulfillInterceptor, responseRejectInterceptor) + +export const showLargeResponsePopUp = setLargeRequestErrorMessage => { + if (!largeResponsePopUpIsOpen) { + const errorMessage = + 'The query result is too large to display. Add a filter (or narrow it) to retrieve fewer results.' + + setLargeRequestErrorMessage(errorMessage) + largeResponsePopUpIsOpen = true + + // openPopUp(ConfirmDialog, { + // message: errorMessage, + // closePopUp: () => { + // largeResponsePopUpIsOpen = false + // } + // }) + } +} diff --git a/scripts/testui.js b/scripts/testui.js index 7d931679b6..b7f17bbc71 100644 --- a/scripts/testui.js +++ b/scripts/testui.js @@ -17,48 +17,53 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ -'use strict'; -const { report } = require('../tests/config'); -const fs = require('fs'); +const { report } = require('../tests/config') +const fs = require('fs') // Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = 'test'; -process.env.NODE_ENV = 'test'; +process.env.BABEL_ENV = 'test' +process.env.NODE_ENV = 'test' // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { - throw err; -}); + throw err +}) // Ensure environment variables are read. -require('../config/env'); +require('../config/env') -const execSync = require('child_process').execSync; -const argv = process.argv.slice(2); +const execSync = require('child_process').execSync +const argv = process.argv.slice(2) // build cucumber executive command -const cucumberCommand = 'cucumber-js --require-module @babel/register --require-module @babel/polyfill ' + - '-f json:' + report + '.json -f html:' + report +'_default.html tests ' + - argv.join(' '); +const cucumberCommand = + 'cucumber-js --require-module @babel/register --require-module @babel/polyfill ' + + '-f json:' + + report + + '.json -f html:' + + report + + '_default.html tests ' + + argv.join(' ') + + '-t @smoke' // check and create report folder -const reportDir = report.split('/').slice(0, -1).join('/'); -console.log(reportDir); +const reportDir = report.split('/').slice(0, -1).join('/') +console.log(reportDir) if (!fs.existsSync(reportDir)) { - fs.mkdirSync(reportDir); + fs.mkdirSync(reportDir) } function runCrossPlatform() { - try { - execSync(cucumberCommand); - return true; - } catch (e) { - return false; - } + try { + execSync(cucumberCommand) + return true + } catch (e) { + return false + } } // cucumber -runCrossPlatform(); +runCrossPlatform() diff --git a/tests/config.js b/tests/config.js index 1d98a14095..987df470be 100644 --- a/tests/config.js +++ b/tests/config.js @@ -17,10 +17,16 @@ illegal under applicable law, and the grant of the foregoing license under the Apache 2.0 license is conditioned upon your compliance with such restriction. */ + +const HEADLESS = process.env.HEADLESS === 'true' || false + +/* eslint-disable-next-line no-console */ +console.log(`DRIVER_SLEEP: ${HEADLESS}`) + module.exports = { timeout: 60000, browser: 'chrome', - headless: false, + headless: HEADLESS, screen_size: { width: 1600, height: 900 }, report: 'tests/reports/cucumber_report', test_url: 'localhost',