<?php
/**
* Test methods for the P4 Model Iterator.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
class P4_Model_IteratorTest extends TestCase
{
/**
* Test the constructor
*/
public function testConstructor()
{
// barebones construct
$iterator = new P4_Model_Iterator;
$this->assertTrue($iterator instanceof P4_Model_Iterator, 'should work');
$this->assertEquals($iterator->count(), 0, 'Models size');
// pass a non-array to the constructor
$iterator = new P4_Model_Iterator('bob');
$this->assertTrue($iterator instanceof P4_Model_Iterator, 'should work');
$this->assertEquals($iterator->count(), 0, 'Models size');
// pass bogus model to constructor
try {
$iterator = new P4_Model_Iterator(array(1));
} catch (InvalidArgumentException $e) {
$this->assertEquals(
'Models array contains one or more invalid elements.',
$e->getMessage(),
'Expected error message'
);
} catch (Exception $e) {
$this->assertFalse(true, 'Unexpected exception: '. $e->getMessage());
}
// pass a valid model to the constructor
$file = new P4_File;
$iterator = new P4_Model_Iterator(array($file));
$this->assertTrue($iterator instanceof P4_Model_Iterator, "should work");
$this->assertEquals($iterator->count(), 1, 'Models size');
// pass a mix array of valid/non-valid models to constructor
try {
$iterator = new P4_Model_Iterator(array($file, $file, 1));
} catch (InvalidArgumentException $e) {
$this->assertEquals(
'Models array contains one or more invalid elements.',
$e->getMessage(),
'Expected error message'
);
} catch (Exception $e) {
$this->assertFalse(true, 'Unexpected exception: '. $e->getMessage());
}
}
/**
* Test various iterator functionality
*/
public function testIterator()
{
// instantiate an iterator
$iterator = new P4_Model_Iterator;
// add a file
$file1 = new P4_File;
$file1->setContentCache('one');
$iterator[] = $file1;
$this->assertEquals($iterator->count(), 1, 'expected size');
$this->assertTrue($iterator->offsetExists(0), 'expected to exist');
$this->assertFalse($iterator->offsetExists(1), 'expected not to exist');
$this->assertFalse($iterator->offsetExists(2), 'expected not to exist');
$this->assertFalse($iterator->offsetExists('bob'), 'expected not to exist');
// add another file
$file2 = new P4_File;
$file2->setContentCache('two');
$iterator[] = $file2;
$this->assertEquals($iterator->count(), 2, 'expected size');
$this->assertTrue($iterator->offsetExists(0), 'expected to exist');
$this->assertTrue($iterator->offsetExists(1), 'expected to exist');
$this->assertFalse($iterator->offsetExists(2), 'expected not to exist');
$this->assertFalse($iterator->offsetExists('bob'), 'expected not to exist');
// rewind the iterator
$iterator->rewind();
$this->assertEquals($iterator->key(), 0, 'expected position');
$this->assertTrue($iterator->valid(), 'should be valid');
$aFile = $iterator->current();
$this->assertEquals($aFile->getDepotContents(), 'one', 'expected file content');
// retrieve an item from the iterator
$iterator->next();
$this->assertEquals($iterator->key(), 1, 'expected position');
$this->assertTrue($iterator->valid(), 'should be valid');
$aFile = $iterator->current();
$this->assertEquals($aFile->getDepotContents(), 'two', 'expected file content');
// retrieve another item from the iterator
$iterator->next();
$this->assertEquals($iterator->key(), null, 'expected invalid position');
$this->assertFalse($iterator->valid(), 'should not be valid');
// seek to position 1
$iterator->seek(1);
$this->assertEquals($iterator->key(), 1, 'expected position');
$this->assertTrue($iterator->valid(), 'should be valid');
$aFile = $iterator->current();
$this->assertEquals($aFile->getDepotContents(), 'two', 'expected file content');
// seek to position 0
$iterator->seek(0);
$this->assertEquals($iterator->key(), 0, 'expected position');
$this->assertTrue($iterator->valid(), 'should be valid');
$aFile = $iterator->current();
$this->assertEquals($aFile->getDepotContents(), 'one', 'expected file content');
// seek to a non-numeric position
try {
$iterator->seek('bob');
} catch(OutOfBoundsException $e) {
$this->assertEquals(
'Invalid seek position.',
$e->getMessage(),
'Expected error message'
);
} catch(Exception $e) {
$this->assertFalse(true, 'Unexpected exception: '. $e->getMessage());
}
// seek to a non-existant position
try {
$iterator->seek(123);
} catch(OutOfBoundsException $e) {
$this->assertEquals(
'Invalid seek position.',
$e->getMessage(),
'Expected error message'
);
} catch(Exception $e) {
$this->assertFalse(true, 'Unexpected exception: '. $e->getMessage());
}
// set a model at a specific offset
$file3 = new P4_File;
$file3->setContentCache('bob');
$iterator->offsetSet('bob', $file3);
$this->assertEquals($iterator->count(), 3, 'expected size');
$this->assertTrue($iterator->offsetExists(0), 'expected to exist');
$this->assertTrue($iterator->offsetExists(1), 'expected to exist');
$this->assertFalse($iterator->offsetExists(2), 'expected not to exist');
$this->assertTrue($iterator->offsetExists('bob'), 'expected to exist');
$this->assertEquals($iterator->key(), 'bob', 'expected position');
$iterator->rewind();
$this->assertEquals($iterator->key(), 0, 'expected position');
// retrieve a model at a specific offsets
$aFile = $iterator->offsetGet(1);
$this->assertEquals($iterator->key(), 0, 'expected position');
$this->assertEquals($aFile->getDepotContents(), 'two', 'expected file content');
$aFile = $iterator->offsetGet('bob');
$this->assertEquals($iterator->key(), '0', 'expected position');
$this->assertEquals($aFile->getDepotContents(), 'bob', 'expected file content');
try {
$aFile = $iterator->offsetGet('does-not-exist');
$this->fail('Expected error');
} catch (Exception $e) {
$this->assertTrue(true);
}
// test foreach access
$content = array();
foreach ($iterator as $item) {
$content[] = $item->getDepotContents();
}
$this->assertEquals(
array('one', 'two', 'bob'),
$content,
'expected foreach content'
);
// test unset
$iterator->offsetUnset('bob');
$this->assertEquals($iterator->count(), 2, 'expected size');
$this->assertTrue($iterator->offsetExists(0), 'expected to exist');
$this->assertTrue($iterator->offsetExists(1), 'expected to exist');
$this->assertFalse($iterator->offsetExists(2), 'expected not to exist');
$this->assertFalse($iterator->offsetExists('bob'), 'expected not to exist');
}
/**
* Test php array-walk functions and their equivalence
* with class-defined counterparts.
*/
public function testArrayWalk()
{
// init
$iterator = new P4_Model_Iterator;
$file1 = new P4_File;
$file1->setContentCache('first');
$iterator[] = $file1;
$file2 = new P4_File;
$file2->setContentCache('second');
$iterator[] = $file2;
$file3 = new P4_File;
$file3->setContentCache('third-special');
$iterator['special'] = $file3;
$file4 = new P4_File;
$file4->setContentCache('fourth');
$iterator[] = $file4;
$file5 = new P4_File;
$file5->setContentCache('fifth');
$iterator[] = $file5;
// test php array-walk functions
$iterator->rewind();
$this->assertEquals($iterator->count(), 5, '-> expected size');
$this->assertEquals(count($iterator), 5, 'expected size');
$this->assertEquals($iterator->key(), '0', '-> expected position');
$this->assertEquals(key($iterator), '0', 'expected position');
$this->assertEquals($iterator->current()->getDepotContents(), 'first', '-> expected file contents');
$this->assertEquals(current($iterator)->getDepotContents(), 'first', 'expected file contents');
$iterator->next();
$this->assertEquals($iterator->count(), 5, '-> expected size');
$this->assertEquals(count($iterator), 5, 'expected size');
$this->assertEquals($iterator->key(), '1', '-> expected position');
$this->assertEquals(key($iterator), '1', 'expected position');
$this->assertEquals($iterator->current()->getDepotContents(), 'second', '-> expected file contents');
$this->assertEquals(current($iterator)->getDepotContents(), 'second', 'expected file contents');
next($iterator);
$this->assertEquals($iterator->key(), 'special', '-> expected position');
$this->assertEquals(key($iterator), 'special', 'expected position');
$this->assertEquals($iterator->current()->getDepotContents(), 'third-special', '-> expected file contents');
$this->assertEquals(current($iterator)->getDepotContents(), 'third-special', 'expected file contents');
$iterator->next();
next($iterator);
$this->assertFalse($iterator->next(), '-> expected not to exist');
$this->assertFalse(next($iterator), 'expected not to exist');
reset($iterator);
$this->assertEquals($iterator->key(), '0', '-> expected position');
$this->assertEquals(key($iterator), '0', 'expected position');
$this->assertEquals(current($iterator)->getDepotContents(), 'first', 'expected file contents');
$this->assertEquals($iterator->current()->getDepotContents(), 'first', '-> expected file contents');
$iterator->next();
next($iterator);
next($iterator);
$this->assertEquals(key($iterator), '2', 'expected position');
$this->assertEquals($iterator->key(), '2', '-> expected position');
$this->assertEquals(current($iterator)->getDepotContents(), 'fourth', 'expected file contents');
$this->assertEquals($iterator->current()->getDepotContents(), 'fourth', '-> expected file contents');
// test each() function
$iterator->rewind();
$currentPair = each($iterator);
$this->assertEquals($currentPair['key'], '0', 'each() expected key');
$this->assertSame($currentPair['value'], $file1, 'each() expected value');
// check that array pointer has been advanced
$this->assertEquals($iterator->key(), '1', '-> expected position');
$this->assertEquals(key($iterator), '1', 'expected position');
$this->assertEquals(current($iterator)->getDepotContents(), 'second', 'expected file contents');
$this->assertEquals($iterator->current()->getDepotContents(), 'second', '-> expected file contents');
$currentPair = each($iterator);
$this->assertEquals($currentPair['key'], '1', 'each() expected key');
$this->assertSame($currentPair['value'], $file2, 'each() expected value');
// check that array pointer has been advanced
$this->assertEquals($iterator->key(), 'special', '-> expected position');
$this->assertEquals(key($iterator), 'special', 'expected position');
$this->assertEquals(current($iterator)->getDepotContents(), 'third-special', 'expected file contents');
$this->assertEquals($iterator->current()->getDepotContents(), 'third-special', '-> expected file contents');
next($iterator);
$iterator->next();
$this->assertEquals($iterator->key(), '3', '-> key() expected key');
$this->assertSame(current($iterator), $file5, 'current() expected value');
$this->assertFalse($iterator->next(), 'expected out of bounds');
}
/**
* Test invoke().
*/
public function testInvoke()
{
$iterator = new P4_Model_Iterator;
$file1 = new P4_File;
$file1->setContentCache('f1');
$iterator[] = $file1;
$file2 = new P4_File;
$file2->setContentCache('f2');
$iterator[] = $file2;
$file3 = new P4_File;
$file3->setContentCache('f3');
$iterator['special'] = $file3;
$fileDepotContentsArray = $iterator->invoke('getDepotContents');
$this->assertEquals(implode('-', $fileDepotContentsArray), 'f1-f2-f3', 'expected invoke result');
}
/**
* Test exceptions with invoke().
*/
public function testInvokeException()
{
$iterator = new P4_Model_Iterator;
$file1 = new P4_File;
$iterator[] = $file1;
$file2 = new P4_File;
$iterator[] = $file2;
// test exception if method doesnt exist
try {
$result = $iterator->invoke('nonexistentMethod99');
} catch (InvalidArgumentException $expected) {
return;
}
$this->fail(
'An expected exception InvalidArgumentException has not been raised '
. 'when trying invoke nonexistent method.'
);
}
/**
* Test retrieve the first iterator item.
*/
public function testFirst()
{
$iterator = new P4_Model_Iterator;
for ($i = 0; $i < 10; $i++) {
$file = new P4_File;
$file->setFilespec('//depot/' . $i);
$iterator[] = $file;
}
$this->assertTrue($iterator->first()->getFilespec() == '//depot/0');
$this->assertTrue($iterator->last()->getFilespec() == '//depot/9');
}
/**
* Test appending to an iterator.
*/
public function testAppend()
{
$iterator = new P4_Model_Iterator;
$iterator[] = new P4_File;
try {
$iterator[] = 123;
$this->fail();
} catch (InvalidArgumentException $e) {
$this->assertTrue(true);
}
}
/**
* Test searching feature of iterator.
*/
public function testSearch()
{
$tests = array(
array(
'label' => __LINE__ .': null fields, null query, null options',
'fields' => null,
'query' => null,
'options' => null,
'expected' => array()
),
array(
'label' => __LINE__ .': null fields, query, null options',
'fields' => null,
'query' => 'A',
'options' => null,
'expected' => array()
),
array(
'label' => __LINE__ .': fields, query, null options',
'fields' => array('bar'),
'query' => 'A',
'options' => null,
'expected' => array('A', 'a')
),
array(
'label' => __LINE__ .': fields, query, null options',
'fields' => array('bar'),
'query' => 'A',
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array('A')
),
array(
'label' => __LINE__ .': field mismatch, query, null options',
'fields' => array('baz'),
'query' => 'A',
'options' => null,
'expected' => array()
),
array(
'label' => __LINE__ .': multiple fields, query, null options',
'fields' => array('foo', 'baz'),
'query' => '3',
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array('C', 'c')
),
);
foreach ($tests as $test) {
$label = $test['label'];
// test the contents prior to filtering
$results = $this->_getTestIterator()->search($test['fields'], $test['query'], $test['options']);
$actual = $results->sortBy('bar')->invoke('getValue', array('bar'));
$this->assertSame($test['expected'], $actual, "$label - expected search results");
}
}
/**
* Test filtering feature of iterator.
*/
public function testFilter()
{
$tests = array(
array(
'label' => __LINE__ .': null fields, null values, null options',
'values' => null,
'options' => null,
'expected' => array(),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, null values, inverse filter',
'values' => null,
'options' => array(P4_Model_Iterator::FILTER_INVERSE),
'expected' => array('A', 'B', 'C', 'a', 'b', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, simple match values, null options',
'values' => array('A', 'C'),
'options' => null,
'expected' => array('A', 'C'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, simple match values, inverse filter',
'values' => array('A', 'C'),
'options' => array(P4_Model_Iterator::FILTER_INVERSE),
'expected' => array('B', 'a', 'b', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, regex match values, null options',
'values' => array('(3|5|6)'),
'options' => array(P4_Model_Iterator::FILTER_REGEX),
'expected' => array('C', 'b', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, regex match values, inverse filter',
'values' => array('(3|5|6)'),
'options' => array(P4_Model_Iterator::FILTER_REGEX, P4_Model_Iterator::FILTER_INVERSE),
'expected' => array('A', 'B', 'a'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, simple match values, no case',
'values' => array('a'),
'options' => array(P4_Model_Iterator::FILTER_NO_CASE),
'expected' => array('A', 'a'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, simple match values, no case inverse filter',
'values' => array('a'),
'options' => array(P4_Model_Iterator::FILTER_NO_CASE, P4_Model_Iterator::FILTER_INVERSE),
'expected' => array('B', 'C', 'b', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, regex match values, no case',
'values' => array('/b/'),
'options' => array(P4_Model_Iterator::FILTER_REGEX, P4_Model_Iterator::FILTER_NO_CASE),
'expected' => array('B', 'b'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, regex match values, no case inversed filter',
'values' => array('/B/'),
'options' => array(
P4_Model_Iterator::FILTER_REGEX,
P4_Model_Iterator::FILTER_NO_CASE,
P4_Model_Iterator::FILTER_INVERSE
),
'expected' => array('A', 'C', 'a', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, contains match values',
'values' => array('0'),
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array('B', 'a', 'b'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, contains match values, inverted filter',
'values' => array('0'),
'options' => array(P4_Model_Iterator::FILTER_CONTAINS, P4_Model_Iterator::FILTER_INVERSE),
'expected' => array('A', 'C', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, match all values #1',
'values' => array('/3/', '/e/'),
'options' => array(P4_Model_Iterator::FILTER_MATCH_ALL, P4_Model_Iterator::FILTER_REGEX),
'expected' => array('C', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, match all values #2',
'values' => array('/3/', '/e/', '/c/'),
'options' => array(P4_Model_Iterator::FILTER_MATCH_ALL, P4_Model_Iterator::FILTER_REGEX),
'expected' => array('c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, match all values #3, no case',
'values' => array('/3/', '/e/', '/c/'),
'options' => array(
P4_Model_Iterator::FILTER_MATCH_ALL,
P4_Model_Iterator::FILTER_REGEX,
P4_Model_Iterator::FILTER_NO_CASE
),
'expected' => array('C', 'c'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, match all values #4, no case inverted filter',
'values' => array('/3/', '/e/', '/c/'),
'options' => array(
P4_Model_Iterator::FILTER_MATCH_ALL,
P4_Model_Iterator::FILTER_REGEX,
P4_Model_Iterator::FILTER_NO_CASE,
P4_Model_Iterator::FILTER_INVERSE
),
'expected' => array('A', 'B', 'a', 'b'),
'copy' => false,
),
array(
'label' => __LINE__ .': null fields, null values, with copy',
'values' => null,
'options' => array(P4_Model_Iterator::FILTER_COPY),
'expected' => array(),
'copy' => true,
),
array(
'label' => __LINE__ .': null fields, simple match values, with copy',
'values' => array('A'),
'options' => array(P4_Model_Iterator::FILTER_COPY),
'expected' => array('A'),
'copy' => true,
),
array(
'label' => __LINE__ .': null fields, boolean values',
'values' => array(false),
'options' => array(P4_Model_Iterator::FILTER_COPY),
'expected' => array(false),
'copy' => true,
'extraData' => array(true, false, true),
),
array(
'label' => __LINE__ .': null fields, object values',
'values' => array('@'),
'options' => array(P4_Model_Iterator::FILTER_COPY),
'expected' => array('@'),
'copy' => true,
'extraData' => array(new stdClass, '@', new stdClass),
),
array(
'label' => __LINE__ .': null fields, nested values, implode enabled',
'values' => array('Z'),
'options' => array(P4_Model_Iterator::FILTER_IMPLODE, P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array(array('@', 'Y', 'Z')),
'copy' => false,
'extraData' => array(9, array('@', 'Y', 'Z'), new stdClass),
),
array(
'label' => __LINE__ .': null fields, nested values, implode disabled',
'values' => array('Z'),
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array(),
'copy' => false,
'extraData' => array(9, array('@', 'Y', 'Z'), new stdClass),
),
array(
'label' => __LINE__ .': null fields, null value',
'values' => array(null),
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array(1),
'copy' => false,
'extraData' => array(new stdClass, 1, null),
),
array(
'label' => __LINE__ .': null fields, \'null\' value',
'values' => array(null),
'options' => array(P4_Model_Iterator::FILTER_CONTAINS),
'expected' => array(),
'copy' => false,
'extraData' => array(new stdClass, 1, 'null'),
)
// see testSearch for tests involving specified/multiple fields
);
foreach ($tests as $test) {
$label = $test['label'];
// test the contents prior to filtering
$extraData = null;
$original = array('A', 'B', 'C', 'a', 'b', 'c');
if (array_key_exists('extraData', $test)) {
$extraData = $test['extraData'];
array_unshift($original, $extraData[1]);
}
$iterator = $this->_getTestIterator($extraData);
$actual = $iterator->sortBy('bar')->invoke('getValue', array('bar'));
$this->assertSame($original, $actual, "$label - expected initial items");
// test the content after filtering
$copy = $iterator->filter(null, $test['values'], $test['options']);
$actual = $copy->sortBy('bar')->invoke('getValue', array('bar'));
$this->assertSame($test['expected'], $actual, "$label - expected final items");
// verify modification, or lack thereof, to original iterator
if ($test['copy']) {
$actual = $iterator->sortBy('bar')->invoke('getValue', array('bar'));
$this->assertSame($original, $actual, "$label - expected no iterator change");
} else {
$expected = $actual;
$actual = $iterator->sortBy('bar')->invoke('getValue', array('bar'));
$this->assertSame($expected, $actual, "$label - expected iterator change");
}
}
}
/**
* Test sorting feature of iterator.
*/
public function testSort()
{
$tests = array(
array(
'label' => __LINE__ .': default sort',
'field' => 'bar',
'options' => array(),
'expected' => array('A', 'B', 'C', 'a', 'b', 'c'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': alpha sort #1',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_ALPHA),
'expected' => array('A', 'B', 'C', 'a', 'b', 'c'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': alpha sort #2',
'field' => 'baz',
'options' => array(P4_Model_Iterator::SORT_ALPHA),
'expected' => array('test 1', 'test 10', 'test 3', 'test 40', 'test 500', 'test 6'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': reverse sort',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_DESCENDING),
'expected' => array('c', 'b', 'a', 'C', 'B', 'A'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': case-insensitive sort',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_NO_CASE),
'expected' => array('a', 'a', 'b', 'b', 'c', 'c'),
'expectedLower' => true,
),
array(
'label' => __LINE__ .': reversed case-insensitive sort',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_NO_CASE, P4_Model_Iterator::SORT_DESCENDING),
'expected' => array('c', 'c', 'b', 'b', 'a', 'a'),
'expectedLower' => true,
),
array(
'label' => __LINE__ .': numeric sort',
'field' => 'foo',
'options' => array(P4_Model_Iterator::SORT_NUMERIC),
'expected' => array(1, 1, 2, 2, 3, 3),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': reversed numeric sort',
'field' => 'foo',
'options' => array(P4_Model_Iterator::SORT_NUMERIC, P4_Model_Iterator::SORT_DESCENDING),
'expected' => array(3, 3, 2, 2, 1, 1),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': natural sort',
'field' => 'baz',
'options' => array(P4_Model_Iterator::SORT_NATURAL),
'expected' => array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': natural, case-insensitive sort',
'field' => 'baz',
'options' => array(P4_Model_Iterator::SORT_NATURAL, P4_Model_Iterator::SORT_NO_CASE),
'expected' => array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': reversed natural sort',
'field' => 'baz',
'options' => array(P4_Model_Iterator::SORT_NATURAL, P4_Model_Iterator::SORT_DESCENDING),
'expected' => array('test 500', 'test 40', 'test 10', 'test 6', 'test 3', 'test 1'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': key/value natural sort #1',
'field' => 'baz',
'options' => array(P4_Model_Iterator::SORT_NATURAL => true),
'expected' => array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': key/value natural sort #2',
'field' => array(
array('baz', array(P4_Model_Iterator::SORT_NATURAL)),
),
'options' => array(),
'expected' => array(
array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': fixed sort - all specified',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_FIXED => array('B', 'a', 'C', 'A', 'b', 'c')),
'expected' => array('B', 'a', 'C', 'A', 'b', 'c'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': fixed sort - some specified',
'field' => 'bar',
'options' => array(P4_Model_Iterator::SORT_FIXED => array('B', 'a', 'C')),
'expected' => array('B', 'a', 'C', 'A', 'b', 'c'),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': nested sort',
'field' => array('foo', 'baz'),
'options' => array(P4_Model_Iterator::SORT_NATURAL),
'expected' => array(
array(1, 1, 2, 2, 3, 3),
array('test 1', 'test 40', 'test 10', 'test 500', 'test 3', 'test 6'),
),
'expectedLower' => false,
),
array(
'label' => __LINE__ .': nested sort, separate options',
'field' => array(
'foo' => array(P4_Model_Iterator::SORT_DESCENDING),
'baz' => array(P4_Model_Iterator::SORT_NATURAL)
),
'options' => array(),
'expected' => array(
array(3, 3, 2, 2, 1, 1),
array('test 3', 'test 6', 'test 10', 'test 500', 'test 1', 'test 40'),
),
'expectedLower' => false,
),
);
foreach ($tests as $test) {
$label = $test['label'];
$iterator = $this->_getTestIterator()->sortBy($test['field'], $test['options']);
$invokeList = $test['field'];
$expectedList = $test['expected'];
if (!is_array($test['field'])) {
$invokeList = (array) $test['field'];
$expectedList = array($expectedList);
}
foreach ($invokeList as $invokeWith => $value) {
$expected = array_shift($expectedList);
if (is_integer($invokeWith)) {
$invokeWith = is_array($value) ? $value[0] : $value;
}
$actual = $iterator->invoke('getValue', array($invokeWith));
if ($test['expectedLower']) {
$actual = array_map('strtolower', $actual);
}
$this->assertSame($expected, $actual, "$label - $invokeWith - expected order");
}
}
}
/**
* Test sorting the same iterator twice.
*/
public function testSortTwice()
{
// test default (alpha, asc) sort.
$iterator = $this->_getTestIterator()->sortBy('bar');
$this->assertSame(
array('A', 'B', 'C', 'a', 'b', 'c'),
$iterator->invoke('getValue', array('bar')),
"Expected alphabetical, ascending order."
);
// test natural sort (using same iterator).
$iterator->sortBy(
'baz',
array(P4_Model_Iterator::SORT_NATURAL)
);
$this->assertSame(
array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
$iterator->invoke('getValue', array('baz')),
"Expected natural, ascending order."
);
}
/**
* Test toArray()
*/
public function testToArray()
{
$iterator = $this->_getTestIterator()->sortBy('bar');
$expected = array('A', 'B', 'C', 'a', 'b', 'c');
$this->assertSame(
$expected,
$iterator->invoke('getValue', array('bar')),
"Expected alphabetical, ascending order."
);
$this->assertFalse(is_array($iterator), 'Iterator should not be an array.');
// shallow test
$array = $iterator->toArray(true);
$this->assertTrue(is_array($array), 'After toArray shallow, should have an array.');
$actual = array();
foreach ($array as $item) {
$actual[] = $item->getValue('bar');
}
$this->assertSame($expected, $actual, 'After toArray shallow, contents should match');
// deep test
$this->assertSame(
array(
array(
'foo' => 1,
'bar' => 'A',
'baz' => 'test 1'
),
array(
'foo' => 2,
'bar' => 'B',
'baz' => 'test 10'
),
array(
'foo' => 3,
'bar' => 'C',
'baz' => 'test 3'
),
array(
'foo' => 1,
'bar' => 'a',
'baz' => 'test 40'
),
array(
'foo' => 2,
'bar' => 'b',
'baz' => 'test 500'
),
array(
'foo' => 3,
'bar' => 'c',
'baz' => 'test 6'
),
),
array_values($iterator->toArray()),
'after toArray deep, contents should match'
);
}
/**
* Test sort w. unexpected options.
*/
public function testSortBadOptions()
{
$tests = array(
array(
'label' => __LINE__ .': null field, null options',
'field' => null,
'options' => null,
'error' => array(
'InvalidArgumentException' => 'Cannot sort. Sort options must be an array.'
),
),
array(
'label' => __LINE__ .': null field, non-array options',
'field' => null,
'options' => 'bob',
'error' => array(
'InvalidArgumentException' => 'Cannot sort. Sort options must be an array.'
),
),
array(
'label' => __LINE__ .': field w/non-array options',
'field' => array('bob' => 'bob'),
'options' => array(),
'error' => array(
'InvalidArgumentException' => 'Cannot sort. Invalid sort field(s) given.'
),
),
array(
'label' => __LINE__ .': field, unknown option',
'field' => 'foo',
'options' => array('bob'),
'error' => array(
'InvalidArgumentException' => 'Unexpected sort option(s) encountered.'
),
),
);
foreach ($tests as $test) {
$label = $test['label'];
try {
$iterator = $this->_getTestIterator()->sortBy($test['field'], $test['options']);
$this->fail("$label - Unexpected success.");
} catch (PHPUnit_Framework_AssertionFailedError $e) {
$this->fail($e->getMessage());
} catch (PHPUnit_Framework_ExpectationFailedError $e) {
$this->fail($e->getMessage());
} catch (Exception $e) {
list($class, $error) = each($test['error']);
$this->assertEquals(
$class,
get_class($e),
"$label - expected exception class: ". $e->getMessage()
);
$this->assertEquals(
$error,
$e->getMessage(),
"$label - expected exception message"
);
}
}
}
/**
* Test sorting with a callback function.
*/
public function testSortCallback()
{
$iterator = $this->_getTestIterator();
$callback = function($a, $b)
{
return strnatcasecmp($a->getValue('baz'), $b->getValue('baz'));
};
// ensure it requires a callback.
try {
$iterator->sortByCallback(null);
$this->fail('Expected exception');
} catch (InvalidArgumentException $e) {
$this->assertTrue(true);
}
// ensure sort works.
$iterator->sortByCallback($callback);
$this->assertSame(
array('test 1', 'test 3', 'test 6', 'test 10', 'test 40', 'test 500'),
$iterator->invoke('getValue', array('baz'))
);
}
/**
* Exercise starts-with filter option.
*/
public function testFilterStartsWith()
{
$iterator = $this->_getTestIterator();
$this->assertSame(
2,
$iterator->filter(null, 'test 1', array('STARTS_WITH'))->count()
);
}
/**
* Get a iterator to test sorting.
*
* @param array|null $extraData An optional extra data array to include in the iterator.
* @return P4_Model_Iterator the test iterator
*/
protected function _getTestIterator($extraData = null)
{
$data = array(
array(1, 'A', 'test 1'),
array(2, 'B', 'test 10'),
array(3, 'C', 'test 3'),
array(1, 'a', 'test 40'),
array(2, 'b', 'test 500'),
array(3, 'c', 'test 6'),
);
// facility to add some extra data to the iterator,
// which is used in testFilter.
if (isset($extraData)) {
$data[] = $extraData;
}
// randomize input data.
shuffle($data);
$iterator = new P4_Model_Iterator;
foreach ($data as $entry) {
$model = new P4_Model_Implementation;
$model->setValues(
array(
'foo' => $entry[0],
'bar' => $entry[1],
'baz' => $entry[2]
)
);
$iterator[] = $model;
}
return $iterator;
}
}