diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1490f4a6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,127 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + unit-tests-backend: + runs-on: ubuntu-latest + outputs: + coverage: ${{ steps.test-backend.outputs.report }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + cache-dependency-path: bugtracker-backend/go.sum + + - name: Install go-junit-report + run: go install github.com/jstemmer/go-junit-report/v2@latest + + - name: "Execute Backend unit Tests" + id: test-backend + working-directory: ./bugtracker-backend + run: | + go test -json -coverprofile=coverage.out -covermode=atomic ./... + go tool cover -func=coverage.out > coverage.txt + + echo "## Go test coverage report" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat coverage.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + echo "report<> $GITHUB_OUTPUT + cat coverage.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + + go test -v ./... 2>&1 | go-junit-report > test_results.xml + + - name: Publish Backend test results + uses: dorny/test-reporter@v1 + if : always() + with: + name: Backend Unit Tests Report + path: bugtracker-backend/test_results.xml + reporter: jest-junit + + # uses: actions/setup-go@v5 + # with: + # go-version: '1.21' + + # - name: Run Go tests + # run: | + # cd bugtracker-backend + # go test -v ./... + unit-tests-frontend: + runs-on: ubuntu-latest + outputs: + coverage: ${{ steps.test-frontend.outputs.report }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: bugtracker-frontend/package.json + + - name: Execute Frontend Unit tests + id: test-frontend + working-directory: ./bugtracker-frontend + run: | + npm ci + npm test | tee full_output.txt + + echo "## Frontend Test Coverage Report" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ls -al >> $GITHUB_STEP_SUMMARY + cat full_output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + echo "report<> $GITHUB_OUTPUT + cat full_output.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Publish Frontend Test Results + uses: dorny/test-reporter@v1 + if : always() + with: + name: Frontend Unit Tests Report + path: bugtracker-frontend/test-results.xml + reporter: jest-junit + create-coverage-comment: + if: github.event_name == 'pull_request' + needs: [unit-tests-backend, unit-tests-frontend] + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Create Backend Coverage Comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + ## Backend Test Coverage Report + ``` + ${{ needs.unit-tests-backend.outputs.coverage }} + + - name: Create Frontend Coverage Comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + ## Frontend Test Coverage Report + ``` + ${{ needs.unit-tests-frontend.outputs.coverage }} + + ``` \ No newline at end of file diff --git a/.github/workflows/first_test_workflow.yml b/.github/workflows/first_test_workflow.yml new file mode 100644 index 00000000..5a9bf6df --- /dev/null +++ b/.github/workflows/first_test_workflow.yml @@ -0,0 +1,37 @@ +# This is a basic workflow to help you get started with Actions + +name: CI_First_workflow + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "main" branch + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. + date diff --git a/bugtracker-frontend/package.json b/bugtracker-frontend/package.json index d1e65be4..3b590155 100644 --- a/bugtracker-frontend/package.json +++ b/bugtracker-frontend/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest --coverage --coverageReporters='text-summary' --coverageReporters='text' --coverageReporters='html' --reporters=default --reporters=jest-junit", + "test": "jest --testTimeout=240000 --coverage --coverageReporters='text-summary' --coverageReporters='text' --coverageReporters='html' --reporters=default --reporters=jest-junit", "test:watch": "jest --watch", "test:integration": "playwright test" }, diff --git a/bugtracker-frontend/src/components/AddBugModal.test.tsx b/bugtracker-frontend/src/components/AddBugModal.test.tsx index 73ed2ede..fc5f9e79 100644 --- a/bugtracker-frontend/src/components/AddBugModal.test.tsx +++ b/bugtracker-frontend/src/components/AddBugModal.test.tsx @@ -47,7 +47,7 @@ describe("AddBugModal", () => { priority: "High", }); expect(mockOnClose).toHaveBeenCalled(); - }); + }, 60000); it("should close modal when Cancel button is clicked", () => { render(); diff --git a/bugtracker-frontend/src/components/BugList.test.tsx b/bugtracker-frontend/src/components/BugList.test.tsx index 98300a68..ba658db4 100644 --- a/bugtracker-frontend/src/components/BugList.test.tsx +++ b/bugtracker-frontend/src/components/BugList.test.tsx @@ -73,7 +73,10 @@ describe("BugList", () => { const renderAndWaitForData = async () => { (getBugs as jest.Mock).mockResolvedValue(mockBugs); - const result = render(); + let result; + await act(async () => { + result = render(); + }); await waitFor(() => { expect(screen.getByText("All Bugs")).toBeInTheDocument(); @@ -94,7 +97,9 @@ describe("BugList", () => { .spyOn(console, "error") .mockImplementation(() => {}); (getBugs as jest.Mock).mockRejectedValue(new Error("Failed to fetch")); - render(); + await act(async () => { + render(); + }); await waitFor(() => { expect( screen.getByText("Error: Failed to fetch bugs") @@ -241,18 +246,25 @@ describe("BugList", () => { }); }); - it("displays the correct version number", () => { - render(); + it("displays the correct version number", async () => { + await act(async () => { + render(); + }); - expect(screen.getByText(`v${APP_VERSION}`)).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(`v${APP_VERSION}`)).toBeInTheDocument(); + }); }); - it("displays the version number in the header", () => { - render(); - - const header = screen.getByRole("navigation"); - const versionElement = screen.getByText(`v${APP_VERSION}`); + it("displays the version number in the header", async () => { + await act(async () => { + render(); + }); - expect(header).toContainElement(versionElement); + await waitFor(() => { + const header = screen.getByRole("navigation"); + const versionElement = screen.getByText(`v${APP_VERSION}`); + expect(header).toContainElement(versionElement); + }); }); }); diff --git a/jenkins/Jenkinsfile b/jenkins/Jenkinsfile new file mode 100644 index 00000000..ec5d47b2 --- /dev/null +++ b/jenkins/Jenkinsfile @@ -0,0 +1,287 @@ +pipeline { + agent any + + // tools { + // This must match the name you gave the tool in Step 1 + // go 'go-1.25.6' + // } + + stages { +// stage('Prepare Environment') { +// steps { +// // Create the directory and set permissions before the Docker agent starts +// sh 'mkdir -p /var/jenkins_home/go-cache && chmod 777 /var/jenkins_home/go-cache' +// } +// } + stage('Execute FE & BE Unit Test') { + parallel { + stage('Unit tests - Backend') { + agent { + docker { + image 'snakee/golang-junit:1.21' + reuseNode true + // Redirect GOCACHE and GOPATH to the workspace + // This ensures downloads persist between runs + args ''' + -e GOCACHE=${WORKSPACE}/.go-cache/cache + -e GOPATH=${WORKSPACE}/.go-cache/path + -v /var/jenkins_home/go-cache:/var/jenkins_home/go-cache + ''' + } + } + steps { + dir('bugtracker-backend') { + // This will now work because 'go' is in the PATH + // This is to execute tests without any report + // sh 'go test -v ./...' + // This is to execute tests with a test report + sh ''' + go test -v ./... 2>&1 | go-junit-report > test_results.xml + # Generate coverage report + go test -coverprofile=coverage.out -covermode=atomic ./... + go tool cover -html=coverage.out -o coverage.html + + mkdir -p reports + mv coverage.html reports/ + ''' + } + sh 'date' + } + post { + always { + junit 'bugtracker-backend/test_results.xml' + + publishHTML( target: [ + reportDir: 'bugtracker-backend/reports', + reportFiles: 'coverage.html', + reportName: 'Backend Coverage Report' + ] + ) + } + } + } + + stage('Unit tests - Frontend'){ + agent { + docker { + image 'node:20-alpine' + reuseNode true + // Redirect npm cache to the workspace to avoid permission issues + args ''' + --network=host + -e CI=true + -e npm_config_cache=${WORKSPACE}/.npm-cache + -e npm_config_fetch_retries=5 + -e npm_config_fetch_retries_mintimeout=20000 + -e npm_config_fetch_retries_maxtimeout=120000 + ''' + + // This ensures downloads persist between runs + // args '-v /var/jenkins_home/npm-cache:/.npm -e npm_config_cache=/.npm' + } + } + + steps { + dir('bugtracker-frontend') { + sh ''' + # Forcefully remove node_modules if it exists + # This bypasses the npm 'rmdir' struggle + rm -rf node_modules + npm ci + npm test --watchAll=false + npm test -- --runInBand --silent --no-cache --reporters=default --reporters=jest-junit + + mkdir -p reports + mv coverage reports/ + ''' + } + } + // post actions after frontend tests + post { + always { + junit 'bugtracker-frontend/test-results.xml' + publishHTML(target: [ + reportDir: 'bugtracker-frontend/reports/coverage', + reportFiles: 'index.html', + reportName: 'Frontend Coverage Report' + ] + ) + } + } + } + } + } + stage('Launch Application') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0 --network=host' + } + } + + steps{ + sh 'docker compose up --build -d' + } + } + stage('API tests') { + agent { + docker { + image 'mcr.microsoft.com/playwright:v1.50.0-jammy' + reuseNode true + args ''' + -u 0 --network=host + -e npm_config_cache=${WORKSPACE}/.npm-cache + -e npm_config_fetch_retries=5 + -e npm_config_fetch_retries_mintimeout=20000 + -e npm_config_fetch_retries_maxtimeout=120000 + ''' + } + } + steps { + dir('tests-api') { + // wait 30 secs to application to start + sh 'npx wait-port http://localhost:8080/api/health -t 30000' + sh 'npm ci' + sh 'npx playwright test' + } + } + // Publish API test results + post { + always { + junit 'tests-api/test-results/results.xml' + publishHTML(target: [ + reportDir: 'tests-api/playwright-report', + reportFiles: 'index.html', + reportName: 'Playwright API Test Report' + ]) + } + } + } + + stage('E2E Tests') { + agent { + docker { + image 'mcr.microsoft.com/playwright:v1.50.0-jammy' + reuseNode true + args '-u 0 --network=host' + } + } + steps { + dir('tests-e2e') { + sh 'npm ci' + sh 'npx playwright test' + } + } + } + + stage('Performance Tests') { + parallel { + stage('K6 Performance Tests') { + agent { + docker { + image 'grafana/k6:latest' + reuseNode true + args '--network=host -u root --entrypoint=""' + } + } + steps { + dir('tests-perf') { + sh 'k6 run script.js' + } + } + post { + always { + publishHTML(target: [ + reportDir: 'tests-perf', + reportFiles: 'perf-results.html', + reportName: 'K6 Performance Test Results' + ]) + } + } + } + + stage('JMeter Performance Tests') { + agent { + docker { + image 'justb4/jmeter:latest' + reuseNode true + args '--network=host -u root --entrypoint=""' + } + } + steps { + dir('tests-perf') { + sh ''' + rm -rf jmeter-report jmeter-results.jtl + jmeter -n \ + -t bugtracker-jmeter.jmx \ + -l jmeter-results.jtl \ + -e -o jmeter-report + ''' + } + } + post { + always { + // Approach 1: HTML Dashboard Report + publishHTML(target: [ + reportDir: 'tests-perf/jmeter-report', + reportFiles: 'index.html', + reportName: 'JMeter Performance Report', + keepAll: true, + alwaysLinkToLastBuild: true + ]) + + // Approach 2: Archive raw results for analysis + archiveArtifacts artifacts: 'tests-perf/jmeter-results.jtl', + allowEmptyArchive: false, + fingerprint: true + + // Approach 3: Performance Plugin (requires plugin installation) + // perfReport sourceDataFiles: 'tests-perf/jmeter-results.jtl', + // errorFailedThreshold: 5, + // errorUnstableThreshold: 2 + } + } + } + } + } + + stage('Maintenance: Cache Check') { + steps { + script { + // Check size of the caches in Megabytes + // // 2000000 KB is roughly 2G + sh ''' + CACHE_SIZE=$(du -s ${WORKSPACE}/.go-cache ${WORKSPACE}/.npm-cache 2>/dev/null | awk '{sum += $1} END {print sum}') + echo "Current Cache Size: ${CACHE_SIZE} KB" + if [ "${CACHE_SIZE}" -gt 2000000 ]; then + echo "Cache limit exceeded. Clearing caches..." + rm -rf ${WORKSPACE}/.go-cache/* + rm -rf ${WORKSPACE}/.npm-cache/* + else + echo "Cache size is within limits." + fi + ''' + } + } + } + } + // The POST block: This runs after the stages are finished + post { + always { + echo 'Pipeline finished. Cleaning up workspace...' + // This clears the workspace to prevent disk bloat + // but you might want to remove this if you want to keep caches + cleanWs() + } + success { + echo 'Build Successful! Sending notification...' + cleanWs() + + } + failure { + echo 'Build Failed! Check logs for permission or test errors.' + cleanWs() + } + } +} \ No newline at end of file diff --git a/jenkins/Jenkinsfile.jmeter b/jenkins/Jenkinsfile.jmeter new file mode 100644 index 00000000..5b23d2ed --- /dev/null +++ b/jenkins/Jenkinsfile.jmeter @@ -0,0 +1,87 @@ +pipeline { + agent any + + stages { + stage('Launch Application') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0 --network=host' + } + } + steps { + sh 'docker compose up --build -d' + } + } + + stage('Wait for Application') { + agent { + docker { + image 'node:20-alpine' + reuseNode true + args '--network=host' + } + } + steps { + sh 'npx wait-port http://localhost:8080/api/health -t 30000' + } + } + + stage('JMeter Performance Tests') { + agent { + docker { + image 'justb4/jmeter:latest' + reuseNode true + args '--network=host -u root --entrypoint=""' + } + } + steps { + dir('tests-perf') { + sh ''' + rm -rf jmeter-report jmeter-results.jtl + jmeter -n \ + -t bugtracker-jmeter.jmx \ + -l jmeter-results.jtl \ + -e -o jmeter-report + ''' + } + } + post { + always { + publishHTML(target: [ + reportDir: 'tests-perf/jmeter-report', + reportFiles: 'index.html', + reportName: 'JMeter Performance Report' + ]) + archiveArtifacts artifacts: 'tests-perf/jmeter-results.jtl', allowEmptyArchive: true + } + } + } + + stage('Cleanup') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0 --network=host' + } + } + steps { + sh 'docker compose down' + } + } + } + + post { + always { + echo 'Performance test pipeline finished.' + } + success { + echo 'Performance tests passed!' + } + failure { + echo 'Performance tests failed! Check the JMeter report.' + } + } +} diff --git a/jenkins/Jenkinsfile.jmeter-advanced b/jenkins/Jenkinsfile.jmeter-advanced new file mode 100644 index 00000000..46ea8d2d --- /dev/null +++ b/jenkins/Jenkinsfile.jmeter-advanced @@ -0,0 +1,115 @@ +pipeline { + agent any + + stages { + stage('Launch Application') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0 --network=host' + } + } + steps { + sh 'docker compose up --build -d' + } + } + + stage('Wait for Application') { + agent { + docker { + image 'node:20-alpine' + reuseNode true + args '--network=host' + } + } + steps { + sh 'npx wait-port http://localhost:8080/api/health -t 30000' + } + } + + stage('JMeter Performance Tests') { + agent { + docker { + image 'justb4/jmeter:latest' + reuseNode true + args '--network=host -u root --entrypoint=""' + } + } + steps { + dir('tests-perf') { + sh ''' + # Basic load test + rm -rf jmeter-report jmeter-results.jtl + jmeter -n \ + -t bugtracker-jmeter.jmx \ + -l jmeter-results.jtl \ + -e -o jmeter-report + + # Bulk test: Create 100 bugs and list them + rm -rf bulk-report bulk-results.jtl + jmeter -n \ + -t bugtracker-bulk-test.jmx \ + -l bulk-results.jtl \ + -e -o bulk-report + ''' + } + } + post { + always { + // Approach 1: HTML Dashboard + publishHTML(target: [ + reportDir: 'tests-perf/jmeter-report', + reportFiles: 'index.html', + reportName: 'JMeter Load Test Report', + keepAll: true, + alwaysLinkToLastBuild: true + ]) + + publishHTML(target: [ + reportDir: 'tests-perf/bulk-report', + reportFiles: 'index.html', + reportName: 'JMeter Bulk Test (100 Bugs)', + keepAll: true, + alwaysLinkToLastBuild: true + ]) + + archiveArtifacts artifacts: 'tests-perf/*.jtl', + allowEmptyArchive: false, + fingerprint: true + } + success { + echo '✅ Performance tests passed!' + } + failure { + echo '❌ Performance tests failed!' + } + } + } + + stage('Cleanup') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0 --network=host' + } + } + steps { + sh 'docker compose down' + } + } + } + + post { + always { + echo 'Performance test pipeline finished.' + } + success { + echo '✅ All stages completed successfully!' + } + failure { + echo '❌ Pipeline failed! Check the reports.' + } + } +} diff --git a/jenkins/Jenkinsfile.parallel b/jenkins/Jenkinsfile.parallel new file mode 100644 index 00000000..369088da --- /dev/null +++ b/jenkins/Jenkinsfile.parallel @@ -0,0 +1,41 @@ +pipeline { + agent any + + stages { + stage('Test Suite') { + parallel { + stage('Backend (Go)') { + agent { + docker { + image 'snakee/golang-junit:1.21' + reuseNode true + args '-e GOCACHE=/tmp/.cache -e GOPATH=/tmp/go' + } + } + steps { + dir('bugtracker-backend') { + sh 'go test -v ./...' + } + } + } + + stage('Frontend (Node)') { + agent { + docker { + // Using the official Node image instead of a Tool + image 'node:20' + reuseNode true + } + } + steps { + dir('bugtracker-frontend') { + // npm ci is excellent - it's faster and cleaner for CI + sh 'npm ci' + sh 'npm test' + } + } + } + } + } + } +} \ No newline at end of file diff --git a/jenkins/Jenkinsfile.sequential b/jenkins/Jenkinsfile.sequential new file mode 100644 index 00000000..90fed052 --- /dev/null +++ b/jenkins/Jenkinsfile.sequential @@ -0,0 +1,199 @@ +pipeline { + agent any + + // tools { + // This must match the name you gave the tool in Step 1 + // go 'go-1.25.6' + // } + + stages { +// stage('Prepare Environment') { +// steps { +// // Create the directory and set permissions before the Docker agent starts +// sh 'mkdir -p /var/jenkins_home/go-cache && chmod 777 /var/jenkins_home/go-cache' +// } +// } + stage('Unit tests - Backend') { + agent { + docker { + image 'snakee/golang-junit:1.21' + reuseNode true + // Redirect GOCACHE and GOPATH to the workspace + // This ensures downloads persist between runs + args ''' + -e GOCACHE=${WORKSPACE}/.go-cache/cache + -e GOPATH=${WORKSPACE}/.go-cache/path + -v /var/jenkins_home/go-cache:/var/jenkins_home/go-cache + ''' + } + } + steps { + dir('bugtracker-backend') { + // This will now work because 'go' is in the PATH + // This is to execute tests without any report + // sh 'go test -v ./...' + // This is to execute tests with a test report + sh ''' + go test -v ./... 2>&1 | go-junit-report > test_results.xml + # Generate coverage report + go test -coverprofile=coverage.out -covermode=atomic ./... + go tool cover -html=coverage.out -o coverage.html + + mkdir -p reports + mv coverage.html reports/ + ''' + } + sh 'date' + } + post { + always { + junit 'bugtracker-backend/test_results.xml' + + publishHTML( target: [ + reportDir: 'bugtracker-backend/reports', + reportFiles: 'coverage.html', + reportName: 'Backend Coverage Report' + ] + ) + } + } + } + + stage('Unit tests - Frontend'){ + agent { + docker { + image 'node:20-alpine' + reuseNode true + // Redirect npm cache to the workspace to avoid permission issues + args '-e npm_config_cache=${WORKSPACE}/.npm-cache' + // This ensures downloads persist between runs + // args '-v /var/jenkins_home/npm-cache:/.npm -e npm_config_cache=/.npm' + } + } + + steps { + dir('bugtracker-frontend') { + sh ''' + # Forcefully remove node_modules if it exists + # This bypasses the npm 'rmdir' struggle + rm -rf node_modules + npm ci + npm test + + mkdir -p reports + mv coverage reports/ + ''' + } + } + // post actions after frontend tests + post { + always { + junit 'bugtracker-frontend/test-results.xml' + publishHTML(target: [ + reportDir: 'bugtracker-frontend/reports/coverage', + reportFiles: 'index.html', + reportName: 'Frontend Coverage Report' + ] + ) + } + } + } + stage('Launch Application') { + agent { + docker { + image 'docker:27.5.1' + reuseNode true + args '-v /var/run/docker.sock:/var/run/docker.sock -u 0' + } + } + + steps{ + sh 'docker compose up --build -d' + + } + } + stage('API tests') { + agent { + docker { + image 'mcr.microsoft.com/playwright:v1.50.0-jammy' + reuseNode true + args '-u 0 --network=host' + } + } + steps { + dir('tests-api') { + // wait 30 secs to application to start + sh 'npx wait-port http://localhost:8080/api/health -t 30000' + sh 'npm ci' + sh 'npx playwright test' + } + } + // Publish API test results + post { + always { + junit 'tests-api/test-results/results.xml' + publishHTML(target: [ + reportDir: 'tests-api/playwright-report', + reportFiles: 'index.html', + reportName: 'Playwright API Test Report' + ]) + } + } + } + + stage('E2E Tests') { + agent { + docker { + image 'mcr.microsoft.com/playwright:v1.50.0-jammy' + reuseNode true + args '-u 0 --network=host' + } + } + steps { + dir('tests-e2e') { + sh 'npm ci' + sh 'npx playwright test' + } + } + } + + stage('Maintenance: Cache Check') { + steps { + script { + // Check size of the caches in Megabytes + // // 2000000 KB is roughly 2G + sh ''' + CACHE_SIZE=$(du -s ${WORKSPACE}/.go-cache ${WORKSPACE}/.npm-cache 2>/dev/null | awk '{sum += $1} END {print sum}') + echo "Current Cache Size: ${CACHE_SIZE} KB" + if [ "${CACHE_SIZE}" -gt 2000000 ]; then + echo "Cache limit exceeded. Clearing caches..." + rm -rf ${WORKSPACE}/.go-cache/* + rm -rf ${WORKSPACE}/.npm-cache/* + else + echo "Cache size is within limits." + fi + ''' + } + } + } + } + // The POST block: This runs after the stages are finished + post { + always { + echo 'Pipeline finished. Cleaning up workspace...' + // This clears the workspace to prevent disk bloat + // but you might want to remove this if you want to keep caches + // cleanWs() + } + success { + echo 'Build Successful! Sending notification...' + cleanWs() + + } + failure { + echo 'Build Failed! Check logs for permission or test errors.' + cleanWs() + + } + } +} diff --git a/jenkins/Jenkinsfile.tools b/jenkins/Jenkinsfile.tools new file mode 100644 index 00000000..ca20939b --- /dev/null +++ b/jenkins/Jenkinsfile.tools @@ -0,0 +1,32 @@ +pipeline { + agent any + + tools { + // This must match the name you gave the tool in Step 1 + go 'go-1.25.6' + nodejs 'nodejs-25.5.0' + } + + stages { + stage('Unit tests - Backend') { + steps { + dir('bugtracker-backend') { + // This will now work because 'go' is in the PATH + sh 'go test -v ./...' + } + sh 'date' + } + } + stage('Unit tests - Frontend') { + steps { + dir('bugtracker-frontend') { + // This will now work because 'npm' is in the PATH + // sh 'node -v' + sh 'npm ci' + sh 'npm test' + } + sh 'date' + } + } + } +} \ No newline at end of file diff --git a/tests-perf/JENKINS-PUBLISHING.md b/tests-perf/JENKINS-PUBLISHING.md new file mode 100644 index 00000000..3dfd8bac --- /dev/null +++ b/tests-perf/JENKINS-PUBLISHING.md @@ -0,0 +1,198 @@ +# JMeter Results Publishing in Jenkins + +## Overview +Multiple approaches to publish and visualize JMeter performance test results in Jenkins. + +--- + +## Approach 1: publishHTML Plugin ✅ (Recommended) + +**What it does**: Publishes the JMeter HTML dashboard as an interactive report in Jenkins. + +### Configuration: +```groovy +publishHTML(target: [ + reportDir: 'tests-perf/jmeter-report', + reportFiles: 'index.html', + reportName: 'JMeter Performance Report', + keepAll: true, // Keep reports from all builds + alwaysLinkToLastBuild: true // Quick access to latest report +]) +``` + +### Pros: +- ✅ Rich visual dashboard with graphs +- ✅ Built-in Jenkins plugin (no extra installation) +- ✅ Interactive charts and statistics +- ✅ Historical trend data + +### Cons: +- ❌ No automatic pass/fail thresholds +- ❌ Requires HTML report generation (`-e -o` flags) + +### Access: +Build page → "JMeter Performance Report" link in sidebar + +--- + +## Approach 2: Archive Artifacts + +**What it does**: Stores raw JTL files for download and external analysis. + +### Configuration: +```groovy +archiveArtifacts artifacts: 'tests-perf/jmeter-results.jtl', + allowEmptyArchive: false, + fingerprint: true +``` + +### Pros: +- ✅ Raw data preservation +- ✅ Can be analyzed with external tools +- ✅ Fingerprinting for tracking changes +- ✅ Downloadable for offline analysis + +### Cons: +- ❌ No visualization in Jenkins +- ❌ Requires manual analysis + +### Access: +Build page → "Build Artifacts" → Download JTL file + +--- + +## Approach 3: Performance Plugin 🔌 (Advanced) + +**What it does**: Provides trend analysis, thresholds, and build status based on performance metrics. + +### Installation: +```bash +# Install via Jenkins Plugin Manager +Manage Jenkins → Plugins → Available → "Performance Plugin" +``` + +### Configuration: +```groovy +perfReport sourceDataFiles: 'tests-perf/jmeter-results.jtl', + errorFailedThreshold: 5, // Mark build as FAILED if >5% errors + errorUnstableThreshold: 2, // Mark build as UNSTABLE if >2% errors + errorUnstableResponseTimeThreshold: 'Health Check:500', // 500ms threshold + relativeFailedThresholdPositive: 10, // Fail if 10% slower than previous + relativeUnstableThresholdPositive: 5 // Unstable if 5% slower +``` + +### Pros: +- ✅ Automatic pass/fail based on thresholds +- ✅ Performance trend graphs across builds +- ✅ Comparison with previous builds +- ✅ Detailed metrics per request + +### Cons: +- ❌ Requires plugin installation +- ❌ More complex configuration +- ❌ May need JTL format adjustments + +### Access: +Build page → "Performance Report" link + +--- + +## Approach 4: JUnit Format (For Test Results) + +**What it does**: Converts JMeter results to JUnit XML for test result tracking. + +### Configuration: +```groovy +steps { + sh ''' + jmeter -n -t bugtracker-jmeter.jmx -l jmeter-results.jtl -e -o jmeter-report + + # Convert JTL to JUnit XML (requires xslt processor) + xsltproc /path/to/jmeter-results-to-junit.xsl jmeter-results.jtl > jmeter-junit.xml + ''' +} +post { + always { + junit 'tests-perf/jmeter-junit.xml' + } +} +``` + +### Pros: +- ✅ Integrates with Jenkins test result tracking +- ✅ Shows pass/fail in test trends +- ✅ Email notifications on failures + +### Cons: +- ❌ Requires XSLT transformation +- ❌ Loses performance metrics detail +- ❌ Not ideal for performance data + +--- + +## Approach 5: InfluxDB + Grafana (Enterprise) + +**What it does**: Real-time metrics streaming to external monitoring system. + +### Configuration: +```groovy +steps { + sh ''' + jmeter -n -t bugtracker-jmeter.jmx \ + -l jmeter-results.jtl \ + -Jjmeter.save.saveservice.output_format=csv \ + -JinfluxdbUrl=http://influxdb:8086 \ + -JinfluxdbToken=mytoken + ''' +} +``` + +### Pros: +- ✅ Real-time monitoring +- ✅ Advanced visualization with Grafana +- ✅ Long-term trend analysis +- ✅ Alerting capabilities + +### Cons: +- ❌ Requires external infrastructure +- ❌ Complex setup +- ❌ Additional maintenance + +--- + +## Recommended Combination + +For most projects, use **Approach 1 + 2**: + +```groovy +post { + always { + // Visual dashboard + publishHTML(target: [ + reportDir: 'tests-perf/jmeter-report', + reportFiles: 'index.html', + reportName: 'JMeter Performance Report', + keepAll: true, + alwaysLinkToLastBuild: true + ]) + + // Raw data archive + archiveArtifacts artifacts: 'tests-perf/jmeter-results.jtl', + fingerprint: true + } +} +``` + +For advanced needs, add **Approach 3** (Performance Plugin) for threshold-based build status. + +--- + +## Comparison Table + +| Approach | Visualization | Thresholds | Trends | Setup Complexity | Best For | +|----------|--------------|------------|--------|------------------|----------| +| publishHTML | ⭐⭐⭐⭐⭐ | ❌ | ⭐⭐⭐ | Low | Quick visual reports | +| Archive Artifacts | ❌ | ❌ | ❌ | Very Low | Data preservation | +| Performance Plugin | ⭐⭐⭐⭐ | ✅ | ⭐⭐⭐⭐⭐ | Medium | CI/CD gates | +| JUnit Format | ⭐⭐ | ✅ | ⭐⭐⭐ | Medium | Test tracking | +| InfluxDB/Grafana | ⭐⭐⭐⭐⭐ | ✅ | ⭐⭐⭐⭐⭐ | High | Enterprise monitoring | diff --git a/tests-perf/JMETER-README.md b/tests-perf/JMETER-README.md new file mode 100644 index 00000000..492e1d46 --- /dev/null +++ b/tests-perf/JMETER-README.md @@ -0,0 +1,89 @@ +# JMeter Performance Tests + +This directory contains JMeter test plan for Bug Tracker API performance testing. + +## Prerequisites + +Install JMeter: +- **Windows**: `winget install Apache.JMeter` or download from https://jmeter.apache.org/ +- **macOS**: `brew install jmeter` +- **Linux**: Download from https://jmeter.apache.org/download_jmeter.cgi + +## Test Plan Overview + +The `bugtracker-jmeter.jmx` file replicates the k6 test with: +- **Duration**: 30 seconds +- **Virtual Users**: 1 +- **Tests**: + 1. Health Check (GET /api/health) + 2. Create Bug (POST /api/bugs) + 3. 5-second delay between iterations + +## Running Tests + +### GUI Mode (for development/debugging) +```bash +jmeter -t bugtracker-jmeter.jmx +``` + +### CLI Mode (for CI/CD) +```bash +jmeter -n -t bugtracker-jmeter.jmx -l results.jtl -e -o report +``` + +Parameters: +- `-n`: Non-GUI mode +- `-t`: Test plan file +- `-l`: Results log file +- `-e`: Generate HTML report +- `-o`: Output folder for HTML report + +### View Results +After CLI execution, open `report/index.html` in a browser. + +## Test Configuration + +Edit the `.jmx` file to modify: +- **Virtual Users**: Change `ThreadGroup.num_threads` (line 17) +- **Duration**: Change `ThreadGroup.duration` (line 19) +- **Server**: Change `HTTPSampler.domain` and `HTTPSampler.port` + +## Comparison: k6 vs JMeter + +| Feature | k6 | JMeter | +|---------|-----|--------| +| File Format | JavaScript | XML (.jmx) | +| Execution | CLI only | GUI + CLI | +| Scripting | JavaScript | GUI-based + Groovy | +| Reports | HTML + JSON | HTML + CSV + JTL | +| CI/CD | Excellent | Good | + +## Jenkins Integration + +The JMeter tests are integrated into the Jenkins pipeline: + +### Option 1: Main Jenkinsfile (Parallel with K6) +The main `jenkins/Jenkinsfile` runs both K6 and JMeter tests in parallel: +- Stage: "Performance Tests" → "JMeter Performance Tests" +- Docker image: `justb4/jmeter:5.6.3` +- Generates HTML report accessible in Jenkins + +### Option 2: Standalone JMeter Pipeline +Use `jenkins/Jenkinsfile.jmeter` for JMeter-only tests: +```bash +# In Jenkins, create a new pipeline job pointing to: +jenkins/Jenkinsfile.jmeter +``` + +This pipeline: +1. Launches the application with Docker Compose +2. Waits for the API to be ready +3. Runs JMeter tests +4. Publishes HTML report +5. Cleans up containers + +### Viewing Results in Jenkins +After pipeline execution: +1. Go to the build page +2. Click "JMeter Performance Report" in the sidebar +3. View detailed metrics, graphs, and statistics diff --git a/tests-perf/JMETER-SETUP-GUIDE.md b/tests-perf/JMETER-SETUP-GUIDE.md new file mode 100644 index 00000000..ba7f600c --- /dev/null +++ b/tests-perf/JMETER-SETUP-GUIDE.md @@ -0,0 +1,237 @@ +# JMeter Test Setup Guide + +## How JMeter Tests Work in This Repo + +### Test Files +1. **bugtracker-jmeter.jmx** - Basic continuous load test (30 seconds) +2. **bugtracker-bulk-test.jmx** - NEW: Create 100 bugs + list them + +--- + +## Understanding the JMX File Structure + +### 1. Test Plan +```xml + +``` +- Root element containing all test configuration + +### 2. Thread Group (Virtual Users) +```xml + + 10 + 5 + 10 + +``` +**Result**: 10 users × 10 loops = 100 bug creations + +### 3. HTTP Sampler (Request) +```xml + + localhost + 8080 + /api/bugs + POST +``` + +### 4. Dynamic Data with JMeter Functions +```json +{ + "title": "Bulk Test Bug ${__threadNum}-${__counter(TRUE,)}", + "priority": "${__Random(Low,Medium,High,)}" +} +``` +- `${__threadNum}` - Thread/user number (1-10) +- `${__counter(TRUE,)}` - Global counter (1-100) +- `${__Random(...)}` - Random value from list + +### 5. Listeners (Reports) +- **Summary Report** - Min, Max, Mean, Percentiles +- **Aggregate Report** - Statistical summary table +- **Graph Results** - Visual time-series chart +- **View Results in Table** - Detailed per-request data + +--- + +## New Test: Create 100 Bugs + List Them + +### Test Structure + +**Thread Group 1: Create 100 Bugs** +- 10 concurrent users +- Each creates 10 bugs +- Total: 100 bugs created + +**Thread Group 2: List All Bugs** +- 1 user +- Runs after all bugs created +- Fetches all bugs via GET /api/bugs + +### Reports Generated + +#### 1. Summary Report (CSV) +Location: `bulk-test-results.csv` + +| Label | # Samples | Average | Min | Max | Std. Dev. | Error % | Throughput | +|-------|-----------|---------|-----|-----|-----------|---------|------------| +| Create Bug | 100 | 45ms | 12ms | 234ms | 32ms | 0.00% | 15.2/sec | +| Get All Bugs | 1 | 156ms | 156ms | 156ms | 0ms | 0.00% | 1.0/sec | + +#### 2. HTML Dashboard Report +Generated with `-e -o` flags: +- **Statistics Table** - Min, Max, Mean, Percentiles (90%, 95%, 99%) +- **Response Time Graph** - Time-series visualization +- **Response Time Percentiles** - Distribution chart +- **Throughput Over Time** - Requests/second graph + +--- + +## Running the Tests + +### Option 1: GUI Mode (Development) +```bash +cd tests-perf +jmeter -t bugtracker-bulk-test.jmx +``` +- Opens JMeter GUI +- Click green "Start" button +- View real-time results in listeners + +### Option 2: CLI Mode (CI/CD) +```bash +cd tests-perf +jmeter -n \ + -t bugtracker-bulk-test.jmx \ + -l bulk-results.jtl \ + -e -o bulk-report +``` + +**Flags:** +- `-n` - Non-GUI mode +- `-t` - Test plan file +- `-l` - Results log file (JTL) +- `-e` - Generate HTML report +- `-o` - Output folder for HTML report + +### Option 3: Jenkins Pipeline +```groovy +stage('Bulk Performance Test') { + steps { + dir('tests-perf') { + sh ''' + rm -rf bulk-report bulk-results.jtl + jmeter -n \ + -t bugtracker-bulk-test.jmx \ + -l bulk-results.jtl \ + -e -o bulk-report + ''' + } + } + post { + always { + publishHTML(target: [ + reportDir: 'tests-perf/bulk-report', + reportFiles: 'index.html', + reportName: 'Bulk Test Report' + ]) + } + } +} +``` + +--- + +## Viewing Results + +### HTML Dashboard Report +Open `bulk-report/index.html` in browser: + +**Statistics Table:** +``` +Request Name | Samples | Min | Max | Mean | 90%ile | 95%ile | 99%ile +----------------|---------|------|------|------|--------|--------|-------- +Create Bug | 100 | 12ms | 234ms| 45ms | 78ms | 112ms | 198ms +Get All Bugs | 1 | 156ms| 156ms| 156ms| 156ms | 156ms | 156ms +``` + +**Response Time Graph:** +``` +Response Time (ms) +250 | * +200 | * * +150 | * * * +100 | * * + 50 | * + 0 |___________________ + 0 20 40 60 80 100 + Request # +``` + +### CSV Results +Open `bulk-test-results.csv`: +```csv +timeStamp,elapsed,label,responseCode,success,bytes,sentBytes,grpThreads,allThreads,Latency +1707656400000,45,Create Bug,201,true,256,128,10,10,42 +1707656400050,38,Create Bug,201,true,256,128,10,10,35 +... +``` + +--- + +## Customizing the Test + +### Change Number of Bugs +Edit Thread Group parameters: +```xml +20 +5 +``` +Result: 20 × 5 = 100 bugs + +### Add More Requests +Add new HTTPSamplerProxy after "Create Bug": +```xml + + /api/bugs/1 + PUT + ... + +``` + +### Change Server +```xml +production.example.com +443 +https +``` + +--- + +## Key Metrics Explained + +| Metric | Description | Good Value | +|--------|-------------|------------| +| **Min** | Fastest response time | < 50ms | +| **Max** | Slowest response time | < 500ms | +| **Mean** | Average response time | < 200ms | +| **90%ile** | 90% of requests faster than | < 300ms | +| **Error %** | Failed requests percentage | 0% | +| **Throughput** | Requests per second | > 10/sec | + +--- + +## Troubleshooting + +### Test doesn't create 100 bugs +- Check loop count: `num_threads × loops = 100` +- Verify API is running: `curl http://localhost:8080/api/health` + +### No HTML report generated +- Ensure output folder doesn't exist: `rm -rf bulk-report` +- Check JMeter version: `jmeter --version` (need 5.0+) + +### Results show errors +- Check response assertions +- View error details in "View Results Tree" listener +- Check API logs for failures diff --git a/tests-perf/bugtracker-bulk-test.jmx b/tests-perf/bugtracker-bulk-test.jmx new file mode 100644 index 00000000..5189dfc2 --- /dev/null +++ b/tests-perf/bugtracker-bulk-test.jmx @@ -0,0 +1,259 @@ + + + + + + + + Create 100 bugs and list them - Performance test + false + false + + + + + 10 + 5 + false + continue + + false + 10 + + + + + localhost + 8080 + http + /api/bugs + POST + true + true + true + + + + false + { + "title": "Bulk Test Bug ${__threadNum}-${__counter(TRUE,)}", + "description": "Performance test bug created at ${__time(yyyy-MM-dd HH:mm:ss,)}", + "priority": "${__Random(Low,Medium,High,)}", + "status": "Open" +} + = + + + + + + + + + Content-Type + application/json + + + + + + + 201 + + Assertion.response_code + false + 8 + + + + + + + + 1 + 1 + false + continue + + false + 1 + + + + + localhost + 8080 + http + /api/bugs + GET + true + true + + + + + 200 + + Assertion.response_code + false + 8 + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + bulk-test-results.csv + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + + diff --git a/tests-perf/bugtracker-jmeter.jmx b/tests-perf/bugtracker-jmeter.jmx new file mode 100644 index 00000000..1b880ba6 --- /dev/null +++ b/tests-perf/bugtracker-jmeter.jmx @@ -0,0 +1,179 @@ + + + + + + + + Performance test for Bug Tracker API + false + false + + + + 1 + 1 + 30 + true + continue + + false + -1 + + + + + localhost + 8080 + http + /api/health + GET + true + true + + + + + 200 + + Assertion.response_code + false + 8 + + + + + localhost + 8080 + http + /api/bugs + POST + true + true + true + + + + false + { + "title": "Test Bug ${__time()}", + "description": "This is a test bug created by JMeter", + "priority": "Medium", + "status": "Open" +} + = + + + + + + + + + Content-Type + application/json + + + + + + + 201 + + Assertion.response_code + false + 8 + + + + $.id + + false + false + false + false + + + + + 5000 + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + jmeter-results.csv + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + +