1import * as machines from "@distilled.cloud/fly-io/machines";
2import { expect } from "alchemy-test";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Ref from "effect/Ref";
6import * as Result from "effect/Result";
7import * as Schedule from "effect/Schedule";
8import * as Stream from "effect/Stream";
9import * as HttpClient from "effect/http/HttpClient";
10
11export const websiteServices = (path: string) => [
12 {
13 protocol: "tcp" as const,
14 internalPort: 3000,
15 autostop: "off" as const,
16 ports: [
17 { port: 80, handlers: ["http"], forceHttps: true },
18 { port: 443, handlers: ["tls", "http"] },
19 ],
20 checks: [
21 {
22 type: "http" as const,
23 port: 3000,
24 path,
25 interval: "5s",
26 timeout: "2s",
27 },
28 ],
29 },
30];
31
32export const websiteChecks = (path: string) => ({
33 website: {
34 type: "http" as const,
35 port: 3000,
36 path,
37 interval: "5s",
38 timeout: "2s",
39 },
40});
41
42export const getText = (url: string) =>
43 HttpClient.get(url, {
44 headers: { connection: "close", "cache-control": "no-cache" },
45 }).pipe(
46 Effect.flatMap((response) =>
47 response.status === 200
48 ? response.text
49 : Effect.fail(new Error(`Website returned HTTP ${response.status}`)),
50 ),
51 Effect.timeout("10 seconds"),
52 );
53
54export const initialText = (url: string) =>
55 getText(url).pipe(
56 Effect.retry({ times: 8, schedule: Schedule.spaced("2 seconds") }),
57 );
58
59export const startTraffic = (url: string) =>
60 Effect.gen(function* () {
61 const samples = yield* Ref.make<
62 Array<{ body: string } | { failure: string }>
63 >([]);
64 let active = true;
65 const fiber = yield* Stream.range(0, 1799, 1).pipe(
66 Stream.takeWhile(() => active),
67 Stream.runForEach(() =>
68 getText(url).pipe(
69 Effect.result,
70 Effect.flatMap((result) =>
71 Ref.update(samples, (values) => [
72 ...values,
73 Result.isSuccess(result)
74 ? { body: result.success }
75 : { failure: String(result.failure) },
76 ]),
77 ),
78 Effect.andThen(Effect.sleep("500 millis")),
79 ),
80 ),
81 Effect.timeout("15 minutes"),
82 Effect.forkScoped,
83 );
84 const finish = Effect.gen(function* () {
85 yield* Effect.sync(() => {
86 active = false;
87 });
88 yield* Fiber.join(fiber);
89 const values = yield* Ref.get(samples);
90 expect(values.filter((sample) => "failure" in sample)).toEqual([]);
91 return values.flatMap((sample) =>
92 "body" in sample ? [sample.body] : [],
93 );
94 });
95 const waitFor = (predicate: (body: string) => boolean) =>
96 Ref.get(samples).pipe(
97 Effect.repeat({
98 until: (values) =>
99 values.some((sample) => "body" in sample && predicate(sample.body)),
100 times: 8,
101 schedule: Schedule.spaced("500 millis"),
102 }),
103 Effect.tap((values) =>
104 Effect.sync(() => {
105 expect(
106 values.some(
107 (sample) => "body" in sample && predicate(sample.body),
108 ),
109 ).toBe(true);
110 }),
111 ),
112 );
113 return { finish, waitFor };
114 });
115
116export const assertOnlyMachine = (appName: string, machineId: string) =>
117 Effect.gen(function* () {
118 const live = (yield* machines.listMachines({ app_name: appName })).filter(
119 (machine) => machine.state !== "destroyed",
120 );
121 expect(live.map((machine) => machine.id)).toEqual([machineId]);
122 });
123
124export const assertMachineGone = (appName: string, machineId: string) =>
125 Effect.gen(function* () {
126 const gone = yield* machines
127 .getMachine({ app_name: appName, machine_id: machineId })
128 .pipe(
129 Effect.map((machine) => machine.state === "destroyed"),
130 Effect.catchTag("NotFound", () => Effect.succeed(true)),
131 );
132 expect(gone).toBe(true);
133 });
134
135export const assertAppGone = (appName: string) =>
136 Effect.gen(function* () {
137 const gone = yield* machines.getApp({ app_name: appName }).pipe(
138 Effect.as(false),
139 Effect.catchTag("NotFound", () => Effect.succeed(true)),
140 Effect.repeat({
141 until: (value) => value,
142 times: 8,
143 schedule: Schedule.spaced("1 second"),
144 }),
145 );
146 expect(gone).toBe(true);
147 });