@moonwall/cli
Version:
Testing framework for the Moon family of projects
136 lines (131 loc) • 4.42 kB
JavaScript
// src/internal/effect/__tests__/PortDiscoveryService.test.ts
import { describe, it, expect } from "vitest";
import { Effect as Effect2, Exit } from "effect";
// src/internal/effect/PortDiscoveryService.ts
import { Effect, Context, Layer, Schedule } from "effect";
import { exec } from "child_process";
import { promisify } from "util";
// src/internal/effect/errors.ts
import { Data } from "effect";
var PortDiscoveryError = class extends Data.TaggedError("PortDiscoveryError") {
};
var NodeLaunchError = class extends Data.TaggedError("NodeLaunchError") {
};
var NodeReadinessError = class extends Data.TaggedError("NodeReadinessError") {
};
var ProcessError = class extends Data.TaggedError("ProcessError") {
};
var StartupCacheError = class extends Data.TaggedError("StartupCacheError") {
};
var FileLockError = class extends Data.TaggedError("FileLockError") {
};
// src/internal/effect/PortDiscoveryService.ts
var execAsync = promisify(exec);
var PortDiscoveryService = class extends Context.Tag("PortDiscoveryService")() {
};
var parsePortsFromLsof = (stdout) => {
const ports = [];
const lines = stdout.split("\n");
for (const line of lines) {
const regex = /(?:.+):(\d+)/;
const match = line.match(regex);
if (match) {
ports.push(Number.parseInt(match[1], 10));
}
}
return ports;
};
var attemptPortDiscovery = (pid) => Effect.tryPromise({
try: () => execAsync(`lsof -p ${pid} -n -P | grep LISTEN`),
catch: (cause) => new PortDiscoveryError({
cause,
pid,
attempts: 1
})
}).pipe(
Effect.flatMap(({ stdout }) => {
const ports = parsePortsFromLsof(stdout);
if (ports.length === 0) {
return Effect.fail(
new PortDiscoveryError({
cause: new Error("No ports found in lsof output"),
pid,
attempts: 1
})
);
}
const rpcPort = ports.find((p) => p !== 30333 && p !== 9615);
if (!rpcPort) {
if (ports.length === 1) {
return Effect.succeed(ports[0]);
}
return Effect.fail(
new PortDiscoveryError({
cause: new Error(
`No RPC port found in range 9000-20000 and multiple ports detected (found ports: ${ports.join(", ")})`
),
pid,
attempts: 1
})
);
}
return Effect.succeed(rpcPort);
})
);
var discoverPortWithRetry = (pid, maxAttempts = 1200) => attemptPortDiscovery(pid).pipe(
Effect.retry(
Schedule.fixed("50 millis").pipe(Schedule.compose(Schedule.recurs(maxAttempts - 1)))
),
Effect.catchAll(
(error) => Effect.fail(
new PortDiscoveryError({
cause: error,
pid,
attempts: maxAttempts
})
)
)
);
var PortDiscoveryServiceLive = Layer.succeed(PortDiscoveryService, {
discoverPort: discoverPortWithRetry
});
// src/internal/effect/__tests__/PortDiscoveryService.test.ts
describe("PortDiscoveryService", () => {
it("should fail with PortDiscoveryError for invalid PID", async () => {
const program = PortDiscoveryService.pipe(
Effect2.flatMap((service) => service.discoverPort(99999999, 3)),
// Invalid PID, 3 attempts
Effect2.provide(PortDiscoveryServiceLive)
);
const exit = await Effect2.runPromiseExit(program);
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(exit.cause._tag).toBe("Fail");
if (exit.cause._tag === "Fail") {
expect(exit.cause.error).toBeInstanceOf(PortDiscoveryError);
expect(exit.cause.error.pid).toBe(99999999);
}
}
});
it("should discover port for current process", async () => {
const currentPid = process.pid;
const program = PortDiscoveryService.pipe(
Effect2.flatMap((service) => service.discoverPort(currentPid, 5)),
// Fewer attempts for speed
Effect2.provide(PortDiscoveryServiceLive)
);
const exit = await Effect2.runPromiseExit(program);
expect(exit._tag).toMatch(/Success|Failure/);
});
it("should include attempt count in error", async () => {
const maxAttempts = 3;
const program = PortDiscoveryService.pipe(
Effect2.flatMap((service) => service.discoverPort(99999999, maxAttempts)),
Effect2.provide(PortDiscoveryServiceLive)
);
const exit = await Effect2.runPromiseExit(program);
if (Exit.isFailure(exit) && exit.cause._tag === "Fail") {
expect(exit.cause.error.attempts).toBe(maxAttempts);
}
});
});