Browse Source

Merge branch 'master' into 3.1

Mark Story 11 years ago
parent
commit
0cb09e6baa

+ 9 - 0
src/Collection/CollectionInterface.php

@@ -847,4 +847,13 @@ interface CollectionInterface extends Iterator, JsonSerializable
      * @return bool
      */
     public function isEmpty();
+
+    /**
+     * Returns the closest nested iterator that can be safely traversed without
+     * losing any possible transformations. This is used mainly to remove empty
+     * IteratorIterator wrappers that can only slowdown the iteration process.
+     *
+     * @return \Iterator
+     */
+    public function unwrap();
 }

+ 34 - 25
src/Collection/CollectionTrait.php

@@ -46,7 +46,7 @@ trait CollectionTrait
      */
     public function each(callable $c)
     {
-        foreach ($this->_unwrap() as $k => $v) {
+        foreach ($this->unwrap() as $k => $v) {
             $c($v, $k);
         }
         return $this;
@@ -64,7 +64,7 @@ trait CollectionTrait
                 return (bool)$v;
             };
         }
-        return new FilterIterator($this->_unwrap(), $c);
+        return new FilterIterator($this->unwrap(), $c);
     }
 
     /**
@@ -74,7 +74,7 @@ trait CollectionTrait
      */
     public function reject(callable $c)
     {
-        return new FilterIterator($this->_unwrap(), function ($key, $value, $items) use ($c) {
+        return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($c) {
             return !$c($key, $value, $items);
         });
     }
@@ -85,7 +85,7 @@ trait CollectionTrait
      */
     public function every(callable $c)
     {
-        foreach ($this->_unwrap() as $key => $value) {
+        foreach ($this->unwrap() as $key => $value) {
             if (!$c($value, $key)) {
                 return false;
             }
@@ -99,7 +99,7 @@ trait CollectionTrait
      */
     public function some(callable $c)
     {
-        foreach ($this->_unwrap() as $key => $value) {
+        foreach ($this->unwrap() as $key => $value) {
             if ($c($value, $key) === true) {
                 return true;
             }
@@ -113,7 +113,7 @@ trait CollectionTrait
      */
     public function contains($value)
     {
-        foreach ($this->_unwrap() as $v) {
+        foreach ($this->unwrap() as $v) {
             if ($value === $v) {
                 return true;
             }
@@ -128,7 +128,7 @@ trait CollectionTrait
      */
     public function map(callable $c)
     {
-        return new ReplaceIterator($this->_unwrap(), $c);
+        return new ReplaceIterator($this->unwrap(), $c);
     }
 
     /**
@@ -143,7 +143,7 @@ trait CollectionTrait
         }
 
         $result = $zero;
-        foreach ($this->_unwrap() as $k => $value) {
+        foreach ($this->unwrap() as $k => $value) {
             if ($isFirst) {
                 $result = $value;
                 $isFirst = false;
@@ -161,7 +161,7 @@ trait CollectionTrait
      */
     public function extract($matcher)
     {
-        return new ExtractIterator($this->_unwrap(), $matcher);
+        return new ExtractIterator($this->unwrap(), $matcher);
     }
 
     /**
@@ -170,7 +170,7 @@ trait CollectionTrait
      */
     public function max($callback, $type = SORT_NUMERIC)
     {
-        return (new SortIterator($this->_unwrap(), $callback, SORT_DESC, $type))->first();
+        return (new SortIterator($this->unwrap(), $callback, SORT_DESC, $type))->first();
     }
 
     /**
@@ -179,7 +179,7 @@ trait CollectionTrait
      */
     public function min($callback, $type = SORT_NUMERIC)
     {
-        return (new SortIterator($this->_unwrap(), $callback, SORT_ASC, $type))->first();
+        return (new SortIterator($this->unwrap(), $callback, SORT_ASC, $type))->first();
     }
 
     /**
@@ -188,7 +188,7 @@ trait CollectionTrait
      */
     public function sortBy($callback, $dir = SORT_DESC, $type = SORT_NUMERIC)
     {
-        return new SortIterator($this->_unwrap(), $callback, $dir, $type);
+        return new SortIterator($this->unwrap(), $callback, $dir, $type);
     }
 
     /**
@@ -234,7 +234,7 @@ trait CollectionTrait
         $reducer = function ($values, $key, $mr) {
             $mr->emit(count($values), $key);
         };
-        return new Collection(new MapReduce($this->_unwrap(), $mapper, $reducer));
+        return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
     }
 
     /**
@@ -278,7 +278,7 @@ trait CollectionTrait
      */
     public function take($size = 1, $from = 0)
     {
-        return new Collection(new LimitIterator($this->_unwrap(), $from, $size));
+        return new Collection(new LimitIterator($this->unwrap(), $from, $size));
     }
 
     /**
@@ -316,10 +316,9 @@ trait CollectionTrait
      */
     public function append($items)
     {
-        $items = $items instanceof Iterator ? $items : new Collection($items);
         $list = new AppendIterator;
-        $list->append($this);
-        $list->append($items->_unwrap());
+        $list->append($this->unwrap());
+        $list->append((new Collection($items))->unwrap());
         return new Collection($list);
     }
 
@@ -359,7 +358,7 @@ trait CollectionTrait
             $mapReduce->emit($result, $key);
         };
 
-        return new Collection(new MapReduce($this->_unwrap(), $mapper, $reducer));
+        return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
     }
 
     /**
@@ -402,7 +401,7 @@ trait CollectionTrait
             $parents[$key]['children'] = $children;
         };
 
-        return (new Collection(new MapReduce($this->_unwrap(), $mapper, $reducer)))
+        return (new Collection(new MapReduce($this->unwrap(), $mapper, $reducer)))
             ->map(function ($value) use (&$isObject) {
                 return $isObject ? $value : $value->getArrayCopy();
             });
@@ -415,7 +414,7 @@ trait CollectionTrait
      */
     public function insert($path, $values)
     {
-        return new InsertIterator($this->_unwrap(), $path, $values);
+        return new InsertIterator($this->unwrap(), $path, $values);
     }
 
     /**
@@ -424,7 +423,7 @@ trait CollectionTrait
      */
     public function toArray($preserveKeys = true)
     {
-        $iterator = $this->_unwrap();
+        $iterator = $this->unwrap();
         if ($iterator instanceof ArrayIterator) {
             $items = $iterator->getArrayCopy();
             return $preserveKeys ? $items : array_values($items);
@@ -540,13 +539,12 @@ trait CollectionTrait
         return iterator_count($this->take(1)) === 0;
     }
 
+
     /**
-     * Returns the closest nested iterator that can be safely traversed without
-     * losing any possible transformations.
+     * {@inheritDoc}
      *
-     * @return \Iterator
      */
-    protected function _unwrap()
+    public function unwrap()
     {
         $iterator = $this;
         while (get_class($iterator) === 'Cake\Collection\Collection') {
@@ -554,4 +552,15 @@ trait CollectionTrait
         }
         return $iterator;
     }
+
+    /**
+     * Backwards compatible wrapper for unwrap()
+     *
+     * @return \Iterator
+     * @deprecated
+     */
+    public function _unwrap()
+    {
+        return $this->unwrap();
+    }
 }

+ 6 - 2
src/Database/Schema/SqliteSchema.php

@@ -253,13 +253,17 @@ class SqliteSchema extends BaseSchema
         if (in_array($data['type'], $hasUnsigned, true) &&
             isset($data['unsigned']) && $data['unsigned'] === true
         ) {
-            $out .= ' UNSIGNED';
+            if ($data['type'] !== 'integer' || [$name] !== (array)$table->primaryKey()) {
+                $out .= ' UNSIGNED';
+            }
         }
         $out .= $typeMap[$data['type']];
 
         $hasLength = ['integer', 'string'];
         if (in_array($data['type'], $hasLength, true) && isset($data['length'])) {
-            $out .= '(' . (int)$data['length'] . ')';
+            if ($data['type'] !== 'integer' || [$name] !== (array)$table->primaryKey()) {
+                $out .= '(' . (int)$data['length'] . ')';
+            }
         }
         $hasPrecision = ['float', 'decimal'];
         if (in_array($data['type'], $hasPrecision, true) &&

+ 3 - 2
src/Datasource/EntityTrait.php

@@ -242,7 +242,6 @@ trait EntityTrait
                 continue;
             }
 
-            unset($this->_mutated[$p]);
             $this->dirty($p, true);
 
             if (!isset($this->_original[$p]) &&
@@ -263,6 +262,8 @@ trait EntityTrait
             }
             $this->_properties[$p] = $value;
         }
+
+        $this->_mutated = [];
         return $this;
     }
 
@@ -364,9 +365,9 @@ trait EntityTrait
         foreach ($property as $p) {
             unset($this->_properties[$p]);
             unset($this->_dirty[$p]);
-            unset($this->_mutated[$p]);
         }
 
+        $this->_mutated = [];
         return $this;
     }
 

+ 15 - 0
src/Form/Form.php

@@ -188,4 +188,19 @@ class Form
     {
         return true;
     }
+
+    /**
+     * Get the printable version of a Form instance.
+     *
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        $special = [
+            '_schema' => $this->schema()->__debugInfo(),
+            '_errors' => $this->errors(),
+            '_validator' => $this->validator()->__debugInfo()
+        ];
+        return $special + get_object_vars($this);
+    }
 }

+ 12 - 0
src/Form/Schema.php

@@ -121,4 +121,16 @@ class Schema
         }
         return $field['type'];
     }
+
+    /**
+     * Get the printable version of this object
+     *
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        return [
+            '_fields' => $this->_fields
+        ];
+    }
 }

+ 10 - 5
src/ORM/Marshaller.php

@@ -282,22 +282,27 @@ class Marshaller
 
         $primaryKey = array_flip($assoc->target()->schema()->primaryKey());
         $records = [];
+        $conditions = [];
+        $primaryCount = count($primaryKey);
 
         foreach ($data as $i => $row) {
             if (array_intersect_key($primaryKey, $row) === $primaryKey) {
-                if (!isset($query)) {
-                    $primaryCount = count($primaryKey);
-                    $query = $assoc->find();
-                }
                 $keys = array_intersect_key($row, $primaryKey);
                 if (count($keys) === $primaryCount) {
-                    $query->orWhere($keys);
+                    $conditions[] = $keys;
                 }
             } else {
                 $records[$i] = $this->one($row, $options);
             }
         }
 
+        if (!empty($conditions)) {
+            $query = $assoc->target()->find();
+            $query->andWhere(function ($exp) use ($conditions) {
+                return $exp->or_($conditions);
+            });
+        }
+
         if (isset($query)) {
             $keyFields = array_keys($primaryKey);
 

+ 37 - 41
src/Shell/Task/ExtractTask.php

@@ -243,31 +243,26 @@ class ExtractTask extends Shell
      *
      * @param string $domain The domain
      * @param string $msgid The message string
-     * @param array $details The file and line references
+     * @param array $details Context and plural form if any, file and line references
      * @return void
      */
     protected function _addTranslation($domain, $msgid, $details = [])
     {
-        if (empty($this->_translations[$domain][$msgid])) {
-            $this->_translations[$domain][$msgid] = [
-                'msgid_plural' => false,
-                'msgctxt' => ''
+        $context = isset($details['msgctxt']) ? $details['msgctxt'] : "";
+
+        if (empty($this->_translations[$domain][$msgid][$context])) {
+            $this->_translations[$domain][$msgid][$context] = [
+                'msgid_plural' => false
             ];
         }
 
         if (isset($details['msgid_plural'])) {
-            $this->_translations[$domain][$msgid]['msgid_plural'] = $details['msgid_plural'];
-        }
-        if (isset($details['msgctxt'])) {
-            $this->_translations[$domain][$msgid]['msgctxt'] = $details['msgctxt'];
+            $this->_translations[$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural'];
         }
 
         if (isset($details['file'])) {
-            $line = 0;
-            if (isset($details['line'])) {
-                $line = $details['line'];
-            }
-            $this->_translations[$domain][$msgid]['references'][$details['file']][] = $line;
+            $line = isset($details['line']) ? $details['line'] : 0;
+            $this->_translations[$domain][$msgid][$context]['references'][$details['file']][] = $line;
         }
     }
 
@@ -449,35 +444,36 @@ class ExtractTask extends Shell
         $paths = $this->_paths;
         $paths[] = realpath(APP) . DS;
         foreach ($this->_translations as $domain => $translations) {
-            foreach ($translations as $msgid => $details) {
-                $plural = $details['msgid_plural'];
-                $context = $details['msgctxt'];
-                $files = $details['references'];
-                $occurrences = [];
-                foreach ($files as $file => $lines) {
-                    $lines = array_unique($lines);
-                    $occurrences[] = $file . ':' . implode(';', $lines);
-                }
-                $occurrences = implode("\n#: ", $occurrences);
-                $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n";
+            foreach ($translations as $msgid => $contexts) {
+                foreach ($contexts as $context => $details) {
+                    $plural = $details['msgid_plural'];
+                    $files = $details['references'];
+                    $occurrences = [];
+                    foreach ($files as $file => $lines) {
+                        $lines = array_unique($lines);
+                        $occurrences[] = $file . ':' . implode(';', $lines);
+                    }
+                    $occurrences = implode("\n#: ", $occurrences);
+                    $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n";
 
-                $sentence = '';
-                if ($context) {
-                    $sentence .= "msgctxt \"{$context}\"\n";
-                }
-                if ($plural === false) {
-                    $sentence .= "msgid \"{$msgid}\"\n";
-                    $sentence .= "msgstr \"\"\n\n";
-                } else {
-                    $sentence .= "msgid \"{$msgid}\"\n";
-                    $sentence .= "msgid_plural \"{$plural}\"\n";
-                    $sentence .= "msgstr[0] \"\"\n";
-                    $sentence .= "msgstr[1] \"\"\n\n";
-                }
+                    $sentence = '';
+                    if ($context !== "") {
+                        $sentence .= "msgctxt \"{$context}\"\n";
+                    }
+                    if ($plural === false) {
+                        $sentence .= "msgid \"{$msgid}\"\n";
+                        $sentence .= "msgstr \"\"\n\n";
+                    } else {
+                        $sentence .= "msgid \"{$msgid}\"\n";
+                        $sentence .= "msgid_plural \"{$plural}\"\n";
+                        $sentence .= "msgstr[0] \"\"\n";
+                        $sentence .= "msgstr[1] \"\"\n\n";
+                    }
 
-                $this->_store($domain, $header, $sentence);
-                if ($domain !== 'default' && $this->_merge) {
-                    $this->_store('default', $header, $sentence);
+                    $this->_store($domain, $header, $sentence);
+                    if ($domain !== 'default' && $this->_merge) {
+                        $this->_store('default', $header, $sentence);
+                    }
                 }
             }
         }

+ 4 - 4
src/Utility/Inflector.php

@@ -511,8 +511,8 @@ class Inflector
             static::$_cache['irregular']['pluralize'] = '(?:' . implode('|', array_keys(static::$_irregular)) . ')';
         }
 
-        if (preg_match('/(.*)\\b(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) {
-            static::$_cache['pluralize'][$word] = $regs[1] . substr($word, 0, 1) .
+        if (preg_match('/(.*(?:\\b|_))(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) {
+            static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
                 substr(static::$_irregular[strtolower($regs[2])], 1);
             return static::$_cache['pluralize'][$word];
         }
@@ -551,8 +551,8 @@ class Inflector
             static::$_cache['irregular']['singular'] = '(?:' . implode('|', static::$_irregular) . ')';
         }
 
-        if (preg_match('/(.*)\\b(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) {
-            static::$_cache['singularize'][$word] = $regs[1] . substr($word, 0, 1) .
+        if (preg_match('/(.*(?:\\b|_))(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) {
+            static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
                 substr(array_search(strtolower($regs[2]), static::$_irregular), 1);
             return static::$_cache['singularize'][$word];
         }

+ 24 - 0
src/Validation/Validator.php

@@ -580,4 +580,28 @@ class Validator implements ArrayAccess, IteratorAggregate, Countable
         }
         return $errors;
     }
+
+    /**
+     * Get the printable version of this object.
+     *
+     * @return array
+     */
+    public function __debugInfo()
+    {
+        $fields = [];
+        foreach ($this->_fields as $name => $fieldSet) {
+            $fields[$name] = [
+                'isPresenceRequired' => $fieldSet->isPresenceRequired(),
+                'isEmptyAllowed' => $fieldSet->isEmptyAllowed(),
+                'rules' => array_keys($fieldSet->rules()),
+            ];
+        }
+        return [
+            '_presenceMessages' => $this->_presenceMessages,
+            '_allowEmptyMessages' => $this->_allowEmptyMessages,
+            '_useI18n' => $this->_useI18n,
+            '_providers' => array_keys($this->_providers),
+            '_fields' => $fields
+        ];
+    }
 }

+ 1 - 1
src/View/Widget/DateTimeWidget.php

@@ -525,8 +525,8 @@ class DateTimeWidget implements WidgetInterface
         if ($leadingZero === false) {
             $i = 1;
             foreach ($months as $key => $name) {
-                $months[$i++] = $name;
                 unset($months[$key]);
+                $months[$i++] = $name;
             }
         }
 

+ 40 - 0
tests/TestCase/Collection/CollectionTest.php

@@ -17,9 +17,31 @@ namespace Cake\Test\TestCase\Collection;
 use ArrayIterator;
 use ArrayObject;
 use Cake\Collection\Collection;
+use Cake\Collection\CollectionInterface;
+use Cake\Collection\CollectionTrait;
 use Cake\TestSuite\TestCase;
 use NoRewindIterator;
 
+class TestCollection extends \IteratorIterator implements CollectionInterface
+{
+    use CollectionTrait;
+
+
+    public function __construct($items)
+    {
+        if (is_array($items)) {
+            $items = new \ArrayIterator($items);
+        }
+
+        if (!($items instanceof \Traversable)) {
+            $msg = 'Only an array or \Traversable is allowed for Collection';
+            throw new \InvalidArgumentException($msg);
+        }
+
+        parent::__construct($items);
+    }
+}
+
 /**
  * CollectionTest
  *
@@ -690,6 +712,24 @@ class CollectionTest extends TestCase
     }
 
     /**
+     * Tests the append method with iterator
+     */
+    public function testAppendIterator()
+    {
+        $collection = new Collection([1, 2, 3]);
+        $iterator = new ArrayIterator([4, 5, 6]);
+        $combined = $collection->append($iterator);
+        $this->assertEquals([1, 2, 3, 4, 5, 6], $combined->toList());
+    }
+
+    public function testAppendNotCollectionInstance()
+    {
+        $collection = new TestCollection([1, 2, 3]);
+        $combined = $collection->append([4, 5, 6]);
+        $this->assertEquals([1, 2, 3, 4, 5, 6], $combined->toList());
+    }
+
+    /**
      * Tests that by calling compile internal iteration operations are not done
      * more than once
      *

+ 3 - 1
tests/TestCase/Database/Schema/SqliteSchemaTest.php

@@ -572,7 +572,9 @@ SQL;
         $table = new Table('articles');
         $table->addColumn('id', [
                 'type' => 'integer',
-                'null' => false
+                'null' => false,
+                'length' => 11,
+                'unsigned' => true
             ])
             ->addConstraint('primary', [
                 'type' => 'primary',

+ 14 - 0
tests/TestCase/Form/FormTest.php

@@ -154,4 +154,18 @@ class FormTest extends TestCase
 
         $this->assertTrue($form->execute($data));
     }
+
+    /**
+     * test __debugInfo
+     *
+     * @return void
+     */
+    public function testDebugInfo()
+    {
+        $form = new Form();
+        $result = $form->__debugInfo();
+        $this->assertArrayHasKey('_schema', $result);
+        $this->assertArrayHasKey('_errors', $result);
+        $this->assertArrayHasKey('_validator', $result);
+    }
 }

+ 24 - 0
tests/TestCase/Form/SchemaTest.php

@@ -116,4 +116,28 @@ class SchemaTest extends TestCase
         $this->assertEquals('decimal', $schema->fieldType('numbery'));
         $this->assertNull($schema->fieldType('nope'));
     }
+
+    /**
+     * test __debugInfo
+     *
+     * @return void
+     */
+    public function testDebugInfo()
+    {
+        $schema = new Schema();
+
+        $schema->addField('name', 'string')
+            ->addField('numbery', [
+                'type' => 'decimal',
+                'required' => true
+            ]);
+        $result = $schema->__debugInfo();
+        $expected = [
+            '_fields' => [
+                'name' => ['type' => 'string', 'length' => null, 'precision' => null],
+                'numbery' => ['type' => 'decimal', 'length' => null, 'precision' => null],
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
 }

+ 21 - 0
tests/TestCase/ORM/EntityTest.php

@@ -277,6 +277,27 @@ class EntityTest extends TestCase
         $this->assertEquals('Dr. ', $entity->get('name'));
     }
 
+    /**
+     * Tests that the get cache is cleared by setting any property.
+     * This is because virtual properties can often rely on other
+     * properties in the entity.
+     *
+     * @return void
+     */
+    public function testGetCacheClearedBySet()
+    {
+        $entity = $this->getMock('\Cake\ORM\Entity', ['_getName']);
+        $entity->last_name = 'Smith';
+        $entity->name = 'John';
+        $entity->expects($this->any())->method('_getName')
+            ->will($this->returnCallback(function ($name) use ($entity) {
+                return 'Dr. ' . $name . ' ' . $entity->last_name;
+            }));
+        $this->assertEquals('Dr. John Smith', $entity->get('name'));
+
+        $entity->last_name = 'Jones';
+        $this->assertEquals('Dr. John Jones', $entity->get('name'));
+    }
 
     /**
      * Test magic property setting with no custom setter

+ 41 - 0
tests/TestCase/ORM/MarshallerTest.php

@@ -1289,6 +1289,47 @@ class MarshallerTest extends TestCase
     }
 
     /**
+     * Tests that merging data to an entity containing belongsToMany as an array
+     * with additional association conditions works.
+     *
+     * @return void
+     */
+    public function testMergeBelongsToManyFromArrayWithConditions()
+    {
+        $this->articles->belongsToMany('Tags', [
+            'conditions' => ['ArticleTags.article_id' => 1]
+        ]);
+
+        $this->articles->Tags->eventManager()
+            ->on('Model.beforeFind', function ($event, $query) use (&$called) {
+                $called = true;
+                return $query->where(['Tags.id >=' => 1]);
+            });
+
+        $entity = new Entity([
+            'title' => 'No tags',
+            'body' => 'Some content here',
+            'tags' => []
+        ]);
+
+        $data = [
+            'title' => 'Haz moar tags',
+            'tags' => [
+                ['id' => 1],
+                ['id' => 2]
+            ]
+        ];
+        $entity->accessible('*', true);
+        $marshall = new Marshaller($this->articles);
+        $result = $marshall->merge($entity, $data, ['associated' => ['Tags']]);
+
+        $this->assertCount(2, $result->tags);
+        $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[0]);
+        $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[1]);
+        $this->assertTrue($called);
+    }
+
+    /**
      * Tests that merging data to an entity containing belongsToMany and _ids
      * will ignore empty values.
      *

+ 8 - 2
tests/TestCase/Shell/Task/ExtractTaskTest.php

@@ -108,8 +108,14 @@ class ExtractTaskTest extends TestCase
         $this->assertContains('msgid "double \\"quoted\\""', $result, 'Strings with quotes not handled correctly');
         $this->assertContains("msgid \"single 'quoted'\"", $result, 'Strings with quotes not handled correctly');
 
-        $pattern = '/\#: (\\\\|\/)extract\.ctp:31\n';
-        $pattern .= 'msgctxt "mail"/';
+        $pattern = '/\#: (\\\\|\/)extract\.ctp:\d+\n';
+        $pattern .= 'msgctxt "mail"\n';
+        $pattern .= 'msgid "letter"/';
+        $this->assertRegExp($pattern, $result);
+
+        $pattern = '/\#: (\\\\|\/)extract\.ctp:\d+\n';
+        $pattern .= 'msgctxt "alphabet"\n';
+        $pattern .= 'msgid "letter"/';
         $this->assertRegExp($pattern, $result);
 
         // extract.ctp - reading the domain.pot

+ 28 - 0
tests/TestCase/Utility/InflectorTest.php

@@ -175,6 +175,7 @@ class InflectorTest extends TestCase
         $this->assertEquals('files_metadata', Inflector::singularize('files_metadata'));
         $this->assertEquals('address', Inflector::singularize('addresses'));
         $this->assertEquals('sieve', Inflector::singularize('sieves'));
+        $this->assertEquals('blue_octopus', Inflector::singularize('blue_octopuses'));
         $this->assertEquals('', Inflector::singularize(''));
     }
 
@@ -253,10 +254,37 @@ class InflectorTest extends TestCase
         $this->assertEquals('stadia', Inflector::pluralize('stadia'));
         $this->assertEquals('Addresses', Inflector::pluralize('Address'));
         $this->assertEquals('sieves', Inflector::pluralize('sieve'));
+        $this->assertEquals('blue_octopuses', Inflector::pluralize('blue_octopus'));
         $this->assertEquals('', Inflector::pluralize(''));
     }
 
     /**
+     * testInflectingMultiWordIrregulars
+     *
+     * @return void
+     */
+    public function testInflectingMultiWordIrregulars()
+    {
+        // unset the default rules in order to avoid them possibly matching
+        // the words in case the irregular regex won't match, the tests
+        // should fail in that case
+        Inflector::rules('plural', [
+            'rules' => [],
+        ]);
+        Inflector::rules('singular', [
+            'rules' => [],
+        ]);
+
+        $this->assertEquals(Inflector::singularize('wisdom teeth'), 'wisdom tooth');
+        $this->assertEquals(Inflector::singularize('wisdom-teeth'), 'wisdom-tooth');
+        $this->assertEquals(Inflector::singularize('wisdom_teeth'), 'wisdom_tooth');
+
+        $this->assertEquals(Inflector::pluralize('sweet potato'), 'sweet potatoes');
+        $this->assertEquals(Inflector::pluralize('sweet-potato'), 'sweet-potatoes');
+        $this->assertEquals(Inflector::pluralize('sweet_potato'), 'sweet_potatoes');
+    }
+
+    /**
      * testSlug method
      *
      * @return void

+ 40 - 0
tests/TestCase/Validation/ValidatorTest.php

@@ -769,4 +769,44 @@ class ValidatorTest extends TestCase
         ];
         $this->assertNotEmpty($validator->errors($data), 'Validation should fail.');
     }
+
+    /**
+     * Test debugInfo helper method.
+     *
+     * @return void
+     */
+    public function testDebugInfo()
+    {
+        $validator = new Validator();
+        $validator->provider('test', $this);
+        $validator->add('title', 'not-empty', ['rule' => 'notEmpty']);
+        $validator->requirePresence('body');
+        $validator->allowEmpty('published');
+
+        $result = $validator->__debugInfo();
+        $expected = [
+            '_providers' => ['test'],
+            '_fields' => [
+                'title' => [
+                    'isPresenceRequired' => false,
+                    'isEmptyAllowed' => false,
+                    'rules' => ['not-empty'],
+                ],
+                'body' => [
+                    'isPresenceRequired' => true,
+                    'isEmptyAllowed' => false,
+                    'rules' => [],
+                ],
+                'published' => [
+                    'isPresenceRequired' => false,
+                    'isEmptyAllowed' => true,
+                    'rules' => [],
+                ],
+            ],
+            '_presenceMessages' => [],
+            '_allowEmptyMessages' => [],
+            '_useI18n' => true,
+        ];
+        $this->assertEquals($expected, $result);
+    }
 }

+ 58 - 0
tests/TestCase/View/Widget/DateTimeWidgetTest.php

@@ -415,6 +415,64 @@ class DateTimeWidgetTest extends TestCase
     }
 
     /**
+     * Test rendering month widget with names and values without leading zeros.
+     *
+     * @return void
+     */
+    public function testRenderMonthWidgetWithNamesNoLeadingZeros()
+    {
+        $now = new \DateTime('2010-12-01 12:00:00');
+        $result = $this->DateTime->render([
+            'name' => 'date',
+            'year' => false,
+            'day' => false,
+            'hour' => false,
+            'minute' => false,
+            'second' => false,
+            'month' => ['data-foo' => 'test', 'names' => true, 'leadingZeroKey' => false],
+            'meridian' => false,
+            'val' => $now,
+        ], $this->context);
+        $expected = [
+            'select' => ['name' => 'date[month]', 'data-foo' => 'test'],
+            ['option' => ['value' => '1']], 'January', '/option',
+            ['option' => ['value' => '2']], 'February', '/option',
+            ['option' => ['value' => '3']], 'March', '/option',
+            ['option' => ['value' => '4']], 'April', '/option',
+            ['option' => ['value' => '5']], 'May', '/option',
+            ['option' => ['value' => '6']], 'June', '/option',
+            ['option' => ['value' => '7']], 'July', '/option',
+            ['option' => ['value' => '8']], 'August', '/option',
+            ['option' => ['value' => '9']], 'September', '/option',
+            ['option' => ['value' => '10']], 'October', '/option',
+            ['option' => ['value' => '11']], 'November', '/option',
+            ['option' => ['value' => '12', 'selected' => 'selected']], 'December', '/option',
+            '/select',
+        ];
+        $this->assertHtml($expected, $result);
+        $this->assertNotContains(
+            '<option value="01">January</option>',
+            $result,
+            'no 01 in value'
+        );
+        $this->assertNotContains(
+            'value="0"',
+            $result,
+            'no 0 in value'
+        );
+        $this->assertNotContains(
+            'value="00"',
+            $result,
+            'no 00 in value'
+        );
+        $this->assertNotContains(
+            'value="13"',
+            $result,
+            'no 13 in value'
+        );
+    }
+    
+    /**
      * Test rendering month widget with names.
      *
      * @return void

+ 3 - 0
tests/test_app/TestApp/Template/Pages/extract.ctp

@@ -29,3 +29,6 @@ __('Hot features!'
 
 // Context
 echo __x('mail', 'letter');
+
+// Duplicated message with different context
+echo __x('alphabet', 'letter');