https://gitlab.com/tezos/tezos
Tip revision: ce7d752cc8d1fb72a99a96158c2a1ba4e3a8ed13 authored by Thomas Letan on 29 May 2023, 14:13:29 UTC
Teztible: Write experiments and scenarios in Yaml scripts
Teztible: Write experiments and scenarios in Yaml scripts
Tip revision: ce7d752
test_pbt.ml
(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2023 Nomadic Labs, <contact@nomadic-labs.com> *)
(* *)
(* Permission is hereby granted, free of charge, to any person obtaining a *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)
(* and/or sell copies of the Software, and to permit persons to whom the *)
(* Software is furnished to do so, subject to the following conditions: *)
(* *)
(* The above copyright notice and this permission notice shall be included *)
(* in all copies or substantial portions of the Software. *)
(* *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)
(* DEALINGS IN THE SOFTWARE. *)
(* *)
(*****************************************************************************)
(* Testing
-------
Component: Gossipsub
Invocation: dune exec test/test_gossipsub.exe -- --file test_pbt.ml
Subject: PBT for gossipsub.
*)
open Test_gossipsub_shared
open Gossipsub_intf
open Tezt_core.Base
module Peer = C.Subconfig.Peer
module Topic = C.Subconfig.Topic
module Message_id = C.Subconfig.Message_id
module Message = C.Subconfig.Message
module Basic_fragments = struct
open Gossipsub_pbt_generators
open Fragment
let prune_backoff = Milliseconds.Span.of_int_s 10
let add_then_remove_peer ~gen_peer : t =
of_input_gen (add_peer ~gen_peer) @@ fun ap ->
[I.add_peer ap; I.remove_peer {peer = ap.peer}]
let join_then_leave_topic ~gen_topic : t =
of_input_gen (join ~gen_topic) @@ fun jp ->
[I.join jp; I.leave {topic = jp.topic}]
let graft_then_prune ~gen_peer ~gen_topic : t =
of_input_gen (graft ~gen_peer ~gen_topic) @@ fun g ->
[
I.graft g;
I.prune
{
peer = g.peer;
topic = g.topic;
px = Seq.empty;
backoff = prune_backoff;
};
]
let heartbeat : t = of_list [I.heartbeat]
(* This creates a list of [count] [add_peer;graft] with distinct peers
such that exactly [count_outbound] of them are [outbound] peers. *)
let add_distinct_peers_and_graft count count_outbound topic =
assert (count_outbound <= count) ;
let many_peer_gens =
List.init ~when_negative_length:() count (fun i ->
let open M in
let+ ap = add_peer ~gen_peer:(M.return i) in
(* Setting [direct=false] otherwise the peers won't be grafted. *)
if i < count_outbound then {ap with direct = false; outbound = true}
else ap)
|> WithExceptions.Result.get_ok ~loc:__LOC__
|> M.flatten_l
in
of_input_gen many_peer_gens @@ fun peers ->
List.map (fun ap -> [I.add_peer ap; I.graft {topic; peer = ap.peer}]) peers
|> List.flatten
end
module Test_message_cache = struct
module L = Message_cache
module R = struct
module M = Map.Make (Int)
type t = {
mutable ticks : int;
cache : Message.t Message_id.Map.t Topic.Map.t M.t;
history_slots : int;
gossip_slots : int;
}
let create ~history_slots ~gossip_slots =
assert (gossip_slots > 0) ;
assert (gossip_slots <= history_slots) ;
{ticks = 0; cache = M.empty; history_slots; gossip_slots}
let add_message message_id message topic t =
{
t with
cache =
M.update
t.ticks
(function
| None ->
Topic.Map.singleton
topic
(Message_id.Map.singleton message_id message)
|> Option.some
| Some map ->
Topic.Map.update
topic
(function
| None ->
Message_id.Map.singleton message_id message
|> Option.some
| Some topic_map ->
Message_id.Map.add message_id message topic_map
|> Option.some)
map
|> Option.some)
t.cache;
}
let get_message_for_peer _peer message_id t =
let found = ref None in
for x = max 0 (t.ticks - t.history_slots + 1) to t.ticks do
match M.find x t.cache with
| None -> ()
| Some topic_map ->
Topic.Map.iter
(fun _topic map ->
match Message_id.Map.find message_id map with
| None -> ()
| Some message -> found := Some message)
topic_map
done ;
let r = !found in
Option.map (fun message -> (t, message)) r
let get_message_ids_to_gossip topic t =
let found = ref Message_id.Set.empty in
for x = max 0 (t.ticks - t.gossip_slots + 1) to t.ticks do
match M.find x t.cache with
| None -> ()
| Some topic_map -> (
match Topic.Map.find topic topic_map with
| None -> ()
| Some message_map ->
let set =
message_map |> Message_id.Map.to_seq |> Seq.map fst
|> Message_id.Set.of_seq
in
found := Message_id.Set.union !found set)
done ;
!found
let shift t =
t.ticks <- t.ticks + 1 ;
t
end
(* If those numbers are too large, we will miss scenarios with collisions. *)
let history_slots = QCheck2.Gen.int_range (-1) 10
let gossip_slots = history_slots
let message_id = QCheck2.Gen.int_range 0 10
(* The data-structure assumes that the id identifies uniquely the
message. To ease the readibility of the test we consider a
messsage constant. *)
let message = QCheck2.Gen.return "m"
let peer = QCheck2.Gen.return 0
let topic =
let open QCheck2.Gen in
let* chars =
QCheck2.Gen.list_size
(QCheck2.Gen.int_range 1 2)
(QCheck2.Gen.char_range 'a' 'c')
in
return (String.of_seq (List.to_seq chars))
type action =
| Add_message of {message_id : int; message : string; topic : string}
| Get_message_for_peer of {peer : int; message_id : int}
| Get_message_ids_to_gossip of {topic : string}
| Shift
let pp_action fmt = function
| Add_message {message_id; message = _; topic} ->
Format.fprintf fmt "ADD_MESSAGE {id:%d;topic:%s}" message_id topic
| Get_message_for_peer {peer = _; message_id} ->
Format.fprintf fmt "GET_MESSAGE {id:%d}" message_id
| Get_message_ids_to_gossip {topic} ->
Format.fprintf fmt "GET_FOR_TOPIC {topic:%s}" topic
| Shift -> Format.fprintf fmt "SHIFT"
let add_message =
let open QCheck2.Gen in
let* message_id in
let* message in
let* topic in
return (Add_message {message_id; message; topic})
let get_message_for_peer =
let open QCheck2.Gen in
let* message_id in
let* peer in
return (Get_message_for_peer {peer; message_id})
let get_message_ids_to_gossip =
let open QCheck2.Gen in
let* topic in
return (Get_message_ids_to_gossip {topic})
let action =
QCheck2.Gen.oneof
[
add_message;
get_message_for_peer;
get_message_ids_to_gossip;
QCheck2.Gen.return Shift;
]
let actions = QCheck2.Gen.(list_size (int_range 1 30) action)
let rec run (left, right) actions =
let remaining_steps = List.length actions in
match actions with
| [] -> None
| Add_message {message_id; message; topic} :: actions ->
let left = L.add_message message_id message topic left in
let right = R.add_message message_id message topic right in
run (left, right) actions
| Get_message_for_peer {peer; message_id} :: actions -> (
let left_result = L.get_message_for_peer peer message_id left in
let right_result = R.get_message_for_peer peer message_id right in
match (left_result, right_result) with
| None, None -> run (left, right) actions
| Some (left, _left_message, _left_counter), Some (right, _right_message)
->
(* By definition of the message generator, messages are equal. *)
run (left, right) actions
| None, Some _ ->
let message = Format.asprintf "Expected: A message. Got: None" in
Some (remaining_steps, message)
| Some _, None ->
let message =
Format.asprintf "Expected: No message. Got: A message"
in
Some (remaining_steps, message))
| Get_message_ids_to_gossip {topic} :: actions ->
let left_result = L.get_message_ids_to_gossip topic left in
let right_result = R.get_message_ids_to_gossip topic right in
let left_set = Message_id.Set.of_list left_result in
if Message_id.Set.equal left_set right_result then
run (left, right) actions
else
let pp_set fmt s =
if Message_id.Set.is_empty s then Format.fprintf fmt "empty set"
else
s |> Message_id.Set.to_seq |> List.of_seq
|> Format.fprintf
fmt
"%a"
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt " ")
Format.pp_print_int)
in
let message =
Format.asprintf
"Expected: %a@.Got: %a@."
pp_set
right_result
pp_set
left_set
in
Some (remaining_steps, message)
| Shift :: actions ->
let left = L.shift left in
let right = R.shift right in
run (left, right) actions
let pp fmt trace =
Format.fprintf
fmt
"%a@."
(Format.pp_print_list ~pp_sep:Format.pp_print_newline pp_action)
trace
let test rng =
Tezt_core.Test.register
~__FILE__
~title:"Gossipsub: check correction of message cache data structure"
~tags:["gossipsub"; "message_cache"]
@@ fun () ->
let scenario =
let open QCheck2.Gen in
let* history_slots in
let* gossip_slots in
let* actions in
let left =
try
L.create ~history_slots ~gossip_slots ~seen_message_slots:10
|> Either.left
with exn -> Either.right exn
in
let right =
try R.create ~history_slots ~gossip_slots |> Either.left
with exn -> Either.right exn
in
match (left, right) with
| Right _, Right _ -> return None
| Left left, Left right -> (
match run (left, right) actions with
| None -> return None
| Some (remaining_steps, explanation) ->
let n = List.length actions - remaining_steps + 1 in
let actions = List.take_n n actions in
return @@ Some (history_slots, gossip_slots, explanation, actions)
)
| Right exn, Left _ ->
let explanation =
Format.asprintf
"Initialisation failed unexpectedly: %s"
(Printexc.to_string exn)
in
return @@ Some (history_slots, gossip_slots, explanation, [])
| Left _, Right exn ->
let explanation =
Format.asprintf
"Initialisation succeeded while it should not. Expected to fail \
with: %s"
(Printexc.to_string exn)
in
return @@ Some (history_slots, gossip_slots, explanation, [])
in
let test =
QCheck2.Test.make ~count:500_000 ~name:"Gossipsub: message cache" scenario
@@ function
| None -> true
| Some (history_slots, gossip_slots, explanation, trace) ->
Tezt.Test.fail
~__LOC__
"@[<v 2>Soundness check failed.@;\
Limits:@;\
history_slots: %d@;\
gossip_slots: %d@;\
@;\
Dumping trace:@;\
@[<v>%a@]@;\
@;\
Explanation:@;\
%s@]"
history_slots
gossip_slots
pp
trace
explanation
in
QCheck2.Test.check_exn ~rand:rng test ;
unit
end
(** Test that removing a peer really removes it from the state *)
module Test_remove_peer = struct
open Gossipsub_pbt_generators
let all_peers = [0; 1; 2; 3]
let fail_if_in_map peers map ~on_error =
let fail = List.find_opt (fun peer -> GS.Peer.Map.mem peer map) peers in
match fail with None -> Ok () | Some peer -> Error (on_error peer)
let fail_if_in_set peers set ~on_error =
let fail = List.find_opt (fun peer -> GS.Peer.Set.mem peer set) peers in
match fail with None -> Ok () | Some peer -> Error (on_error peer)
let not_in_view peers state =
let open GS.Introspection in
let open Result_syntax in
let view = view state in
let check_map str map =
fail_if_in_map peers map ~on_error:(fun peer ->
`peer_not_removed_correctly (view, str, peer))
in
let check_set str set =
fail_if_in_set peers set ~on_error:(fun peer ->
`peer_not_removed_correctly (view, str, peer))
in
let* () = check_map "connections" view.connections in
let* () = check_map "scores" view.scores in
let* () =
Topic.Map.iter_e
(fun topic backoff ->
let str = Format.asprintf "backoff[topic=%a]" Topic.pp topic in
check_map str backoff)
view.backoff
in
(* The last step of the scenario is a heartbeat, which cleans the
[iwant/ihave_per_heartbeat] maps. *)
let* () = check_map "ihave_per_heartbeat" view.ihave_per_heartbeat in
let* () = check_map "iwant_per_heartbeat" view.iwant_per_heartbeat in
let* () =
Topic.Map.iter_e
(fun topic peer_set ->
let str = Format.asprintf "mesh[topic=%a]" Topic.pp topic in
check_set str peer_set)
view.mesh
in
let* () =
Topic.Map.iter_e
(fun topic fanout_peers ->
let str = Format.asprintf "fanout[topic=%a]" Topic.pp topic in
check_set str fanout_peers.peers)
view.fanout
in
Message_cache.Internal_for_tests.get_access_counters view.message_cache
|> Seq.E.iter (fun (message_id, map) ->
let place =
Format.asprintf "message_cache[message_id=%d]" message_id
in
check_map place map)
let predicate (Transition {state' = final_state; _}) =
(* This predicate checks that [peer_id] does not appear in the [view]
of the final state. *)
not_in_view all_peers final_state
let scenario (limits : ('a, 'b, 'c, Milliseconds.span) limits) =
let open Fragment in
let open Basic_fragments in
let gen_peer = M.oneofl all_peers in
let gen_topic =
M.oneofl ["topicA"; "topicB"; "topicC"; "topicD"; "topicE"]
in
let gen_message_id = M.oneofl [42; 43; 44] in
let gen_msg_count = M.int_range 1 5 in
let add_then_remove_peer_wait_and_clean () =
(* In order to purge a peer from the connections, we need to
1. remove it
2. wait until [expire=retain_duration+slack]
3. wait until the next round of cleanup in the heartbeat *)
let expire =
Milliseconds.Span.to_int_s limits.retain_duration
+ (Milliseconds.Span.to_int_s limits.heartbeat_interval * 2)
in
let heartbeat_cleanup_ticks = limits.backoff_cleanup_ticks in
add_then_remove_peer ~gen_peer
@% repeat expire tick
@% repeat heartbeat_cleanup_ticks heartbeat
in
let graft_then_prune_wait_and_clean () =
(* A pruned peer will stay in the backoff table until the
end of the backoff specified in the Prune message.
After pruning, we wait for [backoff + slack] ticks then force
triggering a cleanup of the backoffs in the heartbeat. *)
let backoff =
Milliseconds.to_int_s Basic_fragments.prune_backoff
+ (Milliseconds.Span.to_int_s limits.heartbeat_interval * 2)
in
let heartbeat_cleanup_ticks = limits.backoff_cleanup_ticks in
graft_then_prune ~gen_peer ~gen_topic
@% repeat backoff tick
@% repeat heartbeat_cleanup_ticks heartbeat
in
interleave
[
fork_at_most
4
(repeat_at_most 2 (add_then_remove_peer_wait_and_clean ()));
repeat_at_most 10 @@ join_then_leave_topic ~gen_topic;
repeat_at_most 10 heartbeat;
repeat_at_most 100 tick;
of_input_gen
(ihave ~gen_peer ~gen_topic ~gen_message_id ~gen_msg_count)
(fun ihave -> [I.ihave ihave])
|> repeat_at_most 5;
of_input_gen
(iwant ~gen_peer ~gen_message_id ~gen_msg_count)
(fun iwant -> [I.iwant iwant])
|> repeat_at_most 5;
graft_then_prune_wait_and_clean () |> repeat_at_most 10;
]
@% heartbeat
let pp_backoff fmtr backoff =
let list = backoff |> GS.Peer.Map.bindings in
Format.pp_print_list
~pp_sep:(fun fmtr () -> Format.fprintf fmtr ", ")
(fun fmtr (topic, backoff) ->
Format.fprintf
fmtr
"peer %a -> %a"
GS.Peer.pp
topic
Milliseconds.pp
backoff)
fmtr
list
let pp_state fmtr state =
let open Format in
let v = GS.Introspection.view state in
let cleanup =
Int64.(rem v.heartbeat_ticks (of_int v.limits.backoff_cleanup_ticks)) = 0L
in
if cleanup then fprintf fmtr "heartbeat.clean; " ;
Topic.Map.iter
(fun topic backoff ->
fprintf fmtr "%a: [%a]" Topic.pp topic pp_backoff backoff)
v.backoff ;
Peer.Map.iter
(fun peer score ->
let expires = GS.Score.expires score in
fprintf
fmtr
"peer %a, expire=%a, cleanup=%b"
GS.Peer.pp
peer
(pp_print_option Milliseconds.pp)
expires
cleanup)
v.scores
let test rng limits parameters =
Tezt_core.Test.register
~__FILE__
~title:"Gossipsub: remove peer"
~tags:["gossipsub"; "control"]
@@ fun () ->
let scenario =
let open M in
let* limits =
let+ retain_duration =
M.int_range 0 (Milliseconds.Span.to_int_s limits.retain_duration * 2)
and+ heartbeat_interval =
M.int_range
0
(Milliseconds.Span.to_int_s limits.heartbeat_interval * 2)
and+ backoff_cleanup_ticks =
M.int_range 1 (limits.backoff_cleanup_ticks * 2)
in
let score_cleanup_ticks = backoff_cleanup_ticks in
let retain_duration = Milliseconds.Span.of_int_s retain_duration in
let heartbeat_interval =
Milliseconds.Span.of_int_s heartbeat_interval
in
{
limits with
retain_duration;
heartbeat_interval;
backoff_cleanup_ticks;
score_cleanup_ticks;
prune_backoff = Basic_fragments.prune_backoff;
}
in
let state = GS.make rng limits parameters in
run state (scenario limits)
in
let test =
QCheck2.Test.make ~count:1_000 ~name:"Gossipsub: remove_peer" scenario
@@ fun trace ->
match check_final predicate trace with
| Ok () -> true
| Error e -> (
match e with
| `peer_not_removed_correctly (v, msg, peer) ->
Tezt.Test.fail
~__LOC__
"@[<v 2>Peer %d was not removed correctly from %s.@;\
Limits:@;\
%a@;\
Dumping trace:@;\
@[<v>%a@]@]"
peer
msg
pp_limits
GS.Introspection.(limits v)
(pp_trace ~pp_state ~pp_state':pp_state ())
trace)
in
QCheck2.Test.check_exn ~rand:rng test ;
unit
end
module Test_peers_below_degree_high = struct
open Gossipsub_pbt_generators
(* This test checks the logic around pruning overflowing peers.
The test works as follows:
1. Add many distinct peers (some direct, some not) on some unique topic.
We do so by performing a sequence of [Add_peer] followed by [Graft].
When the mesh becomes full, only [outbound] peers are added by [Graft] so
we add more outbound peers than [degree_high].
2. Perform a heartbeat.
3.1 Check just before the heartbeat that the [to_prune] record is well-formed
and that there are indeed too many peers in the mesh.
3.2 Check in the heartbeat output that the automaton requests to prune enough
peers to reach [degree_optimal].
*)
let topic = "dummy_topic"
let pp_state fmtr state =
let v = GS.Introspection.view state in
Fmt.Dump.record
Fmt.Dump.
[
field
"mesh"
(fun v -> v.GS.Introspection.mesh)
GS.Introspection.(pp_topic_map pp_peer_set);
field
"connections"
(fun v -> v.GS.Introspection.connections)
GS.Introspection.pp_connections;
]
fmtr
v
let predicate degree_high
(Transition {time; input; state; state'; output} : transition) () =
let open Result_syntax in
match input with
| Graft _ -> (
match output with
| Peer_filtered | Unsubscribed_topic | Unexpected_grafting_peer
| Grafting_peer_with_negative_score | Peer_backed_off ->
fail (`unexpected_output (O output))
| Grafting_direct_peer | Peer_already_in_mesh | Grafting_successfully
| Mesh_full ->
return_unit)
| Heartbeat -> (
match output with
| Heartbeat {to_prune; _} ->
let open GS.Introspection in
let previous_state_has_enough_peers_in_mesh s =
(* The mesh should have too many peers in [s]. *)
let v = view s in
(* Get number of peers on the single topic. *)
let peer_set = GS.Topic.Map.find topic v.mesh in
let card =
match peer_set with
| None -> 0
| Some set -> GS.Peer.Set.cardinal set
in
(* Cardinality should be > [degree_high]. *)
if card <= degree_high then
fail (`invalid_state_before_heartbeat s)
else return_unit
in
let pruned_enough_peers s' =
(* The automaton should have pruned enough peers. *)
let v = view s' in
(* First perform an unrelated, opportunistic check: the prune
record doesn't map empty topic sets to any peer. *)
let inconsistent_prune_record =
GS.Peer.Map.exists
(fun _peer topic_set -> GS.Topic.Set.is_empty topic_set)
to_prune
in
if inconsistent_prune_record then
fail (`inconsistent_prune_record to_prune)
else
(* Check that we pruned enough peers to reach [target]. *)
let target = v.limits.degree_optimal in
let peer_set = GS.Topic.Map.find topic v.mesh in
let card =
match peer_set with
| None -> 0
| Some set -> GS.Peer.Set.cardinal set
in
if card <> target then
fail (`too_many_peers (time, card, target, degree_high))
else return_unit
in
let* () = previous_state_has_enough_peers_in_mesh state in
pruned_enough_peers state')
| _ -> return_unit
let scenario limits =
let open Fragment in
let open Basic_fragments in
let open M in
(* In order to satisfy the predicate, [count_outbound > degree_high ] *)
bind_gen
(let* count_outbound =
M.int_range (limits.degree_high + 1) (limits.degree_high * 2)
in
(* We need [peer_count >= count_outbound] *)
let* peer_count = M.int_range count_outbound (count_outbound * 2) in
return (count_outbound, peer_count))
@@ fun (count_outbound, peer_count) ->
of_list [I.join {topic}]
@% add_distinct_peers_and_graft peer_count count_outbound topic
@% heartbeat
let pp_output fmtr (O o) = GS.pp_output fmtr o
(* A generator that should satisfy [Tezos_gossipsub.check_limits]. *)
let limit_generator (default_limits : (_, _, _, _) limits) =
let open M in
let* degree_optimal = M.int_range 1 20 in
let* degree_low, degree_high =
M.pair
(M.int_range 1 degree_optimal)
(M.int_range degree_optimal (2 * degree_optimal))
in
let* degree_out = M.int_range 0 (Int.min degree_low (degree_optimal / 2)) in
let* degree_score = M.int_range 0 (degree_optimal - degree_out) in
let* history_length = M.int_range 1 50 in
let* history_gossip_length = M.int_range 1 history_length in
return
{
default_limits with
degree_optimal;
degree_low;
degree_high;
degree_out;
degree_score;
history_length;
history_gossip_length;
}
let test rng limits parameters =
Tezt_core.Test.register
~__FILE__
~title:"Gossipsub: peers below degree high"
~tags:["gossipsub"; "heartbeat"; "degree"]
@@ fun () ->
let scenario =
let open M in
let* limits = limit_generator limits in
let state = GS.make rng limits parameters in
let+ trace = run state (scenario limits) in
(trace, limits)
in
let test =
QCheck2.Test.make
~count:1_000
~name:"Gossipsub: peers_below_degree_high"
scenario
@@ fun (trace, limits) ->
match check_fold (predicate limits.degree_high) () trace with
| Ok () -> true
| Error (e, prefix) -> (
match e with
| `invalid_state_before_heartbeat s ->
Tezt.Test.fail
~__LOC__
"Dumping trace until failure:@;\
@[<hov>%a@]@;\
Limits:@;\
%a@;\
Faulty test: invalid state before heartbeat %a@;"
(pp_trace ~pp_output ~pp_state ())
prefix
pp_limits
limits
pp_state
s
| `unexpected_output (O o) ->
Tezt.Test.fail
~__LOC__
"Faulty test: unexpected output %a"
GS.pp_output
o
| `inconsistent_prune_record to_prune ->
Tezt.Test.fail
~__LOC__
"At heartbeat, the prune record is ill-formed: %a"
GS.Introspection.(pp_peer_map pp_topic_set)
to_prune
| `too_many_peers (t, remaining, target, degree_high) ->
Tezt.Test.fail
~__LOC__
"@[<v 2>Dumping trace until failure:@;\
@[<hov>%a@]@;\
At time %a: peers in mesh=%d, target = %d, degree_high = %d@;\
Limits:@;\
%a@;\
@]"
(pp_trace ~pp_state ~pp_output ())
prefix
Milliseconds.pp
t
remaining
target
degree_high
pp_limits
limits)
in
QCheck2.Test.check_exn ~rand:rng test ;
unit
end
let register rng limits parameters =
Test_remove_peer.test rng limits parameters ;
Test_message_cache.test rng ;
Test_peers_below_degree_high.test rng limits parameters