// namespacetests.cpp : namespace.{h,cpp} unit tests. // /** * Copyright (C) 2008-2014 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects * for all of the code used other than as permitted herein. If you modify * file(s) with this exception, you may extend this exception to your * version of the file(s), but you are not obligated to do so. If you do not * wish to do so, delete this exception statement from your version. If you * delete this exception statement from all source files in the program, * then also delete it in the license file. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault #include "mongo/platform/basic.h" #include #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database_holder.h" #include "mongo/db/client.h" #include "mongo/db/db_raii.h" #include "mongo/db/index/expression_keys_private.h" #include "mongo/db/index_legacy.h" #include "mongo/db/index_names.h" #include "mongo/db/json.h" #include "mongo/db/query/internal_plans.h" #include "mongo/db/storage/mmap_v1/catalog/namespace.h" #include "mongo/db/storage/mmap_v1/catalog/namespace_details.h" #include "mongo/db/storage/mmap_v1/catalog/namespace_details_rsv1_metadata.h" #include "mongo/db/storage/mmap_v1/extent.h" #include "mongo/db/storage/mmap_v1/extent_manager.h" #include "mongo/db/storage/mmap_v1/mmap_v1_extent_manager.h" #include "mongo/db/storage/mmap_v1/record_store_v1_capped.h" #include "mongo/db/storage/mmap_v1/record_store_v1_simple.h" #include "mongo/db/storage/storage_engine.h" #include "mongo/dbtests/dbtests.h" #include "mongo/util/log.h" namespace NamespaceTests { using std::string; const int MinExtentSize = 4096; namespace MissingFieldTests { /** A missing field is represented as null in a btree index. */ class BtreeIndexMissingField { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; BSONObj spec(BSON("key" << BSON("a" << 1))); ASSERT_EQUALS(jstNULL, IndexLegacy::getMissingField(&opCtx, NULL, spec).firstElement().type()); } }; /** A missing field is represented as null in a 2d index. */ class TwoDIndexMissingField { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; BSONObj spec(BSON("key" << BSON("a" << "2d"))); ASSERT_EQUALS(jstNULL, IndexLegacy::getMissingField(&opCtx, NULL, spec).firstElement().type()); } }; /** A missing field is represented with the hash of null in a hashed index. */ class HashedIndexMissingField { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; BSONObj spec(BSON("key" << BSON("a" << "hashed"))); BSONObj nullObj = BSON("a" << BSONNULL); // Call getKeys on the nullObj. BSONObjSet nullFieldKeySet = SimpleBSONObjComparator::kInstance.makeBSONObjSet(); const CollatorInterface* collator = nullptr; ExpressionKeysPrivate::getHashKeys(nullObj, "a", 0, 0, false, collator, &nullFieldKeySet); BSONElement nullFieldFromKey = nullFieldKeySet.begin()->firstElement(); ASSERT_EQUALS(ExpressionKeysPrivate::makeSingleHashKey(nullObj.firstElement(), 0, 0), nullFieldFromKey.Long()); BSONObj missingField = IndexLegacy::getMissingField(&opCtx, NULL, spec); ASSERT_EQUALS(NumberLong, missingField.firstElement().type()); ASSERT_BSONELT_EQ(nullFieldFromKey, missingField.firstElement()); } }; /** * A missing field is represented with the hash of null in a hashed index. This hash value * depends on the hash seed. */ class HashedIndexMissingFieldAlternateSeed { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; BSONObj spec(BSON("key" << BSON("a" << "hashed") << "seed" << 0x5eed)); BSONObj nullObj = BSON("a" << BSONNULL); BSONObjSet nullFieldKeySet = SimpleBSONObjComparator::kInstance.makeBSONObjSet(); const CollatorInterface* collator = nullptr; ExpressionKeysPrivate::getHashKeys( nullObj, "a", 0x5eed, 0, false, collator, &nullFieldKeySet); BSONElement nullFieldFromKey = nullFieldKeySet.begin()->firstElement(); ASSERT_EQUALS(ExpressionKeysPrivate::makeSingleHashKey(nullObj.firstElement(), 0x5eed, 0), nullFieldFromKey.Long()); // Ensure that getMissingField recognizes that the seed is different (and returns // the right key). BSONObj missingField = IndexLegacy::getMissingField(&opCtx, NULL, spec); ASSERT_EQUALS(NumberLong, missingField.firstElement().type()); ASSERT_BSONELT_EQ(nullFieldFromKey, missingField.firstElement()); } }; } // namespace MissingFieldTests namespace NamespaceDetailsTests { #if 0 // SERVER-13640 class Base { const char *ns_; Lock::GlobalWrite lk; OldClientContext _context; public: Base( const char *ns = "unittests.NamespaceDetailsTests" ) : ns_( ns ) , _context( ns ) {} virtual ~Base() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; if ( !nsd() ) return; _context.db()->dropCollection( &opCtx, ns() ); } protected: void create() { Lock::GlobalWrite lk; const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; ASSERT( userCreateNS( &opCtx, db(), ns(), fromjson( spec() ), false ).isOK() ); } virtual string spec() const = 0; int nRecords() const { int count = 0; const Extent* ext; for ( RecordId extLoc = nsd()->firstExtent(); !extLoc.isNull(); extLoc = ext->xnext) { ext = extentManager()->getExtent(extLoc); int fileNo = ext->firstRecord.a(); if ( fileNo == -1 ) continue; for ( int recOfs = ext->firstRecord.getOfs(); recOfs != RecordId::NullOfs; recOfs = recordStore()->recordFor(RecordId(fileNo, recOfs))->nextOfs() ) { ++count; } } ASSERT_EQUALS( count, nsd()->numRecords() ); return count; } int nExtents() const { int count = 0; for ( RecordId extLoc = nsd()->firstExtent(); !extLoc.isNull(); extLoc = extentManager()->getExtent(extLoc)->xnext ) { ++count; } return count; } const char *ns() const { return ns_; } const NamespaceDetails *nsd() const { Collection* c = collection(); if ( !c ) return NULL; return c->detailsDeprecated(); } const RecordStore* recordStore() const { Collection* c = collection(); if ( !c ) return NULL; return c->getRecordStore(); } Database* db() const { return _context.db(); } const ExtentManager* extentManager() const { return db()->getExtentManager(); } Collection* collection() const { return db()->getCollection( &opCtx, ns() ); } static BSONObj bigObj() { BSONObjBuilder b; b.appendOID("_id", 0, true); string as( 187, 'a' ); b.append( "a", as ); return b.obj(); } }; class Create : public Base { public: void run() { create(); ASSERT( nsd() ); ASSERT_EQUALS( 0, nRecords() ); ASSERT( nsd()->firstExtent() == nsd()->capExtent() ); RecordId initial = RecordId(); initial.setInvalid(); ASSERT( initial == nsd()->capFirstNewRecord() ); } virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":1}"; } }; class SingleAlloc : public Base { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; create(); BSONObj b = bigObj(); ASSERT( collection()->insertDocument( &opCtx, b, true ).isOK() ); ASSERT_EQUALS( 1, nRecords() ); } virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":1}"; } }; class Realloc : public Base { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; create(); const int N = 20; const int Q = 16; // these constants depend on the size of the bson object, the extent // size allocated by the system too RecordId l[ N ]; for ( int i = 0; i < N; ++i ) { BSONObj b = bigObj(); StatusWith status = ASSERT( collection()->insertDocument( &opCtx, b, true ).isOK() ); l[ i ] = status.getValue(); ASSERT( !l[ i ].isNull() ); ASSERT( nRecords() <= Q ); //ASSERT_EQUALS( 1 + i % 2, nRecords() ); if ( i >= 16 ) ASSERT( l[ i ] == l[ i - Q] ); } } virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":1}"; } }; class TwoExtent : public Base { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; create(); ASSERT_EQUALS( 2, nExtents() ); RecordId l[ 8 ]; for ( int i = 0; i < 8; ++i ) { StatusWith status = ASSERT( collection()->insertDocument( &opCtx, bigObj(), true ).isOK() ); l[ i ] = status.getValue(); ASSERT( !l[ i ].isNull() ); //ASSERT_EQUALS( i < 2 ? i + 1 : 3 + i % 2, nRecords() ); //if ( i > 3 ) // ASSERT( l[ i ] == l[ i - 4 ] ); } ASSERT( nRecords() == 8 ); // Too big BSONObjBuilder bob; bob.appendOID( "_id", NULL, true ); bob.append( "a", string( MinExtentSize + 500, 'a' ) ); // min extent size is now 4096 BSONObj bigger = bob.done(); ASSERT( !collection()->insertDocument( &opCtx, bigger, false ).isOK() ); ASSERT_EQUALS( 0, nRecords() ); } private: virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":2}"; } }; BSONObj docForRecordSize( int size ) { BSONObjBuilder b; b.append( "_id", 5 ); b.append( "x", string( size - Record::HeaderSize - 22, 'x' ) ); BSONObj x = b.obj(); ASSERT_EQUALS( Record::HeaderSize + x.objsize(), size ); return x; } /** * alloc() does not quantize records in capped collections. * NB: this actually tests that the code in Database::createCollection doesn't set * PowerOf2Sizes for capped collections. */ class AllocCappedNotQuantized : public Base { public: void run() { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; create(); ASSERT( nsd()->isCapped() ); ASSERT( !nsd()->isUserFlagSet( NamespaceDetails::Flag_UsePowerOf2Sizes ) ); StatusWith result = collection()->insertDocument( &opCtx, docForRecordSize( 300 ), false ); ASSERT( result.isOK() ); Record* record = collection()->getRecordStore()->recordFor( result.getValue() ); // Check that no quantization is performed. ASSERT_EQUALS( 300, record->lengthWithHeaders() ); } virtual string spec() const { return "{capped:true,size:2048}"; } }; /* test NamespaceDetails::cappedTruncateAfter(const char *ns, RecordId loc) */ class TruncateCapped : public Base { virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":2}"; } void pass(int p) { const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; create(); ASSERT_EQUALS( 2, nExtents() ); BSONObj b = bigObj(); int N = MinExtentSize / b.objsize() * nExtents() + 5; int T = N - 4; RecordId truncAt; //RecordId l[ 8 ]; for ( int i = 0; i < N; ++i ) { BSONObj bb = bigObj(); StatusWith status = collection()->insertDocument( &opCtx, bb, true ); ASSERT( status.isOK() ); RecordId a = status.getValue(); if( T == i ) truncAt = a; ASSERT( !a.isNull() ); /*ASSERT_EQUALS( i < 2 ? i + 1 : 3 + i % 2, nRecords() ); if ( i > 3 ) ASSERT( l[ i ] == l[ i - 4 ] );*/ } ASSERT( nRecords() < N ); RecordId last, first; { unique_ptr runner(InternalPlanner::collectionScan(&opCtx, ns(), collection(), InternalPlanner::BACKWARD)); runner->getNext(NULL, &last); ASSERT( !last.isNull() ); } { unique_ptr runner(InternalPlanner::collectionScan(&opCtx, ns(), collection(), InternalPlanner::FORWARD)); runner->getNext(NULL, &first); ASSERT( !first.isNull() ); ASSERT( first != last ) ; } collection()->cappedTruncateAfter(&opCtx, truncAt, false); ASSERT_EQUALS( collection()->numRecords() , 28u ); { RecordId loc; unique_ptr runner(InternalPlanner::collectionScan(&opCtx, ns(), collection(), InternalPlanner::FORWARD)); runner->getNext(NULL, &loc); ASSERT( first == loc); } { unique_ptr runner(InternalPlanner::collectionScan(&opCtx, ns(), collection(), InternalPlanner::BACKWARD)); RecordId loc; runner->getNext(NULL, &loc); ASSERT( last != loc ); ASSERT( !last.isNull() ); } // Too big BSONObjBuilder bob; bob.appendOID("_id", 0, true); bob.append( "a", string( MinExtentSize + 300, 'a' ) ); BSONObj bigger = bob.done(); ASSERT( !collection()->insertDocument( &opCtx, bigger, true ).isOK() ); ASSERT_EQUALS( 0, nRecords() ); } public: void run() { // log() << "******** NOT RUNNING TruncateCapped test yet ************" << endl; pass(0); } }; #endif // SERVER-13640 #if 0 // XXXXXX - once RecordStore is clean, we can put this back class Migrate : public Base { public: void run() { create(); nsd()->deletedListEntry( 2 ) = nsd()->cappedListOfAllDeletedRecords().drec()-> nextDeleted().drec()->nextDeleted(); nsd()->cappedListOfAllDeletedRecords().drec()->nextDeleted().drec()-> nextDeleted().writing() = RecordId(); nsd()->cappedLastDelRecLastExtent().Null(); NamespaceDetails *d = nsd(); zero( &d->capExtent() ); zero( &d->capFirstNewRecord() ); // this has a side effect of called NamespaceDetails::cappedCheckMigrate db()->namespaceIndex().details( ns() ); ASSERT( nsd()->firstExtent() == nsd()->capExtent() ); ASSERT( nsd()->capExtent().getOfs() != 0 ); ASSERT( !nsd()->capFirstNewRecord().isValid() ); int nDeleted = 0; for ( RecordId i = nsd()->cappedListOfAllDeletedRecords(); !i.isNull(); i = i.drec()->nextDeleted(), ++nDeleted ); ASSERT_EQUALS( 10, nDeleted ); ASSERT( nsd()->cappedLastDelRecLastExtent().isNull() ); } private: static void zero( RecordId *d ) { memset( d, 0, sizeof( RecordId ) ); } virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":10}"; } }; #endif // This isn't a particularly useful test, and because it doesn't clean up // after itself, /tmp/unittest needs to be cleared after running. // class BigCollection : public Base { // public: // BigCollection() : Base( "NamespaceDetailsTests_BigCollection" ) {} // void run() { // create(); // ASSERT_EQUALS( 2, nExtents() ); // } // private: // virtual string spec() const { // // NOTE 256 added to size in _userCreateNS() // long long big = DataFile::maxSize() - DataFileHeader::HeaderSize; // stringstream ss; // ss << "{\"capped\":true,\"size\":" << big << "}"; // return ss.str(); // } // }; #if 0 // SERVER-13640 class SwapIndexEntriesTest : public Base { public: void run() { create(); NamespaceDetails *nsd = collection()->detailsWritable(); const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; // Set 2 & 54 as multikey nsd->setIndexIsMultikey(&opCtx, 2, true); nsd->setIndexIsMultikey(&opCtx, 54, true); ASSERT(nsd->isMultikey(2)); ASSERT(nsd->isMultikey(54)); // Flip 2 & 47 nsd->setIndexIsMultikey(&opCtx, 2, false); nsd->setIndexIsMultikey(&opCtx, 47, true); ASSERT(!nsd->isMultikey(2)); ASSERT(nsd->isMultikey(47)); // Reset entries that are already true nsd->setIndexIsMultikey(&opCtx, 54, true); nsd->setIndexIsMultikey(&opCtx, 47, true); ASSERT(nsd->isMultikey(54)); ASSERT(nsd->isMultikey(47)); // Two non-multi-key nsd->setIndexIsMultikey(&opCtx, 2, false); nsd->setIndexIsMultikey(&opCtx, 43, false); ASSERT(!nsd->isMultikey(2)); ASSERT(nsd->isMultikey(54)); ASSERT(nsd->isMultikey(47)); ASSERT(!nsd->isMultikey(43)); } virtual string spec() const { return "{\"capped\":true,\"size\":512,\"$nExtents\":1}"; } }; #endif // SERVER-13640 } // namespace NamespaceDetailsTests namespace DatabaseTests { class RollbackCreateCollection { public: void run() { const string dbName = "rollback_create_collection"; const string committedName = dbName + ".committed"; const string rolledBackName = dbName + ".rolled_back"; const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; Lock::DBLock lk(&opCtx, dbName, MODE_X); bool justCreated; Database* db = dbHolder().openDb(&opCtx, dbName, &justCreated); ASSERT(justCreated); Collection* committedColl; { WriteUnitOfWork wunit(&opCtx); ASSERT_FALSE(db->getCollection(&opCtx, committedName)); committedColl = db->createCollection(&opCtx, committedName); ASSERT_EQUALS(db->getCollection(&opCtx, committedName), committedColl); wunit.commit(); } ASSERT_EQUALS(db->getCollection(&opCtx, committedName), committedColl); { WriteUnitOfWork wunit(&opCtx); ASSERT_FALSE(db->getCollection(&opCtx, rolledBackName)); Collection* rolledBackColl = db->createCollection(&opCtx, rolledBackName); ASSERT_EQUALS(db->getCollection(&opCtx, rolledBackName), rolledBackColl); // not committing so creation should be rolled back } // The rolledBackCollection creation should have been rolled back ASSERT_FALSE(db->getCollection(&opCtx, rolledBackName)); // The committedCollection should not have been affected by the rollback. Holders // of the original Collection pointer should still be valid. ASSERT_EQUALS(db->getCollection(&opCtx, committedName), committedColl); } }; class RollbackDropCollection { public: void run() { const string dbName = "rollback_drop_collection"; const string droppedName = dbName + ".dropped"; const string rolledBackName = dbName + ".rolled_back"; const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext(); OperationContext& opCtx = *opCtxPtr; Lock::DBLock lk(&opCtx, dbName, MODE_X); bool justCreated; Database* db = dbHolder().openDb(&opCtx, dbName, &justCreated); ASSERT(justCreated); { WriteUnitOfWork wunit(&opCtx); ASSERT_FALSE(db->getCollection(&opCtx, droppedName)); Collection* droppedColl; droppedColl = db->createCollection(&opCtx, droppedName); ASSERT_EQUALS(db->getCollection(&opCtx, droppedName), droppedColl); db->dropCollection(&opCtx, droppedName).transitional_ignore(); wunit.commit(); } // Should have been really dropped ASSERT_FALSE(db->getCollection(&opCtx, droppedName)); { WriteUnitOfWork wunit(&opCtx); ASSERT_FALSE(db->getCollection(&opCtx, rolledBackName)); Collection* rolledBackColl = db->createCollection(&opCtx, rolledBackName); wunit.commit(); ASSERT_EQUALS(db->getCollection(&opCtx, rolledBackName), rolledBackColl); db->dropCollection(&opCtx, rolledBackName).transitional_ignore(); // not committing so dropping should be rolled back } // The rolledBackCollection dropping should have been rolled back. // Original Collection pointers are no longer valid. ASSERT(db->getCollection(&opCtx, rolledBackName)); // The droppedCollection should not have been restored by the rollback. ASSERT_FALSE(db->getCollection(&opCtx, droppedName)); } }; } // namespace DatabaseTests class All : public Suite { public: All() : Suite("namespace") {} void setupTests() { add(); add(); add(); add(); // add< NamespaceDetailsTests::Create >(); // add< NamespaceDetailsTests::SingleAlloc >(); // add< NamespaceDetailsTests::Realloc >(); // add< NamespaceDetailsTests::AllocCappedNotQuantized >(); // add< NamespaceDetailsTests::TwoExtent >(); // add< NamespaceDetailsTests::TruncateCapped >(); // add< NamespaceDetailsTests::Migrate >(); // add< NamespaceDetailsTests::SwapIndexEntriesTest >(); // add< NamespaceDetailsTests::BigCollection >(); #if 0 // until ROLLBACK_ENABLED add< DatabaseTests::RollbackCreateCollection >(); add< DatabaseTests::RollbackDropCollection >(); #endif } }; SuiteInstance myall; } // namespace NamespaceTests