Branch data Line data Source code
1 : : // Copyright (c) The Bitcoin Core developers
2 : : // Distributed under the MIT software license, see the accompanying
3 : : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 : :
5 : : #include <dbwrapper.h>
6 : : #include <compat/byteswap.h>
7 : : #include <random.h>
8 : : #include <sync.h>
9 : : #include <test/fuzz/FuzzedDataProvider.h>
10 : : #include <test/fuzz/fuzz.h>
11 : : #include <test/fuzz/util.h>
12 : : #include <test/util/random.h>
13 : : #include <test/util/setup_common.h>
14 : : #include <util/byte_units.h>
15 : : #include <util/check.h>
16 : : #include <util/threadpool.h>
17 : :
18 : : #include <leveldb/env.h>
19 : : #include <leveldb/helpers/memenv/memenv.h>
20 : :
21 : : #include <algorithm>
22 : : #include <cassert>
23 : : #include <cstdint>
24 : : #include <deque>
25 : : #include <functional>
26 : : #include <future>
27 : : #include <latch>
28 : : #include <map>
29 : : #include <memory>
30 : : #include <numeric>
31 : : #include <optional>
32 : : #include <set>
33 : : #include <string>
34 : : #include <tuple>
35 : : #include <vector>
36 : :
37 : : namespace {
38 : :
39 : : /**
40 : : * A leveldb::Env that wraps a memenv and captures scheduled background
41 : : * work (compaction) instead of dispatching to a real thread. The fuzz
42 : : * harness calls RunOne() or DrainWork() at fuzzer-chosen points to
43 : : * execute it, giving deterministic control over when compaction
44 : : * interleaves with foreground operations.
45 : : *
46 : : * Deadlock prevention: LevelDB's MakeRoomForWrite blocks on a condition
47 : : * variable when the previous immutable memtable is still awaiting compaction,
48 : : * or when the L0 file count hits kL0_StopWritesTrigger. Since both conditions
49 : : * can only be resolved by the (deferred) background work, the harness drains
50 : : * all pending work before every write to avoid a single-threaded deadlock.
51 : : * Callers must also DrainWork() before destroying the CDBWrapper, since the
52 : : * leveldb destructor waits for any pending background work to complete.
53 : : *
54 : : * The same reasoning rules out exercising DBOptions::force_compact under
55 : : * this env, because CompactRange(nullptr, nullptr) blocks waiting for
56 : : * background work that is queued on the (blocked) foreground thread. The
57 : : * sibling dbwrapper_threaded target covers that path.
58 : : */
59 : : class DeterministicEnv final : public leveldb::EnvWrapper
60 : : {
61 : : using WorkFunction = void (*)(void*);
62 : :
63 : : struct Work {
64 : : WorkFunction function;
65 : : void* arg;
66 : : };
67 : :
68 : : Mutex m_mutex;
69 : : std::deque<Work> m_queue GUARDED_BY(m_mutex);
70 : :
71 : : public:
72 [ + - ]: 539 : explicit DeterministicEnv(leveldb::Env* base) : EnvWrapper(base) {}
73 : :
74 : 9605 : void Schedule(WorkFunction function, void* arg) override EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
75 : : {
76 : 9605 : LOCK(m_mutex);
77 [ + - + - ]: 9605 : m_queue.push_back({function, arg});
78 : 9605 : }
79 : :
80 : : /** Execute one pending background task. The task may schedule a
81 : : * successor which is left pending for a later call. */
82 : 76807 : bool RunOne() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
83 : : {
84 : 76807 : Work work;
85 : 76807 : {
86 : 76807 : LOCK(m_mutex);
87 [ + + + - ]: 76807 : if (m_queue.empty()) return false;
88 : 9605 : work = m_queue.front();
89 [ + - ]: 9605 : m_queue.pop_front();
90 : 67202 : }
91 : 9605 : work.function(work.arg);
92 : 9605 : return true;
93 : : }
94 : :
95 : : /** Execute pending background tasks until none remain. */
96 [ - - - - : 73482 : void DrainWork() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) { while (RunOne()) {} }
- - - - -
- - - +
+ ]
97 : : };
98 : :
99 : : constexpr size_t MAX_VALUE_LEN{4096};
100 : : constexpr uint8_t MAX_VALUE_MULTIPLIER{8};
101 : : constexpr size_t WRITE_BATCH_HEADER{12}; // See kHeader in db/write_batch.cc
102 : :
103 : : /** Mirror of CDBWrapper::OBFUSCATION_KEY, the fixed key under which leveldb
104 : : * stores the obfuscation metadata entry when obfuscation is enabled. */
105 : : const std::string OBFUSCATION_KEY{"\000obfuscate_key", 14};
106 : :
107 : : /** Generate a deterministic value from key and size. The fuzz input picks
108 : : * a 16-bit length (up to MAX_VALUE_LEN) and an 8-bit multiplier so that a
109 : : * small amount of fuzz input can produce a wide range of value sizes. */
110 : 434635 : std::vector<uint8_t> MakeValue(uint16_t key, uint32_t size)
111 : : {
112 : 434635 : std::vector<uint8_t> v(size);
113 : 434635 : std::iota(v.begin(), v.end(), static_cast<uint8_t>(key ^ (key >> 8)));
114 : 434635 : return v;
115 : : }
116 : :
117 : : /** Equivalent to leveldb::BytewiseComparator() on 2-byte little-endian
118 : : * serialized uint16_t keys, while keeping the oracle keyed by uint16_t. */
119 : : struct LevelDBBytewiseU16Cmp {
120 [ + - - + : 472603 : bool operator()(uint16_t a, uint16_t b) const { return internal_bswap_16(a) < internal_bswap_16(b); }
- - - - +
+ + + + -
+ - + - -
- - - + +
- - - - -
- - - - -
- - - - +
+ + + + +
+ + ]
121 : : };
122 : :
123 : : /** key → value-size map ordered by LevelDB's bytewise comparator. */
124 : : using Oracle = std::map<uint16_t, uint32_t, LevelDBBytewiseU16Cmp>;
125 : :
126 : : struct FailUnserialize {
127 : : template <typename Stream>
128 [ + - ]: 2640 : void Unserialize(Stream&) { throw std::ios_base::failure{"always fail"}; }
129 : : };
130 : :
131 : 223198 : uint16_t ConsumeKey(FuzzedDataProvider& provider) { return provider.ConsumeIntegral<uint16_t>(); }
132 : 63616 : uint32_t ConsumeValueSize(FuzzedDataProvider& provider)
133 : : {
134 : 63616 : const uint16_t len{provider.ConsumeIntegralInRange<uint16_t>(0, MAX_VALUE_LEN)};
135 : 63616 : const uint8_t multiplier{provider.ConsumeIntegralInRange<uint8_t>(1, MAX_VALUE_MULTIPLIER)};
136 : 63616 : return static_cast<uint32_t>(len) * multiplier;
137 : : }
138 : :
139 : : /** Verify that the DB iterator matches the oracle, handling the obfuscation
140 : : * metadata entry (stored under a non-uint16_t key) when obfuscation is on. */
141 : 62089 : void VerifyIterator(CDBWrapper& dbw, const Oracle& oracle,
142 : : bool obfuscate, std::optional<uint16_t> seek_key = std::nullopt)
143 : : {
144 [ + + ]: 62089 : const std::unique_ptr<CDBIterator> it{dbw.NewIterator()};
145 [ + + ]: 62089 : auto oracle_it{seek_key ? oracle.lower_bound(*seek_key) : oracle.begin()};
146 [ + + ]: 62089 : if (seek_key) {
147 [ + - ]: 13180 : it->Seek(*seek_key);
148 : : } else {
149 [ + - ]: 48909 : it->SeekToFirst();
150 : : }
151 [ + - + - : 471724 : for (; it->Valid(); it->Next()) {
+ + ]
152 : 409635 : uint16_t db_key;
153 [ + - - + ]: 409635 : assert(it->GetKey(db_key));
154 [ + + + + ]: 409635 : if (oracle_it != oracle.end() && db_key == oracle_it->first) {
155 : 368609 : std::vector<uint8_t> db_value;
156 [ + - - + ]: 368609 : assert(it->GetValue(db_value));
157 [ + - - + ]: 368609 : assert(db_value == MakeValue(db_key, oracle_it->second));
158 : 368609 : ++oracle_it;
159 : 368609 : } else {
160 [ - + ]: 41026 : assert(obfuscate);
161 [ + - ]: 41026 : std::string key_str;
162 [ + - - + ]: 41026 : assert(it->GetKey(key_str));
163 [ - + ]: 41026 : assert(key_str == OBFUSCATION_KEY);
164 : 41026 : }
165 : : }
166 [ - + ]: 62089 : assert(oracle_it == oracle.end());
167 : 62089 : }
168 : :
169 : : /** Maximum number of concurrent reader threads in dbwrapper_concurrent_reads. */
170 : : constexpr size_t MAX_READ_WORKERS{16};
171 : :
172 : : /** Build randomized DBParams from the fuzz input, shared by all targets. */
173 : 47875 : DBParams ConsumeDBParams(FuzzedDataProvider& provider, leveldb::Env* testing_env,
174 : : bool obfuscate, DBOptions options = {})
175 : : {
176 : 95750 : return DBParams{
177 : : .path = "dbwrapper_fuzz",
178 : 47875 : .cache_bytes = provider.ConsumeIntegralInRange<size_t>(64 << 10, 1_MiB),
179 : : .obfuscate = obfuscate,
180 : : .options = options,
181 : : .testing_env = testing_env,
182 : 47875 : .max_file_size = provider.ConsumeBool()
183 [ + + ]: 47875 : ? DBWRAPPER_MAX_FILE_SIZE
184 : 5705 : : provider.ConsumeIntegralInRange<size_t>(1_MiB, 4_MiB),
185 : 47875 : };
186 : : }
187 : :
188 : : /** A single read-only operation run concurrently in dbwrapper_concurrent_reads. */
189 : : enum class ReadOp { Read, Exists, IteratorSeek };
190 : : using ReadQuery = std::tuple<ReadOp, uint16_t>;
191 : : using Results = std::vector<std::optional<std::string>>;
192 : :
193 : 0 : Results RunReadQueries(CDBWrapper& db, const std::vector<ReadQuery>& queries, FastRandomContext& rng)
194 : : {
195 [ # # ]: 0 : std::vector<size_t> order(queries.size());
196 : 0 : std::iota(order.begin(), order.end(), size_t{0});
197 : 0 : std::shuffle(order.begin(), order.end(), rng);
198 : :
199 [ # # # # ]: 0 : Results results(queries.size());
200 [ # # ]: 0 : for (const auto i : order) {
201 [ # # # # ]: 0 : const auto& [op, key] = queries[i];
202 [ # # # # ]: 0 : std::string v;
203 [ # # # # ]: 0 : switch (op) {
204 : 0 : case ReadOp::Read:
205 [ # # # # ]: 0 : if (db.Read(key, v)) results[i] = std::move(v);
206 : : break;
207 : 0 : case ReadOp::Exists:
208 [ # # # # ]: 0 : if (db.Exists(key)) results[i] = std::move(v);
209 : : break;
210 : 0 : case ReadOp::IteratorSeek: {
211 [ # # # # ]: 0 : const std::unique_ptr<CDBIterator> it{db.NewIterator()};
212 [ # # ]: 0 : it->Seek(key);
213 [ # # # # : 0 : if (it->Valid() && it->GetValue(v)) results[i] = std::move(v);
# # # # ]
214 : 0 : break;
215 : 0 : }
216 : : }
217 : 0 : }
218 : 0 : return results;
219 : 0 : }
220 : :
221 : : template <typename DrainWorkFn, typename RunOneFn>
222 : 1158 : void TestDbWrapper(FuzzedDataProvider& provider,
223 : : leveldb::Env* testing_env,
224 : : DrainWorkFn drain_work,
225 : : RunOneFn run_one,
226 : : bool allow_force_compact)
227 : : {
228 : 1158 : SeedRandomStateForTest(SeedRand::ZEROS);
229 : :
230 : 1158 : const bool obfuscate{provider.ConsumeBool()};
231 : :
232 : 49033 : const auto make_db{[&](DBOptions options = {}) {
233 [ + - + - ]: 95750 : return std::make_unique<CDBWrapper>(ConsumeDBParams(provider, testing_env, obfuscate, options));
234 : : }};
235 : 1158 : std::unique_ptr<CDBWrapper> dbw{make_db()};
236 : :
237 : : // Oracle: key → value size. Content is reconstructed via MakeValue().
238 : 1158 : Oracle oracle;
239 : :
240 [ + + + + ]: 164610 : LIMITED_WHILE(provider.ConsumeBool(), 1'000)
241 : : {
242 [ + - ]: 163452 : CallOneOf(
243 : : provider,
244 : : // --- Mutations ---
245 : 24180 : [&] {
246 : 12090 : const auto key{ConsumeKey(provider)};
247 : 12090 : const auto size{ConsumeValueSize(provider)};
248 : 6086 : drain_work();
249 [ + - + - ]: 12090 : dbw->Write(key, MakeValue(key, size), /*fSync=*/provider.ConsumeBool());
250 : 12090 : oracle[key] = size;
251 : : },
252 : 89886 : [&] {
253 : 44943 : const auto key{ConsumeKey(provider)};
254 : 23943 : drain_work();
255 : 44943 : dbw->Erase(key, /*fSync=*/provider.ConsumeBool());
256 : 44943 : oracle.erase(key);
257 : : },
258 : 7093 : [&] {
259 [ + - + - ]: 7093 : CDBBatch batch{*dbw};
260 [ + - + - ]: 7093 : std::map<uint16_t, uint32_t> batch_writes;
261 : 7093 : std::set<uint16_t> batch_erases;
262 : 19337 : const auto fill{[&] {
263 [ + + + + : 132493 : LIMITED_WHILE(provider.ConsumeBool(), 20)
+ + + + ]
264 : : {
265 : 120249 : const auto key{ConsumeKey(provider)};
266 [ + + + + ]: 120249 : if (provider.ConsumeBool()) {
267 : 51526 : const auto size{ConsumeValueSize(provider)};
268 [ + - + - ]: 51526 : batch.Write(key, MakeValue(key, size));
269 : 51526 : batch_writes[key] = size;
270 : 51526 : batch_erases.erase(key);
271 : : } else {
272 : 68723 : batch.Erase(key);
273 : 68723 : batch_erases.insert(key);
274 : 68723 : batch_writes.erase(key);
275 : : }
276 : : }
277 : : }};
278 [ + - + - ]: 7093 : fill();
279 [ + + + + ]: 7093 : if (provider.ConsumeBool()) {
280 [ + - - + : 5151 : assert(batch.ApproximateSize() >= WRITE_BATCH_HEADER);
+ - - + ]
281 [ + - + - ]: 5151 : batch.Clear();
282 [ + - - + : 5151 : assert(batch.ApproximateSize() == WRITE_BATCH_HEADER);
+ - - + ]
283 : 5151 : batch_writes.clear();
284 : 5151 : batch_erases.clear();
285 [ + - + - ]: 5151 : fill();
286 : : }
287 [ + - ]: 4521 : drain_work();
288 [ + - + - ]: 7093 : dbw->WriteBatch(batch, /*fSync=*/provider.ConsumeBool());
289 [ + - + + : 31187 : for (const auto& [k, v] : batch_writes) oracle[k] = v;
+ - + + ]
290 [ + + + + ]: 23389 : for (const auto& k : batch_erases) oracle.erase(k);
291 : 7093 : },
292 : 93434 : [&] {
293 : 29006 : drain_work();
294 [ + - + - ]: 46717 : dbw.reset();
295 : 46717 : DBOptions options{};
296 [ + - + + : 46717 : if (allow_force_compact && provider.ConsumeBool()) {
- + - - ]
297 : 14234 : options.force_compact = true;
298 : : }
299 : 46717 : dbw = make_db(options);
300 : 46717 : VerifyIterator(*dbw, oracle, obfuscate);
301 : : },
302 : : // --- Reads ---
303 : 9794 : [&] {
304 : 4897 : const auto key{ConsumeKey(provider)};
305 : 4897 : std::vector<uint8_t> value;
306 [ + - + - ]: 4897 : const bool found{dbw->Read(key, value)};
307 [ + + + + ]: 4897 : if (const auto it{oracle.find(key)}; it != oracle.end()) {
308 [ + - + - : 4820 : assert(found && value == MakeValue(key, it->second));
- + + - +
- - + ]
309 : : } else {
310 [ - + - + ]: 2487 : assert(!found);
311 : : }
312 : 4897 : },
313 : 37544 : [&] {
314 : 18772 : const auto key{ConsumeKey(provider)};
315 [ - + - + ]: 18772 : assert(dbw->Exists(key) == oracle.contains(key));
316 : : },
317 : 3990 : [&] {
318 : 1995 : uint16_t key{};
319 [ + + + + : 1995 : if (!oracle.empty() && provider.ConsumeBool()) {
+ + + + ]
320 : 1304 : auto it{oracle.begin()};
321 : 1304 : std::advance(it, provider.ConsumeIntegralInRange<size_t>(0, oracle.size() - 1));
322 : 1304 : key = it->first;
323 : : } else {
324 : 691 : key = ConsumeKey(provider);
325 : : }
326 : : FailUnserialize wrong_type;
327 [ - + - + ]: 1995 : assert(!dbw->Read(key, wrong_type));
328 : : },
329 : 28428 : [&] {
330 : 28428 : const auto seek_key{provider.ConsumeBool()
331 [ + + + + ]: 14214 : ? std::optional<uint16_t>{ConsumeKey(provider)}
332 : : : std::nullopt};
333 : 14214 : VerifyIterator(*dbw, oracle, obfuscate, seek_key);
334 : : },
335 : : // --- Stats ---
336 : 3930 : [&] {
337 [ + + + + : 3218 : assert(dbw->IsEmpty() == (oracle.empty() && !obfuscate));
- + + + +
+ - + ]
338 : : },
339 : 8376 : [&] {
340 : 4188 : const auto [k1, k2]{std::minmax({ConsumeKey(provider), ConsumeKey(provider)}, LevelDBBytewiseU16Cmp{})};
341 : 4188 : const size_t estimate_size{dbw->EstimateSize(k1, k2)};
342 [ + + - + : 4188 : if (k1 == k2) assert(estimate_size == 0);
+ + - + ]
343 : : },
344 : 1512 : [&] {
345 : 1512 : (void)dbw->DynamicMemoryUsage();
346 : : },
347 : : // --- Compaction control (no-op when run_one is no-op) ---
348 : 3325 : [&] {
349 : 3325 : run_one();
350 : : });
351 : : }
352 : :
353 [ + - ]: 1158 : VerifyIterator(*dbw, oracle, obfuscate);
354 [ + - ]: 1158 : drain_work();
355 : 1158 : }
356 : :
357 : : } // namespace
358 : :
359 [ + - + - : 1008 : FUZZ_TARGET(dbwrapper, .init = [] { static auto setup{MakeNoLogFileContext<>()}; })
+ - ]
360 : : {
361 : 539 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
362 : :
363 [ + - ]: 539 : const auto memenv{std::unique_ptr<leveldb::Env>{leveldb::NewMemEnv(leveldb::Env::Default())}};
364 [ + - ]: 539 : DeterministicEnv det_env{memenv.get()};
365 [ + - ]: 539 : TestDbWrapper(
366 : : provider, &det_env,
367 : 64095 : [&] { det_env.DrainWork(); },
368 : 3325 : [&] { return det_env.RunOne(); },
369 : : /*allow_force_compact=*/false);
370 : 539 : }
371 : :
372 [ + - + - : 1087 : FUZZ_TARGET(dbwrapper_threaded, .init = [] { static auto setup{MakeNoLogFileContext<>()}; })
+ - ]
373 : : {
374 : 619 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
375 : :
376 [ + - ]: 619 : const auto memenv{std::unique_ptr<leveldb::Env>{leveldb::NewMemEnv(leveldb::Env::Default())}};
377 [ + - ]: 619 : TestDbWrapper(
378 : : provider, memenv.get(),
379 : : /*drain_work=*/[] {},
380 : : /*run_one=*/[] { return false; },
381 : : /*allow_force_compact=*/true);
382 : 619 : }
383 : :
384 [ + - + - : 468 : FUZZ_TARGET(dbwrapper_concurrent_reads, .init = [] { static auto setup{MakeNoLogFileContext<>()}; })
+ - ]
385 : : {
386 : 0 : SeedRandomStateForTest(SeedRand::ZEROS);
387 : :
388 : 0 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
389 : :
390 [ # # ]: 0 : const auto memenv{std::unique_ptr<leveldb::Env>{leveldb::NewMemEnv(leveldb::Env::Default())}};
391 [ # # ]: 0 : DeterministicEnv det_env{memenv.get()};
392 : :
393 [ # # # # ]: 0 : CDBWrapper db{ConsumeDBParams(provider, &det_env, /*obfuscate=*/provider.ConsumeBool())};
394 : :
395 : : // Seed the DB. Drain work after small batches so we don't deadlock on a
396 : : // scheduled compaction.
397 : 0 : const size_t num_entries{provider.ConsumeIntegralInRange<size_t>(100, 5'000)};
398 : 0 : std::vector<uint16_t> keys;
399 [ # # ]: 0 : keys.reserve(num_entries);
400 : : constexpr size_t SEED_BATCH_SIZE{400};
401 [ # # ]: 0 : for (size_t start{0}; start < num_entries; start += SEED_BATCH_SIZE) {
402 [ # # ]: 0 : CDBBatch batch{db};
403 [ # # ]: 0 : const size_t end{std::min(start + SEED_BATCH_SIZE, num_entries)};
404 [ # # ]: 0 : for (size_t i{start}; i < end; ++i) {
405 : 0 : const auto k{ConsumeKey(provider)};
406 [ # # # # ]: 0 : batch.Write(k, MakeValue(k, ConsumeValueSize(provider)));
407 [ # # ]: 0 : keys.push_back(k);
408 : : }
409 : : det_env.DrainWork();
410 [ # # ]: 0 : db.WriteBatch(batch, /*fSync=*/true);
411 : 0 : }
412 : :
413 [ # # # # : 0 : while (provider.ConsumeBool() && det_env.RunOne()) {}
# # # # ]
414 : :
415 : : // Build query list from seeded and random keys.
416 : 0 : const size_t num_queries{provider.ConsumeIntegralInRange<size_t>(1, 2'000)};
417 : 0 : std::vector<ReadQuery> queries;
418 [ # # ]: 0 : queries.reserve(num_queries);
419 [ # # ]: 0 : for (size_t i{0}; i < num_queries; ++i) {
420 : 0 : const auto op{static_cast<ReadOp>(provider.ConsumeIntegralInRange<int>(0, 2))};
421 : 0 : const uint16_t key{provider.ConsumeBool()
422 [ # # ]: 0 : ? keys[provider.ConsumeIntegralInRange<size_t>(0, keys.size() - 1)]
423 : 0 : : ConsumeKey(provider)};
424 [ # # ]: 0 : queries.emplace_back(op, key);
425 : : }
426 : :
427 : : // Baseline read on a single thread
428 : 0 : FastRandomContext rng{ConsumeUInt256(provider)};
429 [ # # ]: 0 : const Results baseline{RunReadQueries(db, queries, rng)};
430 : :
431 [ # # # # ]: 0 : ThreadPool pool{"dbfuzz"};
432 [ # # ]: 0 : pool.Start(MAX_READ_WORKERS);
433 : :
434 : : // Workers + main thread synchronize on the latch so all reads start together.
435 : 0 : std::latch start_latch{static_cast<ptrdiff_t>(MAX_READ_WORKERS + 1)};
436 [ # # ]: 0 : std::vector<std::function<Results()>> tasks(MAX_READ_WORKERS);
437 [ # # ]: 0 : std::generate(tasks.begin(), tasks.end(), [&] {
438 : 0 : return [&, seed = rng.rand256()]() -> Results {
439 : 0 : FastRandomContext thread_rng{seed};
440 [ # # ]: 0 : start_latch.arrive_and_wait();
441 [ # # ]: 0 : return RunReadQueries(db, queries, thread_rng);
442 : 0 : };
443 : : });
444 [ # # ]: 0 : auto futures{*Assert(pool.Submit(std::move(tasks)))};
445 : :
446 : : // Release the workers and immediately run the queued compaction on this
447 : : // thread, so compaction races against the concurrent reads.
448 [ # # ]: 0 : start_latch.arrive_and_wait();
449 : 0 : det_env.DrainWork();
450 : :
451 [ # # # # : 0 : for (auto& fut : futures) assert(fut.get() == baseline);
# # ]
452 : : det_env.DrainWork();
453 : 0 : }
|