import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync, mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { spawnSync } from 'node:child_process' const root = fileURLToPath(new URL('..', import.meta.url)) const wrapper = resolve(root, 'deploy/scripts/run-ci-maven.sh') const bash = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : 'bash' const shellAvailable = process.platform !== 'win32' || existsSync(bash) test('both Jenkins Maven paths share one cross-job lock and bounded host entry', () => { for (const file of ['Jenkinsfile', 'Jenkinsfile.service']) { const source = readFileSync(resolve(root, file), 'utf8') assert.match(source, /lock\(resource: 'firstleg-test-maven'\)\s*\{\s*timeout\(time: 50, unit: 'MINUTES'\)/) assert.match(source, /sh deploy\/scripts\/run-ci-maven\.sh --settings/) assert.doesNotMatch(source, /(?:^|\n)\s*mvn\s|sh "mvn /) } const source = readFileSync(wrapper, 'utf8') assert.match(source, /exec 9>/) assert.match(source, /flock -n 9/) assert.match(source, /exec timeout --signal=TERM --kill-after=30s 45m mvn -Pci-bounded/) assert.doesNotMatch(source, /exec timeout[^\n]*--foreground/) }) test('test JVM gets a separate heap, JaCoCo late binding, and parent-loss cleanup', () => { const pom = readFileSync(resolve(root, 'pom.xml'), 'utf8') const profile = pom.match(/\s*ci-bounded<\/id>([\s\S]*?)<\/profile>/)?.[1] assert.ok(profile) assert.match(profile, /@\{argLine\}[^<]*-Xmx1024m[^<]*-XX:\+ExitOnOutOfMemoryError<\/argLine>/) for (const [name, value] of Object.entries({ forkCount: '1', enableProcessChecker: 'ping', shutdown: 'kill', forkedProcessTimeoutInSeconds: '1200', forkedProcessExitTimeoutInSeconds: '30' })) assert.ok(profile.includes(`<${name}>${value}`)) }) test('shell rejects parallel reactors and held host lock before invoking Maven; propagates failure', { skip: !shellAvailable }, () => { const directory = mkdtempSync(join(tmpdir(), 'ci-maven-command-test-')) try { // Stand-ins exercise command wiring, not Linux flock or process-group signalling. writeFileSync(join(directory, 'flock'), '#!/bin/sh\nexit "${TEST_LOCK_STATUS:-0}"\n', { mode: 0o755 }) writeFileSync(join(directory, 'mvn'), '#!/bin/sh\nexit 99\n', { mode: 0o755 }) writeFileSync(join(directory, 'timeout'), '#!/bin/sh\nprintf "%s\\n" "$MAVEN_OPTS" "$@"\nexit "${TEST_CHILD_STATUS:-0}"\n', { mode: 0o755 }) const run = (args, extra = {}) => spawnSync(bash, ['-c', 'testbin=$(cd "$1" && pwd); shift; PATH="$testbin:$PATH"; export PATH; exec sh "$@"', 'fixture', directory.replaceAll('\\', '/'), wrapper.replaceAll('\\', '/'), ...args], { encoding: 'utf8', timeout: 10000, env: { ...process.env, ...extra } }) for (const args of [['-T', '3', 'test'], ['-T3', 'test'], ['--threads=3', 'test']]) { const result = run(args) assert.equal(result.status, 64, result.stderr) assert.equal(result.stdout, '') } const held = run(['test'], { TEST_LOCK_STATUS: '1' }) assert.equal(held.status, 75, held.stderr) assert.equal(held.stdout, '') const normal = run(['-pl', 'file', '-am', 'test'], { TEST_CHILD_STATUS: '23', MAVEN_OPTS: '-Xmx8g' }) assert.equal(normal.status, 23, normal.stderr) assert.match(normal.stdout, /-Xmx768m/) assert.doesNotMatch(normal.stdout, /-Xmx8g/) assert.match(normal.stdout, /--signal=TERM\r?\n--kill-after=30s\r?\n45m\r?\nmvn\r?\n-Pci-bounded\r?\n-pl\r?\nfile/) } finally { // Only this newly-created fixture directory; no project copies or shared caches removed. assert.equal(resolve(directory).startsWith(resolve(tmpdir()) + (process.platform === 'win32' ? '\\' : '/')), true) rmSync(directory, { recursive: true }) } })