getTestSuitePlan.ts•9.94 kB
import { klona } from 'klona'
import { getTestSuiteFullName, NamedTestSuiteConfig } from './getTestSuiteInfo'
import { AdapterProviders, adaptersForProvider, Providers, relationModesForAdapter } from './providers'
import { TestSuiteMeta } from './setupTestSuiteMatrix'
import { CliMeta, MatrixOptions } from './types'
export type TestPlanEntry = {
  name: string
  skip: boolean
  suiteConfig: NamedTestSuiteConfig
}
type SuitePlanContext = {
  includedProviders?: string[]
  includedProviderAdapters?: string[]
  excludedProviders: string[]
  excludedDriverAdapters: string[]
  updateSnapshots: 'inline' | 'external' | undefined
}
/**
 * Get a test plan from a list of suite configs. Test plan tells what the name of
 * the tests are, what are their config and whether or not they should be executed or skipped
 * @param suiteMeta
 * @returns [test-suite-title: string, test-suite-config: object]
 */
export function getTestSuitePlan(
  testCliMeta: CliMeta,
  suiteMeta: TestSuiteMeta,
  suiteConfigs: NamedTestSuiteConfig[],
  options?: MatrixOptions,
): TestPlanEntry[] {
  const context = buildPlanContext()
  const expandedSuiteConfigs = suiteConfigs
    .flatMap(getExpandedTestSuitePlanWithProviderFlavors)
    .flatMap(getExpandedTestSuitePlanWithRemoteQpe)
  expandedSuiteConfigs.forEach((config) => {
    config.matrixOptions.engineType ??= testCliMeta.engineType
    config.matrixOptions.clientRuntime ??= testCliMeta.runtime
    config.matrixOptions.previewFeatures ??= testCliMeta.previewFeatures
    config.matrixOptions.generatorType ??= testCliMeta.generatorType
    config.matrixOptions.clientEngineExecutor ??= testCliMeta.clientEngineExecutor
  })
  return expandedSuiteConfigs.map((namedConfig, configIndex) => ({
    name: getTestSuiteFullName(suiteMeta, namedConfig),
    skip: shouldSkipSuiteConfig(context, namedConfig, configIndex, testCliMeta, options),
    suiteConfig: namedConfig,
  }))
}
/**
 * This function takes a regular `testPlanEntry` and expands this into the
 * multiple compatible driver adapters that exist for a given provider. For
 * example, postgres => [postgres (pg), postgres (neon)], put very simply. In
 * other words, a given test matrix is expanded with the provider adapters.
 * @param suiteConfig
 * @returns
 */
function getExpandedTestSuitePlanWithProviderFlavors(suiteConfig: NamedTestSuiteConfig) {
  const provider = suiteConfig.matrixOptions.provider
  const suiteConfigExpansions = adaptersForProvider[provider].map((adapterProvider) => {
    const newSuiteConfig = klona(suiteConfig)
    newSuiteConfig.matrixOptions.driverAdapter = adapterProvider
    newSuiteConfig.parametersString += `, ${adapterProvider}`
    // ^^^ temporary until I get to the TODO in getTestSuiteParametersString
    // if the test is not doing stuff with relation mode already, we set one
    if (newSuiteConfig.matrixOptions.relationMode === undefined) {
      newSuiteConfig.matrixOptions.relationMode = relationModesForAdapter[adapterProvider]
    }
    return newSuiteConfig
  })
  // add the original suite config to the list of expanded configs
  return [suiteConfig, ...suiteConfigExpansions]
}
/**
 * Expands each suiteConfig entry to ensure we have separate entries for
 * client engine's remote executor which doesn't use driver adapters locally
 * and needs to have matrix entries on its own to ensure the names of generated
 * tests don't collide with those using QE without driver adapters. We need
 * this to have different snapshots for, e.g., errors.
 */
function getExpandedTestSuitePlanWithRemoteQpe(suiteConfig: NamedTestSuiteConfig) {
  if (suiteConfig.matrixOptions.driverAdapter) {
    suiteConfig.matrixOptions.clientEngineExecutor = 'local'
    return [suiteConfig]
  } else {
    // For each suite config that doesn't use driver adapters, we need
    // to clone it and create two separate configs:
    //  - one which doesn't require `ClientEngine` specifically and can
    //    be run with any engine type, but requires specifically a local
    //    executor in the event it is executed using `ClientEngine`;
    //  - another one which is only ever used with remote executor of
    //    `ClientEngine` and never runs with either a local exeuctor
    //    of the client engine nor any other engine type.
    // This is required to separate the snapshots between them.
    suiteConfig.matrixOptions.clientEngineExecutor = 'local'
    const remoteExecutorSuiteConfig = klona(suiteConfig)
    remoteExecutorSuiteConfig.matrixOptions.clientRuntime = 'client'
    remoteExecutorSuiteConfig.matrixOptions.clientEngineExecutor = 'remote'
    return [suiteConfig, remoteExecutorSuiteConfig]
  }
}
function shouldSkipSuiteConfig(
  {
    updateSnapshots,
    includedProviders,
    includedProviderAdapters,
    excludedProviders,
    excludedDriverAdapters: excludedProviderFlavors,
  }: SuitePlanContext,
  config: NamedTestSuiteConfig,
  configIndex: number,
  cliMeta: CliMeta,
  options?: MatrixOptions,
): boolean {
  const { provider, driverAdapter, relationMode, engineType, clientRuntime, clientEngineExecutor } =
    config.matrixOptions
  if (updateSnapshots === 'inline' && configIndex > 0) {
    // when updating inline snapshots, we have to run a  single suite only -
    // otherwise jest will fail with "Multiple inline snapshots for the same call are not supported" error
    return true
  }
  if (updateSnapshots === 'external' && configIndex === 0) {
    // when updating external snapshots, we assume that inline snapshots update was run just before it - so
    // there is no reason to re-run the first suite
    return true
  }
  // if one of the skip predicates is true, skip
  let isSkipped = false
  options?.skip?.((pred) => {
    isSkipped ||= typeof pred === 'boolean' ? pred : pred()
  }, config.matrixOptions)
  if (isSkipped) {
    return true
  }
  // if the test doesn't support the engine type, skip
  if (options?.skipEngine?.from.includes(engineType!)) {
    return true
  }
  // if the test needs to skip the dataproxy test, skip
  if (cliMeta.dataProxy && options?.skipDataProxy?.runtimes.includes(cliMeta.runtime)) {
    return true
  }
  // if the client doesn't support the provider, skip
  if (cliMeta.dataProxy && provider === Providers.SQLITE) {
    return true
  }
  // if the provider is not included, skip
  if (includedProviders !== undefined && !includedProviders.includes(provider)) {
    return true
  }
  // if the provider is excluded, skip
  if (excludedProviders.includes(provider)) {
    return true
  }
  // if there is a Driver Adapter to run and it's not included, skip
  if (driverAdapter !== undefined && !includedProviderAdapters?.includes(driverAdapter)) {
    return true
  }
  // if this test can't use a driver adapter, and we run the tests for a specific
  // driver adapter, skip it
  if (driverAdapter === undefined && includedProviderAdapters !== undefined) {
    return true
  }
  // if there is a Driver Adapter to run and it's excluded, skip
  if (driverAdapter && excludedProviderFlavors.includes(driverAdapter)) {
    return true
  }
  // if the Driver Adapter is explicitly skipped in the matrix options, skip
  if (driverAdapter !== undefined && options?.skipDriverAdapter?.from.includes(driverAdapter)) {
    return true
  }
  // if there is a relation mode set and the Driver Adapter doesn't support it, skip
  if (
    driverAdapter !== undefined &&
    relationMode !== undefined &&
    relationModesForAdapter[driverAdapter] !== undefined &&
    relationMode !== relationModesForAdapter[driverAdapter]
  ) {
    return true
  }
  // if Driver Adapters are enabled and the test has no Driver Adapter, skip
  if (includedProviderAdapters !== undefined && driverAdapter === undefined) {
    return true
  }
  // if this test requires a specific client runtime which doesn't match the one we're testing for, skip
  if (clientRuntime !== undefined && clientRuntime !== cliMeta.runtime) {
    return true
  }
  // if this test requires a specific client engine executor in case a client engine is used,
  // and it doesn't match the one we're testing for, skip
  if (clientEngineExecutor !== undefined && clientEngineExecutor !== cliMeta.clientEngineExecutor) {
    return true
  }
  // if this test requires client engine's remote executor and we're not testing QC, skip
  if (clientEngineExecutor === 'remote' && clientRuntime !== 'client') {
    return true
  }
  return false
}
function buildPlanContext(): SuitePlanContext {
  return {
    includedProviders: process.env.ONLY_TEST_PROVIDERS?.split(','),
    includedProviderAdapters: process.env.ONLY_TEST_PROVIDER_ADAPTERS?.split(','),
    excludedProviders: getExclusionsFromEnv(excludeEnvToProviderMap),
    excludedDriverAdapters: getExclusionsFromEnv(excludeEnvToProviderFlavorMap),
    updateSnapshots: process.env.UPDATE_SNAPSHOTS as 'inline' | 'external' | undefined,
  }
}
const excludeEnvToProviderMap = {
  TEST_SKIP_MONGODB: 'mongodb',
  TEST_SKIP_MSSQL: 'sqlserver',
  TEST_SKIP_COCKROACHDB: 'cockroachdb',
  TEST_SKIP_POSTGRESQL: 'postgresql',
  TEST_SKIP_SQLITE: 'sqlite',
}
const excludeEnvToProviderFlavorMap = {
  TEST_SKIP_VITESS: AdapterProviders.VITESS_8,
  TEST_SKIP_PG: AdapterProviders.JS_PG,
  TEST_SKIP_NEON: AdapterProviders.JS_NEON,
  TEST_SKIP_PLANETSCALE: AdapterProviders.JS_PLANETSCALE,
  TEST_SKIP_LIBSQL: AdapterProviders.JS_LIBSQL,
  TEST_SKIP_D1: AdapterProviders.JS_D1,
  TEST_SKIP_BETTER_SQLITE3: AdapterProviders.JS_BETTER_SQLITE3,
  TEST_SKIP_MSSQL: AdapterProviders.JS_MSSQL,
  TEST_SKIP_MARIADB: AdapterProviders.JS_MARIADB,
  TEST_SKIP_PG_COCKROACHDB: AdapterProviders.JS_PG_COCKROACHDB,
}
function getExclusionsFromEnv(exclusionMap: Record<string, string>) {
  return Object.entries(exclusionMap).reduce((acc, [envVarName, exclusionName]) => {
    if (process.env[envVarName]) {
      acc.push(exclusionName.toLowerCase())
    }
    return acc
  }, [] as string[])
}