Skip to content

Commit baea2ff

Browse files
yoshielpil
authored andcommitted
add random operations / property tests
1 parent 1746b8e commit baea2ff

File tree

1 file changed

+199
-0
lines changed

1 file changed

+199
-0
lines changed

test/gleam/dict_test.gleam

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,202 @@ pub fn combine_with_no_overlapping_keys_test() {
391391
assert dict.combine(map1, map2, fn(one, _) { one })
392392
== dict.from_list([#("a", 1), #("b", 2), #("c", 3), #("d", 4)])
393393
}
394+
395+
// Enums without fields all hash to 0 due to how the hash function works -
396+
// we use this fact here to produce and test collisions.
397+
//
398+
// Object.keys() returns [] for variants without fields, so the hash always
399+
// stays on it's initial value.
400+
type CollidingKey {
401+
CollidingKey1
402+
CollidingKey2
403+
}
404+
405+
pub fn hash_collision_overflow_test() {
406+
let d =
407+
dict.new() |> dict.insert(CollidingKey1, 1) |> dict.insert(CollidingKey2, 2)
408+
409+
assert dict.size(d) == 2
410+
assert dict.get(d, CollidingKey1) == Ok(1)
411+
assert dict.get(d, CollidingKey2) == Ok(2)
412+
413+
let d = dict.delete(d, CollidingKey1)
414+
415+
assert dict.size(d) == 1
416+
assert dict.get(d, CollidingKey1) == Error(Nil)
417+
assert dict.get(d, CollidingKey2) == Ok(2)
418+
}
419+
420+
fn test_random_operations(
421+
initial_seed: Int,
422+
num_ops: Int,
423+
key_space: Int,
424+
initial: dict.Dict(Int, Int),
425+
) -> Nil {
426+
test_random_operations_loop(
427+
initial_seed,
428+
prng(initial_seed),
429+
num_ops,
430+
key_space,
431+
dict.to_list(initial),
432+
initial,
433+
)
434+
}
435+
436+
fn test_random_operations_loop(
437+
initial_seed: Int,
438+
seed: Int,
439+
remaining: Int,
440+
key_space: Int,
441+
proplist: List(#(Int, Int)),
442+
dict: dict.Dict(Int, Int),
443+
) -> Nil {
444+
case remaining > 0 {
445+
False -> {
446+
assert_dict_matches_proplist(dict, proplist, initial_seed)
447+
}
448+
True -> {
449+
let seed = prng(seed)
450+
let op_choice = seed % 2
451+
let seed = prng(seed)
452+
let key = seed % key_space
453+
454+
case op_choice {
455+
// Insert
456+
0 -> {
457+
let new_proplist = list.key_set(proplist, key, key * 2)
458+
let new_dict = dict.insert(dict, key, key * 2)
459+
test_random_operations_loop(
460+
initial_seed,
461+
seed,
462+
remaining - 1,
463+
key_space,
464+
new_proplist,
465+
new_dict,
466+
)
467+
}
468+
// Delete
469+
_ -> {
470+
let new_proplist = case list.key_pop(proplist, key) {
471+
Ok(#(_, remaining)) -> remaining
472+
Error(Nil) -> proplist
473+
}
474+
let new_dict = dict.delete(dict, key)
475+
test_random_operations_loop(
476+
initial_seed,
477+
seed,
478+
remaining - 1,
479+
key_space,
480+
new_proplist,
481+
new_dict,
482+
)
483+
}
484+
}
485+
}
486+
}
487+
}
488+
489+
fn run_many_random_tests(
490+
count count: Int,
491+
ops_per_test ops_per_test: Int,
492+
key_space key_space: Int,
493+
initial dict: dict.Dict(Int, Int),
494+
) -> Nil {
495+
case count {
496+
0 -> Nil
497+
_ -> {
498+
let start_seed = int.random(0x7fffffff)
499+
test_random_operations(start_seed, ops_per_test, key_space, dict)
500+
run_many_random_tests(
501+
count: count - 1,
502+
ops_per_test: ops_per_test,
503+
key_space: key_space,
504+
initial: dict,
505+
)
506+
}
507+
}
508+
}
509+
510+
pub fn random_operations_small_test() {
511+
run_many_random_tests(
512+
count: 100,
513+
ops_per_test: 50,
514+
key_space: 32,
515+
initial: dict.new(),
516+
)
517+
}
518+
519+
pub fn random_operations_medium_test() {
520+
run_many_random_tests(
521+
count: 100,
522+
ops_per_test: 50,
523+
key_space: 200,
524+
initial: range_dict(50),
525+
)
526+
}
527+
528+
pub fn random_operations_large_test() {
529+
run_many_random_tests(
530+
count: 100,
531+
ops_per_test: 1000,
532+
key_space: 2000,
533+
initial: range_dict(1000),
534+
)
535+
}
536+
537+
fn range_dict(size) {
538+
list.range(1, size)
539+
|> list.map(fn(x) { #(x, x) })
540+
|> dict.from_list
541+
}
542+
543+
fn prng(state: Int) -> Int {
544+
{ state * 48_271 } % 0x7FFFFFFF
545+
}
546+
547+
fn assert_dict_matches_proplist(
548+
d: dict.Dict(k, v),
549+
proplist: List(#(k, v)),
550+
seed: Int,
551+
) -> Nil {
552+
case dict.size(d) == list.length(proplist) {
553+
True -> Nil
554+
False ->
555+
panic as {
556+
"Size mismatch with seed "
557+
<> int.to_string(seed)
558+
<> ": dict.size="
559+
<> int.to_string(dict.size(d))
560+
<> " proplist.size="
561+
<> int.to_string(list.length(proplist))
562+
}
563+
}
564+
565+
list.each(proplist, fn(pair) {
566+
let #(key, value) = pair
567+
let result = dict.get(d, key)
568+
569+
case result == Ok(value) {
570+
True -> Nil
571+
False ->
572+
panic as {
573+
"Get mismatch with seed "
574+
<> int.to_string(seed)
575+
<> ": key="
576+
<> string.inspect(key)
577+
<> ", value="
578+
<> string.inspect(value)
579+
<> ", dict.get="
580+
<> string.inspect(result)
581+
}
582+
}
583+
})
584+
585+
case d == dict.from_list(proplist) {
586+
True -> Nil
587+
False ->
588+
panic as {
589+
"Structural equality failed with seed " <> int.to_string(seed)
590+
}
591+
}
592+
}

0 commit comments

Comments
 (0)