Skip to content

Commit c5b423a

Browse files
committed
Implemented foundational test
1 parent 0a138e9 commit c5b423a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+7958
-351
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ N4JET_SKIP_SCHEMA_COLLECTION=false
6060
# Default: 10000
6161
N4JET_BATCH_SIZE=10000
6262

63+
# Enable generation of content-based hash IDs
64+
# Set to false to skip hash computation for better performance
65+
# Default: true
66+
N4JET_ENABLE_HASHED_IDS=true
67+
6368
# ===============================================
6469
# Error Handling and Resilience
6570
# ===============================================

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.17.0
1+
0.19.0

Directory.Build.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>0.17.0</Version>
4-
<AssemblyVersion>0.17.0.0</AssemblyVersion>
5-
<FileVersion>0.17.0.0</FileVersion>
3+
<Version>0.19.0</Version>
4+
<AssemblyVersion>0.19.0.0</AssemblyVersion>
5+
<FileVersion>0.19.0.0</FileVersion>
66
</PropertyGroup>
77
</Project>
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2025-present State Government of Victoria
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
/// FsCheck generators for property-based testing of domain types
24+
module Neo4jExport.Tests.Helpers.Generators
25+
26+
open System
27+
open System.Collections.Generic
28+
open FsCheck
29+
open Neo4jExport
30+
open Neo4jExport.ExportTypes
31+
32+
/// Generator for valid Neo4j labels (non-empty strings without special chars)
33+
let labelGen: Gen<string> =
34+
gen {
35+
let! chars =
36+
Gen.arrayOfLength
37+
10
38+
(Gen.elements [ 'A' .. 'Z' ]
39+
|> Gen.map Char.ToLower)
40+
41+
let label = String(chars)
42+
return if String.IsNullOrWhiteSpace(label) then "Label" else label
43+
}
44+
45+
/// Generator for sets of Neo4j labels
46+
let labelSetGen: Gen<Set<string>> =
47+
gen {
48+
let! count = Gen.choose (0, 5)
49+
let! labels = Gen.listOfLength count labelGen
50+
return Set.ofList labels
51+
}
52+
53+
/// Generator for valid Neo4j property keys
54+
let propertyKeyGen: Gen<string> =
55+
gen {
56+
let! prefix = Gen.elements [ "prop"; "attr"; "field"; "value" ]
57+
let! suffix = Gen.choose (1, 100)
58+
return $"{prefix}{suffix}"
59+
}
60+
61+
/// Generator for Neo4j primitive values
62+
let primitiveValueGen: Gen<obj> =
63+
Gen.oneof
64+
[ Gen.constant (box null)
65+
Gen.map box Arb.generate<bool>
66+
Gen.map box Arb.generate<int64>
67+
Gen.map box (Gen.filter Double.IsFinite Arb.generate<float>) // Only finite values
68+
Gen.map box (Gen.filter (fun s -> s <> null) Arb.generate<string>) ]
69+
70+
/// Generator for Neo4j property dictionaries
71+
let propertyDictGen: Gen<IDictionary<string, obj>> =
72+
gen {
73+
let! count = Gen.choose (0, 10)
74+
let! keys = Gen.listOfLength count propertyKeyGen
75+
let! values = Gen.listOfLength count primitiveValueGen
76+
let dict = Dictionary<string, obj>()
77+
78+
List.zip keys values
79+
|> List.iter (fun (k, v) -> dict.[k] <- v)
80+
81+
return dict :> IDictionary<string, obj>
82+
}
83+
84+
/// Generator for ExportConfig
85+
let exportConfigGen: Gen<ExportConfig> =
86+
gen {
87+
let! batchSize = Gen.choose (100, 50_000)
88+
let! maxMemory = Gen.choose (128, 4096) |> Gen.map int64
89+
let! minDisk = Gen.choose (1, 100) |> Gen.map int64
90+
let! enableHash = Arb.generate<bool>
91+
92+
return
93+
{ Uri = Uri("bolt://localhost:7687")
94+
User = "neo4j"
95+
Password = "testpass"
96+
OutputDirectory = "/tmp/test"
97+
MinDiskGb = minDisk
98+
MaxMemoryMb = maxMemory
99+
SkipSchemaCollection = false
100+
MaxRetries = 3
101+
RetryDelayMs = 1000
102+
MaxRetryDelayMs = 30000
103+
QueryTimeoutSeconds = 300
104+
EnableDebugLogging = false
105+
ValidateJsonOutput = false
106+
AllowInsecure = false
107+
BatchSize = batchSize
108+
JsonBufferSizeKb = 64
109+
MaxPathLength = 100_000L
110+
PathFullModeLimit = 10L
111+
PathCompactModeLimit = 100L
112+
PathPropertyDepth = 2
113+
MaxNestedDepth = 10
114+
NestedShallowModeDepth = 2
115+
NestedReferenceModeDepth = 4
116+
MaxCollectionItems = 1000
117+
MaxLabelsPerNode = 10
118+
MaxLabelsInReferenceMode = 3
119+
MaxLabelsInPathCompact = 5
120+
EnableHashedIds = enableHash }
121+
}
122+
123+
/// Coded error message types for safe testing
124+
type TestErrorMessage =
125+
| StandardError
126+
| ValidationError
127+
| ProcessingError
128+
| ResourceError
129+
| SystemError
130+
| DataError
131+
| OperationError
132+
| ServiceError
133+
| ConfigurationError
134+
| RuntimeError
135+
136+
/// Convert coded error type to string
137+
let testMessageToString =
138+
function
139+
| StandardError -> "Standard error occurred"
140+
| ValidationError -> "Validation failed"
141+
| ProcessingError -> "Processing error"
142+
| ResourceError -> "Resource unavailable"
143+
| SystemError -> "System error"
144+
| DataError -> "Data error encountered"
145+
| OperationError -> "Operation failed"
146+
| ServiceError -> "Service error"
147+
| ConfigurationError -> "Configuration issue"
148+
| RuntimeError -> "Runtime error"
149+
150+
/// Generator for coded error messages
151+
let testErrorMessageGen: Gen<TestErrorMessage> =
152+
Gen.elements
153+
[ StandardError
154+
ValidationError
155+
ProcessingError
156+
ResourceError
157+
SystemError
158+
DataError
159+
OperationError
160+
ServiceError
161+
ConfigurationError
162+
RuntimeError ]
163+
164+
/// Generator for safe error messages that won't trigger content filters
165+
let safeStringGen: Gen<string> =
166+
Gen.map testMessageToString testErrorMessageGen
167+
168+
/// Generator for test paths
169+
let testPathGen: Gen<string> =
170+
Gen.elements
171+
[ "/tmp/test"
172+
"/var/tmp/export"
173+
"/tmp/data"
174+
"/home/test/export"
175+
"/opt/app/data" ]
176+
177+
/// Generator for entity types
178+
let entityTypeGen: Gen<string> =
179+
Gen.elements
180+
[ "Node"
181+
"Relationship"
182+
"Property"
183+
"Label"
184+
"Type" ]
185+
186+
/// Generator for query patterns
187+
let queryPatternGen: Gen<string> =
188+
Gen.elements
189+
[ "MATCH (n)"
190+
"MATCH (n:Label)"
191+
"MATCH ()-[r]->()"
192+
"RETURN n"
193+
"WITH n" ]
194+
195+
/// Generator for AppError variants (now includes all types with safe messages)
196+
let appErrorGen: Gen<AppError> =
197+
Gen.oneof
198+
[ Gen.map ConfigError safeStringGen
199+
Gen.map2
200+
(fun msg exn -> ConnectionError(msg, Some exn))
201+
safeStringGen
202+
(Gen.constant (Exception("Test exception")))
203+
Gen.map AuthenticationError safeStringGen // Now included with safe message
204+
Gen.map3
205+
(fun query msg exn -> QueryError(query, msg, Some exn))
206+
queryPatternGen
207+
safeStringGen
208+
(Gen.constant (Exception("Query test")))
209+
Gen.map3
210+
(fun line msg sample -> DataCorruptionError(line, msg, Some sample))
211+
(Gen.choose (1, 1000))
212+
safeStringGen
213+
safeStringGen
214+
Gen.map2
215+
(fun req avail -> DiskSpaceError(req, avail))
216+
(Gen.choose (1000, 10000) |> Gen.map int64)
217+
(Gen.choose (100, 999) |> Gen.map int64)
218+
Gen.map MemoryError safeStringGen
219+
Gen.map2 (fun msg exn -> ExportError(msg, Some exn)) safeStringGen (Gen.constant (Exception("Export test")))
220+
Gen.map3
221+
(fun path msg exn -> FileSystemError(path, msg, Some exn))
222+
testPathGen
223+
safeStringGen
224+
(Gen.constant (Exception("File test")))
225+
Gen.map SecurityError safeStringGen // Now included with safe message
226+
Gen.map2 (fun op dur -> TimeoutError(op, TimeSpan.FromSeconds(float dur))) safeStringGen (Gen.choose (1, 300))
227+
Gen.map2 (fun entityType msg -> PaginationError(entityType, msg)) entityTypeGen safeStringGen ]
228+
229+
/// Generator for NonEmptyList of AppError (used for AggregateError)
230+
/// Note: We create a separate generator first to avoid infinite recursion
231+
let rec appErrorGenWithAggregate (depth: int) : Gen<AppError> =
232+
if depth > 2 then
233+
// At max depth, don't generate more AggregateErrors
234+
Gen.oneof
235+
[ Gen.map ConfigError (Gen.filter ((<>) null) Arb.generate<string>)
236+
Gen.map MemoryError (Gen.filter ((<>) null) Arb.generate<string>)
237+
Gen.map SecurityError (Gen.filter ((<>) null) Arb.generate<string>) ]
238+
else
239+
Gen.frequency
240+
[ (10, appErrorGen) // 10/11 chance of non-aggregate error
241+
(1,
242+
gen { // 1/11 chance of aggregate error
243+
let! head = appErrorGenWithAggregate (depth + 1)
244+
let! tailCount = Gen.choose (0, 2)
245+
let! tail = Gen.listOfLength tailCount (appErrorGenWithAggregate (depth + 1))
246+
return AggregateError(NonEmptyList(head, tail))
247+
}) ]
248+
249+
/// Container type for custom Arbitrary instances
250+
type CustomGenerators =
251+
static member String() =
252+
Arb.Default.String() |> Arb.filter ((<>) null)
253+
254+
static member ExportConfig() = Arb.fromGen exportConfigGen
255+
256+
static member AppError() =
257+
Arb.fromGen (appErrorGenWithAggregate 0)
258+
259+
static member PropertyDict() = Arb.fromGen propertyDictGen
260+
261+
/// Generator for valid Neo4j property values including all supported types
262+
let neo4jValueGen: Gen<obj> =
263+
Gen.frequency
264+
[ (3, Gen.constant (box null))
265+
(2, Gen.map box Arb.generate<bool>)
266+
(3, Gen.map box Arb.generate<int64>)
267+
(3, Gen.map box (Gen.filter Double.IsFinite Arb.generate<float>)) // Only finite values
268+
(5, Gen.map box (Gen.filter (fun s -> s <> null) Arb.generate<string>))
269+
(1, Gen.map box Arb.generate<DateTime>) // Temporal types
270+
(1, Gen.map (fun arr -> box (arr: int64[])) (Gen.arrayOf Arb.generate<int64>)) ] // Arrays
271+
272+
/// Generator for richer property dictionaries with various Neo4j types
273+
let richPropertyDictGen: Gen<IDictionary<string, obj>> =
274+
gen {
275+
let! count = Gen.choose (0, 20) // More properties for thorough testing
276+
let! keys = Gen.listOfLength count propertyKeyGen
277+
let distinctKeys = keys |> List.distinct
278+
let! values = Gen.listOfLength (List.length distinctKeys) neo4jValueGen
279+
let dict = Dictionary<string, obj>()
280+
281+
List.zip distinctKeys values
282+
|> List.iter (fun (k, v) -> dict.[k] <- v)
283+
284+
return dict :> IDictionary<string, obj>
285+
}
286+
287+
/// Generator for node data (labels + properties)
288+
let nodeDataGen: Gen<Set<string> * IDictionary<string, obj>> =
289+
Gen.map2 (fun labels props -> (labels, props)) labelSetGen richPropertyDictGen
290+
291+
/// Generator for element IDs
292+
let elementIdGen: Gen<string> =
293+
Gen.frequency
294+
[ (8, Gen.map (sprintf "element:%d") (Gen.choose (1, 10000)))
295+
(1, Gen.constant "") // Test empty element IDs
296+
(1, Gen.constant null) ] // Test null element IDs
297+
298+
/// Generator for error info tuples
299+
let errorInfoGen: Gen<AppError * string> =
300+
Gen.map2 (fun error elemId -> (error, elemId)) appErrorGen elementIdGen
301+
302+
/// Generator for pagination strategies
303+
let keysetIdGen: Gen<KeysetId> =
304+
Gen.oneof
305+
[ Gen.map NumericId (Gen.choose (1, 1000000) |> Gen.map int64)
306+
Gen.map ElementId (Gen.map (sprintf "element:%d") (Gen.choose (1, 1000000))) ]
307+
308+
let neo4jVersionGen: Gen<Neo4jVersion> =
309+
Gen.elements [ V4x; V5x; V6x; Unknown ]
310+
311+
let paginationStrategyGen: Gen<PaginationStrategy> =
312+
Gen.oneof
313+
[ Gen.map SkipLimit (Gen.choose (0, 10000))
314+
Gen.map2 (fun lastId version -> Keyset(Some lastId, version)) keysetIdGen neo4jVersionGen ]
315+
316+
/// Registers all custom generators with FsCheck
317+
let registerGenerators () =
318+
Arb.register<CustomGenerators> () |> ignore

0 commit comments

Comments
 (0)