tigerbeetle 0.0.34 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/ext/tb_client/extconf.rb +13 -13
  4. data/ext/tb_client/tigerbeetle/LICENSE +177 -0
  5. data/ext/tb_client/tigerbeetle/build.zig +2327 -0
  6. data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
  7. data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
  8. data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
  9. data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
  10. data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
  11. data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
  12. data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
  13. data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
  14. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
  15. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
  16. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
  17. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
  18. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
  19. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
  20. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
  21. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
  22. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
  23. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
  24. data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
  25. data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
  26. data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
  27. data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
  28. data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
  29. data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
  30. data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
  31. data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
  32. data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
  33. data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
  34. data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
  35. data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
  36. data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
  37. data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
  38. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
  39. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
  40. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
  41. data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
  42. data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
  43. data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
  44. data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
  45. data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
  46. data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
  47. data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
  48. data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
  49. data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
  50. data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
  51. data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
  52. data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
  53. data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
  54. data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
  55. data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
  56. data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
  57. data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
  58. data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
  59. data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
  60. data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
  61. data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
  62. data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
  63. data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
  64. data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
  65. data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
  66. data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
  67. data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
  68. data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
  69. data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
  70. data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
  71. data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
  72. data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
  73. data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
  74. data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
  75. data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
  76. data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
  77. data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
  78. data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
  79. data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
  80. data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
  81. data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
  82. data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
  83. data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
  84. data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
  85. data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
  86. data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
  87. data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
  88. data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
  89. data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
  90. data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
  91. data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
  92. data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
  93. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
  94. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
  95. data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
  96. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
  97. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
  98. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
  99. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
  100. data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
  101. data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
  102. data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
  103. data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
  104. data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
  105. data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
  106. data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
  107. data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
  108. data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
  109. data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
  110. data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
  111. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
  112. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
  113. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
  114. data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
  115. data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
  116. data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
  117. data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
  118. data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
  119. data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
  120. data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
  121. data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
  122. data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
  123. data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
  124. data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
  125. data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
  126. data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
  127. data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
  128. data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
  129. data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
  130. data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
  131. data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
  132. data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
  133. data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
  134. data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
  135. data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
  136. data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
  137. data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
  138. data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
  139. data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
  140. data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
  141. data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
  142. data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
  143. data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
  144. data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
  145. data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
  146. data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
  147. data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
  148. data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
  149. data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
  150. data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
  151. data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
  152. data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
  153. data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
  154. data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
  155. data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
  156. data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
  157. data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
  158. data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
  159. data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
  160. data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
  161. data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
  162. data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
  163. data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
  164. data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
  165. data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
  166. data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
  167. data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
  168. data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
  169. data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
  170. data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
  171. data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
  172. data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
  173. data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
  174. data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
  175. data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
  176. data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
  177. data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
  178. data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
  179. data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
  180. data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
  181. data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
  182. data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
  183. data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
  184. data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
  185. data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
  186. data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
  187. data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
  188. data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
  189. data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
  190. data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
  191. data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
  192. data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
  193. data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
  194. data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
  195. data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
  196. data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
  197. data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
  198. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
  199. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
  200. data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
  201. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
  202. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
  203. data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
  204. data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
  205. data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
  206. data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
  207. data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
  208. data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
  209. data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
  210. data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
  211. data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
  212. data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
  213. data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
  214. data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
  215. data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
  216. data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
  217. data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
  218. data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
  219. data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
  220. data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
  221. data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
  222. data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
  223. data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
  224. data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
  225. data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
  226. data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
  227. data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
  228. data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
  229. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
  230. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
  231. data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
  232. data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
  233. data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
  234. data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
  235. data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
  236. data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
  237. data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
  238. data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
  239. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
  240. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
  241. data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
  242. data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
  243. data/lib/tb_client/shared_lib.rb +12 -5
  244. data/lib/tigerbeetle/client.rb +1 -1
  245. data/lib/tigerbeetle/platforms.rb +9 -0
  246. data/lib/tigerbeetle/version.rb +2 -2
  247. data/tigerbeetle.gemspec +22 -5
  248. metadata +242 -3
  249. data/ext/tb_client/pkg.tar.gz +0 -0
@@ -0,0 +1,1866 @@
1
+ //! Continuous Fuzzing Orchestrator.
2
+ //!
3
+ //! We have a number of machines which run
4
+ //!
5
+ //! git clone https://github.com/tigerbeetle/tigerbeetle && cd tigerbeetle
6
+ //! while True:
7
+ //! git fetch origin && git reset --hard origin/main
8
+ //! ./zig/download.sh
9
+ //! ./zig/zig build scripts -- cfo
10
+ //!
11
+ //! By modifying this script, we can make those machines do interesting things.
12
+ //!
13
+ //! The primary use-case is fuzzing: `cfo` runs a random fuzzer, and, if it finds a failure, it is
14
+ //! recorded in devhubdb.
15
+ //!
16
+ //! Specifically:
17
+ //!
18
+ //! CFO keeps `args.concurrency` fuzzes running at the same time. For simplicity, it polls currently
19
+ //! running fuzzers for completion every second in a fuzzing loop. A fuzzer fails if it returns
20
+ //! non-zero error code.
21
+ //!
22
+ //! The fuzzing loop runs for `args.budget`. Any fuzzer that runs for longer than
23
+ //! `args.timeout` is terminated and recorded as a failure. At the end of the fuzzing loop,
24
+ //! any fuzzers that are still running are cancelled. Cancelled seeds are not recorded.
25
+ //!
26
+ //! Note that the budget/refresh timers do not count time spent cloning or compiling code.
27
+ //!
28
+ //! The CFO uses Linux's process namespaces to ensure that all descendant processes are reaped.
29
+ //!
30
+ //! Every `args.refresh`, and at the end of the fuzzing loop:
31
+ //! 1. CFO collects a list of seeds (some of which are failing),
32
+ //! 2. merges this list into the previous set of seeds,
33
+ //! 3. pushes the new list to https://github.com/tigerbeetle/devhubdb/
34
+ //!
35
+ //! Rules for merging:
36
+ //!
37
+ //! - Keep seeds for at most `commit_count_max` distinct commits.
38
+ //! - Prefer fresher commits (based on commit time stamp).
39
+ //! - For each commit and fuzzer combination, keep at most `seed_count_max` seeds.
40
+ //! - Prefer failing seeds to successful seeds.
41
+ //! - Prefer seeds that failed faster.
42
+ //! - Prefer older seeds.
43
+ //! - When dropping a non-failing seed, add its count to some other non-failing seeds.
44
+ //!
45
+ //! The idea here is that we want to keep the set of failing seeds stable, while maintaining some
46
+ //! measure of how much fuzzing work was done in total.
47
+
48
+ const std = @import("std");
49
+ const builtin = @import("builtin");
50
+ const log = std.log;
51
+ const assert = std.debug.assert;
52
+ const maybe = stdx.maybe;
53
+
54
+ const stdx = @import("stdx");
55
+ const Shell = @import("../shell.zig");
56
+
57
+ const MiB = stdx.MiB;
58
+ const log_size_max = 4 * stdx.MiB;
59
+
60
+ pub const CLIArgs = struct {
61
+ /// How long to run the cfo before exiting (so that cfo_supervisor can refresh our code).
62
+ budget: stdx.Duration = .minutes(60),
63
+ /// The interval for flushing accumulated seeds to the devhub.
64
+ /// In addition to this interval, any remaining seeds will be uploaded at the end of the budget.
65
+ refresh: stdx.Duration = .minutes(5),
66
+ /// A fuzzer which takes longer than this timeout is killed and counts as a failure.
67
+ timeout: stdx.Duration = .minutes(30),
68
+ concurrency: ?u32 = null,
69
+ };
70
+
71
+ const Fuzzer = enum {
72
+ canary,
73
+ ewah,
74
+ lsm_cache_map,
75
+ lsm_forest,
76
+ lsm_manifest_level,
77
+ lsm_manifest_log,
78
+ lsm_scan,
79
+ lsm_segmented_array,
80
+ lsm_tree,
81
+ message_bus,
82
+ storage,
83
+ vopr_debug,
84
+ vopr_lite,
85
+ vopr_testing_lite,
86
+ vopr_testing,
87
+ vopr,
88
+ vortex,
89
+ vsr_free_set,
90
+ vsr_superblock_quorums,
91
+ vsr_superblock,
92
+ vsr_multi_batch,
93
+ signal,
94
+ state_machine,
95
+
96
+ const weights = std.enums.EnumArray(Fuzzer, u32).init(.{
97
+ .canary = 1,
98
+ .ewah = 1,
99
+ .lsm_cache_map = 2,
100
+ .lsm_forest = 4,
101
+ .lsm_manifest_level = 2,
102
+ .lsm_manifest_log = 2,
103
+ .lsm_scan = 2,
104
+ .lsm_segmented_array = 1,
105
+ .lsm_tree = 2,
106
+ .message_bus = 2,
107
+ .storage = 1,
108
+ .vopr_debug = 1,
109
+ .vopr_lite = 8,
110
+ .vopr_testing_lite = 8,
111
+ .vopr_testing = 8,
112
+ .vopr = 8,
113
+ .vortex = 1,
114
+ .vsr_free_set = 1,
115
+ .vsr_superblock_quorums = 1,
116
+ .vsr_superblock = 1,
117
+ .vsr_multi_batch = 1,
118
+ .signal = 1,
119
+ .state_machine = 2,
120
+ });
121
+
122
+ fn args_build(comptime fuzzer: Fuzzer) []const []const u8 {
123
+ return comptime switch (fuzzer) {
124
+ .vopr_debug,
125
+ => &.{"vopr:build"},
126
+ .vopr,
127
+ .vopr_lite,
128
+ => &.{ "vopr:build", "-Drelease" },
129
+ .vopr_testing,
130
+ .vopr_testing_lite,
131
+ => &.{ "vopr:build", "-Drelease", "-Dvopr-state-machine=testing" },
132
+ // TODO Once release builds have stack traces, add -Drelease so that we are testing
133
+ // release binaries (to test as close as possible to a real cluster).
134
+ // (Likewise in args_run()).
135
+ .vortex => &.{"vortex:build"},
136
+ else => &.{ "fuzz:build", "-Drelease" },
137
+ };
138
+ }
139
+
140
+ fn args_run(comptime fuzzer: Fuzzer) []const []const u8 {
141
+ return comptime switch (fuzzer) {
142
+ .vopr_debug => .{"vopr"},
143
+ .vopr, .vopr_lite => .{ "vopr", "-Drelease" },
144
+ .vopr_testing,
145
+ .vopr_testing_lite,
146
+ => .{ "vopr", "-Drelease", "-Dvopr-state-machine=testing" },
147
+ .vortex => .{"vortex"},
148
+ else => .{ "fuzz", "-Drelease" },
149
+ } ++ .{"--"} ++ args_exec(fuzzer);
150
+ }
151
+
152
+ fn args_exec(comptime fuzzer: Fuzzer) []const []const u8 {
153
+ return comptime switch (fuzzer) {
154
+ .vopr, .vopr_debug, .vopr_testing => &.{},
155
+ .vopr_lite, .vopr_testing_lite => &.{"--lite"},
156
+ .vortex => &.{
157
+ "supervisor",
158
+ "--log-debug",
159
+ "--replica-count=3",
160
+ "--test-duration=10m",
161
+ },
162
+ else => |f| &.{@tagName(f)},
163
+ };
164
+ }
165
+
166
+ fn capture_logs(fuzzer: Fuzzer) bool {
167
+ return fuzzer == .vortex;
168
+ }
169
+ };
170
+
171
+ pub fn main(shell: *Shell, gpa: std.mem.Allocator, cli_args: CLIArgs) !void {
172
+ if (builtin.os.tag == .windows) {
173
+ log.err("cfo is not supported on Windows", .{});
174
+ return error.NotSupported;
175
+ }
176
+
177
+ if (cli_args.budget.ns == 0) fatal("--budget: must be greater than zero", .{});
178
+ if (cli_args.refresh.ns == 0) fatal("--refresh: must be greater than zero", .{});
179
+ if (cli_args.timeout.ns == 0) fatal("--timeout: must be greater than zero", .{});
180
+
181
+ if (cli_args.budget.ns < cli_args.timeout.ns) {
182
+ log.warn("budget={} is less than timeout={}; no seeds will time out", .{
183
+ cli_args.budget,
184
+ cli_args.timeout,
185
+ });
186
+ }
187
+
188
+ log.info("start {}", .{stdx.InstantUnix.now()});
189
+ defer log.info("end {}", .{stdx.InstantUnix.now()});
190
+
191
+ try shell.exec("git --version", .{});
192
+
193
+ // Read-write token for <https://github.com/tigerbeetle/devhubdb>.
194
+ // See `src/scripts/devhub.zig` for generation instructions.
195
+ const devhub_token_option = shell.env_get_option("DEVHUBDB_PAT");
196
+ if (devhub_token_option == null) {
197
+ log.err("'DEVHUBDB_PAT' environmental variable is not set, will not upload results", .{});
198
+ }
199
+
200
+ // Readonly token for PR metadata of <https://github.com/tigerbeetle/tigerbeetle>.
201
+ const gh_token_option = shell.env_get_option("GH_TOKEN");
202
+ if (gh_token_option == null) {
203
+ log.err("'GH_TOKEN' environmental variable is not set, will not fetch pull requests", .{});
204
+ } else {
205
+ try shell.exec("gh --version", .{});
206
+ }
207
+
208
+ try run_fuzzers(shell, gpa, gh_token_option, .{
209
+ .concurrency = cli_args.concurrency orelse try std.Thread.getCpuCount(),
210
+ .budget = cli_args.budget,
211
+ .refresh = cli_args.refresh,
212
+ .timeout = cli_args.timeout,
213
+ .devhub_token = devhub_token_option,
214
+ });
215
+
216
+ log.info("memory = {}B", .{shell.arena.queryCapacity()});
217
+ }
218
+
219
+ /// Format and print an error message to stderr, then exit with an exit code of 1.
220
+ fn fatal(comptime fmt_string: []const u8, args: anytype) noreturn {
221
+ const stderr = std.io.getStdErr().writer();
222
+ stderr.print("error: " ++ fmt_string ++ "\n", args) catch {};
223
+ std.process.exit(1);
224
+ }
225
+
226
+ fn run_fuzzers(
227
+ shell: *Shell,
228
+ gpa: std.mem.Allocator,
229
+ gh_token: ?[]const u8,
230
+ options: struct {
231
+ concurrency: usize,
232
+ budget: stdx.Duration,
233
+ refresh: stdx.Duration,
234
+ timeout: stdx.Duration,
235
+ devhub_token: ?[]const u8,
236
+ },
237
+ ) !void {
238
+ var seeds = std.ArrayListUnmanaged(SeedRecord){};
239
+ defer seeds.deinit(gpa);
240
+
241
+ var seed_logs = std.ArrayListUnmanaged(?[]const u8){};
242
+ defer {
243
+ for (seed_logs.items) |log_or_null| {
244
+ if (log_or_null) |log_buffer| gpa.free(log_buffer);
245
+ }
246
+ seed_logs.deinit(gpa);
247
+ }
248
+
249
+ const random = std.crypto.random;
250
+
251
+ const FuzzerChild = struct {
252
+ fuzzer: Fuzzer,
253
+ child: std.process.Child,
254
+ log_path: ?[]const u8,
255
+ seed: SeedRecord,
256
+ };
257
+
258
+ const children = try shell.arena.allocator().alloc(?FuzzerChild, options.concurrency);
259
+ @memset(children, null);
260
+ defer for (children) |*fuzzer_or_null| {
261
+ if (fuzzer_or_null.*) |*fuzzer| {
262
+ _ = fuzzer.child.kill() catch {};
263
+ fuzzer_or_null.* = null;
264
+ }
265
+ };
266
+
267
+ var tasks = Tasks.init(shell.arena.allocator());
268
+ defer tasks.deinit();
269
+
270
+ var budget_timer = try std.time.Timer.start();
271
+ const sleep_ns = 100 * std.time.ns_per_ms;
272
+
273
+ var refresh_timer = try std.time.Timer.start();
274
+ var refresh_first = true;
275
+
276
+ while (true) {
277
+ const iteration_refresh = refresh_first or refresh_timer.read() >= options.refresh.ns;
278
+ if (iteration_refresh) refresh_timer.reset();
279
+
280
+ const iteration_last = budget_timer.read() >= options.budget.ns;
281
+ const iteration_push = (iteration_refresh and !refresh_first) or iteration_last;
282
+ refresh_first = false;
283
+
284
+ if (iteration_refresh) {
285
+ try run_fuzzers_prepare_tasks(&tasks, shell, gh_token);
286
+
287
+ for (tasks.list.items) |*task| {
288
+ if (task.generation == tasks.generation) {
289
+ log.info(
290
+ "fuzzing commit={s} timestamp={} fuzzer={s} branch='{s}' weight={}",
291
+ .{
292
+ task.seed_template.commit_sha[0..7],
293
+ task.seed_template.commit_timestamp,
294
+ @tagName(task.seed_template.fuzzer),
295
+ task.seed_template.branch_url,
296
+ task.weight,
297
+ },
298
+ );
299
+ }
300
+ }
301
+ }
302
+
303
+ // Start new fuzzer processes.
304
+ for (children) |*child_or_null| {
305
+ if (child_or_null.* == null) {
306
+ const task = tasks.sample();
307
+ const seed = random.int(u64);
308
+
309
+ // Ensure that multiple fuzzers spawned in the same tick are spread out over tasks.
310
+ task.runtime_virtual += @divFloor(sleep_ns, task.weight);
311
+
312
+ const child = try run_fuzzers_start_fuzzer(shell, .{
313
+ .working_directory = task.working_directory,
314
+ .fuzzer = task.seed_template.fuzzer,
315
+ .seed = seed,
316
+ });
317
+ // NB: take timestamp after spawning to exclude build time.
318
+ const seed_timestamp_start: u64 = @intCast(std.time.nanoTimestamp());
319
+
320
+ child_or_null.* = .{
321
+ .fuzzer = task.seed_template.fuzzer,
322
+ .child = child.process,
323
+ .log_path = child.log,
324
+ .seed = .{
325
+ .commit_timestamp = task.seed_template.commit_timestamp,
326
+ .commit_sha = task.seed_template.commit_sha,
327
+ .fuzzer = @tagName(task.seed_template.fuzzer),
328
+ .branch = task.seed_template.branch_url,
329
+
330
+ .count = 1,
331
+ .seed_timestamp_start = seed_timestamp_start,
332
+ .seed = seed,
333
+ .command = child.command,
334
+
335
+ .ok = false,
336
+ .seed_timestamp_end = 0,
337
+ },
338
+ };
339
+ }
340
+ }
341
+
342
+ // Wait before polling for completion, to avoid hogging the CPU.
343
+ std.time.sleep(sleep_ns);
344
+
345
+ var running_count: u32 = 0;
346
+ for (children) |*fuzzer_or_null| {
347
+ // Poll for completed fuzzers.
348
+
349
+ if (fuzzer_or_null.*) |*fuzzer| {
350
+ running_count += 1;
351
+
352
+ // Update runtime_virtual incrementally every tick so that we have an accurate score
353
+ // for choosing new tasks.
354
+ const task = tasks.get(
355
+ std.meta.stringToEnum(Fuzzer, fuzzer.seed.fuzzer).?,
356
+ fuzzer.seed.commit_sha,
357
+ fuzzer.seed.branch,
358
+ ).?;
359
+ task.runtime_virtual += @divFloor(sleep_ns, task.weight);
360
+
361
+ var fuzzer_done = false;
362
+ _ = fuzzer.child.stdin.?.write(&.{1}) catch |err| {
363
+ switch (err) {
364
+ error.WouldBlock => {},
365
+ error.BrokenPipe => fuzzer_done = true,
366
+ else => return err,
367
+ }
368
+ };
369
+
370
+ const seed_timestamp_start_ns = fuzzer.seed.seed_timestamp_start;
371
+ const seed_duration_ns =
372
+ @as(u64, @intCast(std.time.nanoTimestamp())) - seed_timestamp_start_ns;
373
+ const seed_expired = !fuzzer_done and seed_duration_ns > options.timeout.ns;
374
+
375
+ if (fuzzer_done or seed_expired or iteration_last) {
376
+ log.debug("will reap '{s}' after {}ms{s}", .{
377
+ fuzzer.seed.command,
378
+ @divFloor(seed_duration_ns, std.time.ns_per_ms),
379
+ if (fuzzer_done) "" else " (timeout)",
380
+ });
381
+
382
+ const term = try if (fuzzer_done) fuzzer.child.wait() else fuzzer.child.kill();
383
+ const term_adapted: enum { sigkill, sigterm, other } = term: {
384
+ const code_kill = 128 + std.posix.SIG.KILL;
385
+ const code_term = 128 + std.posix.SIG.TERM;
386
+ const vortex = fuzzer.fuzzer == .vortex;
387
+
388
+ if (std.meta.eql(term, .{ .Signal = std.posix.SIG.KILL }) or
389
+ (std.meta.eql(term, .{ .Exited = @intCast(code_kill) }) and vortex))
390
+ {
391
+ // Something killed the fuzzer. This is likely OOM, so count this seed
392
+ // neither as a success, nor as a failure.
393
+ //
394
+ // Special case Vortex because one of Vortex's child processes might
395
+ // have been SIGKILL'd, which the supervisor bubbles up as 128+SIGKILL.
396
+ // (It can't just SIGKILL itself because the supervisor is immune to
397
+ // internal SIGKILL as it is the init process of a pid namespace.)
398
+ break :term .sigkill;
399
+ }
400
+
401
+ if (iteration_last) {
402
+ if (std.meta.eql(term, .{ .Signal = std.posix.SIG.TERM }) or
403
+ (std.meta.eql(term, .{ .Exited = @intCast(code_term) }) and vortex))
404
+ {
405
+ // We killed the fuzzer because our budgeted time is expired, but
406
+ // the seed itself is indeterminate.
407
+ //
408
+ // Special case Vortex because it uses a handler for SIGTERM to
409
+ // ensure the supervisor's pid namespace is torn down.
410
+ break :term .sigterm;
411
+ }
412
+ }
413
+ break :term .other;
414
+ };
415
+
416
+ switch (term_adapted) {
417
+ .sigkill => log.info("ignored SIGKILL for '{s}'", .{fuzzer.seed.command}),
418
+ .sigterm => log.info("ignored SIGTERM for '{s}'", .{fuzzer.seed.command}),
419
+ .other => {
420
+ var seed_record = fuzzer.seed;
421
+ seed_record.ok = std.meta.eql(term, .{ .Exited = 0 });
422
+ // Convert seed_timestamp_start to seconds as `devhub.js` relies on it.
423
+ seed_record.seed_timestamp_start = @divFloor(
424
+ seed_timestamp_start_ns,
425
+ std.time.ns_per_s,
426
+ );
427
+ seed_record.seed_timestamp_end = @intCast(std.time.timestamp());
428
+ if (!seed_record.ok) {
429
+ seed_record.debug = try shell.fmt("{}", .{term});
430
+ }
431
+
432
+ if (seed_record.ok or !fuzzer.fuzzer.capture_logs()) {
433
+ try seed_logs.append(gpa, null);
434
+ } else done: {
435
+ const log_file =
436
+ shell.cwd.openFile(fuzzer.log_path.?, .{}) catch |err| {
437
+ switch (err) {
438
+ error.FileNotFound => {
439
+ try seed_logs.append(gpa, null);
440
+ break :done;
441
+ },
442
+ else => return err,
443
+ }
444
+ };
445
+ defer log_file.close();
446
+
447
+ // Copy the tail of the (failing seed's) logs into a buffer.
448
+ const log_data = try gpa.alloc(u8, log_size_max);
449
+ errdefer gpa.free(log_data);
450
+
451
+ const log_size_total = (try log_file.metadata()).size();
452
+ try log_file.seekTo(log_size_total -| log_size_max);
453
+
454
+ const log_tail_size = try log_file.readAll(log_data);
455
+ assert(log_tail_size == @min(log_size_max, log_size_total));
456
+
457
+ try seed_logs.append(gpa, log_data[0..log_tail_size]);
458
+ seed_record.log = try create_log_path(shell.arena.allocator());
459
+ }
460
+ try seeds.append(gpa, seed_record);
461
+ },
462
+ }
463
+
464
+ if (std.meta.eql(term, .{ .Signal = std.posix.SIG.ABRT })) {
465
+ // Sanity-check that we definitely record all assertion failures.
466
+ assert(!seeds.getLast().ok);
467
+ }
468
+ task.runtime_total_ns += seed_duration_ns;
469
+
470
+ if (fuzzer.log_path) |log_path| {
471
+ shell.cwd.deleteFile(log_path) catch |err| {
472
+ log.warn("error deleting log file: {} {s}", .{ err, log_path });
473
+ };
474
+ }
475
+
476
+ fuzzer_or_null.* = null;
477
+ }
478
+ }
479
+ }
480
+ assert(running_count == options.concurrency);
481
+
482
+ if (iteration_push) {
483
+ try upload_results(shell, gpa, options.devhub_token, seeds.items, seed_logs.items);
484
+ if (options.devhub_token == null) {
485
+ log.info("skipping upload, no token", .{});
486
+ for (seeds.items) |seed_record| {
487
+ const seed_record_json = try std.json.stringifyAlloc(
488
+ shell.arena.allocator(),
489
+ seed_record,
490
+ .{},
491
+ );
492
+ log.info("{s}", .{seed_record_json});
493
+ }
494
+ }
495
+ for (seed_logs.items) |log_or_null| {
496
+ if (log_or_null) |log_buffer| gpa.free(log_buffer);
497
+ }
498
+ seed_logs.clearRetainingCapacity();
499
+ seeds.clearRetainingCapacity();
500
+ }
501
+
502
+ if (iteration_last) break;
503
+ }
504
+ assert(seeds.items.len == 0);
505
+
506
+ var runtime_total_ns: u64 = 0;
507
+ for (tasks.list.items) |*task| runtime_total_ns += task.runtime_total_ns;
508
+ for (tasks.list.items) |*task| {
509
+ log.info("commit={s} fuzzer={s:<24} runtime={}s {d:.2}% (active={})", .{
510
+ task.seed_template.commit_sha[0..7],
511
+ @tagName(task.seed_template.fuzzer),
512
+ @divFloor(task.runtime_total_ns, std.time.ns_per_s),
513
+ @as(f64, @floatFromInt(task.runtime_total_ns * 100)) /
514
+ @as(f64, @floatFromInt(runtime_total_ns)),
515
+ task.generation == tasks.generation,
516
+ });
517
+ }
518
+ }
519
+
520
+ const Tasks = struct {
521
+ /// Map values index into `list`.
522
+ const Map = std.AutoHashMap(struct {
523
+ fuzzer: Fuzzer,
524
+ commit: [40]u8,
525
+ branch: SeedRecord.Template.Branch,
526
+ }, usize);
527
+ const List = std.ArrayList(Task);
528
+
529
+ const Task = struct {
530
+ // Immutable:
531
+
532
+ working_directory: []const u8,
533
+ seed_template: SeedRecord.Template,
534
+
535
+ // Mutable:
536
+
537
+ /// Higher weight fuzzers are given more runtime.
538
+ weight: u32,
539
+ /// Active tasks have `task.generation == tasks.generation`.
540
+ /// Inactive tasks have `task.generation < tasks.generation`.
541
+ generation: u64,
542
+ /// This is just used for logging, not scheduling.
543
+ runtime_total_ns: u64,
544
+ /// Weight-adjusted runtime used for scheduling. Always positive and finite.
545
+ /// Cumulative `runtime / weight`, but since `weight` can change over time, this is more
546
+ /// precise.
547
+ runtime_virtual: u64,
548
+ };
549
+
550
+ generation: u64 = 1,
551
+ runtime_virtual_init: u64 = 1,
552
+
553
+ list: List,
554
+ map: Map,
555
+
556
+ pub fn init(allocator: std.mem.Allocator) Tasks {
557
+ return .{
558
+ .list = Tasks.List.init(allocator),
559
+ .map = Tasks.Map.init(allocator),
560
+ };
561
+ }
562
+
563
+ pub fn deinit(tasks: *Tasks) void {
564
+ tasks.map.deinit();
565
+ tasks.list.deinit();
566
+ tasks.* = undefined;
567
+ }
568
+
569
+ pub fn verify(tasks: *const Tasks) void {
570
+ assert(tasks.list.items.len == tasks.map.count());
571
+
572
+ var map_iterator = tasks.map.iterator();
573
+ while (map_iterator.next()) |map_entry| {
574
+ const task_index = map_entry.value_ptr.*;
575
+ const task = &tasks.list.items[task_index];
576
+ assert(@intFromPtr(task) >= @intFromPtr(tasks.list.items.ptr));
577
+ assert(@intFromPtr(task) < @intFromPtr(tasks.list.items.ptr) +
578
+ @sizeOf(Task) * tasks.list.items.len);
579
+ assert(task.seed_template.fuzzer == map_entry.key_ptr.fuzzer);
580
+ assert(std.mem.eql(u8, &task.seed_template.commit_sha, &map_entry.key_ptr.commit));
581
+ }
582
+
583
+ for (tasks.list.items) |*task| {
584
+ assert(task.generation <= tasks.generation);
585
+ assert(task.weight > 0);
586
+ assert(task.runtime_virtual >= 1);
587
+ }
588
+ }
589
+
590
+ /// Pick a task to run next.
591
+ /// Returns the task with the minimal virtual runtime.
592
+ /// Break ties by choosing the task with the greater weight.
593
+ pub fn sample(tasks: *const Tasks) *Task {
594
+ assert(tasks.list.items.len == tasks.map.count());
595
+ assert(tasks.list.items.len > 0);
596
+
597
+ var task_best: ?*Task = null;
598
+ for (tasks.list.items) |*task| {
599
+ assert(task.runtime_virtual > 0);
600
+ assert(task.generation <= tasks.generation);
601
+
602
+ if (task.generation == tasks.generation) {
603
+ if (task_best == null or
604
+ task_best.?.runtime_virtual > task.runtime_virtual or
605
+ (task_best.?.runtime_virtual == task.runtime_virtual and
606
+ task_best.?.weight < task.weight))
607
+ {
608
+ task_best = task;
609
+ }
610
+ }
611
+ }
612
+ return task_best.?;
613
+ }
614
+
615
+ pub fn get(
616
+ tasks: *const Tasks,
617
+ fuzzer: Fuzzer,
618
+ commit: [40]u8,
619
+ branch_url: []const u8,
620
+ ) ?*Task {
621
+ const branch = SeedRecord.Template.Branch.parse(branch_url) catch unreachable;
622
+ const task_index = tasks.map.get(.{
623
+ .fuzzer = fuzzer,
624
+ .commit = commit,
625
+ .branch = branch,
626
+ }) orelse return null;
627
+ return &tasks.list.items[task_index];
628
+ }
629
+
630
+ /// Either:
631
+ /// - If the specified task does not already exist, create it.
632
+ /// - Is the specified task does already exist, activate it for the new generation.
633
+ pub fn put(
634
+ tasks: *Tasks,
635
+ working_directory: []const u8,
636
+ seed_template: SeedRecord.Template,
637
+ ) !void {
638
+ const branch = SeedRecord.Template.Branch.parse(seed_template.branch_url) catch unreachable;
639
+ if (tasks.map.get(.{
640
+ .fuzzer = seed_template.fuzzer,
641
+ .commit = seed_template.commit_sha,
642
+ .branch = branch,
643
+ })) |task_existing_index| {
644
+ const task_existing = &tasks.list.items[task_existing_index];
645
+ assert(task_existing.generation < tasks.generation);
646
+ assert(task_existing.seed_template.fuzzer == seed_template.fuzzer);
647
+ assert(std.mem.eql(u8, task_existing.working_directory, working_directory));
648
+
649
+ if (tasks.runtime_virtual_init < task_existing.runtime_virtual) {
650
+ tasks.runtime_virtual_init = task_existing.runtime_virtual;
651
+ } else {
652
+ if (task_existing.generation == tasks.generation - 1) {
653
+ // For tasks that were already active, leave their low `runtime_virtual`
654
+ // unmodified, to ensure they get some runtime soon.
655
+ } else {
656
+ // For tasks which were active in the past, but not in the latest generation,
657
+ // ensure that they are not starved in the new generation.
658
+ task_existing.runtime_virtual = tasks.runtime_virtual_init;
659
+ }
660
+ }
661
+ task_existing.generation = tasks.generation;
662
+ } else {
663
+ try tasks.list.append(.{
664
+ .working_directory = working_directory,
665
+ .seed_template = seed_template,
666
+ .generation = tasks.generation,
667
+ .weight = 0, // To be initialized later.
668
+ .runtime_total_ns = 0,
669
+ .runtime_virtual = tasks.runtime_virtual_init,
670
+ });
671
+
672
+ try tasks.map.putNoClobber(.{
673
+ .fuzzer = seed_template.fuzzer,
674
+ .commit = seed_template.commit_sha,
675
+ .branch = branch,
676
+ }, tasks.list.items.len - 1);
677
+ }
678
+ }
679
+
680
+ pub fn soft_remove_all(tasks: *Tasks) void {
681
+ tasks.verify();
682
+ tasks.generation += 1;
683
+ }
684
+ };
685
+
686
+ fn run_fuzzers_prepare_tasks(tasks: *Tasks, shell: *Shell, gh_token: ?[]const u8) !void {
687
+ tasks.soft_remove_all();
688
+ defer tasks.verify();
689
+
690
+ for ([2]SeedRecord.Template.Branch{ .main, .release }) |branch| {
691
+ // Fuzz in-place when no token is specified, for testing coordinated fuzzer changes.
692
+ // But also fuzz on the release branch, to make sure CFO doesn't break on older branches.
693
+ const local_branch = branch == .main and gh_token == null;
694
+ const working_directory = if (local_branch)
695
+ "."
696
+ else
697
+ try shell.fmt("./working/{s}", .{@tagName(branch)});
698
+ const commit = if (local_branch)
699
+ try run_fuzzers_commit_info(shell)
700
+ else commit: {
701
+ try shell.cwd.makePath(working_directory);
702
+ try shell.pushd(working_directory);
703
+ defer shell.popd();
704
+
705
+ // Fuzz an independent clone of the repository, so that CFO and the fuzzer could be on
706
+ // different branches (to fuzz PRs and releases).
707
+ break :commit try run_fuzzers_prepare_repository(shell, .{
708
+ .branch = @tagName(branch),
709
+ });
710
+ };
711
+
712
+ // Only add fuzzers that also exist on the branch we are fuzzing.
713
+ const branch_cfo = try shell.cwd.readFileAlloc(
714
+ shell.arena.allocator(),
715
+ try shell.fmt("{s}/src/scripts/cfo.zig", .{working_directory}),
716
+ 1 * MiB,
717
+ );
718
+
719
+ for (std.enums.values(Fuzzer)) |fuzzer| {
720
+ const fuzzer_present_on_branch = switch (fuzzer) {
721
+ .vopr, .vopr_debug, .vopr_lite, .vopr_testing, .vopr_testing_lite => true,
722
+ else => std.mem.indexOf(
723
+ u8,
724
+ branch_cfo,
725
+ try shell.fmt(" {s},", .{@tagName(fuzzer)}), // A field in const Fuzzer enum.
726
+ ) != null,
727
+ };
728
+ if (fuzzer == .canary) assert(fuzzer_present_on_branch);
729
+
730
+ if (fuzzer_present_on_branch) {
731
+ try tasks.put(working_directory, .{
732
+ .commit_timestamp = commit.timestamp,
733
+ .commit_sha = commit.sha,
734
+ .fuzzer = fuzzer,
735
+ .branch = branch,
736
+ .branch_url = switch (branch) {
737
+ .main => "https://github.com/tigerbeetle/tigerbeetle",
738
+ .release => "https://github.com/tigerbeetle/tigerbeetle/tree/release",
739
+ else => unreachable,
740
+ },
741
+ });
742
+ }
743
+ }
744
+ }
745
+
746
+ if (gh_token != null) {
747
+ // Any PR labeled like 'fuzz lsm_tree'
748
+ const GhPullRequest = struct {
749
+ const Label = struct {
750
+ id: []const u8,
751
+ name: []const u8,
752
+ description: []const u8,
753
+ color: []const u8,
754
+ };
755
+ number: u32,
756
+ labels: []Label,
757
+ };
758
+
759
+ const pr_list_text = try shell.exec_stdout(
760
+ "gh pr list --state open --json number,labels",
761
+ .{},
762
+ );
763
+ const pr_list = try std.json.parseFromSliceLeaky(
764
+ []GhPullRequest,
765
+ shell.arena.allocator(),
766
+ pr_list_text,
767
+ .{},
768
+ );
769
+
770
+ for (pr_list) |pr| {
771
+ for (pr.labels) |label| {
772
+ if (stdx.cut(label.name, "fuzz ") != null) break;
773
+ } else continue;
774
+
775
+ const pr_directory = try shell.fmt("./working/{d}", .{pr.number});
776
+ try shell.cwd.makePath(pr_directory);
777
+ try shell.pushd(pr_directory);
778
+ defer shell.popd();
779
+
780
+ const commit = try run_fuzzers_prepare_repository(
781
+ shell,
782
+ .{ .pull_request = pr.number },
783
+ );
784
+
785
+ var pr_fuzzers_count: u32 = 0;
786
+ for (std.enums.values(Fuzzer)) |fuzzer| {
787
+ const labeled = for (pr.labels) |label| {
788
+ if (stdx.cut_prefix(label.name, "fuzz ")) |suffix| {
789
+ if (std.mem.eql(u8, suffix, @tagName(fuzzer))) {
790
+ break true;
791
+ }
792
+ }
793
+ } else false;
794
+
795
+ if (labeled or fuzzer == .canary) {
796
+ pr_fuzzers_count += 1;
797
+ try tasks.put(pr_directory, .{
798
+ .commit_timestamp = commit.timestamp,
799
+ .commit_sha = commit.sha,
800
+ .fuzzer = fuzzer,
801
+ .branch = .{ .pull = pr.number },
802
+ .branch_url = try shell.fmt(
803
+ "https://github.com/tigerbeetle/tigerbeetle/pull/{d}",
804
+ .{pr.number},
805
+ ),
806
+ });
807
+ }
808
+ }
809
+ assert(pr_fuzzers_count >= 2); // The canary and at least one different fuzzer.
810
+ }
811
+ }
812
+
813
+ {
814
+ // Assign task weights:
815
+ // - Start by splitting the budget 40:40:20 between main, pull requests, and release.
816
+ // - Then, bump relative priority of more important fuzzers like VOPR.
817
+ var task_count_main: u32 = 0;
818
+ var task_count_pull: u32 = 0;
819
+ var task_count_release: u32 = 0;
820
+ for (tasks.list.items) |*task| {
821
+ if (task.generation == tasks.generation) {
822
+ task_count_main += @intFromBool(task.seed_template.branch == .main);
823
+ task_count_pull += @intFromBool(task.seed_template.branch == .pull);
824
+ task_count_release += @intFromBool(task.seed_template.branch == .release);
825
+ }
826
+ }
827
+
828
+ // When the release was recently pushed (i.e. usually on Friday), give it extra fuzzing
829
+ // weight for the next few days (i.e. until Monday).
830
+ const release_soon = for (tasks.list.items) |*task| {
831
+ if (task.seed_template.branch == .release) {
832
+ break std.time.timestamp() <
833
+ task.seed_template.commit_timestamp + 2 * std.time.s_per_day;
834
+ }
835
+ } else false;
836
+
837
+ const weight_main = 1_000_000;
838
+ const weight_pull = 1_000_000;
839
+ const weight_release = 500_000 * (if (release_soon) @as(u32, 3) else 1);
840
+
841
+ for (tasks.list.items) |*task| {
842
+ if (task.generation == tasks.generation) {
843
+ const multiplier = Fuzzer.weights.get(task.seed_template.fuzzer);
844
+ task.weight = switch (task.seed_template.branch) {
845
+ .main => @divFloor(weight_main, task_count_main),
846
+ .pull => @divFloor(weight_pull, task_count_pull),
847
+ .release => @divFloor(weight_release, task_count_release),
848
+ } * multiplier;
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+ const Commit = struct {
855
+ sha: [40]u8,
856
+ timestamp: u64,
857
+ };
858
+
859
+ // Clones the specified branch or pull request, builds the code and returns the commit that the
860
+ // branch/PR resolves to.
861
+ fn run_fuzzers_prepare_repository(shell: *Shell, target: union(enum) {
862
+ branch: []const u8,
863
+ pull_request: u32,
864
+ }) !Commit {
865
+ // When possible, reuse checkouts so that we can also reuse the zig cache.
866
+ if (!try shell.dir_exists(".git")) {
867
+ try shell.exec("git clone https://github.com/tigerbeetle/tigerbeetle .", .{});
868
+ }
869
+
870
+ switch (target) {
871
+ .branch => |branch| try shell.exec("git fetch origin {branch}", .{ .branch = branch }),
872
+ .pull_request => |pr| try shell.exec("git fetch origin refs/pull/{pr}/head", .{ .pr = pr }),
873
+ }
874
+ try shell.exec("git switch --detach FETCH_HEAD", .{});
875
+ return run_fuzzers_commit_info(shell);
876
+ }
877
+
878
+ fn run_fuzzers_commit_info(shell: *Shell) !Commit {
879
+ const commit_sha: [40]u8 = commit_sha: {
880
+ const commit_str = try shell.exec_stdout("git rev-parse HEAD", .{});
881
+ assert(commit_str.len == 40);
882
+ break :commit_sha commit_str[0..40].*;
883
+ };
884
+ const commit_timestamp = try shell.git_commit_timestamp(&commit_sha);
885
+ return .{ .sha = commit_sha, .timestamp = commit_timestamp.to_seconds() };
886
+ }
887
+
888
+ fn run_fuzzers_start_fuzzer(shell: *Shell, options: struct {
889
+ working_directory: []const u8,
890
+ fuzzer: Fuzzer,
891
+ seed: u64,
892
+ }) !struct {
893
+ command: []const u8, // User-visible string on devhub.
894
+ process: std.process.Child,
895
+ log: ?[]const u8, // Log file name.
896
+ } {
897
+ try shell.pushd(options.working_directory);
898
+ defer shell.popd();
899
+
900
+ const arg_count_max = comptime arg_count_max: {
901
+ var arg_max: u32 = 0;
902
+ for (std.enums.values(Fuzzer)) |fuzzer| {
903
+ arg_max = @max(
904
+ arg_max,
905
+ fuzzer.args_build().len,
906
+ fuzzer.args_exec().len,
907
+ fuzzer.args_run().len,
908
+ );
909
+ }
910
+ assert(arg_max > 0);
911
+
912
+ // +3: zig/zig build <args> SEED
913
+ break :arg_count_max arg_max + 3;
914
+ };
915
+ var args: stdx.BoundedArrayType([]const u8, arg_count_max) = .{};
916
+
917
+ assert(try shell.dir_exists(".git") or shell.file_exists(".git"));
918
+
919
+ // DevHub displays `./zig/zig build run` invocation which you can paste in your shell directly.
920
+ // But CFO actually builds and execs in two separate steps such that:
921
+ // - build time is excluded from overall runtime,
922
+ // - the exit status of the fuzzer process can be inspected, to determine if OOM happened.
923
+ args.clear();
924
+ args.push_slice(&.{ "./zig/zig", "build" });
925
+ args.push_slice(switch (options.fuzzer) {
926
+ inline else => |f| comptime f.args_run(),
927
+ });
928
+ var seed_buffer: [32]u8 = undefined;
929
+ args.push(stdx.array_print(32, &seed_buffer, "{d}", .{options.seed}));
930
+ const command = try std.mem.join(shell.arena.allocator(), " ", args.const_slice());
931
+
932
+ const exe = exe: {
933
+ args.clear();
934
+ args.push_slice(&.{ "build", "-Dprint-exe" });
935
+ args.push_slice(switch (options.fuzzer) {
936
+ inline else => |f| comptime f.args_build(),
937
+ });
938
+ break :exe shell.exec_stdout("{zig} {args}", .{
939
+ .zig = shell.zig_exe.?,
940
+ .args = args.const_slice(),
941
+ }) catch "false"; // Make sure that subsequent run fails if we can't build.
942
+ };
943
+ assert(exe.len > 0);
944
+
945
+ log.debug("will start '{s}' ({s})", .{ command, options.working_directory });
946
+
947
+ const log_path = if (options.fuzzer.capture_logs())
948
+ try shell.fmt("{s}_{d}.log", .{ @tagName(options.fuzzer), options.seed })
949
+ else
950
+ null;
951
+ args.clear();
952
+ args.push_slice(switch (options.fuzzer) {
953
+ inline else => |f| f.args_exec(),
954
+ });
955
+ if (log_path) |path| args.push(try shell.fmt("--log={s}", .{path}));
956
+ const process = try shell.spawn(
957
+ .{ .stdin_behavior = .Pipe },
958
+ "{exe} {args} {seed}",
959
+ .{
960
+ .exe = exe,
961
+ .args = args.const_slice(),
962
+ .seed = options.seed,
963
+ },
964
+ );
965
+
966
+ // Zig doesn't have non-blocking version of child.wait, so we use `BrokenPipe`
967
+ // on writing to child's stdin to detect if a child is dead in a non-blocking
968
+ // manner.
969
+ _ = try std.posix.fcntl(
970
+ process.stdin.?.handle,
971
+ std.posix.F.SETFL,
972
+ @as(u32, @bitCast(std.posix.O{ .NONBLOCK = true })),
973
+ );
974
+
975
+ return .{
976
+ .command = command,
977
+ .process = process,
978
+ .log = if (log_path) |path|
979
+ try shell.fmt("{s}/{s}", .{ options.working_directory, path })
980
+ else
981
+ null,
982
+ };
983
+ }
984
+
985
+ fn upload_results(
986
+ shell: *Shell,
987
+ gpa: std.mem.Allocator,
988
+ token: ?[]const u8,
989
+ seeds_new: []const SeedRecord,
990
+ seeds_new_logs: []const ?[]const u8,
991
+ ) !void {
992
+ assert(seeds_new.len == seeds_new_logs.len);
993
+
994
+ if (token) |_| {
995
+ log.info("uploading {} seeds", .{seeds_new.len});
996
+ }
997
+
998
+ _ = try shell.cwd.deleteTree("./devhubdb");
999
+ try shell.exec(
1000
+ \\git clone --single-branch --depth 1
1001
+ \\ https://oauth2:{token}@github.com/tigerbeetle/devhubdb.git
1002
+ \\ devhubdb
1003
+ , .{
1004
+ // Even when no token is provided, clone and modify the (local) devhubdb so that it is easy
1005
+ // to test CFO changes.
1006
+ .token = token orelse "",
1007
+ });
1008
+ try shell.pushd("./devhubdb");
1009
+ defer shell.popd();
1010
+
1011
+ for (0..32) |_| {
1012
+ // As we need a retry loop here to deal with git conflicts, let's use per-iteration arena.
1013
+ var arena = std.heap.ArenaAllocator.init(gpa);
1014
+ defer arena.deinit();
1015
+
1016
+ try shell.exec("git fetch origin main", .{});
1017
+ try shell.exec("git reset --hard origin/main", .{});
1018
+
1019
+ const max_size = 1 * MiB;
1020
+ const data = try shell.cwd.readFileAlloc(
1021
+ arena.allocator(),
1022
+ "./fuzzing/data.json",
1023
+ max_size,
1024
+ );
1025
+
1026
+ const seeds_old = try SeedRecord.from_json(arena.allocator(), data);
1027
+ const seeds_merged = try SeedRecord.merge(arena.allocator(), .{}, seeds_old, seeds_new);
1028
+ const seeds_json = try SeedRecord.to_json(arena.allocator(), seeds_merged);
1029
+
1030
+ if (std.mem.eql(u8, std.mem.sliceAsBytes(seeds_old), std.mem.sliceAsBytes(seeds_merged))) {
1031
+ // None of the new seeds were merged in, so there is nothing to commit.
1032
+ // This can happen when CFO is being run from a commit that is not one of CFO's targets.
1033
+ // For example, if you run CFO locally on main, but you are a couple commits behind.
1034
+ log.info("no seeds uploaded", .{});
1035
+ return;
1036
+ }
1037
+
1038
+ var seeds_merged_logs = std.StringHashMap(void).init(arena.allocator());
1039
+ for (seeds_merged) |*seed| {
1040
+ if (seed.log) |path| try seeds_merged_logs.putNoClobber(path, {});
1041
+ }
1042
+
1043
+ try shell.cwd.makePath("./fuzzing/logs/");
1044
+ // Create new log files.
1045
+ for (seeds_new, seeds_new_logs) |*seed, seed_log_data| {
1046
+ assert((seed.log == null) == (seed_log_data == null));
1047
+ if (seed.log) |log_path| {
1048
+ if (seeds_merged_logs.contains(log_path)) {
1049
+ try shell.cwd.writeFile(.{ .sub_path = log_path, .data = seed_log_data.? });
1050
+ try shell.exec("git add {path}", .{ .path = log_path });
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ // Delete any now-orphaned log files.
1056
+ for (try shell.find(.{
1057
+ .where = &.{"./fuzzing/logs/"},
1058
+ .extension = ".vopr",
1059
+ })) |log_path| {
1060
+ if (!seeds_merged_logs.contains(log_path)) {
1061
+ try shell.exec("git rm {path}", .{ .path = log_path });
1062
+ }
1063
+ }
1064
+
1065
+ try shell.cwd.writeFile(.{ .sub_path = "./fuzzing/data.json", .data = seeds_json });
1066
+ try shell.exec("git add ./fuzzing/data.json", .{});
1067
+ try shell.git_env_setup(.{ .use_hostname = true });
1068
+ try shell.exec("git commit -m 🌱", .{});
1069
+ if (token) |_| {
1070
+ if (shell.exec("git push", .{})) {
1071
+ log.info("seeds updated", .{});
1072
+ break;
1073
+ } else |_| {
1074
+ log.info("conflict, retrying", .{});
1075
+ }
1076
+ } else {
1077
+ break;
1078
+ }
1079
+ } else {
1080
+ log.err("can't push new data to devhub", .{});
1081
+ return error.CanNotPush;
1082
+ }
1083
+ }
1084
+
1085
+ const SeedRecord = struct {
1086
+ const MergeOptions = struct {
1087
+ commit_count_max: u32 = 32,
1088
+ seed_count_max: u32 = 4,
1089
+ };
1090
+
1091
+ commit_timestamp: u64,
1092
+ commit_sha: [40]u8,
1093
+ // NB: Use []const u8 rather than Fuzzer to support deserializing unknown fuzzers.
1094
+ fuzzer: []const u8,
1095
+ ok: bool = false,
1096
+ // Counts the number of seeds merged into the current one.
1097
+ count: u32 = 1,
1098
+ seed_timestamp_start: u64 = 0,
1099
+ seed_timestamp_end: u64 = 0,
1100
+ seed: u64 = 0,
1101
+ // The following fields are excluded from comparison:
1102
+ command: []const u8 = "",
1103
+ // Branch is a GitHub URL. It affects the UI, where the seeds are grouped by the branch.
1104
+ branch: []const u8,
1105
+ // Arbitrary string output for debugging, e.g., process exit status.
1106
+ debug: ?[]const u8 = null,
1107
+ // Optional path to log file, relative to the devhubdb root.
1108
+ log: ?[]const u8 = null,
1109
+
1110
+ const Template = struct {
1111
+ branch: Branch,
1112
+ branch_url: []const u8,
1113
+ commit_timestamp: u64,
1114
+ commit_sha: [40]u8,
1115
+ fuzzer: Fuzzer,
1116
+
1117
+ const Branch = union(enum) {
1118
+ main,
1119
+ release,
1120
+ pull: u32,
1121
+
1122
+ const main_url = "https://github.com/tigerbeetle/tigerbeetle";
1123
+ const release_url = "https://github.com/tigerbeetle/tigerbeetle/tree/release";
1124
+ const pull_url_prefix = "https://github.com/tigerbeetle/tigerbeetle/pull/";
1125
+
1126
+ pub fn parse(string: []const u8) !@This() {
1127
+ if (std.mem.eql(u8, string, main_url)) return .main;
1128
+ if (std.mem.eql(u8, string, release_url)) return .release;
1129
+ assert(std.mem.startsWith(u8, string, pull_url_prefix));
1130
+ const pull_number_string = string[pull_url_prefix.len..];
1131
+ const pull_number = std.fmt.parseInt(u32, pull_number_string, 10) catch unreachable;
1132
+ return .{ .pull = pull_number };
1133
+ }
1134
+ };
1135
+ };
1136
+
1137
+ fn is_release(record: SeedRecord) bool {
1138
+ return std.mem.eql(u8, record.branch, Template.Branch.release_url);
1139
+ }
1140
+
1141
+ fn order(a: SeedRecord, b: SeedRecord) std.math.Order {
1142
+ return order_by_field(b.commit_timestamp, a.commit_timestamp) orelse // NB: reverse order.
1143
+ order_by_field(a.commit_sha, b.commit_sha) orelse
1144
+ order_by_field(a.fuzzer, b.fuzzer) orelse
1145
+ order_by_field(a.ok, b.ok) orelse
1146
+ order_by_field(b.count, a.count) orelse // NB: reverse order.
1147
+ order_by_seed_duration(a, b) orelse
1148
+ order_by_seed_timestamp_start(a, b) orelse
1149
+ order_by_field(a.seed_timestamp_end, b.seed_timestamp_end) orelse
1150
+ order_by_field(a.seed, b.seed) orelse
1151
+ .eq;
1152
+ }
1153
+
1154
+ fn order_by_field(a: anytype, b: @TypeOf(a)) ?std.math.Order {
1155
+ const full_order = switch (@TypeOf(a)) {
1156
+ []const u8 => std.mem.order(u8, a, b),
1157
+ [40]u8 => std.mem.order(u8, &a, &b),
1158
+ bool => std.math.order(@intFromBool(a), @intFromBool(b)),
1159
+ Fuzzer => std.math.order(@intFromEnum(a), @intFromEnum(b)),
1160
+ else => std.math.order(a, b),
1161
+ };
1162
+ return if (full_order == .eq) null else full_order;
1163
+ }
1164
+
1165
+ fn order_by_seed_timestamp_start(a: SeedRecord, b: SeedRecord) ?std.math.Order {
1166
+ // For canaries, prefer newer seeds to show that the canary is alive.
1167
+ // For other fuzzers, prefer older seeds to keep them stable.
1168
+ return if (std.mem.eql(u8, a.fuzzer, "canary"))
1169
+ order_by_field(b.seed_timestamp_start, a.seed_timestamp_start)
1170
+ else
1171
+ order_by_field(a.seed_timestamp_start, b.seed_timestamp_start);
1172
+ }
1173
+
1174
+ fn order_by_seed_duration(a: SeedRecord, b: SeedRecord) ?std.math.Order {
1175
+ assert(a.ok == b.ok);
1176
+ if (a.ok) {
1177
+ // Passing seeds: prefer long durations -- near-timeouts might be interesting, and it
1178
+ // gives us a p100 for the fuzzer's runtime.
1179
+ return order_by_field(b.seed_duration(), a.seed_duration());
1180
+ } else {
1181
+ // Failing seeds: prefer short duration, as coarse seed minimization.
1182
+ return order_by_field(a.seed_duration(), b.seed_duration());
1183
+ }
1184
+ }
1185
+
1186
+ // Normally, records are sorted by commit timestamp, such that inactive or merged pull requests
1187
+ // sink down naturally. However, we want to "pin" the latest release commit even if it is old,
1188
+ // so we rig comparison function here.
1189
+ fn less_than(release_latest: u64, a: SeedRecord, b: SeedRecord) bool {
1190
+ maybe(release_latest == 0);
1191
+ const a_latest_release = a.commit_timestamp == release_latest and a.is_release();
1192
+ const b_latest_release = b.commit_timestamp == release_latest and b.is_release();
1193
+ if (a_latest_release and !b_latest_release) return true;
1194
+ if (!a_latest_release and b_latest_release) return false;
1195
+ return a.order(b) == .lt;
1196
+ }
1197
+
1198
+ fn seed_duration(record: SeedRecord) u64 {
1199
+ return record.seed_timestamp_end - record.seed_timestamp_start;
1200
+ }
1201
+
1202
+ fn from_json(arena: std.mem.Allocator, json_str: []const u8) ![]SeedRecord {
1203
+ return try std.json.parseFromSliceLeaky([]SeedRecord, arena, json_str, .{});
1204
+ }
1205
+
1206
+ fn to_json(arena: std.mem.Allocator, records: []const SeedRecord) ![]const u8 {
1207
+ return try std.json.stringifyAlloc(arena, records, .{
1208
+ .emit_null_optional_fields = false, // Omit `"debug: ""`.
1209
+ .whitespace = .indent_2,
1210
+ });
1211
+ }
1212
+
1213
+ // Merges two sets of seeds keeping the more interesting one. A direct way to write this would
1214
+ // be to group the seeds by commit & fuzzer and do a union of nested hash maps, but that's a
1215
+ // pain to implement in Zig. Luckily, by cleverly implementing the ordering on seeds it is
1216
+ // possible to implement the merge by concatenation, sorting, and a single-pass counting scan.
1217
+ fn merge(
1218
+ arena: std.mem.Allocator,
1219
+ options: MergeOptions,
1220
+ current: []const SeedRecord,
1221
+ new: []const SeedRecord,
1222
+ ) ![]const SeedRecord {
1223
+ const current_and_new = try std.mem.concat(arena, SeedRecord, &.{ current, new });
1224
+
1225
+ var release_latest: u64 = 0;
1226
+ for (current_and_new) |record| {
1227
+ if (record.is_release() and record.commit_timestamp > release_latest) {
1228
+ release_latest = record.commit_timestamp;
1229
+ }
1230
+ }
1231
+ std.mem.sort(SeedRecord, current_and_new, release_latest, SeedRecord.less_than);
1232
+
1233
+ var result = try std.ArrayList(SeedRecord).initCapacity(arena, current.len);
1234
+
1235
+ var commit_sha_previous: ?[40]u8 = null;
1236
+ var commit_count: u32 = 0;
1237
+
1238
+ var fuzzer_previous: ?[]const u8 = null;
1239
+
1240
+ var seed_previous: ?u64 = null;
1241
+ var seed_count: u32 = 0;
1242
+
1243
+ for (current_and_new) |record| {
1244
+ if (commit_sha_previous == null or
1245
+ !std.meta.eql(commit_sha_previous.?, record.commit_sha))
1246
+ {
1247
+ commit_sha_previous = record.commit_sha;
1248
+ commit_count += 1;
1249
+ fuzzer_previous = null;
1250
+ }
1251
+
1252
+ if (commit_count > options.commit_count_max) {
1253
+ break;
1254
+ }
1255
+
1256
+ if (fuzzer_previous == null or
1257
+ !std.mem.eql(u8, fuzzer_previous.?, record.fuzzer))
1258
+ {
1259
+ fuzzer_previous = record.fuzzer;
1260
+ seed_previous = null;
1261
+ seed_count = 0;
1262
+ }
1263
+
1264
+ if (seed_previous == record.seed) {
1265
+ continue;
1266
+ }
1267
+ seed_previous = record.seed;
1268
+
1269
+ if (record.ok) {
1270
+ assert(record.log == null);
1271
+ // Merge counts with the first ok record for this fuzzer/commit, to make it easy for
1272
+ // the front-end to show the total count by displaying just the first record.
1273
+ if (result.getLastOrNull()) |record_previous| {
1274
+ if (record_previous.ok and
1275
+ std.mem.eql(u8, record_previous.fuzzer, record.fuzzer) and
1276
+ std.meta.eql(record_previous.commit_sha, record.commit_sha))
1277
+ {
1278
+ result.items[result.items.len - 1].count += record.count;
1279
+ continue;
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ if (seed_count < options.seed_count_max) {
1285
+ try result.append(record);
1286
+ seed_count += 1;
1287
+ }
1288
+ }
1289
+
1290
+ return result.items;
1291
+ }
1292
+ };
1293
+
1294
+ fn create_log_path(arena: std.mem.Allocator) ![]const u8 {
1295
+ const name = std.crypto.random.int(u128);
1296
+ return std.fmt.allocPrint(arena, "./fuzzing/logs/{x:0>32}.vopr", .{name});
1297
+ }
1298
+
1299
+ const Snap = stdx.Snap;
1300
+ const snap = Snap.snap_fn("src");
1301
+
1302
+ test "cfo: deserialization" {
1303
+ // Smoke test that we can still deserialize&migrate old devhub data.
1304
+ // Handy when adding new fields!
1305
+ const old_json =
1306
+ \\[{
1307
+ \\ "commit_timestamp": 1721095881,
1308
+ \\ "commit_sha": "c4bb1eaa658b77c37646d3854dd911adba71b764",
1309
+ \\ "fuzzer": "canary",
1310
+ \\ "ok": false,
1311
+ \\ "seed_timestamp_start": 1721096948,
1312
+ \\ "seed_timestamp_end": 1721096949,
1313
+ \\ "seed": 17154947449604939200,
1314
+ \\ "command": "./zig/zig build -Drelease fuzz -- canary 17154947449604939200",
1315
+ \\ "branch": "https://github.com/tigerbeetle/tigerbeetle/pull/2104",
1316
+ \\ "count": 1
1317
+ \\}]
1318
+ ;
1319
+
1320
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1321
+ defer arena.deinit();
1322
+
1323
+ const old_records = try SeedRecord.from_json(arena.allocator(), old_json);
1324
+
1325
+ const new_records = try SeedRecord.merge(arena.allocator(), .{}, old_records, &.{});
1326
+ const new_json = try SeedRecord.to_json(arena.allocator(), new_records);
1327
+
1328
+ try snap(@src(),
1329
+ \\[
1330
+ \\ {
1331
+ \\ "commit_timestamp": 1721095881,
1332
+ \\ "commit_sha": "c4bb1eaa658b77c37646d3854dd911adba71b764",
1333
+ \\ "fuzzer": "canary",
1334
+ \\ "ok": false,
1335
+ \\ "count": 1,
1336
+ \\ "seed_timestamp_start": 1721096948,
1337
+ \\ "seed_timestamp_end": 1721096949,
1338
+ \\ "seed": 17154947449604939200,
1339
+ \\ "command": "./zig/zig build -Drelease fuzz -- canary 17154947449604939200",
1340
+ \\ "branch": "https://github.com/tigerbeetle/tigerbeetle/pull/2104"
1341
+ \\ }
1342
+ \\]
1343
+ ).diff(new_json);
1344
+ }
1345
+
1346
+ test "cfo: SeedRecord.merge" {
1347
+ const T = struct {
1348
+ fn check(current: []const SeedRecord, new: []const SeedRecord, want: Snap) !void {
1349
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1350
+ defer arena.deinit();
1351
+
1352
+ const options = SeedRecord.MergeOptions{
1353
+ .commit_count_max = 2,
1354
+ .seed_count_max = 2,
1355
+ };
1356
+ const merged = try SeedRecord.merge(arena.allocator(), options, current, new);
1357
+ const got = try SeedRecord.to_json(arena.allocator(), merged);
1358
+ try want.diff(got);
1359
+ }
1360
+ };
1361
+
1362
+ try T.check(&.{}, &.{}, snap(@src(),
1363
+ \\[]
1364
+ ));
1365
+
1366
+ try T.check(
1367
+ &.{
1368
+ // First commit, one failure.
1369
+ .{
1370
+ .commit_timestamp = 1,
1371
+ .commit_sha = .{'1'} ** 40,
1372
+ .fuzzer = "ewah",
1373
+ .ok = false,
1374
+ .seed_timestamp_start = 1,
1375
+ .seed_timestamp_end = 1,
1376
+ .seed = 1,
1377
+ .command = "fuzz ewah",
1378
+ .branch = "main",
1379
+ },
1380
+ // Second commit, two successes.
1381
+ .{
1382
+ .commit_timestamp = 2,
1383
+ .commit_sha = .{'2'} ** 40,
1384
+ .fuzzer = "ewah",
1385
+ .ok = true,
1386
+ .seed_timestamp_start = 1,
1387
+ .seed_timestamp_end = 1,
1388
+ .seed = 1,
1389
+ .command = "fuzz ewah",
1390
+ .branch = "main",
1391
+ },
1392
+ .{
1393
+ .commit_timestamp = 2,
1394
+ .commit_sha = .{'2'} ** 40,
1395
+ .fuzzer = "ewah",
1396
+ .ok = true,
1397
+ .seed_timestamp_start = 2,
1398
+ .seed_timestamp_end = 2,
1399
+ .seed = 2,
1400
+ .command = "fuzz ewah",
1401
+ .branch = "main",
1402
+ },
1403
+ },
1404
+ &.{
1405
+ // Two new failures for the first commit, one will be added.
1406
+ .{
1407
+ .commit_timestamp = 1,
1408
+ .commit_sha = .{'1'} ** 40,
1409
+ .fuzzer = "ewah",
1410
+ .ok = false,
1411
+ .seed_timestamp_start = 2,
1412
+ .seed_timestamp_end = 2,
1413
+ .seed = 2,
1414
+ .command = "fuzz ewah",
1415
+ .branch = "main",
1416
+ },
1417
+ .{
1418
+ .commit_timestamp = 1,
1419
+ .commit_sha = .{'1'} ** 40,
1420
+ .fuzzer = "ewah",
1421
+ .ok = false,
1422
+ .seed_timestamp_start = 3,
1423
+ .seed_timestamp_end = 3,
1424
+ .seed = 3,
1425
+ .command = "fuzz ewah",
1426
+ .branch = "main",
1427
+ },
1428
+ // One failure for the second commit, it will replace one success.
1429
+ .{
1430
+ .commit_timestamp = 2,
1431
+ .commit_sha = .{'2'} ** 40,
1432
+ .fuzzer = "ewah",
1433
+ .ok = false,
1434
+ .seed_timestamp_start = 4,
1435
+ .seed_timestamp_end = 4,
1436
+ .seed = 4,
1437
+ .command = "fuzz ewah",
1438
+ .branch = "main",
1439
+ },
1440
+ },
1441
+ snap(@src(),
1442
+ \\[
1443
+ \\ {
1444
+ \\ "commit_timestamp": 2,
1445
+ \\ "commit_sha": "2222222222222222222222222222222222222222",
1446
+ \\ "fuzzer": "ewah",
1447
+ \\ "ok": false,
1448
+ \\ "count": 1,
1449
+ \\ "seed_timestamp_start": 4,
1450
+ \\ "seed_timestamp_end": 4,
1451
+ \\ "seed": 4,
1452
+ \\ "command": "fuzz ewah",
1453
+ \\ "branch": "main"
1454
+ \\ },
1455
+ \\ {
1456
+ \\ "commit_timestamp": 2,
1457
+ \\ "commit_sha": "2222222222222222222222222222222222222222",
1458
+ \\ "fuzzer": "ewah",
1459
+ \\ "ok": true,
1460
+ \\ "count": 2,
1461
+ \\ "seed_timestamp_start": 1,
1462
+ \\ "seed_timestamp_end": 1,
1463
+ \\ "seed": 1,
1464
+ \\ "command": "fuzz ewah",
1465
+ \\ "branch": "main"
1466
+ \\ },
1467
+ \\ {
1468
+ \\ "commit_timestamp": 1,
1469
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1470
+ \\ "fuzzer": "ewah",
1471
+ \\ "ok": false,
1472
+ \\ "count": 1,
1473
+ \\ "seed_timestamp_start": 1,
1474
+ \\ "seed_timestamp_end": 1,
1475
+ \\ "seed": 1,
1476
+ \\ "command": "fuzz ewah",
1477
+ \\ "branch": "main"
1478
+ \\ },
1479
+ \\ {
1480
+ \\ "commit_timestamp": 1,
1481
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1482
+ \\ "fuzzer": "ewah",
1483
+ \\ "ok": false,
1484
+ \\ "count": 1,
1485
+ \\ "seed_timestamp_start": 2,
1486
+ \\ "seed_timestamp_end": 2,
1487
+ \\ "seed": 2,
1488
+ \\ "command": "fuzz ewah",
1489
+ \\ "branch": "main"
1490
+ \\ }
1491
+ \\]
1492
+ ),
1493
+ );
1494
+
1495
+ try T.check(
1496
+ &.{
1497
+ // Two failing commits.
1498
+ .{
1499
+ .commit_timestamp = 1,
1500
+ .commit_sha = .{'1'} ** 40,
1501
+ .fuzzer = "ewah",
1502
+ .ok = false,
1503
+ .seed_timestamp_start = 1,
1504
+ .seed_timestamp_end = 1,
1505
+ .seed = 1,
1506
+ .command = "fuzz ewah",
1507
+ .branch = "main",
1508
+ },
1509
+ .{
1510
+ .commit_timestamp = 2,
1511
+ .commit_sha = .{'2'} ** 40,
1512
+ .fuzzer = "ewah",
1513
+ .ok = false,
1514
+ .seed_timestamp_start = 1,
1515
+ .seed_timestamp_end = 1,
1516
+ .seed = 1,
1517
+ .command = "fuzz ewah",
1518
+ .branch = "main",
1519
+ },
1520
+ },
1521
+ &.{
1522
+ // A new successful commit displaces the older failure.
1523
+ .{
1524
+ .commit_timestamp = 3,
1525
+ .commit_sha = .{'3'} ** 40,
1526
+ .fuzzer = "ewah",
1527
+ .ok = true,
1528
+ .seed_timestamp_start = 1,
1529
+ .seed_timestamp_end = 1,
1530
+ .seed = 1,
1531
+ .command = "fuzz ewah",
1532
+ .branch = "main",
1533
+ },
1534
+ },
1535
+ snap(@src(),
1536
+ \\[
1537
+ \\ {
1538
+ \\ "commit_timestamp": 3,
1539
+ \\ "commit_sha": "3333333333333333333333333333333333333333",
1540
+ \\ "fuzzer": "ewah",
1541
+ \\ "ok": true,
1542
+ \\ "count": 1,
1543
+ \\ "seed_timestamp_start": 1,
1544
+ \\ "seed_timestamp_end": 1,
1545
+ \\ "seed": 1,
1546
+ \\ "command": "fuzz ewah",
1547
+ \\ "branch": "main"
1548
+ \\ },
1549
+ \\ {
1550
+ \\ "commit_timestamp": 2,
1551
+ \\ "commit_sha": "2222222222222222222222222222222222222222",
1552
+ \\ "fuzzer": "ewah",
1553
+ \\ "ok": false,
1554
+ \\ "count": 1,
1555
+ \\ "seed_timestamp_start": 1,
1556
+ \\ "seed_timestamp_end": 1,
1557
+ \\ "seed": 1,
1558
+ \\ "command": "fuzz ewah",
1559
+ \\ "branch": "main"
1560
+ \\ }
1561
+ \\]
1562
+ ),
1563
+ );
1564
+
1565
+ // Deduplicates identical seeds
1566
+ try T.check(
1567
+ &.{
1568
+ .{
1569
+ .commit_timestamp = 1,
1570
+ .commit_sha = .{'1'} ** 40,
1571
+ .fuzzer = "ewah",
1572
+ .ok = false,
1573
+ .seed_timestamp_start = 1,
1574
+ .seed_timestamp_end = 1,
1575
+ .seed = 1,
1576
+ .command = "fuzz ewah",
1577
+ .branch = "main",
1578
+ },
1579
+ },
1580
+ &.{
1581
+ .{
1582
+ .commit_timestamp = 1,
1583
+ .commit_sha = .{'1'} ** 40,
1584
+ .fuzzer = "ewah",
1585
+ .ok = false,
1586
+ .seed_timestamp_start = 1,
1587
+ .seed_timestamp_end = 1,
1588
+ .seed = 1,
1589
+ .command = "fuzz ewah",
1590
+ .branch = "main",
1591
+ },
1592
+ },
1593
+ snap(@src(),
1594
+ \\[
1595
+ \\ {
1596
+ \\ "commit_timestamp": 1,
1597
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1598
+ \\ "fuzzer": "ewah",
1599
+ \\ "ok": false,
1600
+ \\ "count": 1,
1601
+ \\ "seed_timestamp_start": 1,
1602
+ \\ "seed_timestamp_end": 1,
1603
+ \\ "seed": 1,
1604
+ \\ "command": "fuzz ewah",
1605
+ \\ "branch": "main"
1606
+ \\ }
1607
+ \\]
1608
+ ),
1609
+ );
1610
+
1611
+ // Prefer older seeds rather than smaller seeds.
1612
+ try T.check(
1613
+ &.{
1614
+ .{
1615
+ .commit_timestamp = 1,
1616
+ .commit_sha = .{'1'} ** 40,
1617
+ .fuzzer = "ewah",
1618
+ .ok = false,
1619
+ .seed_timestamp_start = 10,
1620
+ .seed_timestamp_end = 10,
1621
+ .seed = 10,
1622
+ .command = "fuzz ewah",
1623
+ .branch = "main",
1624
+ },
1625
+ .{
1626
+ .commit_timestamp = 1,
1627
+ .commit_sha = .{'1'} ** 40,
1628
+ .fuzzer = "ewah",
1629
+ .ok = false,
1630
+ .seed_timestamp_start = 20,
1631
+ .seed_timestamp_end = 20,
1632
+ .seed = 20,
1633
+ .command = "fuzz ewah",
1634
+ .branch = "main",
1635
+ },
1636
+ },
1637
+ &.{
1638
+ .{
1639
+ .commit_timestamp = 1,
1640
+ .commit_sha = .{'1'} ** 40,
1641
+ .fuzzer = "ewah",
1642
+ .ok = false,
1643
+ .seed_timestamp_start = 5,
1644
+ .seed_timestamp_end = 5,
1645
+ .seed = 999,
1646
+ .command = "fuzz ewah",
1647
+ .branch = "main",
1648
+ },
1649
+ },
1650
+ snap(@src(),
1651
+ \\[
1652
+ \\ {
1653
+ \\ "commit_timestamp": 1,
1654
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1655
+ \\ "fuzzer": "ewah",
1656
+ \\ "ok": false,
1657
+ \\ "count": 1,
1658
+ \\ "seed_timestamp_start": 5,
1659
+ \\ "seed_timestamp_end": 5,
1660
+ \\ "seed": 999,
1661
+ \\ "command": "fuzz ewah",
1662
+ \\ "branch": "main"
1663
+ \\ },
1664
+ \\ {
1665
+ \\ "commit_timestamp": 1,
1666
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1667
+ \\ "fuzzer": "ewah",
1668
+ \\ "ok": false,
1669
+ \\ "count": 1,
1670
+ \\ "seed_timestamp_start": 10,
1671
+ \\ "seed_timestamp_end": 10,
1672
+ \\ "seed": 10,
1673
+ \\ "command": "fuzz ewah",
1674
+ \\ "branch": "main"
1675
+ \\ }
1676
+ \\]
1677
+ ),
1678
+ );
1679
+
1680
+ // Prefer newer seeds for canary (special case).
1681
+ try T.check(
1682
+ &.{
1683
+ .{
1684
+ .commit_timestamp = 1,
1685
+ .commit_sha = .{'1'} ** 40,
1686
+ .fuzzer = "canary",
1687
+ .ok = false,
1688
+ .seed_timestamp_start = 10,
1689
+ .seed_timestamp_end = 10,
1690
+ .seed = 3,
1691
+ .command = "fuzz canary",
1692
+ .branch = "main",
1693
+ },
1694
+ .{
1695
+ .commit_timestamp = 1,
1696
+ .commit_sha = .{'1'} ** 40,
1697
+ .fuzzer = "canary",
1698
+ .ok = false,
1699
+ .seed_timestamp_start = 30,
1700
+ .seed_timestamp_end = 30,
1701
+ .seed = 2,
1702
+ .command = "fuzz canary",
1703
+ .branch = "main",
1704
+ },
1705
+ },
1706
+ &.{
1707
+ .{
1708
+ .commit_timestamp = 1,
1709
+ .commit_sha = .{'1'} ** 40,
1710
+ .fuzzer = "canary",
1711
+ .ok = false,
1712
+ .seed_timestamp_start = 20,
1713
+ .seed_timestamp_end = 20,
1714
+ .seed = 1,
1715
+ .command = "fuzz canary",
1716
+ .branch = "main",
1717
+ },
1718
+ },
1719
+ snap(@src(),
1720
+ \\[
1721
+ \\ {
1722
+ \\ "commit_timestamp": 1,
1723
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1724
+ \\ "fuzzer": "canary",
1725
+ \\ "ok": false,
1726
+ \\ "count": 1,
1727
+ \\ "seed_timestamp_start": 30,
1728
+ \\ "seed_timestamp_end": 30,
1729
+ \\ "seed": 2,
1730
+ \\ "command": "fuzz canary",
1731
+ \\ "branch": "main"
1732
+ \\ },
1733
+ \\ {
1734
+ \\ "commit_timestamp": 1,
1735
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1736
+ \\ "fuzzer": "canary",
1737
+ \\ "ok": false,
1738
+ \\ "count": 1,
1739
+ \\ "seed_timestamp_start": 20,
1740
+ \\ "seed_timestamp_end": 20,
1741
+ \\ "seed": 1,
1742
+ \\ "command": "fuzz canary",
1743
+ \\ "branch": "main"
1744
+ \\ }
1745
+ \\]
1746
+ ),
1747
+ );
1748
+
1749
+ // Tolerates unknown fuzzers
1750
+ try T.check(
1751
+ &.{
1752
+ .{
1753
+ .commit_timestamp = 1,
1754
+ .commit_sha = .{'1'} ** 40,
1755
+ .fuzzer = "ewah",
1756
+ .ok = false,
1757
+ .seed_timestamp_start = 1,
1758
+ .seed_timestamp_end = 1,
1759
+ .seed = 1,
1760
+ .command = "fuzz ewah",
1761
+ .branch = "main",
1762
+ },
1763
+ },
1764
+ &.{
1765
+ .{
1766
+ .commit_timestamp = 1,
1767
+ .commit_sha = .{'1'} ** 40,
1768
+ .fuzzer = "American Fuzzy Lop",
1769
+ .ok = false,
1770
+ .seed_timestamp_start = 1,
1771
+ .seed_timestamp_end = 1,
1772
+ .seed = 1,
1773
+ .command = "very fluffy",
1774
+ .branch = "main",
1775
+ },
1776
+ },
1777
+ snap(@src(),
1778
+ \\[
1779
+ \\ {
1780
+ \\ "commit_timestamp": 1,
1781
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1782
+ \\ "fuzzer": "American Fuzzy Lop",
1783
+ \\ "ok": false,
1784
+ \\ "count": 1,
1785
+ \\ "seed_timestamp_start": 1,
1786
+ \\ "seed_timestamp_end": 1,
1787
+ \\ "seed": 1,
1788
+ \\ "command": "very fluffy",
1789
+ \\ "branch": "main"
1790
+ \\ },
1791
+ \\ {
1792
+ \\ "commit_timestamp": 1,
1793
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1794
+ \\ "fuzzer": "ewah",
1795
+ \\ "ok": false,
1796
+ \\ "count": 1,
1797
+ \\ "seed_timestamp_start": 1,
1798
+ \\ "seed_timestamp_end": 1,
1799
+ \\ "seed": 1,
1800
+ \\ "command": "fuzz ewah",
1801
+ \\ "branch": "main"
1802
+ \\ }
1803
+ \\]
1804
+ ),
1805
+ );
1806
+
1807
+ // Sums up counts
1808
+ try T.check(
1809
+ &.{
1810
+ .{
1811
+ .commit_timestamp = 1,
1812
+ .commit_sha = .{'1'} ** 40,
1813
+ .fuzzer = "ewah",
1814
+ .ok = true,
1815
+ .seed_timestamp_start = 1,
1816
+ .seed_timestamp_end = 1,
1817
+ .seed = 1,
1818
+ .command = "fuzz ewah",
1819
+ .branch = "main",
1820
+ .count = 2,
1821
+ },
1822
+ .{
1823
+ .commit_timestamp = 1,
1824
+ .commit_sha = .{'1'} ** 40,
1825
+ .fuzzer = "ewah",
1826
+ .ok = true,
1827
+ .seed_timestamp_start = 1,
1828
+ .seed_timestamp_end = 1,
1829
+ .seed = 2,
1830
+ .command = "fuzz ewah",
1831
+ .branch = "main",
1832
+ .count = 1,
1833
+ },
1834
+ },
1835
+ &.{
1836
+ .{
1837
+ .commit_timestamp = 1,
1838
+ .commit_sha = .{'1'} ** 40,
1839
+ .fuzzer = "ewah",
1840
+ .ok = true,
1841
+ .seed_timestamp_start = 1,
1842
+ .seed_timestamp_end = 1,
1843
+ .seed = 3,
1844
+ .command = "fuzz ewah",
1845
+ .branch = "main",
1846
+ .count = 3,
1847
+ },
1848
+ },
1849
+ snap(@src(),
1850
+ \\[
1851
+ \\ {
1852
+ \\ "commit_timestamp": 1,
1853
+ \\ "commit_sha": "1111111111111111111111111111111111111111",
1854
+ \\ "fuzzer": "ewah",
1855
+ \\ "ok": true,
1856
+ \\ "count": 6,
1857
+ \\ "seed_timestamp_start": 1,
1858
+ \\ "seed_timestamp_end": 1,
1859
+ \\ "seed": 3,
1860
+ \\ "command": "fuzz ewah",
1861
+ \\ "branch": "main"
1862
+ \\ }
1863
+ \\]
1864
+ ),
1865
+ );
1866
+ }