Shrink.h 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /*
  2. * Copyright (c) 2023, Martin Janiczek <martin@janiczek.cz>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #pragma once
  7. #include <LibTest/Randomized/Generator.h>
  8. #include <LibTest/Randomized/RandomRun.h>
  9. #include <LibTest/Randomized/RandomnessSource.h>
  10. #include <LibTest/Randomized/ShrinkCommand.h>
  11. #include <LibTest/TestResult.h>
  12. #include <AK/String.h>
  13. namespace Test {
  14. namespace Randomized {
  15. enum class WasImprovement {
  16. Yes,
  17. No,
  18. };
  19. struct ShrinkResult {
  20. WasImprovement was_improvement;
  21. RandomRun run;
  22. };
  23. inline ShrinkResult no_improvement(RandomRun run)
  24. {
  25. return ShrinkResult { WasImprovement::No, run };
  26. }
  27. // When calling keep_if_better we have a new RandomRun that might be better than
  28. // our current_best (which is guaranteed to generate a value and fail the test).
  29. //
  30. // We need to try to generate a value from the new_run and run the test. If the
  31. // generated value fails the test, we say it was an improvement (because of our
  32. // convention for generators that _shorter / smaller RandomRuns lead to simpler
  33. // values_). In all other cases we say it wasn't an improvement.
  34. template<typename Fn>
  35. ShrinkResult keep_if_better(RandomRun const& new_run, RandomRun const& current_best, Fn const& test_function)
  36. {
  37. if (!new_run.is_shortlex_smaller_than(current_best))
  38. // The new run is worse or equal to the current best. Let's not even try!
  39. return no_improvement(current_best);
  40. set_randomness_source(RandomnessSource::recorded(new_run));
  41. set_current_test_result(TestResult::NotRun);
  42. test_function();
  43. if (current_test_result() == TestResult::NotRun) {
  44. set_current_test_result(TestResult::Passed);
  45. }
  46. switch (current_test_result()) {
  47. case TestResult::Failed:
  48. // Our smaller RandomRun resulted in a simpler failing value!
  49. // Let's keep it.
  50. return ShrinkResult { WasImprovement::Yes, new_run };
  51. case TestResult::Passed:
  52. case TestResult::Rejected:
  53. case TestResult::Overrun:
  54. // Passing: We shrank from a failing value to a passing value.
  55. // Rejected: We shrank to value that doesn't get past the ASSUME(...)
  56. // macro.
  57. // Overrun: Generators can't draw enough random bits to generate all
  58. // needed values.
  59. // In all cases: Let's try something else.
  60. return no_improvement(current_best);
  61. case TestResult::NotRun:
  62. default:
  63. // We've literally just set it to Passed if it was NotRun!
  64. VERIFY_NOT_REACHED();
  65. return no_improvement(current_best);
  66. }
  67. }
  68. template<typename Fn, typename UpdateRunFn>
  69. ShrinkResult binary_shrink(u64 orig_low, u64 orig_high, UpdateRunFn update_run, RandomRun const& orig_run, Fn const& test_function)
  70. {
  71. if (orig_low == orig_high) {
  72. return no_improvement(orig_run);
  73. }
  74. RandomRun current_best = orig_run;
  75. u64 low = orig_low;
  76. u64 high = orig_high;
  77. // Let's try with the best case (low = most shrunk) first
  78. RandomRun run_with_low = update_run(low, current_best);
  79. ShrinkResult after_low = keep_if_better(run_with_low, current_best, test_function);
  80. switch (after_low.was_improvement) {
  81. case WasImprovement::Yes:
  82. // We can't do any better
  83. return after_low;
  84. case WasImprovement::No:
  85. break;
  86. }
  87. // Ah well, gotta do some actual work.
  88. //
  89. // We're already guaranteed that `high` makes the test fail. We're trying to
  90. // get as low as `low` (but we know `low` doesn't make the test fail, else
  91. // the `if` above would succeed and return).
  92. //
  93. // Failing value above, passing value below; we try to find the lowest value
  94. // that still fails.
  95. //
  96. // `high` is always guaranteed to fail, `low` is always guaranteed to
  97. // pass/reject/overrun.
  98. ShrinkResult result = after_low;
  99. while (low + 1 < high) {
  100. u64 mid = low + (high - low) / 2;
  101. RandomRun run_with_mid = update_run(mid, current_best);
  102. ShrinkResult after_mid = keep_if_better(run_with_mid, current_best, test_function);
  103. switch (after_mid.was_improvement) {
  104. case WasImprovement::Yes:
  105. high = mid;
  106. break;
  107. case WasImprovement::No:
  108. low = mid;
  109. break;
  110. }
  111. result = after_mid;
  112. current_best = after_mid.run;
  113. }
  114. // did we get below the original `high` at all?
  115. if (current_best.is_shortlex_smaller_than(orig_run)) {
  116. result.was_improvement = WasImprovement::Yes;
  117. } else {
  118. result.was_improvement = WasImprovement::No;
  119. result.run = orig_run;
  120. }
  121. set_current_test_result(TestResult::Failed);
  122. return result;
  123. }
  124. template<typename Fn>
  125. ShrinkResult shrink_zero(ZeroChunk command, RandomRun const& run, Fn const& test_function)
  126. {
  127. RandomRun new_run = run;
  128. size_t end = command.chunk.index + command.chunk.size;
  129. for (size_t i = command.chunk.index; i < end; i++) {
  130. new_run[i] = 0;
  131. }
  132. return keep_if_better(new_run, run, test_function);
  133. }
  134. template<typename Fn>
  135. ShrinkResult shrink_sort(SortChunk command, RandomRun const& run, Fn const& test_function)
  136. {
  137. RandomRun new_run = run.with_sorted(command.chunk);
  138. return keep_if_better(new_run, run, test_function);
  139. }
  140. template<typename Fn>
  141. ShrinkResult shrink_delete(DeleteChunkAndMaybeDecPrevious command, RandomRun const& run, Fn const& test_function)
  142. {
  143. RandomRun run_deleted = run.with_deleted(command.chunk);
  144. // Optional: decrement the previous value. This deals with a non-optimal but
  145. // relatively common generation pattern: run length encoding.
  146. //
  147. // Example: let's say somebody generates lists in this way:
  148. // * generate a random integer >=0 for the length of the list.
  149. // * then, generate that many items
  150. //
  151. // This results in RandomRuns like this one:
  152. // [ 3 (length), 50 (item 1), 21 (item 2), 1 (item 3) ]
  153. //
  154. // Then if we tried deleting the second item without decrementing
  155. // the length, it would fail:
  156. // [ 3 (length), 21 (item 1), 1 (item 2) ] ... runs out of randomness when
  157. // trying to generate the third item!
  158. //
  159. // That's why we try to decrement the number right before the deleted items:
  160. // [ 2 (length), 21 (item 1), 1 (item 2) ] ... generates fine!
  161. //
  162. // Aside: this is why we're generating lists in a different way that plays
  163. // nicer with shrinking: we flip a coin (generate a bool which is `true` with
  164. // a certain probability) to see whether to generate another item. This makes
  165. // items "local" instead of entangled with the non-local length.
  166. if (run_deleted.size() > command.chunk.index - 1 && run_deleted[command.chunk.index - 1] > 0) {
  167. RandomRun run_decremented = run_deleted;
  168. run_decremented[command.chunk.index - 1]--;
  169. return keep_if_better(run_decremented, run, test_function);
  170. }
  171. // Decrementing didn't work; let's try with just the deletion.
  172. return keep_if_better(run_deleted, run, test_function);
  173. }
  174. template<typename Fn>
  175. ShrinkResult shrink_minimize(MinimizeChoice command, RandomRun const& run, Fn const& test_function)
  176. {
  177. u64 value = run[command.index];
  178. // We can't minimize 0! Already the best possible case.
  179. if (value == 0) {
  180. return no_improvement(run);
  181. }
  182. return binary_shrink(
  183. 0,
  184. value,
  185. [&](u64 new_value, RandomRun const& run) {
  186. RandomRun copied_run = run;
  187. copied_run[command.index] = new_value;
  188. return copied_run;
  189. },
  190. run,
  191. test_function);
  192. }
  193. template<typename Fn>
  194. ShrinkResult shrink_swap_chunk(SwapChunkWithNeighbour command, RandomRun const& run, Fn const& test_function)
  195. {
  196. RandomRun run_swapped = run;
  197. // The safety of these swaps was guaranteed by has_a_chance() earlier
  198. for (size_t i = command.chunk.index; i < command.chunk.index + command.chunk.size; ++i) {
  199. AK::swap(run_swapped[i], run_swapped[i + command.chunk.size]);
  200. }
  201. return keep_if_better(run_swapped, run, test_function);
  202. }
  203. template<typename Fn>
  204. ShrinkResult shrink_redistribute(RedistributeChoicesAndMaybeInc command, RandomRun const& run, Fn const& test_function)
  205. {
  206. RandomRun current_best = run;
  207. RandomRun run_after_swap = current_best;
  208. // First try to swap them if they're out of order.
  209. if (run_after_swap[command.left_index] > run_after_swap[command.right_index])
  210. AK::swap(run_after_swap[command.left_index], run_after_swap[command.right_index]);
  211. ShrinkResult after_swap = keep_if_better(run_after_swap, current_best, test_function);
  212. current_best = after_swap.run;
  213. u64 constant_sum = current_best[command.right_index] + current_best[command.left_index];
  214. ShrinkResult after_redistribute = binary_shrink(
  215. 0,
  216. current_best[command.left_index],
  217. [&](u64 new_value, RandomRun const& run) {
  218. RandomRun copied_run = run;
  219. copied_run[command.left_index] = new_value;
  220. copied_run[command.right_index] = constant_sum - new_value;
  221. return copied_run;
  222. },
  223. current_best,
  224. test_function);
  225. switch (after_redistribute.was_improvement) {
  226. case WasImprovement::Yes:
  227. return after_redistribute;
  228. break;
  229. case WasImprovement::No:
  230. break;
  231. }
  232. // If the redistribute failed, this can sometimes signal that a value needs
  233. // to fall into the next `int_frequency` bucket. We can try one last-ditch
  234. // attempt and see if incrementing the number right before the right index
  235. // helps.
  236. if (command.left_index == command.right_index - 1) {
  237. // There's no "bucket index" between the left and right index.
  238. // Let's not even try.
  239. return after_swap;
  240. }
  241. RandomRun run_after_increment = after_redistribute.run;
  242. ++run_after_increment[command.right_index - 1];
  243. ShrinkResult after_inc_redistribute = binary_shrink(
  244. 0,
  245. current_best[command.left_index],
  246. [&](u64 new_value, RandomRun const& run) {
  247. RandomRun copied_run = run;
  248. copied_run[command.left_index] = new_value;
  249. copied_run[command.right_index] = constant_sum - new_value;
  250. return copied_run;
  251. },
  252. current_best,
  253. test_function);
  254. switch (after_inc_redistribute.was_improvement) {
  255. case WasImprovement::Yes:
  256. return after_inc_redistribute;
  257. break;
  258. case WasImprovement::No:
  259. break;
  260. }
  261. return after_swap;
  262. }
  263. template<typename Fn>
  264. ShrinkResult shrink_with_command(ShrinkCommand command, RandomRun const& run, Fn const& test_function)
  265. {
  266. return command.visit(
  267. [&](ZeroChunk c) { return shrink_zero(c, run, test_function); },
  268. [&](SortChunk c) { return shrink_sort(c, run, test_function); },
  269. [&](DeleteChunkAndMaybeDecPrevious c) { return shrink_delete(c, run, test_function); },
  270. [&](MinimizeChoice c) { return shrink_minimize(c, run, test_function); },
  271. [&](RedistributeChoicesAndMaybeInc c) { return shrink_redistribute(c, run, test_function); },
  272. [&](SwapChunkWithNeighbour c) { return shrink_swap_chunk(c, run, test_function); });
  273. }
  274. template<typename Fn>
  275. RandomRun shrink_once(RandomRun const& run, Fn const& test_function)
  276. {
  277. RandomRun current = run;
  278. auto commands = ShrinkCommand::for_run(run);
  279. for (ShrinkCommand command : commands) {
  280. // We're keeping the list of ShrinkCommands we generated from the initial
  281. // RandomRun, as we try to shrink our current best RandomRun.
  282. //
  283. // That means some of the ShrinkCommands might have no chance to
  284. // successfully finish (eg. the command's chunk is out of bounds of the
  285. // run). That's what we check here and based on what we skip those
  286. // commands early.
  287. //
  288. // In the next `shrink -> shrink_once` loop we'll generate a better set
  289. // of commands, more tailored to the current best RandomRun.
  290. if (!command.has_a_chance(current)) {
  291. continue;
  292. }
  293. ShrinkResult result = shrink_with_command(command, current, test_function);
  294. switch (result.was_improvement) {
  295. case WasImprovement::Yes:
  296. current = result.run;
  297. break;
  298. case WasImprovement::No:
  299. break;
  300. }
  301. }
  302. return current;
  303. }
  304. template<typename Fn>
  305. RandomRun shrink(RandomRun const& first_failure, Fn const& test_function)
  306. {
  307. if (first_failure.is_empty()) {
  308. // We can't do any better
  309. return first_failure;
  310. }
  311. RandomRun next = first_failure;
  312. RandomRun current;
  313. do {
  314. current = next;
  315. next = shrink_once(current, test_function);
  316. } while (next.is_shortlex_smaller_than(current));
  317. set_current_test_result(TestResult::Failed);
  318. return next;
  319. }
  320. } // namespace Randomized
  321. } // namespace Test