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