refacto & cleanup of toplevel functions, use @p42/p42Modules, test microframework
This commit is contained in:
@@ -19,7 +19,7 @@ export function positionAt(agent, t) {
|
||||
})
|
||||
}
|
||||
|
||||
function distanceBetween(agentA, agentB, t) {
|
||||
export function distanceBetween(agentA, agentB, t) {
|
||||
const a = positionAt(agentA, t)
|
||||
const b = positionAt(agentB, t)
|
||||
const dx = a.x - b.x
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AccesRights } from '../accesRights.js'
|
||||
import { createMysqlPool } from '../mysqlClient.js'
|
||||
import { MySQLClient } from '@p42/p42modules'
|
||||
import { SimRepository } from './simRepository.js'
|
||||
import { ArenaGroom } from './arenaGroom.js'
|
||||
import { MaestroState } from './orchestrationState.js'
|
||||
@@ -61,8 +61,8 @@ export class maestroServer {
|
||||
console.error('[Maestro] Missing mysql config')
|
||||
return(false)
|
||||
}
|
||||
this.db = await createMysqlPool(mysqlCfg)
|
||||
this.simRepo = new SimRepository(this.db, this.debug)
|
||||
this.db = await MySQLClient.createPool(mysqlCfg)
|
||||
this.simRepo = new SimRepository(this.db, MySQLClient.resolveDatabases(mysqlCfg), this.debug)
|
||||
if(this.debug) console.log('[Maestro] MySQL pool ready')
|
||||
return(true)
|
||||
}
|
||||
|
||||
+28
-22
@@ -1,4 +1,4 @@
|
||||
import { mysqlExecute } from '../mysqlClient.js'
|
||||
import { MySQLClient } from '@p42/p42modules'
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
@@ -6,7 +6,20 @@ export function isValidUuid(val) {
|
||||
return(typeof(val) === 'string' && UUID_RE.test(val))
|
||||
}
|
||||
|
||||
function parseGpsValues(raw) {
|
||||
export class SimRepository {
|
||||
|
||||
constructor(dbPool, databases, debug = false) {
|
||||
this.db = dbPool
|
||||
this.guiDb = databases.guiDatabase
|
||||
this.simDb = databases.simDatabase
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
#qualify(db, table) {
|
||||
return(`\`${db}\`.${table}`)
|
||||
}
|
||||
|
||||
#parseGpsValues(raw) {
|
||||
let v = raw
|
||||
if(v == null) return(null)
|
||||
if(typeof(v) === 'string') {
|
||||
@@ -25,13 +38,6 @@ function parseGpsValues(raw) {
|
||||
position: { x: position.x, y: position.y, z: position.z },
|
||||
vector: { x: speed.x, y: speed.y, z: speed.z },
|
||||
})
|
||||
}
|
||||
|
||||
export class SimRepository {
|
||||
|
||||
constructor(dbPool, debug = false) {
|
||||
this.db = dbPool
|
||||
this.debug = debug
|
||||
}
|
||||
|
||||
async validateSimulationAccess(userUuid, simulationUuid, keyframeId) {
|
||||
@@ -39,13 +45,13 @@ export class SimRepository {
|
||||
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
|
||||
if(!isValidUuid(keyframeId)) return({ ok: false, err: 'Invalid keyframe ID' })
|
||||
|
||||
const rows = await mysqlExecute(this.db, `
|
||||
const rows = await MySQLClient.poolExecute(this.db, `
|
||||
SELECT s.sim_id,
|
||||
BIN_TO_UUID(s.sim_uuid) AS sim_uuid,
|
||||
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid
|
||||
FROM p42SIM.simulations s
|
||||
INNER JOIN p42GUI.simowners o ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN p42GUI.users u ON o.own_usr_id = u.usr_id
|
||||
FROM ${this.#qualify(this.simDb, 'simulations')} s
|
||||
INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id
|
||||
WHERE u.usr_uuid = ?
|
||||
AND s.sim_uuid = UUID_TO_BIN(?)
|
||||
`, [userUuid, simulationUuid])
|
||||
@@ -57,9 +63,9 @@ export class SimRepository {
|
||||
return({ ok: false, err: 'Keyframe does not match simulation root keyframe' })
|
||||
}
|
||||
|
||||
const kfRows = await mysqlExecute(this.db, `
|
||||
const kfRows = await MySQLClient.poolExecute(this.db, `
|
||||
SELECT ekf_uuid
|
||||
FROM p42SIM.edited_keyframes
|
||||
FROM ${this.#qualify(this.simDb, 'edited_keyframes')}
|
||||
WHERE ekf_uuid = UUID_TO_BIN(?)
|
||||
`, [keyframeId])
|
||||
if(!kfRows.length) return({ ok: false, err: 'Keyframe not found' })
|
||||
@@ -71,13 +77,13 @@ export class SimRepository {
|
||||
if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' })
|
||||
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
|
||||
|
||||
const rows = await mysqlExecute(this.db, `
|
||||
const rows = await MySQLClient.poolExecute(this.db, `
|
||||
SELECT s.sim_id,
|
||||
BIN_TO_UUID(s.sim_uuid) AS sim_uuid,
|
||||
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid
|
||||
FROM p42SIM.simulations s
|
||||
INNER JOIN p42GUI.simowners o ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN p42GUI.users u ON o.own_usr_id = u.usr_id
|
||||
FROM ${this.#qualify(this.simDb, 'simulations')} s
|
||||
INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id
|
||||
WHERE u.usr_uuid = ?
|
||||
AND s.sim_uuid = UUID_TO_BIN(?)
|
||||
`, [userUuid, simulationUuid])
|
||||
@@ -87,9 +93,9 @@ export class SimRepository {
|
||||
}
|
||||
|
||||
async loadKeyframeAgents(keyframeId) {
|
||||
const rows = await mysqlExecute(this.db, `
|
||||
const rows = await MySQLClient.poolExecute(this.db, `
|
||||
SELECT BIN_TO_UUID(ekfs_agent_id) AS agent_id, ekfs_gps_values
|
||||
FROM p42SIM.edited_kf_store
|
||||
FROM ${this.#qualify(this.simDb, 'edited_kf_store')}
|
||||
WHERE ekfs_ekf_uuid = UUID_TO_BIN(?)
|
||||
`, [keyframeId])
|
||||
|
||||
@@ -97,7 +103,7 @@ export class SimRepository {
|
||||
const errors = []
|
||||
|
||||
for(const row of rows) {
|
||||
const parsed = parseGpsValues(row.ekfs_gps_values)
|
||||
const parsed = this.#parseGpsValues(row.ekfs_gps_values)
|
||||
if(!parsed) {
|
||||
errors.push(`Invalid GPS values for agent ${row.agent_id}`)
|
||||
continue
|
||||
|
||||
+2
-1
@@ -63,7 +63,8 @@
|
||||
},
|
||||
"mysql": {
|
||||
"socketPath": "/var/run/mysqld/mysqld.sock",
|
||||
"database": "p42GUI"
|
||||
"guiDatabase": "p42GUI",
|
||||
"simDatabase": "p42SIM"
|
||||
},
|
||||
"observer": {
|
||||
"observerActionsChannel": "system:requests:observer",
|
||||
|
||||
+2
-1
@@ -148,7 +148,8 @@
|
||||
"socketPath": { "type": "string" },
|
||||
"host": { "type": "string" },
|
||||
"port": { "type": "integer" },
|
||||
"database": { "type": "string" },
|
||||
"guiDatabase": { "type": "string" },
|
||||
"simDatabase": { "type": "string" },
|
||||
"connectionLimit": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"required": []
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import mysql from 'mysql2/promise'
|
||||
import { loadP42Secrets } from './secretsLoader.js'
|
||||
|
||||
export function resolveMysqlCredentials(config = {}) {
|
||||
loadP42Secrets()
|
||||
const user = process.env.mysql_user
|
||||
const password = process.env.mysql_pass
|
||||
if(!user || !password) {
|
||||
throw new Error('Missing MySQL credentials: set mysql_user and mysql_pass in environment')
|
||||
}
|
||||
return({
|
||||
socketPath: config.socketPath,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user,
|
||||
password,
|
||||
database: config.database ?? 'p42GUI',
|
||||
waitForConnections: true,
|
||||
connectionLimit: config.connectionLimit ?? 5,
|
||||
queueLimit: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createMysqlPool(config) {
|
||||
return(await mysql.createPool(resolveMysqlCredentials(config)))
|
||||
}
|
||||
|
||||
export async function mysqlExecute(pool, query, values = []) {
|
||||
const [rows] = await pool.execute(query, values)
|
||||
return(rows)
|
||||
}
|
||||
+1
-1
@@ -2,8 +2,8 @@
|
||||
"name": "p42GodDaemons",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@p42/p42modules": "^0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"mysql2": "^3.11.0",
|
||||
"redis": "^4.3.0",
|
||||
"yargs": "^17.7.2"
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import fs from 'fs'
|
||||
|
||||
const DEFAULT_SECRETS_PATH = '/etc/p42/secrets.env'
|
||||
|
||||
function stripQuotes(value) {
|
||||
if(
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
return(value.slice(1, -1))
|
||||
}
|
||||
return(value)
|
||||
}
|
||||
|
||||
export function loadP42Secrets(filePath = DEFAULT_SECRETS_PATH) {
|
||||
if(process.env.mysql_user && process.env.mysql_pass) return(true)
|
||||
|
||||
if(!fs.existsSync(filePath)) return(false)
|
||||
|
||||
const text = fs.readFileSync(filePath, 'utf8')
|
||||
for(const rawLine of text.split('\n')) {
|
||||
const line = rawLine.trim()
|
||||
if(!line || line.startsWith('#')) continue
|
||||
const eq = line.indexOf('=')
|
||||
if(eq < 1) continue
|
||||
const key = line.slice(0, eq).trim()
|
||||
const value = stripQuotes(line.slice(eq + 1).trim())
|
||||
if(key === 'mysql_user' || key === 'mysql_pass') {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return(Boolean(process.env.mysql_user && process.env.mysql_pass))
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { MySQLClient } from '@p42/p42modules'
|
||||
import { SimRepository } from '../../SimMaestro/simRepository.js'
|
||||
|
||||
function agentHashKey(template, agentId) {
|
||||
return(template.replace(/\[UID\]/g, agentId))
|
||||
}
|
||||
|
||||
function parseJsonField(raw) {
|
||||
if(raw == null) return(null)
|
||||
if(typeof(raw) === 'object') return(raw)
|
||||
try { return(JSON.parse(raw)) }
|
||||
catch { return(null) }
|
||||
}
|
||||
|
||||
function vectorsEqual(a, b) {
|
||||
for(const axis of ['x', 'y', 'z']) {
|
||||
if(a[axis] !== b[axis]) return(false)
|
||||
}
|
||||
return(true)
|
||||
}
|
||||
|
||||
async function findSimulationFixture(ctx) {
|
||||
const { guiDatabase, simDatabase } = ctx.databases
|
||||
const rows = await MySQLClient.poolExecute(ctx.db, `
|
||||
SELECT u.usr_uuid AS user_uuid,
|
||||
BIN_TO_UUID(s.sim_uuid) AS simulation_uuid,
|
||||
BIN_TO_UUID(s.sim_root_kf_uuid) AS keyframe_id
|
||||
FROM \`${guiDatabase}\`.users u
|
||||
INNER JOIN \`${guiDatabase}\`.simowners o ON o.own_usr_id = u.usr_id
|
||||
INNER JOIN \`${simDatabase}\`.simulations s ON o.own_sim_uuid = s.sim_uuid
|
||||
INNER JOIN \`${simDatabase}\`.edited_kf_store ekfs ON ekfs.ekfs_ekf_uuid = s.sim_root_kf_uuid
|
||||
GROUP BY u.usr_uuid, s.sim_uuid, s.sim_root_kf_uuid
|
||||
HAVING COUNT(ekfs.ekfs_agent_id) > 0
|
||||
LIMIT 1
|
||||
`)
|
||||
|
||||
if(!rows.length) {
|
||||
throw(new Error(
|
||||
'No simulation fixture found in MySQL (need user-owned sim with root keyframe agents). '
|
||||
+ 'Pass --userUuid, --simulationUuid, --keyframeId explicitly, or use --guiDatabase / --simDatabase for a test DB.'
|
||||
))
|
||||
}
|
||||
|
||||
return({
|
||||
userUuid: rows[0].user_uuid,
|
||||
simulationUuid: rows[0].simulation_uuid,
|
||||
keyframeId: rows[0].keyframe_id,
|
||||
})
|
||||
}
|
||||
|
||||
function waitForLifecycleEvent(ctx, eventType, timeoutMs) {
|
||||
return(new Promise((resolve, reject) => {
|
||||
const lifecyclePattern = ctx.config.maestro.lifecycle.arenaChannel
|
||||
const timer = setTimeout(() => {
|
||||
ctx.offArenaMessage(handler)
|
||||
reject(new Error(`Timeout waiting for ${eventType} on ${lifecyclePattern} (${timeoutMs}ms)`))
|
||||
}, timeoutMs)
|
||||
|
||||
const handler = (msg, chan) => {
|
||||
if(!ctx.arenaCnx.matchesChan(chan, lifecyclePattern)) return
|
||||
if(msg.eventType !== eventType) return
|
||||
clearTimeout(timer)
|
||||
ctx.offArenaMessage(handler)
|
||||
resolve(msg)
|
||||
}
|
||||
|
||||
ctx.onArenaMessage(handler)
|
||||
}))
|
||||
}
|
||||
|
||||
export function configureYargs(yargsBuilder) {
|
||||
return(yargsBuilder.options({
|
||||
userUuid: {
|
||||
describe: 'User UUID to send STARTSIMULATION as (auto-discovered if omitted)',
|
||||
type: 'string',
|
||||
},
|
||||
simulationUuid: {
|
||||
describe: 'Simulation UUID (auto-discovered if omitted)',
|
||||
type: 'string',
|
||||
},
|
||||
keyframeId: {
|
||||
describe: 'Root keyframe UUID (auto-discovered if omitted)',
|
||||
type: 'string',
|
||||
},
|
||||
timeout: {
|
||||
describe: 'Milliseconds to wait for onYourMarks',
|
||||
default: 15000,
|
||||
type: 'number',
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export async function run(ctx) {
|
||||
const { log, argv, config, systemCnx, arenaCnx } = ctx
|
||||
const arenaStorage = config.gps?.arenaStorage ?? {
|
||||
agentHashKey: 'arena:agents:[UID]',
|
||||
agentsIndexKey: 'arena:agents',
|
||||
}
|
||||
|
||||
log('action', 'Resolving simulation fixture from MySQL...')
|
||||
let userUuid = argv.userUuid
|
||||
let simulationUuid = argv.simulationUuid
|
||||
let keyframeId = argv.keyframeId
|
||||
|
||||
if(!userUuid || !simulationUuid || !keyframeId) {
|
||||
const fixture = await findSimulationFixture(ctx)
|
||||
userUuid = userUuid ?? fixture.userUuid
|
||||
simulationUuid = simulationUuid ?? fixture.simulationUuid
|
||||
keyframeId = keyframeId ?? fixture.keyframeId
|
||||
}
|
||||
|
||||
log('action', `User: ${userUuid}`)
|
||||
log('action', `Simulation: ${simulationUuid}`)
|
||||
log('action', `Keyframe: ${keyframeId}`)
|
||||
|
||||
const simRepo = new SimRepository(ctx.db, ctx.databases, false)
|
||||
const access = await simRepo.validateSimulationAccess(userUuid, simulationUuid, keyframeId)
|
||||
if(!access.ok) throw(new Error(`Simulation access check failed: ${access.err}`))
|
||||
|
||||
const agentsResult = await simRepo.loadKeyframeAgents(keyframeId)
|
||||
if(!agentsResult.ok) throw(new Error(`Failed to load keyframe agents: ${agentsResult.err}`))
|
||||
if(!agentsResult.agents.length) throw(new Error('Keyframe has no agents with valid GPS values'))
|
||||
|
||||
log('success', `Loaded ${agentsResult.agents.length} agent(s) from MySQL`)
|
||||
|
||||
const expectedById = new Map(agentsResult.agents.map(a => [a.id, a]))
|
||||
const lifecycleWait = waitForLifecycleEvent(ctx, 'onYourMarks', argv.timeout)
|
||||
|
||||
const reqid = `maestro1-${Date.now()}`
|
||||
const actionsChan = config.maestro.maestroActionsChannel
|
||||
|
||||
log('action', `Publishing STARTSIMULATION on ${actionsChan} (reqid=${reqid})...`)
|
||||
await systemCnx.redisPublish(actionsChan, {
|
||||
action: 'STARTSIMULATION',
|
||||
reqid,
|
||||
sender: userUuid,
|
||||
roles: ['*'],
|
||||
payload: {
|
||||
simulationUuid,
|
||||
keyframeId,
|
||||
infraId: null,
|
||||
},
|
||||
})
|
||||
|
||||
log('action', `Waiting for onYourMarks on ${config.maestro.lifecycle.arenaChannel}...`)
|
||||
const lifecycleMsg = await lifecycleWait
|
||||
|
||||
const payload = lifecycleMsg.payload ?? {}
|
||||
if(payload.simulationId !== simulationUuid) {
|
||||
throw(new Error(`onYourMarks simulationId mismatch: expected ${simulationUuid}, got ${payload.simulationId}`))
|
||||
}
|
||||
|
||||
log('success', `Received onYourMarks for simulationId=${payload.simulationId}`)
|
||||
|
||||
log('action', 'Reading arena store...')
|
||||
const indexIds = await arenaCnx.redisSmembers(arenaStorage.agentsIndexKey)
|
||||
const expectedIds = [...expectedById.keys()].sort()
|
||||
const actualIds = [...indexIds].sort()
|
||||
|
||||
if(actualIds.length !== expectedIds.length) {
|
||||
throw(new Error(`Agent index count mismatch: expected ${expectedIds.length}, got ${actualIds.length}`))
|
||||
}
|
||||
|
||||
for(let i = 0; i < expectedIds.length; i++) {
|
||||
if(actualIds[i] !== expectedIds[i]) {
|
||||
throw(new Error(`Agent index mismatch at ${i}: expected ${expectedIds[i]}, got ${actualIds[i]}`))
|
||||
}
|
||||
}
|
||||
|
||||
log('success', `Arena agents index contains ${actualIds.length} agent(s)`)
|
||||
|
||||
for(const agentId of expectedIds) {
|
||||
const key = agentHashKey(arenaStorage.agentHashKey, agentId)
|
||||
const hash = await arenaCnx.redisHgetall(key)
|
||||
const expected = expectedById.get(agentId)
|
||||
|
||||
const position = parseJsonField(hash.position)
|
||||
const vector = parseJsonField(hash.vector)
|
||||
|
||||
if(!position || !vector) {
|
||||
throw(new Error(`Agent ${agentId}: missing position or vector in arena hash ${key}`))
|
||||
}
|
||||
|
||||
if(!vectorsEqual(position, expected.position)) {
|
||||
throw(new Error(`Agent ${agentId}: position mismatch (MySQL vs arena store)`))
|
||||
}
|
||||
|
||||
if(!vectorsEqual(vector, expected.vector)) {
|
||||
throw(new Error(`Agent ${agentId}: vector mismatch (MySQL vs arena store)`))
|
||||
}
|
||||
|
||||
log('success', `Agent ${agentId}: position and vector match MySQL`)
|
||||
}
|
||||
|
||||
log('success', 'Arena store seeded correctly from MySQL (Maestro will stall waiting for readyToStart without GPS)')
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import yargs from 'yargs/yargs'
|
||||
import { hideBin } from 'yargs/helpers'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
import { RedisConnexion } from '../redisConnexion.js'
|
||||
import { configHelper } from '../configHelper.js'
|
||||
import { MySQLClient } from '@p42/p42modules'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const LOG_COLORS = {
|
||||
action: '\x1b[37m',
|
||||
success: '\x1b[32m',
|
||||
error: '\x1b[31m',
|
||||
reset: '\x1b[0m',
|
||||
}
|
||||
|
||||
function createTestMesh(handlers) {
|
||||
return({
|
||||
dispatchMessage(_cnx, msg, chan) {
|
||||
for(const handler of handlers) handler(msg, chan)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function buildBaseParser() {
|
||||
return(yargs(hideBin(process.argv))
|
||||
.usage('$0 <module> [options]')
|
||||
.command('$0 <module>', 'Run a GodDaemons test module', y => y
|
||||
.positional('module', {
|
||||
describe: 'Test module name (e.g. maestro1)',
|
||||
type: 'string',
|
||||
})
|
||||
)
|
||||
.options({
|
||||
config: {
|
||||
alias: 'c',
|
||||
describe: 'Path to config.json',
|
||||
default: join(__dirname, '..', 'config.json'),
|
||||
type: 'string',
|
||||
},
|
||||
guiDatabase: {
|
||||
describe: 'Override mysql.guiDatabase for this run',
|
||||
type: 'string',
|
||||
},
|
||||
simDatabase: {
|
||||
describe: 'Override mysql.simDatabase for this run',
|
||||
type: 'string',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.version(false)
|
||||
)
|
||||
}
|
||||
|
||||
function meshRedisConn(mesh, meshName, rootConfig, meshModule) {
|
||||
const { redis, ...meshConfig } = mesh
|
||||
return(redis.map(cfg =>
|
||||
new RedisConnexion({
|
||||
debug: false,
|
||||
config: { ...cfg, ...meshConfig, ...rootConfig },
|
||||
redisId: cfg.redisId,
|
||||
meshName,
|
||||
meshModule,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
async function loadTestModule(moduleName) {
|
||||
const modulePath = join(__dirname, 'modules', `${moduleName}.js`)
|
||||
try {
|
||||
return(await import(pathToFileURL(modulePath).href))
|
||||
} catch(err) {
|
||||
if(err.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
throw(new Error(`Test module not found: ${moduleName} (expected ${modulePath})`))
|
||||
}
|
||||
throw(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const preArgv = buildBaseParser().parseSync()
|
||||
const moduleName = preArgv.module ?? preArgv._[0]
|
||||
if(!moduleName) {
|
||||
buildBaseParser().showHelp()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const testModule = await loadTestModule(moduleName)
|
||||
|
||||
let parser = buildBaseParser()
|
||||
if(typeof(testModule.configureYargs) === 'function') {
|
||||
parser = testModule.configureYargs(parser)
|
||||
}
|
||||
const argv = parser.parseSync()
|
||||
const resolvedModule = argv.module ?? argv._[0]
|
||||
|
||||
const cfgh = new configHelper({ localfile: argv.config })
|
||||
await cfgh.fetchConfigFile()
|
||||
const config = cfgh.config
|
||||
|
||||
if(argv.guiDatabase) config.mysql.guiDatabase = argv.guiDatabase
|
||||
if(argv.simDatabase) config.mysql.simDatabase = argv.simDatabase
|
||||
|
||||
const arenaHandlers = new Set()
|
||||
const testMesh = createTestMesh(arenaHandlers)
|
||||
|
||||
const systemConns = meshRedisConn(config.systemMesh, 'system', config, null)
|
||||
const arenaConns = meshRedisConn(config.arenaMesh, 'arena', config, testMesh)
|
||||
const systemCnx = systemConns.find(c => c.redisConfig.role === 'primary') ?? systemConns[0]
|
||||
const arenaCnx = arenaConns.find(c => c.redisConfig.role === 'primary') ?? arenaConns[0]
|
||||
|
||||
const log = (type, message) => {
|
||||
const color = LOG_COLORS[type] ?? LOG_COLORS.action
|
||||
console.log(`${color}${message}${LOG_COLORS.reset}`)
|
||||
}
|
||||
|
||||
log('action', `Loading test module: ${resolvedModule}`)
|
||||
log('action', `Config: ${argv.config}`)
|
||||
|
||||
const databases = MySQLClient.resolveDatabases(config.mysql)
|
||||
if(argv.guiDatabase || argv.simDatabase) {
|
||||
log('action', `MySQL databases: gui=${databases.guiDatabase}, sim=${databases.simDatabase}`)
|
||||
}
|
||||
|
||||
log('action', 'Connecting to Redis (system)...')
|
||||
await systemCnx.redisLogin()
|
||||
log('success', `System Redis connected (${systemCnx.redisConfig.host}:${systemCnx.redisConfig.port})`)
|
||||
|
||||
log('action', 'Connecting to Redis (arena)...')
|
||||
await arenaCnx.redisLogin()
|
||||
await arenaCnx.redisChansStart()
|
||||
log('success', `Arena Redis connected (${arenaCnx.redisConfig.host}:${arenaCnx.redisConfig.port})`)
|
||||
|
||||
log('action', 'Connecting to MySQL...')
|
||||
const db = await MySQLClient.createPool(config.mysql)
|
||||
log('success', 'MySQL pool ready')
|
||||
|
||||
const ctx = {
|
||||
argv,
|
||||
config,
|
||||
databases,
|
||||
db,
|
||||
systemCnx,
|
||||
arenaCnx,
|
||||
log,
|
||||
onArenaMessage(handler) {
|
||||
arenaHandlers.add(handler)
|
||||
},
|
||||
offArenaMessage(handler) {
|
||||
arenaHandlers.delete(handler)
|
||||
},
|
||||
}
|
||||
|
||||
let exitCode = 0
|
||||
try {
|
||||
if(typeof(testModule.run) !== 'function') {
|
||||
throw(new Error(`Test module ${resolvedModule} must export a run(ctx) function`))
|
||||
}
|
||||
await testModule.run(ctx)
|
||||
log('success', `Test module ${resolvedModule} finished`)
|
||||
} catch(err) {
|
||||
log('error', err.message ?? String(err))
|
||||
exitCode = 1
|
||||
} finally {
|
||||
await db.end()
|
||||
await systemCnx.redisClient.quit()
|
||||
await arenaCnx.redisClient.quit()
|
||||
if(arenaCnx.redisSubscriber) await arenaCnx.redisSubscriber.quit()
|
||||
}
|
||||
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(`${LOG_COLORS.error}${err.message ?? err}${LOG_COLORS.reset}`)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user