Skip to content

Commit 77745ac

Browse files
authored
Merge pull request #1724 from fsprojects/repo-assist/eng-fix-windows-ci-port-binding-2026-04-03-003461bcee6010de
[Repo Assist] test: use TcpListener(0) for reliable free-port selection in HTTP test server
2 parents 5f11283 + 87d8606 commit 77745ac

File tree

1 file changed

+14
-24
lines changed
  • tests/FSharp.Data.Core.Tests

1 file changed

+14
-24
lines changed

tests/FSharp.Data.Core.Tests/Http.fs

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ open System.Text
1111
open System.Threading.Tasks
1212
open Microsoft.AspNetCore.Builder
1313
open Microsoft.AspNetCore.Http
14-
open System.Net.NetworkInformation
14+
open System.Net.Sockets
1515

1616
type ITestHttpServer =
1717
inherit IDisposable
@@ -47,14 +47,14 @@ let startHttpLocalServer() =
4747
} |> Async.StartAsTask :> Task
4848
)) |> ignore
4949

50+
// Use TcpListener(0) to ask the OS for a free port, then release it.
51+
// The TOCTOU window (between Stop and Kestrel's bind) is microseconds,
52+
// far more reliable than the previous random-port-then-check approach.
5053
let freePort =
51-
let random = new System.Random()
52-
let mutable port = random.Next(10000, 65000) // Use a random high port instead of a fixed port
53-
while
54-
IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners()
55-
|> Array.map (fun x -> x.Port)
56-
|> Array.contains port do
57-
port <- random.Next(10000, 65000)
54+
let listener = new TcpListener(System.Net.IPAddress.Loopback, 0)
55+
listener.Start()
56+
let port = (listener.LocalEndpoint :?> System.Net.IPEndPoint).Port
57+
listener.Stop()
5858
port
5959

6060
let baseAddress = $"http://127.0.0.1:{freePort}"
@@ -64,7 +64,7 @@ let startHttpLocalServer() =
6464

6565
{ new ITestHttpServer with
6666
member this.Dispose() =
67-
app.StopAsync() |> Async.AwaitTask |> ignore
67+
app.StopAsync() |> Async.AwaitTask |> Async.RunSynchronously
6868
printfn $"Stopped local http server with address {baseAddress}"
6969
member this.WorkerTask = workerTask
7070
member this.BaseAddress = baseAddress }
@@ -294,22 +294,12 @@ let testFormDataBodySize (size: int) =
294294

295295
[<Test; TestCaseSource("testFormDataSizesInBytes")>]
296296
let testMultipartFormDataBodySize (size: int) =
297-
// Skip this test on Windows when running in CI because of flaky port binding behavior on some Windows CI agents.
298-
let isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)
299-
let inCi =
300-
let env v = Environment.GetEnvironmentVariable v
301-
[ "CI"; "GITHUB_ACTIONS"; "TF_BUILD"; "APPVEYOR"; "GITLAB_CI"; "JENKINS_URL" ]
302-
|> List.exists (fun e -> not (String.IsNullOrEmpty (env e)))
303-
304-
if isWindows && inCi then
305-
Assert.Ignore("Skipping test on Windows in CI")
306-
else
307-
use localServer = startHttpLocalServer()
308-
let bodyString = seq {for _i in 0..size -> "x\n"} |> String.concat ""
309-
let multipartItem = [ MultipartItem("input", "input.txt", new MemoryStream(Encoding.UTF8.GetBytes(bodyString)) :> Stream) ]
310-
let body = Multipart(Guid.NewGuid().ToString(), multipartItem)
297+
use localServer = startHttpLocalServer()
298+
let bodyString = seq {for _i in 0..size -> "x\n"} |> String.concat ""
299+
let multipartItem = [ MultipartItem("input", "input.txt", new MemoryStream(Encoding.UTF8.GetBytes(bodyString)) :> Stream) ]
300+
let body = Multipart(Guid.NewGuid().ToString(), multipartItem)
311301

312-
Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _)
302+
Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _)
313303

314304
[<Test>]
315305
let ``escaping of url parameters`` () =

0 commit comments

Comments
 (0)