From a8ee2649b93beb7485313fcbc75594a489c61ff2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 15 Sep 2026 17:48:34 +0200 Subject: [PATCH 1/4] fixup! Publish CI test results into PR discussions, harden artifact download, and add PR preview publishing (#570) Restore the indentation of IntrospectionUpdateTests.fs The file was re-indented so that `task { ... }` bodies became offside (`let options` stayed at the outer column while the surrounding lines moved in). Both SDK 10.0.303 and 10.0.401 reject it with FS0010/FS0058, so the integration test project has not compiled since. CI stayed green only because it never built the project. The original change was whitespace-only, so this restores the previous version of the file verbatim. Co-Authored-By: Claude Opus 5 (1M context) --- .../IntrospectionUpdateTests.fs | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs index ce0e7eae2..c3931b295 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs @@ -20,9 +20,9 @@ let normalizeJsonDocument options (document : JsonDocument) = JsonDocument.Parse buffer let parseAndNormalizeJsonAsync ct options stream = task { - let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) - return normalizeJsonDocument options document - } + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return normalizeJsonDocument options document +} let areSchemasEqual (document1 : JsonDocument) (document2 : JsonDocument) = let schema1 = document1.RootElement.GetProperty("data").GetProperty ("__schema") @@ -30,56 +30,56 @@ let areSchemasEqual (document1 : JsonDocument) (document2 : JsonDocument) = schema1.GetRawText () = schema2.GetRawText () let readDestinationDocumentAsync ct (stream : FileStream) = task { - try - let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) - return ValueSome document - with :? JsonException -> - return ValueNone - } + try + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return ValueSome document + with :? JsonException -> + return ValueNone +} let updateIntrospectionFileAsync ct sourceStream = task { - use destinationStream = - new FileStream (introspectionFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read) + use destinationStream = + new FileStream (introspectionFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read) let options = JsonWriterOptions (Indented = true) - let! sourceDocument = parseAndNormalizeJsonAsync ct options sourceStream - destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore - let! destinationDocument = readDestinationDocumentAsync ct destinationStream + let! sourceDocument = parseAndNormalizeJsonAsync ct options sourceStream + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + let! destinationDocument = readDestinationDocumentAsync ct destinationStream - let shouldUpdate = - match destinationDocument with - | ValueNone -> true - | ValueSome document -> not (areSchemasEqual document sourceDocument) + let shouldUpdate = + match destinationDocument with + | ValueNone -> true + | ValueSome document -> not (areSchemasEqual document sourceDocument) - if shouldUpdate then - destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore - destinationStream.SetLength 0 - use writer = new Utf8JsonWriter (destinationStream, options) - sourceDocument.WriteTo writer - writer.Flush () + if shouldUpdate then + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + destinationStream.SetLength 0 + use writer = new Utf8JsonWriter (destinationStream, options) + sourceDocument.WriteTo writer + writer.Flush () - return shouldUpdate - } + return shouldUpdate +} [] let ``Get GraphQL introspection response returns schema`` () = task { - use httpClient = TestHosts.createIntegrationHttpClient () + use httpClient = TestHosts.createIntegrationHttpClient () let! response = httpClient.GetFromJsonAsync ("/", CancellationToken.None) let schema = response.GetProperty("data").GetProperty ("__schema") Assert.NotEqual (Unchecked.defaultof, schema) - let hasErrors, _ = response.TryGetProperty "errors" - Assert.False hasErrors - } + let hasErrors, _ = response.TryGetProperty "errors" + Assert.False hasErrors +} [] let ``Update integration introspection file when schema changes`` () = task { - use httpClient = TestHosts.createIntegrationHttpClient () + use httpClient = TestHosts.createIntegrationHttpClient () let! sourceStream = httpClient.GetStreamAsync ("/") - let! wasUpdated = updateIntrospectionFileAsync CancellationToken.None sourceStream + let! wasUpdated = updateIntrospectionFileAsync CancellationToken.None sourceStream Assert.True (File.Exists introspectionFilePath) - if wasUpdated then + if wasUpdated then let! sourceStreamSecondRun = httpClient.GetStreamAsync ("/") - use sourceStreamForVerification = sourceStreamSecondRun - let! wasUpdatedSecondRun = updateIntrospectionFileAsync CancellationToken.None sourceStreamForVerification - Assert.False wasUpdatedSecondRun - } + use sourceStreamForVerification = sourceStreamSecondRun + let! wasUpdatedSecondRun = updateIntrospectionFileAsync CancellationToken.None sourceStreamForVerification + Assert.False wasUpdatedSecondRun +} From fd9394466e4a3bbd0bd94cc98a585c9a7e18bcc5 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 15 Sep 2026 17:50:35 +0200 Subject: [PATCH 2/4] fixup! Publish CI test results into PR discussions, harden artifact download, and add PR preview publishing (#570) Build the integration tests before running them, fail on empty runs `UpdateIntrospectionFile` switched to `dotnet test --no-build`, but nothing restores or builds FSharp.Data.GraphQL.Integration.slnx: the restore target only covers `src/**` and the build target only builds FSharp.Data.GraphQL.slnx. On an unrestored project the test SDK props are not imported, so `dotnet test` runs nothing and exits with 0. The step has been a silent no-op on CI, which let a non-compiling integration test project stay green. - Add a `BuildIntegrationTests` target that builds the integration solution after `Build` (it needs the client provider assembly). - Make `runTests` fail when the run leaves no results file, deleting a stale one first so an earlier run cannot mask it. - Reuse `runTests` for `UpdateIntrospectionFile` instead of repeating the `dotnet test` options, with its own results file name so it does not overwrite other runs. Co-Authored-By: Claude Opus 5 (1M context) --- build/Program.fs | 70 +++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/build/Program.fs b/build/Program.fs index 526b0adaf..27bc21ac8 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -70,9 +70,8 @@ Target.create RestoreTarget <| fun _ -> |> Seq.iter (fun pattern -> DotNet.restore DotNetCli.setRestoreOptions pattern) -let [] BuildTarget = "Build" -Target.create BuildTarget <| fun _ -> - "FSharp.Data.GraphQL.slnx" +let buildSolution (solution : string) = + solution |> DotNet.build (fun options -> { options with Common = options.Common.WithRedirectOutput true |> DotNetCli.setVersion @@ -88,6 +87,14 @@ Target.create BuildTarget <| fun _ -> } }) +let [] BuildTarget = "Build" +Target.create BuildTarget <| fun _ -> buildSolution "FSharp.Data.GraphQL.slnx" + +// The integration tests are not part of FSharp.Data.GraphQL.slnx and are neither restored nor built by BuildTarget. +// They reference the client provider assembly produced by BuildTarget, so this must run after it. +let [] BuildIntegrationTestsTarget = "BuildIntegrationTests" +Target.create BuildIntegrationTestsTarget <| fun _ -> buildSolution "FSharp.Data.GraphQL.Integration.slnx" + let startGraphQLServer (project : string) port (streamRef : DataRef) = CreateProcess.fromRawCommandLine "dotnet" $"run --project {project} --no-build --no-launch-profile --configuration {configurationString} --urls=http://localhost:%i{port}/" |> CreateProcess.withStandardInput (CreatePipe streamRef) @@ -98,9 +105,14 @@ let startGraphQLServer (project : string) port (streamRef : DataRef) = System.Threading.Thread.Sleep (2000) -let runTests (project : string) = - let projectName = Path.GetFileNameWithoutExtension project - let resultsFileName = $"{projectName}.trx" +let [] TestResultsDirectory = "test-results" + +let runTests (project : string) (resultsFileName : string) (filter : string voption) = + let resultsFilePath = TestResultsDirectory resultsFileName + // A stale results file from a previous run must not hide a run that produced none + // (checked first, because File.Delete throws when the results directory does not exist yet) + if File.Exists resultsFilePath then + File.Delete resultsFilePath DotNet.test (fun options -> @@ -108,9 +120,13 @@ let runTests (project : string) = options with NoBuild = true Logger = Some $"trx;LogFileName={resultsFileName}" - ResultsDirectory = Some "test-results" + ResultsDirectory = Some TestResultsDirectory Framework = Some DotNetMoniker Configuration = configuration + Common = { + options.Common with + CustomParams = filter |> ValueOption.map (fun filter -> $"--filter {filter}") |> ValueOption.toOption + } MSBuildParams = { options.MSBuildParams with DisableInternalBinLog = true @@ -126,6 +142,11 @@ let runTests (project : string) = |> _.WithCommon(DotNetCli.setVersion)) project + // `dotnet test --no-build` on a project that was never restored does not import the test SDK, + // so it runs nothing and still exits with 0. The missing results file is the only trace of that. + if not (File.Exists resultsFilePath) then + failwith $"'dotnet test {project}' produced no test results at '{resultsFilePath}'. Was the project restored and built?" + let integrationTestServerProjectPath = "tests" "FSharp.Data.GraphQL.IntegrationTests.Server" @@ -169,36 +190,10 @@ let integrationTestsProjectPath = let [] UpdateIntrospectionFileTarget = "UpdateIntrospectionFile" Target.create UpdateIntrospectionFileTarget <| fun _ -> - let projectName = Path.GetFileNameWithoutExtension integrationTestsProjectPath - let resultsFileName = $"{projectName}.trx" - - DotNet.test - (fun options -> - { - options with - NoBuild = true - Logger = Some $"trx;LogFileName={resultsFileName}" - ResultsDirectory = Some "test-results" - Framework = Some DotNetMoniker - Configuration = configuration - Common = { - options.Common with - CustomParams = Some "--filter FullyQualifiedName~IntrospectionUpdateTests" - } - MSBuildParams = { - options.MSBuildParams with - DisableInternalBinLog = true - Verbosity = Some Normal - Properties = [ - if embedAll then - ("DebugType", "embedded") - ("EmbedAllSources", "true") - ] - } - } - |> _.WithRedirectOutput(true) - |> _.WithCommon(DotNetCli.setVersion)) + runTests integrationTestsProjectPath + "FSharp.Data.GraphQL.IntegrationTests.IntrospectionUpdate.trx" + (ValueSome "FullyQualifiedName~IntrospectionUpdateTests") let unitTestsProjectPath = "tests" @@ -207,7 +202,7 @@ let unitTestsProjectPath = let [] RunUnitTestsTarget = "RunUnitTests" Target.create RunUnitTestsTarget <| fun _ -> - runTests unitTestsProjectPath + runTests unitTestsProjectPath "FSharp.Data.GraphQL.Tests.trx" ValueNone let prepareDocGen () = Shell.rm "docs/release-notes.md" @@ -384,6 +379,7 @@ Target.create "PackAndPush" ignore ==> RestoreTarget ==> BuildTarget ==> RunUnitTestsTarget +==> BuildIntegrationTestsTarget ==> UpdateIntrospectionFileTarget ==> "All" =?> (GenerateDocsTarget, Environment.environVar "GITHUB_ACTIONS" = "True") From 7742cb10042b35c4a994782ba56fa89713affcf4 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 15 Sep 2026 17:51:04 +0200 Subject: [PATCH 3/4] fixup! Migrate integration tests to in-process hosts via `WebApplicationFactory` and remove external server orchestration from build (#564) Run the whole integration test suite again The migration to in-process hosts dropped the `RunIntegrationTests` target and left only `UpdateIntrospectionFile`, which filters to `IntrospectionUpdateTests`. The client provider, operation error and reserved scalar tests have not been run by the build pipeline since. Restore a `RunIntegrationTests` target after `UpdateIntrospectionFile` that runs every other integration test into its own results file. Co-Authored-By: Claude Opus 5 (1M context) --- build/Program.fs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build/Program.fs b/build/Program.fs index 27bc21ac8..05a0de5d0 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -195,6 +195,14 @@ Target.create UpdateIntrospectionFileTarget <| fun _ -> "FSharp.Data.GraphQL.IntegrationTests.IntrospectionUpdate.trx" (ValueSome "FullyQualifiedName~IntrospectionUpdateTests") +// Runs the rest of the integration tests; the introspection update tests already ran in UpdateIntrospectionFileTarget +let [] RunIntegrationTestsTarget = "RunIntegrationTests" +Target.create RunIntegrationTestsTarget <| fun _ -> + runTests + integrationTestsProjectPath + "FSharp.Data.GraphQL.IntegrationTests.trx" + (ValueSome "FullyQualifiedName!~IntrospectionUpdateTests") + let unitTestsProjectPath = "tests" "FSharp.Data.GraphQL.Tests" @@ -381,6 +389,7 @@ Target.create "PackAndPush" ignore ==> RunUnitTestsTarget ==> BuildIntegrationTestsTarget ==> UpdateIntrospectionFileTarget +==> RunIntegrationTestsTarget ==> "All" =?> (GenerateDocsTarget, Environment.environVar "GITHUB_ACTIONS" = "True") |> ignore From bdada17aac88e56830912a5b4079d1067f27f714 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Tue, 15 Sep 2026 18:01:26 +0200 Subject: [PATCH 4/4] Give the remote Swapi provider tests their own operation names SwapiLocalProviderTests and SwapiRemoteProviderTests both instantiate `GraphQLProvider<"introspection.json">` and both declare inline operations named `Q` and `M`. With the F# compiler from SDK 10.0.3xx, which CI is pinned to, a clean build of the integration tests then fails with FS0193 ("type ...Operations.Q.OperationResult is not compatible with type ...Operations.Q.OperationResult"). SDK 10.0.401 compiles it, and reverting FSharp.TypeProviders.SDK to 8.1.0 does not help, so this is a compiler issue rather than a change in this repository. It went unnoticed because CI stopped building the integration tests. Rename the remote operations to `RemoteQ` and `RemoteM`. Co-Authored-By: Claude Opus 5 (1M context) --- .../SwapiRemoteProviderTests.fs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs index bc92cb8a7..f85d2775f 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs @@ -5,6 +5,9 @@ open Helpers open FSharp.Data.GraphQL open System.Threading.Tasks +// SwapiLocalProviderTests instantiates the same provider with the same static arguments. +// The F# compiler in SDK 10.0.3xx fails with FS0193 ("type X is not compatible with type X") when both files +// declare inline operations with the same name, so the operations here are named differently. type Provider = GraphQLProvider<"introspection.json"> let connection = TestHosts.createStarWarsConnection () @@ -15,7 +18,7 @@ type Episode = Provider.Types.Episode module SimpleOperation = let operation = - Provider.Operation<"""query Q { + Provider.Operation<"""query RemoteQ { hero (id: "1000") { name appearsIn @@ -43,7 +46,7 @@ hero (id: "1000") { } }"""> () - type Operation = Provider.Operations.Q + type Operation = Provider.Operations.RemoteQ let validateResult (result : Operation.OperationResult) = result.CustomData.ContainsKey ("documentId") |> equals true @@ -162,7 +165,7 @@ let ``Should be able to use pattern matching methods on an union type`` () = module MutationOperation = let operation = - Provider.Operation<"""mutation M { + Provider.Operation<"""mutation RemoteM { setMoon (id: "1", isMoon: true) { id name @@ -170,7 +173,7 @@ module MutationOperation = } }"""> () - type Operation = Provider.Operations.M + type Operation = Provider.Operations.RemoteM let validateResult (result : Operation.OperationResult) = result.CustomData.ContainsKey ("documentId") |> equals true