diff --git a/.github/workflows/k6.yaml b/.github/workflows/k6.yaml new file mode 100644 index 0000000000..0dc9c3d8a7 --- /dev/null +++ b/.github/workflows/k6.yaml @@ -0,0 +1,83 @@ +name: Run k6 + +on: + push: + branches: + - main + - master + - develop + pull_request: + branches: + - main + - master + - develop + +jobs: + protocol: + runs-on: ubuntu-latest + services: + redis: + image: "redis:8.0.1" + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 6379 on service container to the host + - 6379:6379 + steps: + - uses: actions/checkout@v2 + + - run: cp install/package.json package.json + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: NPM Install + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Setup on Redis + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567/forum", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "redis", + "redis:host": "127.0.0.1", + "redis:port": 6379, + "redis:password": "", + "redis:database": 0 + } + CI: >- + { + "host": "127.0.0.1", + "database": 1, + "port": 6379 + } + run: | + node app --setup="${SETUP}" --ci="${CI}" + - name: Build NodeBB + run: ./nodebb build + - name: Start NodeBB server + run: | + ./nodebb start + sleep 20 + - uses: grafana/setup-k6-action@v1 + - uses: grafana/run-k6-action@v1 + with: + path: | + ./test/load-test.js + - name: Stop NodeBB server + if: always() + run: ./nodebb stop diff --git a/.mocharc.yml b/.mocharc.yml index 16d8518d1b..a602d06ac6 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -2,3 +2,5 @@ reporter: dot timeout: 25000 exit: true bail: true +ignore: + - test/load-test.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 47cfa158f5..f26710566b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,6 +29,7 @@ export default defineConfig([ 'test/files/', '*.min.js', 'install/docker/', + 'test/load-test.js', ], }, // tests diff --git a/test/load-test.js b/test/load-test.js new file mode 100644 index 0000000000..0d9f0aaad3 --- /dev/null +++ b/test/load-test.js @@ -0,0 +1,48 @@ +import http from "k6/http"; +import { check, sleep } from "k6"; + +export const options = { + stages: [ + { duration: "10s", target: 5 }, + { duration: "20s", target: 10 }, + { duration: "10s", target: 0 }, + ], + thresholds: { + http_req_duration: ["p(95)<500"], + http_req_failed: ["rate<0.1"], + }, +}; + +const BASE_URL = "http://127.0.0.1:4567"; + +export default function () { + const homeRes = http.get(`${BASE_URL}/`); + check(homeRes, { + "homepage returns 200": (r) => r.status === 200, + "homepage loads within 500ms": (r) => r.timings.duration < 500, + }); + + sleep(1); + + const categoriesRes = http.get(`${BASE_URL}/api/categories`); + check(categoriesRes, { + "categories API returns 200": (r) => r.status === 200, + "categories API loads within 500ms": (r) => r.timings.duration < 500, + }); + + sleep(1); + + const generalRes = http.get(`${BASE_URL}/category/2/general-discussion`); + check(generalRes, { + "General Discussion returns 200": (r) => r.status === 200, + }); + + sleep(1); + + const announcementsRes = http.get(`${BASE_URL}/category/1/announcements`); + check(announcementsRes, { + "Announcements returns 200": (r) => r.status === 200, + }); + + sleep(1); +}