diff options
author | Dwight <dmerriman@gmail.com> | 2008-09-03 16:43:00 -0400 |
---|---|---|
committer | Dwight <dmerriman@gmail.com> | 2008-09-03 16:43:00 -0400 |
commit | 8cac7e4dc4c1102b69598ba6b1e60df8d8e1dcdd (patch) | |
tree | d7a65a1cd57a9446c04903e268484eca65862adb | |
parent | fd876a27f769b22bfe6cd48f45cf3c4e3704c9b7 (diff) | |
download | mongo-8cac7e4dc4c1102b69598ba6b1e60df8d8e1dcdd.tar.gz |
advertise new dbs for replication; handle user asserts on repl
-rw-r--r-- | db/db.cpp | 5 | ||||
-rw-r--r-- | db/db.vcproj | 4 | ||||
-rw-r--r-- | db/pdfile.cpp | 19 | ||||
-rw-r--r-- | db/pdfile.h | 6 | ||||
-rw-r--r-- | db/query.cpp | 2 | ||||
-rw-r--r-- | db/repl.cpp | 206 | ||||
-rw-r--r-- | db/repl.h | 15 | ||||
-rw-r--r-- | db/replset.h | 49 | ||||
-rw-r--r-- | mongo.1 | 79 | ||||
-rw-r--r-- | stdafx.cpp | 4 | ||||
-rw-r--r-- | stdafx.h | 11 | ||||
-rw-r--r-- | util/goodies.h | 8 | ||||
-rw-r--r-- | util/util.cpp | 4 |
13 files changed, 243 insertions, 169 deletions
diff --git a/db/db.cpp b/db/db.cpp index 240a71c3480..ad62ed623ed 100644 --- a/db/db.cpp +++ b/db/db.cpp @@ -44,6 +44,7 @@ int dbLocked = 0; void closeAllSockets(); void startReplication(); +void pairWith(const char *remoteEnd); struct MyStartupTests { MyStartupTests() { @@ -896,6 +897,9 @@ int main(int argc, char* argv[], char *envp[] ) master = true; else if( s == "--slave" ) slave = true; + else if( s == "--pairwith" ) { + pairWith( argv[++i] ); + } else if( s == "--dbpath" ) dbpath = argv[++i]; else if( s == "--appsrvpath" ) @@ -930,6 +934,7 @@ int main(int argc, char* argv[], char *envp[] ) cout << " --port <portno> --dbpath <root> --appsrvpath <root of appsrv>" << endl; cout << " --nocursors --nojni" << endl; cout << " --oplog<n> 0=off 1=W 2=R 3=both 7=W+some reads" << endl; + cout << " --pairwith <server:port>" << endl; cout << endl; return 0; diff --git a/db/db.vcproj b/db/db.vcproj index 7e6e0285868..6ab824bf3c7 100644 --- a/db/db.vcproj +++ b/db/db.vcproj @@ -361,6 +361,10 @@ >
</File>
<File
+ RelativePath=".\replset.h"
+ >
+ </File>
+ <File
RelativePath=".\resource.h"
>
</File>
diff --git a/db/pdfile.cpp b/db/pdfile.cpp index 84b76b79978..712924b8f6e 100644 --- a/db/pdfile.cpp +++ b/db/pdfile.cpp @@ -152,15 +152,13 @@ void PhysicalDataFile::open(int fn, const char *filename) { assert( length >= 64*1024*1024 ); - if( strstr(filename, "_hudsonSmall") ) { - int mult = 1; - if ( fn > 1 && fn < 1000 ) - mult = fn; - length = 1024 * 512 * mult; - cout << "Warning : using small files for _hudsonSmall" << endl; - } - - + if( strstr(filename, "_hudsonSmall") ) { + int mult = 1; + if ( fn > 1 && fn < 1000 ) + mult = fn; + length = 1024 * 512 * mult; + cout << "Warning : using small files for _hudsonSmall" << endl; + } assert( length % 4096 == 0 ); assert(fn == fileNo); @@ -826,6 +824,9 @@ DiskLoc DataFileMgr::insert(const char *ns, const void *buf, int len, bool god) return loc; } +/* special version of insert for transaction logging -- streamlined a bit. + assumes ns is capped and no indexes +*/ Record* DataFileMgr::fast_oplog_insert(NamespaceDetails *d, const char *ns, int len) { RARELY assert( d == nsdetails(ns) ); diff --git a/db/pdfile.h b/db/pdfile.h index 9e7a8433d51..c0e9b2d44c1 100644 --- a/db/pdfile.h +++ b/db/pdfile.h @@ -209,7 +209,11 @@ public: static int headerSize() { return sizeof(PDFHeader) - 4; } - bool uninitialized() { if( version == 0 ) return true; assert(version == VERSION); return false; } + bool uninitialized() { + if( version == 0 ) return true; + assert(version == VERSION); + return false; + } Record* getRecord(DiskLoc dl) { int ofs = dl.getOfs(); diff --git a/db/query.cpp b/db/query.cpp index d35cab071ef..c39f38b1392 100644 --- a/db/query.cpp +++ b/db/query.cpp @@ -362,7 +362,7 @@ int _updateObjects(const char *ns, JSObj updateobj, JSObj pattern, bool upsert, set<string>& idxKeys = ndt.indexKeys(); for( vector<Mod>::iterator i = mods.begin(); i != mods.end(); i++ ) { if( idxKeys.count(i->fieldName) ) { - assert(false); + uassert("can't $inc/$set an indexed field", false); } } diff --git a/db/repl.cpp b/db/repl.cpp index 12722b37377..ea6b693e21c 100644 --- a/db/repl.cpp +++ b/db/repl.cpp @@ -68,7 +68,7 @@ int test2() { /* --------------------------------------------------------------*/ -Source::Source(JSObj o) : nClonedThisPass(0) { +ReplSource::ReplSource(JSObj o) : nClonedThisPass(0) { only = o.getStringField("only"); hostName = o.getStringField("host"); sourceName = o.getStringField("source"); @@ -95,7 +95,7 @@ Source::Source(JSObj o) : nClonedThisPass(0) { } /* Turn our C++ Source object into a JSObj */ -JSObj Source::jsobj() { +JSObj ReplSource::jsobj() { JSObjBuilder b; b.append("host", hostName); b.append("source", sourceName); @@ -112,7 +112,7 @@ JSObj Source::jsobj() { return b.doneAndDecouple(); } -void Source::save() { +void ReplSource::save() { JSObjBuilder b; b.append("host", hostName); b.append("source", sourceName); @@ -129,13 +129,13 @@ void Source::save() { client = 0; } -void Source::cleanup(vector<Source*>& v) { - for( vector<Source*>::iterator i = v.begin(); i != v.end(); i++ ) +void ReplSource::cleanup(vector<ReplSource*>& v) { + for( vector<ReplSource*>::iterator i = v.begin(); i != v.end(); i++ ) delete *i; } -static void addSourceToList(vector<Source*>&v, Source& s, vector<Source*>&old) { - for( vector<Source*>::iterator i = old.begin(); i != old.end(); ) { +static void addSourceToList(vector<ReplSource*>&v, ReplSource& s, vector<ReplSource*>&old) { + for( vector<ReplSource*>::iterator i = old.begin(); i != old.end(); ) { if( s == **i ) { v.push_back(*i); old.erase(i); @@ -144,32 +144,32 @@ static void addSourceToList(vector<Source*>&v, Source& s, vector<Source*>&old) { i++; } - v.push_back( new Source(s) ); + v.push_back( new ReplSource(s) ); } /* we reuse our existing objects so that we can keep our existing connection and cursor in effect. */ -void Source::loadAll(vector<Source*>& v) { - vector<Source *> old = v; +void ReplSource::loadAll(vector<ReplSource*>& v) { + vector<ReplSource *> old = v; v.erase(v.begin(), v.end()); setClient("local.sources"); auto_ptr<Cursor> c = findTableScan("local.sources", emptyObj); while( c->ok() ) { - Source tmp(c->current()); + ReplSource tmp(c->current()); addSourceToList(v, tmp, old); c->advance(); } client = 0; - for( vector<Source*>::iterator i = old.begin(); i != old.end(); i++ ) + for( vector<ReplSource*>::iterator i = old.begin(); i != old.end(); i++ ) delete *i; } JSObj opTimeQuery = fromjson("{getoptime:1}"); -bool Source::resync(string db) { +bool ReplSource::resync(string db) { { log() << "resync: dropping database " << db << endl; string dummyns = db + "."; @@ -200,9 +200,11 @@ bool Source::resync(string db) { return true; } -/* { ts: ..., op: <optype>, ns: ..., o: <obj> , o2: <extraobj>, b: <boolflag> } +/* local.$oplog.main is of the form: + { ts: ..., op: <optype>, ns: ..., o: <obj> , o2: <extraobj>, b: <boolflag> } + ... */ -void Source::applyOperation(JSObj& op) { +void ReplSource::sync_pullOpLog_applyOperation(JSObj& op) { char clientName[MaxClientLen]; const char *ns = op.getStringField("ns"); nsToClient(ns, clientName); @@ -234,45 +236,53 @@ void Source::applyOperation(JSObj& op) { stringstream ss; const char *opType = op.getStringField("op"); JSObj o = op.getObjectField("o"); - if( *opType == 'i' ) { - const char *p = strchr(ns, '.'); - if( p && strcmp(p, ".system.indexes") == 0 ) { - // updates aren't allowed for indexes -- so we will do a regular insert. if index already - // exists, that is ok. - theDataFileMgr.insert(ns, (void*) o.objdata(), o.objsize()); - } - else { - // do upserts for inserts as we might get replayed more than once - OID *oid = o.getOID(); - if( oid == 0 ) { - _updateObjects(ns, o, o, true, ss); - } - else { - JSObjBuilder b; - b.appendOID("_id", oid); - RARELY ensureHaveIdIndex(ns); // otherwise updates will be super slow - _updateObjects(ns, o, b.done(), true, ss); - } - } - } - else if( *opType == 'u' ) { - RARELY ensureHaveIdIndex(ns); // otherwise updates will be super slow - _updateObjects(ns, o, op.getObjectField("o2"), op.getBoolField("b"), ss); - } - else if( *opType == 'd' ) { - deleteObjects(ns, o, op.getBoolField("b")); + try { + if( *opType == 'i' ) { + const char *p = strchr(ns, '.'); + if( p && strcmp(p, ".system.indexes") == 0 ) { + // updates aren't allowed for indexes -- so we will do a regular insert. if index already + // exists, that is ok. + theDataFileMgr.insert(ns, (void*) o.objdata(), o.objsize()); + } + else { + // do upserts for inserts as we might get replayed more than once + OID *oid = o.getOID(); + if( oid == 0 ) { + _updateObjects(ns, o, o, true, ss); + } + else { + JSObjBuilder b; + b.appendOID("_id", oid); + RARELY ensureHaveIdIndex(ns); // otherwise updates will be super slow + _updateObjects(ns, o, b.done(), true, ss); + } + } + } + else if( *opType == 'u' ) { + RARELY ensureHaveIdIndex(ns); // otherwise updates will be super slow + _updateObjects(ns, o, op.getObjectField("o2"), op.getBoolField("b"), ss); + } + else if( *opType == 'd' ) { + if( opType[1] == 0 ) + deleteObjects(ns, o, op.getBoolField("b")); + else + assert( opType[1] == 'b' ); // "db" advertisement + } + else { + BufBuilder bb; + JSObjBuilder ob; + assert( *opType == 'c' ); + _runCommands(ns, o, ss, bb, ob); + } } - else { - BufBuilder bb; - JSObjBuilder ob; - assert( *opType == 'c' ); - _runCommands(ns, o, ss, bb, ob); + catch( UserAssertionException e ) { + log() << "sync: caught user assertion " << e.msg << '\n'; } client = 0; } /* note: not yet in mutex at this point. */ -void Source::pullOpLog() { +void ReplSource::sync_pullOpLog() { string ns = string("local.oplog.$") + sourceName; bool tailing = true; @@ -323,7 +333,7 @@ void Source::pullOpLog() { log() << "pull: initial run\n"; } { - applyOperation(op); + sync_pullOpLog_applyOperation(op); n++; } } @@ -359,7 +369,7 @@ void Source::pullOpLog() { uassert("bad 'ts' value in sources", false); } - applyOperation(op); + sync_pullOpLog_applyOperation(op); n++; } } @@ -368,7 +378,7 @@ void Source::pullOpLog() { /* note: not yet in mutex at this point. returns true if everything happy. return false if you want to reconnect. */ -bool Source::sync() { +bool ReplSource::sync() { log() << "pull: " << sourceName << '@' << hostName << endl; nClonedThisPass = 0; @@ -402,7 +412,7 @@ bool Source::sync() { OpTime serverCurTime; serverCurTime.asDate() = e.date(); */ - pullOpLog(); + sync_pullOpLog(); return true; } @@ -416,7 +426,13 @@ Client *localOplogClient = 0; { ts : ..., op: ..., ns: ..., o: ... } ts: an OpTime timestamp op: - 'i' = insert + "i" insert + "u" update + "d" delete + "c" db cmd + bb: + if not null, specifies a boolean to pass along to the other side as b: param. + used for "justOne" or "upsert" flags on 'd', 'u' */ void _logOp(const char *opstr, const char *ns, JSObj& obj, JSObj *o2, bool *bb) { if( strncmp(ns, "local.", 6) == 0 ) @@ -471,19 +487,19 @@ _ reuse that cursor when we can */ void replMain() { - vector<Source*> sources; + vector<ReplSource*> sources; while( 1 ) { { dblock lk; - Source::loadAll(sources); + ReplSource::loadAll(sources); } if( sources.empty() ) sleepsecs(20); - for( vector<Source*>::iterator i = sources.begin(); i != sources.end(); i++ ) { - Source *s = *i; + for( vector<ReplSource*>::iterator i = sources.begin(); i != sources.end(); i++ ) { + ReplSource *s = *i; bool ok = false; try { ok = s->sync(); @@ -503,7 +519,7 @@ void replMain() { sleepsecs(3); } - Source::cleanup(sources); + ReplSource::cleanup(sources); } int debug_stop_repl = 0; @@ -524,23 +540,75 @@ void replSlaveThread() { } } +/* used to verify that slave knows what databases we have */ +void logOurDbsPresence() { + path dbs(dbpath); + directory_iterator end; + directory_iterator i(dbs); + + dblock lk; + + int k = 0; + while( i != end ) { + path p = *i; + string f = p.leaf(); + if( endsWith(f.c_str(), ".ns") ) { + /* note: we keep trailing "." so that when slave calls setClient(ns) everything is happy; e.g., + valid namespaces must always have a dot, even though here it is just a placeholder not + a real one + */ + string dbname = string(f.c_str(), f.size() - 2); + if( dbname != "local." ) + logOp("db", dbname.c_str(), emptyObj); + } + i++; + } +} + +/* we have to log the db presence periodically as that "advertisement" will roll out of the log + as it is of finite length. also as we only do one db cloning per pass, we could skip over a bunch of + advertisements and thus need to see them again later. so this mechanism can actually be very slow to + work, and should be improved. +*/ +void replMasterThread() { + sleepsecs(15); + while( 1 ) { + logOurDbsPresence(); + sleepsecs(60 * 10); + } +} + +#include "replset.h" + +ReplSet *replSet = 0; + void startReplication() { if( slave ) { log() << "slave=true" << endl; boost::thread repl_thread(replSlaveThread); } - if( master ) { + if( master || replSet ) { log() << "master=true" << endl; - dblock lk; - /* create an oplog collection, if it doesn't yet exist. */ - JSObjBuilder b; - b.append("size", 254.0 * 1000 * 1000); - b.appendBool("capped", 1); - setClientTempNs("local.oplog.$main"); - string err; - JSObj o = b.done(); - userCreateNS("local.oplog.$main", o, err); - client = 0; + { + dblock lk; + /* create an oplog collection, if it doesn't yet exist. */ + JSObjBuilder b; + b.append("size", 254.0 * 1000 * 1000); + b.appendBool("capped", 1); + setClientTempNs("local.oplog.$main"); + string err; + JSObj o = b.done(); + userCreateNS("local.oplog.$main", o, err); + client = 0; + } + + boost::thread mt(replMasterThread); } } + +/* called from main at server startup */ +void pairWith(const char *remoteEnd) { + replSet = new ReplSet(remoteEnd); +// uassert("not done yet!", false); +} diff --git a/db/repl.h b/db/repl.h index d3708f56860..a95d5530115 100644 --- a/db/repl.h +++ b/db/repl.h @@ -85,10 +85,10 @@ struct SyncException { { host: ..., source: ..., syncedTo: ..., dbs: { ... } } */ -class Source { +class ReplSource { bool resync(string db); - void pullOpLog(); - void applyOperation(JSObj& op); + void sync_pullOpLog(); + void sync_pullOpLog_applyOperation(JSObj& op); auto_ptr<DBClientConnection> conn; auto_ptr<DBClientCursor> cursor; @@ -108,9 +108,9 @@ public: int nClonedThisPass; - static void loadAll(vector<Source*>&); - static void cleanup(vector<Source*>&); - Source(JSObj); + static void loadAll(vector<ReplSource*>&); + static void cleanup(vector<ReplSource*>&); + ReplSource(JSObj); bool sync(); void save(); // write ourself to local.sources void resetConnection() { conn = auto_ptr<DBClientConnection>(0); } @@ -119,7 +119,7 @@ public: // { host: ..., source: ..., syncedTo: ... } JSObj jsobj(); - bool operator==(const Source&r) const { + bool operator==(const ReplSource&r) const { return hostName == r.hostName && sourceName == r.sourceName; } }; @@ -129,6 +129,7 @@ public: "u" update "d" delete "c" db cmd + "db" declares presence of a database (ns is set to the db name + '.') */ void _logOp(const char *opstr, const char *ns, JSObj& obj, JSObj *patt, bool *b); inline void logOp(const char *opstr, const char *ns, JSObj& obj, JSObj *patt = 0, bool *b = 0) { diff --git a/db/replset.h b/db/replset.h new file mode 100644 index 00000000000..106b36c1189 --- /dev/null +++ b/db/replset.h @@ -0,0 +1,49 @@ +/** +* 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 + +/* ReplSet is a pair of db servers replicating to one another and cooperating. + + Only one member of the pair is active at a time; so this is a smart master/slave + configuration basically. + + You may read from the slave at anytime though (if you don't mind the slight lag). + + todo: Could be extended to be more than a pair, thus the name 'Set' -- for example, + a set of 3... +*/ + +class ReplSet { + +public: + int remotePort; + string remoteHost; + + ReplSet(const char *remoteEnd); + +}; + +ReplSet::ReplSet(const char *remoteEnd) { + remotePort = DBPort; + remoteHost = remoteEnd; + const char *p = strchr(remoteEnd, ':'); + if( p ) { + remoteHost = string(remoteEnd, p-remoteEnd); + remotePort = atoi(p+1); + uassert("bad port #", remotePort > 0 && remotePort < 0x10000 ); + } +} diff --git a/mongo.1 b/mongo.1 deleted file mode 100644 index 1ad5f8ffba9..00000000000 --- a/mongo.1 +++ /dev/null @@ -1,79 +0,0 @@ -.\"Modified from man(1) of FreeBSD, the NetBSD mdoc.template, and mdoc.samples.
-.\"See Also:
-.\"man mdoc.samples for a complete listing of options
-.\"man mdoc for the short list of editing options
-.\"/usr/share/misc/mdoc.template
-.Dd 8/12/08 \" DATE
-.Dt mongo 1 \" Program name and manual section number
-.Os Darwin
-.Sh NAME \" Section Header - required - don't modify
-.Nm mongo,
-.\" The following lines are read in generating the apropos(man -k) database. Use only key
-.\" words here as the database is built based on the words here and in the .ND line.
-.Nm Other_name_for_same_program(),
-.Nm Yet another name for the same program.
-.\" Use .Nm macro to designate other names for the documented program.
-.Nd This line parsed for whatis database.
-.Sh SYNOPSIS \" Section Header - required - don't modify
-.Nm
-.Op Fl abcd \" [-abcd]
-.Op Fl a Ar path \" [-a path]
-.Op Ar file \" [file]
-.Op Ar \" [file ...]
-.Ar arg0 \" Underlined argument - use .Ar anywhere to underline
-arg2 ... \" Arguments
-.Sh DESCRIPTION \" Section Header - required - don't modify
-Use the .Nm macro to refer to your program throughout the man page like such:
-.Nm
-Underlining is accomplished with the .Ar macro like this:
-.Ar underlined text .
-.Pp \" Inserts a space
-A list of items with descriptions:
-.Bl -tag -width -indent \" Begins a tagged list
-.It item a \" Each item preceded by .It macro
-Description of item a
-.It item b
-Description of item b
-.El \" Ends the list
-.Pp
-A list of flags and their descriptions:
-.Bl -tag -width -indent \" Differs from above in tag removed
-.It Fl a \"-a flag as a list item
-Description of -a flag
-.It Fl b
-Description of -b flag
-.El \" Ends the list
-.Pp
-.\" .Sh ENVIRONMENT \" May not be needed
-.\" .Bl -tag -width "ENV_VAR_1" -indent \" ENV_VAR_1 is width of the string ENV_VAR_1
-.\" .It Ev ENV_VAR_1
-.\" Description of ENV_VAR_1
-.\" .It Ev ENV_VAR_2
-.\" Description of ENV_VAR_2
-.\" .El
-.Sh FILES \" File used or created by the topic of the man page
-.Bl -tag -width "/Users/joeuser/Library/really_long_file_name" -compact
-.It Pa /usr/share/file_name
-FILE_1 description
-.It Pa /Users/joeuser/Library/really_long_file_name
-FILE_2 description
-.El \" Ends the list
-.\" .Sh DIAGNOSTICS \" May not be needed
-.\" .Bl -diag
-.\" .It Diagnostic Tag
-.\" Diagnostic informtion here.
-.\" .It Diagnostic Tag
-.\" Diagnostic informtion here.
-.\" .El
-.Sh SEE ALSO
-.\" List links in ascending order by section, alphabetically within a section.
-.\" Please do not reference files that do not exist without filing a bug report
-.Xr a 1 ,
-.Xr b 1 ,
-.Xr c 1 ,
-.Xr a 2 ,
-.Xr b 2 ,
-.Xr a 3 ,
-.Xr b 3
-.\" .Sh BUGS \" Document known, unremedied bugs
-.\" .Sh HISTORY \" Document history if command behaves in a unique manner
\ No newline at end of file diff --git a/stdafx.cpp b/stdafx.cpp index 89c5194be32..56ea439d52e 100644 --- a/stdafx.cpp +++ b/stdafx.cpp @@ -41,14 +41,14 @@ void wasserted(const char *msg, const char *file, unsigned line) { sayDbContext(); } -void asserted(const char *msg, const char *file, unsigned line) { +void asserted(const char *msg, const char *file, unsigned line) { wasserted(msg, file, line); throw AssertionException(); } void uasserted(const char *msg) { problem() << "User Assertion " << msg << endl; - throw AssertionException(); + throw UserAssertionException(msg); } void msgasserted(const char *msg) { @@ -53,7 +53,16 @@ inline void * ourrealloc(void *ptr, size_t size) { // you can catch these class AssertionException { public: - AssertionException() { } + const char *msg; + AssertionException() { msg = ""; } + virtual bool isUserAssertion() { return false; } +}; + +/* we use the same mechanism for bad things the user does -- which are really just errors */ +class UserAssertionException : public AssertionException { +public: + UserAssertionException(const char *_msg) { msg = _msg; } + virtual bool isUserAssertion() { return true; } }; void asserted(const char *msg, const char *file, unsigned line); diff --git a/util/goodies.h b/util/goodies.h index 631ed12b9f6..7c38f8d1e93 100644 --- a/util/goodies.h +++ b/util/goodies.h @@ -191,3 +191,11 @@ public: */ //typedef boostlock lock; + +inline bool endsWith(const char *p, const char *suffix) { + int a = strlen(p); + int b = strlen(suffix); + if( b > a ) return false; + return strcmp(p + a - b, suffix) == 0; +} + diff --git a/util/util.cpp b/util/util.cpp index 9c09267ae9c..51fb7dfc7f7 100644 --- a/util/util.cpp +++ b/util/util.cpp @@ -64,5 +64,9 @@ struct UtilTest { assert( !isPrime(6) ); assert( nextPrime(4) == 5 ); assert( nextPrime(8) == 11 ); + + assert( endsWith("abcde", "de") ); + assert( !endsWith("abcde", "dasdfasdfashkfde") ); + } } utilTest; |