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