Initialize unit testing and code coverage

* add: vitest configs

* added vitest scripts to package

* test coverage of src code

* initial unit testing

* added new testing workflows

* comments added, overlapping tests removed

* decouple env, tests

---------

Co-authored-by: Kevin Dang <kevinthedang_1@outlook.com>
This commit is contained in:
Jonathan Smoley
2024-06-05 08:50:56 -07:00
committed by GitHub
parent 496ce43939
commit 9f77c5287f
12 changed files with 2196 additions and 85 deletions

View File

@@ -1,82 +0,0 @@
name: Builds
run-name: Validate Node and Docker Builds
on:
push:
branches:
- master
jobs:
Discord-Node-Build: # test if the node install and run
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: 'npm'
- name: Install Project Dependencies
run: |
npm install
- name: Build Application
run: |
npm run build
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
# set -e ensures if nohup fails, this section fails
- name: Startup Discord Bot Client
run: |
set -e
nohup npm run prod &
Discord-Ollama-Container-Build: # test docker build and run
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: 'npm'
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images
run: |
npm run docker:start-cpu
- name: Check Images Exist
run: |
(docker images | grep -q 'discord/bot' && docker images | grep -qE 'ollama/ollama') || exit 1
- name: Check Containers Exist
run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1

81
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Builds
run-name: Validate Node and Docker Builds
on:
push:
branches:
- master
jobs:
Discord-Node-Build: # test if the node install and run
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: "npm"
- name: Install Project Dependencies
run: |
npm install
- name: Build Application
run: |
npm run build
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
# set -e ensures if nohup fails, this section fails
- name: Startup Discord Bot Client
run: |
set -e
nohup npm run prod &
Discord-Ollama-Container-Build: # test docker build and run
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: "npm"
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images
run: |
npm run docker:start-cpu
- name: Check Images Exist
run: |
(docker images | grep -q 'discord/bot' && docker images | grep -qE 'ollama/ollama') || exit 1
- name: Check Containers Exist
run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1

71
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Tests
run-name: Test source code for errors
on:
push:
branches:
- unit-testing
jobs:
Discord-Node-Test:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: "npm"
- name: Install Project Dependencies
run: |
npm install
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Test Application
run: |
npm run test:run
Discord-Ollama-Container-Test:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment v18.18.2
uses: actions/setup-node@v4
with:
node-version: 18.18.2
cache: "npm"
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
echo CHANNEL_ID = ${{ secrets.CHANNEL_ID }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images
run: |
npm run docker:start-cpu
- name: Test Docker Container
run: |
npm run docker:test

View File

@@ -4,7 +4,8 @@
<h3><a href="#"></a>Ollama as your Discord AI Assistant</h3>
<p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build-test.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build-test.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Testing Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a>
</div>
## About/Goals

1846
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@
"dev-tsx": "tsx watch src/index.ts",
"dev-mon": "nodemon --config nodemon.json src/index.ts",
"build": "tsc",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"prod": "node .",
"client": "npm run build && npm run prod",
"clean": "docker compose down && docker rmi $(docker images | grep $(node -p \"require('./package.json').version\") | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
@@ -17,6 +19,7 @@
"docker:clean": "docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
"docker:build": "docker build --no-cache -t discord/bot:$(node -p \"require('./package.json').version\") .",
"docker:test": "docker run -d --rm -v discord:/src/app --name test discord/bot:$(node -p \"require('./package.json').version\") npm run test:run",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 discord/bot:$(node -p \"require('./package.json').version\")",
"docker:ollama": "docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest"
@@ -30,10 +33,12 @@
},
"devDependencies": {
"@types/node": "^20.10.5",
"@vitest/coverage-v8": "^1.6.0",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"tsx": "^4.6.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.6.0"
},
"type": "module",
"engines": {

27
tests/commands.test.ts Normal file
View File

@@ -0,0 +1,27 @@
// describe marks a test suite
// expect takes a value from an expression
// it marks a test case
import { describe, expect, it } from 'vitest'
import commands from '../src/commands'
/**
* Commands test suite, tests the commands object
* Each command is to be tested elsewhere, this file
* is to ensure that the commands object is defined.
*
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#commands', () => {
// test definition of commands object
it('references defined object', () => {
// toBe compares the value to the expected value
expect(typeof commands).toBe('object')
})
// test specific commands in the object
it('references specific commands', () => {
const commandsString = commands.map(e => e.name).join(', ')
expect(commandsString).toBe('thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity')
})
})

23
tests/events.test.ts Normal file
View File

@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import events from '../src/events'
/**
* Events test suite, tests the events object
* Each event is to be tested elsewhere, this file
* is to ensure that the events object is defined.
*
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#events', () => {
// test definition of events object
it('references defined object', () => {
expect(typeof events).toBe('object')
})
// test specific events in the object
it('references specific events', () => {
const eventsString = events.map(e => e.key.toString()).join(', ')
expect(eventsString).toBe('ready, messageCreate, interactionCreate')
})
})

50
tests/getEnvVar.test.ts Normal file
View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { getEnvVar } from '../src/utils'
/**
* getEnvVar test suite, tests the getEnvVar function
*
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#getEnvVar', () => {
// dummy set of keys
const keys = {
clientToken: 'CLIENT_TOKEN',
}
// set keys in environment
process.env['clientToken'] = keys.clientToken
// test for non-empty string
it('returns a non-empty string', () => {
expect(getEnvVar('CLIENT_TOKEN')).not.toBe('')
})
// test for string type
it('returns a string', () => {
expect(typeof getEnvVar('CLIENT_TOKEN')).toBe('string')
})
// test for distinct key
it('returns a distinct key', () => {
expect(getEnvVar('CLIENT_TOKEN')).toEqual(process.env[keys.clientToken])
})
// test for fallback case
it('returns a fallback', () => {
expect(getEnvVar('NON_EXISTENT_KEY', 'fallback')).toBe('fallback')
})
// test that all keys are consistently found
it('returns all keys found', () => {
for (const key in keys) {
expect(getEnvVar(key)).toEqual(keys[key])
}
})
// test that an error is thrown if key is not found
it('throws an error if key is not found', () => {
expect(() => getEnvVar('NON_EXISTENT_KEY')).toThrowError()
})
})

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { clean } from '../src/utils/mentionClean'
import { getEnvVar } from '../src/utils'
/**
* MentionClean test suite, tests the clean function
*
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#clean', () => {
// test for id removal from message
it('removes the mention from a message', () => {
const message = `<@${getEnvVar('CLIENT_UID')}> Hello, World!`
expect(clean(message)).toBe('Hello, World!')
})
})

62
tests/queue.test.ts Normal file
View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import { Queue } from '../src/queues/queue'
/**
* Queue test suite, tests the Queue class
*
* @param name name of the test suite
* @param fn function holding tests to run
*/
describe('#queue', () => {
let queue= new Queue<string>()
// test for queue creation
it('creates a new queue', () => {
expect(queue).not.toBeNull()
})
// test for queue capacity creation
it('adds specific capacity to the queue', () => {
queue = new Queue<string>(2)
expect(queue).not.toBeNull()
})
// test for enqueue success, size update
it('adds items to the queue', () => {
queue.enqueue('hello')
expect(queue.size()).toBe(1)
})
// test for multiple enqueue success, size update
it('adds multiple items to the queue', () => {
queue.enqueue('world')
expect(queue.size()).toBe(2)
})
// test for enqueue failure upon capacity overflow
it('throws an error when the queue is full', () => {
expect(() => queue.enqueue('!')).toThrowError()
})
// test for getItems success
it('returns all items in the queue', () => {
expect(queue.getItems()).toEqual(['hello', 'world'])
})
// test for pop success, size update
it('removes an item from the front of the queue', () => {
queue.pop()
expect(queue.size() && queue.getItems()[0]).toBe(1 && 'hello')
})
// test for dequeue success, size update
it('removes an item from the back of the queue', () => {
queue.dequeue()
expect(queue.size() && queue.getItems()[0]).toBe(0 && 'world')
})
// test for getItems success with nothing in the list
it('returns an empty array when the queue is empty', () => {
expect(queue.getItems()).toEqual([])
})
})

12
vitest.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig, configDefaults } from 'vitest/config'
// config for vitest
export default defineConfig({
test: {
globals: true, // <-- reduces test file imports
coverage: {
exclude: [...configDefaults.exclude, 'build/*'], // <-- exclude JS build
reporter: ['text', 'html'] // <-- reports in text, html
}
}
})