ChangeTest.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • tests/
  • phpunit/
  • P4/
  • ChangeTest.php
  • View
  • Commits
  • Open Download .zip Download (65 KB)
<?php
/**
 * Test methods for the P4 Change class.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
class P4_ChangeTest extends TestCase
{
    /**
     * Test instantiation.
     */
    public function testInstantiation()
    {
        $change = new P4_Change;
        $this->assertTrue($change instanceof P4_Change, 'Expected class');
        $originalConnection = $change->getConnection();

        // test setting connection via constructor.
        $p4 = P4_Connection::factory();
        $change = new P4_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(P4_Change::exists(123), 'Given change should not exist.');

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

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

        // create a change and ensure it exists.
        $change = new P4_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(P4_Change::exists(1),   'Change 1 should exist.');
        $this->assertTrue(P4_Change::exists('1'), 'Change "1" should exist.');
    }

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

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

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

    /**
     * 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 P4_File;
        $file->setFilespec('//depot/test-file')
              ->add();

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

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

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

        $types = array('Id', 'DateTime', '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.
     */

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

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

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

        // create a change by another user, in another workspace.
        $user = new P4_User;
        $password = 'AnotherPass';
        $user->setId('alternate')
             ->setEmail('alternate@p4cms.perforce.com')
             ->setFullName('Alternate User')
             ->save();
        $client = new P4_Client;
        $clientId = 'another-test-client';
        $client->setId($clientId)
               ->setRoot(DATA_PATH . "/clients/$clientId")
               ->setView(array('//depot/... //another-test-client/...'))
               ->save();
        $p4 = P4_Connection::factory(
            $this->p4->getPort(),
            $user->getId(),
            $client->getId()
        );
        $change = new P4_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 P4_Change($p4);
        $change->addFile($integFilespec)->setDescription('Integration')->submit();

        // Testing begins: ensure correct number of changes returned.
        $changes = P4_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[0]->getValues();
        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(
                    P4_Change::FETCH_MAXIMUM => 1,
                ),
                'expected'  => array(
                    "Integration\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 2',
                'options'   => array(
                    P4_Change::FETCH_MAXIMUM => 2,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 3',
                'options'   => array(
                    P4_Change::FETCH_MAXIMUM => 3,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                    "Has 2 files\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch maximum 4',
                'options'   => array(
                    P4_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(
                    P4_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(
                    P4_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(
                    P4_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(
                    P4_Change::FETCH_BY_FILESPEC => '//depot/.../test-file',
                ),
                'expected'  => array(
                    "test-2",
                    "test-1"
                ),
            ),

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

            array(
                'label' => __LINE__ .': fetch by client regular',
                'options'   => array(
                    P4_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(
                    P4_Change::FETCH_BY_CLIENT => $clientId,
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),

            array(
                'label' => __LINE__ .': fetch by user regular',
                'options'   => array(
                    P4_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(
                    P4_Change::FETCH_BY_USER => $user->getId(),
                ),
                'expected'  => array(
                    "Integration\n",
                    "in alternate client\n",
                ),
            ),

            array(
                'label' => __LINE__ .': fetch without integrated',
                'options'   => array(
                    P4_Change::FETCH_INTEGRATED  => false,
                    P4_Change::FETCH_BY_FILESPEC => $integFilespec,
                ),
                'expected'  => array(
                    "Integration\n",
                ),
            ),
            array(
                'label' => __LINE__ .': fetch with integrated',
                'options'   => array(
                    P4_Change::FETCH_INTEGRATED  => true,
                    P4_Change::FETCH_BY_FILESPEC => $integFilespec,
                ),
                'expected'  => array(
                    "Integration\n",
                    "test-1"
                ),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $changes = P4_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 P4_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 P4_Job;
        $job->setValue('Description', 'This is job #1');
        $job->save();

        // create a change to associate with the job
        $change = new P4_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 (P4_Connection_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 P4_Job;
        $job2->setValue('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 = P4_Change::fetchAll();
        $this->assertEquals(1, count($changes), 'Expect one change.');
        $this->assertSame(2, count($changes[0]->getJobs()), 'Expect 2 jobs with fetched change.');
        $this->assertSame(
            array($extraJobId, $job->getId()),
            $changes[0]->getJobs(),
            'Expected jobs in fetched change.'
        );
        $this->assertTrue($changes[0]->isPending(), 'Change should be pending.');
        $this->assertFalse($changes[0]->isSubmitted(), 'Change should not be submitted.');

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

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

        // create a new change, with non-existant jobId, and submit it.
        $change = new P4_Change;
        $change->setDescription('Try submitting with non-existant job.');
        $thirdJobId = 'yetAnotherJob';
        $change->addJob($thirdJobId);
        $file = new P4_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 (P4_Connection_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 P4_Job;
        $job3->setValue('Description', 'Was non-existant')
             ->setId($thirdJobId)
             ->save();

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

    /**
     * Test getFiles and setFiles.
     */
    public function testGetSetFiles()
    {
        // test initial state of files in a fresh change object.
        $change = new P4_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 P4_File;
        $filespec1 = '//depot/change1.txt';
        $file1->setFilespec($filespec1)->add()->setLocalContents('content1')->submit('File 1');

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

        $file3 = new P4_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 = P4_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 = P4_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 = P4_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 = P4_Change::fetch(2);
            $change->setFiles(array($filespec3));
            $this->fail('Unexpected success setting files on a submitted changelist.');
        } catch (P4_Spec_Exception $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 P4_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 = P4_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 P4_Model_Iterator;
        $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 P4_Change;
        $description = "this is a test\n";
        $change->setDescription($description);
        $change->save();

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

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

        // save a change with a file and a job.
        $change2      = new P4_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 P4_Change,
                'method'        => 'getId',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getDateTime',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getUser',
                'expected'      => $this->p4->getUser()
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getClient',
                'expected'      => $this->p4->getClient()
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getStatus',
                'expected'      => 'pending'
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getDescription',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_Change,
                'method'        => 'getJobStatus',
                'expected'      => null
            ),
            array(
                'label'         => __LINE__ . ': New change object',
                'change'        => new P4_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 P4_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 P4_Model_Iterator;
        $file = new P4_File;
        $files[] = $file->setFilespec('//depot/file1.txt');
        $file = new P4_File;
        $files[] = $file->setFilespec('//depot/file2.txt');
        $file = new P4_File;
        $files[] = $file->setFilespec('//depot/file3.txt');

        $filesIteratorInvalid = new P4_Model_Iterator;
        $filesIteratorInvalid[] = $file;
        $filesIteratorInvalid[] = new P4_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 P4_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 P4_Model_Iterator;
        $job    = new P4_Job;
        $jobs[] = $job->setId('job000001');
        $job    = new P4_Job;
        $jobs[] = $job->setId('job000002');
        $job    = new P4_Job;
        $jobs[] = $job->setId('job000003');

        $jobsIteratorInvalid = new P4_Model_Iterator;
        $jobsIteratorInvalid[] = $job;
        $jobsIteratorInvalid[] = new P4_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 iterator job must be a P4_Job object.'),
                '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 P4_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 P4_File;
        $file->setFilespec("//depot/test-file")
             ->add();

        // put the file in a change.
        $change1 = new P4_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 P4_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 P4_Change;
        try {
            $change3->setDescription("test 3")->addFile("//depot/foo")->save();
            $this->fail("Save change with unopened file should throw exception.");
        } catch (P4_UnopenedException $e) {
            $this->assertTrue(true);
        }
    }

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

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

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

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

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

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

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

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

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

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

        $change = P4_Change::fetch($file->getStatus('headChange'));
        try {
            $change->delete();
            $this->fail("Delete of submitted change should fail.");
        } catch (P4_Spec_Exception $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 (P4_Spec_Exception $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 = P4_Change::fetch($change->getId());
        $change->delete(true);
        $this->assertFalse(
            P4_Change::exists($change->getId()),
            "Change should no longer exist."
        );
    }

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

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

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

        // ensure change was successful.
        $changes = P4_Change::fetchAll();
        $this->assertSame(
            1,
            count($changes),
            "There should be one change."
        );
        $this->assertSame(
            P4_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 (P4_Spec_Exception $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 (P4_Spec_Exception $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 (P4_Spec_Exception $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()
            );
        }
    }

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

        // connect w. second client.
        $p4 = P4_Connection::factory(
            $this->p4->getPort(),
            $this->p4->getUser(),
            $client->getId(),
            $this->utility->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 P4_File;
        $file1->setFilespec("//depot/foo")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new P4_File($p4);
        $file2->setFilespec("//depot/foo")
              ->sync()
              ->edit()
              ->submit("change 2");

        // try to submit a change w. files needing resolve.
        $change = new P4_Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (P4_Connection_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 P4_File;
        $file1->setFilespec("//depot/bar")
              ->setLocalContents("contents-1")
              ->add();
        $file2 = new P4_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 P4_Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (P4_Connection_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 P4_File;
        $file1->setFilespec("//depot/baz")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new P4_File($p4);
        $file2->setFilespec("//depot/baz")
              ->sync()
              ->delete()
              ->submit("change 2");

        // try to submit a change w. files needing revert.
        $change = new P4_Change;
        try {
            $change->addFile($file1)
                   ->submit("main client submit");
            $this->fail("Unexpected success; submit should fail.");
        } catch (P4_Connection_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 P4_Client;
        $client->setId("client-2")
               ->setRoot($this->utility->getP4Params('clientRoot') . '/client-2')
               ->addView("//depot/...", "//client-2/...")
               ->save();

        // connect w. second client.
        $p4 = P4_Connection::factory(
            $this->p4->getPort(),
            $this->p4->getUser(),
            $client->getId(),
            $this->utility->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 P4_File;
        $file1->setFilespec("//depot/foo")
              ->setLocalContents("contents-1")
              ->add()
              ->submit("change 1")
              ->edit();
        $file2 = new P4_File($p4);
        $file2->setFilespec("//depot/foo")
              ->sync()
              ->edit()
              ->submit("change 2");

        // try to submit a change w. files needing resolve.
        $change = new P4_Change;
        $change->addFile($file1)
               ->submit("main client submit", P4_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 = P4_Change::fetch('default');
        $change->setDescription('Test submit')
               ->save();
    }

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

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

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

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

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

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

        // check that we have one file open.
        $this->assertSame(
            1,
            P4_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 P4_File;
        $file1->setFilespec('//depot/one')
              ->add()
              ->setLocalContents("contents-1");

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

        $change = new P4_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');
        P4_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(P4_File::fetch('//depot/one#2'))->getDepotFilename(),
            'Expected getFileObject to provide proper depot filename'
        );
        $this->assertSame(
            '//depot/two',
            $change->getFileObject(P4_File::fetch('//depot/two'))->getDepotFilename(),
            'Expected getFileObject to provide proper depot filename'
        );
        $this->assertSame(
            '1',
            $change->getFileObject(P4_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) {

        }
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/tests/phpunit/P4/ChangeTest.php
#1 8972 Matt Attaway Initial add of the Chronicle source code