213 lines
6.5 KiB
JavaScript
213 lines
6.5 KiB
JavaScript
import yargs from 'yargs/yargs'
|
|
import { hideBin } from 'yargs/helpers'
|
|
import { pathToFileURL } from 'url'
|
|
import { fileURLToPath } from 'url'
|
|
import { dirname, join, resolve } 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 __filename = fileURLToPath(import.meta.url)
|
|
|
|
const UUID_DASHED_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
const UUID_HEX32_RE = /^[0-9a-f]{32}$/i
|
|
|
|
export function normalizeUuid(value) {
|
|
if(typeof(value) !== 'string') {
|
|
throw(new Error('UUID must be a string'))
|
|
}
|
|
|
|
const trimmed = value.trim()
|
|
if(!trimmed) throw(new Error('UUID must be a non-empty string'))
|
|
|
|
if(UUID_DASHED_RE.test(trimmed)) {
|
|
return(trimmed.toLowerCase())
|
|
}
|
|
|
|
let hex = trimmed
|
|
if(/^0x/i.test(hex)) hex = hex.slice(2)
|
|
if(!UUID_HEX32_RE.test(hex)) {
|
|
throw(new Error(
|
|
`Invalid UUID: ${value} (expected dashed form or 32-char hex, optionally prefixed with 0x)`
|
|
))
|
|
}
|
|
|
|
hex = hex.toLowerCase()
|
|
return(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`)
|
|
}
|
|
|
|
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,
|
|
normalizeUuid,
|
|
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)
|
|
}
|
|
|
|
const isMain = process.argv[1] && resolve(__filename) === resolve(process.argv[1])
|
|
|
|
if(isMain) {
|
|
main().catch(err => {
|
|
console.error(`${LOG_COLORS.error}${err.message ?? err}${LOG_COLORS.reset}`)
|
|
process.exit(1)
|
|
})
|
|
}
|