fix test 3

This commit is contained in:
Evgeny @ SimpleX Chat
2026-02-05 23:11:09 +00:00
parent d38f2783ad
commit df3b7a5af9
3 changed files with 58 additions and 70 deletions
+35 -64
View File
@@ -1,85 +1,55 @@
import {spawn, execSync, ChildProcess} from 'child_process'
import {createHash} from 'crypto'
import {createConnection} from 'net'
import {createConnection, createServer} from 'net'
import {resolve, join} from 'path'
import {readFileSync, mkdtempSync, writeFileSync, copyFileSync, existsSync, unlinkSync} from 'fs'
import {tmpdir} from 'os'
const XFTP_PORT = 7000
const LOCK_FILE = join(tmpdir(), 'xftp-test-server.pid')
export const PORT_FILE = join(tmpdir(), 'xftp-test-server.port')
// Find a free port by binding to port 0
function findFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const srv = createServer()
srv.listen(0, '127.0.0.1', () => {
const addr = srv.address()
if (addr && typeof addr === 'object') {
const port = addr.port
srv.close(() => resolve(port))
} else {
srv.close(() => reject(new Error('Could not get port')))
}
})
srv.on('error', reject)
})
}
let server: ChildProcess | null = null
let isOwner = false
// Kill any process listening on the given port (cross-platform)
function killProcessOnPort(port: number): void {
try {
// Try lsof first (common on Mac/Linux)
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, {stdio: 'ignore'})
return
} catch (_) {}
try {
// Fallback: use netstat + awk (works on most systems)
const cmd = process.platform === 'darwin'
? `netstat -anv -p tcp | awk '$4 ~ /:${port}$/ && $6 == "LISTEN" {print $9}' | xargs kill -9 2>/dev/null`
: `ss -tlnp 'sport = :${port}' | awk 'NR>1 {match($0, /pid=([0-9]+)/, a); print a[1]}' | xargs kill -9 2>/dev/null`
execSync(cmd, {stdio: 'ignore'})
} catch (_) {}
}
// Check if port is currently in use
function isPortInUse(port: number): Promise<boolean> {
return new Promise((resolve) => {
const sock = createConnection({port, host: 'localhost'}, () => {
sock.destroy()
resolve(true)
})
sock.on('error', () => {
sock.destroy()
resolve(false)
})
})
}
export async function setup() {
// Always check if port is in use first, regardless of lock file state
if (await isPortInUse(XFTP_PORT)) {
// Check if we have a valid lock file owner
if (existsSync(LOCK_FILE)) {
const pid = parseInt(readFileSync(LOCK_FILE, 'utf-8').trim(), 10)
try {
process.kill(pid, 0) // check if process exists
// Lock owner is alive and port is in use — likely a valid running server
await waitForPort(XFTP_PORT)
return
} catch (_) {
// Lock owner is dead but port is in use — orphaned process
}
}
// Port in use but no valid lock owner — kill the orphaned process
console.log('[globalSetup] Port in use without valid lock, killing orphaned process...')
killProcessOnPort(XFTP_PORT)
await new Promise(r => setTimeout(r, 500))
// Verify port is now free
if (await isPortInUse(XFTP_PORT)) {
throw new Error(`Port ${XFTP_PORT} still in use after cleanup attempt`)
}
}
// Clean up stale lock file if it exists
if (existsSync(LOCK_FILE)) {
// Check if another test process owns the server
if (existsSync(LOCK_FILE) && existsSync(PORT_FILE)) {
const pid = parseInt(readFileSync(LOCK_FILE, 'utf-8').trim(), 10)
const port = parseInt(readFileSync(PORT_FILE, 'utf-8').trim(), 10)
try {
process.kill(pid, 0)
// Process exists and port wasn't in use — wait for it
await waitForPort(XFTP_PORT)
process.kill(pid, 0) // check if process exists
// Lock owner is alive — wait for server to be ready
await waitForPort(port)
return
} catch (_) {
unlinkSync(LOCK_FILE)
// Lock owner is dead — clean up
try { unlinkSync(LOCK_FILE) } catch (_) {}
try { unlinkSync(PORT_FILE) } catch (_) {}
}
}
// Find a free port dynamically
const xftpPort = await findFreePort()
writeFileSync(LOCK_FILE, String(process.pid))
writeFileSync(PORT_FILE, String(xftpPort))
isOwner = true
const fixtures = resolve(__dirname, '../../tests/fixtures')
@@ -107,7 +77,7 @@ enable: off
[TRANSPORT]
host: localhost
port: ${XFTP_PORT}
port: ${xftpPort}
[FILES]
path: ${filesDir}
@@ -136,12 +106,13 @@ key: ${join(fixtures, 'web.key')}
})
// Poll-connect until the server is actually listening
await waitForServerReady(server, XFTP_PORT)
await waitForServerReady(server, xftpPort)
}
export async function teardown() {
if (isOwner) {
try { unlinkSync(LOCK_FILE) } catch (_) {}
try { unlinkSync(PORT_FILE) } catch (_) {}
if (server) {
server.kill('SIGTERM')
await new Promise<void>(resolve => {
+4 -1
View File
@@ -2,6 +2,7 @@ import {defineConfig, type Plugin} from 'vite'
import {readFileSync} from 'fs'
import {createHash} from 'crypto'
import presets from './web/servers.json'
import {PORT_FILE} from './test/globalSetup'
function parseHost(addr: string): string {
const m = addr.match(/@(.+)$/)
@@ -35,7 +36,9 @@ export default defineConfig(({mode}) => {
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s/g, ''), 'base64')
const fp = createHash('sha256').update(der).digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_')
servers = [`xftp://${fp}@localhost:7000`]
// PORT_FILE is written by globalSetup before vite build runs
const port = readFileSync(PORT_FILE, 'utf-8').trim()
servers = [`xftp://${fp}@localhost:${port}`]
define['__XFTP_SERVERS__'] = JSON.stringify(servers)
} else {
servers = [...presets.simplex, ...presets.flux]
+19 -5
View File
@@ -1,19 +1,33 @@
import {defineConfig} from 'vitest/config'
import {defineConfig, type Plugin} from 'vitest/config'
import {readFileSync} from 'fs'
import {createHash} from 'crypto'
import {PORT_FILE} from './test/globalSetup'
// Compute fingerprint from ca.crt (SHA-256 of DER, same as Haskell's loadFileFingerprint)
const pem = readFileSync('../tests/fixtures/ca.crt', 'utf-8')
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s/g, ''), 'base64')
const fingerprint = createHash('sha256').update(der).digest('base64').replace(/\+/g, '-').replace(/\//g, '_')
const serverAddr = `xftp://${fingerprint}@localhost:7000`
// Plugin to inject XFTP_SERVER at transform time (after globalSetup writes PORT_FILE)
function xftpServerPlugin(): Plugin {
let serverAddr: string | null = null
return {
name: 'xftp-server-define',
transform(code, id) {
if (!code.includes('import.meta.env.XFTP_SERVER')) return null
if (!serverAddr) {
const port = readFileSync(PORT_FILE, 'utf-8').trim()
serverAddr = `xftp://${fingerprint}@localhost:${port}`
}
return code.replace(/import\.meta\.env\.XFTP_SERVER/g, JSON.stringify(serverAddr))
}
}
}
export default defineConfig({
esbuild: {target: 'esnext'},
optimizeDeps: {esbuildOptions: {target: 'esnext'}},
define: {
'import.meta.env.XFTP_SERVER': JSON.stringify(serverAddr)
},
plugins: [xftpServerPlugin()],
test: {
include: ['test/**/*.test.ts'],
browser: {