initial import to initialize repository - framework horde/support package
authorChuck Hagenbuch <chuck@nazik.horde.org>
Tue, 21 Oct 2008 01:49:49 +0000 (21:49 -0400)
committerChuck Hagenbuch <chuck@nazik.horde.org>
Tue, 21 Oct 2008 01:49:49 +0000 (21:49 -0400)
14 files changed:
framework/Support/lib/Horde/Support/Array.php [new file with mode: 0644]
framework/Support/lib/Horde/Support/ConsistentHash.php [new file with mode: 0644]
framework/Support/lib/Horde/Support/Inflector.php [new file with mode: 0644]
framework/Support/lib/Horde/Support/Stub.php [new file with mode: 0644]
framework/Support/lib/Horde/Support/Timer.php [new file with mode: 0644]
framework/Support/lib/Horde/Support/Uuid.php [new file with mode: 0644]
framework/Support/package.xml [new file with mode: 0644]
framework/Support/test/Horde/Support/AllTests.php [new file with mode: 0644]
framework/Support/test/Horde/Support/ArrayTest.php [new file with mode: 0644]
framework/Support/test/Horde/Support/ConsistentHashTest.php [new file with mode: 0644]
framework/Support/test/Horde/Support/InflectorTest.php [new file with mode: 0644]
framework/Support/test/Horde/Support/StubTest.php [new file with mode: 0644]
framework/Support/test/Horde/Support/TimerTest.php [new file with mode: 0644]
framework/Support/test/Horde/Support/UuidTest.php [new file with mode: 0644]

diff --git a/framework/Support/lib/Horde/Support/Array.php b/framework/Support/lib/Horde/Support/Array.php
new file mode 100644 (file)
index 0000000..21541bb
--- /dev/null
@@ -0,0 +1,240 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ *
+ * Incorporate stuff from Horde_Array?
+ * http://docs.python.org/lib/typesmapping.html
+ */
+class Horde_Support_Array implements ArrayAccess, Countable, Iterator
+{
+    /**
+     * Array variables
+     */
+    protected $_array = array();
+
+    /**
+     */
+    public function __construct($vars = array())
+    {
+        if (is_array($vars)) {
+            $this->update($vars);
+        }
+    }
+
+    /**
+     */
+    public function get($key, $default = null)
+    {
+        return isset($this->_array[$key]) ? $this->_array[$key] : $default;
+    }
+
+    /**
+     * Gets the value at $offset. If no value exists at that offset, or the
+     * value $offset is NULL, then $default is set as the value of $offset.
+     *
+     * @param string $offset   Offset to retrieve and set if unset
+     * @param string $default  Default value if $offset does not exist
+     *
+     * @return mixed Value at $offset or $default
+     */
+    public function getOrSet($offset, $default = null)
+    {
+        $value = $this->offsetGet($offset);
+        if (is_null($value)) {
+            $this->offsetSet($offset, $value = $default);
+        }
+        return $value;
+    }
+
+    /**
+     * Gets the value at $offset and deletes it from the array. If no value
+     * exists at $offset, or the value at $offset is null, then $default
+     * will be returned.
+     *
+     * @param string $offset   Offset to pop
+     * @param string $default  Default value
+     *
+     * @return mixed Value at $offset or $default
+     */
+    public function pop($offset, $default = null)
+    {
+        $value = $this->offsetGet($offset);
+        $this->offsetUnset($offset);
+        return isset($value) ? $value : $default;
+    }
+
+    /**
+     * Update the array with the key/value pairs from $array
+     *
+     * @param array $array Key/value pairs to set/change in the array.
+     */
+    public function update($array)
+    {
+        if (!is_array($array) && !$array instanceof Traversable) {
+            throw new InvalidArgumentException('expected array or traversable, got ' . gettype($array));
+        }
+
+        foreach ($array as $key => $val) {
+            $this->offsetSet($key, $val);
+        }
+    }
+
+    /**
+     * Get the keys in the array
+     *
+     * @return array
+     */
+    public function getKeys()
+    {
+        return array_keys($this->_array);
+    }
+
+    /**
+     * Get the values in the array
+     *
+     * @return array
+     */
+    public function getValues()
+    {
+        return array_values($this->_array);
+    }
+
+    /**
+     * Clear out the array
+     */
+    public function clear()
+    {
+        $this->_array = array();
+    }
+
+    /**
+     */
+    public function __get($key)
+    {
+        return $this->get($key);
+    }
+
+    /**
+     */
+    public function __set($key, $value)
+    {
+        $this->_array[$key] = $value;
+    }
+
+    /**
+     * Checks the existance of $key in this array
+     */
+    public function __isset($key)
+    {
+        return array_key_exists($key, $this->_array);
+    }
+
+    /**
+     * Removes $key from this array
+     */
+    public function __unset($key)
+    {
+        unset($this->_array[$key]);
+    }
+
+    /**
+     * Count the number of elements
+     *
+     * @return integer
+     */
+    public function count()
+    {
+        return count($this->_array);
+    }
+
+    /**
+     * Gets the current value of this array's Iterator
+     */
+    public function current()
+    {
+        return current($this->_array);
+    }
+
+    /**
+     * Advances this array's Iterator to the next value
+     */
+    public function next()
+    {
+        return next($this->_array);
+    }
+
+    /**
+     * Returns the current key of this array's Iterator
+     */
+    public function key()
+    {
+        return key($this->_array);
+    }
+
+    /**
+     * Checks if this array's Iterator is in a valid position
+     */
+    public function valid()
+    {
+        return $this->current() !== false;
+    }
+
+    /**
+     * Rewinds this array's Iterator
+     */
+    public function rewind()
+    {
+        reset($this->_array);
+    }
+
+    /**
+     * Gets the value of $offset in this array
+     *
+     * @see __get()
+     */
+    public function offsetGet($offset)
+    {
+        return $this->__get($offset);
+    }
+
+    /**
+     * Sets the value of $offset to $value
+     *
+     * @see __set()
+     */
+    public function offsetSet($offset, $value)
+    {
+        return $this->__set($offset, $value);
+    }
+
+    /**
+     * Checks the existence of $offset in this array
+     *
+     * @see __isset()
+     */
+    public function offsetExists($offset)
+    {
+        return $this->__isset($offset);
+    }
+
+    /**
+     * Removes $offset from this array
+     *
+     * @see __unset()
+     */
+    public function offsetUnset($offset)
+    {
+        return $this->__unset($offset);
+    }
+
+}
diff --git a/framework/Support/lib/Horde/Support/ConsistentHash.php b/framework/Support/lib/Horde/Support/ConsistentHash.php
new file mode 100644 (file)
index 0000000..b5d052e
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ *
+ * For a thorough description of consistent hashing, see
+ * http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/,
+ * and also the original paper:
+ * http://www8.org/w8-papers/2a-webserver/caching/paper2.html
+ */
+class Horde_Support_ConsistentHash
+{
+    /**
+     * Number of times to put each node into the hash circle per weight value.
+     * @var integer
+     */
+    protected $_numberOfReplicas = 100;
+
+    /**
+     * Array representing our circle
+     * @var array
+     */
+    protected $_circle = array();
+
+    /**
+     * Numeric indices into the circle by hash position
+     * @var array
+     */
+    protected $_pointMap = array();
+
+    /**
+     * Number of points on the circle
+     * @var integer
+     */
+    protected $_pointCount = 0;
+
+    /**
+     * Array of nodes.
+     * @var array
+     */
+    protected $_nodes = array();
+
+    /**
+     * Number of nodes
+     * @var integer
+     */
+    protected $_nodeCount = 0;
+
+    /**
+     * Create a new consistent hash, with initial $nodes at $numberOfReplicas
+     *
+     * @param array    $nodes             Initial array of nodes to add at $weight.
+     * @param integer  $weight            The weight for the initial node list.
+     * @param integer  $numberOfReplicas  The number of points on the circle to generate for each node.
+     */
+    public function __construct($nodes = array(), $weight = 1, $numberOfReplicas = 100)
+    {
+        $this->_numberOfReplicas = $numberOfReplicas;
+        $this->addNodes($nodes, $weight);
+    }
+
+    /**
+     * Get the primary node for $key.
+     *
+     * @param string $key  The key to look up.
+     *
+     * @param string  The primary node for $key.
+     */
+    public function get($key)
+    {
+        $nodes = $this->getNodes($key, 1);
+        if (!$nodes) {
+            throw new Exception('No nodes found');
+        }
+        return $nodes[0];
+    }
+
+    /**
+     * Get an ordered list of nodes for $key.
+     *
+     * @param string   $key    The key to look up.
+     * @param integer  $count  The number of nodes to look up.
+     *
+     * @return array  An ordered array of nodes.
+     */
+    public function getNodes($key, $count = 5)
+    {
+        // Degenerate cases
+        if ($this->_nodeCount < $count) {
+            throw new Exception('Not enough nodes (have ' . $this->_nodeCount . ', ' . $count . ' requested)');
+        }
+        if ($this->_nodeCount == 0) {
+            return array();
+        }
+
+        // Simple case
+        if ($this->_nodeCount == 1) {
+            return array($this->_nodes[0]['n']);
+        }
+
+        $hash = $this->hash(serialize($key));
+
+        // Find the first point on the circle greater than $hash by binary search.
+        $low = 0;
+        $high = $this->_pointCount - 1;
+        $index = null;
+        while (true) {
+            $mid = (int)(($low + $high) / 2);
+            if ($mid == $this->_pointCount) {
+                $index = 0;
+                break;
+            }
+
+            $midval = $this->_pointMap[$mid];
+            $midval1 = ($mid == 0) ? 0 : $this->_pointMap[$mid - 1];
+            if ($midval1 < $hash && $hash <= $midval) {
+                $index = $mid;
+                break;
+            }
+
+            if ($midval > $hash) {
+                $high = $mid - 1;
+            } else {
+                $low = $mid + 1;
+            }
+
+            if ($low > $high) {
+                $index = 0;
+                break;
+            }
+        }
+
+        $nodes = array();
+        while (count($nodes) < $count) {
+            $nodeIndex = $this->_pointMap[$index++ % $this->_pointCount];
+            $nodes[$nodeIndex] = $this->_nodes[$this->_circle[$nodeIndex]]['n'];
+        }
+        return array_values($nodes);
+    }
+
+    /**
+     * Add $node with weight $weight
+     *
+     * @param mixed $node
+     */
+    public function add($node, $weight = 1)
+    {
+        // Delegate to addNodes so that the circle is only regenerated once when
+        // adding multiple nodes.
+        $this->addNodes(array($node), $weight);
+    }
+
+    /**
+     * Add multiple nodes to the hash with the same weight.
+     *
+     * @param array    $nodes   An array of nodes.
+     * @param integer  $weight  The weight to add the nodes with.
+     */
+    public function addNodes($nodes, $weight = 1)
+    {
+        foreach ($nodes as $node) {
+            $this->_nodes[] = array('n' => $node, 'w' => $weight);
+            $this->_nodeCount++;
+
+            $nodeIndex = $this->_nodeCount - 1;
+            $nodeString = serialize($node);
+
+            $numberOfReplicas = (int)($weight * $this->_numberOfReplicas);
+            for ($i = 0; $i < $numberOfReplicas; $i++) {
+                $this->_circle[$this->hash($nodeString . $i)] = $nodeIndex;
+            }
+        }
+
+        $this->_updateCircle();
+    }
+
+    /**
+     * Remove $node from the hash.
+     *
+     * @param mixed $node
+     */
+    public function remove($node)
+    {
+        $nodeIndex = null;
+        $nodeString = serialize($node);
+
+        // Search for the node in the node list
+        foreach (array_keys($this->_nodes) as $i) {
+            if ($this->_nodes[$i]['n'] === $node) {
+                $nodeIndex = $i;
+                break;
+            }
+        }
+
+        if (is_null($nodeIndex)) {
+            throw new InvalidArgumentException('Node was not in the hash');
+        }
+
+        // Remove all points from the circle
+        $numberOfReplicas = (int)($this->_nodes[$nodeIndex]['w'] * $this->_numberOfReplicas);
+        for ($i = 0; $i < $numberOfReplicas; $i++) {
+            unset($this->_circle[$this->hash($nodeString . $i)]);
+        }
+        $this->_updateCircle();
+
+        // Unset the node from the node list
+        unset($this->_nodes[$nodeIndex]);
+        $this->_nodeCount--;
+    }
+
+    /**
+     * Expose the hash function for testing, probing, and extension.
+     *
+     * @param string $key
+     *
+     * @return string Hash value
+     */
+    public function hash($key)
+    {
+        return substr(md5($key), 0, 8);
+    }
+
+    /**
+     * Maintain the circle and arrays of points.
+     */
+    protected function _updateCircle()
+    {
+        // Sort the circle
+        ksort($this->_circle);
+
+        // Now that the hashes are sorted, generate numeric indices into the
+        // circle.
+        $this->_pointMap = array_keys($this->_circle);
+        $this->_pointCount = count($this->_pointMap);
+    }
+
+}
diff --git a/framework/Support/lib/Horde/Support/Inflector.php b/framework/Support/lib/Horde/Support/Inflector.php
new file mode 100644 (file)
index 0000000..e5ec15f
--- /dev/null
@@ -0,0 +1,266 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * Horde Inflector class.
+ *
+ * @category   Horde
+ * @package    Support
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_Inflector {
+
+    /**
+     * Inflection cache
+     * @var array
+     */
+    protected $_cache = array();
+
+    /**
+     * Rules for pluralizing English nouns.
+     *
+     * @var array
+     */
+    protected $_pluralizationRules = array(
+        '/move$/i' => 'moves',
+        '/sex$/i' => 'sexes',
+        '/child$/i' => 'children',
+        '/man$/i' => 'men',
+        '/foot$/i' => 'feet',
+        '/person$/i' => 'people',
+        '/(quiz)$/i' => '$1zes',
+        '/^(ox)$/i' => '$1en',
+        '/(m|l)ouse$/i' => '$1ice',
+        '/(matr|vert|ind)ix|ex$/i' => '$1ices',
+        '/(x|ch|ss|sh)$/i' => '$1es',
+        '/([^aeiouy]|qu)ies$/i' => '$1y',
+        '/([^aeiouy]|qu)y$/i' => '$1ies',
+        '/(?:([^f])fe|([lr])f)$/i' => '$1$2ves',
+        '/sis$/i' => 'ses',
+        '/([ti])um$/i' => '$1a',
+        '/(buffal|tomat)o$/i' => '$1oes',
+        '/(bu)s$/i' => '$1ses',
+        '/(alias|status)$/i' => '$1es',
+        '/(octop|vir)us$/i' => '$1i',
+        '/(ax|test)is$/i' => '$1es',
+        '/s$/i' => 's',
+        '/$/' => 's',
+    );
+
+    /**
+     * Rules for singularizing English nouns.
+     *
+     * @var array
+     */
+    protected $_singularizationRules = array(
+        '/cookies$/i' => 'cookie',
+        '/moves$/i' => 'move',
+        '/sexes$/i' => 'sex',
+        '/children$/i' => 'child',
+        '/men$/i' => 'man',
+        '/feet$/i' => 'foot',
+        '/people$/i' => 'person',
+        '/databases$/i'=> 'database',
+        '/(quiz)zes$/i' => '\1',
+        '/(matr)ices$/i' => '\1ix',
+        '/(vert|ind)ices$/i' => '\1ex',
+        '/^(ox)en/i' => '\1',
+        '/(alias|status)es$/i' => '\1',
+        '/([octop|vir])i$/i' => '\1us',
+        '/(cris|ax|test)es$/i' => '\1is',
+        '/(shoe)s$/i' => '\1',
+        '/(o)es$/i' => '\1',
+        '/(bus)es$/i' => '\1',
+        '/([m|l])ice$/i' => '\1ouse',
+        '/(x|ch|ss|sh)es$/i' => '\1',
+        '/(m)ovies$/i' => '\1ovie',
+        '/(s)eries$/i' => '\1eries',
+        '/([^aeiouy]|qu)ies$/i' => '\1y',
+        '/([lr])ves$/i' => '\1f',
+        '/(tive)s$/i' => '\1',
+        '/(hive)s$/i' => '\1',
+        '/([^f])ves$/i' => '\1fe',
+        '/(^analy)ses$/i' => '\1sis',
+        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
+        '/([ti])a$/i' => '\1um',
+        '/(n)ews$/i' => '\1ews',
+        '/(.*)s$/i' => '\1',
+    );
+
+    /**
+     * An array of words with the same singular and plural spellings.
+     *
+     * @var array
+     */
+    protected $_uncountables = array(
+        'aircraft',
+        'cannon',
+        'deer',
+        'equipment',
+        'fish',
+        'information',
+        'money',
+        'moose',
+        'rice',
+        'series',
+        'sheep',
+        'species',
+        'swine',
+    );
+
+    /**
+     * Constructor
+     *
+     * Store a map of the uncountable words for quicker checks.
+     */
+    public function __construct()
+    {
+        $this->_uncountables_keys = array_flip($this->_uncountables);
+    }
+
+    /**
+     * Add an uncountable word.
+     *
+     * @param string $word The uncountable word.
+     */
+    public function uncountable($word)
+    {
+        $this->_uncountables[] = $word;
+        $this->_uncountables_keys[$word] = true;
+    }
+
+    /**
+     * Singular English word to pluralize.
+     *
+     * @param string $word Word to pluralize.
+     *
+     * @return string Plural form of $word.
+     */
+    public function pluralize($word)
+    {
+        if ($plural = $this->_getCache($word, 'pluralize')) {
+            return $plural;
+        }
+
+        if (isset($this->_uncountables_keys[$word])) {
+            return $word;
+        }
+
+        foreach ($this->_pluralizationRules as $regexp => $replacement) {
+            $plural = preg_replace($regexp, $replacement, $word, -1, $matches);
+            if ($matches > 0) {
+                return $this->_cache($word, 'pluralize', $plural);
+            }
+        }
+
+        return $this->_cache($word, 'pluralize', $word);
+    }
+
+    /**
+     * Plural English word to singularize.
+     *
+     * @param string $word Word to singularize.
+     *
+     * @return string Singular form of $word.
+     */
+    public function singularize($word)
+    {
+        if ($singular = $this->_getCache($word, 'singularize')) {
+            return $singular;
+        }
+
+        if (isset($this->_uncountables_keys[$word])) {
+            return $word;
+        }
+
+        foreach ($this->_singularizationRules as $regexp => $replacement) {
+            $singular = preg_replace($regexp, $replacement, $word, -1, $matches);
+            if ($matches > 0) {
+                return $this->_cache($word, 'singularize', $singular);
+            }
+        }
+
+        return $this->_cache($word, 'singularize', $word);
+    }
+
+    /**
+     * Camel-case a word
+     *
+     * @param string $word The word to camel-case
+     * @param string $firstLetter Whether to upper or lower case the first
+     * letter of each slash-separated section. Defaults to 'upper';
+     *
+     * @return string Camelized $word
+     */
+    public function camelize($word, $firstLetter = 'upper')
+    {
+        if ($camelized = $this->_getCache($word, 'camelize' . $firstLetter)) {
+            return $camelized;
+        }
+
+        $camelized = $word;
+        if (strtolower($camelized) != $camelized && strpos($camelized, '_') !== false) {
+            $camelized = str_replace('_', '/', $camelized);
+        }
+        if (strpos($camelized, '/') !== false) {
+            $camelized = str_replace('/', '/ ', $camelized);
+        }
+        if (strpos($camelized, '_') !== false) {
+            $camelized = strtr($camelized, '_', ' ');
+        }
+
+        $camelized = str_replace(' ' , '', ucwords($camelized));
+
+        if ($firstLetter == 'lower') {
+            $parts = array();
+            foreach (explode('/', $camelized) as $part) {
+                $part[0] = strtolower($part[0]);
+                $parts[] = $part;
+            }
+            $camelized = implode('/', $parts);
+        }
+
+        return $this->_cache($word, 'camelize' . $firstLetter, $camelized);
+    }
+
+    /**
+     * Get a cached inflection
+     *
+     * @return string | false
+     */
+    protected function _getCache($word, $rule)
+    {
+        return isset($this->_cache[$word . '|' . $rule]) ?
+            $this->_cache[$word . '|' . $rule] : false;
+    }
+
+    /**
+     * Cache an inflection
+     *
+     * @param string $word The word being inflected
+     * @param string $rule The inflection rule
+     * @param string $value The inflected value of $word
+     *
+     * @return string The inflected value
+     */
+    protected function _cache($word, $rule, $value)
+    {
+        $this->_cache[$word . '|' . $rule] = $value;
+        return $value;
+    }
+
+    /**
+     * Clear the inflection cache
+     */
+    public function clearCache()
+    {
+        $this->_cache = array();
+    }
+
+}
diff --git a/framework/Support/lib/Horde/Support/Stub.php b/framework/Support/lib/Horde/Support/Stub.php
new file mode 100644 (file)
index 0000000..946b723
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * Class that can substitute for any object and safely do nothing.
+ *
+ * @category   Horde
+ * @package    Support
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_Stub
+{
+    /**
+     * Cooerce to an empty string
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return '';
+    }
+
+    /**
+     * Return self for any requested property.
+     *
+     * @param string $key The requested object property
+     *
+     * @return null
+     */
+    public function __get($key)
+    {
+    }
+
+    /**
+     * Gracefully accept any method call and do nothing.
+     *
+     * @param string $method The method that was called
+     * @param array $args The method's arguments
+     *
+     * @return null
+     */
+    public function __call($method, $args)
+    {
+    }
+
+    /**
+     * Gracefully accept any static method call and do nothing.
+     *
+     * @param string $method The method that was called
+     * @param array $args The method's arguments
+     *
+     * @return null
+     */
+    public static function __callStatic($method, $args)
+    {
+    }
+
+}
diff --git a/framework/Support/lib/Horde/Support/Timer.php b/framework/Support/lib/Horde/Support/Timer.php
new file mode 100644 (file)
index 0000000..440b100
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * Simple interface for timing operations.
+ *
+ * <code>
+ *  $t = new Horde_Support_Timer;
+ *  $t->push();
+ *  $elapsed = $t->pop();
+ * </code>
+ *
+ * @category   Horde
+ * @package    Support
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_Timer
+{
+    /**
+     * @var array
+     */
+    protected $_start = array();
+
+    /**
+     * @var integer
+     */
+    protected $_idx = 0;
+
+    /**
+     * Push a new timer start on the stack.
+     */
+    public function push()
+    {
+        $start = $this->_start[$this->_idx++] = microtime(true);
+        return $start;
+    }
+
+    /**
+     * Pop the latest timer start and return the difference with the current
+     * time.
+     */
+    public function pop()
+    {
+        $etime = microtime(true);
+
+        if (! ($this->_idx > 0)) {
+            throw new Exception('No timers have been started');
+        }
+
+        return $etime - $this->_start[--$this->_idx];
+    }
+
+}
diff --git a/framework/Support/lib/Horde/Support/Uuid.php b/framework/Support/lib/Horde/Support/Uuid.php
new file mode 100644 (file)
index 0000000..a5a9142
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * Class for generating RFC 4122 UUIDs. Usage:
+ *
+ * <code>
+ *  <?php
+ *
+ *  $uuid = (string)new Horde_Support_Uuid;
+ *
+ *  ?>
+ * </code>
+ *
+ * @category   Horde
+ * @package    Support
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_Uuid
+{
+    /**
+     * Generated UUID
+     * @var string
+     */
+    private $_uuid;
+
+    /**
+     * New UUID
+     */
+    public function __construct()
+    {
+        $this->generate();
+    }
+
+    /**
+     * Generate a 36-character RFC 4122 UUID, without the urn:uuid: prefix.
+     *
+     * @see http://www.ietf.org/rfc/rfc4122.txt
+     * @see http://labs.omniti.com/alexandria/trunk/OmniTI/Util/UUID.php
+     *
+     * @return string
+     */
+    public function generate()
+    {
+        list($time_mid, $time_low) = explode(' ', microtime());
+        $time_low = (int)$time_low;
+        $time_mid = (int)substr($time_mid, 2) & 0xffff;
+        $time_high = mt_rand(0, 0x0fff) | 0x4000;
+
+        $clock = mt_rand(0, 0x3fff) | 0x8000;
+
+        $node_low = function_exists('zend_thread_id') ?
+            zend_thread_id() : getmypid();
+        $node_high = isset($_SERVER['SERVER_ADDR']) ?
+            ip2long($_SERVER['SERVER_ADDR']) : crc32(php_uname());
+        $node = bin2hex(pack('nN', $node_low, $node_high));
+
+        $this->_uuid = sprintf('%08x-%04x-%04x-%04x-%s',
+            $time_low, $time_mid, $time_high, $clock, $node);
+    }
+
+    /**
+     * Cooerce to string
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->_uuid;
+    }
+
+}
diff --git a/framework/Support/package.xml b/framework/Support/package.xml
new file mode 100644 (file)
index 0000000..a21039f
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Support</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde support package</summary>
+ <description>This package provides supporting functionality for Horde that is not tied to Horde but is used by it. These classes can be used outside of Horde as well.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-08-01</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>
+   * Initial horde/support package
+   * Initial Horde_Support_Array object
+   * Initial Horde_Support_ConsistentHash object
+   * Initial Horde_Support_Inflector object
+   * Initial Horde_Support_Stub object
+   * Initial Horde_Support_Timer object
+   * Initial Horde_Support_Uuid object
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Support">
+      <file name="Array.php" role="php" />
+      <file name="ConsistentHash.php" role="php" />
+      <file name="Inflector.php" role="php" />
+      <file name="Stub.php" role="php" />
+      <file name="Timer.php" role="php" />
+      <file name="Uuid.php" role="php" />
+     </dir> <!-- /lib/Horde/Support -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Support/Array.php" as="Horde/Support/Array.php" />
+   <install name="lib/Horde/Support/ConsistentHash.php" as="Horde/Support/ConsistentHash.php" />
+   <install name="lib/Horde/Support/Inflector.php" as="Horde/Support/Inflector.php" />
+   <install name="lib/Horde/Support/Stub.php" as="Horde/Support/Stub.php" />
+   <install name="lib/Horde/Support/Timer.php" as="Horde/Support/Timer.php" />
+   <install name="lib/Horde/Support/Uuid.php" as="Horde/Support/Uuid.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Support/test/Horde/Support/AllTests.php b/framework/Support/test/Horde/Support/AllTests.php
new file mode 100644 (file)
index 0000000..7fac8b1
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Support_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+class Horde_Support_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        set_include_path(dirname(__FILE__) . '/../../../lib' . PATH_SEPARATOR . get_include_path());
+        if (!spl_autoload_functions()) {
+            spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); include "$filename.php";'));
+        }
+
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Support');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Support_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Support_AllTests::main') {
+    Horde_Support_AllTests::main();
+}
diff --git a/framework/Support/test/Horde/Support/ArrayTest.php b/framework/Support/test/Horde/Support/ArrayTest.php
new file mode 100644 (file)
index 0000000..f4d8fd3
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_ArrayTest extends PHPUnit_Framework_TestCase
+{
+    public function testImplementsArrayAccess()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertType('ArrayAccess', $o);
+    }
+
+    public function testImplementsIterator()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertType('Iterator', $o);
+    }
+
+    public function testImplementsCountable()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertType('Countable', $o);
+    }
+
+    // offsetGet()
+
+    public function testOffsetGetReturnsValueAtOffset()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $this->assertEquals('bar', $o->offsetGet('foo'));
+    }
+
+    public function testOffsetGetReturnsNullWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertNull($o->offsetGet('foo'));
+    }
+
+    // get()
+
+    public function testGetReturnsValueAtOffset()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $this->assertEquals('bar', $o->get('foo'));
+    }
+
+    public function testGetReturnsNullByDefaultWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertNull($o->get('foo'));
+    }
+
+    public function testGetReturnsDefaultSpecifiedWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertEquals('bar', $o->get('foo', 'bar'));
+    }
+
+    public function testGetReturnsDefaultSpecifiedWhenValueAtOffsetIsNull()
+    {
+        $o = new Horde_Support_Array(array('foo' => null));
+        $this->assertEquals('bar', $o->get('foo', 'bar'));
+    }
+
+    // getOrSet()
+
+    public function testGetOrSetReturnsValueAtOffset()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $this->assertEquals('bar', $o->getOrSet('foo'));
+    }
+
+    public function testGetOrSetReturnsAndSetsNullWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertNull($o->getOrSet('foo'));
+        $this->assertTrue($o->offsetExists('foo'));
+        $this->assertNull($o->offsetGet('foo'));
+    }
+
+    public function testGetOrSetReturnsAndSetsDefaultSpecifiedWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertEquals('bar', $o->getOrSet('foo', 'bar'));
+        $this->assertTrue($o->offsetExists('foo'));
+        $this->assertEquals('bar', $o->offsetGet('foo'));
+    }
+
+    public function testGetOrSetReturnsAndSetsDefaultSpecifiedValueAtOffsetIsNull()
+    {
+        $o = new Horde_Support_Array(array('foo' => null));
+        $this->assertEquals('bar', $o->getOrSet('foo', 'bar'));
+        $this->assertTrue($o->offsetExists('foo'));
+        $this->assertEquals('bar', $o->offsetGet('foo'));
+    }
+
+    // pop()
+
+    public function testPopReturnsValueAtOffsetAndUnsetsIt()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $this->assertEquals('bar', $o->pop('foo'));
+        $this->assertFalse($o->offsetExists('foo'));
+    }
+
+    public function testPopReturnsNullByDefaultWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertNull($o->pop('foo'));
+    }
+
+    public function testPopReturnsDefaultSpecifiedWhenOffsetDoesNotExist()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertEquals('bar', $o->pop('foo', 'bar'));
+    }
+
+    public function testPopReturnsDefaultSpecifiedWhenValueAtOffsetIsNull()
+    {
+        $o = new Horde_Support_Array(array('foo' => null));
+        $this->assertEquals('bar', $o->pop('foo', 'bar'));
+    }
+
+    // update()
+
+    public function testUpdateDoesNotThrowWhenArgumentIsAnArray()
+    {
+        $o = new Horde_Support_Array();
+        $o->update(array());
+    }
+
+    public function testUpdateDoesNotThrowWhenArgumentIsTraversable()
+    {
+        $o = new Horde_Support_Array();
+        $o->update(new ArrayObject());
+    }
+
+    public function testUpdateMergesNewValuesFromArayInArgument()
+    {
+        $o = new Horde_Support_Array();
+        $o->update(array('foo' => 'bar'));
+        $this->assertEquals('bar', $o->offsetGet('foo'));
+    }
+
+    public function testUpdateMergesAndOverwritesExistingOffsets()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $o->update(array('foo' => 'baz'));
+        $this->assertEquals('baz', $o->offsetGet('foo'));
+    }
+
+    public function testUpdateMergeDoesNotAffectUnrelatedKeys()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $o->update(array('baz' => 'qux'));
+        $this->assertEquals('qux', $o->offsetGet('baz'));
+    }
+
+    // clear()
+
+    public function testClearErasesTheArray()
+    {
+        $o = new Horde_Support_Array(array('foo' => 'bar'));
+        $o->clear();
+        $this->assertEquals(0, $o->count());
+    }
+
+    // getKeys()
+
+    public function testGetKeysReturnsEmptyArrayWhenArrayIsEmpty()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertSame(array(), $o->getKeys());
+    }
+
+    public function testGetKeysReturnsArrayOfKeysInTheArray()
+    {
+        $o = new Horde_Support_Array(array('foo'=> 1, 'bar' => 2));
+        $this->assertSame(array('foo', 'bar'), $o->getKeys());
+    }
+
+    // getValues()
+
+    public function testGetValuesReturnsEmptyArrayWhenArrayIsEmpty()
+    {
+        $o = new Horde_Support_Array();
+        $this->assertSame(array(), $o->getValues());
+    }
+
+    public function testGetValuesReturnsArrayOfValuesInTheArray()
+    {
+        $o = new Horde_Support_Array(array('foo' => 1, 'bar' => 2));
+        $this->assertSame(array(1, 2), $o->getValues());
+    }
+
+}
diff --git a/framework/Support/test/Horde/Support/ConsistentHashTest.php b/framework/Support/test/Horde/Support/ConsistentHashTest.php
new file mode 100644 (file)
index 0000000..7d16d90
--- /dev/null
@@ -0,0 +1,234 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_ConsistentHashTest extends PHPUnit_Framework_TestCase
+{
+    public function testAddUpdatesCount()
+    {
+        $h = new Horde_Support_ConsistentHash;
+        $this->assertEquals(0, $this->readAttribute($h, '_nodeCount'));
+
+        $h->add('a');
+        $this->assertEquals(1, $this->readAttribute($h, '_nodeCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_nodes')), $this->readAttribute($h, '_nodeCount'));
+    }
+
+    public function testAddUpdatesPointCount()
+    {
+        $numberOfReplicas = 100;
+        $h = new Horde_Support_ConsistentHash(array(), 1, $numberOfReplicas);
+        $this->assertEquals(0, $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_circle')), $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_pointMap')), $this->readAttribute($h, '_pointCount'));
+
+        $h->add('a');
+        $this->assertEquals(100, $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_circle')), $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_pointMap')), $this->readAttribute($h, '_pointCount'));
+    }
+
+    public function testAddWithWeightGeneratesMorePoints()
+    {
+        $weight = 2;
+        $numberOfReplicas = 100;
+        $h = new Horde_Support_ConsistentHash(array(), 1, $numberOfReplicas);
+        $this->assertEquals(0, $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_circle')), $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_pointMap')), $this->readAttribute($h, '_pointCount'));
+
+        $h->add('a', $weight);
+        $this->assertEquals($numberOfReplicas * $weight, $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_circle')), $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_pointMap')), $this->readAttribute($h, '_pointCount'));
+    }
+
+    public function testRemoveRemovesPoints()
+    {
+        $h = new Horde_Support_ConsistentHash;
+        $this->assertEquals(0, $this->readAttribute($h, '_nodeCount'));
+
+        $h->add('a');
+        $h->remove('a');
+        $this->assertEquals(0, $this->readAttribute($h, '_nodeCount'));
+        $this->assertEquals(0, $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_circle')), $this->readAttribute($h, '_pointCount'));
+        $this->assertEquals(count($this->readAttribute($h, '_pointMap')), $this->readAttribute($h, '_pointCount'));
+    }
+
+    public function testRemoveThrowsOnNonexistentNode()
+    {
+        $h = new Horde_Support_ConsistentHash;
+        $this->setExpectedException('InvalidArgumentException');
+        $h->remove('a');
+    }
+
+    public function testLookupsReturnValidNodes()
+    {
+        $nodes = range(1, 10);
+        $h = new Horde_Support_ConsistentHash($nodes);
+
+        foreach (range(1, 10) as $i) {
+            $this->assertContains($h->get($i), $nodes);
+        }
+    }
+
+    public function testLookupRatiosWithDifferentNodeWeights()
+    {
+        $h = new Horde_Support_ConsistentHash;
+        $h->add('a', 2);
+        $h->add('b', 1);
+        $h->add('c', 3);
+        $h->add('d', 4);
+
+        $choices = array('a' => 0, 'b' => 0, 'c' => 0, 'd' => 0);
+        for ($i = 0; $i < 1000; $i++) {
+            $choices[$h->get(uniqid(mt_rand()))]++;
+        }
+
+        // Due to randomness it's entirely possible to have some overlap in the
+        // middle, but the highest-weighted node should definitely be chosen
+        // more than the lowest-weighted one.
+        $this->assertGreaterThan($choices['b'], $choices['d']);
+    }
+
+    public function testRepeatableLookups()
+    {
+        $h = new Horde_Support_ConsistentHash(range(1, 10));
+
+        $this->assertEquals($h->get('t1'), $h->get('t1'));
+        $this->assertEquals($h->get('t2'), $h->get('t2'));
+    }
+
+    public function testRepeatableLookupsAfterAddingAndRemoving()
+    {
+        $h = new Horde_Support_ConsistentHash(range(1, 100));
+
+        $results1 = array();
+        foreach (range(1, 100) as $i)
+            $results1[] = $h->get($i);
+
+        $h->add('new');
+        $h->remove('new');
+        $h->add('new');
+        $h->remove('new');
+
+        $results2 = array();
+        foreach (range(1, 100) as $i)
+            $results2[] = $h->get($i);
+
+        $this->assertEquals($results1, $results2);
+    }
+
+    public function testRepeatableLookupsBetweenInstances()
+    {
+        $h1 = new Horde_Support_ConsistentHash(range(1, 10));
+        $results1 = array();
+        foreach (range(1, 100) as $i)
+            $results1[] = $h1->get($i);
+
+        $h2 = new Horde_Support_ConsistentHash(range(1, 10));
+        $results2 = array();
+        foreach (range(1, 100) as $i)
+            $results2[] = $h2->get($i);
+
+        $this->assertEquals($results1, $results2);
+    }
+
+    public function testGetNodes()
+    {
+        $h = new Horde_Support_ConsistentHash(range(1, 10));
+        $nodes = $h->getNodes('r', 2);
+
+        $this->assertType('array', $nodes);
+        $this->assertEquals(count($nodes), 2);
+        $this->assertNotEquals($nodes[0], $nodes[1]);
+    }
+
+    public function testGetNodesWithNotEnoughNodes()
+    {
+        $h = new Horde_Support_ConsistentHash(array('t'));
+
+        $this->setExpectedException('Exception');
+        $nodes = $h->getNodes('resource', 2);
+    }
+
+    public function testGetNodesWrapsToBeginningOfCircle()
+    {
+        $h = new Horde_Support_ConsistentHash(array(), 1, 1);
+
+        // Create an array of random values and one fixed test value and sort
+        // them by their hashes
+        $nodes = array();
+        for ($i = 0; $i < 10; $i++) {
+            $val = uniqid(mt_rand(), true);
+            $nodes[$h->hash(serialize($val) . '0')] = $val;
+        }
+        $nodes[$h->hash(serialize('key'))] = 'key';
+        ksort($nodes);
+
+        // Remove the fixed test value.
+        $nodes = array_values($nodes);
+        $testindex = array_search('key', $nodes);
+        $testvalue = array_shift(array_splice($nodes, $testindex, 1));
+
+        foreach ($nodes as $node) {
+            $h->add($node);
+        }
+
+        $expected = array();
+        for ($i = 0; $i < 10; $i++) {
+            $expected[] = $nodes[($testindex + $i) % 10];
+        }
+
+        $this->assertEquals(
+            $expected,
+            $h->getNodes($testvalue, 10));
+    }
+
+    public function testFallbackWhenANodeIsRemoved()
+    {
+        $h = new Horde_Support_ConsistentHash(array(), 1, 1);
+
+        // Create an array of random values and one fixed test value and sort
+        // them by their hashes
+        $nodes = array();
+        for ($i = 0; $i < 10; $i++) {
+            $val = uniqid(mt_rand(), true);
+            $nodes[$h->hash(serialize($val) . '0')] = $val;
+        }
+        $nodes[$h->hash(serialize('key'))] = 'key';
+        ksort($nodes);
+
+        // Remove the fixed test value.
+        $nodes = array_values($nodes);
+        $testindex = array_search('key', $nodes);
+        $testvalue = array_shift(array_splice($nodes, $testindex, 1));
+
+        foreach ($nodes as $node) {
+            $h->add($node);
+        }
+
+        $this->assertEquals($h->get('key'), $nodes[$testindex]);
+
+        $h->remove($nodes[$testindex]);
+        $this->assertEquals($h->get('key'), $nodes[($testindex + 1) % 10]);
+
+        $h->remove($nodes[($testindex + 1) % 10]);
+        $this->assertEquals($h->get('key'), $nodes[($testindex + 2) % 10]);
+    }
+
+}
diff --git a/framework/Support/test/Horde/Support/InflectorTest.php b/framework/Support/test/Horde/Support/InflectorTest.php
new file mode 100644 (file)
index 0000000..b15722d
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2007-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_InflectorTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Words to test
+     *
+     * @var array $words
+     */
+    public $words = array(
+        'sheep' => 'sheep',
+        'man' => 'men',
+        'woman' => 'women',
+        'user' => 'users',
+        'foot' => 'feet',
+        'hive' => 'hives',
+        'chive' => 'chives',
+        'event' => 'events',
+        'task' => 'tasks',
+        'preference' => 'preferences',
+        'child' => 'children',
+        'moose' => 'moose',
+        'mouse' => 'mice',
+    );
+
+    public function setUp()
+    {
+        $this->inflector = new Horde_Support_Inflector;
+    }
+
+    public function testSingularizeAndPluralize()
+    {
+        foreach ($this->words as $singular => $plural) {
+            $this->assertEquals($plural, $this->inflector->pluralize($singular));
+            $this->assertEquals($singular, $this->inflector->singularize($plural));
+        }
+    }
+
+    public function testCamelize()
+    {
+        // underscore => camelize
+        $this->assertEquals('Test', $this->inflector->camelize('test'));
+        $this->assertEquals('TestCase', $this->inflector->camelize('test_case'));
+        $this->assertEquals('Test/Case', $this->inflector->camelize('test/case'));
+        $this->assertEquals('TestCase/Name', $this->inflector->camelize('test_case/name'));
+
+        // already camelized
+        $this->assertEquals('Test', $this->inflector->camelize('Test'));
+        $this->assertEquals('TestCase', $this->inflector->camelize('testCase'));
+        $this->assertEquals('TestCase', $this->inflector->camelize('TestCase'));
+        $this->assertEquals('Test/Case', $this->inflector->camelize('Test_Case'));
+    }
+
+    public function testCamelizeLower()
+    {
+        // underscore => camelize
+        $this->assertEquals('test', $this->inflector->camelize('test', 'lower'));
+        $this->assertEquals('testCase', $this->inflector->camelize('test_case', 'lower'));
+        $this->assertEquals('test/case', $this->inflector->camelize('test/case', 'lower'));
+        $this->assertEquals('testCase/name', $this->inflector->camelize('test_case/name', 'lower'));
+
+        // already camelized
+        $this->assertEquals('test', $this->inflector->camelize('Test', 'lower'));
+        $this->assertEquals('testCase', $this->inflector->camelize('testCase', 'lower'));
+        $this->assertEquals('testCase', $this->inflector->camelize('TestCase', 'lower'));
+        $this->assertEquals('test/case', $this->inflector->camelize('Test_Case', 'lower'));
+    }
+
+}
diff --git a/framework/Support/test/Horde/Support/StubTest.php b/framework/Support/test/Horde/Support/StubTest.php
new file mode 100644 (file)
index 0000000..0d89323
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_StubTest extends PHPUnit_Framework_TestCase
+{
+    public function testAnyOffsetIsGettable()
+    {
+        $stub = new Horde_Support_Stub;
+        $oldTrackErrors = ini_set('track_errors', 1);
+        $php_errormsg = null;
+        $this->assertNull($stub->{uniqid()});
+        $this->assertNull($php_errormsg);
+    }
+
+    public function testAnyMethodIsCallable()
+    {
+        $stub = new Horde_Support_Stub;
+        $this->assertTrue(is_callable(array($stub, uniqid())));
+    }
+
+    public function testAnyStaticMethodIsCallable()
+    {
+        if (version_compare(PHP_VERSION, '5.3', '<')) {
+            $this->markTestSkipped();
+        }
+        $this->assertTrue(is_callable(array('Horde_Support_Stub', uniqid())));
+    }
+
+}
diff --git a/framework/Support/test/Horde/Support/TimerTest.php b/framework/Support/test/Horde/Support/TimerTest.php
new file mode 100644 (file)
index 0000000..6fc11cd
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_TimerTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * test instantiating a normal timer
+     */
+    public function testNormalTiming()
+    {
+        $t = new Horde_Support_Timer;
+        $start = $t->push();
+        $elapsed = $t->pop();
+
+        $this->assertTrue(is_float($start));
+        $this->assertTrue(is_float($elapsed));
+        $this->assertTrue($elapsed > 0);
+    }
+
+    /**
+     * test getting the finish time before starting the timer
+     * @expectedException Exception
+     */
+    public function testNotStartedYetThrowsException()
+    {
+        $t = new Horde_Support_Timer();
+        $t->pop();
+    }
+
+}
diff --git a/framework/Support/test/Horde/Support/UuidTest.php b/framework/Support/test/Horde/Support/UuidTest.php
new file mode 100644 (file)
index 0000000..153d0fd
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * @group      support
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  1999-2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+class Horde_Support_UuidTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * test instantiating a normal timer
+     */
+    public function testUuidLength()
+    {
+        $uuid = (string)new Horde_Support_Uuid;
+        $this->assertEquals(36, strlen($uuid));
+    }
+
+}