format.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { NodeFileSystem } from "@effect/platform-node"
  2. import { describe, expect } from "bun:test"
  3. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  4. import { Effect, Layer } from "effect"
  5. import { provideTmpdirInstance, testInstanceStoreLayer, TestInstance } from "../fixture/fixture"
  6. import { testEffect } from "../lib/effect"
  7. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  8. import { Format } from "../../src/format"
  9. import * as Formatter from "../../src/format/formatter"
  10. const it = testEffect(
  11. Layer.mergeAll(LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])), NodeFileSystem.layer),
  12. )
  13. describe("Format", () => {
  14. it.instance("status() returns empty list when no formatters are configured", () =>
  15. Format.Service.use((fmt) =>
  16. Effect.gen(function* () {
  17. expect(yield* fmt.status()).toEqual([])
  18. }),
  19. ),
  20. )
  21. it.instance(
  22. "status() returns built-in formatters when formatter is true",
  23. () =>
  24. Format.Service.use((fmt) =>
  25. Effect.gen(function* () {
  26. const statuses = yield* fmt.status()
  27. const gofmt = statuses.find((item) => item.name === "gofmt")
  28. expect(gofmt).toBeDefined()
  29. expect(gofmt!.extensions).toContain(".go")
  30. }),
  31. ),
  32. { config: { formatter: true } },
  33. )
  34. it.instance(
  35. "status() keeps built-in formatters when config object is provided",
  36. () =>
  37. Format.Service.use((fmt) =>
  38. Effect.gen(function* () {
  39. const statuses = yield* fmt.status()
  40. const gofmt = statuses.find((item) => item.name === "gofmt")
  41. const mix = statuses.find((item) => item.name === "mix")
  42. expect(gofmt).toBeDefined()
  43. expect(gofmt!.extensions).toContain(".go")
  44. expect(mix).toBeDefined()
  45. }),
  46. ),
  47. { config: { formatter: { gofmt: {} } } },
  48. )
  49. it.instance(
  50. "status() excludes formatters marked as disabled in config",
  51. () =>
  52. Format.Service.use((fmt) =>
  53. Effect.gen(function* () {
  54. const statuses = yield* fmt.status()
  55. const gofmt = statuses.find((item) => item.name === "gofmt")
  56. const mix = statuses.find((item) => item.name === "mix")
  57. expect(gofmt).toBeUndefined()
  58. expect(mix).toBeDefined()
  59. }),
  60. ),
  61. { config: { formatter: { gofmt: { disabled: true } } } },
  62. )
  63. it.instance(
  64. "status() excludes uv when ruff is disabled",
  65. () =>
  66. Format.Service.use((fmt) =>
  67. Effect.gen(function* () {
  68. const statuses = yield* fmt.status()
  69. expect(statuses.find((item) => item.name === "ruff")).toBeUndefined()
  70. expect(statuses.find((item) => item.name === "uv")).toBeUndefined()
  71. }),
  72. ),
  73. { config: { formatter: { ruff: { disabled: true } } } },
  74. )
  75. it.instance(
  76. "status() excludes ruff when uv is disabled",
  77. () =>
  78. Format.Service.use((fmt) =>
  79. Effect.gen(function* () {
  80. const statuses = yield* fmt.status()
  81. expect(statuses.find((item) => item.name === "ruff")).toBeUndefined()
  82. expect(statuses.find((item) => item.name === "uv")).toBeUndefined()
  83. }),
  84. ),
  85. { config: { formatter: { uv: { disabled: true } } } },
  86. )
  87. it.instance("service initializes without error", () => Format.Service.use(() => Effect.void))
  88. it.instance(
  89. "file() returns false when no formatter runs",
  90. () =>
  91. Effect.gen(function* () {
  92. const test = yield* TestInstance
  93. const file = `${test.directory}/test.txt`
  94. yield* Effect.promise(() => Bun.write(file, "x"))
  95. const formatted = yield* Format.use.file(file)
  96. expect(formatted).toBe(false)
  97. }),
  98. { config: { formatter: false } },
  99. )
  100. testEffect(
  101. Layer.mergeAll(
  102. LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])),
  103. NodeFileSystem.layer,
  104. testInstanceStoreLayer,
  105. ),
  106. ).live("status() initializes formatter state per directory", () =>
  107. Effect.gen(function* () {
  108. const a = yield* provideTmpdirInstance(() => Format.use.status(), {
  109. config: { formatter: false },
  110. })
  111. const b = yield* provideTmpdirInstance(() => Format.use.status(), {
  112. config: {
  113. formatter: true,
  114. },
  115. })
  116. expect(a).toEqual([])
  117. expect(b.find((item) => item.name === "gofmt")).toBeDefined()
  118. }),
  119. )
  120. it.instance(
  121. "runs enabled checks for matching formatters in parallel",
  122. () =>
  123. Effect.gen(function* () {
  124. const test = yield* TestInstance
  125. const file = `${test.directory}/test.parallel`
  126. yield* Effect.promise(() => Bun.write(file, "x"))
  127. const one = {
  128. extensions: Formatter.gofmt.extensions,
  129. enabled: Formatter.gofmt.enabled,
  130. }
  131. const two = {
  132. extensions: Formatter.mix.extensions,
  133. enabled: Formatter.mix.enabled,
  134. }
  135. let active = 0
  136. let max = 0
  137. yield* Effect.acquireUseRelease(
  138. Effect.sync(() => {
  139. Formatter.gofmt.extensions = [".parallel"]
  140. Formatter.mix.extensions = [".parallel"]
  141. Formatter.gofmt.enabled = async () => {
  142. active++
  143. max = Math.max(max, active)
  144. await Promise.resolve()
  145. active--
  146. return ["sh", "-c", "true"]
  147. }
  148. Formatter.mix.enabled = async () => {
  149. active++
  150. max = Math.max(max, active)
  151. await Promise.resolve()
  152. active--
  153. return ["sh", "-c", "true"]
  154. }
  155. }),
  156. () =>
  157. Format.Service.use((fmt) =>
  158. Effect.gen(function* () {
  159. yield* fmt.init()
  160. yield* fmt.file(file)
  161. }),
  162. ),
  163. () =>
  164. Effect.sync(() => {
  165. Formatter.gofmt.extensions = one.extensions
  166. Formatter.gofmt.enabled = one.enabled
  167. Formatter.mix.extensions = two.extensions
  168. Formatter.mix.enabled = two.enabled
  169. }),
  170. )
  171. expect(max).toBe(2)
  172. }),
  173. { config: { formatter: { gofmt: {}, mix: {} } } },
  174. )
  175. it.instance(
  176. "runs matching formatters sequentially for the same file",
  177. () =>
  178. Effect.gen(function* () {
  179. const test = yield* TestInstance
  180. const file = `${test.directory}/test.seq`
  181. yield* Effect.promise(() => Bun.write(file, "x"))
  182. yield* Format.Service.use((fmt) =>
  183. Effect.gen(function* () {
  184. yield* fmt.init()
  185. expect(yield* fmt.file(file)).toBe(true)
  186. }),
  187. )
  188. expect(yield* Effect.promise(() => Bun.file(file).text())).toBe("xAB")
  189. }),
  190. {
  191. config: {
  192. formatter: {
  193. first: {
  194. command: [
  195. "node",
  196. "-e",
  197. "const fs = require('fs'); const file = process.argv[1]; fs.writeFileSync(file, fs.readFileSync(file, 'utf8') + 'A')",
  198. "$FILE",
  199. ],
  200. extensions: [".seq"],
  201. },
  202. second: {
  203. command: [
  204. "node",
  205. "-e",
  206. "const fs = require('fs'); const file = process.argv[1]; fs.writeFileSync(file, fs.readFileSync(file, 'utf8') + 'B')",
  207. "$FILE",
  208. ],
  209. extensions: [".seq"],
  210. },
  211. },
  212. },
  213. },
  214. )
  215. })