Skip to content

Commit b8f94ba

Browse files
committed
Add type wrapper kind safety tests using F# scripts
Introduce a new test suite to verify compile-time enforcement of input/output directionality for ListOf, Nullable, and StructNullable wrappers. Replace previous inline tests with F# script-based checks that are executed via dotnet fsi. Update the test project to include these scripts as content files and ensure References.fsx is managed and ignored appropriately.
1 parent 8207578 commit b8f94ba

10 files changed

Lines changed: 259 additions & 30 deletions

tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,18 @@
3838
<Compile Include="Helpers and Extensions\ObservableExtensionsTests.fs" />
3939
<Compile Include="PropertyTrackingTests.fs" />
4040
<Compile Include="AbstractionTests.fs" />
41+
<Compile Include="TypeWrappersKindSafetyTests.fs" />
4142
<Compile Include="UnionInterfaceTests.fs" />
4243
<Compile Include="DirectivesTests.fs" />
4344
<Compile Include="TypeValidationTests.fs" />
44-
<Compile Include="TypeWrappersKindSafetyTests.fs" />
45+
<None Include="TypeWrappersKindSafety\References.fsx" Condition="exists('TypeWrappersKindSafety\References.fsx')" CopyToOutputDirectory="PreserveNewest" />
46+
<None Include="TypeWrappersKindSafety\ListOf.InputAsOutput.fsx" CopyToOutputDirectory="PreserveNewest" />
47+
<None Include="TypeWrappersKindSafety\ListOf.OutputAsInput.fsx" CopyToOutputDirectory="PreserveNewest" />
48+
<None Include="TypeWrappersKindSafety\Nullable.InputAsOutput.fsx" CopyToOutputDirectory="PreserveNewest" />
49+
<None Include="TypeWrappersKindSafety\Nullable.OutputAsInput.fsx" CopyToOutputDirectory="PreserveNewest" />
50+
<None Include="TypeWrappersKindSafety\StructNullable.InputAsOutput.fsx" CopyToOutputDirectory="PreserveNewest" />
51+
<None Include="TypeWrappersKindSafety\StructNullable.OutputAsInput.fsx" CopyToOutputDirectory="PreserveNewest" />
52+
<None Include="TypeWrappersKindSafety\Valid.fsx" CopyToOutputDirectory="PreserveNewest" />
4553
<Compile Include="AstValidationTests.fs" />
4654
<Compile Include="ParserTests.fs" />
4755
<Compile Include="SchemaTests.fs" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
References.fsx
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let inputOnlyType =
9+
Define.InputObject<InputOnly>(
10+
name = "InputOnlyType",
11+
fields = [ Define.Input("value", IntType) ]
12+
)
13+
14+
// This should fail: InputDef cannot be assigned to OutputDef
15+
let _ : OutputDef<InputOnly list> = ListOf inputOnlyType
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let outputOnlyType =
9+
Define.Object<OutputOnly>(
10+
name = "OutputOnlyType",
11+
fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ]
12+
)
13+
14+
// This should fail: OutputDef cannot be assigned to InputDef
15+
let _ : InputDef<OutputOnly list> = ListOf outputOnlyType
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let inputOnlyType =
9+
Define.InputObject<InputOnly>(
10+
name = "InputOnlyType",
11+
fields = [ Define.Input("value", IntType) ]
12+
)
13+
14+
// This should fail: InputDef cannot be assigned to OutputDef
15+
let _ : OutputDef<InputOnly option> = Nullable inputOnlyType
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let outputOnlyType =
9+
Define.Object<OutputOnly>(
10+
name = "OutputOnlyType",
11+
fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ]
12+
)
13+
14+
// This should fail: OutputDef cannot be assigned to InputDef
15+
let _ : InputDef<OutputOnly option> = Nullable outputOnlyType
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let inputOnlyType =
9+
Define.InputObject<InputOnly>(
10+
name = "InputOnlyType",
11+
fields = [ Define.Input("value", IntType) ]
12+
)
13+
14+
// This should fail: InputDef cannot be assigned to OutputDef
15+
let _ : OutputDef<InputOnly voption> = StructNullable inputOnlyType
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let outputOnlyType =
9+
Define.Object<OutputOnly>(
10+
name = "OutputOnlyType",
11+
fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ]
12+
)
13+
14+
// This should fail: OutputDef cannot be assigned to InputDef
15+
let _ : InputDef<OutputOnly voption> = StructNullable outputOnlyType
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#load "References.fsx"
2+
3+
open FSharp.Data.GraphQL.Types
4+
5+
type InputOnly = { Value: int }
6+
type OutputOnly = { Value: int }
7+
8+
let inputOnlyType =
9+
Define.InputObject<InputOnly>(
10+
name = "InputOnlyType",
11+
fields = [ Define.Input("value", IntType) ]
12+
)
13+
14+
let outputOnlyType =
15+
Define.Object<OutputOnly>(
16+
name = "OutputOnlyType",
17+
fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ]
18+
)
19+
20+
// These are all valid assignments and must compile successfully
21+
let _inputList : InputDef<InputOnly list> = ListOf inputOnlyType
22+
let _outputList : OutputDef<OutputOnly list> = ListOf outputOnlyType
23+
let _inputNullable : InputDef<InputOnly option> = Nullable inputOnlyType
24+
let _outputNullable : OutputDef<OutputOnly option> = Nullable outputOnlyType
25+
let _inputStruct : InputDef<InputOnly voption> = StructNullable inputOnlyType
26+
let _outputStruct : OutputDef<OutputOnly voption> = StructNullable outputOnlyType
Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,144 @@
11
module FSharp.Data.GraphQL.Tests.TypeWrappersKindSafetyTests
22

3+
open System
4+
open System.Diagnostics
5+
open System.Threading.Tasks
36
open FSharp.Data.GraphQL.Types
47
open Xunit
58

69
type private InputOnly = { Value : int }
710
type private OutputOnly = { Value : int }
811

912
let private InputOnlyType =
10-
Define.InputObject<InputOnly>(
11-
name = "InputOnlyType",
12-
fields = [ Define.Input("value", IntType) ]
13-
)
13+
Define.InputObject<InputOnly> (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ])
1414

1515
let private OutputOnlyType =
16-
Define.Object<OutputOnly>(
17-
name = "OutputOnlyType",
18-
fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ]
19-
)
20-
21-
[<Fact>]
22-
let ``ListOf keeps input-output direction`` () =
23-
let inputList : InputDef<InputOnly list> = ListOf InputOnlyType
24-
let outputList : OutputDef<OutputOnly list> = ListOf OutputOnlyType
25-
Assert.Equal ("[InputOnlyType!]!", inputList.ToString ())
26-
Assert.Equal ("[OutputOnlyType!]!", outputList.ToString ())
27-
28-
[<Fact>]
29-
let ``Nullable keeps input-output direction`` () =
30-
let nullableInput : InputDef<InputOnly option> = Nullable InputOnlyType
31-
let nullableOutput : OutputDef<OutputOnly option> = Nullable OutputOnlyType
32-
Assert.Equal ("InputOnlyType", nullableInput.ToString ())
33-
Assert.Equal ("OutputOnlyType", nullableOutput.ToString ())
34-
35-
[<Fact>]
36-
let ``StructNullable keeps input-output direction`` () =
37-
let nullableInput : InputDef<InputOnly voption> = StructNullable InputOnlyType
38-
let nullableOutput : OutputDef<OutputOnly voption> = StructNullable OutputOnlyType
39-
Assert.Equal ("InputOnlyType", nullableInput.ToString ())
40-
Assert.Equal ("OutputOnlyType", nullableOutput.ToString ())
16+
Define.Object<OutputOnly> (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ])
17+
18+
type TypeWrappersKindSafetyFixture () =
19+
20+
let scriptsDir = IO.Path.Combine (AppContext.BaseDirectory, "TypeWrappersKindSafety")
21+
let referencesPath = IO.Path.Combine (scriptsDir, "References.fsx")
22+
let sourceProjectDir =
23+
IO.Path.GetFullPath (IO.Path.Combine (AppContext.BaseDirectory, "..", "..", ".."))
24+
let sourceScriptsDir = IO.Path.Combine (sourceProjectDir, "TypeWrappersKindSafety")
25+
let sourceReferencesPath = IO.Path.Combine (sourceScriptsDir, "References.fsx")
26+
27+
let ensureFileContentAsync (path : string) (content : string) : Task = task {
28+
if IO.File.Exists (path) then
29+
let! existing = IO.File.ReadAllTextAsync (path)
30+
31+
if not (String.Equals (existing, content, StringComparison.Ordinal)) then
32+
do! IO.File.WriteAllTextAsync (path, content)
33+
else
34+
do! IO.File.WriteAllTextAsync (path, content)
35+
}
36+
37+
member _.ScriptPath (name : string) = IO.Path.Combine (scriptsDir, name)
38+
39+
member _.RunFsiCheckAsync (scriptPath : string) : Task<int> = task {
40+
let psi =
41+
ProcessStartInfo (
42+
"dotnet",
43+
sprintf "fsi --noninteractive \"%s\"" scriptPath,
44+
UseShellExecute = false,
45+
RedirectStandardOutput = true,
46+
RedirectStandardError = true,
47+
WorkingDirectory = AppContext.BaseDirectory
48+
)
49+
50+
use proc = Process.Start (psi)
51+
do! proc.WaitForExitAsync ()
52+
return proc.ExitCode
53+
}
54+
55+
member private _.ReferencesContent =
56+
let sharedAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Shared.dll")
57+
let serverAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Server.dll")
58+
59+
[ sprintf "#r @\"%s\"" sharedAssembly; sprintf "#r @\"%s\"" serverAssembly ]
60+
|> String.concat "\n"
61+
62+
interface IAsyncLifetime with
63+
64+
member this.InitializeAsync () : Task =
65+
task {
66+
IO.Directory.CreateDirectory (scriptsDir) |> ignore
67+
IO.Directory.CreateDirectory (sourceScriptsDir) |> ignore
68+
69+
let content = this.ReferencesContent
70+
71+
do! ensureFileContentAsync referencesPath content
72+
do! ensureFileContentAsync sourceReferencesPath content
73+
}
74+
75+
member _.DisposeAsync () = Task.CompletedTask
76+
77+
type TypeWrappersKindSafetyTests (fixture : TypeWrappersKindSafetyFixture) =
78+
interface IClassFixture<TypeWrappersKindSafetyFixture>
79+
80+
[<Fact>]
81+
member _.``ListOf keeps input-output direction`` () : Task = task {
82+
let inputList : InputDef<InputOnly list> = ListOf InputOnlyType
83+
let outputList : OutputDef<OutputOnly list> = ListOf OutputOnlyType
84+
Assert.Equal ("[InputOnlyType!]!", inputList.ToString ())
85+
Assert.Equal ("[OutputOnlyType!]!", outputList.ToString ())
86+
}
87+
88+
[<Fact>]
89+
member _.``Nullable keeps input-output direction`` () : Task = task {
90+
let nullableInput : InputDef<InputOnly option> = Nullable InputOnlyType
91+
let nullableOutput : OutputDef<OutputOnly option> = Nullable OutputOnlyType
92+
Assert.Equal ("InputOnlyType", nullableInput.ToString ())
93+
Assert.Equal ("OutputOnlyType", nullableOutput.ToString ())
94+
}
95+
96+
[<Fact>]
97+
member _.``StructNullable keeps input-output direction`` () : Task = task {
98+
let nullableInput : InputDef<InputOnly voption> = StructNullable InputOnlyType
99+
let nullableOutput : OutputDef<OutputOnly voption> = StructNullable OutputOnlyType
100+
Assert.Equal ("InputOnlyType", nullableInput.ToString ())
101+
Assert.Equal ("OutputOnlyType", nullableOutput.ToString ())
102+
}
103+
104+
[<Fact>]
105+
member _.``Valid script compiles successfully`` () : Task = task {
106+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Valid.fsx"))
107+
Assert.Equal (0, exitCode)
108+
}
109+
110+
[<Fact>]
111+
member _.``ListOf rejects output type as input at compile time`` () : Task = task {
112+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.OutputAsInput.fsx"))
113+
Assert.NotEqual (0, exitCode)
114+
}
115+
116+
[<Fact>]
117+
member _.``ListOf rejects input type as output at compile time`` () : Task = task {
118+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.InputAsOutput.fsx"))
119+
Assert.NotEqual (0, exitCode)
120+
}
121+
122+
[<Fact>]
123+
member _.``Nullable rejects output type as input at compile time`` () : Task = task {
124+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.OutputAsInput.fsx"))
125+
Assert.NotEqual (0, exitCode)
126+
}
127+
128+
[<Fact>]
129+
member _.``Nullable rejects input type as output at compile time`` () : Task = task {
130+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.InputAsOutput.fsx"))
131+
Assert.NotEqual (0, exitCode)
132+
}
133+
134+
[<Fact>]
135+
member _.``StructNullable rejects output type as input at compile time`` () : Task = task {
136+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.OutputAsInput.fsx"))
137+
Assert.NotEqual (0, exitCode)
138+
}
139+
140+
[<Fact>]
141+
member _.``StructNullable rejects input type as output at compile time`` () : Task = task {
142+
let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.InputAsOutput.fsx"))
143+
Assert.NotEqual (0, exitCode)
144+
}

0 commit comments

Comments
 (0)