From 1c648e8fedc1bdd18f074c047d7eb3dfd0f7a79c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 04:50:53 +0000 Subject: [PATCH 1/2] test: use TcpListener(0) for reliable free-port selection in HTTP test server Replace the random-port-plus-check approach with TcpListener(0), which asks the OS to assign a free port. The TOCTOU window (between TcpListener.Stop() and Kestrel's bind) is microseconds, far more reliable than the previous approach that randomly selected a port from 10000-65000 and then checked IPGlobalProperties.GetActiveTcpListeners(). Also removes the Windows-CI skip workaround from testMultipartFormDataBodySize, since the root cause (flaky port selection) is now fixed. Also fixes Dispose() to properly await StopAsync instead of fire-and-forget. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/FSharp.Data.Core.Tests/Http.fs | 38 ++++++++++------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/tests/FSharp.Data.Core.Tests/Http.fs b/tests/FSharp.Data.Core.Tests/Http.fs index a8524f121..f636b40e6 100644 --- a/tests/FSharp.Data.Core.Tests/Http.fs +++ b/tests/FSharp.Data.Core.Tests/Http.fs @@ -11,7 +11,7 @@ open System.Text open System.Threading.Tasks open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Http -open System.Net.NetworkInformation +open System.Net.Sockets type ITestHttpServer = inherit IDisposable @@ -47,14 +47,14 @@ let startHttpLocalServer() = } |> Async.StartAsTask :> Task )) |> ignore + // Use TcpListener(0) to ask the OS for a free port, then release it. + // The TOCTOU window (between Stop and Kestrel's bind) is microseconds, + // far more reliable than the previous random-port-then-check approach. let freePort = - let random = new System.Random() - let mutable port = random.Next(10000, 65000) // Use a random high port instead of a fixed port - while - IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners() - |> Array.map (fun x -> x.Port) - |> Array.contains port do - port <- random.Next(10000, 65000) + let listener = new TcpListener(System.Net.IPAddress.Loopback, 0) + listener.Start() + let port = (listener.LocalEndpoint :?> System.Net.IPEndPoint).Port + listener.Stop() port let baseAddress = $"http://127.0.0.1:{freePort}" @@ -64,7 +64,7 @@ let startHttpLocalServer() = { new ITestHttpServer with member this.Dispose() = - app.StopAsync() |> Async.AwaitTask |> ignore + app.StopAsync() |> Async.AwaitTask |> Async.RunSynchronously printfn $"Stopped local http server with address {baseAddress}" member this.WorkerTask = workerTask member this.BaseAddress = baseAddress } @@ -294,22 +294,12 @@ let testFormDataBodySize (size: int) = [] let testMultipartFormDataBodySize (size: int) = - // Skip this test on Windows when running in CI because of flaky port binding behavior on some Windows CI agents. - let isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) - let inCi = - let env v = Environment.GetEnvironmentVariable v - [ "CI"; "GITHUB_ACTIONS"; "TF_BUILD"; "APPVEYOR"; "GITLAB_CI"; "JENKINS_URL" ] - |> List.exists (fun e -> not (String.IsNullOrEmpty (env e))) - - if isWindows && inCi then - Assert.Ignore("Skipping test on Windows in CI") - else - use localServer = startHttpLocalServer() - let bodyString = seq {for _i in 0..size -> "x\n"} |> String.concat "" - let multipartItem = [ MultipartItem("input", "input.txt", new MemoryStream(Encoding.UTF8.GetBytes(bodyString)) :> Stream) ] - let body = Multipart(Guid.NewGuid().ToString(), multipartItem) + use localServer = startHttpLocalServer() + let bodyString = seq {for _i in 0..size -> "x\n"} |> String.concat "" + let multipartItem = [ MultipartItem("input", "input.txt", new MemoryStream(Encoding.UTF8.GetBytes(bodyString)) :> Stream) ] + let body = Multipart(Guid.NewGuid().ToString(), multipartItem) - Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) + Assert.DoesNotThrowAsync(fun () -> Http.AsyncRequest (url= localServer.BaseAddress + "/200", httpMethod="POST", body=body, timeout = 10000) |> Async.Ignore |> Async.StartAsTask :> _) [] let ``escaping of url parameters`` () = From 87d8606ad7f761411d6b0d910d199495d77ce308 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Apr 2026 04:50:56 +0000 Subject: [PATCH 2/2] ci: trigger checks