From 8e925927c6fe1bb485030bdc0151f0b90e8ceab4 Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Tue, 10 Aug 2010 17:27:02 -0700 Subject: [PATCH 1/7] First commit of message throttling code --- plugins/Irc/IrcPlugin.php | 23 +++- plugins/Irc/Irc_waiting_message.php | 125 ++++++++++++++++++ .../phergie/Phergie/Plugin/Statusnet.php | 30 +++++ plugins/Irc/ircmanager.php | 100 ++++++++++++-- 4 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 plugins/Irc/Irc_waiting_message.php diff --git a/plugins/Irc/IrcPlugin.php b/plugins/Irc/IrcPlugin.php index 85c348d9b3..54b7585211 100644 --- a/plugins/Irc/IrcPlugin.php +++ b/plugins/Irc/IrcPlugin.php @@ -126,6 +126,7 @@ class IrcPlugin extends ImPlugin { include_once $dir . '/'.strtolower($cls).'.php'; return false; case 'Fake_Irc': + case 'Irc_waiting_message': case 'ChannelResponseChannel': include_once $dir . '/'. $cls .'.php'; return false; @@ -150,6 +151,25 @@ class IrcPlugin extends ImPlugin { return true; } + /** + * Ensure the database table is present + * + */ + public function onCheckSchema() { + $schema = Schema::get(); + + // For storing messages while sessions become ready + $schema->ensureTable('irc_waiting_message', + array(new ColumnDef('id', 'integer', null, + false, 'PRI', null, null, true), + new ColumnDef('data', 'blob', null, false), + new ColumnDef('prioritise', 'tinyint', 1, false), + new ColumnDef('created', 'datetime', null, false), + new ColumnDef('claimed', 'datetime'))); + + return true; + } + /** * Get a microid URI for the given screenname * @@ -171,7 +191,7 @@ class IrcPlugin extends ImPlugin { $lines = explode("\n", $body); foreach ($lines as $line) { $this->fake_irc->doPrivmsg($screenname, $line); - $this->enqueue_outgoing_raw(array('type' => 'message', 'data' => $this->fake_irc->would_be_sent)); + $this->enqueue_outgoing_raw(array('type' => 'message', 'prioritise' => 0, 'data' => $this->fake_irc->would_be_sent)); } return true; } @@ -297,6 +317,7 @@ class IrcPlugin extends ImPlugin { $this->enqueue_outgoing_raw( array( 'type' => 'nickcheck', + 'prioritise' => 1, 'data' => $this->fake_irc->would_be_sent, 'nickdata' => array( diff --git a/plugins/Irc/Irc_waiting_message.php b/plugins/Irc/Irc_waiting_message.php new file mode 100644 index 0000000000..05f72754c7 --- /dev/null +++ b/plugins/Irc/Irc_waiting_message.php @@ -0,0 +1,125 @@ + DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'data' => DB_DATAOBJECT_BLOB + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'prioritise' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'claimed' => DB_DATAOBJECT_TIME + DB_DATAOBJECT_STR); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + public function keys() { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + public function keyTypes() { + return array('id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + public function sequenceKey() { + return array(false, false, false); + } + + /** + * Get the next item in the queue + * + * @return Irc_waiting_message Next message if there is one + */ + public static function top() { + $wm = new Irc_waiting_message(); + + $wm->orderBy('prioritise DESC, created'); + $wm->whereAdd('claimed is null'); + + $wm->limit(1); + + $cnt = $wm->find(true); + + if ($cnt) { + # XXX: potential race condition + # can we force it to only update if claimed is still null + # (or old)? + common_log(LOG_INFO, 'claiming IRC waiting message id = ' . $wm->id); + $orig = clone($wm); + $wm->claimed = common_sql_now(); + $result = $wm->update($orig); + if ($result) { + common_log(LOG_INFO, 'claim succeeded.'); + return $wm; + } else { + common_log(LOG_INFO, 'claim failed.'); + } + } + $wm = null; + return null; + } + + /** + * Release a claimed item. + */ + public function releaseClaim() { + // DB_DataObject doesn't let us save nulls right now + $sql = sprintf("UPDATE irc_waiting_message SET claimed=NULL WHERE id=%d", $this->id); + $this->query($sql); + + $this->claimed = null; + $this->encache(); + } +} diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php index fb75f1d798..dc2680a6df 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Statusnet.php @@ -41,6 +41,13 @@ class Phergie_Plugin_Statusnet extends Phergie_Plugin_Abstract { */ protected $regCallback; + /** + * Connection established callback details + * + * @var array + */ + protected $connectedCallback; + /** * Load callback from config */ @@ -59,6 +66,13 @@ class Phergie_Plugin_Statusnet extends Phergie_Plugin_Abstract { $this->regCallback = NULL; } + $connectedCallback = $this->config['statusnet.connectedcallback']; + if (is_callable($connectedCallback)) { + $this->connectedCallback = $connectedCallback; + } else { + $this->connectedCallback = NULL; + } + $this->unregRegexp = $this->getConfig('statusnet.unregregexp', '/\x02(.*?)\x02 (?:isn\'t|is not) registered/i'); $this->regRegexp = $this->getConfig('statusnet.regregexp', '/(?:\A|\x02)(\w+?)\x02? (?:\(account|is \w+?\z)/i'); } @@ -110,4 +124,20 @@ class Phergie_Plugin_Statusnet extends Phergie_Plugin_Abstract { } } } + + /** + * Intercepts the end of the "message of the day" response and tells + * StatusNet we're connected + * + * @return void + */ + public function onResponse() { + switch ($this->getEvent()->getCode()) { + case Phergie_Event_Response::RPL_ENDOFMOTD: + case Phergie_Event_Response::ERR_NOMOTD: + if ($this->connectedCallback !== NULL) { + call_user_func($this->connectedCallback); + } + } + } } diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index 96c019b7fc..7e2b32b23e 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -32,10 +32,14 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } class IrcManager extends ImManager { protected $conn = null; protected $lastPing = null; + protected $messageWaiting = true; + protected $lastMessage = null; protected $regChecks = array(); protected $regChecksLookup = array(); + protected $connected = false; + /** * Initialize connection to server. * @@ -65,16 +69,38 @@ class IrcManager extends ImManager { } } + public function timeout() { + return 1; + } + /** * Idle processing for io manager's execution loop. - * Send keepalive pings to server. * * @return void */ public function idle() { + // Send a ping if necessary if (empty($this->lastPing) || time() - $this->lastPing > 120) { $this->sendPing(); } + + if ($this->connected) { + // Send a waiting message if appropriate + if ($this->messageWaiting && time() - $this->lastMessage > 1) { + $wm = Irc_waiting_message::top(); + if ($wm === NULL) { + $this->messageWaiting = false; + return; + } + $data = unserialize($wm->data); + + if (!$this->send_raw_message($data)) { + $this->plugin->enqueue_outgoing_raw($data); + } + + $wm->delete(); + } + } } /** @@ -90,6 +116,7 @@ class IrcManager extends ImManager { try { $this->conn->handleEvents(); } catch (Phergie_Driver_Exception $e) { + $this->connected = false; $this->conn->reconnect(); } } @@ -142,6 +169,7 @@ class IrcManager extends ImManager { 'statusnet.messagecallback' => array($this, 'handle_irc_message'), 'statusnet.regcallback' => array($this, 'handle_reg_response'), + 'statusnet.connectedcallback' => array($this, 'handle_connected'), 'statusnet.unregregexp' => $this->plugin->unregregexp, 'statusnet.regregexp' => $this->plugin->regregexp ) @@ -150,6 +178,7 @@ class IrcManager extends ImManager { $this->conn->setConfig($config); $this->conn->connect(); $this->lastPing = time(); + $this->lastMessage = time(); } return $this->conn; } @@ -211,6 +240,38 @@ class IrcManager extends ImManager { } } + /** + * Called when the connection is established + * + * @return void + */ + public function handle_connected() { + $this->connected = true; + } + + /** + * Enters a message into the database for sending when ready + * + * @param string $command Command + * @param array $args Arguments + * @return boolean + */ + protected function enqueue_waiting_message($data) { + $wm = new Irc_waiting_message(); + + $wm->data = serialize($data); + $wm->prioritise = $data['prioritise']; + $wm->created = common_sql_now(); + $result = $wm->insert(); + + if (!$result) { + common_log_db_error($wm, 'INSERT', __FILE__); + throw new ServerException('DB error inserting IRC waiting queue item'); + } + + return true; + } + /** * Send a message using the daemon * @@ -223,28 +284,45 @@ class IrcManager extends ImManager { return false; } - if ($data['type'] != 'message') { - // Nick checking - $nickdata = $data['nickdata']; - $usernick = $nickdata['user']->nickname; - $screenname = $nickdata['screenname']; + if ($data['type'] != 'delayedmessage') { + if ($data['type'] != 'message') { + // Nick checking + $nickdata = $data['nickdata']; + $usernick = $nickdata['user']->nickname; + $screenname = $nickdata['screenname']; - // Cancel any existing checks for this user - if (isset($this->regChecksLookup[$usernick])) { - unset($this->regChecks[$this->regChecksLookup[$usernick]]); + // Cancel any existing checks for this user + if (isset($this->regChecksLookup[$usernick])) { + unset($this->regChecks[$this->regChecksLookup[$usernick]]); + } + + $this->regChecks[$screenname] = $nickdata; + $this->regChecksLookup[$usernick] = $screenname; } - $this->regChecks[$screenname] = $nickdata; - $this->regChecksLookup[$usernick] = $screenname; + // If there is a backlog or we need to wait, queue the message + if ($this->messageWaiting || time() - $this->lastMessage < 1) { + $this->enqueue_waiting_message( + array( + 'type' => 'delayedmessage', + 'prioritise' => $data['prioritise'], + 'data' => $data['data'] + ) + ); + $this->messageWaiting = true; + return true; + } } try { $this->conn->send($data['data']['command'], $data['data']['args']); } catch (Phergie_Driver_Exception $e) { + $this->connected = false; $this->conn->reconnect(); return false; } + $this->lastMessage = time(); return true; } From 8005bdb421eaad1c77195d8efa4aaf169082d51d Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Tue, 10 Aug 2010 18:34:12 -0700 Subject: [PATCH 2/7] Reset message to non-delay type before requeuing --- plugins/Irc/ircmanager.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index 7e2b32b23e..aa0ff3539f 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -69,8 +69,17 @@ class IrcManager extends ImManager { } } + /** + * Request a maximum timeout for listeners before the next idle period. + * + * @return integer Maximum timeout + */ public function timeout() { - return 1; + if ($this->messageWaiting) { + return 1; + } else { + return 120; + } } /** @@ -95,7 +104,13 @@ class IrcManager extends ImManager { $data = unserialize($wm->data); if (!$this->send_raw_message($data)) { - $this->plugin->enqueue_outgoing_raw($data); + $this->plugin->enqueue_outgoing_raw( + array( + 'type' => 'message', + 'prioritise' => $data['prioritise'], + 'data' => $data['data'] + ) + ); } $wm->delete(); From 9da2368383701988dbca08544479c95ad8a7c11f Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Tue, 10 Aug 2010 19:23:45 -0700 Subject: [PATCH 3/7] Retry using the waiting queue so as to preserve message ordering --- plugins/Irc/IrcPlugin.php | 1 + plugins/Irc/Irc_waiting_message.php | 17 +++++++++++++++++ plugins/Irc/ircmanager.php | 23 +++++++++++++---------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/plugins/Irc/IrcPlugin.php b/plugins/Irc/IrcPlugin.php index 54b7585211..e073d6f13d 100644 --- a/plugins/Irc/IrcPlugin.php +++ b/plugins/Irc/IrcPlugin.php @@ -164,6 +164,7 @@ class IrcPlugin extends ImPlugin { false, 'PRI', null, null, true), new ColumnDef('data', 'blob', null, false), new ColumnDef('prioritise', 'tinyint', 1, false), + new ColumnDef('attempts', 'integer', null, false), new ColumnDef('created', 'datetime', null, false), new ColumnDef('claimed', 'datetime'))); diff --git a/plugins/Irc/Irc_waiting_message.php b/plugins/Irc/Irc_waiting_message.php index 05f72754c7..59eec63d8d 100644 --- a/plugins/Irc/Irc_waiting_message.php +++ b/plugins/Irc/Irc_waiting_message.php @@ -10,6 +10,7 @@ class Irc_waiting_message extends Memcached_DataObject { public $id; // int primary_key not_null auto_increment public $data; // blob not_null public $prioritise; // tinyint(1) not_null + public $attempts; // int not_null public $created; // datetime() not_null public $claimed; // datetime() @@ -111,6 +112,22 @@ class Irc_waiting_message extends Memcached_DataObject { return null; } + /** + * Increment the attempts count + * + * @return void + * @throws Exception + */ + public function incAttempts() { + $orig = clone($this); + $this->attempts++; + $result = $this->update($orig); + + if (!$result) { + throw Exception(sprintf(_m("Could not increment attempts count for %d"), $this->id)); + } + } + /** * Release a claimed item. */ diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index aa0ff3539f..5f55e6b34d 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -101,19 +101,21 @@ class IrcManager extends ImManager { $this->messageWaiting = false; return; } + $data = unserialize($wm->data); + $wm->incAttempts(); - if (!$this->send_raw_message($data)) { - $this->plugin->enqueue_outgoing_raw( - array( - 'type' => 'message', - 'prioritise' => $data['prioritise'], - 'data' => $data['data'] - ) - ); + if ($this->send_raw_message($data)) { + $wm->delete(); + } else { + if ($wm->attempts <= common_config('queue', 'max_retries')) { + // Try again next idle + $wm->releaseClaim(); + } else { + // Exceeded the maximum number of retries + $wm->delete(); + } } - - $wm->delete(); } } } @@ -276,6 +278,7 @@ class IrcManager extends ImManager { $wm->data = serialize($data); $wm->prioritise = $data['prioritise']; + $wm->attempts = 0; $wm->created = common_sql_now(); $result = $wm->insert(); From e10ff3475bc3b5a456c86f78074472d17badd3c3 Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Wed, 11 Aug 2010 10:35:20 -0700 Subject: [PATCH 4/7] Disabled debugging output --- plugins/Irc/ircmanager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index 5f55e6b34d..76c02660ff 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -177,7 +177,7 @@ class IrcManager extends ImManager { 'plugins.autoload' => true, - 'ui.enabled' => true, + //'ui.enabled' => true, 'nickserv.password' => $this->plugin->nickservpassword, 'nickserv.identify_message' => $this->plugin->nickservidentifyregexp, From daa3fef96dc709b9aca1fb44802c45612320987f Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Wed, 11 Aug 2010 10:42:11 -0700 Subject: [PATCH 5/7] Added comment to show debugging toggle --- plugins/Irc/ircmanager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index 76c02660ff..5c8e09e356 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -177,6 +177,7 @@ class IrcManager extends ImManager { 'plugins.autoload' => true, + // Uncomment to enable debugging output //'ui.enabled' => true, 'nickserv.password' => $this->plugin->nickservpassword, From d2c72d8ae195525bcfb8f99c7f1e740fd3a84104 Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Wed, 11 Aug 2010 10:49:09 -0700 Subject: [PATCH 6/7] Add an config option to change the ping interval --- plugins/Irc/IrcPlugin.php | 4 ++++ plugins/Irc/README | 2 ++ plugins/Irc/ircmanager.php | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/Irc/IrcPlugin.php b/plugins/Irc/IrcPlugin.php index e073d6f13d..c555580c38 100644 --- a/plugins/Irc/IrcPlugin.php +++ b/plugins/Irc/IrcPlugin.php @@ -60,6 +60,7 @@ class IrcPlugin extends ImPlugin { public $channels = null; public $transporttype = null; public $encoding = null; + public $pinginterval = null; public $regcheck = null; public $unregregexp = null; @@ -359,6 +360,9 @@ class IrcPlugin extends ImPlugin { if (!isset($this->encoding)) { $this->encoding = 'UTF-8'; } + if (!isset($this->pinginterval)) { + $this->pinginterval = 120; + } if (!isset($this->regcheck)) { $this->regcheck = true; diff --git a/plugins/Irc/README b/plugins/Irc/README index bc45688f1e..0a5d9ea83f 100644 --- a/plugins/Irc/README +++ b/plugins/Irc/README @@ -23,6 +23,8 @@ nickservidentifyregexp: Override existing regexp matching request for identifica channels: Channels for bot to idle in transporttype: Set to 'ssl' to enable SSL encoding: Set to change encoding +pinginterval: Set to change the number of seconds between pings (helps keep the connection open) + Defaults to 120 seconds regcheck: Check user's nicknames are registered, enabled by default, set to false to disable regregexp: Override existing regexp matching response from NickServ if nick checked is registered. Must contain a capturing group catching the nick diff --git a/plugins/Irc/ircmanager.php b/plugins/Irc/ircmanager.php index 5c8e09e356..f036ef3091 100644 --- a/plugins/Irc/ircmanager.php +++ b/plugins/Irc/ircmanager.php @@ -78,7 +78,7 @@ class IrcManager extends ImManager { if ($this->messageWaiting) { return 1; } else { - return 120; + return $this->plugin->pinginterval; } } @@ -89,7 +89,7 @@ class IrcManager extends ImManager { */ public function idle() { // Send a ping if necessary - if (empty($this->lastPing) || time() - $this->lastPing > 120) { + if (empty($this->lastPing) || time() - $this->lastPing > $this->plugin->pinginterval) { $this->sendPing(); } From a3fea6f673392f4cfa94164899cc7b07fd65ce9c Mon Sep 17 00:00:00 2001 From: Luke Fitzgerald Date: Thu, 12 Aug 2010 11:58:53 -0700 Subject: [PATCH 7/7] Merge in Phergie changes --- .../Irc/extlib/phergie/Phergie/Connection.php | 8 +- .../extlib/phergie/Phergie/Plugin/Cron.php | 2 +- .../extlib/phergie/Phergie/Plugin/Handler.php | 19 +- .../extlib/phergie/Phergie/Plugin/Karma.php | 80 ++- .../extlib/phergie/Phergie/Plugin/Message.php | 4 +- .../extlib/phergie/Phergie/Plugin/Reload.php | 41 +- .../phergie/Phergie/Plugin/SpellCheck.php | 18 +- .../phergie/Phergie/Plugin/TerryChay.php | 51 +- .../phergie/Tests/Phergie/ConnectionTest.php | 262 ++++++++++ .../Tests/Phergie/Plugin/HandlerTest.php | 129 ++++- .../Tests/Phergie/Plugin/KarmaTest.php | 335 ++++++++++++ .../phergie/Tests/Phergie/Plugin/PingTest.php | 201 ++++---- .../phergie/Tests/Phergie/Plugin/PongTest.php | 48 +- .../Tests/Phergie/Plugin/SpellCheckTest.php | 281 +++++----- .../Tests/Phergie/Plugin/TerryChayTest.php | 136 +++-- .../phergie/Tests/Phergie/Plugin/TestCase.php | 488 +++++++++++++----- 16 files changed, 1513 insertions(+), 590 deletions(-) create mode 100644 plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php create mode 100644 plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php diff --git a/plugins/Irc/extlib/phergie/Phergie/Connection.php b/plugins/Irc/extlib/phergie/Phergie/Connection.php index b3f0acf83d..746dec05f4 100755 --- a/plugins/Irc/extlib/phergie/Phergie/Connection.php +++ b/plugins/Irc/extlib/phergie/Phergie/Connection.php @@ -141,9 +141,9 @@ class Phergie_Connection { if (empty($this->hostmask)) { $this->hostmask = new Phergie_Hostmask( - $this->nick, - $this->username, - $this->host + $this->getNick(), + $this->getUsername(), + $this->getHost() ); } @@ -223,7 +223,7 @@ class Phergie_Connection if (!in_array($this->transport, stream_get_transports())) { throw new Phergie_Connection_Exception( 'Transport ' . $this->transport . ' is not supported', - Phergie_Connection_Exception::TRANSPORT_NOT_SUPPORTED + Phergie_Connection_Exception::ERR_TRANSPORT_NOT_SUPPORTED ); } diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php index 1c27f97842..d24910fa32 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Cron.php @@ -110,7 +110,7 @@ class Phergie_Plugin_Cron extends Phergie_Plugin_Abstract */ public function onTick() { - $now = time(); + $time = time(); foreach ($this->callbacks as $key => &$callback) { $callbackString = $this->getCallbackString($callback); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php index 335c10f881..c3086587be 100755 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Handler.php @@ -98,6 +98,12 @@ class Phergie_Plugin_Handler implements IteratorAggregate, Countable $this->paths = array(); $this->autoload = false; + if (!empty($config['plugins.paths'])) { + foreach ($config['plugins.paths'] as $dir => $prefix) { + $this->addPath($dir, $prefix); + } + } + $this->addPath(dirname(__FILE__), 'Phergie_Plugin_'); } @@ -134,6 +140,7 @@ class Phergie_Plugin_Handler implements IteratorAggregate, Countable * Returns metadata corresponding to a specified plugin. * * @param string $plugin Short name of the plugin class + * * @throws Phergie_Plugin_Exception Class file can't be found * * @return array|boolean Associative array containing the path to the @@ -142,7 +149,7 @@ class Phergie_Plugin_Handler implements IteratorAggregate, Countable */ public function getPluginInfo($plugin) { - foreach (array_reverse($this->paths) as $path) { + foreach (array_reverse($this->paths) as $path) { $file = $path['path'] . $plugin . '.php'; if (file_exists($file)) { $path = array( @@ -444,15 +451,21 @@ class Phergie_Plugin_Handler implements IteratorAggregate, Countable $valid = true; try { + $error_reporting = error_reporting(0); // ignore autoloader errors $r = new ReflectionClass($class); - $valid = $r->isSubclassOf('FilterIterator'); + error_reporting($error_reporting); + if (!$r->isSubclassOf('FilterIterator')) { + $message = 'Class ' . $class . ' is not a subclass of FilterIterator'; + $valid = false; + } } catch (ReflectionException $e) { + $message = $e->getMessage(); $valid = false; } if (!$valid) { throw new Phergie_Plugin_Exception( - $e->getMessage(), + $message, Phergie_Plugin_Exception::ERR_INVALID_ITERATOR_CLASS ); } diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php index e6227be4a5..27b4a087d3 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Karma.php @@ -31,7 +31,6 @@ * @uses extension PDO * @uses extension pdo_sqlite * @uses Phergie_Plugin_Command pear.phergie.org - * @uses Phergie_Plugin_Message pear.phergie.org */ class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract { @@ -94,11 +93,16 @@ class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract { $plugins = $this->getPluginHandler(); $plugins->getPlugin('Command'); - $plugins->getPlugin('Message'); - - $file = dirname(__FILE__) . '/Karma/karma.db'; - $this->db = new PDO('sqlite:' . $file); + $this->getDb(); + } + /** + * Initializes prepared statements used by the plugin. + * + * @return void + */ + protected function initializePreparedStatements() + { $this->fetchKarma = $this->db->prepare(' SELECT karma FROM karmas @@ -139,6 +143,36 @@ class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract '); } + /** + * Returns a connection to the plugin database, initializing one if none + * is explicitly set. + * + * @return PDO Database connection + */ + public function getDb() + { + if (empty($this->db)) { + $this->db = new PDO('sqlite:' . dirname(__FILE__) . '/Karma/karma.db'); + $this->initializePreparedStatements(); + } + return $this->db; + } + + /** + * Sets the connection to the plugin database, mainly intended for unit + * testing. + * + * @param PDO $db Database connection + * + * @return Phergie_Plugin_Karma Provides a fluent interface + */ + public function setDb(PDO $db) + { + $this->db = $db; + $this->initializePreparedStatements(); + return $this; + } + /** * Get the canonical form of a given term. * @@ -228,15 +262,11 @@ REGEX; $source = $this->getEvent()->getSource(); $nick = $this->getEvent()->getNick(); - if (empty($term)) { - return; - } - $canonicalTerm = $this->getCanonicalTerm($term); $fixedKarma = $this->fetchFixedKarma($canonicalTerm); if ($fixedKarma) { - $message = $nick . ': ' . $term . ' ' . $fixedKarma . '.'; + $message = $nick . ': ' . $term . ' ' . $fixedKarma; $this->doPrivmsg($source, $message); return; } @@ -302,33 +332,29 @@ REGEX; $fixedKarma0 = $this->fetchFixedKarma($canonicalTerm0); $fixedKarma1 = $this->fetchFixedKarma($canonicalTerm1); - if ($fixedKarma0 - || $fixedKarma1 - || empty($canonicalTerm0) - || empty($canonicalTerm1) - ) { + if ($fixedKarma0 || $fixedKarma1) { return; } if ($canonicalTerm0 == 'everything') { $change = $method == '<' ? '++' : '--'; - $this->modifyKarma($canonicalTerm1, $change); $karma0 = 0; - $karma1 = $this->fetchKarma($canonicalTerm1); + $karma1 = $this->modifyKarma($canonicalTerm1, $change); } elseif ($canonicalTerm1 == 'everything') { $change = $method == '<' ? '--' : '++'; - $this->modifyKarma($canonicalTerm0, $change); - $karma0 = $this->fetchKarma($canonicalTerm1); + $karma0 = $this->modifyKarma($canonicalTerm0, $change); $karma1 = 0; } else { $karma0 = $this->fetchKarma($canonicalTerm0); $karma1 = $this->fetchKarma($canonicalTerm1); } - if (($method == '<' - && $karma0 < $karma1) - || ($method == '>' - && $karma0 > $karma1)) { + // Combining the first and second branches here causes an odd + // single-line lapse in code coverage, but the lapse disappears if + // they're separated + if ($method == '<' && $karma0 < $karma1) { + $replies = $this->fetchPositiveAnswer; + } elseif ($method == '>' && $karma0 > $karma1) { $replies = $this->fetchPositiveAnswer; } else { $replies = $this->fetchNegativeAnswer; @@ -356,14 +382,10 @@ REGEX; * @param string $term Term to modify * @param string $action Karma action (either ++ or --) * - * @return void + * @return int Modified karma rating */ protected function modifyKarma($term, $action) { - if (empty($term)) { - return; - } - $karma = $this->fetchKarma($term); if ($karma !== false) { $statement = $this->updateKarma; @@ -378,6 +400,8 @@ REGEX; ':karma' => $karma ); $statement->execute($args); + + return $karma; } /** diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php index 4dfbb30dee..af8fc7287b 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Message.php @@ -52,14 +52,14 @@ class Phergie_Plugin_Message extends Phergie_Plugin_Abstract $}ix REGEX; - return !$event->isInChannel() + return !$event->isInChannel() || preg_match($targetPattern, $event->getText()) > 0; } /** * Allow for prefix and bot name aware extraction of a message * - * @return string|bool $message The message, which is possibly targeted at the + * @return string|bool $message The message, which is possibly targeted at the * bot or false if a prefix requirement failed */ public function getMessage() diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php index 90e3adcd52..4305770812 100755 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/Reload.php @@ -54,10 +54,27 @@ class Phergie_Plugin_Reload extends Phergie_Plugin_Abstract { $plugin = ucfirst($plugin); + $evalClass = true; + if (strpos($plugin, ' ') !== false) { + $args = explode(' ', $plugin); + $plugin = $args[0]; + if (strtolower($args[1]) == 'force') { + $evalClass = false; + } + } + if (!$this->plugins->hasPlugin($plugin)) { echo 'DEBUG(Reload): ' . ucfirst($plugin) . ' is not loaded yet, loading', PHP_EOL; - $this->plugins->getPlugin($plugin); - $this->plugins->command->populateMethodCache(); + try { + $this->plugins->getPlugin($plugin); + $this->plugins->command->populateMethodCache(); + } catch (Phergie_Plugin_Exception $e) { + if ($e->getCode() == Phergie_Plugin_Exception::ERR_CLASS_NOT_FOUND) { + echo 'DEBUG(Reload): ', $e->getMessage(), PHP_EOL; + } else { + throw $e; + } + } return; } @@ -75,17 +92,19 @@ class Phergie_Plugin_Reload extends Phergie_Plugin_Abstract $newClass = $class . '_' . sha1($contents); if (class_exists($newClass, false)) { - echo 'DEBUG(Reload): Class ', $class, ' has not changed since last reload', PHP_EOL; - return; + if ($evalClass == true) { + echo 'DEBUG(Reload): Class ', $class, ' has not changed since last reload', PHP_EOL; + return; + } + } else { + $contents = preg_replace( + array('/^<\?(?:php)?/', '/class\s+' . $class . '/i'), + array('', 'class ' . $newClass), + $contents + ); + eval($contents); } - $contents = preg_replace( - array('/^<\?(?:php)?/', '/class\s+' . $class . '/i'), - array('', 'class ' . $newClass), - $contents - ); - eval($contents); - $instance = new $newClass; $instance->setName($plugin); $instance->setEvent($this->event); diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php index 33240829d8..b731cffc87 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/SpellCheck.php @@ -1,6 +1,6 @@ * @copyright 2008-2010 Phergie Development Team (http://phergie.org) @@ -24,7 +24,7 @@ * either confirmation of correctly spelled words or potential correct * spellings for misspelled words. * - * @category Phergie + * @category Phergie * @package Phergie_Plugin_SpellCheck * @author Phergie Development Team * @license http://phergie.org/license New BSD License @@ -34,7 +34,6 @@ */ class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract { - /** * Spell check dictionary handler * @@ -65,7 +64,7 @@ class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract } $this->plugins->getPlugin('Command'); - + set_error_handler(array($this, 'loadDictionaryError')); $this->pspell = pspell_new($this->getConfig('spellcheck.lang')); restore_error_handler(); @@ -86,11 +85,11 @@ class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract $target = $this->event->getNick(); $message = $target . ': The word "' . $word; - $message .= '" seems to be spelt correctly.'; + $message .= '" seems to be spelled correctly.'; if (!pspell_check($this->pspell, $word)) { $suggestions = pspell_suggest($this->pspell, $word); - - $message = $target; + + $message = $target; $message .= ': I could not find any suggestions for "' . $word . '".'; if (!empty($suggestions)) { $suggestions = array_splice($suggestions, 0, $this->limit); @@ -98,7 +97,7 @@ class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract $message .= $word . '": ' . implode(', ', $suggestions) . '.'; } } - + $this->doPrivmsg($source, $message); } @@ -116,5 +115,4 @@ class Phergie_Plugin_SpellCheck extends Phergie_Plugin_Abstract { $this->fail($errstr); } - } diff --git a/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php b/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php index 611dfc9ef1..246cfc3986 100644 --- a/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php +++ b/plugins/Irc/extlib/phergie/Phergie/Plugin/TerryChay.php @@ -1,6 +1,6 @@ * @copyright 2008-2010 Phergie Development Team (http://phergie.org) @@ -21,9 +21,9 @@ /** * Parses incoming messages for the words "Terry Chay" or tychay and responds - * with a random Terry fact retrieved from the Chayism web service. + * with a random Terry fact retrieved from the Chayism web service. * - * @category Phergie + * @category Phergie * @package Phergie_Plugin_TerryChay * @author Phergie Development Team * @license http://phergie.org/license New BSD License @@ -53,21 +53,25 @@ class Phergie_Plugin_TerryChay extends Phergie_Plugin_Abstract */ public function onLoad() { - $this->http = $this->getPluginHandler()->getPlugin('Http'); + $this->getPluginHandler()->getPlugin('Http'); } /** * Fetches a chayism. * - * @return string|bool Fetched chayism or FALSE if the operation failed + * @return string|bool Fetched chayism or FALSE if the operation failed */ public function getChayism() { - return $this->http->get(self::URL)->getContent(); + return $this + ->getPluginHandler() + ->getPlugin('Http') + ->get(self::URL) + ->getContent(); } /** - * Parses incoming messages for "Terry Chay" and related variations and + * Parses incoming messages for "Terry Chay" and related variations and * responds with a chayism. * * @return void @@ -77,33 +81,14 @@ class Phergie_Plugin_TerryChay extends Phergie_Plugin_Abstract $event = $this->getEvent(); $source = $event->getSource(); $message = $event->getText(); - $pattern - = '{^(' . preg_quote($this->getConfig('command.prefix')) . + $pattern + = '{^(' . preg_quote($this->getConfig('command.prefix')) . '\s*)?.*(terry\s+chay|tychay)}ix'; - if (preg_match($pattern, $message) - && $fact = $this->getChayism() - ) { - $this->doPrivmsg($source, 'Fact: ' . $fact); - } - } - - /** - * Parses incoming CTCP request for "Terry Chay" and related variations - * and responds with a chayism. - * - * @return void - */ - public function onCtcp() - { - $event = $this->getEvent(); - $source = $event->getSource(); - $ctcp = $event->getArgument(1); - - if (preg_match('({terry[\s_+-]*chay}|tychay)ix', $ctcp) - && $fact = $this->getChayism() - ) { - $this->doCtcpReply($source, 'TERRYCHAY', $fact); + if (preg_match($pattern, $message)) { + if($fact = $this->getChayism()) { + $this->doPrivmsg($source, 'Fact: ' . $fact); + } } } } diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php new file mode 100644 index 0000000000..ba94cd0ea6 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/ConnectionTest.php @@ -0,0 +1,262 @@ + + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Connection. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_ConnectionTest extends PHPUnit_Framework_TestCase +{ + /** + * Associative array containing an option-to-value mapping + * + * @var array + */ + private $options = array( + 'host' => 'example.com', + 'port' => 4080, + 'transport' => 'udp', + 'encoding' => 'ASCII', + 'nick' => 'MyNick', + 'username' => 'MyUsername', + 'realname' => 'MyRealName', + 'password' => 'MyPassword', + ); + + /** + * Data provider for testGetOptionReturnsDefault(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetOptionReturnsDefault() + */ + public function dataProviderTestGetOptionReturnsDefault() + { + return array( + array('transport', 'tcp'), + array('encoding', 'ISO-8859-1'), + array('port', 6667), + array('password', null), + ); + } + + /** + * Tests that a default values are used for some options. + * + * @param string $option Name of the option with a default value + * @param mixed $value Default value of the option + * + * @return void + * @dataProvider dataProviderTestGetOptionReturnsDefault + */ + public function testGetOptionReturnsDefault($option, $value) + { + $connection = new Phergie_Connection; + $this->assertEquals($value, $connection->{'get' . ucfirst($option)}()); + } + + /** + * Tests that a default encoding is used if one isn't specified. + * + * @return void + */ + public function testGetEncodingReturnsDefault() + { + $connection = new Phergie_Connection; + $this->assertEquals('ISO-8859-1', $connection->getEncoding()); + } + + /** + * Tests that options can be set via the constructor. + * + * @return void + */ + public function testSetOptionsViaConstructor() + { + $connection = new Phergie_Connection($this->options); + foreach ($this->options as $key => $value) { + $this->assertEquals($value, $connection->{'get' . ucfirst($key)}()); + } + } + + /** + * Data provider for testGetHostmaskMissingDataGeneratesException(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetHostmaskMissingDataGeneratesException() + */ + public function dataProviderTestGetHostmaskMissingDataGeneratesException() + { + return array( + array(null, $this->options['username'], $this->options['host']), + array($this->options['nick'], null, $this->options['host']), + array($this->options['nick'], $this->options['username'], null), + ); + } + + /** + * Tests that attempting to retrieve a hostmask without option values + * for all of its constituents generates an exception. + * + * @param string $nick Bot nick + * @param string $username Bot username + * @param string $host Server hostname + * + * @return void + * @dataProvider dataProviderTestGetHostmaskMissingDataGeneratesException + */ + public function testGetHostmaskMissingDataGeneratesException($nick, $username, $host) + { + $options = array( + 'nick' => $nick, + 'username' => $username, + 'host' => $host, + ); + + $connection = new Phergie_Connection($options); + + try { + $hostmask = $connection->getHostmask(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to retrieve a hostmask with all required + * options is successful. + * + * @return void + */ + public function testGetHostmaskWithValidData() + { + $options = array( + 'nick' => 'MyNick', + 'username' => 'MyUsername', + 'host' => 'example.com' + ); + + $connection = new Phergie_Connection($options); + $hostmask = $connection->getHostmask(); + $this->assertType('Phergie_Hostmask', $hostmask); + } + + /** + * Data provider for testGetRequiredOptionsWithoutValuesSet(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameters for a single call to + * testGetRequiredOptionsWithoutValuesSet() + */ + public function dataProviderTestGetRequiredOptionsWithoutValuesSet() + { + return array( + array('host'), + array('nick'), + array('username'), + array('realname'), + ); + } + + /** + * Tests that attempting to retrieve values of required options when no + * values are set results in an exception. + * + * @param string $option Option name + * + * @return void + * @dataProvider dataProviderTestGetRequiredOptionsWithoutValuesSet + */ + public function testGetRequiredOptionsWithoutValuesSet($option) + { + try { + $connection = new Phergie_Connection; + $value = $connection->{'get' . ucfirst($option)}(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to set an invalid value for the transport + * results in an exception. + * + * @return void + */ + public function testSetTransportWithInvalidValue() + { + $connection = new Phergie_Connection; + try { + $connection->setTransport('blah'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that attempting to set an invalid value for the encoding + * results in an exception. + * + * @return void + */ + public function testSetEncodingWithInvalidValue() + { + $connection = new Phergie_Connection; + try { + $connection->setEncoding('blah'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Connection_Exception $e) { + return; + } catch (Exception $e) { + $this->fail('Unexpected exception was thrown'); + } + } + + /** + * Tests that options can be set collectively after the connection is + * instantiated. + * + * @return void + */ + public function testSetOptions() + { + $connection = new Phergie_Connection; + $connection->setOptions($this->options); + foreach ($this->options as $key => $value) { + $this->assertEquals($value, $connection->{'get' . ucfirst($key)}()); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php index dcf52a65a1..9ecdd327ae 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/HandlerTest.php @@ -108,6 +108,77 @@ class Phergie_Plugin_HandlerTest extends PHPUnit_Framework_TestCase ); } + /** + * Tests that a default iterator is returned if none is explicitly set. + * + * @return void + */ + public function testGetIteratorReturnsDefault() + { + $this->assertType( + 'Phergie_Plugin_Iterator', + $this->handler->getIterator() + ); + } + + /** + * Tests the ability to change the handler's iterator class when a valid + * class is specified. + * + * @return void + */ + public function testSetIteratorClassWithValidClass() + { + eval(' + class DummyIterator extends FilterIterator { + public function accept() { + return true; + } + } + '); + + $this->handler->setIteratorClass('DummyIterator'); + + $this->assertType( + 'DummyIterator', + $this->handler->getIterator() + ); + } + + /** + * Tests that a failure occurs when a nonexistent iterator class is + * specified. + * + * @return void + */ + public function testSetIteratorClassWithNonexistentClass() + { + try { + $this->handler->setIteratorClass('FooIterator'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Tests that a failure occurs when a class that is not a subclass of + * FilterIterator is specified. + * + * @return void + */ + public function testSetIteratorClassWithNonFilterIteratorClass() + { + try { + $this->handler->setIteratorClass('ArrayIterator'); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + /** * Tests countability of the plugin handler. * @@ -714,23 +785,53 @@ class Phergie_Plugin_HandlerTest extends PHPUnit_Framework_TestCase } /** - * Tests the plugin receiving and using a predefined iterator instance. + * Tests that multiple plugin iterators can be used concurrently. * - * @depends testGetPlugins * @return void */ - public function testSetIterator() + public function testUseMultiplePluginIteratorsConcurrently() { - $plugin = $this->getMockPlugin('TestPlugin'); - $this->handler->addPlugin($plugin); - $plugins = $this->handler->getPlugins(); - $iterator = new ArrayIterator($plugins); - $this->handler->setIterator($iterator); - $this->assertSame($this->handler->getIterator(), $iterator); - $iterated = array(); - foreach ($this->handler as $plugin) { - $iterated[strtolower($plugin->getName())] = $plugin; - } - $this->assertEquals($iterated, $plugins); + $plugin1 = $this->getMockPlugin('TestPlugin1'); + $this->handler->addPlugin($plugin1); + + $plugin2 = $this->getMockPlugin('TestPlugin2'); + $this->handler->addPlugin($plugin2); + + $iterator1 = $this->handler->getIterator(); + $iterator1->next(); + $this->assertSame($plugin2, $iterator1->current()); + + $iterator2 = $this->handler->getIterator(); + $this->assertSame($plugin1, $iterator2->current()); + } + + /** + * Tests adding plugin paths via configuration. + * + * @return void + */ + public function testAddPluginPathsViaConfiguration() + { + $dir = dirname(__FILE__); + $prefix = 'Phergie_Plugin_'; + $paths = array($dir => $prefix); + $this->config + ->expects($this->any()) + ->method('offsetExists') + ->will($this->returnValue(true)); + $this->config + ->expects($this->any()) + ->method('offsetGet') + ->will($this->returnValue($paths)); + + // Reinitialize the handler so the configuration change takes effect + // within the constructor + $this->handler = new Phergie_Plugin_Handler( + $this->config, + $this->events + ); + + $this->handler->setAutoload(true); + $this->handler->getPlugin('Mock'); } } diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php new file mode 100644 index 0000000000..6b72316743 --- /dev/null +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/KarmaTest.php @@ -0,0 +1,335 @@ + + * @copyright 2008-2010 Phergie Development Team (http://phergie.org) + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ + +/** + * Unit test suite for Pherge_Plugin_Karma. + * + * @category Phergie + * @package Phergie_Tests + * @author Phergie Development Team + * @license http://phergie.org/license New BSD License + * @link http://pear.phergie.org/package/Phergie_Tests + */ +class Phergie_Plugin_KarmaTest extends Phergie_Plugin_TestCase +{ + /** + * Skips tests if the SQLite PDO driver is not available. + * + * @return void + */ + public function setUp() + { + if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { + $this->markTestSkipped('PDO or pdo_sqlite extension is required'); + } + + parent::setUp(); + } + + /** + * Configures the plugin to use a temporary copy of the database. + * + * @return PDO Connection to the temporary database + */ + private function createMockDatabase() + { + $dbPath = $this->getPluginsPath('Karma/karma.db'); + $db = $this->getMockDatabase($dbPath); + $this->plugin->setDb($db); + return $db; + } + + /** + * Tests the requirement of the Command plugin. + * + * @return void + */ + public function testRequiresCommandPlugin() + { + $this->assertRequiresPlugin('Command'); + $this->plugin->onLoad(); + } + + /** + * Initiates a karma event with a specified term. + * + * @param string $term Karma term + * + * @return Phergie_Event_Request Initiated mock event + */ + private function initiateKarmaEvent($term) + { + $args = array( + 'receiver' => $this->source, + 'text' => 'karma ' . $term + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + return $event; + } + + /** + * Checks for an expected karma response. + * + * @param Phergie_Event_Request $event Event containing the karma + * request + * @param string $term Karma term + * @param string $response Portion of the response + * message following the term + * from the original event + * + * @return void + */ + private function checkForKarmaResponse($event, $term, $response) + { + $text = $event->getNick() . ': ' . $response; + $this->assertEmitsEvent('privmsg', array($event->getSource(), $text)); + $this->plugin->onCommandKarma($term); + } + + /** + * Tests that a default database is used when none is specified. + * + * @return void + */ + public function testGetDb() + { + $db = $this->plugin->getDb(); + $this->assertType('PDO', $db); + } + + /** + * Tests specifying a custom database for the plugin to use. + * + * @return void + */ + public function testSetDb() + { + $db = $this->createMockDatabase(); + $this->assertSame($db, $this->plugin->getDb()); + } + + /** + * Tests that issuing the karma command with an unknown term returns a + * neutral rating. + * + * @return void + */ + public function testKarmaCommandOnUnknownTerm() + { + $term = 'foo'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has neutral karma.'); + } + + /** + * Tests that issuing the karma command with the term "me" returns the + * the karma rating for the initiating user. + * + * @return void + */ + public function testKarmaCommandOnUser() + { + $term = 'me'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, 'You have neutral karma.'); + } + + /** + * Tests that issuing the karma command with a term that has a fixed + * karma rating results in that rating being returned. + * + * @return void + */ + public function testKarmaCommandWithFixedKarmaTerm() + { + $term = 'phergie'; + $this->createMockDatabase(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, 'phergie has karma of awesome.'); + } + + /** + * Supporting method that tests the result of a karma term rating change. + * + * @param string $term Karma term for which the rating is being + * changed + * @param string $operation ++ or -- + * @param int $karma Expected karma rating after the change is + * applied + */ + private function checkForKarmaRatingChange($term, $operation, $karma) + { + $args = array( + 'receiver' => $this->source, + 'text' => $term . $operation + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->plugin->onPrivmsg(); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has karma of ' . $karma . '.'); + } + + /** + * Tests incrementing the karma rating of a new term. + * + * @return void + */ + public function testIncrementingKarmaRating() + { + $this->createMockDatabase(); + $this->checkForKarmaRatingChange('foo', '++', 1); + } + + /** + * Tests decrementing the karma rating of a new term. + * + * @return void + */ + public function testDecrementingKarmaRating() + { + $this->createMockDatabase(); + $this->checkForKarmaRatingChange('foo', '--', -1); + } + + /** + * Tests modifying the karma rating of an existing term. + * + * @return void + */ + public function testChangingExistingKarmaRating() + { + $term = 'foo'; + $this->createMockDatabase(); + $this->checkForKarmaRatingChange($term, '++', 1); + $this->checkForKarmaRatingChange($term, '++', 2); + } + + /** + * Tests resetting the karma rating of an existing term to 0. + * + * @return void + */ + public function testResettingExistingKarmaRating() + { + $term = 'foo'; + $this->createMockDatabase(); + $this->checkForKarmaRatingChange($term, '++', 1); + $this->plugin->onCommandReincarnate($term); + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has neutral karma.'); + } + + /** + * Data provider for testKarmaComparisons(). + * + * @return array Enumerated array of enumerated arrays each containing a + * set of parameter values for a single call to + * testKarmaComparisons() + */ + public function dataProviderTestKarmaComparisons() + { + $term1 = 'foo'; + $term2 = 'bar'; + + $positive = 'True that.'; + $negative = 'No sir, not at all.'; + + return array( + array($term1, $term2, 1, 0, '>', $positive), + array($term1, $term2, 0, 1, '>', $negative), + array($term1, $term2, 1, 1, '>', $negative), + array($term1, $term2, 1, 0, '<', $negative), + array($term1, $term2, 0, 1, '<', $positive), + array($term1, $term2, 1, 1, '<', $negative), + array($term1, 'phergie', 1, 0, '>', $positive), + array('phergie', $term2, 0, 1, '<', $positive), + array($term1, 'everything', 0, 0, '>', $positive), + array('everything', $term2, 0, 0, '>', $positive), + ); + } + + /** + * Tests comparing the karma ratings of two terms. + * + * @param string $term1 First term + * @param string $term2 Second term + * @param int $karma1 Karma rating of the first time, 0 or 1 + * @param int $karma2 Karma rating of the second term, 0 or 1 + * @param string $operator Comparison operator, > or < + * @param string $response Response to check for + * + * @return void + * @dataProvider dataProviderTestKarmaComparisons + */ + public function testKarmaComparisons($term1, $term2, $karma1, $karma2, + $operator, $response + ) { + $db = $this->createMockDatabase(); + + // Reduce answer tables to expected response + $stmt = $db->prepare('DELETE FROM positive_answers WHERE answer != ?'); + $stmt->execute(array($response)); + $stmt = $db->prepare('DELETE FROM negative_answers WHERE answer != ?'); + $stmt->execute(array($response)); + + if ($karma1) { + $this->checkForKarmaRatingChange($term1, '++', 1); + } + + if ($karma2) { + $this->checkForKarmaRatingChange($term2, '++', 1); + } + + $args = array( + 'receiver' => $this->source, + 'text' => $term1 . ' ' . $operator . ' ' . $term2 + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + + // Test lack of a response for terms with fixed karma ratings + if ($term1 == 'phergie' || $term2 == 'phergie') { + $callback = 'assertDoesNotEmitEvent'; + } else { + $callback = 'assertEmitsEvent'; + } + + $this->$callback('privmsg', array($event->getSource(), $response)); + $this->plugin->onPrivmsg(); + + // Test for karma changes when one term is "everything" + if ($term1 == 'everything' || $term2 == 'everything') { + if ($term1 == 'everything') { + $term = $term2; + $karma = ($operator == '>') ? -1 : 1; + } else { + $term = $term1; + $karma = ($operator == '>') ? 1 : -1; + } + $event = $this->initiateKarmaEvent($term); + $this->checkForKarmaResponse($event, $term, $term . ' has karma of ' . $karma . '.'); + } + } +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php index b9c2dde3d4..ac30d46ffe 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PingTest.php @@ -12,15 +12,13 @@ * http://phergie.org/license * * @category Phergie - * @package Phergie + * @package Phergie_Tests * @author Phergie Development Team * @copyright 2008-2010 Phergie Development Team (http://phergie.org) * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ -require_once(dirname(__FILE__) . '/TestCase.php'); - /** * Unit test suite for Pherge_Plugin_Ping. * @@ -28,148 +26,139 @@ require_once(dirname(__FILE__) . '/TestCase.php'); * @package Phergie_Tests * @author Phergie Development Team * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ class Phergie_Plugin_PingTest extends Phergie_Plugin_TestCase { - protected $config = array('ping.ping' => 10, - 'ping.event' => 300); - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - $this->setPlugin(new Phergie_Plugin_Ping); - } - - /** - * Test the lastEvent setter and getter - */ - public function testSetGetLastEvent() - { - $expected = rand(100000,200000); - $this->plugin->setLastEvent($expected); - $this->assertEquals($expected, - $this->plugin->getLastEvent(), - 'Assert that the last event was set and gotten ' . - 'correctly'); - } - - /** - * Test the lastPing setter and getter - */ - public function testSetGetLastPing() - { - - $expected = rand(100000,200000); - $this->plugin->setLastPing($expected); - $this->assertEquals($expected, - $this->plugin->getLastPing(), - 'Assert that the last ping was set and gotten ' . - 'correctly'); - } - - /** - * Tests the onConnect hook + * Tests that the last ping and event are initialized on connection to + * the server. + * + * @return void */ public function testOnConnect() { - $time = time() - 1; - // We need to make sure time() is going to be creater next time it is called - $this->plugin->onConnect(); - $this->assertNull($this->plugin->getLastPing(), - 'onConnect should set last ping to null'); - $this->assertGreaterThan($time, - $this->plugin->getLastEvent(), - 'onConnect should update lastEvent with the ' . - 'current timestamp'); - $this->assertLessThan($time + 2, - $this->plugin->getLastEvent(), - 'onConnect should update lastEvent with the ' . - 'current timestamp'); + + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + $expected = null; + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); } /** - * Test that the preEvent method updates the lastEvent with the current time + * Tests that the last event is reset when an event occurs. + * + * @return void */ public function testPreEvent() { - $time = time() -1; $this->plugin->preEvent(); - $this->assertGreaterThan($time, - $this->plugin->getLastEvent(), - 'Last event time was set properly on preEvent'); - $this->assertLessThan($time +2, - $this->plugin->getLastEvent(), - 'Last Event time was set properly on preEvent'); + + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); } /** - * @todo Implement testOnPingResponse(). + * Tests that the last ping is reset when a ping is received. + * + * @return void */ public function testOnPingResponse() { - $this->plugin->setLastPing(time()); $this->plugin->onPingResponse(); - $this->assertNull($this->plugin->getLastPing(), - 'Last ping time should be null after onPingResponse'); + $expected = null; + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); } /** - * Test that the plugin issues a quit when the ping threashold - * has been exceeded + * Tests that the test suite is able to manipulate the value of the last + * event. + * + * @return void */ - public function testOnTickExceededPingThresholdQuits() + public function testSetLastEvent() { - $this->plugin->setLastPing(1); - $this->plugin->onTick(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_QUIT); - } - - /** - * Test that the plugin issues a quit when the ping threashold - * has been exceeded - */ - public function testOnTickPingWithinThresholdDoesNotQuits() - { - $this->plugin->setLastPing(time()); - $this->plugin->onTick(); - $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_QUIT); + $expected = time() + 1; + $this->plugin->setLastEvent($expected); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + $this->plugin->setLastEvent(); + $expected = time(); + $actual = $this->plugin->getLastEvent(); + $this->assertEquals($expected, $actual); + + try { + $this->plugin->setLastEvent('foo'); + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { } } /** - * Test that a ping is emitted when the event threashold is exceeded + * Tests that the test suite is able to manipulate the value of the last + * ping. + * + * @return void */ - public function testPingEmittedAfterThresholdExceeded() + public function testSetLastPing() { - $this->plugin->setLastEvent(time() - $this->config['ping.event'] - 1); - $this->plugin->onTick(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_PING); - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PING); - foreach ($events as $event) { - $this->assertEventEmitter($event, - $this->plugin, - 'Assert that the event was emitted by the tested plugin'); - } + $expected = time() + 1; + $this->plugin->setLastPing($expected); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + + $this->plugin->setLastPing(); + $expected = time(); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); + + try { + $this->plugin->setLastPing('foo'); + $this->fail('Expected exception was not thrown'); + } catch (Exception $e) { } } /** - * Test that no ping is emitted when the event thresthold is not exceeded + * Tests that a ping event is sent after the appropriate time period has + * lapsed since receiving an event. + * + * @depends testSetLastEvent + * @return void */ - public function testNoPingEmittedWhenThresholdNotExceeded() + public function testPing() { - $this->plugin->setLastEvent(time() - $this->config['ping.event'] +1); + $pingEvent = 10; + $this->setConfig('ping.event', $pingEvent); + $lastEvent = time() - ($pingEvent + 1); + $this->plugin->setLastEvent($lastEvent); + $expected = time(); + $this->assertEmitsEvent('ping', array($this->nick, $expected)); $this->plugin->onTick(); - $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PING); + $actual = $this->plugin->getLastPing(); + $this->assertEquals($expected, $actual); } - public function tearDown() + /** + * Tests that a quit event is sent after the appropriate time period has + * lapsed since sending a ping event. + * + * @depends testPing + * @return void + */ + public function testQuit() { - $this->handler->clearEvents(); + $pingPing = 10; + $this->setConfig('ping.ping', $pingPing); + $lastPing = time() - ($pingPing + 1); + $this->plugin->setLastPing($lastPing); + $this->assertEmitsEvent('quit'); + $this->plugin->onTick(); } - -} \ No newline at end of file +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php index a8bc8fd05f..e8351fef24 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/PongTest.php @@ -12,15 +12,13 @@ * http://phergie.org/license * * @category Phergie - * @package Phergie + * @package Phergie_Tests * @author Phergie Development Team * @copyright 2008-2010 Phergie Development Team (http://phergie.org) * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ -require_once(dirname(__FILE__) . '/TestCase.php'); - /** * Unit test suite for Pherge_Plugin_Pong. * @@ -28,47 +26,21 @@ require_once(dirname(__FILE__) . '/TestCase.php'); * @package Phergie_Tests * @author Phergie Development Team * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ class Phergie_Plugin_PongTest extends Phergie_Plugin_TestCase { /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() - { - $this->setPlugin(new Phergie_Plugin_Pong); - } - - /** - * Test that when a ping is received, a Phergie_Event_Command::TYPE_PONG - * is set to the handler + * Test that a pong event is sent when a ping event is received. * - * @event Phergie_Event_Command::TYPE_PING + * @return void */ - public function testOnPing() + public function testPong() { + $expected = 'irc.freenode.net'; + $event = $this->getMockEvent('ping', array($expected)); + $this->plugin->setEvent($event); + $this->assertEmitsEvent('pong', array($expected)); $this->plugin->onPing(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_PONG); } - - /** - * Test that when a ping is received, a Phergie_Event_Command::TYPE_PONG - * is set to the handler - * - * @event Phergie_Event_Command::TYPE_PING - */ - public function testOnPingResponseArguement() - { - $this->plugin->onPing(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_PONG); - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PONG); - $this->assertTrue(count($events) === 1, 'Assert that only one pong is emitted'); - $this->assertEventEmitter(current($events), - $this->plugin, - 'Assert that the tested plugin emitted the event'); - - } - } diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php index 8ed9f0d36d..369a0c6444 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/SpellCheckTest.php @@ -12,15 +12,13 @@ * http://phergie.org/license * * @category Phergie - * @package Phergie + * @package Phergie_Tests * @author Phergie Development Team * @copyright 2008-2010 Phergie Development Team (http://phergie.org) * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ -require_once dirname(__FILE__) . '/TestCase.php'; - /** * Unit test suite for Pherge_Plugin_SpellCheck. * @@ -28,178 +26,141 @@ require_once dirname(__FILE__) . '/TestCase.php'; * @package Phergie_Tests * @author Phergie Development Team * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ class Phergie_Plugin_SpellCheckTest extends Phergie_Plugin_TestCase { - /** - * Current SpellCheck plugin instance + * Checks for the pspell extension. * - * @var Phergie_Plugin_SpellCheck - */ - protected $spell; - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - * * @return void */ - protected function setUp() + public function setUp() { - $this->config = array('spellcheck.lang' => 'en'); + parent::setUp(); - $this->spell = new Phergie_Plugin_SpellCheck(); - $this->setPlugin(new Phergie_Plugin_Command()); - - $config = $this->plugin->getConfig(); - - $handler = new Phergie_Plugin_Handler($config, $this->handler); - $this->plugin->setPluginHandler($handler); - - $handler->addPlugin($this->plugin); - $handler->addPlugin($this->spell); - - $this->spell->setEventHandler($this->handler); - $this->spell->setConnection($this->connection); - } - - /** - * @event Phergie_Event_Request::privmsg - * @eventArg #zftalk - * @eventArg spell - */ - public function testSpell() - { - $this->spell->onLoad(); - - $this->copyEvent(); - $this->plugin->onPrivMsg(); - $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG); - } - - /** - * @event Phergie_Event_Request::privmsg - * @eventArg #phergie - * @eventArg spell test - */ - public function testSpellTest() - { - $this->spell->onLoad(); - - $this->copyEvent(); - $this->plugin->onPrivMsg(); - - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG); - - $this->assertEquals(1, count($events)); - foreach ($events as $event) { - $args = $event->getArguments(); - - $this->assertEquals('#phergie', $args[0]); - - $this->assertContains('CheckSpellUser:', $args[1]); - $this->assertContains('test', $args[1]); - $this->assertContains('correct', $args[1]); - } - } - - /** - * @event Phergie_Event_Request::privmsg - * @eventArg #phergie - * @eventArg spell testz - */ - public function testSpellTestz() - { - $this->spell->onLoad(); - - $this->copyEvent(); - $this->plugin->onPrivMsg(); - - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG); - - $this->assertEquals(1, count($events)); - foreach ($events as $event) { - $args = $event->getArguments(); - - $this->assertEquals('#phergie', $args[0]); - - $this->assertContains('CheckSpellUser:', $args[1]); - $this->assertRegExp('/([a-z]+, ){4}/', $args[1]); - $this->assertContains('testz', $args[1]); - $this->assertContains('test,', $args[1]); + if (!extension_loaded('pspell')) { + $this->markTestSkipped('pspell extension not available'); } } /** - * @event Phergie_Event_Request::privmsg - * @eventArg #phergie - * @eventArg spell testz - */ - public function testSpellMoreSuggestions() - { - $config = $this->spell->getConfig(); - - $this->copyEvent(); - $config['spellcheck.limit'] = 6; - - $this->spell->onLoad(); - $this->plugin->onPrivMsg(); - - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG); - - $this->assertEquals(1, count($events)); - foreach ($events as $event) { - $args = $event->getArguments(); - - $this->assertEquals('#phergie', $args[0]); - - $this->assertContains('CheckSpellUser:', $args[1]); - $this->assertRegExp('/([a-z]+, ){5}/', $args[1]); - $this->assertContains('testz', $args[1]); - $this->assertContains('test,', $args[1]); - } - } - - /** - * @event Phergie_Event_Request::privmsg - * @eventArg #phergie - * @eventArg spell qwertyuiopasdfghjklzxcvbnm - */ - public function testSpellNoSuggestions() - { - $this->spell->onLoad(); - - $this->copyEvent(); - $this->plugin->onPrivMsg(); - - $events = $this->getResponseEvents(Phergie_Event_Command::TYPE_PRIVMSG); - - $this->assertEquals(1, count($events)); - foreach ($events as $event) { - $args = $event->getArguments(); - - $this->assertEquals('#phergie', $args[0]); - - $this->assertContains('CheckSpellUser:', $args[1]); - $this->assertContains('find any suggestions', $args[1]); - } - } - - /** - * Copy event from command to spell plugin - * + * Tests for the plugin failing to load when the language setting is not + * specified. + * * @return void */ - protected function copyEvent() + public function testLanguageSettingNotSet() { - $hostmask = Phergie_Hostmask::fromString('CheckSpellUser!test@testing.org'); - - $event = $this->plugin->getEvent(); - $event->setHostmask($hostmask); - - $this->spell->setEvent($event); + try { + $this->plugin->onLoad(); + $this->fail('Expected exception was not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); } + /** + * Tests for the plugin requiring the Command plugin as a dependency. + * + * @return void + */ + public function testRequiresCommandPlugin() + { + $this->setConfig('spellcheck.lang', 'en'); + $this->assertRequiresPlugin('Command'); + $this->plugin->onLoad(); + } + + /** + * Tests for the plugin failing to load because of a dictionary error. + * + * @return void + */ + public function testLoadDictionaryError() + { + $this->setConfig('spellcheck.lang', 'foo'); + try { + $this->plugin->onLoad(); + $this->fail('Expected exception not thrown'); + } catch (Phergie_Plugin_Exception $e) { + return; + } + $this->fail('Unexpected exception was thrown'); + } + + /** + * Initializes a spell check event. + * + * @param string $word Word to be checked + * + * @return void + */ + private function initializeSpellCheckEvent($word) + { + $this->setConfig('spellcheck.lang', 'en'); + $this->plugin->onLoad(); + $args = array( + 'receiver' => $this->source, + 'text' => 'spell ' . $word + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + } + + /** + * Checks for a specified response to a spell check event. + * + * @param string $word Work being checked + * @param string $response Expected response + * + * @return void + */ + private function checkForSpellCheckResponse($word, $response) + { + $this->assertEmitsEvent('privmsg', array($this->source, $response)); + $this->plugin->onCommandSpell($word); + } + + /** + * Tests for the plugin returning a response for a correctly spelled word. + * + * @return void + */ + public function testRespondsForCorrectlySpelledWord() + { + $word = 'test'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': The word "' . $word . '" seems to be spelled correctly.'; + $this->checkForSpellCheckResponse($word, $response); + } + + /** + * Tests for the plugin returning a response when it can't find any + * suggestions for a word. + * + * @return void + */ + public function testRespondsWithoutSuggestions() + { + $word = 'kjlfljlkjljkljlj'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': I could not find any suggestions for "' . $word . '".'; + $this->checkForSpellCheckResponse($word, $response); + } + + /** + * Tests for the plugin returning a response when it is able to find + * suggestions for a word. + * + * @return void + */ + public function testRespondsWithSuggestions() + { + $word = 'teh'; + $this->initializeSpellCheckEvent($word); + $response = $this->nick . ': Suggestions for "' . $word . '": the, Te, tech, Th, eh.'; + $this->checkForSpellCheckResponse($word, $response); + } } diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php index e76020b6b3..e58ac6f290 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TerryChayTest.php @@ -12,15 +12,13 @@ * http://phergie.org/license * * @category Phergie - * @package Phergie + * @package Phergie_Tests * @author Phergie Development Team * @copyright 2008-2010 Phergie Development Team (http://phergie.org) * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ -require_once(dirname(__FILE__) . '/TestCase.php'); - /** * Unit test suite for Pherge_Plugin_TerryChay. * @@ -28,72 +26,110 @@ require_once(dirname(__FILE__) . '/TestCase.php'); * @package Phergie_Tests * @author Phergie Development Team * @license http://phergie.org/license New BSD License - * @link http://pear.phergie.org/package/Phergie + * @link http://pear.phergie.org/package/Phergie_Tests */ class Phergie_Plugin_TerryChayTest extends Phergie_Plugin_TestCase { /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. + * Chayism used as a consistent response when related events are + * triggered + * + * @var string */ - protected function setUp() + private $chayism = 'Terry Chay doesn\'t need a framework; he already knows everyone\'s code'; + + /** + * Configures the mock plugin handler to return a mock Http plugin with + * a mock response object populated with predetermined content. + * + * @return void + */ + public function setUpHttpClient() { - $this->setPlugin(new Phergie_Plugin_TerryChay()); - $config = new Phergie_Config(); - $handler = new Phergie_Plugin_Handler($config, $this->handler); - $this->plugin->setPluginHandler($handler); - $handler->addPlugin($this->plugin); - $handler->addPlugin(new Phergie_Plugin_Http($config)); - $this->plugin->setConfig($config); - $this->connection->setNick('phergie'); + $response = $this->getMock('Phergie_Plugin_Http_Response'); + $response + ->expects($this->any()) + ->method('getContent') + ->will($this->returnValue($this->chayism)); + + $plugin = $this->getMock('Phergie_Plugin_Http'); + $plugin + ->expects($this->any()) + ->method('get') + ->will($this->returnValue($response)); + + $this->getMockPluginHandler() + ->expects($this->any()) + ->method('getPlugin') + ->with('Http') + ->will($this->returnValue($plugin)); + } + + /** + * Tests that the plugin requires the Http plugin as a dependency. + * + * @return void + */ + public function testRequiresHttpPlugin() + { + $this->assertRequiresPlugin('Http'); $this->plugin->onLoad(); } /** - * @event Phergie_Event_Request::privmsg - * @eventArg #zftalk - * @eventArg tychay + * Data provider for testPrivmsgTriggerReturnsChayism(). + * + * @return array Enumerated array of enumerated arrays each containing + * a set of parameters for a single call to + * testPrivmsgTriggerReturnsChayism() */ - public function testWithTyChay() + public function dataProviderTestPrivmsgTriggerReturnsChayism() { - $this->plugin->onPrivMsg(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_PRIVMSG); + return array( + array('terry chay'), + array('terry chay'), + array('tychay'), + array('!tychay'), + array('! tychay'), + array('foo tychay bar'), + ); } /** - * @event Phergie_Event_Request::privmsg - * @eventArg #zftalk - * @eventArg terrychay + * Tests that appropriate triggers result in a response with a Chayism. + * + * @return void + * @dataProvider dataProviderTestPrivmsgTriggerReturnsChayism */ - public function testWithTerryChay() + public function testPrivmsgTriggerReturnsChayism($trigger) { - $this->plugin->onPrivMsg(); - $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG, - 'string "terrychay" should not invoke a response'); - } - - /** - * @event Phergie_Event_Request::privmsg - * @eventArg #zftalk - * @eventArg terry chay - */ - public function testWithTerry_Chay() - { - $this->plugin->onPrivMsg(); - $this->assertHasEvent(Phergie_Event_Command::TYPE_PRIVMSG, - 'string "terry chay" should invoke a response'); + $this->setConfig('command.prefix', '!'); + $this->setUpHttpClient(); + $args = array( + 'receiver' => $this->source, + 'text' => $trigger + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->assertEmitsEvent('privmsg', array($this->source, 'Fact: ' . $this->chayism)); + $this->plugin->onPrivmsg(); } /** - * @event Phergie_Event_Request::privmsg - * @eventArg #zftalk - * @eventArg Elazar is not Mr. Chay + * Tests that lack of an appropriate trigger results in no response with + * a Chayism. + * + * @return void */ - public function testWithNoTyChay() + public function testNoPrivmsgTriggerDoesNotReturnChayism() { - $this->plugin->onPrivMsg(); - $this->assertDoesNotHaveEvent(Phergie_Event_Command::TYPE_PRIVMSG, - 'Failed asserting that elazar is not ' . - 'tychay'); + $args = array( + 'receiver' => $this->source, + 'text' => 'foo bar baz' + ); + $event = $this->getMockEvent('privmsg', $args); + $this->plugin->setEvent($event); + $this->assertDoesNotEmitEvent('privmsg', array($this->source, 'Fact: ' . $this->chayism)); + $this->plugin->onPrivmsg(); } -} \ No newline at end of file +} diff --git a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php index 36b81d6fae..941e7cb410 100644 --- a/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php +++ b/plugins/Irc/extlib/phergie/Tests/Phergie/Plugin/TestCase.php @@ -20,7 +20,7 @@ */ /** - * Unit test suite for Pherge_Plugin classes + * Unit test suite for plugin classes. * * @category Phergie * @package Phergie_Tests @@ -31,177 +31,405 @@ abstract class Phergie_Plugin_TestCase extends PHPUnit_Framework_TestCase { /** - * @var Phergie_Event_Handler + * Mock configuration + * + * @var Phergie_Config */ - protected $handler; + protected $config; /** + * Associative array for configuration setting values, accessed by the + * mock configuration object using a callback + * + * @var array + */ + protected $settings = array(); + + /** + * Mock connection + * * @var Phergie_Connection */ protected $connection; /** - * @var array + * Mock event handler + * + * @var Phergie_Event_Handler */ - protected $eventArgs; + protected $events; /** + * Mock plugin handler + * + * @var Phergie_Plugin_Handler + */ + protected $plugins; + + /** + * Plugin instance being tested + * * @var Phergie_Plugin_Abstract */ protected $plugin; /** - * @var array - */ - protected $config = array(); - - /** - * Constructs a test case with the given name. + * Full name of the plugin class being tested, may be explicitly + * specified in subclasses but is otherwise automatically derived from + * the test case class name * - * @param string $name - * @param array $data - * @param string $dataName + * @var string */ - public function __construct($name = NULL, array $data = array(), $dataName = '') - { - parent::__construct($name, $data, $dataName); - $this->connection = new Phergie_Connection(); - $this->handler = new Phergie_Event_Handler(); - } + protected $pluginClass; /** - * Assert that a given event type exists in the event handler - * @param string $event - * @param string $message - */ - public function assertHasEvent($event, $message = null) - { - self::assertTrue($this->handler->hasEventOfType($event), $message); - } - - /** - * Assert that a given event type DOES NOT exist in the event handler - * @param string $event - * @param string $message - */ - public function assertDoesNotHaveEvent($event, $message = null) - { - self::assertFalse($this->handler->hasEventOfType($event), $message); - } - - /** - * Assert that the emitter of the given command event was the given - * plugin + * User nick used in any events requiring one * - * @param Phergie_Event_Command $event - * @param Phergie_Plugin_Abstract $plugin - * @param string $message + * @var string */ - public function assertEventEmitter(Phergie_Event_Command $event, - Phergie_Plugin_Abstract $plugin, - $message = null) - { - $this->assertSame($plugin, $event->getPlugin(), $message); - } + protected $nick = 'nick'; /** - * Gets the events added to the handler by the plugin - * @param string $type - * @return array | null + * Event source used in any events requiring one + * + * @var string */ - public function getResponseEvents($type = null) + protected $source = '#channel'; + + /** + * Initializes instance properties. + * + * @return void + */ + public function setUp() { - if (is_string($type) && strlen($type) > 0) { - return $this->handler->getEventsOfType($type); + if (empty($this->pluginClass)) { + $this->pluginClass = preg_replace('/Test$/', '', get_class($this)); } - return $this->handler->getEvents(); - } - /** - * Sets the event for the test - * @param array $event - * @param array $eventArgs - */ - public function setEvent(array $event, array $eventArgs = null) - { - $eventClass = 'Phergie_Event_Request'; - if (is_array($event)) { - $eventClass = $event[0]; - $eventType = $event[1]; - } else { - throw new InvalidArgumentException("Invalid value for \$event"); + if (empty($this->plugin)) { + $this->plugin = new $this->pluginClass; } - $event = new $eventClass(); - $event->setType($eventType); - $event->setArguments($eventArgs); - $this->plugin->setEvent($event); - $this->eventArgs = $eventArgs; + + $this->plugin->setConfig($this->getMockConfig()); + $this->plugin->setConnection($this->getMockConnection()); + $this->plugin->setEventHandler($this->getMockEventHandler()); + $this->plugin->setPluginHandler($this->getMockPluginHandler()); } /** - * Sets the plugin to be tested - * If a plugin requries config for testing, an array placed in - * $this->config will be parsed into a Phergie_Config object and - * attached to the plugin + * Destroys all initialized instance properties. + * + * @return void */ - protected function setPlugin(Phergie_Plugin_Abstract $plugin) + public function tearDown() { - $this->plugin = $plugin; - $this->plugin->setEventHandler($this->handler); - $this->plugin->setConnection($this->connection); - $this->connection->setNick('test'); - if (!empty($this->config)) { - $config = new Phergie_Config(); - foreach ($this->config as $configKey => $configValue) { - $config[$configKey] = $configValue; - } - $plugin->setConfig($config); + unset( + $this->plugins, + $this->events, + $this->connection, + $this->config, + $this->plugin + ); + } + + /** + * Returns a mock configuration object. + * + * @return Phergie_Config + */ + protected function getMockConfig() + { + if (empty($this->config)) { + $this->config = $this->getMock('Phergie_Config', array('offsetExists', 'offsetGet')); + $this->config + ->expects($this->any()) + ->method('offsetExists') + ->will($this->returnCallback(array($this, 'configOffsetExists'))); + $this->config + ->expects($this->any()) + ->method('offsetGet') + ->will($this->returnCallback(array($this, 'configOffsetGet'))); } + return $this->config; } /** - * Overrides the runTest method to add additional annotations - * @return PHPUnit_Framework_TestResult + * Returns whether a specific configuration setting has a value. Only + * intended for use by this class, but must be public for PHPUnit to + * call them. + * + * @param string $name Name of the setting + * + * @return boolean TRUE if the setting has a value, FALSE otherwise */ - protected function runTest() + public function configOffsetExists($name) { - if (null === $this->plugin) { - throw new RuntimeException( - 'Tests cannot be run before plugin is set' + return isset($this->settings[$name]); + } + + /** + * Returns the value of a specific configuration setting. Only intended + * for use by this class, but must be public for PHPUnit to call them. + * + * @param string $name Name of the setting + * + * @return mixed Value of the setting + */ + public function configOffsetGet($name) + { + return $this->settings[$name]; + } + + /** + * Returns a mock connection object. + * + * @return Phergie_Connection + */ + protected function getMockConnection() + { + if (empty($this->connection)) { + $this->connection = $this->getMock('Phergie_Connection'); + $this->connection + ->expects($this->any()) + ->method('getNick') + ->will($this->returnValue($this->nick)); + } + return $this->connection; + } + + /** + * Returns a mock event handler object. + * + * @return Phergie_Event_Handler + */ + protected function getMockEventHandler() + { + if (empty($this->events)) { + $this->events = $this->getMock('Phergie_Event_Handler', array('addEvent')); + } + return $this->events; + } + + /** + * Returns a mock plugin handler object. + * + * @return Phergie_Plugin_Handler + */ + protected function getMockPluginHandler() + { + if (empty($this->plugins)) { + $config = $this->getMockConfig(); + $events = $this->getMockEventHandler(); + $this->plugins = $this->getMock( + 'Phergie_Plugin_Handler', + array(), // mock everything + array($config, $events) ); } - - // Clean the event handler... important! - $this->handler->clearEvents(); - - $info = $this->getAnnotations(); - $event = null; - $eventArgs = array(); - if (isset($info['method']['event']) && isset($info['method']['event'][0])) { - if (!is_string($info['method']['event'][0])) { - throw new InvalidArgumentException( - 'Only one event may be specified' - ); - } - $event = $info['method']['event'][0]; - - if (stristr($event, '::')) { - $event = explode('::', $event); - } - } - if (isset($info['method']['eventArg'])) { - $eventArgs = $info['method']['eventArg']; - } - if (null !== $event) { - $this->setEvent($event, $eventArgs); - } - - $testResult = parent::runTest(); - - // Clean the event handler again... just incase this time. - $this->handler->clearEvents(); - - return $testResult; + return $this->plugins; } + /** + * Returns a mock event object. + * + * @param string $type Event type + * @param array $args Optional associative array of event arguments + * @param string $nick Optional user nick to associate with the event + * @param string $source Optional user nick or channel name to associate + * with the event as its source + * + * @return Phergie_Event_Request + */ + protected function getMockEvent($type, array $args = array(), + $nick = null, $source = null + ) { + $methods = array('getNick', 'getSource'); + foreach (array_keys($args) as $arg) { + if (is_int($arg) || ctype_digit($arg)) { + $methods[] = 'getArgument'; + } else { + $methods[] = 'get' . ucfirst($arg); + } + } + + $event = $this->getMock( + 'Phergie_Event_Request', + $methods + ); + + $nick = $nick ? $nick : $this->nick; + $event + ->expects($this->any()) + ->method('getNick') + ->will($this->returnValue($nick)); + + $source = $source ? $source : $this->source; + $event + ->expects($this->any()) + ->method('getSource') + ->will($this->returnValue($source)); + + foreach ($args as $key => $value) { + if (is_int($key) || ctype_digit($key)) { + $event + ->expects($this->any()) + ->method('getArgument') + ->with($key) + ->will($this->returnValue($value)); + } else { + $event + ->expects($this->any()) + ->method('get' . ucfirst($key)) + ->will($this->returnValue($value)); + } + } + + return $event; + } + + /** + * Sets the value of a configuration setting. + * + * @param string $setting Name of the setting + * @param mixed $value Value for the setting + * + * @return void + */ + protected function setConfig($setting, $value) + { + $this->settings[$setting] = $value; + } + + /** + * Returns the absolute path to the Phergie/Plugin directory. Useful in + * conjunction with getMockDatabase(). + * + * @param string $subpath Optional path to append to the directory path + * + * @return string Directory path + */ + protected function getPluginsPath($subpath = null) + { + $path = realpath(dirname(__FILE__) . '/../../../Phergie/Plugin'); + if (!empty($subpath)) { + $path .= '/' . ltrim($subpath, '/'); + } + return $path; + } + + /** + * Modifies the event handler to include an expectation of an event + * being added by the plugin being tested. Note that this must be called + * BEFORE executing the plugin code intended to initiate the event. + * + * @param string $type Event type + * @param array $args Optional enumerated array of event arguments + * + * @return void + */ + protected function assertEmitsEvent($type, array $args = array()) + { + $this->events + ->expects($this->at(0)) + ->method('addEvent') + ->with($this->plugin, $type, $args); + } + + /** + * Modifies the event handler to include an expectation of an event NOT + * being added by the plugin being tested. Note that this must be called + * BEFORE executing plugin code that may initiate the event. + * + * @param string $type Event type + * @param array $args Optional enumerated array of event arguments + * + * @return void + */ + protected function assertDoesNotEmitEvent($type, array $args = array()) + { + // Ugly hack to get around an issue in PHPUnit + // @link http://github.com/sebastianbergmann/phpunit-mock-objects/issues/issue/5#issue/5/comment/343524 + $callback = create_function( + '$plugin, $type, $args', + 'if (get_class($plugin) == "' . $this->pluginClass . '" + && $type == "' . $type . '" + && $args == "' . var_export($args, true) . '") { + trigger_error("Instance of ' . $this->pluginClass + . ' unexpectedly emitted event of type ' . $type + . '", E_USER_ERROR); + }' + ); + + $this->events + ->expects($this->any()) + ->method('addEvent') + ->will($this->returnCallback($callback)); + } + + /** + * Modifies the plugin handler to include an expectation of a plugin + * being retrieved, indicating a dependency. Note that this must be + * called BEFORE executing the plugin code that may load that plugin + * dependency, which is usually located in onLoad(). + * + * @param string $name Short name of the plugin required as a dependency + * + * @return void + */ + public function assertRequiresPlugin($name) + { + $this->plugins + ->expects($this->atLeastOnce()) + ->method('getPlugin') + ->with($name); + } + + /** + * Creates an in-memory copy of a specified SQLite database file and + * returns a connection to it. + * + * @param string $path Path to the SQLite file to copy + * + * @return PDO Connection to the database copy + */ + public function getMockDatabase($path) + { + $original = new PDO('sqlite:' . $path); + $copy = new PDO('sqlite::memory:'); + + $result = $original->query('SELECT sql FROM sqlite_master'); + while ($sql = $result->fetchColumn()) { + $copy->exec($sql); + } + + $tables = array(); + $result = $original->query('SELECT name FROM sqlite_master WHERE type = "table"'); + while ($table = $result->fetchColumn()) { + $tables[] = $table; + } + + foreach ($tables as $table) { + $result = $original->query('SELECT * FROM ' . $table); + $insert = null; + $copy->beginTransaction(); + while ($row = $result->fetch(PDO::FETCH_ASSOC)) { + $columns = array_keys($row); + if (empty($insert)) { + $insert = $copy->prepare( + 'INSERT INTO "' . $table . '" (' . + '"' . implode('", "', $columns) . '"' . + ') VALUES (' . + ':' . implode(', :', $columns) . + ')' + ); + } + $insert->execute($row); + } + $copy->commit(); + unset($insert); + } + + return $copy; + } }