@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
22import { createServer } from "node:net" ;
33import type { Server } from "node:net" ;
44import { Effect } from "effect" ;
5- import { allocatePorts , DEFAULT_PORTS } from "./PortAllocator.ts" ;
5+ import { allocatePorts , DEFAULT_PORTS , PortAllocationError } from "./PortAllocator.ts" ;
66
77const listen = ( port : number ) =>
88 Effect . callback < Server , Error > ( ( resume ) => {
@@ -22,20 +22,6 @@ const close = (server: Server) =>
2222 return Effect . void ;
2323 } ) ;
2424
25- const getFreePort = ( ) =>
26- Effect . acquireUseRelease (
27- listen ( 0 ) ,
28- ( server ) =>
29- Effect . sync ( ( ) => {
30- const addr = server . address ( ) ;
31- if ( addr == null || typeof addr === "string" ) {
32- throw new Error ( "Expected TCP server address" ) ;
33- }
34- return addr . port ;
35- } ) ,
36- close ,
37- ) ;
38-
3925/** Occupy an OS-assigned port for the duration of a scoped effect. */
4026const occupyFreePort = ( ) =>
4127 Effect . acquireRelease (
@@ -49,9 +35,45 @@ const occupyFreePort = () =>
4935 ( { server } ) => close ( server ) ,
5036 ) ;
5137
38+ const fakePortProbe = (
39+ options : {
40+ readonly unavailable ?: ReadonlySet < number > ;
41+ readonly randomPorts ?: readonly number [ ] ;
42+ } = { } ,
43+ ) => {
44+ const unavailable = options . unavailable ?? new Set < number > ( ) ;
45+ const randomPorts =
46+ options . randomPorts ?? Array . from ( { length : 100 } , ( _ , index ) => 30001 + index ) ;
47+ let randomIndex = 0 ;
48+
49+ return {
50+ exact : ( port : number ) =>
51+ unavailable . has ( port )
52+ ? Effect . fail ( new PortAllocationError ( { detail : `Port ${ port } is not available` } ) )
53+ : Effect . succeed ( port ) ,
54+ random : ( exclude : ReadonlySet < number > ) =>
55+ Effect . gen ( function * ( ) {
56+ while ( randomIndex < randomPorts . length ) {
57+ const port = randomPorts [ randomIndex ] ;
58+ randomIndex += 1 ;
59+ if ( port === undefined ) {
60+ continue ;
61+ }
62+ if ( ! exclude . has ( port ) && ! unavailable . has ( port ) ) {
63+ return port ;
64+ }
65+ }
66+
67+ return yield * Effect . fail (
68+ new PortAllocationError ( { detail : "No fake random ports available" } ) ,
69+ ) ;
70+ } ) ,
71+ } ;
72+ } ;
73+
5274describe ( "allocatePorts" , ( ) => {
5375 it ( "all allocated ports are unique" , async ( ) => {
54- const ports = await Effect . runPromise ( allocatePorts ( { } ) ) ;
76+ const ports = await Effect . runPromise ( allocatePorts ( { } , { probe : fakePortProbe ( ) } ) ) ;
5577 const values = Object . values ( ports ) as number [ ] ;
5678 const unique = new Set ( values ) ;
5779 expect ( unique . size ) . toBe ( values . length ) ;
@@ -61,9 +83,11 @@ describe("allocatePorts", () => {
6183 } ) ;
6284
6385 it ( "reserved ports are skipped by later allocations" , async ( ) => {
64- const a = await Effect . runPromise ( allocatePorts ( { } ) ) ;
86+ const a = await Effect . runPromise ( allocatePorts ( { } , { probe : fakePortProbe ( ) } ) ) ;
6587 const aPorts = new Set ( Object . values ( a ) as number [ ] ) ;
66- const b = await Effect . runPromise ( allocatePorts ( { } , { reserved : aPorts } ) ) ;
88+ const b = await Effect . runPromise (
89+ allocatePorts ( { } , { reserved : aPorts , probe : fakePortProbe ( ) } ) ,
90+ ) ;
6791 const bPorts = Object . values ( b ) as number [ ] ;
6892
6993 for ( const port of bPorts ) {
@@ -72,10 +96,13 @@ describe("allocatePorts", () => {
7296 } ) ;
7397
7498 it ( "explicit port is respected when available" , async ( ) => {
75- const requestedApiPort = await Effect . runPromise ( getFreePort ( ) ) ;
76- const requestedDbPort = await Effect . runPromise ( getFreePort ( ) ) ;
99+ const requestedApiPort = 21001 ;
100+ const requestedDbPort = 21002 ;
77101 const ports = await Effect . runPromise (
78- allocatePorts ( { apiPort : requestedApiPort , dbPort : requestedDbPort } ) ,
102+ allocatePorts (
103+ { apiPort : requestedApiPort , dbPort : requestedDbPort } ,
104+ { probe : fakePortProbe ( ) } ,
105+ ) ,
79106 ) ;
80107 expect ( ports . apiPort ) . toBe ( requestedApiPort ) ;
81108 expect ( ports . dbPort ) . toBe ( requestedDbPort ) ;
@@ -99,9 +126,9 @@ describe("allocatePorts", () => {
99126 } ) ;
100127
101128 it ( "preferred ports are reused when available" , async ( ) => {
102- const apiPort = await Effect . runPromise ( getFreePort ( ) ) ;
103- const dbPort = await Effect . runPromise ( getFreePort ( ) ) ;
104- const studioPort = await Effect . runPromise ( getFreePort ( ) ) ;
129+ const apiPort = 21003 ;
130+ const dbPort = 21004 ;
131+ const studioPort = 21005 ;
105132 const ports = await Effect . runPromise (
106133 allocatePorts (
107134 { } ,
@@ -111,6 +138,7 @@ describe("allocatePorts", () => {
111138 dbPort,
112139 studioPort,
113140 } ,
141+ probe : fakePortProbe ( ) ,
114142 } ,
115143 ) ,
116144 ) ;
@@ -121,27 +149,26 @@ describe("allocatePorts", () => {
121149 } ) ;
122150
123151 it ( "preferred ports fall back to random ports when unavailable" , async ( ) => {
124- const dbPort = await Effect . runPromise ( getFreePort ( ) ) ;
125- const exit = await Effect . runPromise (
126- Effect . scoped (
127- Effect . gen ( function * ( ) {
128- const occupied = yield * occupyFreePort ( ) ;
129-
130- return yield * allocatePorts (
131- { } ,
132- {
133- preferred : {
134- apiPort : occupied . port ,
135- dbPort,
136- } ,
137- } ,
138- ) ;
139- } ) ,
152+ const apiPort = 21006 ;
153+ const dbPort = 21007 ;
154+ const ports = await Effect . runPromise (
155+ allocatePorts (
156+ { } ,
157+ {
158+ preferred : {
159+ apiPort,
160+ dbPort,
161+ } ,
162+ probe : fakePortProbe ( {
163+ unavailable : new Set ( [ apiPort ] ) ,
164+ randomPorts : Array . from ( { length : 20 } , ( _ , index ) => 31001 + index ) ,
165+ } ) ,
166+ } ,
140167 ) ,
141168 ) ;
142169
143- expect ( exit . apiPort ) . not . toBe ( exit . dbPort ) ;
144- expect ( exit . dbPort ) . toBe ( dbPort ) ;
170+ expect ( ports . apiPort ) . toBe ( 31001 ) ;
171+ expect ( ports . dbPort ) . toBe ( dbPort ) ;
145172 } ) ;
146173
147174 it ( "explicit ports cannot override reserved ownership" , async ( ) => {
@@ -170,6 +197,7 @@ describe("allocatePorts", () => {
170197 apiPort : 23001 ,
171198 } ,
172199 reserved : new Set ( [ 23001 ] ) ,
200+ probe : fakePortProbe ( ) ,
173201 } ,
174202 ) ,
175203 ) ;
0 commit comments