Ctrl K
ComunidadGuías…

test-helpers/integration-python.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { execFileSync } from 'node:child_process'

/**
 * Interpreter the integration tests run the deepnote-toolkit server with: `DEEPNOTE_PYTHON` when
 * set (as in CI), otherwise `python3` from PATH.
 */
export function integrationPython(): string {
  const configured = process.env.DEEPNOTE_PYTHON
  return configured && configured.trim().length > 0 ? configured : 'python3'
}

/** Fails loudly, with setup instructions, when the interpreter cannot start the toolkit server. */
export function requireToolkit(python: string): void {
  try {
    execFileSync(python, ['-c', 'import deepnote_toolkit, jupyter_server'], { stdio: 'ignore', timeout: 60_000 })
  } catch {
    throw new Error(
      `Integration tests need deepnote-toolkit with its server extra installed for ${python}. ` +
        'Install it with pip install "deepnote-toolkit[server]" or point DEEPNOTE_PYTHON at a venv that has it.'
    )
  }
}

/** Direct child processes of `pid`, as `{ pid, command }`, via pgrep (Linux and macOS). */
export function childProcesses(pid: number): Array<{ pid: number; command: string }> {
  let pids: string
  try {
    pids = execFileSync('pgrep', ['-P', String(pid)], { encoding: 'utf-8' })
  } catch {
    return []
  }
  return pids
    .split('\n')
    .map(line => Number(line.trim()))
    .filter(child => Number.isInteger(child) && child > 0)
    .map(child => {
      let command = ''
      try {
        command = execFileSync('ps', ['-o', 'command=', '-p', String(child)], { encoding: 'utf-8' }).trim()
      } catch {
        // Already gone.
      }
      return { pid: child, command }
    })
}

export interface ToolkitLeakGuard {
  /**
   * Fails when toolkit runtime processes (supervisor, Jupyter server, language server, kernels) from
   * the interpreter's environment are running that were not already running when the guard was
   * created. `context` is appended to the failure message to help debugging.
   */
  assertNone(context?: string): Promise<void>
}

/**
 * Snapshots the toolkit processes of the interpreter's environment so that later checks only flag
 * processes a test started. The environment is the interpreter's `sys.prefix`, which also covers a
 * bare `python3` on PATH, not just venv paths.
 *
 * The guard sees every toolkit process of that interpreter on the machine, so two integration
 * suites using the same interpreter must not run at the same time: each would flag the other's
 * servers as leaks.
 */
export function createToolkitLeakGuard(python: string): ToolkitLeakGuard {
  const prefix = interpreterPrefix(python)
  const baseline = new Set(listToolkitProcesses(prefix).map(entry => entry.pid))

  return {
    async assertNone(context = '') {
      // Give children a moment to disappear after their parent exited.
      await new Promise(resolve => setTimeout(resolve, 1000))
      const leaked = listToolkitProcesses(prefix).filter(entry => !baseline.has(entry.pid))
      if (leaked.length > 0) {
        const listing = leaked.map(entry => `${entry.pid} ${entry.command}`).join('\n')
        throw new Error(`Toolkit processes were left running:\n${listing}${context ? `\n\n${context}` : ''}`)
      }
    },
  }
}

/** `sys.prefix` of the interpreter, or null when it cannot be determined. */
function interpreterPrefix(python: string): string | null {
  try {
    const prefix = execFileSync(python, ['-c', 'import sys; print(sys.prefix)'], {
      encoding: 'utf-8',
      timeout: 60_000,
    }).trim()
    return prefix.length > 0 ? prefix : null
  } catch {
    return null
  }
}

/** Toolkit runtime processes whose command line runs from `prefix`, via pgrep (Linux and macOS). */
function listToolkitProcesses(prefix: string | null): Array<{ pid: number; command: string }> {
  if (!prefix) return []
  const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  let listing: string
  try {
    listing = execFileSync(
      'pgrep',
      ['-fl', `${escapedPrefix}.*(deepnote_toolkit|jupyter-server|ipykernel_launcher|pylsp)`],
      { encoding: 'utf-8' }
    )
  } catch {
    // pgrep exits 1 when nothing matches.
    return []
  }
  return listing
    .split('\n')
    .map(line => line.trim())
    .filter(line => line.length > 0)
    .map(line => {
      const [pid, ...command] = line.split(/\s+/)
      return { pid: Number(pid), command: command.join(' ') }
    })
    .filter(entry => Number.isInteger(entry.pid))
}