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(const char* path) {
109     if (cb::io::isDirectory(path)) {
110         cb::io::rmrf(path);
111     }
112     if (cb::io::isDirectory(path) || cb::io::isFile(path)) {
113         std::cerr << "Failed to remove: " << path << " " << std::endl;
114         return FAIL;
115     }
116     return SUCCESS;
117 }
118 
119 bool test_setup(EngineIface* h) {
120     wait_for_warmup_complete(h);
121     wait_for_flusher_to_settle(h);
122 
123     check(set_vbucket_state(h, Vbid(0), vbucket_state_active),
124           "Failed to set VB0 state.");
125 
126     const auto bucket_type = get_str_stat(h, "ep_bucket_type");
127     if (bucket_type == "persistent") {
128         // Wait for vb0's state (active) to be persisted to disk, that way
129         // we know the KVStore files exist on disk.
130         wait_for_stat_to_be_gte(h, "ep_persist_vbstate_total", 1);
131     } else if (bucket_type == "ephemeral") {
132         // No persistence to wait for here.
133     } else {
134         check(false,
135               (std::string("test_setup: unknown bucket_type '") + bucket_type +
136                "' - cannot continue.")
137                       .c_str());
138         return false;
139     }
140 
141     std::unique_ptr<MockCookie> cookie = std::make_unique<MockCookie>();
142     // warmup is complete, notify ep engine that it must now enable
143     // data traffic
144     auto request = createPacket(cb::mcbp::ClientOpcode::EnableTraffic);
145 
146     checkeq(cb::engine_errc::success,
147             h->unknown_command(cookie.get(), *request, add_response),
148             "Failed to enable data traffic");
149 
150     return true;
151 }
152 
153 bool teardown(EngineIface*) {
154     vals.clear();
155     return true;
156 }
157 
158 bool teardown_v2(engine_test_t* test) {
159     (void)test;
160     vals.clear();
161     return true;
162 }
163 
164 std::string get_dbname(const std::string& test_cfg) {
165     if (test_cfg.empty()) {
166         return dbname_env;
167     }
168 
169     auto idx = test_cfg.find("dbname=");
170     if (idx == std::string::npos) {
171         return dbname_env;
172     }
173 
174     auto dbname = test_cfg.substr(idx + 7);
175     std::string::size_type end = dbname.find(';');
176     if (end != dbname.npos) {
177         dbname.resize(end);
178     }
179 
180     return dbname;
181 }
182 
183 enum test_result prepare(engine_test_t *test) {
184     std::string dbname = get_dbname(test->cfg);
185     /* Remove if the same DB directory already exists */
186     try {
187         rmdb(dbname.c_str());
188     } catch (std::system_error& e) {
189         if (e.code() != std::error_code(ENOENT, std::system_category())) {
190             throw e;
191         }
192     }
193     cb::io::mkdirp(dbname);
194     return SUCCESS;
195 }
196 
197 /// Prepare a test which is currently broken (i.e. under investigation).
198 enum test_result prepare_broken_test(engine_test_t* test) {
199     return SKIPPED;
200 }
201 
202 enum test_result prepare_ep_bucket(engine_test_t* test) {
203     std::string cfg{test->cfg};
204     if (cfg.find("bucket_type=ephemeral") != std::string::npos) {
205         return SKIPPED;
206     }
207 
208     // Perform whatever prep the "base class" function wants.
209     return prepare(test);
210 }
211 
212 enum test_result prepare_ep_bucket_skip_broken_under_rocks(engine_test_t* test) {
213     std::string cfg{test->cfg};
214     if (cfg.find("backend=rocksdb") != std::string::npos) {
215         return SKIPPED_UNDER_ROCKSDB;
216     }
217 
218     // Perform whatever prep the ep bucket function wants.
219     return prepare_ep_bucket(test);
220 }
221 
222 enum test_result prepare_ep_bucket_skip_broken_under_magma(
223         engine_test_t* test) {
224     if (std::string(test->cfg).find("backend=magma") != std::string::npos) {
225         return SKIPPED_UNDER_MAGMA;
226     }
227 
228     // Perform whatever prep the ep bucket function wants.
229     return prepare_ep_bucket(test);
230 }
231 
232 enum test_result prepare_ep_bucket_skip_broken_under_rocks_and_magma(
233         engine_test_t* test) {
234     std::string cfg{test->cfg};
235     if (cfg.find("backend=rocksdb") != std::string::npos) {
236         return SKIPPED_UNDER_ROCKSDB;
237     }
238     if (cfg.find("backend=magma") != std::string::npos) {
239         return SKIPPED_UNDER_MAGMA;
240     }
241 
242     // Perform whatever prep the ep bucket function wants.
243     return prepare_ep_bucket(test);
244 }
245 
246 enum test_result prepare_ep_bucket_skip_broken_under_rocks_full_eviction(
247         engine_test_t* test) {
248     std::string cfg{test->cfg};
249     if (cfg.find("bucket_type=ephemeral") != std::string::npos) {
250         return SKIPPED;
251     }
252 
253     if (cfg.find("backend=rocksdb") != std::string::npos &&
254         cfg.find("item_eviction_policy=full_eviction")) {
255         return SKIPPED_UNDER_ROCKSDB;
256     }
257 
258     return prepare_ep_bucket(test);
259 }
260 
261 enum test_result prepare_skip_broken_under_rocks(engine_test_t* test) {
262     std::string cfg{test->cfg};
263     if (cfg.find("backend=rocksdb") != std::string::npos) {
264         return SKIPPED_UNDER_ROCKSDB;
265     }
266 
267     // Perform whatever prep the "base class" function wants.
268     return prepare(test);
269 }
270 
271 enum test_result prepare_skip_broken_under_magma(engine_test_t* test) {
272     if (std::string(test->cfg).find("backend=magma") != std::string::npos) {
273         return SKIPPED_UNDER_MAGMA;
274     }
275 
276     // Perform whatever prep the "base class" function wants.
277     return prepare(test);
278 }
279 
280 enum test_result prepare_skip_broken_under_rocks_and_magma(
281         engine_test_t* test) {
282     std::string cfg{test->cfg};
283     if (cfg.find("backend=rocksdb") != std::string::npos) {
284         return SKIPPED_UNDER_ROCKSDB;
285     }
286     if (cfg.find("backend=magma") != std::string::npos) {
287         return SKIPPED_UNDER_MAGMA;
288     }
289 
290     // Perform whatever prep the "base class" function wants.
291     return prepare(test);
292 }
293 
294 enum test_result prepare_skip_broken_under_ephemeral_and_rocks(
295         engine_test_t* test) {
296     return prepare_ep_bucket_skip_broken_under_rocks(test);
297 }
298 
299 enum test_result prepare_ephemeral_bucket(engine_test_t* test) {
300     std::string cfg{test->cfg};
301     if (cfg.find("bucket_type=ephemeral") == std::string::npos) {
302         return SKIPPED;
303     }
304 
305     // Perform whatever prep the "base class" function wants.
306     return prepare(test);
307 }
308 
309 enum test_result prepare_full_eviction(engine_test_t *test) {
310 
311     // If we cannot find FE in the config, skip the test
312     if (std::string(test->cfg).find("item_eviction_policy=full_eviction") ==
313         std::string::npos) {
314         return SKIPPED;
315     }
316 
317     // Ephemeral buckets don't support full eviction.
318     if (std::string(test->cfg).find("bucket_type=ephemeral")
319             != std::string::npos) {
320         return SKIPPED;
321     }
322 
323     // Perform whatever prep the "base class" function wants.
324     return prepare(test);
325 }
326 
327 enum test_result prepare_full_eviction_skip_under_rocks(engine_test_t *test) {
328 
329     std::string cfg{test->cfg};
330     if (cfg.find("backend=rocksdb") != std::string::npos) {
331         return SKIPPED_UNDER_ROCKSDB;
332     }
333 
334     // Perform whatever prep the "base class" function wants.
335     return prepare_full_eviction(test);
336 
337 }
338 
339 enum test_result prepare_skip_broken_under_rocks_full_eviction(
340         engine_test_t* test) {
341     std::string cfg{test->cfg};
342     if (cfg.find("bucket_type=ephemeral") == std::string::npos) {
343         if (cfg.find("backend=rocksdb") != std::string::npos &&
344             cfg.find("item_eviction_policy=full_eviction")) {
345             return SKIPPED_UNDER_ROCKSDB;
346         }
347     }
348 
349     // Perform whatever prep the "base class" function wants.
350     return prepare_full_eviction(test);
351 }
352 
353 enum test_result prepare_skip_broken_under_ephemeral(engine_test_t *test) {
354     return prepare_ep_bucket(test);
355 }
356 
357 void cleanup(engine_test_t *test, enum test_result result) {
358     (void)result;
359     // Nuke the database files we created
360     std::string dbname = get_dbname(test->cfg);
361     /* Remove only the db file this test created */
362     try {
363         rmdb(dbname.c_str());
364     } catch (std::system_error& e) {
365         if (e.code() != std::error_code(ENOENT, std::system_category())) {
366             throw e;
367         }
368     }
369 }
370 
371 // Should only one test be run, and if so which number? If -1 then all tests
372 // are run.
373 static int oneTestIdx;
374 
375 struct test_harness* testHarness;
376 
377 // Array of testcases. Provided by the specific testsuite.
378 extern BaseTestCase testsuite_testcases[];
379 
380 // Examines the list of tests provided by the specific testsuite
381 // via the testsuite_testcases[] array, populates `testcases` and returns it.
382 std::vector<engine_test_t> get_tests() {
383     std::vector<engine_test_t> testcases;
384 
385     // Calculate the size of the tests..
386     int num = 0;
387     while (testsuite_testcases[num].getName() != nullptr) {
388         ++num;
389     }
390 
391     oneTestIdx = -1;
392     char *testNum = getenv("EP_TEST_NUM");
393     if (testNum) {
394         sscanf(testNum, "%d", &oneTestIdx);
395         if (oneTestIdx < 0 || oneTestIdx > num) {
396             oneTestIdx = -1;
397         }
398     }
399     dbname_env = getenv("EP_TEST_DIR");
400     if (!dbname_env) {
401         dbname_env = default_dbname;
402     }
403 
404     if (oneTestIdx == -1) {
405         for (int jj = 0; jj < num; ++jj) {
406             auto* r = testsuite_testcases[jj].getTest();
407             if (r) {
408                 testcases.emplace_back(*r);
409             }
410         }
411     } else {
412         auto* r = testsuite_testcases[oneTestIdx].getTest();
413         if (r) {
414             testcases.emplace_back(*r);
415         }
416     }
417 
418     return testcases;
419 }
420 
421 BucketType get_bucket_type() {
422     return BucketType::Couchbase;
423 }
424 
425 bool setup_suite(struct test_harness *th) {
426     testHarness = th;
427     return true;
428 }
429 
430 
431 bool teardown_suite() {
432     return true;
433 }
434 
435 /*
436  * Create n_buckets and return how many were actually created.
437  */
438 int create_buckets(const std::string& cfg,
439                    int n_buckets,
440                    std::vector<BucketHolder>& buckets) {
441     std::string dbname = get_dbname(cfg);
442 
443     for (int ii = 0; ii < n_buckets; ii++) {
444         std::stringstream config, dbpath;
445         dbpath << dbname.c_str() << ii;
446         std::string str_cfg(cfg);
447         /* Find the position of "dbname=" in str_cfg */
448         size_t pos = str_cfg.find("dbname=");
449         if (pos != std::string::npos) {
450             /* Move till end of the dbname */
451             size_t new_pos = str_cfg.find(';', pos);
452             str_cfg.insert(new_pos, std::to_string(ii));
453             config << str_cfg;
454         } else {
455             config << str_cfg << "dbname=" << dbpath.str();
456         }
457 
458         try {
459             rmdb(dbpath.str().c_str());
460         } catch (std::system_error& e) {
461             if (e.code() != std::error_code(ENOENT, std::system_category())) {
462                 throw e;
463             }
464         }
465         auto* handle = testHarness->create_bucket(true, config.str());
466         if (handle) {
467             buckets.emplace_back(BucketHolder(handle, dbpath.str()));
468         } else {
469             return ii;
470         }
471     }
472     return n_buckets;
473 }
474 
475 void destroy_buckets(std::vector<BucketHolder> &buckets) {
476     for(auto bucket : buckets) {
477         testHarness->destroy_bucket(bucket.h, false);
478         rmdb(bucket.dbpath.c_str());
479     }
480 }
481 
482 void check_key_value(EngineIface* h,
483                      const char* key,
484                      const char* val,
485                      size_t vlen,
486                      Vbid vbucket) {
487     // Fetch item itself, to ensure we maintain a ref-count on the underlying
488     // Blob while comparing the key.
489     auto getResult = get(h, nullptr, key, vbucket);
490     checkeq(cb::engine_errc::success,
491             getResult.first,
492             "Failed to fetch document");
493     item_info info;
494     check(h->get_item_info(*getResult.second.get(), info),
495           "Failed to get_item_info");
496 
497     std::string_view payload;
498     cb::compression::Buffer inflated;
499     if (isCompressionEnabled(h) &&
500         (info.datatype & PROTOCOL_BINARY_DATATYPE_SNAPPY)) {
501         cb::compression::inflate(cb::compression::Algorithm::Snappy,
502                                  {static_cast<const char *>(info.value[0].iov_base),
503                                   info.value[0].iov_len},
504                                  inflated);
505         payload = inflated;
506     } else {
507         payload = {static_cast<const char *>(info.value[0].iov_base),
508                                              info.value[0].iov_len};
509     }
510 
511     checkeq(std::string_view(val, vlen), payload, "Data mismatch");
512 }
513 
514 bool isCompressionEnabled(EngineIface* h) {
515     return h->getCompressionMode() != BucketCompressionMode::Off;
516 }
517 
518 bool isActiveCompressionEnabled(EngineIface* h) {
519     return h->getCompressionMode() == BucketCompressionMode::Active;
520 }
521 
522 bool isPassiveCompressionEnabled(EngineIface* h) {
523     return h->getCompressionMode() == BucketCompressionMode::Passive;
524 }
525 
526 bool isWarmupEnabled(EngineIface* h) {
527     return isPersistentBucket(h) && get_bool_stat(h, "ep_warmup");
528 }
529 
530 bool isPersistentBucket(EngineIface* h) {
531     return get_str_stat(h, "ep_bucket_type") == "persistent";
532 }
533 
534 bool isEphemeralBucket(EngineIface* h) {
535     return get_str_stat(h, "ep_bucket_type") == "ephemeral";
536 }
537 
538 bool isFollyExecutorPool(EngineIface* h) {
539     return get_str_stat(h, "ep_executor_pool_backend", "config") == "folly";
540 }
541 
542 void checkPersistentBucketTempItems(EngineIface* h, int exp) {
543     if (isPersistentBucket(h)) {
544         checkeq(exp,
545                 get_int_stat(h, "curr_temp_items"),
546                 "CheckPersistentBucketTempItems(): Num temp items not as "
547                 "expected");
548     }
549 }
550 
551 void setAndWaitForQuotaChange(EngineIface* h, uint64_t newQuota) {
552     set_param(h,
553               EngineParamCategory::Flush,
554               "max_size",
555               std::to_string(newQuota).c_str());
556     wait_for_stat_to_be(h, "ep_max_size", newQuota);
557 }
558