recorded-runner.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { test, type TestOptions } from "bun:test"
  2. import { Effect, type Layer } from "effect"
  3. import { testEffect } from "./lib/effect"
  4. import { cassetteName, classifiedTags, matchesSelected, missingEnv, unique } from "./recorded-utils"
  5. export type RecordedBody<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
  6. export type RecordedGroupOptions = {
  7. readonly prefix: string
  8. readonly provider?: string
  9. readonly protocol?: string
  10. readonly requires?: ReadonlyArray<string>
  11. readonly tags?: ReadonlyArray<string>
  12. readonly metadata?: Record<string, unknown>
  13. }
  14. export type RecordedCaseOptions = {
  15. readonly cassette?: string
  16. readonly id?: string
  17. readonly provider?: string
  18. readonly protocol?: string
  19. readonly requires?: ReadonlyArray<string>
  20. readonly tags?: ReadonlyArray<string>
  21. readonly metadata?: Record<string, unknown>
  22. }
  23. export const recordedEffectGroup = <
  24. R,
  25. E,
  26. Options extends RecordedGroupOptions,
  27. CaseOptions extends RecordedCaseOptions,
  28. >(input: {
  29. readonly duplicateLabel: string
  30. readonly options: Options
  31. readonly cassetteExists: (cassette: string) => boolean
  32. readonly layer: (input: {
  33. readonly cassette: string
  34. readonly tags: ReadonlyArray<string>
  35. readonly metadata: Record<string, unknown>
  36. readonly recording: boolean
  37. readonly options: Options
  38. readonly caseOptions: CaseOptions
  39. }) => Layer.Layer<R, E>
  40. }) => {
  41. const cassettes = new Set<string>()
  42. const run = <A, E2>(
  43. name: string,
  44. caseOptions: CaseOptions,
  45. body: RecordedBody<A, E2, R>,
  46. testOptions?: number | TestOptions,
  47. ) => {
  48. const cassette = cassetteName(input.options.prefix, name, caseOptions)
  49. if (cassettes.has(cassette)) throw new Error(`Duplicate ${input.duplicateLabel} "${cassette}"`)
  50. cassettes.add(cassette)
  51. const tags = unique([
  52. ...classifiedTags(input.options),
  53. ...classifiedTags({
  54. provider: caseOptions.provider,
  55. protocol: caseOptions.protocol,
  56. tags: caseOptions.tags,
  57. }),
  58. ])
  59. if (!matchesSelected({ prefix: input.options.prefix, name, cassette, tags }))
  60. return test.skip(name, () => {}, testOptions)
  61. const recording = process.env.RECORD === "true"
  62. if (recording) {
  63. if (missingEnv([...(input.options.requires ?? []), ...(caseOptions.requires ?? [])]).length > 0) {
  64. return test.skip(name, () => {}, testOptions)
  65. }
  66. } else if (!input.cassetteExists(cassette)) {
  67. return test.skip(name, () => {}, testOptions)
  68. }
  69. return testEffect(
  70. input.layer({
  71. cassette,
  72. tags,
  73. metadata: { ...input.options.metadata, ...caseOptions.metadata, tags },
  74. recording,
  75. options: input.options,
  76. caseOptions,
  77. }),
  78. ).live(name, body, testOptions)
  79. }
  80. const effect = <A, E2>(name: string, body: RecordedBody<A, E2, R>, testOptions?: number | TestOptions) =>
  81. run(name, {} as CaseOptions, body, testOptions)
  82. effect.with = <A, E2>(
  83. name: string,
  84. caseOptions: CaseOptions,
  85. body: RecordedBody<A, E2, R>,
  86. testOptions?: number | TestOptions,
  87. ) => run(name, caseOptions, body, testOptions)
  88. return { effect }
  89. }