Files
2026-06-20 18:50:26 +00:00

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)
})
}