1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3  *     Copyright 2015-Present Couchbase, Inc.
4  *
5  *   Use of this software is governed by the Business Source License included
6  *   in the file licenses/BSL-Couchbase.txt.  As of the Change Date specified
7  *   in that file, in accordance with the Business Source License, use of this
8  *   software will be governed by the Apache License, Version 2.0, included in
9  *   the file licenses/APL2.txt.
10  */
11 
12 // mock_cookie.h must be included before ep_test_apis.h as ep_test_apis.h
13 // define a macro named check and some of the folly headers also use the
14 // name check
15 #include <programs/engine_testapp/mock_cookie.h>
16 
17 #include "ep_test_apis.h"
18 #include "ep_testsuite_common.h"
19 #include <platform/cb_malloc.h>
20 #include <platform/compress.h>
21 #include <platform/dirutils.h>
22 #include <iostream>
23 #include <string>
24 
25 const char *dbname_env = nullptr;
26 
27 static enum test_result skipped_test_function(EngineIface* h);
28 
29 BaseTestCase::BaseTestCase(const char *_name, const char *_cfg, bool _skip)
30   : name(_name),
31     cfg(_cfg),
32     skip(_skip) {
33 }
34 
35 TestCase::TestCase(const char* _name,
36                    enum test_result (*_tfun)(EngineIface*),
37                    bool (*_test_setup)(EngineIface*),
38                    bool (*_test_teardown)(EngineIface*),
39                    const char* _cfg,
40                    enum test_result (*_prepare)(engine_test_t* test),
41                    void (*_cleanup)(engine_test_t* test,
42                                     enum test_result result),
43                    bool _skip)
44     : BaseTestCase(_name, _cfg, _skip) {
45     test.tfun = _tfun;
46     test.test_setup = _test_setup;
47     test.test_teardown = _test_teardown;
48     test.prepare = _prepare;
49     test.cleanup = _cleanup;
50 }
51 
52 TestCaseV2::TestCaseV2(const char *_name,
53                        enum test_result(*_tfun)(engine_test_t *),
54                        bool(*_test_setup)(engine_test_t *),
55                        bool(*_test_teardown)(engine_test_t *),
56                        const char *_cfg,
57                        enum test_result (*_prepare)(engine_test_t *test),
58                        void (*_cleanup)(engine_test_t *test, enum test_result result),
59                        bool _skip)
60   : BaseTestCase(_name, _cfg, _skip) {
61     test.api_v2.tfun = _tfun;
62     test.api_v2.test_setup = _test_setup;
63     test.api_v2.test_teardown = _test_teardown;
64     test.prepare = _prepare;
65     test.cleanup = _cleanup;
66 }
67 
68 engine_test_t* BaseTestCase::getTest() {
69     engine_test_t *ret = &test;
70 
71     std::string nm(name);
72     std::stringstream ss;
73 
74     if (cfg != nullptr) {
75         ss << cfg << ";";
76     }
77 
78     // Default to the suite's dbname if the test config didn't already
79     // specify it.
80     if ((cfg == nullptr) ||
81         (std::string(cfg).find("dbname=") == std::string::npos)) {
82         ss << "dbname=" << default_dbname << ";";
83     }
84 
85     // Default to 4 vBuckets (faster to setup/teardown) if the test config
86     // didn't already specify it or shards.
87     if ((cfg == nullptr) ||
88         ((std::string(cfg).find("max_vbuckets=") == std::string::npos) &&
89         (std::string(cfg).find("max_num_shards=") == std::string::npos))) {
90         ss << "max_vbuckets=4;max_num_shards=4;";
91     }
92 
93     if (skip) {
94         nm.append(" (skipped)");
95         ret->tfun = skipped_test_function;
96     }
97 
98     ret->name = std::move(nm);
99     ret->cfg = ss.str();
100 
101     return ret;
102 }
103 
104 static enum test_result skipped_test_function(EngineIface*) {
105     return SKIPPED;
106 }
107 
108 enum test_result rmdb(std::string_view path) {
109     std::filesystem::remove_all(path);
110     return SUCCESS;
111 }
112 
113 bool test_setup(EngineIface* h) {
114     wait_for_warmup_complete(h);
115     wait_for_flusher_to_settle(h);
116 
117     check(set_vbucket_state(h, Vbid(0), vbucket_state_active),
118           "Failed to set VB0 state.");
119 
120     const auto bucket_type = get_str_stat(h, "ep_bucket_type");
121     if (bucket_type == "persistent") {
122         // Wait for vb0's state (active) to be persisted to disk, that way
123         // we know the KVStore files exist on disk.
124         wait_for_stat_to_be_gte(h, "ep_persist_vbstate_total", 1);
125     } else if (bucket_type == "ephemeral") {
126         // No persistence to wait for here.
127     } else {
128         check(false,
129               (std::string("test_setup: unknown bucket_type '") + bucket_type +
130                "' - cannot continue.")
131                       .c_str());
132         return false;
133     }
134 
135     std::unique_ptr<MockCookie> cookie = std::make_unique<MockCookie>();
136     // warmup is complete, notify ep engine that it must now enable
137     // data traffic
138     auto request = createPacket(cb::mcbp::ClientOpcode::EnableTraffic);
139 
140     checkeq(cb::engine_errc::success,
141             h->unknown_command(*cookie, *request, add_response),
142             "Failed to enable data traffic");
143 
144     return true;
145 }
146 
147 bool teardown(EngineIface*) {
148     vals.clear();
149     return true;
150 }
151 
152 bool teardown_v2(engine_test_t* test) {
153     (void)test;
154     vals.clear();
155     return true;
156 }
157 
158 std::string get_dbname(const std::string& test_cfg) {
159     if (test_cfg.empty()) {
160         return dbname_env;
161     }
162 
163     auto idx = test_cfg.find("dbname=");
164     if (idx == std::string::npos) {
165         return dbname_env;
166     }
167 
168     auto dbname = test_cfg.substr(idx + 7);
169     std::string::size_type end = dbname.find(';');
170     if (end != dbname.npos) {
171         dbname.resize(end);
172     }
173 
174     return dbname;
175 }
176 
177 enum test_result prepare(engine_test_t *test) {
178     std::string dbname = get_dbname(test->cfg);
179     /* Remove if the same DB directory already exists */
180     rmdb(dbname);
181     std::filesystem::create_directories(dbname);
182     return SUCCESS;
183 }
184 
185 /// Prepare a test which is currently broken (i.e. under investigation).
186 enum test_result prepare_broken_test(engine_test_t* test) {
187     return SKIPPED;
188 }
189 
190 enum test_result prepare_ep_bucket(engine_test_t* test) {
191     std::string cfg{test->cfg};
192     if (cfg.find("bucket_type=ephemeral") != std::string::npos) {
193         return SKIPPED;
194     }
195 
196     // Perform whatever prep the "base class" function wants.
197     return prepare(test);
198 }
199 
200 enum test_result prepare_ep_bucket_skip_broken_under_rocks(engine_test_t* test) {
201     std::string cfg{test->cfg};
202     if (cfg.find("backend=rocksdb") != std::string::npos) {
203         return SKIPPED_UNDER_ROCKSDB;
204     }
205 
206     // Perform whatever prep the ep bucket function wants.
207     return prepare_ep_bucket(test);
208 }
209 
210 enum test_result prepare_ep_bucket_skip_broken_under_magma(
211         engine_test_t* test) {
212     if (std::string(test->cfg).find("backend=magma") != std::string::npos) {
213         return SKIPPED_UNDER_MAGMA;
214     }
215 
216     // Perform whatever prep the ep bucket function wants.
217     return prepare_ep_bucket(test);
218 }
219 
220 enum test_result prepare_ep_bucket_skip_broken_under_rocks_and_magma(
221         engine_test_t* test) {
222     std::string cfg{test->cfg};
223     if (cfg.find("backend=rocksdb") != std::string::npos) {
224         return SKIPPED_UNDER_ROCKSDB;
225     }
226     if (cfg.find("backend=magma") != std::string::npos) {
227         return SKIPPED_UNDER_MAGMA;
228     }
229 
230     // Perform whatever prep the ep bucket function wants.
231     return prepare_ep_bucket(test);
232 }
233 
234 enum test_result prepare_ep_bucket_skip_broken_under_rocks_full_eviction(
235         engine_test_t* test) {
236     std::string cfg{test->cfg};
237     if (cfg.find("bucket_type=ephemeral") != std::string::npos) {
238         return SKIPPED;
239     }
240 
241     if (cfg.find("backend=rocksdb") != std::string::npos &&
242         cfg.find("item_eviction_policy=full_eviction")) {
243         return SKIPPED_UNDER_ROCKSDB;
244     }
245 
246     return prepare_ep_bucket(test);
247 }
248 
249 enum test_result prepare_skip_broken_under_rocks(engine_test_t* test) {
250     std::string cfg{test->cfg};
251     if (cfg.find("backend=rocksdb") != std::string::npos) {
252         return SKIPPED_UNDER_ROCKSDB;
253     }
254 
255     // Perform whatever prep the "base class" function wants.
256     return prepare(test);
257 }
258 
259 enum test_result prepare_skip_broken_under_magma(engine_test_t* test) {
260     if (std::string(test->cfg).find("backend=magma") != std::string::npos) {
261         return SKIPPED_UNDER_MAGMA;
262     }
263 
264     // Perform whatever prep the "base class" function wants.
265     return prepare(test);
266 }
267 
268 enum test_result prepare_skip_broken_under_rocks_and_magma(
269         engine_test_t* test) {
270     std::string cfg{test->cfg};
271     if (cfg.find("backend=rocksdb") != std::string::npos) {
272         return SKIPPED_UNDER_ROCKSDB;
273     }
274     if (cfg.find("backend=magma") != std::string::npos) {
275         return SKIPPED_UNDER_MAGMA;
276     }
277 
278     // Perform whatever prep the "base class" function wants.
279     return prepare(test);
280 }
281 
282 enum test_result prepare_skip_broken_under_ephemeral_and_rocks(
283         engine_test_t* test) {
284     return prepare_ep_bucket_skip_broken_under_rocks(test);
285 }
286 
287 enum test_result prepare_ephemeral_bucket(engine_test_t* test) {
288     std::string cfg{test->cfg};
289     if (cfg.find("bucket_type=ephemeral") == std::string::npos) {
290         return SKIPPED;
291     }
292 
293     // Perform whatever prep the "base class" function wants.
294     return prepare(test);
295 }
296 
297 enum test_result prepare_full_eviction(engine_test_t *test) {
298 
299     // If we cannot find FE in the config, skip the test
300     if (std::string(test->cfg).find("item_eviction_policy=full_eviction") ==
301         std::string::npos) {
302         return SKIPPED;
303     }
304 
305     // Ephemeral buckets don't support full eviction.
306     if (std::string(test->cfg).find("bucket_type=ephemeral")
307             != std::string::npos) {
308         return SKIPPED;
309     }
310 
311     // Perform whatever prep the "base class" function wants.
312     return prepare(test);
313 }
314 
315 enum test_result prepare_full_eviction_skip_under_rocks(engine_test_t *test) {
316 
317     std::string cfg{test->cfg};
318     if (cfg.find("backend=rocksdb") != std::string::npos) {
319         return SKIPPED_UNDER_ROCKSDB;
320     }
321 
322     // Perform whatever prep the "base class" function wants.
323     return prepare_full_eviction(test);
324 
325 }
326 
327 enum test_result prepare_skip_broken_under_rocks_full_eviction(
328         engine_test_t* test) {
329     std::string cfg{test->cfg};
330     if (cfg.find("bucket_type=ephemeral") == std::string::npos) {
331         if (cfg.find("backend=rocksdb") != std::string::npos &&
332             cfg.find("item_eviction_policy=full_eviction")) {
333             return SKIPPED_UNDER_ROCKSDB;
334         }
335     }
336 
337     // Perform whatever prep the "base class" function wants.
338     return prepare_full_eviction(test);
339 }
340 
341 enum test_result prepare_skip_broken_under_ephemeral(engine_test_t *test) {
342     return prepare_ep_bucket(test);
343 }
344 
345 void cleanup(engine_test_t *test, enum test_result result) {
346     (void)result;
347     // Nuke the database files we created
348     std::string dbname = get_dbname(test->cfg);
349     /* Remove only the db file this test created */
350     rmdb(dbname);
351 }
352 
353 // Should only one test be run, and if so which number? If -1 then all tests
354 // are run.
355 static int oneTestIdx;
356 
357 struct test_harness* testHarness;
358 
359 // Array of testcases. Provided by the specific testsuite.
360 extern BaseTestCase testsuite_testcases[];
361 
362 // Examines the list of tests provided by the specific testsuite
363 // via the testsuite_testcases[] array, populates `testcases` and returns it.
364 std::vector<engine_test_t> get_tests() {
365     std::vector<engine_test_t> testcases;
366 
367     // Calculate the size of the tests..
368     int num = 0;
369     while (testsuite_testcases[num].getName() != nullptr) {
370         ++num;
371     }
372 
373     oneTestIdx = -1;
374     char *testNum = getenv("EP_TEST_NUM");
375     if (testNum) {
376         sscanf(testNum, "%d", &oneTestIdx);
377         if (oneTestIdx < 0 || oneTestIdx > num) {
378             oneTestIdx = -1;
379         }
380     }
381     dbname_env = getenv("EP_TEST_DIR");
382     if (!dbname_env) {
383         dbname_env = default_dbname;
384     }
385 
386     if (oneTestIdx == -1) {
387         for (int jj = 0; jj < num; ++jj) {
388             auto* r = testsuite_testcases[jj].getTest();
389             if (r) {
390                 testcases.emplace_back(*r);
391             }
392         }
393     } else {
394         auto* r = testsuite_testcases[oneTestIdx].getTest();
395         if (r) {
396             testcases.emplace_back(*r);
397         }
398     }
399 
400     return testcases;
401 }
402 
403 BucketType get_bucket_type() {
404     return BucketType::Couchbase;
405 }
406 
407 bool setup_suite(struct test_harness *th) {
408     testHarness = th;
409     return true;
410 }
411 
412 
413 bool teardown_suite() {
414     return true;
415 }
416 
417 /*
418  * Create n_buckets and return how many were actually created.
419  */
420 int create_buckets(const std::string& cfg,
421                    int n_buckets,
422                    std::vector<BucketHolder>& buckets) {
423     std::string dbname = get_dbname(cfg);
424 
425     for (int ii = 0; ii < n_buckets; ii++) {
426         std::stringstream config, dbpath;
427         dbpath << dbname.c_str() << ii;
428         std::string str_cfg(cfg);
429         /* Find the position of "dbname=" in str_cfg */
430         size_t pos = str_cfg.find("dbname=");
431         if (pos != std::string::npos) {
432             /* Move till end of the dbname */
433             size_t new_pos = str_cfg.find(';', pos);
434             str_cfg.insert(new_pos, std::to_string(ii));
435             config << str_cfg;
436         } else {
437             config << str_cfg << "dbname=" << dbpath.str();
438         }
439 
440         rmdb(dbpath.str());
441         auto* handle = testHarness->create_bucket(true, config.str());
442         if (handle) {
443             buckets.emplace_back(BucketHolder(handle, dbpath.str()));
444         } else {
445             return ii;
446         }
447     }
448     return n_buckets;
449 }
450 
451 void destroy_buckets(std::vector<BucketHolder> &buckets) {
452     for(auto bucket : buckets) {
453         testHarness->destroy_bucket(bucket.h, false);
454         rmdb(bucket.dbpath);
455     }
456 }
457 
458 void check_key_value(EngineIface* h,
459                      const char* key,
460                      const char* val,
461                      size_t vlen,
462                      Vbid vbucket) {
463     // Fetch item itself, to ensure we maintain a ref-count on the underlying
464     // Blob while comparing the key.
465     auto getResult = get(h, nullptr, key, vbucket);
466     checkeq(cb::engine_errc::success,
467             getResult.first,
468             "Failed to fetch document");
469     item_info info;
470     check(h->get_item_info(*getResult.second.get(), info),
471           "Failed to get_item_info");
472 
473     std::string_view payload;
474     cb::compression::Buffer inflated;
475     if (isCompressionEnabled(h) &&
476         (info.datatype & PROTOCOL_BINARY_DATATYPE_SNAPPY)) {
477         cb::compression::inflate(cb::compression::Algorithm::Snappy,
478                                  {static_cast<const char *>(info.value[0].iov_base),
479                                   info.value[0].iov_len},
480                                  inflated);
481         payload = inflated;
482     } else {
483         payload = {static_cast<const char *>(info.value[0].iov_base),
484                                              info.value[0].iov_len};
485     }
486 
487     checkeq(std::string_view(val, vlen), payload, "Data mismatch");
488 }
489 
490 bool isCompressionEnabled(EngineIface* h) {
491     return h->getCompressionMode() != BucketCompressionMode::Off;
492 }
493 
494 bool isActiveCompressionEnabled(EngineIface* h) {
495     return h->getCompressionMode() == BucketCompressionMode::Active;
496 }
497 
498 bool isPassiveCompressionEnabled(EngineIface* h) {
499     return h->getCompressionMode() == BucketCompressionMode::Passive;
500 }
501 
502 bool isWarmupEnabled(EngineIface* h) {
503     return isPersistentBucket(h) && get_bool_stat(h, "ep_warmup");
504 }
505 
506 bool isPersistentBucket(EngineIface* h) {
507     return get_str_stat(h, "ep_bucket_type") == "persistent";
508 }
509 
510 bool isEphemeralBucket(EngineIface* h) {
511     return get_str_stat(h, "ep_bucket_type") == "ephemeral";
512 }
513 
514 bool isFollyExecutorPool(EngineIface* h) {
515     return get_str_stat(h, "ep_executor_pool_backend", "config") == "folly";
516 }
517 
518 void checkPersistentBucketTempItems(EngineIface* h, int exp) {
519     if (isPersistentBucket(h)) {
520         checkeq(exp,
521                 get_int_stat(h, "curr_temp_items"),
522                 "CheckPersistentBucketTempItems(): Num temp items not as "
523                 "expected");
524     }
525 }
526 
527 void setAndWaitForQuotaChange(EngineIface* h, uint64_t newQuota) {
528     set_param(h,
529               EngineParamCategory::Flush,
530               "max_size",
531               std::to_string(newQuota).c_str());
532     wait_for_stat_to_be(h, "ep_max_size", newQuota);
533 }
534