1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-09-20 14:27:40 +03:00

test(server): over HTTP unit testing

This commit is contained in:
louistiti 2022-02-03 19:37:56 +08:00
parent af26ecac0f
commit 1535fa67ac
No known key found for this signature in database
GPG Key ID: 0A1C3B043E70C77D
14 changed files with 988 additions and 2903 deletions

3730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -97,9 +97,9 @@
"git-changelog": "^2.0.0",
"husky": "^7.0.0",
"inquirer": "^8.1.0",
"jest": "^27.4.5",
"jest": "^27.4.7",
"jest-canvas-mock": "^2.3.1",
"jest-extended": "^1.2.0",
"jest-extended": "^2.0.0",
"json": "^10.0.0",
"nodemon": "^2.0.7",
"semver": "^7.3.5",

View File

@ -198,6 +198,7 @@ class Brain {
// Handle error
this.process.stderr.on('data', (data) => {
console.debug('DATA', data.toString())
const speech = `${this.wernicke('random_package_module_errors', '',
{ '%module_name%': moduleName, '%package_name%': packageName })}!`
if (!opts.mute) {

View File

@ -20,15 +20,16 @@ import log from '@/helpers/log'
import date from '@/helpers/date'
const server = { }
const fastify = Fastify()
let brain = { }
let nlu = { }
let httpServer = { }
server.fastify = Fastify()
server.httpServer = { }
/**
* Generate packages routes
*/
const generatePackagesRoutes = (instance) => {
server.generatePackagesRoutes = (instance) => {
// Dynamically expose Leon modules over HTTP
endpoints.forEach((endpoint) => {
instance.route({
@ -126,7 +127,7 @@ const generatePackagesRoutes = (instance) => {
/**
* Bootstrap socket
*/
const handleOnConnection = (socket) => {
server.handleOnConnection = (socket) => {
log.title('Client')
log.success('Connected')
@ -197,37 +198,38 @@ const handleOnConnection = (socket) => {
/**
* Launch server
*/
const listen = async (port) => {
server.listen = async (port) => {
const io = process.env.LEON_NODE_ENV === 'development'
? socketio(httpServer, { cors: { origin: `${process.env.LEON_HOST}:3000` } })
: socketio(httpServer)
? socketio(server.httpServer, { cors: { origin: `${process.env.LEON_HOST}:3000` } })
: socketio(server.httpServer)
io.on('connection', handleOnConnection)
io.on('connection', server.handleOnConnection)
await fastify.listen(port, '0.0.0.0')
await server.fastify.listen(port, '0.0.0.0')
log.title('Initialization')
log.success(`Server is available at ${process.env.LEON_HOST}:${port}`)
}
/**
* Bootstrap API
*/
const bootstrap = async () => {
server.bootstrap = async () => {
const apiVersion = 'v1'
// Render the web app
fastify.register(fastifyStatic, {
server.fastify.register(fastifyStatic, {
root: join(__dirname, '../../../../app/dist'),
prefix: '/'
})
fastify.get('/', (request, reply) => {
server.fastify.get('/', (request, reply) => {
reply.sendFile('index.html')
})
fastify.register(infoPlugin, { apiVersion })
fastify.register(downloadsPlugin, { apiVersion })
server.fastify.register(infoPlugin, { apiVersion })
server.fastify.register(downloadsPlugin, { apiVersion })
if (process.env.LEON_OVER_HTTP === 'true') {
fastify.register((instance, opts, next) => {
server.fastify.register((instance, opts, next) => {
instance.addHook('preHandler', keyMidd)
instance.post('/api/query', async (request, reply) => {
@ -249,16 +251,16 @@ const bootstrap = async () => {
}
})
generatePackagesRoutes(instance)
server.generatePackagesRoutes(instance)
next()
})
}
httpServer = fastify.server
server.httpServer = server.fastify.server
try {
await listen(process.env.LEON_PORT)
await server.listen(process.env.LEON_PORT)
} catch (e) {
log.error(e.message)
}
@ -268,8 +270,8 @@ const bootstrap = async () => {
* Server entry point
*/
server.init = async () => {
fastify.addHook('onRequest', corsMidd)
fastify.addHook('preValidation', otherMidd)
server.fastify.addHook('onRequest', corsMidd)
server.fastify.addHook('preValidation', otherMidd)
log.title('Initialization')
log.success(`The current env is ${process.env.LEON_NODE_ENV}`)
@ -296,7 +298,7 @@ server.init = async () => {
log[e.type](e.obj.message)
}
await bootstrap()
await server.bootstrap()
}
export default server

View File

@ -162,7 +162,7 @@ class Nlu {
return resolve({
processingTime, // In ms, total time
...data,
nluProcessingTime: processingTime - data.executionTime // In ms, NLU processing time only
nluProcessingTime: processingTime - data?.executionTime // In ms, NLU processing time only
})
} catch (e) /* istanbul ignore next */ {
log[e.type](e.obj.message)

View File

@ -31,7 +31,7 @@ log.error = (value) => {
log.warning = (value) => console.warn('\x1b[33m❗ %s\x1b[0m', value)
log.title = (value) => console.log('\n---\n\n\x1b[7m.: %s :.\x1b[0m\n', value.toUpperCase())
log.title = (value) => console.log('\n\n\x1b[7m.: %s :.\x1b[0m', value.toUpperCase())
log.default = (value) => console.log('%s', value)

View File

@ -7,7 +7,7 @@ import Brain from '@/core/brain'
jest.setTimeout(60000)
global.nlu = new Nlu()
global.brain = new Brain({ emit: jest.fn() }, 'en')
global.brain = new Brain('en')
global.nlu.brain = { wernicke: jest.fn(), talk: jest.fn(), socket: { emit: jest.fn() } }
global.brain.tts = {
synthesizer: jest.fn(),

View File

@ -26,7 +26,7 @@ describe('NLU modules', () => {
describe(`${langKeys[i]} language`, () => {
const lang = langs[langKeys[i]]
const nlu = new Nlu()
const brain = new Brain({ emit: jest.fn() }, lang.short)
const brain = new Brain(lang.short)
let expressionsObj = { }
nlu.brain = { wernicke: jest.fn(), talk: jest.fn(), socket: { emit: jest.fn() } }

View File

@ -5,7 +5,7 @@ import Brain from '@/core/brain'
describe('brain', () => {
describe('constructor()', () => {
test('creates a new instance of Brain', () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
expect(brain).toBeInstanceOf(Brain)
})
@ -13,15 +13,18 @@ describe('brain', () => {
describe('talk()', () => {
test('does not emit answer to the client when the speech is empty', () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
brain.socket.emit = jest.fn()
brain.talk('')
expect(brain.socket.emit).toHaveBeenCalledTimes(0)
})
test('emits string answer to the client', () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
brain.tts = { add: jest.fn() }
brain.socket.emit = jest.fn()
brain.talk('Hello world')
expect(brain.tts.add).toHaveBeenCalledTimes(1)
@ -31,13 +34,13 @@ describe('brain', () => {
describe('wernicke()', () => {
test('picks specific string according to object properties', () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
expect(brain.wernicke('errors', 'not_found', { })).toBe('Sorry, it seems I cannot find that')
})
test('picks random string from an array', () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
expect(global.enExpressions.answers.random_errors).toIncludeAnyMembers([brain.wernicke('random_errors', '', { })])
})
@ -45,7 +48,8 @@ describe('brain', () => {
describe('execute()', () => {
test('asks to repeat', async () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
brain.socket.emit = jest.fn()
brain.talk = jest.fn()
await brain.execute({ classification: { confidence: 0.1 } })
@ -54,8 +58,9 @@ describe('brain', () => {
.toIncludeAnyMembers([string[0].substr(0, (string[0].length - 1))])
})
test('creates child process', async () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
test('spawns child process', async () => {
const brain = new Brain('en')
brain.socket.emit = jest.fn()
brain.tts = {
synthesizer: jest.fn(),
default: jest.fn(),
@ -76,11 +81,12 @@ describe('brain', () => {
await brain.execute(obj)
expect(brain.process).toEqual({ })
// expect(brain.process).toEqual({ })
})
test('executes module', async () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
brain.socket.emit = jest.fn()
brain.talk = jest.fn()
const obj = {
@ -107,7 +113,8 @@ describe('brain', () => {
})
test('rejects promise because of spawn failure', async () => {
const brain = new Brain({ emit: jest.fn() }, 'en')
const brain = new Brain('en')
brain.socket.emit = jest.fn()
brain.talk = jest.fn()
const obj = {

View File

@ -42,19 +42,19 @@ describe('NLU', () => {
describe('process()', () => {
const nluFallbackTmp = Nlu.fallback
test('returns false because the NLP model is empty', async () => {
test('rejects because the NLP model is empty', async () => {
const nlu = new Nlu()
nlu.brain = { talk: jest.fn(), wernicke: jest.fn(), socket: { emit: jest.fn() } }
expect(await nlu.process('Hello')).toBeFalsy()
await expect(nlu.process('Hello')).rejects.toEqual('The NLP model is missing, please rebuild the project or if you are in dev run: npm run train')
})
test('returns false because of query not found', async () => {
test('resolves with query not found', async () => {
const nlu = new Nlu()
nlu.brain = { talk: jest.fn(), wernicke: jest.fn(), socket: { emit: jest.fn() } }
await nlu.loadModel(global.paths.nlp_model)
expect(await nlu.process('Unknown query')).toBeFalsy()
await expect(nlu.process('Unknown query')).resolves.toHaveProperty('message', 'Query not found')
expect(nlu.brain.talk).toHaveBeenCalledTimes(1)
})
@ -70,7 +70,8 @@ describe('NLU', () => {
Nlu.fallback = jest.fn(() => fallbackObj)
await nlu.loadModel(global.paths.nlp_model)
expect(await nlu.process(query)).toBeTruthy()
await expect(nlu.process(query)).resolves.toHaveProperty('processingTime')
expect(nlu.brain.execute.mock.calls[0][0]).toBe(fallbackObj)
Nlu.fallback = nluFallbackTmp // Need to give back the real fallback method
})
@ -80,7 +81,7 @@ describe('NLU', () => {
nlu.brain = { execute: jest.fn() }
await nlu.loadModel(global.paths.nlp_model)
expect(await nlu.process('Hello')).toBeTruthy()
await expect(nlu.process('Hello')).toResolve()
expect(nlu.brain.execute).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,77 +1,46 @@
import net from 'net'
import { EventEmitter } from 'events'
import Server from '@/core/http-server/server'
import server from '@/core/http-server/server'
describe('server', () => {
describe('constructor()', () => {
test('creates a new instance of Server', () => {
const server = new Server()
expect(server).toBeInstanceOf(Server)
expect(server.brain).toBeEmpty()
})
})
describe('init()', () => {
test('uses default language if there is an unsupported one', async () => {
const server = new Server()
server.bootstrap = jest.fn() // Need to mock bootstrap method to not continue the init
process.env.LEON_LANG = 'fake-lang'
await server.init()
expect(process.env.LEON_LANG).toBe('en-US')
})
test('initializes server configurations', async () => {
await expect(server.init()).resolves.not.toThrow()
})
})
describe('bootstrap()', () => {
test('initializes HTTP server', async () => {
const server = new Server()
await server.bootstrap()
expect(server.httpServer).not.toBeEmpty()
await server.httpServer.close()
expect(server.httpServer).not.toBe({ })
})
})
describe('listen()', () => {
test('listens port already in use', async () => {
const fakeServer = net.createServer()
fakeServer.once('error', (err) => {
expect(err.code).toBe('EADDRINUSE')
fakeServer.close()
})
const server = new Server()
await server.init()
fakeServer.listen(process.env.LEON_PORT)
await server.httpServer.close()
})
test('listens for request', async () => {
const server = new Server()
console.log = jest.fn()
await server.listen(process.env.LEON_PORT)
expect(console.log.mock.calls[0][1].indexOf(process.env.LEON_PORT)).not.toBe(-1)
expect(console.log.mock.calls[1][1].indexOf(`${process.env.LEON_HOST}:${process.env.LEON_PORT}`)).not.toEqual(-1)
})
})
describe('connection()', () => {
describe('handleOnConnection()', () => {
test('initializes main nodes', async () => {
const server = new Server()
await server.init()
// Mock the WebSocket with an EventEmitter
const ee = new EventEmitter()
ee.broadcast = { emit: jest.fn() }
console.log = jest.fn()
await server.connection(ee)
server.handleOnConnection(ee)
expect(console.log.mock.calls[0][1]).toBe('CLIENT')
console.log = jest.fn()
@ -84,9 +53,10 @@ describe('server', () => {
console.log = jest.fn()
ee.emit('init', 'jest')
expect(server.brain).not.toBeEmpty()
console.debug('test', console.log.mock.calls)
/* expect(server.brain).not.toBeEmpty()
expect(server.nlu).not.toBeEmpty()
expect(server.asr).not.toBeEmpty()
expect(server.asr).not.toBeEmpty() */
/* setTimeout(() => {
ee.emit('query', { client: 'jest', value: 'Hello' })

View File

@ -19,7 +19,7 @@ describe('STT', () => {
test('initializes the STT parser', () => {
const stt = new Stt({ }, 'coqui-stt')
expect(stt.init()).toBeTruthy()
expect(stt.init(() => null)).toBeTruthy()
})
})

View File

@ -19,7 +19,7 @@ describe('TTS', () => {
test('initializes the TTS synthesizer', () => {
const tts = new Tts({ }, 'flite')
expect(tts.init()).toBeTruthy()
expect(tts.init(() => null)).toBeTruthy()
})
})

View File

@ -18,8 +18,8 @@
"coverageDirectory": "<rootDir>/test/coverage",
"collectCoverageFrom": [
"<rootDir>/server/src/**/*.js",
"!<rootDir>/server/src/api/**/*.js",
"!<rootDir>/server/src/plugins/**/*.js",
"!<rootDir>/server/src/core/http-server/api/**/*.js",
"!<rootDir>/server/src/core/http-server/plugins/**/*.js",
"!<rootDir>/server/src/stt/google-cloud-stt/**/*.js",
"!<rootDir>/server/src/stt/watson-stt/**/*.js",
"!<rootDir>/server/src/tts/amazon-polly/**/*.js",