NaturalCycles/dev-lib

View on GitHub
src/util/jest.util.ts

Summary

Maintainability
A
3 hrs
Test Coverage
F
0%
import fs from 'node:fs'
import os from 'node:os'
import { _range, _uniq } from '@naturalcycles/js-lib'
import { execVoidCommandSync, dimGrey, white } from '@naturalcycles/nodejs-lib'
import { cfgDir } from '../cnst/paths.cnst'
import { nodeModuleExists } from './test.util'

export function getJestConfigPath(): string {
  return fs.existsSync(`./jest.config.js`) ? './jest.config.js' : `${cfgDir}/jest.config.js`
}

export function getJestIntegrationConfigPath(): string {
  return fs.existsSync(`./jest.integration-test.config.js`)
    ? `./jest.integration-test.config.js`
    : `${cfgDir}/jest.integration-test.config.js`
}

export function getJestManualConfigPath(): string {
  return fs.existsSync(`./jest.manual-test.config.js`)
    ? `./jest.manual-test.config.js`
    : `${cfgDir}/jest.manual-test.config.js`
}

/**
 * Detects if jest is run with all tests, or with specific tests.
 */
export function isRunningAllTests(): boolean {
  const args = process.argv.slice(2)
  const positionalArgs = args.filter(a => !a.startsWith('-'))

  // console.log(process.argv, positionalArgs)

  return !positionalArgs.length
}

interface RunJestOpt {
  integration?: boolean
  manual?: boolean
  leaks?: boolean
}

/**
 * 1. Adds `--silent` if running all tests at once.
 */
export function runJest(opt: RunJestOpt = {}): void {
  if (!nodeModuleExists('jest')) {
    console.log(dimGrey(`node_modules/${white('jest')} not found, skipping tests`))
    return
  }

  const {
    CI,
    CIRCLECI,
    CPU_LIMIT,
    TZ = 'UTC',
    APP_ENV,
    JEST_NO_ALPHABETIC,
    JEST_SHARDS,
    NODE_OPTIONS = 'not defined',
  } = process.env
  const { node } = process.versions
  const cpuLimit = Number(CPU_LIMIT) || undefined
  const { integration, manual, leaks } = opt
  const processArgs = process.argv.slice(2)

  let jestConfig: string

  if (manual) {
    jestConfig = getJestManualConfigPath()
  } else if (integration) {
    jestConfig = getJestIntegrationConfigPath()
  } else {
    jestConfig = getJestConfigPath()
  }

  // Allow to override --maxWorkers
  let maxWorkers = processArgs.find(a => a.startsWith('--maxWorkers'))

  const args: string[] = [
    `--config=${jestConfig}`,
    '--logHeapUsage',
    '--passWithNoTests',
    ...processArgs,
  ]

  const env = {
    TZ,
    DEBUG_COLORS: '1',
  }

  if (CI) {
    args.push('--ci')

    // Works with both --coverage=false and --no-coverage syntaxes
    if (!integration && !manual && !processArgs.some(a => a.includes('-coverage'))) {
      // Coverage only makes sense for unit tests, not for integration/manual
      args.push('--coverage')
    }

    if (!maxWorkers) {
      if (cpuLimit && cpuLimit > 1) {
        maxWorkers = `--maxWorkers=${cpuLimit - 1}`
      } else if (CIRCLECI) {
        // We used to default to 2, but due to memory being an issue for Jest - now we default to 1,
        // as it's the most memory-efficient way
        // Since `workerIdleMemoryLimit` was introduced by default - we're changing default back to 2 workers
        // We now only do it for CircleCI (not for CI in general), as it reports cpus as 36
        // Github Actions don't do that and report correct number of cpus
        maxWorkers = '--maxWorkers=2'
      }
    }
  }

  // Running all tests - will use `--silent` to suppress console-logs, will also set process.env.JEST_SILENT=1
  if (CI || isRunningAllTests()) {
    args.push('--silent')
  }

  if (leaks) {
    args.push('--detectOpenHandles', '--detectLeaks')
    maxWorkers ||= '--maxWorkers=1'
  }

  if (maxWorkers) args.push(maxWorkers)

  if (args.includes('--silent')) {
    Object.assign(env, {
      JEST_SILENT: '1',
    })
  }

  if (!integration && !manual && !APP_ENV) {
    Object.assign(env, {
      APP_ENV: 'test',
    })
  }

  if (!JEST_NO_ALPHABETIC) {
    args.push(`--testSequencer=${cfgDir}/jest.alphabetic.sequencer.js`)
  }

  const availableParallelism = os.availableParallelism?.()
  const cpus = os.cpus().length
  console.log(
    `${dimGrey(
      Object.entries({
        node,
        NODE_OPTIONS,
        cpus,
        availableParallelism,
        cpuLimit,
      })
        .map(([k, v]) => `${k}: ${v}`)
        .join(', '),
    )}`,
  )

  if (JEST_SHARDS) {
    const totalShards = Number(JEST_SHARDS)
    const shards = _range(1, totalShards + 1)

    for (const shard of shards) {
      execVoidCommandSync('jest', _uniq([...args, `--shard=${shard}/${totalShards}`]), {
        env,
      })
    }
  } else {
    execVoidCommandSync('jest', _uniq(args), {
      env,
    })
  }
}