diff options
Diffstat (limited to 'testing')
-rw-r--r-- | testing/BUILD.bazel | 9 | ||||
-rw-r--r-- | testing/random_testing.cc | 396 |
2 files changed, 405 insertions, 0 deletions
diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 6cc7018e..075f00d1 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel | |||
@@ -40,3 +40,12 @@ cc_binary( | |||
40 | "@sndfile", | 40 | "@sndfile", |
41 | ], | 41 | ], |
42 | ) | 42 | ) |
43 | |||
44 | cc_binary( | ||
45 | name = "random_testing", | ||
46 | srcs = ["random_testing.cc"], | ||
47 | deps = [ | ||
48 | ":misc_tools", | ||
49 | "//c-toxcore/toxcore", | ||
50 | ], | ||
51 | ) | ||
diff --git a/testing/random_testing.cc b/testing/random_testing.cc new file mode 100644 index 00000000..3e5be731 --- /dev/null +++ b/testing/random_testing.cc | |||
@@ -0,0 +1,396 @@ | |||
1 | // Program to perform random actions in a network of toxes. | ||
2 | // | ||
3 | // Useful to find reproducing test cases for seemingly random bugs. | ||
4 | |||
5 | #include <algorithm> | ||
6 | #include <cassert> | ||
7 | #include <cstdlib> | ||
8 | #include <memory> | ||
9 | #include <random> | ||
10 | #include <vector> | ||
11 | |||
12 | #include "../toxcore/tox.h" | ||
13 | #include "misc_tools.c" | ||
14 | |||
15 | namespace { | ||
16 | |||
17 | // Whether to write log messages when handling callbacks. | ||
18 | constexpr bool LOG_CALLBACKS = 0; | ||
19 | |||
20 | // Number of participants in the test run. | ||
21 | constexpr uint32_t NUM_TOXES = 10; | ||
22 | // Maximum amount of time in iterations to wait for bootstrapping and friend | ||
23 | // connections to succeed. | ||
24 | constexpr uint32_t MAX_BOOTSTRAP_ITERATIONS = 1000; | ||
25 | // Number of conferences up to which users may create their own new conferences. | ||
26 | // They may still be invited and join. | ||
27 | constexpr uint32_t MAX_CONFERENCES_PER_USER = 3; | ||
28 | // Maximum number of actions per program execution. | ||
29 | constexpr uint32_t MAX_ACTIONS = 10000; | ||
30 | // Maximum number of attempts at executing a random action. | ||
31 | constexpr uint32_t MAX_ACTION_ATTEMPTS = 100; | ||
32 | // Number of tox_iterate calls between each action. | ||
33 | constexpr uint32_t ITERATIONS_PER_ACTION = 1; | ||
34 | // Amount of time in milliseconds to wait between tox_iterate calls. | ||
35 | constexpr uint32_t ITERATION_INTERVAL = 5; | ||
36 | |||
37 | struct Tox_Options_Deleter { | ||
38 | void operator()(Tox_Options *options) const { tox_options_free(options); } | ||
39 | }; | ||
40 | |||
41 | using Tox_Options_Ptr = std::unique_ptr<Tox_Options, Tox_Options_Deleter>; | ||
42 | |||
43 | struct Tox_Deleter { | ||
44 | void operator()(Tox *tox) const { tox_kill(tox); } | ||
45 | }; | ||
46 | |||
47 | using Tox_Ptr = std::unique_ptr<Tox, Tox_Deleter>; | ||
48 | |||
49 | struct Local_State { | ||
50 | Tox_Ptr tox; | ||
51 | |||
52 | uint32_t friends_online = 0; | ||
53 | uint32_t next_invite = 0; | ||
54 | |||
55 | explicit Local_State(Tox_Ptr tox, uint32_t id) : tox(std::move(tox)), id_(id) {} | ||
56 | |||
57 | uint32_t id() const { return id_; } | ||
58 | |||
59 | private: | ||
60 | uint32_t id_; | ||
61 | }; | ||
62 | |||
63 | struct Random { | ||
64 | std::uniform_int_distribution<> tox_selector; | ||
65 | std::uniform_int_distribution<> friend_selector; | ||
66 | std::uniform_int_distribution<> name_length_selector; | ||
67 | std::uniform_int_distribution<> message_length_selector; | ||
68 | std::uniform_int_distribution<> byte_selector; | ||
69 | |||
70 | std::vector<size_t> action_weights; | ||
71 | std::discrete_distribution<size_t> action_selector; | ||
72 | |||
73 | Random(); | ||
74 | }; | ||
75 | |||
76 | struct Action { | ||
77 | uint32_t weight; | ||
78 | char const *title; | ||
79 | bool (*can)(Local_State const &state); | ||
80 | void (*run)(Local_State &state, Random &rnd, std::mt19937 &rng); | ||
81 | }; | ||
82 | |||
83 | Action const actions[] = { | ||
84 | { | ||
85 | 10, | ||
86 | "creates a new conference", | ||
87 | [](Local_State const &state) { | ||
88 | return tox_conference_get_chatlist_size(state.tox.get()) < MAX_CONFERENCES_PER_USER; | ||
89 | }, | ||
90 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
91 | TOX_ERR_CONFERENCE_NEW err; | ||
92 | tox_conference_new(state.tox.get(), &err); | ||
93 | assert(err == TOX_ERR_CONFERENCE_NEW_OK); | ||
94 | }, | ||
95 | }, | ||
96 | { | ||
97 | 10, | ||
98 | "invites a random friend to a conference", | ||
99 | [](Local_State const &state) { | ||
100 | return tox_conference_get_chatlist_size(state.tox.get()) != 0; | ||
101 | }, | ||
102 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
103 | TOX_ERR_CONFERENCE_INVITE err; | ||
104 | tox_conference_invite( | ||
105 | state.tox.get(), rnd.friend_selector(rng), | ||
106 | state.next_invite % tox_conference_get_chatlist_size(state.tox.get()), &err); | ||
107 | state.next_invite++; | ||
108 | assert(err == TOX_ERR_CONFERENCE_INVITE_OK); | ||
109 | }, | ||
110 | }, | ||
111 | { | ||
112 | 10, | ||
113 | "deletes the last conference", | ||
114 | [](Local_State const &state) { | ||
115 | return tox_conference_get_chatlist_size(state.tox.get()) != 0; | ||
116 | }, | ||
117 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
118 | TOX_ERR_CONFERENCE_DELETE err; | ||
119 | tox_conference_delete(state.tox.get(), | ||
120 | tox_conference_get_chatlist_size(state.tox.get()) - 1, &err); | ||
121 | assert(err == TOX_ERR_CONFERENCE_DELETE_OK); | ||
122 | }, | ||
123 | }, | ||
124 | { | ||
125 | 10, | ||
126 | "sends a message to the last conference", | ||
127 | [](Local_State const &state) { | ||
128 | return tox_conference_get_chatlist_size(state.tox.get()) != 0; | ||
129 | }, | ||
130 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
131 | std::vector<uint8_t> message(rnd.message_length_selector(rng)); | ||
132 | for (uint8_t &byte : message) { | ||
133 | byte = rnd.byte_selector(rng); | ||
134 | } | ||
135 | |||
136 | TOX_ERR_CONFERENCE_SEND_MESSAGE err; | ||
137 | tox_conference_send_message( | ||
138 | state.tox.get(), tox_conference_get_chatlist_size(state.tox.get()) - 1, | ||
139 | TOX_MESSAGE_TYPE_NORMAL, message.data(), message.size(), &err); | ||
140 | if (err == TOX_ERR_CONFERENCE_SEND_MESSAGE_OK) { | ||
141 | printf(" (OK, length = %u)", (unsigned)message.size()); | ||
142 | } else { | ||
143 | printf(" (FAILED: %u)", err); | ||
144 | } | ||
145 | }, | ||
146 | }, | ||
147 | { | ||
148 | 10, | ||
149 | "changes their name", | ||
150 | [](Local_State const &state) { return true; }, | ||
151 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
152 | std::vector<uint8_t> name(rnd.name_length_selector(rng)); | ||
153 | for (uint8_t &byte : name) { | ||
154 | byte = rnd.byte_selector(rng); | ||
155 | } | ||
156 | |||
157 | TOX_ERR_SET_INFO err; | ||
158 | tox_self_set_name(state.tox.get(), name.data(), name.size(), &err); | ||
159 | assert(err == TOX_ERR_SET_INFO_OK); | ||
160 | |||
161 | printf(" (length = %u)", (unsigned)name.size()); | ||
162 | }, | ||
163 | }, | ||
164 | { | ||
165 | 10, | ||
166 | "sets their name to empty", | ||
167 | [](Local_State const &state) { return true; }, | ||
168 | [](Local_State &state, Random &rnd, std::mt19937 &rng) { | ||
169 | TOX_ERR_SET_INFO err; | ||
170 | tox_self_set_name(state.tox.get(), nullptr, 0, &err); | ||
171 | assert(err == TOX_ERR_SET_INFO_OK); | ||
172 | }, | ||
173 | }, | ||
174 | }; | ||
175 | |||
176 | std::vector<size_t> get_action_weights() { | ||
177 | std::vector<size_t> weights; | ||
178 | for (Action const &action : actions) { | ||
179 | weights.push_back(action.weight); | ||
180 | } | ||
181 | return weights; | ||
182 | } | ||
183 | |||
184 | Random::Random() | ||
185 | : tox_selector(0, NUM_TOXES - 1), | ||
186 | friend_selector(0, NUM_TOXES - 2), | ||
187 | name_length_selector(0, TOX_MAX_NAME_LENGTH - 1), | ||
188 | message_length_selector(0, TOX_MAX_MESSAGE_LENGTH - 1), | ||
189 | byte_selector(0, 255), | ||
190 | action_weights(get_action_weights()), | ||
191 | action_selector(action_weights.begin(), action_weights.end()) {} | ||
192 | |||
193 | struct Global_State : std::vector<Local_State> { | ||
194 | // Non-copyable; | ||
195 | Global_State(Global_State const &) = delete; | ||
196 | Global_State(Global_State &&) = default; | ||
197 | Global_State() : action_counter(rnd.action_weights.size()) {} | ||
198 | |||
199 | Random rnd; | ||
200 | std::vector<unsigned> action_counter; | ||
201 | }; | ||
202 | |||
203 | void handle_friend_connection_status(Tox *tox, uint32_t friend_number, | ||
204 | TOX_CONNECTION connection_status, void *user_data) { | ||
205 | Local_State *state = static_cast<Local_State *>(user_data); | ||
206 | |||
207 | if (connection_status == TOX_CONNECTION_NONE) { | ||
208 | std::printf("Tox #%u lost friend %u!\n", state->id(), friend_number); | ||
209 | state->friends_online--; | ||
210 | } else { | ||
211 | state->friends_online++; | ||
212 | } | ||
213 | } | ||
214 | |||
215 | void handle_conference_invite(Tox *tox, uint32_t friend_number, TOX_CONFERENCE_TYPE type, | ||
216 | const uint8_t *cookie, size_t length, void *user_data) { | ||
217 | Local_State *state = static_cast<Local_State *>(user_data); | ||
218 | |||
219 | if (LOG_CALLBACKS) { | ||
220 | std::printf("Tox #%u joins the conference it was invited to\n", state->id()); | ||
221 | } | ||
222 | |||
223 | TOX_ERR_CONFERENCE_JOIN err; | ||
224 | tox_conference_join(tox, friend_number, cookie, length, &err); | ||
225 | assert(err == TOX_ERR_CONFERENCE_JOIN_OK); | ||
226 | } | ||
227 | |||
228 | void handle_conference_message(Tox *tox, uint32_t conference_number, uint32_t peer_number, | ||
229 | TOX_MESSAGE_TYPE type, const uint8_t *message, size_t length, | ||
230 | void *user_data) { | ||
231 | Local_State *state = static_cast<Local_State *>(user_data); | ||
232 | |||
233 | if (LOG_CALLBACKS) { | ||
234 | std::printf("Tox #%u received a message of length %u\n", state->id(), (unsigned)length); | ||
235 | } | ||
236 | } | ||
237 | |||
238 | void handle_conference_peer_list_changed(Tox *tox, uint32_t conference_number, void *user_data) { | ||
239 | Local_State *state = static_cast<Local_State *>(user_data); | ||
240 | |||
241 | if (LOG_CALLBACKS) { | ||
242 | std::printf("Tox #%u rebuilds peer list for conference %u\n", state->id(), conference_number); | ||
243 | } | ||
244 | |||
245 | TOX_ERR_CONFERENCE_PEER_QUERY err; | ||
246 | uint32_t const count = tox_conference_peer_count(tox, conference_number, &err); | ||
247 | assert(err == TOX_ERR_CONFERENCE_PEER_QUERY_OK); | ||
248 | |||
249 | for (uint32_t peer_number = 0; peer_number < count; peer_number++) { | ||
250 | size_t size = tox_conference_peer_get_name_size(tox, conference_number, peer_number, &err); | ||
251 | assert(err == TOX_ERR_CONFERENCE_PEER_QUERY_OK); | ||
252 | |||
253 | std::vector<uint8_t> name(size); | ||
254 | tox_conference_peer_get_name(tox, conference_number, peer_number, &name[0], &err); | ||
255 | assert(err == TOX_ERR_CONFERENCE_PEER_QUERY_OK); | ||
256 | } | ||
257 | } | ||
258 | |||
259 | Global_State make_toxes() { | ||
260 | Global_State toxes; | ||
261 | |||
262 | Tox_Options_Ptr options(tox_options_new(nullptr)); | ||
263 | tox_options_set_local_discovery_enabled(options.get(), false); | ||
264 | |||
265 | for (uint32_t i = 0; i < NUM_TOXES; i++) { | ||
266 | TOX_ERR_NEW err; | ||
267 | toxes.emplace_back(Tox_Ptr(tox_new(options.get(), &err)), i); | ||
268 | assert(err == TOX_ERR_NEW_OK); | ||
269 | assert(toxes.back().tox != nullptr); | ||
270 | |||
271 | tox_callback_friend_connection_status(toxes.back().tox.get(), handle_friend_connection_status); | ||
272 | tox_callback_conference_invite(toxes.back().tox.get(), handle_conference_invite); | ||
273 | tox_callback_conference_message(toxes.back().tox.get(), handle_conference_message); | ||
274 | tox_callback_conference_peer_list_changed(toxes.back().tox.get(), | ||
275 | handle_conference_peer_list_changed); | ||
276 | } | ||
277 | |||
278 | std::printf("Bootstrapping %u toxes\n", NUM_TOXES); | ||
279 | |||
280 | uint8_t dht_key[TOX_PUBLIC_KEY_SIZE]; | ||
281 | tox_self_get_dht_id(toxes.front().tox.get(), dht_key); | ||
282 | const uint16_t dht_port = tox_self_get_udp_port(toxes.front().tox.get(), nullptr); | ||
283 | |||
284 | for (Local_State const &state : toxes) { | ||
285 | TOX_ERR_BOOTSTRAP err; | ||
286 | tox_bootstrap(state.tox.get(), "localhost", dht_port, dht_key, &err); | ||
287 | assert(err == TOX_ERR_BOOTSTRAP_OK); | ||
288 | } | ||
289 | |||
290 | std::printf("Creating full mesh of friendships\n"); | ||
291 | |||
292 | for (Local_State const &state1 : toxes) { | ||
293 | for (Local_State const &state2 : toxes) { | ||
294 | if (state1.tox != state2.tox) { | ||
295 | TOX_ERR_FRIEND_ADD err; | ||
296 | uint8_t key[TOX_PUBLIC_KEY_SIZE]; | ||
297 | |||
298 | tox_self_get_public_key(state1.tox.get(), key); | ||
299 | tox_friend_add_norequest(state2.tox.get(), key, &err); | ||
300 | assert(err == TOX_ERR_FRIEND_ADD_OK); | ||
301 | } | ||
302 | } | ||
303 | } | ||
304 | |||
305 | return toxes; | ||
306 | } | ||
307 | |||
308 | bool all_connected(Global_State const &toxes) { | ||
309 | return std::all_of(toxes.begin(), toxes.end(), [](Local_State const &state) { | ||
310 | return state.friends_online == NUM_TOXES - 1; | ||
311 | }); | ||
312 | } | ||
313 | |||
314 | bool bootstrap_toxes(Global_State &toxes) { | ||
315 | std::printf("Waiting for %u iterations for all friends to come online\n", | ||
316 | MAX_BOOTSTRAP_ITERATIONS); | ||
317 | |||
318 | for (uint32_t i = 0; i < MAX_BOOTSTRAP_ITERATIONS; i++) { | ||
319 | c_sleep(tox_iteration_interval(toxes.front().tox.get())); | ||
320 | |||
321 | for (Local_State &state : toxes) { | ||
322 | tox_iterate(state.tox.get(), &state); | ||
323 | } | ||
324 | |||
325 | if (all_connected(toxes)) { | ||
326 | std::printf("Took %u iterations\n", i); | ||
327 | return true; | ||
328 | } | ||
329 | } | ||
330 | |||
331 | return false; | ||
332 | } | ||
333 | |||
334 | bool execute_random_action(Global_State &toxes, std::mt19937 &rng) { | ||
335 | // First, choose a random actor. | ||
336 | Local_State &actor = toxes.at(toxes.rnd.tox_selector(rng)); | ||
337 | size_t const action_id = toxes.rnd.action_selector(rng); | ||
338 | Action const &action = actions[action_id]; | ||
339 | if (!action.can(actor)) { | ||
340 | return false; | ||
341 | } | ||
342 | |||
343 | std::printf("Tox #%u %s", actor.id(), action.title); | ||
344 | action.run(actor, toxes.rnd, rng); | ||
345 | std::printf("\n"); | ||
346 | |||
347 | toxes.action_counter.at(action_id)++; | ||
348 | |||
349 | return true; | ||
350 | } | ||
351 | |||
352 | bool attempt_action(Global_State &toxes, std::mt19937 &rng) { | ||
353 | for (uint32_t i = 0; i < MAX_ACTION_ATTEMPTS; i++) { | ||
354 | if (execute_random_action(toxes, rng)) { | ||
355 | return true; | ||
356 | } | ||
357 | } | ||
358 | |||
359 | return false; | ||
360 | } | ||
361 | |||
362 | } // namespace | ||
363 | |||
364 | int main() { | ||
365 | Global_State toxes = make_toxes(); | ||
366 | |||
367 | std::mt19937 rng; | ||
368 | uint32_t action_number; | ||
369 | for (action_number = 0; action_number < MAX_ACTIONS; action_number++) { | ||
370 | if (!all_connected(toxes) && !bootstrap_toxes(toxes)) { | ||
371 | std::printf("Bootstrapping took too long; %u actions performed\n", action_number); | ||
372 | return EXIT_FAILURE; | ||
373 | } | ||
374 | |||
375 | if (!attempt_action(toxes, rng)) { | ||
376 | std::printf( | ||
377 | "System is stuck after %u actions: none of the toxes can perform an action anymore\n", | ||
378 | action_number); | ||
379 | return EXIT_FAILURE; | ||
380 | } | ||
381 | |||
382 | for (uint32_t i = 0; i < ITERATIONS_PER_ACTION; i++) { | ||
383 | c_sleep(ITERATION_INTERVAL); | ||
384 | |||
385 | for (Local_State &state : toxes) { | ||
386 | tox_iterate(state.tox.get(), &state); | ||
387 | } | ||
388 | } | ||
389 | } | ||
390 | |||
391 | std::printf("Test execution success: %u actions performed\n", action_number); | ||
392 | std::printf("Per-action statistics:\n"); | ||
393 | for (uint32_t i = 0; i < toxes.action_counter.size(); i++) { | ||
394 | std::printf("%u x '%s'\n", toxes.action_counter.at(i), actions[i].title); | ||
395 | } | ||
396 | } | ||