diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14371b23..daaee95b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,10 +45,18 @@ jobs: - name: Build And Lint run: | yarn build:ui:gulp - parallel --tty -j+0 yarn ::: lint:ci lint:ui:ci build build:ui:production:webpack + parallel --tty -j+0 yarn ::: lint:ci lint:ui:ci build build:ui:production:webpack test - name: Publish if: ${{ github.event_name == 'push' }} run: node build/publish.js + - name: Benchmark + run: yarn benchmark + - name: Benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: Benchmark + tool: customBiggerIsBetter + output-file-path: benchmark.json # web: # needs: build # permissions: diff --git a/.gitignore b/.gitignore index 490a99eb..56fb2974 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ *.tsbuildinfo *.hydro .coverage +benchmark.json globalConfig.json *.out .clinic/ diff --git a/package.json b/package.json index 0dda284c..e22b91e1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "build:ui:dev": "yarn build:ui:gulp && node --trace-deprecation packages/ui-default/build --dev", "build:ui:production": "yarn build:ui:gulp && node packages/ui-default/build --production", "build:ui:production:webpack": "node packages/ui-default/build --production", + "test": "mocha", + "benchmark": "cross-env BENCHMARK=true mocha", "lint": "eslint packages --ext ts --fix", "lint:ci": "eslint packages --ext ts", "lint:ui": "yarn workspace @hydrooj/ui-default eslint --ext .js,.ts,.jsx,.tsx . --fix", @@ -28,30 +30,37 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "devDependencies": { + "@types/autocannon": "^7.6.1", "@types/cross-spawn": "^6.0.2", - "@types/node": "^17.0.21", + "@types/node": "^17.0.23", "@types/semver": "^7.3.9", + "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "5.14.0", "@typescript-eslint/parser": "5.14.0", + "autocannon": "^7.8.1", "cac": "^6.7.12", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "esbuild": "0.14.3", - "eslint": "^8.11.0", + "eslint": "^8.12.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "16.1.1", "eslint-import-resolver-typescript": "2.5.0", "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-import": "2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.29.3", + "eslint-plugin-react": "^7.29.4", "eslint-plugin-simple-import-sort": "7.0.0", "fs-extra": "^10.0.1", "globby": "11.1.0", "latest-version": "^6.0.0", + "mocha": "^9.2.2", + "mongo-mock": "^4.1.0", "mongodb": "^3.7.3", + "mongodb-memory-server": "^8.4.2", "ora": "5.4.1", "semver": "^7.3.5", + "supertest": "^6.2.2", "typedoc": "^0.22.13", "typescript": "4.6.2" } diff --git a/packages/center/package.json b/packages/center/package.json index 21ad7af1..f259298d 100644 --- a/packages/center/package.json +++ b/packages/center/package.json @@ -8,7 +8,7 @@ "preferUnplugged": true, "dependencies": { "js-yaml": "^4.1.0", - "superagent": "^7.1.1" + "superagent": "^7.1.2" }, "devDependencies": { "@types/js-yaml": "^4.0.5" diff --git a/packages/geoip/package.json b/packages/geoip/package.json index 5b1503a3..8b8018d5 100644 --- a/packages/geoip/package.json +++ b/packages/geoip/package.json @@ -6,7 +6,7 @@ "author": "undefined ", "license": "AGPL-3.0-or-later", "dependencies": { - "maxmind": "^4.3.5" + "maxmind": "^4.3.6" }, "preferUnplugged": true, "scripts": { diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index ecd00b0a..6a40e07e 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -23,7 +23,7 @@ "devDependencies": { "@types/fs-extra": "^9.0.13", "@types/js-yaml": "^4.0.5", - "@types/lodash": "^4.14.179", + "@types/lodash": "^4.14.181", "@types/shell-quote": "^1.7.1", "@types/ws": "^8.5.3" }, diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index ea604b27..72198e3d 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -12,20 +12,20 @@ }, "preferUnplugged": true, "dependencies": { - "@graphql-tools/schema": "^8.3.2", + "@graphql-tools/schema": "^8.3.6", "@hydrooj/utils": "workspace:*", "adm-zip": "0.5.5", - "ajv": "^8.10.0", + "ajv": "^8.11.0", "ansi_up": "^5.1.0", "cac": "^6.7.12", "cookies": "^0.8.0", "detect-browser": "^5.3.0", - "emoji-regex": "^10.0.1", + "emoji-regex": "^10.1.0", "emojis-list": "2.1.0", "esbuild": "0.14.3", "fs-extra": "^10.0.1", "graphql": "^16.3.0", - "graphql-scalars": "1.15.0", + "graphql-scalars": "1.17.0", "js-yaml": "^4.1.0", "koa": "^2.13.4", "koa-body": "^4.2.0", @@ -34,40 +34,39 @@ "koa-router": "^10.1.1", "koa-static-cache": "^5.1.4", "lodash": "^4.17.21", - "lru-cache": "7.4.4", - "mime-types": "^2.1.34", + "lru-cache": "7.7.3", + "mime-types": "^2.1.35", "minio": "7.0.25", "moment-timezone": "^0.5.34", "mongodb": "^3.7.3", - "nanoid": "^3.3.1", - "nodemailer": "^6.7.2", + "nanoid": "^3.3.2", + "nodemailer": "^6.7.3", "notp": "^2.0.3", "p-queue": "^7.2.0", - "reflect-metadata": "^0.1.13", "require-resolve-hook": "^1.1.0", "semver": "^7.3.5", "serialize-javascript": "^6.0.0", "sockjs": "^0.3.24", "source-map-support": "^0.5.21", - "superagent": "^7.1.1", + "superagent": "^7.1.2", "thirty-two": "^1.0.2", "tx2": "^1.0.5" }, "devDependencies": { - "@types/adm-zip": "^0.4.34", + "@types/adm-zip": "^0.5.0", "@types/fs-extra": "^9.0.13", "@types/js-yaml": "^4.0.5", "@types/koa": "^2.13.4", "@types/koa-compress": "^4.0.3", "@types/koa-router": "^7.4.4", "@types/koa-static-cache": "^5.1.1", - "@types/lodash": "^4.14.179", - "@types/lru-cache": "^7.4.0", + "@types/lodash": "^4.14.181", + "@types/lru-cache": "^7.6.1", "@types/mime-types": "^2.1.1", "@types/minio": "^7.0.12", "@types/mongodb": "^3.6.20", "@types/nodemailer": "^6.4.4", - "@types/notp": "^2.0.1", + "@types/notp": "^2.0.2", "@types/semver": "^7.3.9", "@types/serialize-javascript": "^5.0.2", "@types/sockjs": "^0.3.33", diff --git a/packages/hydrooj/src/handler/api.ts b/packages/hydrooj/src/handler/api.ts index a0d17563..26b08c49 100644 --- a/packages/hydrooj/src/handler/api.ts +++ b/packages/hydrooj/src/handler/api.ts @@ -39,7 +39,7 @@ export function registerResolver( registerValue(typeName, key, value, description); const wrappedFunc = async (arg, ctx, info) => { const res = await func(arg, ctx, info); - if (typeof res !== 'object') return res; + if (typeof res !== 'object' || res === null) return res; const node = value.includes('!') ? value.split('!')[0] : value; if (handlers[node]) Object.assign(res, handlers[node]); ctx.parent = res; diff --git a/packages/hydrooj/src/loader.ts b/packages/hydrooj/src/loader.ts index 45bf71d6..4ed62b7a 100644 --- a/packages/hydrooj/src/loader.ts +++ b/packages/hydrooj/src/loader.ts @@ -1,7 +1,6 @@ /* eslint-disable simple-import-sort/imports */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-eval */ -import 'reflect-metadata'; import './init'; import './interface'; import path from 'path'; diff --git a/packages/hydrooj/src/model/task.ts b/packages/hydrooj/src/model/task.ts index f90fb183..03069d6d 100644 --- a/packages/hydrooj/src/model/task.ts +++ b/packages/hydrooj/src/model/task.ts @@ -12,6 +12,7 @@ const coll = db.collection('task'); const collEvent = db.collection('event'); async function getFirst(query: FilterQuery) { + if (process.env.CI) return null; const q = { ...query }; q.executeAfter = q.executeAfter || { $lt: new Date() }; const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } }); diff --git a/packages/hydrooj/src/service/db.ts b/packages/hydrooj/src/service/db.ts index cc5a72d2..2480d980 100644 --- a/packages/hydrooj/src/service/db.ts +++ b/packages/hydrooj/src/service/db.ts @@ -34,8 +34,13 @@ class MongoService implements BaseService { } async start(opts: MongoConfig) { + let mongourl = MongoService.buildUrl(opts); + if (process.env.CI) { + const { MongoMemoryServer } = require('mongodb-memory-server'); + const mongod = await MongoMemoryServer.create(); + mongourl = mongod.getUri(); + } this.opts = opts; - const mongourl = MongoService.buildUrl(opts); this.client = await MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); this.db = this.client.db(opts.name); await bus.parallel('database/connect', this.db); diff --git a/packages/login-with-github/package.json b/packages/login-with-github/package.json index 342bae52..334bd6b5 100644 --- a/packages/login-with-github/package.json +++ b/packages/login-with-github/package.json @@ -13,6 +13,6 @@ "hydrooj": "*" }, "dependencies": { - "superagent": "^7.1.1" + "superagent": "^7.1.2" } } diff --git a/packages/login-with-google/package.json b/packages/login-with-google/package.json index e9678fa5..e951bdd1 100644 --- a/packages/login-with-google/package.json +++ b/packages/login-with-google/package.json @@ -13,6 +13,6 @@ "hydrooj": "*" }, "dependencies": { - "superagent": "^7.1.1" + "superagent": "^7.1.2" } } diff --git a/packages/recaptcha/package.json b/packages/recaptcha/package.json index 7d6a30b3..aae34fa8 100644 --- a/packages/recaptcha/package.json +++ b/packages/recaptcha/package.json @@ -8,6 +8,6 @@ "license": "AGPL-3.0-or-later", "preferUnplugged": true, "dependencies": { - "superagent": "^7.1.1" + "superagent": "^7.1.2" } } diff --git a/packages/sonic/package.json b/packages/sonic/package.json index a6fb1c2a..cd7ea2ed 100644 --- a/packages/sonic/package.json +++ b/packages/sonic/package.json @@ -9,6 +9,6 @@ "author": "undefined ", "license": "AGPL-3.0-or-later", "dependencies": { - "sonic-channel": "^1.2.6" + "sonic-channel": "^1.2.7" } } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index a7633c07..fa357ef5 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -10,20 +10,20 @@ "lint": "eslint ." }, "devDependencies": { - "@blueprintjs/core": "^3.53.0", - "@fontsource/dm-mono": "^4.5.4", - "@fontsource/fira-code": "^4.5.5", - "@fontsource/inconsolata": "^4.5.2", - "@fontsource/jetbrains-mono": "^4.5.3", - "@fontsource/pt-mono": "^4.5.3", - "@fontsource/roboto-mono": "^4.5.3", - "@fontsource/source-code-pro": "^4.5.4", - "@fontsource/ubuntu-mono": "^4.5.4", + "@blueprintjs/core": "^3.54.0", + "@fontsource/dm-mono": "^4.5.7", + "@fontsource/fira-code": "^4.5.8", + "@fontsource/inconsolata": "^4.5.4", + "@fontsource/jetbrains-mono": "^4.5.5", + "@fontsource/pt-mono": "^4.5.5", + "@fontsource/roboto-mono": "^4.5.5", + "@fontsource/source-code-pro": "^4.5.6", + "@fontsource/ubuntu-mono": "^4.5.6", "@hydrooj/utils": "workspace:*", "@types/gulp-if": "^0.0.34", "@types/jquery": "^3.5.14", - "@types/json-schema": "^7.0.9", - "@types/katex": "^0.11.1", + "@types/json-schema": "^7.0.11", + "@types/katex": "^0.14.0", "@types/qrcode": "^1.4.2", "@types/redux-logger": "^3.0.9", "@types/sockjs-client": "^1.5.1", @@ -39,15 +39,15 @@ "copy-webpack-plugin": "^6.4.1", "css-loader": "^4.3.0", "diff-dom": "^4.2.3", - "echarts": "^5.3.1", + "echarts": "^5.3.2", "emojis-keywords": "2.0.0", "emojis-list": "2.1.0", "esbuild-loader": "^2.18.0", - "eslint": "^8.11.0", + "eslint": "^8.12.0", "fancy-log": "^2.0.0", "file-loader": "^6.2.0", "friendly-errors-webpack-plugin": "^1.7.0", - "graphiql": "^1.7.1", + "graphiql": "^1.8.3", "gulp": "^4.0.2", "gulp-cli": "^2.3.0", "gulp-iconfont": "^11.0.1", @@ -64,7 +64,7 @@ "monaco-editor-nls": "^2.0.0", "monaco-editor-webpack-plugin": "^7.0.1", "monaco-themes": "^0.4.0", - "nanoid": "^3.3.1", + "nanoid": "^3.3.2", "normalize.css": "^8.0.1", "nprogress": "^0.2.0", "pickadate": "^3.6.4", @@ -78,7 +78,7 @@ "raw-loader": "^4.0.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-redux": "^7.2.6", + "react-redux": "^7.2.8", "react-split-pane": "^0.1.92", "reconnecting-websocket": "^4.4.0", "redux": "^4.1.2", @@ -91,7 +91,7 @@ "speed-measure-webpack-plugin": "^1.5.0", "sticky-kit": "^1.1.3", "style-loader": "^2.0.0", - "stylus": "^0.56.0", + "stylus": "^0.57.0", "stylus-loader": "^3.0.2", "tether": "^1.4.7", "tether-drop": "^1.4.2", @@ -99,7 +99,7 @@ "through2": "^4.0.2", "timeago-react": "^3.0.4", "timeago.js": "^4.0.2", - "vditor": "^3.8.12", + "vditor": "^3.8.13", "vinyl-buffer": "^1.0.1", "web-streams-polyfill": "^3.2.0", "webpack": "^4.46.0", @@ -112,7 +112,7 @@ "fs-extra": "^10.0.1", "js-yaml": "^4.1.0", "jsesc": "^3.0.2", - "katex": "^0.15.2", + "katex": "^0.15.3", "lodash": "^4.17.21", "markdown-it": "^12.3.2", "markdown-it-anchor": "^8.4.1", @@ -124,7 +124,7 @@ "mongodb": "^3.7.3", "nunjucks": "^3.2.3", "p-queue": "^7.2.0", - "react-query": "^3.34.16", + "react-query": "^3.34.19", "streamsaver": "^2.0.6", "xss": "^1.0.11" } diff --git a/packages/utils/package.json b/packages/utils/package.json index 53354dd3..f616b9f5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -12,7 +12,7 @@ "js-yaml": "^4.1.0", "moment": "^2.29.1", "mongodb": "^3.7.3", - "systeminformation": "^5.11.8" + "systeminformation": "^5.11.9" }, "devDependencies": { "@types/fs-extra": "^9.0.13", diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index 889eb82b..654118a1 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -12,16 +12,16 @@ "chrome-finder": "^1.0.7", "jsdom": "^19.0.0", "lodash": "^4.17.21", - "puppeteer-core": "^13.5.1", + "puppeteer-core": "^13.5.2", "puppeteer-extra": "^3.2.3", "puppeteer-extra-plugin-portal": "^3.0.1", "puppeteer-extra-plugin-stealth": "^2.9.0", - "superagent": "^7.1.1", + "superagent": "^7.1.2", "superagent-proxy": "^3.0.0" }, "devDependencies": { "@types/jsdom": "^16.2.14", - "@types/lodash": "^4.14.179", + "@types/lodash": "^4.14.181", "@types/superagent": "^4.1.15", "@types/superagent-proxy": "^3.0.0" } diff --git a/test/entry.js b/test/entry.js new file mode 100644 index 00000000..fa586802 --- /dev/null +++ b/test/entry.js @@ -0,0 +1,3 @@ +process.env.CI = true; +require('hydrooj/bin/hydrooj'); +require('./main'); diff --git a/test/main.ts b/test/main.ts new file mode 100644 index 00000000..2d5210cf --- /dev/null +++ b/test/main.ts @@ -0,0 +1,78 @@ +import assert from 'assert'; +import autocannon from 'autocannon'; +import { writeFileSync } from 'fs-extra'; +import * as supertest from 'supertest'; +import * as bus from 'hydrooj/src/service/bus'; + +const Root = { + username: 'root', + password: '123456', + creditionals: null, +}; + +describe('App', () => { + let agent: supertest.SuperAgentTest; + before('init', function init(done) { + this.timeout(30000); + bus.on('app/started', () => setTimeout(() => { + agent = supertest.agent(require('hydrooj/src/service/server').server); + done(); + }, 2000)); + }); + + const routes = ['/', '/api', '/p', '/contest', '/homework', '/user/1', '/training']; + routes.forEach((route) => it(`GET ${route}`, () => agent.get(route).expect(200))); + + it('API user', async () => { + await agent.get('/api?{user(id:1){uname}}').expect({ data: { user: { uname: 'Hydro' } } }); + await agent.get('/api?{user(id:2){uname}}').expect({ data: { user: null } }); + }); + + it('Create User', async () => { + const redirect = await agent.post('/register') + .send({ mail: 'test@example.com' }) + .expect(302) + .then((res) => res.headers.location); + await agent.post(redirect) + .send({ uname: Root.username, password: Root.password, verifyPassword: Root.password }) + .expect(302); + }); + + it('Login', async () => { + const cookie = await agent.post('/login') + .send({ uname: Root.username, password: Root.password }) + .expect(302) + .then((res) => res.headers['set-cookie']); + Root.creditionals = cookie; + }); + + it('API registered user', async () => { + await agent.get('/api?{user(id:2){uname}}').expect({ data: { user: { uname: 'root' } } }); + }); + + // TODO add more tests + + const results: Record = {}; + if (process.env.BENCHMARK) { + routes.forEach((route) => it(`Performance test ${route}`, async function test() { + this.timeout(60000); + await global.Hydro.model.system.set('limit.global', 99999); + const result = await autocannon({ url: `http://localhost:8888${route}` }); + assert(result.errors === 0, `test ${route} returns errors`); + results[route] = result; + })); + } + + after(() => { + const metrics = []; + for (const key in results) { + metrics.push({ + name: `Benchmark - ${key} - Req/sec`, + unit: 'Req/sec', + value: results[key].requests.average, + }); + } + writeFileSync('./benchmark.json', JSON.stringify(metrics, null, 2)); + setTimeout(() => process.exit(0), 1000); + }); +});