ChangeTest.php #1

  • //
  • guest/
  • thomas_gray/
  • jambox/
  • main/
  • swarm/
  • tests/
  • phpunit/
  • P4Test/
  • Spec/
  • ChangeTest.php
  • View
  • Commits
  • Open Download .zip Download (78 KB)
<?php
/**
 * Test methods for the P4 Change class.
 *
 * @copyright   2012 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace P4Test\Spec;

use P4Test\TestCase;
use P4\Connection\Connection;
use P4\Connection\Exception\CommandException;
use P4\Connection\Exception\ConflictException;
use P4\File\File;
use P4\File\Query as FileQuery;
use P4\Log\Logger;
use P4\Model\Fielded\Iterator as FieldedIterator;
use P4\Spec\Client;
use P4\Spec\Change;
use P4\Spec\Definition;
use P4\Spec\Exception\NotFoundException;
use P4\Spec\Exception\UnopenedException;
use P4\Spec\Exception\Exception as SpecException;
use P4\Spec\Job;
use P4\Spec\User;
use Zend\Log\Logger as ZendLogger;
use Zend\Log\Writer\Mock as MockLog;

class ChangeTest extends TestCase
{
    /**
     * Test instantiation.
     */
    public function testInstantiation()
    {
        $change = new Change;
        $this->assertTrue($change instanceof Change, 'Expected class');
        $originalConnection = $change->getConnection();

        // test setting connection via constructor.
        $p4 = Connection::factory();
        $change = new Change($p4);
        $this->assertSame(
            $p4,
            $change->getConnection(),
            'getConnection() should match connection passed to constructor.'
        );

        // ensure two connections differ.
        $this->assertNotSame(
            $change->getConnection(),
            $originalConnection,
            'Connections should not match.'
        );
    }

    /**
     * Test exists.
     */
    public function testExists()
    {
        // ensure id-exists returns false for non-existant change
        // (we have no changes yet)
        $this->assertFalse(Change::exists(123), 'Given change should not exist.');

        // test that a bogus id does not exist
        $this->assertFalse(Change::exists('*'), 'Bogus change should not exist.');

        // ensure default change exists.
        $this->assertTrue(Change::exists('default'), 'Default change should exist.');

        // create a change and ensure it exists.
        $change = new Change;
        $change->setDescription('this is a test');
        $change->save();

        // new change should have id of 1.
        $this->assertTrue($change->getId() === 1, 'Change number should be one.');

        // change number 1 should exist.
        $this->assertTrue(Change::exists(1), 'Change 1 should exist.');
        $this->assertTrue(Change::exists('1'), 'Change "1" should exist.');
    }

    /**
     * Test fetch.
     */
    public function testFetch()
    {
        // ensure fetch fails for a non-existant change.
        try {
            Change::fetch(1234);
            $this->fail('Fetch should fail for a non-existant change.');
        } catch (NotFoundException $e) {
            $this->assertTrue(true);
        }

        // ensure fetch succeeds for default change.
        try {
            Change::fetch('default');
            $this->assertTrue(true);
        } catch (NotFoundException $e) {
            $this->fail('Fetch should succeed for default change.');
        }

        // ensure fetch succeeds for numbered change.
        $change = new Change;
        $description = "this is a test\n";
        $change->setDescription($description);
        $change->save();
        try {
            $fetched = Change::fetch($change->getId());
            $this->assertTrue(true);
        } catch (NotFoundException $e) {
            $this->fail('Fetch should succeed for a numbered change that exists.');
        }

        // ensure it only takes one command to fetch a change and read
        // basic values from it -- we verify this by peeking at the log
        $original = Logger::hasLogger() ? Logger::getLogger() : null;
        $logger   = new ZendLogger;
        $mock     = new MockLog;
        $logger->addWriter($mock);
        Logger::setLogger($logger);

        $fetched = Change::fetch($change->getId());
        $this->assertSame($description, $fetched->getDescription());
        $this->assertInternalType('int', $fetched->getTime());
        $this->assertSame(1, count($mock->events));

        // restore original logger if there is one.
        Logger::setLogger($original);
    }

    /**
     * Test that there is no difference between a saved change and a fetched change.
     *
     * @todo test get files.
     */
    public function testSavedVsFetched()
    {
        // open a file for add.
        $file = new File;
        $file->setFilespec('//depot/test-file')
              ->add();

        // create a job.
        $job = new Job;
        $job->set('Description', 'fix something')
            ->save();

        // save a change with a file and a job.
        $change = new Change;
        $change->setDescription("a change with a file and a job.\n")
               ->setFiles(array($file->getFilespec()))
               ->setJobs(array($job->getId()))
               ->save();

        $fetched = Change::fetch($change->getId());

        $types = array('Id', 'Date', 'User', 'Status', 'Description', 'JobStatus', 'Jobs');
        foreach ($types as $type) {
            $method = "get$type";
            $this->assertSame($fetched->$method(), $change->$method(), "Expect matching $type.");
        }
    }

    /**
     * Test the fetch all method.
     */
    public function testFetchAll()
    {
        /*
        *         FETCH_MAXIMUM - set to integer value to limit to the
        *                         first 'max' number of entries.
        *     FETCH_BY_FILESPEC - set to a filespec to limit changes to those
        *                         affecting the file(s) matching the filespec.
        *       FETCH_BY_STATUS - set to a valid change status to limit result
        *                         to changes with that status (e.g. 'pending').
        *      FETCH_INTEGRATED - set to true to include changes integrated
        *                         into the specified files.
        *       FETCH_BY_CLIENT - set to a client to limit changes to those
        *                         on the named client.
        *         FETCH_BY_USER - set to a user to limit changes to those
        *                         owned by the named user.
        *           FETCH_AFTER - set to an id _after_ which to start collecting entries
        *                         note: entries seen before 'after' count towards max.
        */

        // create a file and submitted change
        $file1 = new File;
        $file1->setFilespec('//depot/path-a/test-file')->add()->setLocalContents('test')->submit('test-1');

        // create a file and submitted change
        $file2 = new File;
        $file2->setFilespec('//depot/path-b/test-file')->add()->setLocalContents('test')->submit('test-2');

        // create a pending change with 2 files
        $files = new FieldedIterator;
        $file3 = new File;
        $files[] = $file3->setFilespec('//depot/path-c/test-file1')
                         ->add()
                         ->setLocalContents('test');
        $file4 = new File;
        $files[] = $file4->setFilespec('//depot/path-c/test-file2')
                         ->add()
                         ->setLocalContents('test');
        $change = new Change;
        $change->setFiles($files)->setDescription("Has 2 files\n")->save();

        // create a change by another user, in another workspace.
        $user = new User;
        $password = 'AnotherPass';
        $user->setId('alternate')
             ->setEmail('[email protected]')
             ->setFullName('Alternate User')
             ->save();
        $client = new Client;
        $clientId = 'another-test-client';
        $client->setId($clientId)
               ->setRoot(DATA_PATH . "/clients/$clientId")
               ->setView(array('//depot/... //another-test-client/...'))
               ->save();
        $p4 = Connection::factory(
            $this->p4->getPort(),
            $user->getId(),
            $client->getId()
        );
        $change = new Change($p4);
        $change->setDescription("in alternate client\n")->save();

        // and have the alternate user integrate an existing file
        $integFilespec = '//depot/path-a/test-integ';
        $result = $p4->run('integrate', array('-f', $file1->getFilespec(), $integFilespec));

        $change = new Change($p4);
        $change->addFile($integFilespec)->setDescription('Integration')->submit();

        // Testing begins: ensure correct number of changes returned.
        $changes = Change::fetchAll();
        $this->assertEquals(
            5,
            $changes->count(),
            'There should be 5 changes.'
        );

        // ensure that first change matches last submitted change.
        $expected = array(
            'Change'        => 5,
            'Client'        => $clientId,
            'User'          => $user->getId(),
            'Status'        => 'submitted',
            'Description'   => "Integration\n",
            'Type'          => 'public',
            'JobStatus'     => null,
            'Jobs'          => array()
        );
        $actual = $changes->first()->get();
        unset($actual['Date'], $actual['Files']);
        $this->assertEquals(
            $expected,
            $actual,
            'Fetched change should match expected values.'
        );

        // battery of tests against this setup with various options
        $tests = array(
            array(
                'label' => __LINE__ .': defaults',
                'options'   => array(),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),

            array(
                'label' => __LINE__ .': fetch maximum 1',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 1,
                ),
                'expected'  => array(
                    "Integration\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 2',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 2,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 3',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 3,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 4',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 4,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                    "test-2",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 5',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 5,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 6',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 6,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 0',
                'options'   => array(
                    Change::FETCH_MAXIMUM => 0,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),

            array(
                'label' => __LINE__ .': fetch by filespec //depot/.../test-file',
                'options'   => array(
                    Change::FETCH_BY_FILESPEC => '//depot/.../test-file',
                ),
                'expected'  => array(
                    "test-2",
                    "test-1"
                ),
            ),

            array(
                'label' => __LINE__ .': fetch by status submitted',
                'options'   => array(
                    Change::FETCH_BY_STATUS => 'submitted',
                ),
                'expected'  => array(
                    "Integration\n",
                    "test-2",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch by status pending',
                'options'   => array(
                    Change::FETCH_BY_STATUS => 'pending',
                ),
                'expected'  => array(
                    "in alternate client\n",
                    "Has 2 files\n",
                ),
            ),

            array(
                'label' => __LINE__ .': fetch by client regular',
                'options'   => array(
                    Change::FETCH_BY_CLIENT => $this->p4->getClient(),
                ),
                'expected'  => array(
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch by client alternate',
                'options'   => array(
                    Change::FETCH_BY_CLIENT => $clientId,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),

            array(
                'label' => __LINE__ .': fetch by user regular',
                'options'   => array(
                    Change::FETCH_BY_USER => $this->p4->getUser(),
                ),
                'expected'  => array(
                    "Has 2 files\n",
                    "test-2",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch by user alternate',
                'options'   => array(
                    Change::FETCH_BY_USER => $user->getId(),
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),

            array(
                'label' => __LINE__ .': fetch without integrated',
                'options'   => array(
                    Change::FETCH_INTEGRATED  => false,
                    Change::FETCH_BY_FILESPEC => $integFilespec,
                ),
                'expected'  => array(
                    "Integration\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch with integrated',
                'options'   => array(
                    Change::FETCH_INTEGRATED  => true,
                    Change::FETCH_BY_FILESPEC => $integFilespec,
                ),
                'expected'  => array(
                    "Integration\n",
                    "test-1"
                ),
            ),
            array(
                'label' => __LINE__ .': fetch with after',
                'options'   => array(
                    Change::FETCH_AFTER => 4
                ),
                'expected'  => array(
                    "Has 2 files\n",
                    'test-2',
                    'test-1'
                ),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $changes = Change::fetchAll($test['options']);
            $this->assertEquals(
                count($test['expected']),
                count($changes),
                "$label - Expected change count."
            );
            $descriptions = array();
            foreach ($changes as $change) {
                $descriptions[] = $change->getDescription();
            }
            $this->assertSame(
                $test['expected'],
                $descriptions,
                "$label - Expected change descriptions."
            );
        }

    }

    /**
     * Test getJobs and setJobs.
     */
    public function testAddGetSetJobs()
    {
        // test initial state of jobs in a fresh change object.
        $change = new Change;
        $jobs = $change->getJobs();
        $this->assertTrue(is_array($jobs), 'Expect an array from getJobs.');
        $this->assertSame(0, count($jobs), 'There should be no jobs associated with a fresh change.');

        // create a job
        $job = new Job;
        $job->set('Description', 'This is job #1');
        $job->save();

        // create a change to associate with the job
        $change = new Change;
        $change->setJobs(array($job->getId()));
        $jobs = $change->getJobs();
        $this->assertSame(1, count($jobs), 'Expect one job.');
        $this->assertSame($job->getId(), $jobs[0], 'Expect matching job id.');

        // add another job
        $extraJobId = 'anotherJob';
        $change->addJob($extraJobId);
        $jobs = $change->getJobs();
        $this->assertSame(2, count($jobs), 'Expect two jobs.');
        $this->assertSame($job->getId(), $jobs[0], 'Expect matching job id.');
        $this->assertSame($extraJobId, $jobs[1], 'Expect matching job id for added job.');

        // attempt to save the change with a non-existant job
        try {
            $change->setDescription('A change with jobs.')
                   ->save('save the change');
            $this->fail('Unexpected success saving change with a non-existant job.');
        } catch (CommandException $e) {
            $this->assertRegexp(
                "/Job 'anotherJob' doesn't exist\./",
                $e->getMessage(),
                'Expected error saving change with a non-existant job.'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception saving change with a non-existant job ('
                . get_class($e) .') '. $e->getMessage()
            );
        }

        // create the non-existant job.
        $job2 = new Job;
        $job2->set('Description', 'Was non-existant')
             ->setId($extraJobId)
             ->save();

        // re-attempt saving the change now that the job exists.
        $change->setDescription('Change with jobs.')
               ->save();

        // fetch the changes we have, and verify the jobs.
        $changes = Change::fetchAll();
        $this->assertEquals(1, count($changes), 'Expect one change.');
        $this->assertSame(2, count($changes->first()->getJobs()), 'Expect 2 jobs with fetched change.');
        $this->assertSame(
            array($extraJobId, $job->getId()),
            $changes->first()->getJobs(),
            'Expected jobs in fetched change.'
        );
        $this->assertTrue($changes->first()->isPending(), 'Change should be pending.');
        $this->assertFalse($changes->first()->isSubmitted(), 'Change should not be submitted.');

        // now submit the change, and check the jobs.
        $file = new File;
        $file->setFilespec('//depot/file.txt')->add()->setLocalContents('File content.');
        $change->addFile($file);
        $change->submit();

        $changes = Change::fetchAll();
        $this->assertEquals(1, count($changes), 'Expect one change.');
        $this->assertSame(2, count($changes->first()->getJobs()), 'Expect 2 jobs with fetched & submitted change.');
        $this->assertSame(
            array($extraJobId, $job->getId()),
            $changes->first()->getJobs(),
            'Expected jobs in fetched & submitted change.'
        );
        $this->assertFalse($changes->first()->isPending(), 'Change should not be pending.');
        $this->assertTrue($changes->first()->isSubmitted(), 'Change should be submitted.');

        // create a new change, with non-existant jobId, and submit it.
        $change = new Change;
        $change->setDescription('Try submitting with non-existant job.');
        $thirdJobId = 'yetAnotherJob';
        $change->addJob($thirdJobId);
        $file = new File;
        $file->setFileSpec('//depot/file2.txt')->add()->setLocalContents('File content.');
        $change->addFile($file);
        $this->assertSame(null, $change->getId(), 'Expect id prior to submit.');

        try {
            $change->submit('Attempt 1.');
            $this->fail('Unexpected success submitting change with a non-existant job.');
        } catch (CommandException $e) {
            $this->assertRegexp(
                "/Change 2 created.*Job '$thirdJobId' doesn't exist\./s",
                $e->getMessage(),
                'Expected error message submitting change with a non-existant job.'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception submitting change with a non-existant job ('
                . get_class($e) .') '. $e->getMessage()
            );
        }
        $this->assertSame(2, $change->getId(), 'Expected id after submit.');

        // create the job
        $job3 = new Job;
        $job3->set('Description', 'Was non-existant')
             ->setId($thirdJobId)
             ->save();

        // submit should now succeed.
        $change->submit('Attempt 2.');
        $changes = Change::fetchAll();
        $this->assertEquals(2, count($changes), 'Expect two changes.');
        $this->assertEquals(1, count($changes->first()->getJobs()), 'Expect 1 job with second submitted change.');
        $this->assertSame(
            array($thirdJobId),
            $changes->first()->getJobs(),
            'Expected jobs in second submitted change.'
        );
    }

    /**
     * Test functionality of the setFixStatus() method.
     */
    public function testFixStatus()
    {
        // modify job spec to add custom statuses
        $spec = Definition::fetch('job');
        $fields = $spec->getFields();
        $fields['Status']['options'] = array('a', 'b', 'c', 'd', 'closed');
        $fields['Status']['default'] = 'a';
        $spec->setFields($fields)->save();

        // open a file for add
        $file = new File;
        $file->setFilespec("//depot/foo")->setLocalContents("this is a test")->add();

        // create couple of jobs
        $job1 = new Job;
        $job1->set('Description', 'job 1')
             ->save();
        $job2 = new Job;
        $job2->set('Description', 'job 2')
             ->setStatus('b')
             ->save();

        // save a change with fixStatus set to 'same'
        $change = new Change;
        $change->setDescription("change test 1.\n")
               ->addFile($file)
               ->setJobs(array($job1->getId(), $job2->getId()))
               ->setFixStatus('same')
               ->save();

        // verify job statuses before submit
        $this->assertSame('a', $job1->getStatus());
        $this->assertSame('b', $job2->getStatus());

        // submit a change and verify job statuses after
        $change->submit();

        $this->assertSame('a', Job::fetch($job1->getId())->getStatus());
        $this->assertSame('b', Job::fetch($job2->getId())->getStatus());

        // try again with a new change and fixStatus set to 'd'
        $file = new File;
        $file->setFilespec("//depot/bar")->setLocalContents("this is a test")->add();
        $change = new Change;
        $change->setDescription("change test 2.\n")
               ->addFile($file)
               ->setJobs(array($job1->getId(), $job2->getId()))
               ->setFixStatus('d')
               ->save()
               ->submit();

        // verify job statuses after submit
        $this->assertSame('d', Job::fetch($job1->getId())->getStatus());
        $this->assertSame('d', Job::fetch($job2->getId())->getStatus());

        // try again without using fixStatus
        $file = new File;
        $file->setFilespec("//depot/baz")->setLocalContents("this is a test")->add();
        $change = new Change;
        $change->setDescription("change test 3.\n")
               ->addFile($file)
               ->setJobs(array($job1->getId(), $job2->getId()))
               ->save()
               ->submit();

        // verify job statuses after submit
        $this->assertSame('closed', Job::fetch($job1->getId())->getStatus());
        $this->assertSame('closed', Job::fetch($job2->getId())->getStatus());
    }

    /**
     * Test getFiles and setFiles.
     */
    public function testGetSetFiles()
    {
        // test initial state of files in a fresh change object.
        $change = new Change;
        $files = $change->getFiles();
        $this->assertTrue(is_array($files), 'Expect an array from getFiles.');
        $this->assertSame(0, count($files), 'There should be no files associated with a fresh change.');

        // create two submitted changes, and one pending
        $file1 = new File;
        $filespec1 = '//depot/change1.txt';
        $file1->setFilespec($filespec1)->add()->setLocalContents('content1')->submit('File 1');

        $file2 = new File;
        $filespec2 = '//depot/change2.txt';
        $file2->setFilespec($filespec2)->add()->setLocalContents('content2')->submit('File 2');

        $file3 = new File;
        $filespec3 = '//depot/change3.txt';
        $file3->setFilespec($filespec3)->add()->setLocalContents('content3');
        $this->assertTrue($file3->isOpened(), 'File #3 should be opened.');

        // test that we get the appropriate files back for each change
        $change = Change::fetch(1);
        $this->assertFalse($change->isPending(), 'Change 1 should not be pending.');
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 1.');
        $this->assertSame($filespec1.'#1', $files[0], 'Expected filespec for change 1.');

        $change = Change::fetch(2);
        $this->assertFalse($change->isPending(), 'Change 2 should not be pending.');
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 2.');
        $this->assertSame($filespec2.'#1', $files[0], 'Expected filespec for change 2.');

        $change = Change::fetch('default');
        $this->assertTrue($change->isPending(), 'Change 3 should be pending.');
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 3.');
        $this->assertSame($filespec3, $files[0], 'Expected filespec for change 3.');

        // test that setting a comment on a pending changelist does not influence
        // getFiles handling.
        $change->setDescription('This is the default change.');
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 3.');
        $this->assertSame($filespec3, $files[0], 'Expected filespec for change 3.');

        // test that we cannot setFiles on a submitted changelist.
        try {
            $change = Change::fetch(2);
            $change->setFiles(array($filespec3));
            $this->fail('Unexpected success setting files on a submitted changelist.');
        } catch (SpecException $e) {
            $this->assertSame(
                'Cannot set files on a submitted change.',
                $e->getMessage(),
                'Expected error setting files on a submitted changelist'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception setting files on a submitted changelist: ('
                . get_class($e) .') '. $e->getMessage()
            );
        }

        // test that we can setFiles on a pending changelist, and that getFiles
        // returns the same list.
        $change = new Change;
        $change->setId('default');
        $change->setFiles(array($filespec1));
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 3 after setFiles.');
        $this->assertSame($filespec1, $files[0], 'Expected filespec for change 3 after setFiles.');

        // test that we can set the files to null to empty the list, and that we get
        // empty list in return.
        $change->setFiles(null);
        $files = $change->getFiles();
        $this->assertSame(0, count($files), 'There should now be no files associated with change 3.');

        // test that fetching a new change object returns the original files.
        $change = Change::fetch('default');
        $files = $change->getFiles();
        $this->assertSame(1, count($files), 'There should be one file associated with change 3.');
        $this->assertSame($filespec3, $files[0], 'Expected filespec for change 3.');

        // test that we can setFiles with an iterator of P4\File objects.
        $files = new FieldedIterator;
        $files[] = $file1;
        $files[] = $file2;
        $files[] = $file3;
        $change->setFiles($files);
        $files = $change->getFiles();
        $this->assertSame(3, count($files), 'There should now be three files associated with change 3.');
        $this->assertSame($filespec1, $files[0], 'Expected filespec for change 3, file 0.');
        $this->assertSame($filespec2, $files[1], 'Expected filespec for change 3, file 1.');
        $this->assertSame($filespec3, $files[2], 'Expected filespec for change 3, file 2.');
    }

    /**
     * Test accessors.
     */
    public function testAccessors()
    {
        // save a basic change.
        $change = new Change;
        $description = "this is a test\n";
        $change->setDescription($description);
        $change->save();

        // open a file for add.
        $file = new File;
        $file->setFilespec('//depot/test-file')
              ->add();

        // create a job.
        $job = new Job;
        $job->set('Description', 'fix something')
            ->save();

        // save a change with a file and a job.
        $change2      = new Change;
        $description2 = "a change with a file and a job.\n";
        $change2->setDescription($description2)
                ->setFiles(array($file->getFilespec()))
                ->setJobs(array($job->getId()))
                ->save();

        // ensure fetched change contains expected data.
        $tests = array(

            // test accessors for a brand new (unpopulated) change object.
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getId',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getDate',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getUser',
                'expected'      => $this->p4->getUser()
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getClient',
                'expected'      => $this->p4->getClient()
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getStatus',
                'expected'      => 'pending'
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getDescription',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getJobStatus',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new Change,
                'method'        => 'getJobs',
                'expected'      => array()
            ),

            // test accessors for a saved change object.
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getId',
                'expected'      => 1
            ),
            // datetime not tested here - see later test
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getUser',
                'expected'      => $this->p4->getUser()
            ),
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getClient',
                'expected'      => $this->p4->getClient()
            ),
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getStatus',
                'expected'      => 'pending'
            ),
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getDescription',
                'expected'      => $description
            ),
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getJobStatus',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': Saved change object',
                'change'        => $change,
                'method'        => 'getJobs',
                'expected'      => array()
            ),

            // test accessors for a change object with a file and a job attached.
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getId',
                'expected'      => 2
            ),
            // datetime not tested here - see later test
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getUser',
                'expected'      => $this->p4->getUser()
            ),
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getClient',
                'expected'      => $this->p4->getClient()
            ),
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getStatus',
                'expected'      => 'pending'
            ),
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getDescription',
                'expected'      => $description2
            ),
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getJobStatus',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': Change w. a file and a job',
                'change'        => $change2,
                'method'        => 'getJobs',
                'expected'      => array($job->getId())
            ),
        );

        // run each test.
        foreach ($tests as $test) {
            $label = $test['label'] .' - '. $test['method'];
            $this->assertSame(
                $test['expected'],
                $test['change']->{$test['method']}(),
                "$label - expected value."
            );
        }
    }

    /**
     * test setDescription.
     */
    public function testSetDescription()
    {
        $tests = array(
            array(
                'label'       => __LINE__ .': null',
                'description' => null,
                'error'       => false,
            ),
            array(
                'label'       => __LINE__ .': empty string',
                'description' => '',
                'error'       => false,
            ),
            array(
                'label'       => __LINE__ .': array',
                'description' => array(),
                'error'       => true,
            ),
            array(
                'label'       => __LINE__ .': integer',
                'description' => 123,
                'error'       => true,
            ),
            array(
                'label'       => __LINE__ .': float',
                'description' => -1.23,
                'error'       => true,
            ),
            array(
                'label'       => __LINE__ .': string',
                'description' => "have a nice day\n",
                'error'       => false,
            ),
        );

        foreach ($tests as $test) {
            $change = new Change;
            $label = $test['label'];
            try {
                $change->setDescription($test['description']);
                if ($test['error']) {
                    $this->fail("$label - Unexpected success.");
                }
            } catch (\InvalidArgumentException $e) {
                if ($test['error']) {
                    $this->assertSame(
                        'Cannot set description. Invalid type given.',
                        $e->getMessage(),
                        "$label - Expected error."
                    );
                } else {
                    $this->fail("$label - Unexpected argument exception: ".  $e->getMessage());
                }
            } catch (\Exception $e) {
                $this->fail("$label - Unexpected exception: (". get_class($e) .') '.  $e->getMessage());
            }

            if (!$test['error']) {
                $this->assertSame(
                    $test['description'],
                    $change->getDescription(),
                    "$label - Expect to get same description as set."
                );
            }
        }
    }

    /**
     * test setFiles.
     *
     * @todo add a test for setting files on a submitted change when submits work.
     */
    public function testSetFiles()
    {
        // create an iterator with files
        $files = new FieldedIterator;
        $file = new File;
        $files[] = $file->setFilespec('//depot/file1.txt');
        $file = new File;
        $files[] = $file->setFilespec('//depot/file2.txt');
        $file = new File;
        $files[] = $file->setFilespec('//depot/file3.txt');

        $filesIteratorInvalid = new FieldedIterator;
        $filesIteratorInvalid[] = $file;
        $filesIteratorInvalid[] = new Client;

        $tests = array(
            array(
                'label'    => __LINE__ .': iterator',
                'files'    => $files,
                'error'    => false,
                'expected' => array(
                    $files[0]->getFilespec(),
                    $files[1]->getFilespec(),
                    $files[2]->getFilespec(),
                ),
            ),
            array(
                'label'    => __LINE__ .': invalid iterator',
                'files'    => $filesIteratorInvalid,
                'error'    => new \InvalidArgumentException('All files must be a string or P4\File'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': null',
                'files'    => null,
                'error'    => false,
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': empty string',
                'files'    => '',
                'error'    => new \InvalidArgumentException('Cannot set files. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': integer',
                'files'    => 123,
                'error'    => new \InvalidArgumentException('Cannot set files. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': float',
                'files'    => -1.23,
                'error'    => new \InvalidArgumentException('Cannot set files. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': string',
                'files'    => "have a nice day\n",
                'error'    => new \InvalidArgumentException('Cannot set files. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': empty array',
                'files'    => array(),
                'error'    => false,
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with numerics',
                'files'    => array(1, 2),
                'error'    => new \InvalidArgumentException('All files must be a string or P4\File'),
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with strings and numerics',
                'files'    => array('one', 2),
                'error'    => new \InvalidArgumentException('All files must be a string or P4\File'),
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with a string',
                'files'    => array('one'),
                'error'    => false,
                'expected' => array('one'),
            ),
            array(
                'label'    => __LINE__ .': array with multiple strings',
                'files'    => array('one', 'two', 'three'),
                'error'    => false,
                'expected' => array('one', 'two', 'three'),
            ),
        );

        foreach ($tests as $test) {
            $change = new Change;
            $label = $test['label'];
            try {
                $change->setFiles($test['files']);
                if ($test['error']) {
                    $this->fail("$label - Unexpected success.");
                }
            } catch (\Exception $e) {
                if ($test['error']) {
                    $this->assertSame(
                        $test['error']->getMessage(),
                        $e->getMessage(),
                        "$label - Expected error."
                    );
                    $this->assertSame(
                        get_class($test['error']),
                        get_class($e),
                        "$label - Expected error class."
                    );
                } else {
                    $this->fail("$label - Unexpected exception: (". get_class($e) .') '.  $e->getMessage());
                }
            }

            if (!$test['error']) {
                $this->assertTrue(
                    is_array($change->getFiles()),
                    "$label - Change getFiles should return array."
                );
                $this->assertSame(
                    count($test['expected']),
                    count($change->getFiles()),
                    "$label - Change should contain same number of files."
                );
            }
        }
    }

    /**
     * test setJobs.
     *
     * @todo add a test for setting jobs on a submitted change when submits work.
     */
    public function testSetJobs()
    {
        // create an iterator with jobs.
        $jobs   = new FieldedIterator;
        $job    = new Job;
        $jobs[] = $job->setId('job000001');
        $job    = new Job;
        $jobs[] = $job->setId('job000002');
        $job    = new Job;
        $jobs[] = $job->setId('job000003');

        $jobsIteratorInvalid = new FieldedIterator;
        $jobsIteratorInvalid[] = $job;
        $jobsIteratorInvalid[] = new Client;

        // define inputs to set jobs.
        $tests = array(
            array(
                'label'    => __LINE__ .': iterator',
                'jobs'     => $jobs,
                'error'    => false,
                'expected' => array('job000001', 'job000002', 'job000003'),
            ),
            array(
                'label'    => __LINE__ .': iterator with invalid element',
                'jobs'     => $jobsIteratorInvalid,
                'error'    => new \InvalidArgumentException('Each job must be a string.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': null',
                'jobs'     => null,
                'error'    => false,
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': empty string',
                'jobs'     => '',
                'error'    => new \InvalidArgumentException('Cannot set jobs. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': integer',
                'jobs'     => 123,
                'error'    => new \InvalidArgumentException('Cannot set jobs. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': float',
                'jobs'     => -1.23,
                'error'    => new \InvalidArgumentException('Cannot set jobs. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': string',
                'jobs'     => "have a nice day\n",
                'error'    => new \InvalidArgumentException('Cannot set jobs. Invalid type given.'),
                'expected' => false,
            ),
            array(
                'label'    => __LINE__ .': empty array',
                'jobs'     => array(),
                'error'    => false,
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with numerics',
                'jobs'     => array(1, 2),
                'error'    => new \InvalidArgumentException('Each job must be a string.'),
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with strings and numerics',
                'jobs'     => array('one', 2),
                'error'    => new \InvalidArgumentException('Each job must be a string.'),
                'expected' => array(),
            ),
            array(
                'label'    => __LINE__ .': array with a string',
                'jobs'     => array('one'),
                'error'    => false,
                'expected' => array('one'),
            ),
            array(
                'label'    => __LINE__ .': array with multiple strings',
                'jobs'     => array('one', 'two', 'three'),
                'error'    => false,
                'expected' => array('one', 'two', 'three'),
            ),
        );

        foreach ($tests as $test) {
            $change = new Change;
            $label = $test['label'];
            try {
                $change->setJobs($test['jobs']);
                if ($test['error']) {
                    $this->fail("$label - Unexpected success.");
                }
            } catch (\Exception $e) {
                if ($test['error']) {
                    $this->assertSame(
                        $test['error']->getMessage(),
                        $e->getMessage(),
                        "$label - Expected error."
                    );
                    $this->assertSame(
                        get_class($test['error']),
                        get_class($e),
                        "$label - Expected error class."
                    );
                } else {
                    $this->fail("$label - Unexpected exception: (". get_class($e) .') '.  $e->getMessage());
                }
            }

            if (!$test['error']) {
                $this->assertSame(
                    $test['expected'],
                    $change->getJobs(),
                    "$label - Expected jobs."
                );
            }
        }
    }

    /**
     * Test moving files between changelists.
     */
    public function testReopen()
    {
        // open a file for add.
        $file = new File;
        $file->setFilespec("//depot/test-file")
             ->add();

        // put the file in a change.
        $change1 = new Change;
        $change1->setDescription("test 1")
                ->addFile("//depot/test-file")
                ->save();
        $this->assertTrue(
            count($change1->getFiles()) == 1,
            "Change should have one file."
        );
        $this->assertTrue(
            in_array("//depot/test-file", $change1->getFiles()),
            "test-file should be in change."
        );

        // try to put the same file in a different change.
        $change2 = new Change;
        $change2->setDescription("test 2")
                ->addFile("//depot/test-file")
                ->save();
        $this->assertTrue(
            count($change2->getFiles()) == 1,
            "Change should have one file."
        );
        $this->assertTrue(
            in_array("//depot/test-file", $change2->getFiles()),
            "test-file should be in change."
        );

        // try to put same file back in first (now numbered) change.
        $change1->addFile("//depot/test-file")->save();
        $this->assertTrue(
            count($change1->getFiles()) == 1,
            "Change should have one file."
        );
        $this->assertTrue(
            in_array("//depot/test-file", $change1->getFiles()),
            "test-file should be in change."
        );

        // attempting to put a un-opened file in a change should fail.
        $change3 = new Change;
        try {
            $change3->setDescription("test 3")->addFile("//depot/foo")->save();
            $this->fail("Save change with unopened file should throw exception.");
        } catch (UnopenedException $e) {
            $this->assertTrue(true);
        }
    }

    /**
     * Test deleting changes.
     */
    public function testDelete()
    {
        // delete an unidentified change.
        $change = new Change;
        try {
            $change->delete();
            $this->fail("Delete change without id should fail.");
        } catch (SpecException $e) {
            $this->assertTrue(true);
        }

        // delete the default change.
        $change = new Change;
        $change->setId('default');
        try {
            $change->delete();
            $this->fail("Delete default change should fail.");
        } catch (SpecException $e) {
            $this->assertTrue(true);
        }

        // delete a non-existant change.
        $change = new Change;
        $change->setId('123');
        try {
            $change->delete();
            $this->fail("Delete non-existent change should fail.");
        } catch (NotFoundException $e) {
            $this->assertTrue(true);
        }

        // delete a real pending change w. no files.
        $change = new Change;
        $change->setDescription("test")->save()->delete();
        $this->assertFalse(
            Change::exists($change->getId()),
            "Deleted change should no longer exist."
        );

        // delete a real pending change w. files.
        $file = new File;
        $file->setFilespec("//depot/test-file")->add();
        $change = new Change;
        $change->setDescription("test")->addFile("//depot/test-file")->save()->delete();
        $this->assertFalse(
            Change::exists($change->getId()),
            "Deleted change should no longer exist."
        );

        // delete a real pending change w. jobs attached.
        $job = new Job;
        $job->set("Description", "test-job")->save();
        $change = new Change;
        $change->setDescription("test")->addJob($job->getId())->save()->delete();
        $this->assertFalse(
            Change::exists($change->getId()),
            "Deleted change should no longer exist."
        );

        // create a change under another client workspace.
        $client = new Client;
        $client->setId("another-test-client")
               ->setRoot(DATA_PATH . "/clients/another-test-client")
               ->save();
        $p4 = Connection::factory(
            $this->p4->getPort(),
            $this->p4->getUser(),
            $client->getId(),
            $this->getP4Params('password')
        );
        $change = new Change($p4);
        $change->setDescription("test-change")->save();
        $id = $change->getId();

        // delete another client's change.
        $change = Change::fetch($id);
        try {
            $change->delete();
            $this->fail("Delete of another client change should fail.");
        } catch (SpecException $e) {
            $this->assertTrue(true);
        }

        // delete again, but with force option.
        $change->delete(true);
        $this->assertFalse(
            Change::exists($change->getId()),
            "Deleted change should no longer exist."
        );

        // test delete of a submitted change.
        $file = new File;
        $file->setFilespec("//depot/foo")
             ->setLocalContents("this is a test")
             ->add()
             ->submit("test");

        $change = Change::fetch($file->getStatus('headChange'));
        try {
            $change->delete();
            $this->fail("Delete of submitted change should fail.");
        } catch (SpecException $e) {
            $this->assertSame(
                "Cannot delete a submitted change without the force option.",
                $e->getMessage(),
                "Unexpected exception message."
            );
        }

        try {
            $change->delete(true);
            $this->fail("Delete of submitted change should fail.");
        } catch (SpecException $e) {
            $this->assertSame(
                "Cannot delete a submitted change that contains files.",
                $e->getMessage(),
                "Unexpected exception message."
            );
        }

        // obliterate the files in change.
        $this->p4->run("obliterate", array("-y", "//...@=" . $change->getId()));
        $change = Change::fetch($change->getId());
        $change->delete(true);
        $this->assertFalse(
            Change::exists($change->getId()),
            "Change should no longer exist."
        );
    }

    /**
     * Test submitting of changes.
     */
    public function testSubmit()
    {
        $file = new File;
        $file->setFilespec("//depot/foo")->setLocalContents("this is a test")->add();

        // ensure no changes to start.
        $changes = Change::fetchAll();
        $this->assertSame(
            0,
            count($changes),
            "There should be no changes."
        );

        // do a submit.
        $change = new Change;
        $change->addFile($file)->submit("test submit");

        // ensure change was successful.
        $changes = Change::fetchAll();
        $this->assertSame(
            1,
            count($changes),
            "There should be one change."
        );
        $this->assertSame(
            Change::SUBMITTED_CHANGE,
            $change->getStatus(),
            "Change should be submitted."
        );

        // test that saving a submitted change results in exception
        try {
            $change->save();
            $this->fail('Unexpected success saving a submitted change.');
        } catch (SpecException $e) {
            $this->assertSame(
                'Cannot update a submitted change without the force option.',
                $e->getMessage(),
                'Expected exception saving a submitted change.'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception saving a submitted change ('
                . get_class($e) .') '. $e->getMessage()
            );
        }

        // test that submitted a submitted change results in exception
        try {
            $change->submit();
            $this->fail('Unexpected success submitting a submitted change.');
        } catch (SpecException $e) {
            $this->assertSame(
                'Can only submit pending changes.',
                $e->getMessage(),
                'Expected exception submitting a submitted change.'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception submitting a submitted change ('
                . get_class($e) .') '. $e->getMessage()
            );
        }

        // test that setting jobs on a submitted change results in exception
        try {
            $change->setJobs(array());
            $this->fail('Unexpected success setting jobs on a submitted change.');
        } catch (SpecException $e) {
            $this->assertSame(
                'Cannot set jobs on a submitted change.',
                $e->getMessage(),
                'Expected exception setting jobs on a submitted change.'
            );
        } catch (\Exception $e) {
            $this->fail(
                'Unexpected exception setting jobs on a submitted change ('
                . get_class($e) .') '. $e->getMessage()
            );
        }
    }

    /**
     * Verify submit works if change is renumbered (was broken at one point).
     */
    public function testSubmitRenumber()
    {
        $file = new File;
        $file->setFilespec("//depot/foo")->setLocalContents("this is a test")->add();

        // make the change.
        $change = new Change;
        $change->addFile($file)->save();

        // make another change to induce renumbering
        $change2 = new Change;
        $change2->setDescription('bump change number')->save();

        $change->submit("test submit");

        // ensure submit was successful.
        $changes = Change::fetchAll();
        $this->assertSame(
            2,
            count($changes),
            "There should be two changes."
        );
        $this->assertSame(
            Change::SUBMITTED_CHANGE,
            $change->getStatus(),
            "Change should be submitted."
        );
    }

    /**
     * Test submit resolve behavior.
     */
    public function testSubmitConflicts()
    {
        // create a second client.
        $client = new Client;
        $client->setId("client-2")
               ->setRoot($this->getP4Params('clientRoot') . '/client-2')
               ->addView("//depot/...", "//client-2/...")
               ->save();

        // connect w. second client.
        $p4 = Connection::factory(
            $this->p4->getPort(),
            $this->p4->getUser(),
            $client->getId(),
            $this->getP4Params('password')
        );

        // create a situation where resolve is needed.
        //  a. from the main test client add/submit 'foo', then edit it.
        //  b. from another client sync/edit/submit 'foo'.
        $file1 = new File;
        $file1->setFilespec("//depot/foo")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new File($p4);
        $file2->setFilespec("//depot/foo")
              ->sync()
              ->edit()
              ->submit("change 2");

        // try to submit a change w. files needing resolve.
        $change = new Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (ConflictException $e) {
            $files = $change->getFilesToResolve();
            $this->assertEquals(
                1,
                count($files),
                "Expected one file needing resolve for submit."
            );
            $this->assertSame(
                $file1->getFilespec(),
                $files[0]->getFilespec(),
                "Expected matching filespecs."
            );
        }

        // create a situation where revert is needed.
        //  a. from the main test client add 'foo'.
        //  b. from another client add/submit 'foo'.
        $file1 = new File;
        $file1->setFilespec("//depot/bar")
              ->setLocalContents("contents-1")
              ->add();
        $file2 = new File($p4);
        $file2->setFilespec("//depot/bar")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 2")
              ->edit()
              ->submit("change 3");

        // try to submit a change w. files needing revert.
        $change = new Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (ConflictException $e) {
            $files = $change->getFilesToRevert();
            $this->assertEquals(
                1,
                count($files),
                "Expected one file needing resolve for revert."
            );
            $this->assertSame(
                $file1->getFilespec(),
                $files[0]->getFilespec(),
                "Expected matching filespecs."
            );
        }

        // create another situation where revert is needed.
        //  a. from the main test client add/submit 'foo', then edit it.
        //  b. from another client sync/delete/submit 'foo'.
        $file1 = new File;
        $file1->setFilespec("//depot/baz")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new File($p4);
        $file2->setFilespec("//depot/baz")
              ->sync()
              ->delete()
              ->submit("change 2");

        // try to submit a change w. files needing revert.
        $change = new Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (ConflictException $e) {
            $files = $change->getFilesToRevert();
            $this->assertSame(
                1,
                count($files),
                "Expected one file needing resolve."
            );
            $this->assertSame(
                $file1->getFilespec(),
                $files[0]->getFilespec(),
                "Expected matching filespecs."
            );
        }
    }

    /**
     * Test submit resolve behavior when passing resolve options.
     */
    public function testSubmitResolveConflicts()
    {
        // create a second client.
        $client = new Client;
        $client->setId("client-2")
               ->setRoot($this->getP4Params('clientRoot') . '/client-2')
               ->addView("//depot/...", "//client-2/...")
               ->save();

        // connect w. second client.
        $p4 = Connection::factory(
            $this->p4->getPort(),
            $this->p4->getUser(),
            $client->getId(),
            $this->getP4Params('password')
        );

        // create a situation where resolve is needed.
        //  a. from the main test client add/submit 'foo', then edit it.
        //  b. from another client sync/edit/submit 'foo'.
        $file1 = new File;
        $file1->setFilespec("//depot/foo")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new File($p4);
        $file2->setFilespec("//depot/foo")
              ->sync()
              ->edit()
              ->submit("change 2");

        // try to submit a change w. files needing resolve.
        $change = new Change;
        $change->addFile($file1)
               ->submit("main client submit", Change::RESOLVE_ACCEPT_YOURS);
    }

    /**
     * test bad change numbers against P4\Validate\ChangeNumber
     */
    public function testBadValidateChangeNumber()
    {
        $tests = array (
            array(
                'label'     => __LINE__ .': null',
                'value'     => null,
            ),
            array(
                'label'     => __LINE__ .': empty',
                'value'     => '',
            ),
            array(
                'label'     => __LINE__ .': negative',
                'value'     => -1,
            ),
            array(
                'label'     => __LINE__ .': float',
                'value'     => 10.10,
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];

            $validator = new \P4\Validate\ChangeNumber();

            $this->assertSame(
                false,
                $validator->isValid($test['value']),
                "$label - Expected Invalid"
            );
        }
    }

    /**
     * Test a fetch/save use case that failed in the past.
     */
    public function testSave()
    {
        $change = Change::fetch('default');
        $change->setDescription('Test submit')
               ->save();
    }

    /**
     * Test a save but with no description
     */
    public function testSaveNoDescription()
    {
        $change = Change::fetch('default');
        $change->save();
    }

    /**
     * Test updating a submitted change description.
     */
    public function testUpdateSubmitted()
    {
        $file = new File;
        $file->setFilespec("//depot/foo")
             ->setLocalContents("this is a test")
             ->add()
             ->submit('original description');

        $change = Change::fetch(1);
        $this->assertSame("original description\n", $change->getDescription());

        $change->setDescription('updated description')
               ->save(true);

        $change = Change::fetch(1);
        $this->assertSame("updated description\n", $change->getDescription());
    }

    /**
     * Test reverting an entire change.
     */
    public function testRevert()
    {
        $file1 = new File;
        $file1->setFilespec('//depot/one');
        $file1->add();

        $file2 = new File;
        $file2->setFilespec('//depot/two');
        $file2->add();

        $file3 = new File;
        $file3->setFilespec('//depot/three');
        $file3->add();

        $change = new Change;
        $change->setDescription("Test change");
        $change->setFiles(array('//depot/two', '//depot/three'));
        $change->save();

        // check that we have three files open.
        $query = FileQuery::create()
                 ->addFilespec('//depot/...')
                 ->setLimitToOpened(true);
        $this->assertSame(
            3,
            File::fetchAll($query)->count(),
            "Expected three open files"
        );

        // revert the pending change.
        $change->revert();

        // check that we have one file open.
        $this->assertSame(
            1,
            File::fetchAll($query)->count(),
            "Expected one open file"
        );

        // check that the correct files are opened.
        $file1->clearStatusCache();
        $this->assertTrue($file1->isOpened());
        $file2->clearStatusCache();
        $this->assertFalse($file2->isOpened());
        $file3->clearStatusCache();
        $this->assertFalse($file3->isOpened());
    }

    /**
     * Test getFileObjects and getFileObject
     */
    public function testGetFileObjectsObject()
    {
        $file1 = new File;
        $file1->setFilespec('//depot/one')
              ->add()
              ->setLocalContents("contents-1");

        $file2 = new File;
        $file2->setFilespec('//depot/two')
              ->add()
              ->setLocalContents("contents-2");

        $change = new Change;
        $change->setDescription("Test change")
               ->setFiles(array($file1, $file2))
               ->save();

        $this->assertSame(
            $change->getFiles(),
            $change->getFileObjects()->invoke('getDepotFilename'),
            'Expected get files to match fileobject list pre-submit'
        );

        $change->submit('test');
        File::fetch('//depot/one')->edit()->submit('rev two');

        $this->assertSame(
            array('//depot/one#1', '//depot/two#1'),
            $change->getFiles(),
            'Expected matching list of post-submit files'
        );

        $this->assertSame(
            array('1', '1'),
            $change->getFileObjects()->invoke('getStatus', array('headRev')),
            'Expected properly reved file objects post submit'
        );

        $this->assertSame(
            '//depot/one',
            $change->getFileObject(File::fetch('//depot/one#2'))->getDepotFilename(),
            'Expected getFileObject to provide proper depot filename'
        );
        $this->assertSame(
            '//depot/two',
            $change->getFileObject(File::fetch('//depot/two'))->getDepotFilename(),
            'Expected getFileObject to provide proper depot filename'
        );
        $this->assertSame(
            '1',
            $change->getFileObject(File::fetch('//depot/one#2'))->getStatus('headRev'),
            'Expected getFileObject to provide proper rev'
        );

        try {
            $change->getFileObject('//depot/three');
            $this->fail('Expected exception on getFileObject for invalid entry');
        } catch (\InvalidArgumentException $e) {
        }

        try {
            $change->getFileObject(12);
            $this->fail('Expected exception on getFileObject for invalid type');
        } catch (\InvalidArgumentException $e) {

        }
    }

    /**
     * Test getJobObjects and getJobObject
     */
    public function testGetJobObjectsObject()
    {
        $job1 = new Job;
        $job1->setId('job1')
             ->setDescription('job1')
             ->save();
        $job2 = new Job;
        $job2->setId('job2')
             ->setDescription('job2')
             ->save();

        $change = new Change;
        $change->setDescription("Test change")
            ->setJobs(array($job1, $job2))
            ->save();

        $this->assertSame(
            $change->getJobs(),
            $change->getJobObjects()->invoke('getId'),
            'Expected get jobs to match job object list pre-submit'
        );

        $this->assertSame(
            'job1',
            $change->getJobObject('job1')->getId(),
            'Expected get job object to return result'
        );

        // verify invalid id fails
        try {
            $change->getJobObject('job3');
        } catch (\InvalidArgumentException $e) {
        }
    }

    /**
     * Test running fetch all with an array of ids
     */
    public function testFetchAllByIds()
    {
        $file = new File;
        $file->setFilespec("//depot/file")
             ->add()
             ->setLocalContents("one")
             ->submit("test");
        $file->edit()
             ->setLocalContents("two")
             ->submit("test2");
        $file->edit()
            ->setLocalContents("three")
            ->submit("test3");

        $changes = Change::fetchAll(array(Change::FETCH_BY_IDS => array('1', '3')));

        $this->assertSame(
            array(1, 3),
            $changes->invoke('getId'),
            'expected matching result'
        );
    }

    /**
     * Test getting changes data
     */
    public function testGetChangesData()
    {
        $change = new Change;
        try {
            $change->getChangesData();
            $this->assertFalse(true, "Expected exception");
        } catch (\P4\Spec\Exception\Exception $e) {
            $this->assertTrue(true);
        }

        $file = new File;
        $file->setFilespec("//depot/file")
             ->add()
             ->setLocalContents("one")
             ->submit("test");

        $change = Change::fetch(1);
        $data   = $change->getChangesData();

        $this->assertSame('1',         $data['change']);
        $this->assertSame('tester',    $data['user']);
        $this->assertSame('//depot/*', $data['path']);

        // ensure it doesn't take any more commands to get the change path
        // and original id -- we verify this by peeking at the log
        $original = Logger::hasLogger() ? Logger::getLogger() : null;
        $logger   = new ZendLogger;
        $mock     = new MockLog;
        $logger->addWriter($mock);
        Logger::setLogger($logger);

        $this->assertSame('//depot', $change->getPath());
        $this->assertSame(1, $change->getOriginalId());
        $this->assertSame(0, count($mock->events));

        // restore original logger if there is one.
        Logger::setLogger($original);
    }

    /**
     * Test setting a new client on an existing change
     */
    public function testSetClient()
    {
        $change = new Change($this->p4);
        $change->setDescription('test')->save();
        $oldClient = $change->getClient();

        $this->p4->getService('clients')->grab();
        $change->setClient($this->p4->getClient());
        $change->save();
        $this->p4->getService('clients')->release();

        $this->assertFalse(
            $oldClient == Change::fetch($change->getId(), $this->p4)->getClient(),
            'expected client change to have saved.'
        );
    }

    /**
     * Test what happens when 'type' field isn't present
     */
    public function testNoType()
    {
        // journal patch the server so it appears to have a change spec that was locked at 2006.1
        $this->p4->run(
            'admin',
            'import',
            "@pv@ 0 @db.bodtext@ @change@ 0 @Change;code:201;fmt:L;rq;ro;seq:1;len:10;;Date;code:202;"
            . "type:date;opt:always;fmt:R;ro;seq:3;len:20;;Client;code:203;opt:always;fmt:L;ro;seq:2;"
            . "len:32;;User;code:204;opt:always;fmt:L;ro;seq:4;len:32;;Status;code:205;opt:always;"
            . "fmt:R;ro;seq:5;len:10;;Description;code:206;type:text;opt:required;rq;seq:6;;JobStatus;"
            . "code:207;type:select;fmt:I;seq:8;;Jobs;code:208;type:wlist;seq:7;len:32;;Files;code:210;"
            . "type:llist;len:64;;@\n"
        );

        $change = new Change;
        $this->assertSame(Change::PUBLIC_CHANGE, $change->getType());
    }

    /**
     * Test a saving with a different user id set
     */
    public function testSaveOtherUser()
    {
        // verify we cannot swap to an invalid user
        $this->assertFalse(User::exists('swapped'));
        $change = Change::fetch('default');

        try {
            $change->setDescription('Test submit')
                   ->setUser('swapped')
                   ->save();
            $this->fail('should not have worked');
        } catch (CommandException $e) {
            $this->assertSame(
                "Command failed: Error in change specification.\nUser swapped doesn't exist.",
                $e->getMessage()
            );
        }


        // add the user swapped to make later attempts work
        $user = new User;
        $user->setId('swapped')->setEmail('[email protected]')->setFullName('Swapped User')->save();


        // verify set user works on existing change
        $change = Change::fetch('default');
        $change->setDescription('Test submit2')
               ->save();

        $this->assertSame('tester', Change::fetch(1)->getUser());

        $change = Change::fetch(1);
        $change->setUser('swapped')->save();

        $this->assertSame('swapped', Change::fetch(1)->getUser());


        // verify out of the gate set user works
        $change = Change::fetch('default');
        $change->setDescription('Test submit')
               ->setUser('swapped')
               ->save();

        $this->assertSame('swapped', Change::fetch(2)->getUser());
    }

    /**
     * Test canAccess() method.
     */
    public function testCanAccess()
    {
        // create user 'foo' with limited access to depot
        $p4Foo = $this->connectWithAccess('foo', array('//depot/foo/...'));

        // create few changes to test with
        $file   = new File;
        $file->setFilespec('//depot/test1')->open()->setLocalContents('abc');
        $change = new Change($this->p4);
        $change->setType(Change::RESTRICTED_CHANGE)->addFile($file)->submit('restricted #1'); // NOT accessible by 'foo'
        $id1 = $change->getId();

        $file   = new File;
        $file->setFilespec('//depot/foo/test3')->open()->setLocalContents('def');
        $change = new Change($this->p4);
        $change->setType(Change::RESTRICTED_CHANGE)->addFile($file)->submit('restricted #2'); // accessible by 'foo'
        $id2 = $change->getId();

        $file   = new File;
        $file->setFilespec('//depot/test2')->open()->setLocalContents('ghi');
        $change = new Change($this->p4);
        $change->setType(Change::PUBLIC_CHANGE)->addFile($file)->submit('public'); // accessible by 'foo'
        $id3 = $change->getId();

        // for users other than 'foo', all changes should be accessible
        $this->assertTrue(Change::fetch($id1, $this->p4)->canAccess());
        $this->assertTrue(Change::fetch($id2, $this->p4)->canAccess());
        $this->assertTrue(Change::fetch($id3, $this->p4)->canAccess());

        // ensure user 'foo' cannot access change id1, but can access the others
        $this->assertFalse(Change::fetch($id1, $p4Foo)->canAccess());
        $this->assertTrue(Change::fetch($id2, $p4Foo)->canAccess());
        $this->assertTrue(Change::fetch($id3, $p4Foo)->canAccess());
    }

    public function testGetFileData()
    {
        $files = array();
        for ($i = 0; $i < 10; $i++) {
            $file    = new File;
            $files[] = $file;
            $file->setFilespec('//depot/' . $i)
                 ->add()
                 ->setLocalContents('contents-' . $i);
        }

        $change = new Change;
        $change->setDescription("Test change")
               ->setFiles($files)
               ->save('test');

        $change = Change::fetch(1);
        $files  = $change->getFileData();
        $this->assertSame(10, count($files));
        $this->assertSame('//depot/0', current(current($files)));

        // test max limiting
        $files = $change->getFileData(null, 5);
        $this->assertSame(5, count($files));

        // test that shelved/unshelved files get cached separately
        $files = $change->getFileData(true);
        $this->assertSame(0, count($files));
    }
}
# Change User Description Committed
#1 18730 Liz Lam clean up code and move things around