@@ -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