diff options
author | Dwight <dmerriman@gmail.com> | 2008-09-09 14:30:22 -0400 |
---|---|---|
committer | Dwight <dmerriman@gmail.com> | 2008-09-09 14:30:22 -0400 |
commit | 90d088511674f59227b964a91630008861fb1d02 (patch) | |
tree | 2c326ca6ebe586f581d61dfc2519d88b9d0e82ba | |
parent | 98d4a02dcbb943961c0c30e3b556bd4150f62c4c (diff) | |
parent | 4af0da7918b1794e85751ae3a67f1759f39182fd (diff) | |
download | mongo-90d088511674f59227b964a91630008861fb1d02.tar.gz |
Merge branch 'master' of ssh://git.10gen.com/data/gitroot/p
Conflicts:
db/makefile
-rw-r--r-- | db/commands.cpp | 464 | ||||
-rw-r--r-- | db/db.cpp | 36 | ||||
-rw-r--r-- | db/db.vcproj | 8 | ||||
-rw-r--r-- | db/makefile | 6 | ||||
-rw-r--r-- | db/matcher.cpp | 484 | ||||
-rw-r--r-- | db/query.cpp | 430 | ||||
-rw-r--r-- | db/repl.cpp | 1 | ||||
-rw-r--r-- | util/mmap.cpp | 2 | ||||
-rw-r--r-- | util/unittest.h | 40 | ||||
-rw-r--r-- | util/util.cpp | 7 |
10 files changed, 1024 insertions, 454 deletions
diff --git a/db/commands.cpp b/db/commands.cpp new file mode 100644 index 00000000000..78c812e8f7d --- /dev/null +++ b/db/commands.cpp @@ -0,0 +1,464 @@ +/* commands.cpp + db "commands" (sent via db.$cmd.findOne(...)) + */ + +/** +* Copyright (C) 2008 10gen 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 <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" +#include "query.h" +#include "pdfile.h" +#include "jsobj.h" +#include "../util/builder.h" +#include <time.h> +#include "introspect.h" +#include "btree.h" +#include "../util/lruishmap.h" +#include "javajs.h" +#include "json.h" +#include "repl.h" + +extern int queryTraceLevel; +extern int otherTraceLevel; +extern int opLogging; +bool userCreateNS(const char *ns, JSObj& j, string& err); +void flushOpLog(); +int runCount(const char *ns, JSObj& cmd, string& err); + +const int edebug=0; + +bool dbEval(JSObj& cmd, JSObjBuilder& result) { + Element e = cmd.firstElement(); + assert( e.type() == Code ); + const char *code = e.valuestr(); + if ( ! JavaJS ) { + result.append("errmsg", "db side execution is disabled"); + return false; + } + + jlong f = JavaJS->functionCreate(code); + if( f == 0 ) { + result.append("errmsg", "compile failed"); + return false; + } + + Scope s; + s.setString("$client", client->name.c_str()); + Element args = cmd.findElement("args"); + if( args.type() == Array ) { + JSObj eo = args.embeddedObject(); + if( edebug ) { + cout << "args:" << eo.toString() << endl; + cout << "code:\n" << code << endl; + } + s.setObject("args", eo); + } + + int res = s.invoke(f); + if( res ) { + result.append("errno", (double) res); + result.append("errmsg", "invoke failed"); + return false; + } + + int type = s.type("return"); + if( type == Object || type == Array ) + result.append("retval", s.getObject("return")); + else if( type == Number ) + result.append("retval", s.getNumber("return")); + else if( type == String ) + result.append("retval", s.getString("return").c_str()); + else if( type == Bool ) { + result.appendBool("retval", s.getBoolean("return")); + } + + return true; +} + +void clean(const char *ns, NamespaceDetails *d) { + for( int i = 0; i < Buckets; i++ ) + d->deletedList[i].Null(); +} + +string validateNS(const char *ns, NamespaceDetails *d) { + bool valid = true; + stringstream ss; + ss << "\nvalidate\n"; + ss << " details: " << hex << d << " ofs:" << nsindex(ns)->detailsOffset(d) << dec << endl; + if( d->capped ) + ss << " capped:" << d->capped << " max:" << d->max << '\n'; + + ss << " firstExtent:" << d->firstExtent.toString() << " ns:" << d->firstExtent.ext()->ns.buf << '\n'; + ss << " lastExtent:" << d->lastExtent.toString() << " ns:" << d->lastExtent.ext()->ns.buf << '\n'; + try { + d->firstExtent.ext()->assertOk(); + d->lastExtent.ext()->assertOk(); + } catch(...) { valid=false; ss << " extent asserted "; } + + ss << " datasize?:" << d->datasize << " nrecords?:" << d->nrecords << " lastExtentSize:" << d->lastExtentSize << '\n'; + ss << " padding:" << d->paddingFactor << '\n'; + try { + + try { + ss << " first extent:\n"; + d->firstExtent.ext()->dump(ss); + valid = valid && d->firstExtent.ext()->validates(); + } + catch(...) { + ss << "\n exception firstextent\n" << endl; + } + + auto_ptr<Cursor> c = theDataFileMgr.findAll(ns); + int n = 0; + long long len = 0; + long long nlen = 0; + set<DiskLoc> recs; + int outOfOrder = 0; + DiskLoc cl_last; + while( c->ok() ) { + n++; + + DiskLoc cl = c->currLoc(); + if( n < 1000000 ) + recs.insert(cl); + if( d->capped ) { + if( cl < cl_last ) + outOfOrder++; + cl_last = cl; + } + + Record *r = c->_current(); + len += r->lengthWithHeaders; + nlen += r->netLength(); + c->advance(); + } + if( d->capped ) { + ss << " capped outOfOrder:" << outOfOrder; + if( outOfOrder > 1 ) { + valid = false; + ss << " ???"; + } + else ss << " (OK)"; + ss << '\n'; + } + ss << " " << n << " objects found, nobj:" << d->nrecords << "\n"; + ss << " " << len << " bytes data w/headers\n"; + ss << " " << nlen << " bytes data wout/headers\n"; + + ss << " deletedList: "; + for( int i = 0; i < Buckets; i++ ) { + ss << (d->deletedList[i].isNull() ? '0' : '1'); + } + ss << endl; + int ndel = 0; + long long delSize = 0; + int incorrect = 0; + for( int i = 0; i < Buckets; i++ ) { + DiskLoc loc = d->deletedList[i]; + try { + int k = 0; + while( !loc.isNull() ) { + if( recs.count(loc) ) + incorrect++; + ndel++; + + if( loc.questionable() ) { + if( loc.a() <= 0 || strstr(ns, "hudsonSmall") == 0 ) { + ss << " ?bad deleted loc: " << loc.toString() << " bucket:" << i << " k:" << k << endl; + valid = false; + break; + } + } + + DeletedRecord *d = loc.drec(); + delSize += d->lengthWithHeaders; + loc = d->nextDeleted; + k++; + } + } catch(...) { ss <<" ?exception in deleted chain for bucket " << i << endl; valid = false; } + } + ss << " deleted: n: " << ndel << " size: " << delSize << '\n'; + if( incorrect ) { + ss << " ?corrupt: " << incorrect << " records from datafile are in deleted list\n"; + valid = false; + } + + int idxn = 0; + try { + ss << " nIndexes:" << d->nIndexes << endl; + for( ; idxn < d->nIndexes; idxn++ ) { + ss << " " << d->indexes[idxn].indexNamespace() << " keys:" << + d->indexes[idxn].head.btree()->fullValidate(d->indexes[idxn].head) << endl; + } + } + catch(...) { + ss << "\n exception during index validate idxn:" << idxn << endl; valid=false; + } + + } + catch(AssertionException) { + ss << "\n exception during validate\n" << endl; + valid = false; + } + + if( !valid ) + ss << " ns corrupt, requires dbchk\n"; + + return ss.str(); +} + +// e.g. +// system.cmd$.find( { queryTraceLevel: 2 } ); +// +// returns true if ran a cmd +// +bool _runCommands(const char *ns, JSObj& jsobj, stringstream& ss, BufBuilder &b, JSObjBuilder& anObjBuilder) { + + const char *p = strchr(ns, '.'); + if( !p ) return false; + if( strcmp(p, ".$cmd") != 0 ) return false; + + bool ok = false; + bool valid = false; + + //cout << jsobj.toString() << endl; + + Element e; + e = jsobj.firstElement(); + + if( e.eoo() ) goto done; + if( e.type() == Code ) { + valid = true; + ok = dbEval(jsobj, anObjBuilder); + } + else if( e.type() == Number ) { + if( strcmp(e.fieldName(), "getoptime") == 0 ) { + valid = true; + ok = true; + anObjBuilder.appendDate("optime", OpTime::now().asDate()); + } + else if( strcmp(e.fieldName(), "dropDatabase") == 0 ) { + if( 1 ) { + cout << "dropDatabase " << ns << endl; + valid = true; + int p = (int) e.number(); + if( p != 1 ) { + ok = false; + } else { + dropDatabase(ns); + logOp("c", ns, jsobj); + ok = true; + } + } + else { + cout << "TEMP CODE: dropdatabase commented out " << endl; + } + } + else if( strcmp(e.fieldName(), "profile") == 0 ) { + anObjBuilder.append("was", (double) client->profile); + int p = (int) e.number(); + valid = true; + if( p == -1 ) + ok = true; + else if( p >= 0 && p <= 2 ) { + ok = true; + client->profile = p; + } + else { + ok = false; + } + } + else { + // admin only commands. + if( strncmp(ns, "admin", p-ns) != 0 ) + return false; + if( strcmp(e.fieldName(),"opLogging") == 0 ) { + valid = ok = true; + opLogging = (int) e.number(); + flushOpLog(); + log() << "CMD: opLogging set to " << opLogging << endl; + } else if( strcmp(e.fieldName(),"queryTraceLevel") == 0 ) { + valid = ok = true; + queryTraceLevel = (int) e.number(); + } else if( strcmp(e.fieldName(),"traceAll") == 0 ) { + valid = ok = true; + queryTraceLevel = (int) e.number(); + otherTraceLevel = (int) e.number(); + } + } + } + else if( e.type() == String ) { + /* { count: "collectionname"[, query: <query>] } */ + string us(ns, p-ns); + + if( strcmp( e.fieldName(), "count" ) == 0 ) { + valid = true; + string ns = us + '.' + e.valuestr(); + string err; + int n = runCount(ns.c_str(), jsobj, err); + int nn = n; + ok = true; + if( n < 0 ) { + ok = false; + nn = 0; + if( !err.empty() ) + anObjBuilder.append("errmsg", err.c_str()); + } + anObjBuilder.append("n", (double) nn); + } + else if( strcmp( e.fieldName(), "clone") == 0 ) { + valid = true; + string err; + ok = cloneFrom(e.valuestr(), err); + if( !err.empty() ) + anObjBuilder.append("errmsg", err.c_str()); + } + else if( strcmp( e.fieldName(), "create") == 0 ) { + valid = true; + string ns = us + '.' + e.valuestr(); + string err; + ok = userCreateNS(ns.c_str(), jsobj, err); + if( ok ) + logOp("c", ns.c_str(), jsobj); + if( !ok && !err.empty() ) + anObjBuilder.append("errmsg", err.c_str()); + } + else if( strcmp( e.fieldName(), "clean") == 0 ) { + valid = true; + string dropNs = us + '.' + e.valuestr(); + NamespaceDetails *d = nsdetails(dropNs.c_str()); + log() << "CMD: clean " << dropNs << endl; + if( d ) { + ok = true; + anObjBuilder.append("ns", dropNs.c_str()); + clean(dropNs.c_str(), d); + } + else { + anObjBuilder.append("errmsg", "ns not found"); + } + } + else if( strcmp( e.fieldName(), "drop") == 0 ) { + valid = true; + string nsToDrop = us + '.' + e.valuestr(); + NamespaceDetails *d = nsdetails(nsToDrop.c_str()); + log() << "CMD: drop " << nsToDrop << endl; + if( d == 0 ) { + anObjBuilder.append("errmsg", "ns not found"); + } + else if( d->nIndexes != 0 ) { + // client is supposed to drop the indexes first + anObjBuilder.append("errmsg", "ns has indexes (not permitted on drop)"); + } + else { + ok = true; + anObjBuilder.append("ns", nsToDrop.c_str()); + ClientCursor::invalidate(nsToDrop.c_str()); + dropNS(nsToDrop); + logOp("c", ns, jsobj); + /* + { + JSObjBuilder b; + b.append("name", dropNs.c_str()); + JSObj cond = b.done(); // { name: "colltodropname" } + deleteObjects("system.namespaces", cond, false, true); + } + client->namespaceIndex.kill(dropNs.c_str()); + */ + } + } + else if( strcmp( e.fieldName(), "validate") == 0 ) { + valid = true; + string toValidateNs = us + '.' + e.valuestr(); + NamespaceDetails *d = nsdetails(toValidateNs.c_str()); + log() << "CMD: validate " << toValidateNs << endl; + if( d ) { + ok = true; + anObjBuilder.append("ns", toValidateNs.c_str()); + string s = validateNS(toValidateNs.c_str(), d); + anObjBuilder.append("result", s.c_str()); + } + else { + anObjBuilder.append("errmsg", "ns not found"); + } + } + else if( strcmp(e.fieldName(),"deleteIndexes") == 0 ) { + valid = true; + /* note: temp implementation. space not reclaimed! */ + string toDeleteNs = us + '.' + e.valuestr(); + NamespaceDetails *d = nsdetails(toDeleteNs.c_str()); + log() << "CMD: deleteIndexes " << toDeleteNs << endl; + if( d ) { + Element f = jsobj.findElement("index"); + if( !f.eoo() ) { + + d->aboutToDeleteAnIndex(); + + ClientCursor::invalidate(toDeleteNs.c_str()); + logOp("c", ns, jsobj); + + // delete a specific index or all? + if( f.type() == String ) { + const char *idxName = f.valuestr(); + if( *idxName == '*' && idxName[1] == 0 ) { + ok = true; + log() << " d->nIndexes was " << d->nIndexes << '\n'; + anObjBuilder.append("nIndexesWas", (double)d->nIndexes); + anObjBuilder.append("msg", "all indexes deleted for collection"); + for( int i = 0; i < d->nIndexes; i++ ) + d->indexes[i].kill(); + d->nIndexes = 0; + log() << " alpha implementation, space not reclaimed" << endl; + } + else { + // delete just one index + int x = d->findIndexByName(idxName); + if( x >= 0 ) { + cout << " d->nIndexes was " << d->nIndexes << endl; + anObjBuilder.append("nIndexesWas", (double)d->nIndexes); + + /* note it is important we remove the IndexDetails with this + call, otherwise, on recreate, the old one would be reused, and its + IndexDetails::info ptr would be bad info. + */ + d->indexes[x].kill(); + + d->nIndexes--; + for( int i = x; i < d->nIndexes; i++ ) + d->indexes[i] = d->indexes[i+1]; + ok=true; + cout << " alpha implementation, space not reclaimed\n"; + } else { + cout << "deleteIndexes: " << idxName << " not found" << endl; + } + } + } + } + } + else { + anObjBuilder.append("errmsg", "ns not found"); + } + } + } + +done: + if( !valid ) + anObjBuilder.append("errmsg", "no such cmd"); + anObjBuilder.append("ok", ok?1.0:0.0); + JSObj x = anObjBuilder.done(); + b.append((void*) x.objdata(), x.objsize()); + return true; +} + diff --git a/db/db.cpp b/db/db.cpp index 03f54fa2678..7c3a8e44522 100644 --- a/db/db.cpp +++ b/db/db.cpp @@ -29,6 +29,7 @@ #include "query.h" #include "introspect.h" #include "repl.h" +#include "../util/unittest.h" bool slave = false; bool master = false; // true means keep an op log @@ -740,8 +741,7 @@ void segvhandler(int x) { void mysighandler(int x) { signal(x, SIG_IGN); - cout << "got kill or ctrl c signal " << x << ", will terminate after current cmd ends" << endl; - problem() << "got kill or ctrl c signal " << x << ", will terminate after current cmd ends" << endl; + log() << "got kill or ctrl c signal " << x << ", will terminate after current cmd ends" << endl; { dblock lk; problem() << " now exiting" << endl; @@ -760,25 +760,25 @@ void setupSignals() {} void initAndListen(int listenPort, const char *dbPath, const char *appserverLoc = null) { if( opLogging ) - log() << "opLogging = " << opLogging << endl; - _oplog.init(); + log() << "opLogging = " << opLogging << endl; + _oplog.init(); #if !defined(_WIN32) - assert( signal(SIGSEGV, segvhandler) != SIG_ERR ); + assert( signal(SIGSEGV, segvhandler) != SIG_ERR ); #endif - /* - * ensure that the dbpath ends with a path delim if not supplied - * @TODO - the following is embarassing - not sure of there's a clean way to - * find the platform delim - */ - - char endchar = '/'; - char *endstr = "/"; + /* + * ensure that the dbpath ends with a path delim if not supplied + * @TODO - the following is embarassing - not sure of there's a clean way to + * find the platform delim + */ + + char endchar = '/'; + const char *endstr = "/"; #if defined(_WIN32) - endchar = '\\'; - endstr = "\\"; + endchar = '\\'; + endstr = "\\"; #endif if (dbPath && dbPath[strlen(dbPath)-1] != endchar) { @@ -824,6 +824,8 @@ int main(int argc, char* argv[], char *envp[] ) #endif srand(curTimeMillis()); + UnitTest::runTests(); + if( argc >= 2 ) { if( strcmp(argv[1], "quicktest") == 0 ) { quicktest(); @@ -948,13 +950,13 @@ int main(int argc, char* argv[], char *envp[] ) #undef exit void dbexit(int rc, const char *why) { - cout << " dbexit: " << why << "; flushing op log and files" << endl; + log() << " dbexit: " << why << "; flushing op log and files" << endl; flushOpLog(); /* must do this before unmapping mem or you may get a seg fault */ closeAllSockets(); MemoryMappedFile::closeAllFiles(); - cout << " dbexit: really exiting now" << endl; + log() << " dbexit: really exiting now" << endl; exit(rc); } diff --git a/db/db.vcproj b/db/db.vcproj index ece0d5c6bf4..4fdae169214 100644 --- a/db/db.vcproj +++ b/db/db.vcproj @@ -191,6 +191,10 @@ >
</File>
<File
+ RelativePath=".\commands.cpp"
+ >
+ </File>
+ <File
RelativePath=".\db.cpp"
>
</File>
@@ -388,6 +392,10 @@ RelativePath=".\storage.h"
>
</File>
+ <File
+ RelativePath="..\util\unittest.h"
+ >
+ </File>
</Filter>
<Filter
Name="Resource Files"
diff --git a/db/makefile b/db/makefile index abbf77161be..89c6a590197 100644 --- a/db/makefile +++ b/db/makefile @@ -10,11 +10,9 @@ DBGRID_LIBS = $(LIB_BOOST) -lstdc++ JVM_LIBS = -L/opt/java/lib/ -OBJS=../stdafx.o ../util/sock.o ../grid/message.o ../util/mmap.o pdfile.o query.o jsobj.o introspect.o btree.o clientcursor.o ../util/util.o javajs.o tests.o json.o repl.o dbclient.o btreecursor.o cloner.o namespace.o +OBJS=../stdafx.o ../util/sock.o ../grid/message.o ../util/mmap.o pdfile.o query.o jsobj.o introspect.o btree.o clientcursor.o ../util/util.o javajs.o tests.o json.o repl.o dbclient.o btreecursor.o cloner.o namespace.o commands.o matcher.o -DBGRID_OBJS=../stdafx.o ../util/sock.o ../grid/message.o ../util/util.o dbclient.o ../dbgrid/dbgrid.o - -# ../grid/protorecv.o ../grid/protosend.o +DBGRID_OBJS=../stdafx.o ../util/sock.o ../grid/message.o ../util/util.o jsobj.o dbclient.o ../dbgrid/dbgrid.o GPP = g++ diff --git a/db/matcher.cpp b/db/matcher.cpp new file mode 100644 index 00000000000..24c5fdf8ab8 --- /dev/null +++ b/db/matcher.cpp @@ -0,0 +1,484 @@ +// matcher.cpp
+
+/* JSMatcher is our boolean expression evaluator for "where" clauses */
+
+/** +* Copyright (C) 2008 10gen 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 <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" +#include "jsobj.h" +#include "../util/goodies.h" +#include "javajs.h" +#include "../util/unittest.h" + +#if defined(_WIN32) + +#include <hash_map> +using namespace stdext; + +typedef const char * MyStr; +struct less_str { + bool operator()(const MyStr & x, const MyStr & y) const { + if ( strcmp(x, y) > 0) + return true; + + return false; + } +}; + +typedef hash_map<const char*, int, hash_compare<const char *, less_str> > strhashmap; + +#else + +#include <ext/hash_map> +using namespace __gnu_cxx; + +typedef const char * MyStr; +struct eq_str { + bool operator()(const MyStr & x, const MyStr & y) const { + if ( strcmp(x, y) == 0) + return true; + + return false; + } +}; + +typedef hash_map<const char*, int, hash<const char *>, eq_str > strhashmap; + +#endif + +#include "minilex.h" + +MiniLex minilex; + +class Where { +public: + Where() { codeCopy = 0; } + ~Where() { + JavaJS->scopeFree(scope); + delete codeCopy; + scope = 0; func = 0; codeCopy = 0; + } + jlong scope, func; + strhashmap fields; +// map<string,int> fields; + bool fullObject; + int nFields; + char *codeCopy; + + void setFunc(const char *code) { + codeCopy = new char[strlen(code)+1]; + strcpy(codeCopy,code); + func = JavaJS->functionCreate( code ); + minilex.grabVariables(codeCopy, fields); + // if user references db, eg db.foo.save(obj), + // we make sure we have the whole thing. + fullObject = fields.count("fullObject") + + fields.count("db") > 0; + nFields = fields.size(); + } + + void buildSubset(JSObj& src, JSObjBuilder& dst) { + JSElemIter it(src); + int n = 0; + if( !it.more() ) return; + while( 1 ) { + Element e = it.next(); + if( e.eoo() ) + break; + if( //n == 0 && + fields.find(e.fieldName()) != fields.end() + //fields.count(e.fieldName()) + ) { + dst.append(e); + if( ++n >= nFields ) + break; + } + } + } +}; + +JSMatcher::~JSMatcher() { + for( int i = 0; i < nBuilders; i++ ) + delete builders[i]; + delete in; + delete where; +} + +#include "pdfile.h" + +JSMatcher::JSMatcher(JSObj &_jsobj) : + in(0), where(0), jsobj(_jsobj), nRegex(0) +{ + nBuilders = 0; + + JSElemIter i(jsobj); + n = 0; + while( i.more() ) { + Element e = i.next(); + if( e.eoo() ) + break; + + if( e.type() == Code && strcmp(e.fieldName(), "$where")==0 ) { + // $where: function()... + assert( where == 0 ); + where = new Where(); + const char *code = e.valuestr(); + massert( "$where query, but jni is disabled", JavaJS ); + where->scope = JavaJS->scopeCreate(); + JavaJS->scopeSetString(where->scope, "$client", client->name.c_str()); + where->setFunc(code); + continue; + } + + if( e.type() == RegEx ) { + if( nRegex >= 4 ) { + cout << "ERROR: too many regexes in query" << endl; + } + else { + pcrecpp::RE_Options options; + options.set_utf8(true); + const char *flags = e.regexFlags(); + while( flags && *flags ) { + if( *flags == 'i' ) + options.set_caseless(true); + else if( *flags == 'm' ) + options.set_multiline(true); + else if( *flags == 'x' ) + options.set_extended(true); + flags++; + } + regexs[nRegex].re = new pcrecpp::RE(e.regex(), options); + regexs[nRegex].fieldName = e.fieldName(); + nRegex++; + } + continue; + } + + // greater than / less than... + // e.g., e == { a : { $gt : 3 } } + // or + // { a : { $in : [1,2,3] } } + if( e.type() == Object ) { + // e.g., fe == { $gt : 3 } + JSElemIter j(e.embeddedObject()); + bool ok = false; + while( j.more() ) { + Element fe = j.next(); + if( fe.eoo() ) + break; + // Element fe = e.embeddedObject().firstElement(); + const char *fn = fe.fieldName(); + if( fn[0] == '$' && fn[1] ) { + if( fn[2] == 't' ) { + int op = Equality; + if( fn[1] == 'g' ) { + if( fn[3] == 0 ) op = GT; + else if( fn[3] == 'e' && fn[4] == 0 ) op = GTE; + } + else if( fn[1] == 'l' ) { + if( fn[3] == 0 ) op = LT; + else if( fn[3] == 'e' && fn[4] == 0 ) op = LTE; + } + if( op && nBuilders < 8) { + JSObjBuilder *b = new JSObjBuilder(); + builders[nBuilders++] = b; + b->appendAs(fe, e.fieldName()); + toMatch.push_back( b->done().firstElement() ); + compareOp.push_back(op); + n++; + ok = true; + } + } + else if( fn[1] == 'i' && fn[2] == 'n' && fn[3] == 0 && fe.type() == Array ) { + // $in + assert( in == 0 ); // only one per query supported so far. finish... + in = new set<Element,element_lt>(); + JSElemIter i(fe.embeddedObject()); + if( i.more() ) { + while( 1 ) { + Element ie = i.next(); + if( ie.eoo() ) + break; + in->insert(ie); + } + } + toMatch.push_back(e); // not actually used at the moment + compareOp.push_back(opIN); + n++; + ok = true; + } + } + else { + ok = false; + break; + } + } + if( ok ) + continue; + } + + { + // normal, simple case e.g. { a : "foo" } + toMatch.push_back(e); + compareOp.push_back(Equality); + n++; + } + } +} + +inline int JSMatcher::valuesMatch(Element& l, Element& r, int op) { + if( op == 0 ) + return l.valuesEqual(r); + + if( op == opIN ) { + // { $in : [1,2,3] } + int c = in->count(l); + return c; + } + + /* check LT, GTE, ... */ + if( l.type() != r.type() ) + return false; + int c = compareElementValues(l, r); + int z = 1 << (c+1); + return (op & z); +} + +/* Check if a particular field matches. + + fieldName - field to match "a.b" if we are reaching into an embedded object. + toMatch - element we want to match. + obj - database object to check against + compareOp - Equality, LT, GT, etc. + deep - out param. set to true/false if we scanned an array + isArr - + + Special forms: + + { "a.b" : 3 } means obj.a.b == 3 + { a : { $lt : 3 } } means obj.a < 3 + { a : { $in : [1,2] } } means [1,2].contains(obj.a) + + return value + -1 mismatch + 0 missing element + 1 match +*/ +int JSMatcher::matchesDotted(const char *fieldName, Element& toMatch, JSObj& obj, int compareOp, bool *deep, bool isArr) { + { + const char *p = strchr(fieldName, '.'); + if( p ) { + string left(fieldName, p-fieldName); + + Element e = obj.getField(left.c_str()); + if( e.eoo() ) + return 0; + if( e.type() != Object && e.type() != Array ) + return -1; + + JSObj eo = e.embeddedObject(); + return matchesDotted(p+1, toMatch, eo, compareOp, deep, e.type() == Array); + } + } + + Element e = obj.getField(fieldName); + + if( valuesMatch(e, toMatch, compareOp) ) { + return 1; + } + else if( e.type() == Array ) { + JSElemIter ai(e.embeddedObject()); + while( ai.more() ) { + Element z = ai.next(); + if( valuesMatch( z, toMatch, compareOp) ) { + if( deep ) + *deep = true; + return 1; + } + } + } + else if( isArr ) { + JSElemIter ai(obj); + while( ai.more() ) { + Element z = ai.next(); + if( z.type() == Object ) { + JSObj eo = z.embeddedObject(); + int cmp = matchesDotted(fieldName, toMatch, eo, compareOp, deep); + if( cmp > 0 ) { + if( deep ) *deep = true; + return 1; + } + } + } + } + else if( e.eoo() ) { + return 0; + } + return -1; +} + +extern int dump; + +inline bool _regexMatches(RegexMatcher& rm, Element& e) { + char buf[64]; + const char *p = buf; + if( e.type() == String || e.type() == Symbol ) + p = e.valuestr(); + else if( e.type() == Number ) { + sprintf(buf, "%f", e.number()); + } + else if( e.type() == Date ) { + unsigned long long d = e.date(); + time_t t = (d/1000); + time_t_to_String(t, buf); + } + else + return false; + return rm.re->PartialMatch(p); +} +/* todo: internal dotted notation scans -- not done yet here. */ +inline bool regexMatches(RegexMatcher& rm, Element& e, bool *deep) { + if( e.type() != Array ) + return _regexMatches(rm, e); + + JSElemIter ai(e.embeddedObject()); + while( ai.more() ) { + Element z = ai.next(); + if( _regexMatches(rm, z) ) { + if( deep ) + *deep = true; + return true; + } + } + return false; +} + +/* See if an object matches the query. + deep - return true when meanswe looked into arrays for a match +*/ +bool JSMatcher::matches(JSObj& jsobj, bool *deep) { + if( deep ) + *deep = false; + + /* assuming there is usually only one thing to match. if more this + could be slow sometimes. */ + + // check normal non-regex cases: + for( int i = 0; i < n; i++ ) { + Element& m = toMatch[i]; + int cmp = matchesDotted(toMatch[i].fieldName(), toMatch[i], jsobj, compareOp[i], deep); + + /* missing is ok iff we were looking for null */ + if( cmp < 0 ) + return false; + if( cmp == 0 && (m.type() != jstNULL && m.type() != Undefined ) ) + return false; + } + + for( int r = 0; r < nRegex; r++ ) { + RegexMatcher& rm = regexs[r]; + Element e = jsobj.getFieldDotted(rm.fieldName); + if( e.eoo() ) + return false; + if( !regexMatches(rm, e, deep) ) + return false; + } + + if( where ) { + if( where->func == 0 ) + return false; // didn't compile + if( jsobj.objsize() < 200 || where->fullObject ) { + JavaJS->scopeSetObject(where->scope, "obj", &jsobj); + } else { + JSObjBuilder b; + where->buildSubset(jsobj, b); + JSObj temp = b.done(); + JavaJS->scopeSetObject(where->scope, "obj", &temp); + } + if( JavaJS->invoke(where->scope, where->func) ) + return false; + return JavaJS->scopeGetBoolean(where->scope, "return") != 0; + } + + return true; +} + +struct JSObj1 js1; + +#pragma pack(push,1) +struct JSObj2 { + JSObj2() { + totsize=sizeof(JSObj2); + s = String; strcpy_s(sname, 7, "abcdef"); slen = 10; + strcpy_s(sval, 10, "123456789"); eoo = EOO; + } + unsigned totsize; + char s; + char sname[7]; + unsigned slen; + char sval[10]; + char eoo; +} js2; + +struct JSUnitTest : public UnitTest { + void run() { + + JSObj j1((const char *) &js1); + JSObj j2((const char *) &js2); + cout << "j1:" << j1.toString() << endl; + cout << "j2:" << j2.toString() << endl; + JSMatcher m(j2); + assert( m.matches(j1) ); + js2.sval[0] = 'z'; + assert( !m.matches(j1) ); + JSMatcher n(j1); + assert( n.matches(j1) ); + cout << "j2:" << j2.toString() << endl; + assert( !n.matches(j2) ); + +cout << "temp1" << endl; + JSObj j0 = emptyObj; +// JSObj j0((const char *) &js0); + JSMatcher p(j0); + assert( p.matches(j1) ); + assert( p.matches(j2) ); + } +} jsunittest; + +#pragma pack(pop) + +struct RXTest : public UnitTest { + void run() { + /* + static const boost::regex e("(\\d{4}[- ]){3}\\d{4}"); + static const boost::regex b("....."); + cout << "regex result: " << regex_match("hello", e) << endl; + cout << "regex result: " << regex_match("abcoo", b) << endl; + */ + pcrecpp::RE re1(")({a}h.*o"); + pcrecpp::RE re("h.llo"); + assert( re.FullMatch("hello") ); + assert( !re1.FullMatch("hello") ); + + + pcrecpp::RE_Options options; + options.set_utf8(true); + pcrecpp::RE part("dwi", options); + assert( part.PartialMatch("dwight") ); + } +} rxtest; + diff --git a/db/query.cpp b/db/query.cpp index 3e29977f461..7f5a575dc3c 100644 --- a/db/query.cpp +++ b/db/query.cpp @@ -43,7 +43,6 @@ extern bool useCursors; int getGtLtOp(Element& e); void appendElementHandlingGtLt(JSObjBuilder& b, Element& e); -int runCount(const char *ns, JSObj& cmd, string& err); /* todo: _ cache query plans _ use index on partial match with the query @@ -413,434 +412,7 @@ int otherTraceLevel = 0; int initialExtentSize(int len); -void clean(const char *ns, NamespaceDetails *d) { - for( int i = 0; i < Buckets; i++ ) - d->deletedList[i].Null(); -} - -string validateNS(const char *ns, NamespaceDetails *d) { - bool valid = true; - stringstream ss; - ss << "\nvalidate\n"; - ss << " details: " << hex << d << " ofs:" << nsindex(ns)->detailsOffset(d) << dec << endl; - if( d->capped ) - ss << " capped:" << d->capped << " max:" << d->max << '\n'; - - ss << " firstExtent:" << d->firstExtent.toString() << " ns:" << d->firstExtent.ext()->ns.buf << '\n'; - ss << " lastExtent:" << d->lastExtent.toString() << " ns:" << d->lastExtent.ext()->ns.buf << '\n'; - try { - d->firstExtent.ext()->assertOk(); - d->lastExtent.ext()->assertOk(); - } catch(...) { valid=false; ss << " extent asserted "; } - - ss << " datasize?:" << d->datasize << " nrecords?:" << d->nrecords << " lastExtentSize:" << d->lastExtentSize << '\n'; - ss << " padding:" << d->paddingFactor << '\n'; - try { - - try { - ss << " first extent:\n"; - d->firstExtent.ext()->dump(ss); - valid = valid && d->firstExtent.ext()->validates(); - } - catch(...) { - ss << "\n exception firstextent\n" << endl; - } - - auto_ptr<Cursor> c = theDataFileMgr.findAll(ns); - int n = 0; - long long len = 0; - long long nlen = 0; - set<DiskLoc> recs; - int outOfOrder = 0; - DiskLoc cl_last; - while( c->ok() ) { - n++; - - DiskLoc cl = c->currLoc(); - if( n < 1000000 ) - recs.insert(cl); - if( d->capped ) { - if( cl < cl_last ) - outOfOrder++; - cl_last = cl; - } - - Record *r = c->_current(); - len += r->lengthWithHeaders; - nlen += r->netLength(); - c->advance(); - } - if( d->capped ) { - ss << " capped outOfOrder:" << outOfOrder; - if( outOfOrder > 1 ) { - valid = false; - ss << " ???"; - } - else ss << " (OK)"; - ss << '\n'; - } - ss << " " << n << " objects found, nobj:" << d->nrecords << "\n"; - ss << " " << len << " bytes data w/headers\n"; - ss << " " << nlen << " bytes data wout/headers\n"; - - ss << " deletedList: "; - for( int i = 0; i < Buckets; i++ ) { - ss << (d->deletedList[i].isNull() ? '0' : '1'); - } - ss << endl; - int ndel = 0; - long long delSize = 0; - int incorrect = 0; - for( int i = 0; i < Buckets; i++ ) { - DiskLoc loc = d->deletedList[i]; - try { - int k = 0; - while( !loc.isNull() ) { - if( recs.count(loc) ) - incorrect++; - ndel++; - - if( loc.questionable() ) { - if( loc.a() <= 0 || strstr(ns, "hudsonSmall") == 0 ) { - ss << " ?bad deleted loc: " << loc.toString() << " bucket:" << i << " k:" << k << endl; - valid = false; - break; - } - } - - DeletedRecord *d = loc.drec(); - delSize += d->lengthWithHeaders; - loc = d->nextDeleted; - k++; - } - } catch(...) { ss <<" ?exception in deleted chain for bucket " << i << endl; valid = false; } - } - ss << " deleted: n: " << ndel << " size: " << delSize << '\n'; - if( incorrect ) { - ss << " ?corrupt: " << incorrect << " records from datafile are in deleted list\n"; - valid = false; - } - - int idxn = 0; - try { - ss << " nIndexes:" << d->nIndexes << endl; - for( ; idxn < d->nIndexes; idxn++ ) { - ss << " " << d->indexes[idxn].indexNamespace() << " keys:" << - d->indexes[idxn].head.btree()->fullValidate(d->indexes[idxn].head) << endl; - } - } - catch(...) { - ss << "\n exception during index validate idxn:" << idxn << endl; valid=false; - } - - } - catch(AssertionException) { - ss << "\n exception during validate\n" << endl; - valid = false; - } - - if( !valid ) - ss << " ns corrupt, requires dbchk\n"; - - return ss.str(); -} - -bool userCreateNS(const char *ns, JSObj& j, string& err); - -const int edebug=0; - -bool dbEval(JSObj& cmd, JSObjBuilder& result) { - Element e = cmd.firstElement(); - assert( e.type() == Code ); - const char *code = e.valuestr(); - if ( ! JavaJS ) { - result.append("errmsg", "db side execution is disabled"); - return false; - } - - jlong f = JavaJS->functionCreate(code); - if( f == 0 ) { - result.append("errmsg", "compile failed"); - return false; - } - - Scope s; - s.setString("$client", client->name.c_str()); - Element args = cmd.findElement("args"); - if( args.type() == Array ) { - JSObj eo = args.embeddedObject(); - if( edebug ) { - cout << "args:" << eo.toString() << endl; - cout << "code:\n" << code << endl; - } - s.setObject("args", eo); - } - - int res = s.invoke(f); - if( res ) { - result.append("errno", (double) res); - result.append("errmsg", "invoke failed"); - return false; - } - - int type = s.type("return"); - if( type == Object || type == Array ) - result.append("retval", s.getObject("return")); - else if( type == Number ) - result.append("retval", s.getNumber("return")); - else if( type == String ) - result.append("retval", s.getString("return").c_str()); - else if( type == Bool ) { - result.appendBool("retval", s.getBoolean("return")); - } - - return true; -} - -extern int opLogging; -void flushOpLog(); - -// e.g. -// system.cmd$.find( { queryTraceLevel: 2 } ); -// -// returns true if ran a cmd -// -bool _runCommands(const char *ns, JSObj& jsobj, stringstream& ss, BufBuilder &b, JSObjBuilder& anObjBuilder) { - - const char *p = strchr(ns, '.'); - if( !p ) return false; - if( strcmp(p, ".$cmd") != 0 ) return false; - - bool ok = false; - bool valid = false; - - //cout << jsobj.toString() << endl; - - Element e; - e = jsobj.firstElement(); - - if( e.eoo() ) goto done; - if( e.type() == Code ) { - valid = true; - ok = dbEval(jsobj, anObjBuilder); - } - else if( e.type() == Number ) { - if( strcmp(e.fieldName(), "getoptime") == 0 ) { - valid = true; - ok = true; - anObjBuilder.appendDate("optime", OpTime::now().asDate()); - } - else if( strcmp(e.fieldName(), "dropDatabase") == 0 ) { - if( 1 ) { - cout << "dropDatabase " << ns << endl; - valid = true; - int p = (int) e.number(); - if( p != 1 ) { - ok = false; - } else { - dropDatabase(ns); - logOp("c", ns, jsobj); - ok = true; - } - } - else { - cout << "TEMP CODE: dropdatabase commented out " << endl; - } - } - else if( strcmp(e.fieldName(), "profile") == 0 ) { - anObjBuilder.append("was", (double) client->profile); - int p = (int) e.number(); - valid = true; - if( p == -1 ) - ok = true; - else if( p >= 0 && p <= 2 ) { - ok = true; - client->profile = p; - } - else { - ok = false; - } - } - else { - // admin only commands. - if( strncmp(ns, "admin", p-ns) != 0 ) - return false; - if( strcmp(e.fieldName(),"opLogging") == 0 ) { - valid = ok = true; - opLogging = (int) e.number(); - flushOpLog(); - log() << "CMD: opLogging set to " << opLogging << endl; - } else if( strcmp(e.fieldName(),"queryTraceLevel") == 0 ) { - valid = ok = true; - queryTraceLevel = (int) e.number(); - } else if( strcmp(e.fieldName(),"traceAll") == 0 ) { - valid = ok = true; - queryTraceLevel = (int) e.number(); - otherTraceLevel = (int) e.number(); - } - } - } - else if( e.type() == String ) { - /* { count: "collectionname"[, query: <query>] } */ - string us(ns, p-ns); - - if( strcmp( e.fieldName(), "count" ) == 0 ) { - valid = true; - string ns = us + '.' + e.valuestr(); - string err; - int n = runCount(ns.c_str(), jsobj, err); - int nn = n; - ok = true; - if( n < 0 ) { - ok = false; - nn = 0; - if( !err.empty() ) - anObjBuilder.append("errmsg", err.c_str()); - } - anObjBuilder.append("n", (double) nn); - } - else if( strcmp( e.fieldName(), "clone") == 0 ) { - valid = true; - string err; - ok = cloneFrom(e.valuestr(), err); - if( !err.empty() ) - anObjBuilder.append("errmsg", err.c_str()); - } - else if( strcmp( e.fieldName(), "create") == 0 ) { - valid = true; - string ns = us + '.' + e.valuestr(); - string err; - ok = userCreateNS(ns.c_str(), jsobj, err); - if( ok ) - logOp("c", ns.c_str(), jsobj); - if( !ok && !err.empty() ) - anObjBuilder.append("errmsg", err.c_str()); - } - else if( strcmp( e.fieldName(), "clean") == 0 ) { - valid = true; - string dropNs = us + '.' + e.valuestr(); - NamespaceDetails *d = nsdetails(dropNs.c_str()); - log() << "CMD: clean " << dropNs << endl; - if( d ) { - ok = true; - anObjBuilder.append("ns", dropNs.c_str()); - clean(dropNs.c_str(), d); - } - else { - anObjBuilder.append("errmsg", "ns not found"); - } - } - else if( strcmp( e.fieldName(), "drop") == 0 ) { - valid = true; - string nsToDrop = us + '.' + e.valuestr(); - NamespaceDetails *d = nsdetails(nsToDrop.c_str()); - log() << "CMD: drop " << nsToDrop << endl; - if( d == 0 ) { - anObjBuilder.append("errmsg", "ns not found"); - } - else if( d->nIndexes != 0 ) { - // client is supposed to drop the indexes first - anObjBuilder.append("errmsg", "ns has indexes (not permitted on drop)"); - } - else { - ok = true; - anObjBuilder.append("ns", nsToDrop.c_str()); - ClientCursor::invalidate(nsToDrop.c_str()); - dropNS(nsToDrop); - logOp("c", ns, jsobj); - /* - { - JSObjBuilder b; - b.append("name", dropNs.c_str()); - JSObj cond = b.done(); // { name: "colltodropname" } - deleteObjects("system.namespaces", cond, false, true); - } - client->namespaceIndex.kill(dropNs.c_str()); - */ - } - } - else if( strcmp( e.fieldName(), "validate") == 0 ) { - valid = true; - string toValidateNs = us + '.' + e.valuestr(); - NamespaceDetails *d = nsdetails(toValidateNs.c_str()); - log() << "CMD: validate " << toValidateNs << endl; - if( d ) { - ok = true; - anObjBuilder.append("ns", toValidateNs.c_str()); - string s = validateNS(toValidateNs.c_str(), d); - anObjBuilder.append("result", s.c_str()); - } - else { - anObjBuilder.append("errmsg", "ns not found"); - } - } - else if( strcmp(e.fieldName(),"deleteIndexes") == 0 ) { - valid = true; - /* note: temp implementation. space not reclaimed! */ - string toDeleteNs = us + '.' + e.valuestr(); - NamespaceDetails *d = nsdetails(toDeleteNs.c_str()); - log() << "CMD: deleteIndexes " << toDeleteNs << endl; - if( d ) { - Element f = jsobj.findElement("index"); - if( !f.eoo() ) { - - d->aboutToDeleteAnIndex(); - - ClientCursor::invalidate(toDeleteNs.c_str()); - logOp("c", ns, jsobj); - - // delete a specific index or all? - if( f.type() == String ) { - const char *idxName = f.valuestr(); - if( *idxName == '*' && idxName[1] == 0 ) { - ok = true; - cout << " d->nIndexes was " << d->nIndexes << endl; - anObjBuilder.append("nIndexesWas", (double)d->nIndexes); - anObjBuilder.append("msg", "all indexes deleted for collection"); - cout << " alpha implementation, space not reclaimed" << endl; - for( int i = 0; i < d->nIndexes; i++ ) - d->indexes[i].kill(); - d->nIndexes = 0; - } - else { - // delete just one index - int x = d->findIndexByName(idxName); - if( x >= 0 ) { - cout << " d->nIndexes was " << d->nIndexes << endl; - anObjBuilder.append("nIndexesWas", (double)d->nIndexes); - - /* note it is important we remove the IndexDetails with this - call, otherwise, on recreate, the old one would be reused, and its - IndexDetails::info ptr would be bad info. - */ - d->indexes[x].kill(); - - d->nIndexes--; - for( int i = x; i < d->nIndexes; i++ ) - d->indexes[i] = d->indexes[i+1]; - ok=true; - cout << " alpha implementation, space not reclaimed\n"; - } else { - cout << "deleteIndexes: " << idxName << " not found" << endl; - } - } - } - } - } - else { - anObjBuilder.append("errmsg", "ns not found"); - } - } - } - -done: - if( !valid ) - anObjBuilder.append("errmsg", "no such cmd"); - anObjBuilder.append("ok", ok?1.0:0.0); - JSObj x = anObjBuilder.done(); - b.append((void*) x.objdata(), x.objsize()); - return true; -} +bool _runCommands(const char *ns, JSObj& jsobj, stringstream& ss, BufBuilder &b, JSObjBuilder& anObjBuilder); bool runCommands(const char *ns, JSObj& jsobj, stringstream& ss, BufBuilder &b, JSObjBuilder& anObjBuilder) { try { diff --git a/db/repl.cpp b/db/repl.cpp index 1973afad48d..e41473b0e5c 100644 --- a/db/repl.cpp +++ b/db/repl.cpp @@ -3,7 +3,6 @@ /* TODO PAIRING - _ better concurrency; ie don't block if connection to slave blocks _ on a syncexception, don't allow going back to master state? */ diff --git a/util/mmap.cpp b/util/mmap.cpp index a3296cd2412..e67fe4560b0 100644 --- a/util/mmap.cpp +++ b/util/mmap.cpp @@ -36,7 +36,7 @@ void MemoryMappedFile::closeAllFiles() { ++closingAllFiles; for( set<MemoryMappedFile*>::iterator i = mmfiles.begin(); i != mmfiles.end(); i++ ) (*i)->close(); - cout << " closeAllFiles() finished" << endl; + log() << " closeAllFiles() finished" << endl; --closingAllFiles; } diff --git a/util/unittest.h b/util/unittest.h new file mode 100644 index 00000000000..db80517c463 --- /dev/null +++ b/util/unittest.h @@ -0,0 +1,40 @@ +// unittest.h + +/** +* Copyright (C) 2008 10gen 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 <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +/* the idea here is to let all initialization of global variables (classes inheriting from UnitTest) + complete before we run the tests -- otherwise order of initilization being arbitrary may mess + us up. The app's main() function should call runTests. +*/ +struct UnitTest { + UnitTest() { registerTest(this); } + + // assert if fails + virtual void run() = 0; + + static vector<UnitTest*> tests; + + static void registerTest(UnitTest *t) { tests.push_back(t); } + + static void runTests() { + for( vector<UnitTest*>::iterator i = tests.begin(); i != tests.end(); i++ ) { + } + } +}; + diff --git a/util/util.cpp b/util/util.cpp index c78767f3a69..0879a27b50b 100644 --- a/util/util.cpp +++ b/util/util.cpp @@ -16,6 +16,9 @@ #include "stdafx.h" #include "goodies.h" +#include "unittest.h" + +vector<UnitTest*> UnitTest::tests; Nullstream nullstream; Logstream logstream; @@ -43,8 +46,8 @@ int nextPrime(int n) { return n; } -struct UtilTest { - UtilTest() { +struct UtilTest : public UnitTest { + void run() { assert( WrappingInt(0) <= WrappingInt(0) ); assert( WrappingInt(0) <= WrappingInt(1) ); assert( !(WrappingInt(1) <= WrappingInt(0)) ); |