Test.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • tests/
  • phpunit/
  • P4/
  • File/
  • Test.php
  • View
  • Commits
  • Open Download .zip Download (97 KB)
<?php
/**
 * Test methods for the P4 Model Iterator.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
class P4_File_Test extends TestCase
{
    /**
     * Test fetchAll filespec handling.
     */
    public function testFetchAllSingular()
    {
        $tests = array(
            array(
                'label' => __LINE__ .': null query',
                'query' => P4_File_Query::create(),
                'error' => 'Cannot fetch files. No filespecs provided in query.',
            ),
            array(
                'label' => __LINE__ .': valid, but nonexistant filespec',
                'query' => P4_File_Query::create()->addFilespec('//depot/foobartesting'),
                'error' => '',
            ),
            array(
                'label' => __LINE__ .': valid, but nonexistant depot filespec',
                'query' => P4_File_Query::create()->addFilespec('//depotadsadsa/foobartesting'),
                'error' => '',
            )
        );

        foreach ($tests as $test) {
            try {
                $files = P4_File::fetchAll($test['query']);
                if ($test['error']) {
                    $this->fail($test['label'] .' - unexpected success');
                }
            } catch (InvalidArgumentException $e) {
                if ($test['error']) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        $test['label'] .' - Expected error'
                    );
                } else {
                    $this->fail($test['error'] .' - unexpected failure: '.  $e->getMessage());
                }
            } catch (Exception $e) {
                $this->fail($test['label'] .' - Unexpected exception: '.  $e->getMessage());
            }

        }
    }

    /**
     * Test fetchAll with multiple filespecs.
     */
    public function testFetchAllMultiple()
    {
        $basePath = '//depot/';
        $testFiles = $this->_prepareFetchAllTests($basePath, false);

        $query = P4_File_Query::create()->addFilespecs(array($basePath .'*small.txt', $basePath .'medium.jpg'));
        $files = P4_File::fetchAll($query);

        $filenames = array();
        foreach ($files as $file) {
            $filenames[] = $file->getFilespec();
        }
        $this->assertSame(
            array(
                '//depot/another_small.txt',
                '//depot/small.txt',
                '//depot/medium.jpg',
            ),
            $filenames,
            "Expected matching filenames when using multiple filespecs."
        );
    }

    /**
     * Test sync and flush.
     */
    public function testSyncFlush()
    {
        // ensure sync of non-existent file throws exception.
        $file = new P4_File;
        $file->setFilespec('//depot/non-existent-file');
        try {
            $file->sync();
            $this->fail("Excepted exception syncing non-existent file.");
        } catch (P4_File_Exception $e) {
            $this->assertTrue(true);
        }

        // ensure flush of non-existent file throws exception.
        try {
            $file->flush();
            $this->fail("Excepted exception syncing non-existent file.");
        } catch (P4_File_Exception $e) {
            $this->assertTrue(true);
        }

        // create a file
        $file = new P4_File;
        $content = 'Content.';
        $file->setFilespec('//depot/file.txt')
             ->add()
             ->setLocalContents($content)
             ->submit('Add file.');
        $this->assertTrue(
            is_file($file->getLocalFilename()),
            'Expect file to exist after create/submit.'
        );

        // delete client file
        $file->deleteLocalFile();
        $this->assertFalse(
            is_file($file->getLocalFilename()),
            'Expect file to no longer exist after delete.'
        );

        // sync the file; should fail as server thinks we have the file.
        $file->sync();
        $this->assertFalse(
            is_file($file->getLocalFilename()),
            'Expect file to still exist after sync without force.'
        );

        // sync again, with force.
        $file->sync(true);
        $this->assertTrue(
            is_file($file->getLocalFilename()),
            'Expect file to now exist after sync with force.'
        );

        // verify content
        $this->assertSame(
            $content,
            $file->getLocalContents(),
            'Expected content after sync.'
        );

        // revise content and flush; sync should not affect client file.
        $newContent = 'Some new content.';
        $file->setLocalContents($newContent)
             ->flush()
             ->sync();
        $this->assertSame(
            $newContent,
            $file->getLocalContents(),
            'Expected content after flush/sync'
        );

        // force sync again, content should revert to original.
        $file->sync(true);
        $this->assertSame(
            $content,
            $file->getLocalContents(),
            'Expected content after force sync'
        );
    }

    /**
     * Test getBasename.
     */
    public function testGetBasename()
    {
        $file = new P4_File;
        $path = 'a/path/to/a/file.txt';
        $root = $this->p4->getClientRoot();
        $filespec = "$root/$path";
        $file->setFilespec($filespec);

        $this->assertEquals(
            'file.txt',
            $file->getBasename(),
            'Expected basename'
        );
    }

    /**
     * Test getFileSize and getLocalFileSize.
     */
    public function testGetFileSizeAndGetLocalFileSize()
    {

        $basePath = '//depot/testFileSize/';
        $files = $this->_prepareFetchAllTests($basePath);

        $expectedSizes = array(
            'small.txt'         => 13,
            'another_small.txt' => 13,
            'medium.jpg'        => 40,
            'ze_medium_2.jpg'   => 40,
            'large.txt'         => 3599,
            'opened.txt'        => 7,
        );

        foreach ($files as $file) {
            $filename = basename($file->getFilespec());
            if ($filename === 'opened.txt') {
                $this->assertSame(
                    $expectedSizes[$filename],
                    $file->getLocalFileSize(),
                    "Expected filesize for '$filename'"
                );
            } else {
                $this->assertSame(
                    $expectedSizes[$filename],
                    $file->getFilesize(),
                    "Expected filesize for '$filename'"
                );
            }
        }

        // test getting sizes for non-existant files.
        $file = new P4_File;
        $file->setFilespec('//depot/does_not_exist.txt');

        try {
            $size = $file->getFileSize();
            $this->fail("Unexpected success for getFileSize().");
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                'The file does not have a fileSize attribute.',
                $e->getMessage(),
                'Expected error for getFileSize().'
            );
        } catch (Exception $e) {
            $this->fail(
                "Unexpected exception for getFileSize() ("
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        try {
            $size = $file->getLocalFileSize();
            $this->fail("Unexpected success for getLocalFileSize().");
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                'The local file does not exist.',
                $e->getMessage(),
                'Expected error for getLocalFileSize().'
            );
        } catch (Exception $e) {
            $this->fail(
                "Unexpected exception for getLocalFileSize() ("
                . get_class($e) .') :'. $e->getMessage()
            );
        }
    }

    /**
     * Test filename in local-file syntax.
     */
    public function testFilenameInLocalSyntax()
    {
        $file = new P4_File;
        $path = 'a/path/to/a/file.txt';
        $root = $this->p4->getClientRoot();
        $filespec = "$root/$path";
        $file->setFilespec($filespec);

        $this->assertSame(
            $filespec,
            $file->getLocalFilename(),
            'Expected local filename.'
        );

        $this->assertSame(
            "//depot/$path",
            $file->getDepotFilename(),
            'Expected depot filename.'
        );

        // test file object with no filespec set
        $file = new P4_File;
        try {
            $file->getLocalFilename();
            $this->fail('Unexpected success for getLocalFilename with no filespec');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                'Cannot complete operation, no filespec has been specified',
                $e->getMessage(),
                'Expected error for getLocalFilename with no filespec.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception for getLocalFilename with no filespec ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }
        try {
            $file->getDepotFilename();
            $this->fail('Unexpected success for getDepotFilename with no filespec');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                'Cannot complete operation, no filespec has been specified',
                $e->getMessage(),
                'Expected error for getDepotFilename with no filespec.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception for getDepotFilename with no filespec ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }
    }

    /**
     * Test count method.
     */
    public function testCount()
    {
        if (getenv('SKIP_LONG_TESTS')) {
            $this->markTestSkipped('long fetchAll test skipped, because asked');
            return;
        }

        $basePath = '//depot/testFetchOptions/';
        $testFiles = $this->_prepareFetchAllTests($basePath, true);

        $tests = array(
            // sorting tests
            array(
                'label'     => __LINE__ .': defaults',
                'query'     => P4_File_Query::create(),
                'expected'  => 6,
            ),
            array(
                'label'     => __LINE__ .': reversed default sort',
                'query'     => P4_File_Query::create()->setReverseOrder(true),
                'expected'  => 6,
            ),
            array(
                'label'     => __LINE__ .': sort',
                'query'     => P4_File_Query::create()->setSortBy('fileSize'),
                'expected'  => 6,
            ),
            array(
                'label'     => __LINE__ .': reversed sort',
                'query'     => P4_File_Query::create()->setReverseOrder(true)->setSortBy('fileSize'),
                'expected'  => 6,
            ),

            // limit results tests (limits should have no effect)
            array(
                'label'     => __LINE__ .': limit',
                'query'     => P4_File_Query::create()->setMaxFiles(1),
                'expected'  => 1,
            ),
            array(
                'label'     => __LINE__ .': reversed limit',
                'query'     => P4_File_Query::create()->setMaxFiles(1)->setReverseOrder(true),
                'expected'  => 1,
            ),

            // filtering tests
            array(
                'label'     => __LINE__ .': filter fileSize > 1000',
                'query'     => P4_File_Query::create()->setFilter('fileSize > 1000'),
                'expected'  => 1,
            ),
            array(
                'label'     => __LINE__ .': filter fileSize <= 1000',
                'query'     => P4_File_Query::create()->setFilter('fileSize <= 1000'),
                'expected'  => 4,
            ),
            array(
                'label'     => __LINE__ .': filter scrunchy',
                'query'     => P4_File_Query::create()->setFilter('scrunchy'),
                'expected'  => 0,
            ),
            array(
                'label'     => __LINE__ .': filter with existing rowNumber',
                'query'     => P4_File_Query::create()->setFilter('fileSize <= 1000 & rowNumber >= 4'),
                'expected'  => 1,
            ),
            array(
                'label'     => __LINE__ .': filter with rowNumber existing',
                'query'     => P4_File_Query::create()->setFilter('rowNumber >= 5 & fileSize <= 1000'),
                'expected'  => 0,
            ),
            array(
                'label'     => __LINE__ .': filter with existing rowNumber existing',
                'query'     => P4_File_Query::create()->setFilter('fileSize <= 1000 rowNumber >= 2 & fileSize <= 1000'),
                'expected'  => 3,
            ),

            // change and content tests
            array(
                'label'     => __LINE__ .': change 1',
                'query'     => P4_File_Query::create()->setLimitToChangelist(1),
                'expected'  => 1,
            ),

            // opened tests
            array(
                'label'     => __LINE__ .': opened',
                'query'     => P4_File_Query::create()->setLimitToOpened(true),
                'expected'  => 1,
            ),
        );

        foreach ($tests as $test) {
            $label  = $test['label'];
            $file   = new P4_File;
            $count  = null;

            $test['query']->addFilespec($basePath .'...');
            try {
                $count = P4_File::count($test['query']);
            } catch (Exception $e) {
                $this->fail("$label - unexpected failure (". get_class($e) .': '. $e->getMessage());
            }

            if (array_key_exists('dump', $test)) {
                var_dump($file->getConnection()->run('fstat', array('-Ol', '//...')));
                exit;
            }

            $this->assertEquals(
                $test['expected'],
                $count,
                "$label - expected count"
            );
        }
    }

    /**
     * Test count with no filespec.
     */
    public function testCountWithoutFilespec()
    {
        try {
            $count = P4_File::count(new P4_File_Query);
            $this->fail('Unexpected success');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                'Cannot count files. No filespecs provided in query.',
                $e->getMessage(),
                'Expected exception'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception ('. get_class($e) .') :'. $e->getMessage());
        }
    }

    /**
     * Test fetchAll options.
     *
     * @todo fixup and remove skip for sorting tests when/if server sorts
     * by size/data properly.
     */
    public function testFetchAllOptions()
    {
        if (getenv('SKIP_LONG_TESTS')) {
            $this->markTestSkipped('long fetchAll test skipped, because asked');
            return;
        }

        $basePath = '//depot/testFetchOptions/';
        $testFiles = $this->_prepareFetchAllTests($basePath, true);

        $tests = array(
            // sorting tests
            array(
                'label'     => __LINE__ .': defaults',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...'),
                'expected'  => array(
                    'another_small.txt',
                    'large.txt',
                    'medium.jpg',
                    'opened.txt',
                    'small.txt',
                    'ze_medium_2.jpg',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed default sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'ze_medium_2.jpg',
                    'small.txt',
                    'opened.txt',
                    'medium.jpg',
                    'large.txt',
                    'another_small.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': filesize sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_FILE_SIZE),
                'expected'  => array(
                    'opened.txt',
                    'another_small.txt',
                    'small.txt',
                    'medium.jpg',
                    'ze_medium_2.jpg',
                    'large.txt',
                ),
                'content'   => false,
            ),

            array(
                'label'     => __LINE__ .': reversed filesize sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_FILE_SIZE)
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'large.txt',
                    'medium.jpg',
                    'ze_medium_2.jpg',
                    'another_small.txt',
                    'small.txt',
                    'opened.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': date sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_DATE),
                'expected'  => array(
                    'opened.txt',
                    'small.txt',
                    'medium.jpg',
                    'large.txt',
                    'another_small.txt',
                    'ze_medium_2.jpg',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed date sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_DATE)
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'ze_medium_2.jpg',
                    'another_small.txt',
                    'large.txt',
                    'medium.jpg',
                    'small.txt',
                    'opened.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': filetype sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_FILE_TYPE),
                'expected'  => array(
                    'medium.jpg',
                    'ze_medium_2.jpg',
                    'another_small.txt',
                    'large.txt',
                    'opened.txt',
                    'small.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed filetype sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(P4_File_Query::SORT_FILE_TYPE)
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'another_small.txt',
                    'large.txt',
                    'opened.txt',
                    'small.txt',
                    'medium.jpg',
                    'ze_medium_2.jpg',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': attribute sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy('foo'),
                'expected'  => array(
                    'opened.txt',
                    'ze_medium_2.jpg',
                    'large.txt',
                    'medium.jpg',
                    'another_small.txt',
                    'small.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed attribute sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy('foo', array(P4_File_Query::SORT_DESCENDING)),
                'expected'  => array(
                    'another_small.txt',
                    'small.txt',
                    'medium.jpg',
                    'large.txt',
                    'ze_medium_2.jpg',
                    'opened.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': attribute + date sort',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setSortBy(array('foo', P4_File_Query::SORT_DATE)),
                'expected'  => array(
                    'opened.txt',
                    'ze_medium_2.jpg',
                    'large.txt',
                    'medium.jpg',
                    'small.txt',
                    'another_small.txt',
                ),
                'content'   => false,
            ),

            // limit results tests
            array(
                'label'     => __LINE__ .': limit 1',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setMaxFiles(1),
                'expected'  => array(
                    'another_small.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed limit 1',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setMaxFiles(1)
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'ze_medium_2.jpg',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': limit 2',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setMaxFiles(2),
                'expected'  => array(
                    'another_small.txt',
                    'large.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': reversed limit 2',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setMaxFiles(2)
                                                      ->setReverseOrder(true),
                'expected'  => array(
                    'ze_medium_2.jpg',
                    'small.txt',
                ),
                'content'   => false,
            ),

            // filtering tests
            array(
                'label'     => __LINE__ .': filter fileSize > 1000',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setFilter('fileSize > 1000'),
                'expected'  => array(
                    'large.txt',
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': filter fileSize <= 1000',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setFilter('fileSize <= 1000'),
                'expected'  => array(
                    'another_small.txt',
                    'medium.jpg',
                    'small.txt',
                    'ze_medium_2.jpg',
                ),
                'content'   => false,
            ),

            // change and content tests
            array(
                'label'     => __LINE__ .': change 1',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist(1),
                'expected'  => array(
                    basename($testFiles[0]->getFilespec()),
                ),
                'content'   => false,
            ),
            array(
                'label'     => __LINE__ .': change 2',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist(2),
                'expected'  => array(
                    basename($testFiles[1]->getFilespec()),
                ),
                'content'   => true,
            ),
            array(
                'label'     => __LINE__ .': change 3',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist(3),
                'expected'  => array(
                    basename($testFiles[2]->getFilespec()),
                ),
                'content'   => true,
            ),
            array(
                'label'     => __LINE__ .': change 4',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist(4),
                'expected'  => array(
                    basename($testFiles[3]->getFilespec()),
                ),
                'content'   => true,
            ),
            array(
                'label'     => __LINE__ .': change 5',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist(5),
                'expected'  => array(
                    basename($testFiles[4]->getFilespec()),
                ),
                'content'   => true,
            ),
            array(
                'label'     => __LINE__ .': change default',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToChangelist('default'),
                'expected'  => array(
                    basename($testFiles[5]->getFilespec()),
                ),
                'content'   => true,
            ),

            // opened tests
            array(
                'label'     => __LINE__ .': opened',
                'query'     => P4_File_Query::create()->addFilespec($basePath .'...')
                                                      ->setLimitToOpened(true),
                'expected'  => array(
                    basename($testFiles[5]->getFilespec()),
                ),
                'content'   => true,
            ),
        );

        $showSizes = 0;
        foreach ($tests as $test) {
            $label = $test['label'];
            $file  = new P4_File;

            if (array_key_exists('dumpQuery', $test) and $test['dumpQuery']) {
                print var_export($test['query']->toArray(), true);
            }
            try {
                $files = P4_File::fetchAll($test['query']);
            } catch (Exception $e) {
                $this->fail("$label - unexpected failure: ". $e->getMessage());
            }

            if (array_key_exists('dump', $test)) {
                var_dump($file->getConnection()->run('info'));
                var_dump($file->getConnection()->run('fstat', array('-Oal', '//depot/testFetchOptions/...')));
                exit;
            }

            $this->assertEquals(
                count($test['expected']),
                count($files),
                "$label - expected file count"
            );
            $fileNames = array();
            foreach ($files as $file) {
                $filename = $file->getFilespec();
                $filesize = $file->getLocalFileSize();
                $fileNames[] = basename($file->getFilespec());
                if ($showSizes) print "'$filename' is '$filesize' bytes.\n";
            }
            $showSizes = 0;

            $this->assertSame(
                $test['expected'],
                $fileNames,
                "$label - expected file list"
            );

            if ($test['content']) {
                for ($i = 0; $i < count($test['expected']); $i++) {
                    $method = ($testFiles[$test['expected'][$i]]->isOpened()) ? 'getLocalContents' : 'getDepotContents';
                    $this->assertSame(
                        $testFiles[$test['expected'][$i]]->$method(),
                        $files[$i]->$method(),
                        "$label - Expected content for file #$i"
                    );
                }
            }
        }
    }

    /**
     * Verify we are doing a proper natural order sort on attributes
     */
    public function testNaturalSort()
    {
        $basePath = "//depot/test/";
        $file = new P4_File;
        $file->setFilespec($basePath .'10.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'give-me-a-10')
             ->submit('10');
        $file->setFilespec($basePath .'2.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'give-me-a-2')
             ->submit('2');
        $file->setFilespec($basePath .'1.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'give-me-a-1')
             ->submit('1');
        $file->setFilespec($basePath .'just1.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', '1')
             ->submit('just digit 1');
        $file->setFilespec($basePath .'a.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'a')
             ->submit('a');
        $file->setFilespec($basePath .'Aa.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'Aa')
             ->submit('A');
        $file->setFilespec($basePath .'z.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'z')
             ->submit('z');
        $file->setFilespec($basePath .'Zz.txt')
             ->open()
             ->setLocalContents('i am a file')
             ->setAttribute('foo', 'Zz')
             ->submit('Zz');

        $foos = P4_File::fetchAll(
            P4_File_Query::create()->setFilespecs($basePath . "...")->setSortBy('foo')
        );

        $this->assertSame(
            array(
                '1',
                'a',
                'Aa',
                'give-me-a-1',
                'give-me-a-2',
                'give-me-a-10',
                'z',
                'Zz'
            ),
            $foos->invoke('getAttribute', array('foo')),
            'Expecting sorted attributes'
        );
    }

    /**
     * Test an already opened file.
     */
    public function testAlreadyOpened()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/notAlreadyOpened.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist #1.');
        $this->assertFalse($file->isOpened(), 'Expect file to not be opened #1.');

        $file->open()->setLocalContents('Some content.');
        $this->assertFalse($file->exists($file->getFilespec()), 'Still expect file to not exist #1.');
        $this->assertTrue($file->isOpened(), 'Expect file to be opened #1.');

        $file->open();
        $this->assertFalse($file->exists($file->getFilespec()), 'Still expect file to not exist #1.');
        $this->assertTrue($file->isOpened(), 'Expect file to still be opened #1.');
        $file->revert();

        // once again, with initial add instead of open
        $file = new P4_File;
        $file->setFilespec('//depot/notAlreadyOpened.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist #2.');
        $this->assertFalse($file->isOpened(), 'Expect file to not be opened. #2');

        $file->add(null, 'binary')->setLocalContents('Some content.');
        $this->assertFalse($file->exists($file->getFilespec()), 'Still expect file to not exist #2.');
        $this->assertTrue($file->isOpened(), 'Expect file to be opened. #2');

        $file->open();
        $this->assertFalse($file->exists($file->getFilespec()), 'Still expect file to not exist #2.');
        $this->assertTrue($file->isOpened(), 'Expect file to still be opened #2.');
    }

    /**
     * Test file deletion.
     *
     * @todo enable when appropriate error handling exists
     */
    public function testFileDeletion()
    {
        // first, create a file to delete.
        $file = new P4_File;
        $file->setFilespec('//depot/a_file_to_delete.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist.');
        $file->open()
             ->setLocalContents('Some content to delete.')
             ->submit('Adding a file to delete.');
        $this->assertTrue((bool)$file->exists($file->getFilespec()), 'Expect file to exist.');
        $this->assertFalse($file->isDeleted(), 'Expect file to not have deleted status after create');

        // expect file to still exist if exclude deleted is true.
        $this->assertTrue(
            (bool)P4_File::exists($file->getFilespec(), null, true),
            "Expected file to exist with excluded deleted = true."
        );

        // test deleting the opened file.
        $file->open();
        $this->assertTrue($file->isOpened(), 'Expect file to be opened.');

        // first, with force set to false
        try {
            $file->delete(null, false);
            $this->fail('Unexpected success deleting an opened file without force.');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Failed to open file for delete: //depot/a_file_to_delete.txt"
                . " - can't delete (already opened for edit)",
                $e->getMessage(),
                'Expected error message deleting an opened file without force.'
            );
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (Exception $e) {
            $this->fail('Unexpected exception on delete opened file: '.  $e->getMessage());
        }

        // again with force defaulting to true
        try {
            $file->delete();
        } catch (Exception $e) {
            $this->fail('Unexpected exception on delete opened file: '.  $e->getMessage());
        }

        // test deleting the file again.
        try {
            $file->delete();
        } catch (Exception $e) {
            $this->fail('Unexpected exception on delete opened file again: '.  $e->getMessage());
        }

        // now submit the deletion.
        $file->submit('Delete the file.');
        $this->assertTrue((bool)$file->exists($file->getFilespec()), 'Expect file to still exist.');
        $this->assertTrue(
            $file->hasStatusField('headAction')
            && $file->getStatus('headAction') == 'delete',
            'Expect file to be deleted'
        );
        $this->assertTrue($file->isDeleted(), 'Expect file to have deleted status after delete.');

        // expect file to not exist if exclude deleted is true.
        $this->assertFalse(
            P4_File::exists($file->getFilespec(), null, true),
            "Expected file to not exist with excluded deleted = true."
        );

        // test deleting the file again after submission.
        try {
            $file->delete();
            $this->fail('Unexpected success deleting an already deleted file');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Failed to open file for delete: //depot/a_file_to_delete.txt - file(s) not on client.",
                $e->getMessage(),
                'Expected error message deleting an already deleted file.'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception on delete deleted file: '.  $e->getMessage());
        }
    }

    /**
     * Test edit after delete.
     */
    public function testEditAfterDelete()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/file.txt')
             ->add()
             ->setLocalContents('Some content.')
             ->submit('Created it.');

        // delete the file, and then attempt to edit it with force=false.
        $file->delete();
        try {
            $file->edit(null, null, false);
            $this->fail('Unexpected success editing a file opened for delete');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Failed to open file for edit: //depot/file.txt - can't edit (already opened for delete)",
                $e->getMessage(),
                'Expected error editing a file opened for delete.'
            );
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception editing a file opened for delete ('
                . get_class($e) .'): '. $e->getMessage()
            );
        }

        // try to edit again, with force set. Should succeed.
        $file->edit(null, null, true)
             ->setLocalContents('New text.')
             ->submit('Update after delete.');
    }

    /**
     * Test invalid submit description.
     */
    public function testInvalidSubmitDescription()
    {
        $tests = array(
            array(
                'label'         => __LINE__ .': null description',
                'description'   => null,
                'error'         => new InvalidArgumentException(
                    'Cannot submit. Description must be a non-empty string.'
                ),
            ),
            array(
                'label'         => __LINE__ .': empty description',
                'description'   => '',
                'error'         => new InvalidArgumentException(
                    'Cannot submit. Description must be a non-empty string.'
                ),
            ),
            array(
                'label'         => __LINE__ .': array description',
                'description'   => array('1', '2'),
                'error'         => new InvalidArgumentException(
                    'Cannot submit. Description must be a non-empty string.'
                ),
            ),
            array(
                'label'         => __LINE__ .': 0 description',
                'description'   => 0,
                'error'         => new InvalidArgumentException(
                    'Cannot submit. Description must be a non-empty string.'
                ),
            ),
            array(
                'label'         => __LINE__ .': 1 description',
                'description'   => 1,
                'error'         => new InvalidArgumentException(
                    'Cannot submit. Description must be a non-empty string.'
                ),
            ),
            array(
                'label'         => __LINE__ .': string description',
                'description'   => 'abcde',
                'error'         => false,
            ),
        );

        $counter = 0;
        foreach ($tests as $test) {
            $label = $test['label'];
            $file = new P4_File;
            $file->setFilespec('//depot/file'. $counter++ .'.txt')
                 ->add()
                 ->setLocalContents('Content');
            try {
                $file->submit($test['description']);
                if ($test['error']) {
                    $this->fail("$label - unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (Exception $e) {
                if ($test['error']) {
                    $this->assertSame(
                        get_class($test['error']),
                        get_class($e),
                        "$label - expected exception class: ". $e->getMessage()
                    );
                    $this->assertSame(
                        $test['error']->getMessage(),
                        $e->getMessage(),
                        "$label - expected error message."
                    );
                } else {
                    $this->fail(
                        "$label - unexpected exception ("
                        . get_class($e) .') :'. $e->getMessage()
                    );
                }
            }
        }
    }

    /**
     * Test setStatusCache.
     */
    public function testSetStatusCache()
    {
        $file = new P4_File();
        $file->setFilespec('//depot/file')->add()->setLocalContents('A')->submit('A');
        $originalStatus = $modifiedStatus = $file->getStatus();
        foreach ($modifiedStatus as $key => $value) {
            $modifiedStatus[$key] = 'head';
        }
        $file->setStatusCache($modifiedStatus);
        $this->assertSame(
            $modifiedStatus,
            $file->getStatus(),
            'Expected modified status'
        );

        $file->clearStatusCache();
        $this->assertSame(
            $originalStatus,
            $file->getStatus(),
            'Expected original status'
        );

        try {
            $file->setStatusCache(1);
            $this->fail('Unexpected success setting numeric status');
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                'Cannot set status cache. Status must be an array.',
                $e->getMessage(),
                'Expected error setting numeric status'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception setting numeric status ('
                . get_class($e) .') '. $e->getMessage()
            );
        }
    }

    /**
     * Test open attributes.
     */
    public function testOpenAttributes()
    {
        // setup a file
        $file = new P4_File;
        $file->setFilespec('//depot/attribute_test.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist.');
        $file->open();

        // test that it has no attributes
        $attributes = $file->getAttributes();
        $this->assertSame(0, count($attributes), 'Expected attribute count on new file.');

        // verify lack of attribute
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to not exist.');
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to not exist.');

        // set the attribute
        $file->setAttribute('foobar', 'bazbar', false, false);

        // check that set attribute requires a string
        try {
            $file->setAttribute('fooerror', 2, false, false);
            $this->fail('Unexpected success setting attribute without string');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertEquals(
                'Cannot set attribute. Value must be a string.',
                $e->getMessage(),
                'Expected exception setting attribute without string'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception ('. get_class($e) .': '.  $e->getMessage());
        }

        // check that depot attribute is not set
        $attributes = $file->getAttributes(false);
        $this->assertSame(0, count($attributes), 'Expected depot attribute count after setting attribute.');

        // check that client attribute is set
        $attributes = $file->getAttributes(true);
        $this->assertSame(1, count($attributes), 'Expected client attribute count after setting attribute.');
        $this->assertTrue($file->hasOpenAttribute('foobar'), 'Expect client foobar to exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to not exist.');

        // verify the attribute value
        $this->assertSame(
            'bazbar',
            $file->getOpenAttribute('foobar'),
            'Expected client foobar value.'
        );
        try {
            $value = $file->getAttribute('foobar');
            $this->fail('Unexpected success getting depot foobar.');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Can't fetch status. The requested field "
                . "('attr-foobar') does not exist.",
                $e->getMessage(),
                'Expected exception using getAttribute on open attribute'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception when getting open attribute: '.  $e->getMessage()
                ."\n$e"
            );
        }

        // clear the attribute
        $file->clearAttribute('foobar', false);

        // verify lack of attribute
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to no longer exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to still not exist.');
    }

    /**
     * Test depot attributes.
     */
    public function testDepotAttributes()
    {
        // setup a file
        $file = new P4_File;
        $file->setFilespec('//depot/attribute_test.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist.');
        $file->open()
             ->setLocalContents('Some content.')
             ->submit('Attribute file for testing');

        // verify lack of attribute
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to not exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to not exist.');

        // set the attribute
        $file->setAttribute('foobar', 'bazbar', false, true);

        // check that client attribute is not set
        $attributes = $file->getAttributes(true);
        $this->assertSame(0, count($attributes), 'Expected client attribute count after setting attribute.');

        // check that depot attribute is set
        $attributes = $file->getAttributes(false);
        $this->assertSame(1, count($attributes), 'Expected depot attribute count after setting attribute.');
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to not exist.');
        $this->assertTrue($file->hasAttribute('foobar'), 'Expect depot foobar to exist.');

        // verify the attribute value
        $this->assertSame(
            'bazbar',
            $file->getAttribute('foobar'),
            'Expected depot foobar value.'
        );
        try {
            $value = $file->getOpenAttribute('foobar');
            $this->fail('Unexpected success getting client foobar.');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Can't fetch status. The requested field "
                . "('openattr-foobar') does not exist.",
                $e->getMessage(),
                'Expected exception using getOpenAttribute on attribute'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception when getting attribute: '.  $e->getMessage()
                ."\n$e"
            );
        }

        // clear the attribute
        $file->clearAttribute('foobar', true);

        // verify lack of attribute
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to still not exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to no longer exist.');
    }


    /**
     * Test attribute propagation.
     */
    public function testAttributePropagation()
    {
        // setup a file
        $file = new P4_File;
        $file->setFilespec('//depot/attribute_test.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist.');
        $file->open();

        // verify lack of attribute
        $this->assertFalse($file->hasOpenAttribute('foobar'), 'Expect client foobar to not exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to not exist.');

        // set the attribute with propagate, and test existence
        $file->setAttribute('foobar', 'bazbar', true, false);
        $this->assertTrue($file->hasOpenAttribute('foobar'), 'Expect client foobar to exist.');
        $this->assertFalse($file->hasAttribute('foobar'), 'Expect depot foobar to not exist.');
        $this->assertSame(
            'bazbar',
            $file->getOpenAttribute('foobar'),
            'Expected client foobar value.'
        );

        // set an additional attribute without propagate, and test existence
        $file->setAttribute('nopropagate', 'loseme', false, false);
        $this->assertTrue($file->hasOpenAttribute('nopropagate'), 'Expect client nopropagate to exist.');
        $this->assertFalse($file->hasAttribute('nopropagate'), 'Expect depot nopropagate to not exist.');
        $this->assertSame(
            'loseme',
            $file->getOpenAttribute('nopropagate'),
            'Expected client nopropagate value.'
        );

        // submit the file #1
        $file->setLocalContents('Some content.')
             ->submit('Attribute file for testing');

        // use a fresh file object, and verify attributes
        $file2 = new P4_File;
        $file2->setFilespec('//depot/attribute_test.txt');
        $this->assertFalse($file2->hasOpenAttribute('foobar'), 'Expect client foobar to no longer exist.');
        $this->assertTrue($file2->hasAttribute('foobar'), 'Expect depot foobar to exist now.');
        $this->assertSame(
            'bazbar',
            $file2->getAttribute('foobar'),
            'Expected depot foobar value.'
        );
        // nopropagate should only be in depot, for this revision
        $this->assertFalse($file2->hasOpenAttribute('nopropagate'), 'Expect client nopropagate to no longer exist.');
        $this->assertTrue($file2->hasAttribute('nopropagate'), 'Expect depot nopropagate to exist now.');

        // now open the file and test whether the depot attribute has been opened
        $file2->open();
        $this->assertTrue($file2->hasOpenAttribute('foobar'), 'Expect client foobar to have been opened.');
        $this->assertTrue($file2->hasAttribute('foobar'), 'Expect depot foobar to exist now.');
        $this->assertSame(
            'bazbar',
            $file2->getOpenAttribute('foobar'),
            'Expected client foobar value.'
        );
        // make sure nopropagate does not get promoted to client
        $this->assertFalse(
            $file2->hasOpenAttribute('nopropagate'),
            'Expect client nopropagate to not exist after open.'
        );

        // submit the file #2, and make sure attribute propagates
        $file2->setLocalContents('Some new content.')
              ->submit('Change attribute file content');

        // use a fresh file object
        $file3 = new P4_File;
        $file3->setFilespec('//depot/attribute_test.txt');

        // verify original attribute exists
        $this->assertFalse($file3->hasOpenAttribute('foobar'), 'Expect client foobar to still not exist.');
        $this->assertTrue($file3->hasAttribute('foobar'), 'Expect depot foobar to still exist.');
        $this->assertSame(
            'bazbar',
            $file3->getAttribute('foobar'),
            'Expected client foobar value.'
        );

        // now open the file and test whether the depot attribute has been opened
        $file3->open();
        $this->assertTrue($file3->hasOpenAttribute('foobar'), 'Expect client foobar to have been opened.');
        $this->assertTrue($file3->hasAttribute('foobar'), 'Expect depot foobar to exist now.');
        $this->assertSame(
            'bazbar',
            $file3->getOpenAttribute('foobar'),
            'Expected client foobar value.'
        );

        // verify nopropagate attribute does not exist
        $this->assertFalse(
            $file3->hasOpenAttribute('nopropagate'),
            'Expect client nopropagate to still not exist.'
        );
        $this->assertFalse(
            $file3->hasAttribute('nopropagate'),
            'Expect depot nopropagate to no longer exist.'
        );

        // clear attribute at depot
        $file3->clearAttribute('foobar', false);
        $this->assertFalse(
            $file3->hasOpenAttribute('foobar'),
            'Expect client foobar to not exist after clear.'
        );
        $this->assertTrue(
            $file3->hasAttribute('foobar'),
            'Expect depot foobar to still exist after clear.'
        );

        // submit an update to verify lack of propagation
        $file3->setLocalContents('Final content.');
        $file3->submit('Update attribute file content');

        // use a fresh file object
        $file4 = new P4_File;
        $file4->setFilespec('//depot/attribute_test.txt');

        // verify depot attribute still does not exist
        $this->assertFalse(
            $file4->hasAttribute('foobar'),
            'Expect depot foobar to still be cleared.'
        );

        // verify non-existant depot attribute does not get promoted to client
        // after opening
        $file4->open();
        $this->assertFalse(
            $file4->hasOpenAttribute('foobar'),
            'Expect client foobar to still, still, still not exist.'
        );
    }

    /**
     * Test setAttributes.
     */
    public function testSetAttributes()
    {
        // setup a file
        $file = new P4_File;
        $file->setFilespec('//depot/multi_attribute_test.txt');
        $this->assertFalse($file->exists($file->getFilespec()), 'Expect file to not exist.');
        $file->open();

        // verify lack of attribute
        $attributes = $file->getAttributes(true);
        $this->assertSame(0, count($attributes), 'Expected initial attribute count.');

        // first, verify that we get an exception unless the
        // attributes array is an array
        try {
            $file->setAttributes(null);
            $this->fail('Unexpected success without attributes.');
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Can't set attributes. Attributes must be an array.",
                $e->getMessage(),
                'Expected error message.'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception with no attributes: '.  $e->getMessage());
        }

        // set a list of client, no-propagate attributes
        $attributeList = array(
            'foobar'    => 'bazbar',
            'another'   => 'value',
            'third'     => 'three',
        );
        $file->setAttributes($attributeList, false, false);

        $attributes = $file->getAttributes(true);
        $this->assertSame(3, count($attributes), 'Expected attribute count after set.');

        foreach ($attributeList as $key => $value) {
            $this->assertTrue(
                $file->hasOpenAttribute($key),
                "Expect client '$key' to be set."
            );
            $this->assertSame(
                $value,
                $file->getOpenAttribute($key),
                "Exected value for '$key'"
            );
        }

        // test that invalid attributes are rejected.
        $tests = array(
            '1234',
            '-test',
            'test foo',
            'test*',
            'test...',
            'test#',
            'test@',
            123,
            'foo/bar',
            "test\x00",
            array("lkasdfj\tasdf"),
        );
        for ($i = 0; $i < count($tests); $i++) {
            try {
                $file->setAttribute($tests[$i], 'foo');
                $this->fail('Test #' . $i . ': Unexpected success setting invalid attribute.');
            } catch (InvalidArgumentException $e) {
                $this->assertTrue(true);
            } catch (Exception $e) {
                $this->fail('Test #' . $i . ': Unexpected exception setting invalid attribute.');
            }
        }
    }

    /**
     * test deleteLocalFile.
     */
    public function testDeleteLocalFile()
    {
        $file = new P4_File;
        $filespec = '//depot/a_file_to_delete.txt';
        $file->setFilespec($filespec);
        $this->assertFalse($file->exists($filespec), 'Expect depot file to not exist.');
        $this->assertFalse(file_exists($file->getLocalFilename()), 'Expect local file to not exist.');

        try {
            $file->deleteLocalFile();
            $this->fail('Unexpected success deleting file that does not exist.');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Cannot delete local file. File does not exist.",
                $e->getMessage(),
                'Expected error deleting a non-existant file'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception deleting non-existant file: '.  $e->getMessage());
        }

        // create file
        $file->open()
             ->setLocalContents('Some file content.')
             ->submit('Establish file.');
        $this->assertTrue((bool)$file->exists($filespec), 'Expect depot file to exist.');
        $this->assertTrue((bool)file_exists($file->getLocalFilename()), 'Expect local file to exist.');
        $this->assertFalse($file->isDeleted(), 'Expect file to not have deleted status after create');

        // delete the local file
        $file->deleteLocalFile();
        $this->assertTrue((bool)$file->exists($filespec), 'Expect depot file to exist.');
        $this->assertFalse(file_exists($file->getLocalFilename()), 'Expect local file to no longer exist.');
        $this->assertFalse($file->isDeleted(), 'Expect file to not have deleted status after deleting local file');
    }

    /**
     * Test opening for add.
     */
    public function testAdd()
    {
        $file = new P4_File;
        $filespec = '//depot/a_file_to_delete.txt';
        $file->setFilespec($filespec);
        $this->assertFalse($file->exists($filespec), 'Expect depot file to not exist.');
        $this->assertFalse(file_exists($file->getLocalFilename()), 'Expect local file to not exist.');

        $content = 'Some content.';
        $bytes = file_put_contents($file->getLocalFilename(), $content);
        $this->assertSame(strlen($content), $bytes, 'Expected content length');

        // open for add
        $object = $file->add();
        $this->assertSame($file, $object, 'Expect fluent interface return');
        $this->assertTrue($file->isOpened(), 'Expect file to be opened.');

        // open it again
        $object = $file->add();

        // revert file
        $file->revert();
        $this->assertFalse($file->isOpened(), 'Expect file to not be opened.');

        // add with type
        $object = $file->add(null, 'text');
        $this->assertTrue($file->isOpened(), 'Expect file to be opened again.');

        // submit the file, open it, and then try to add it.
        $file->submit('adding the file');
        $file->open();
        try {
            $file->add();
            $this->fail('Unexpected success adding a file opened for edit.');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Failed to open file for add: //depot/a_file_to_delete.txt - can't add (already opened for edit)",
                $e->getMessage(),
                'Expected error adding a file opened for edit.'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception adding a file opened for edit: '.  $e->getMessage());
        }

        // test adding into a specific change.
        $change = new P4_Change;
        $change->setDescription('test')->save();
        $file = new P4_File;
        $file->setFilespec('//depot/testeroo')
             ->touchLocalFile()
             ->add($change->getId());
        $this->assertSame(
            intval($file->getStatus('change')),
            $change->getId(),
            "Expected file to be open in specified change."
        );
        $this->assertSame(
            array('//depot/testeroo'),
            $change->getFiles(),
            "Expected file in change."
        );

        // test adding into a different change.
        $change = new P4_Change;
        $change->setDescription('test2')->save();
        $file->add($change->getId());
        $this->assertSame(
            intval($file->getStatus('change')),
            $change->getId(),
            "Expected file to be open in specified change."
        );
    }

    /**
     * Test fetching a file, plus content cache manipulation.
     */
    public function testFetch()
    {
        $file = new P4_File;
        $filespec = '//depot/a_file_to_fetch.txt';

        // test non-existant file
        $file->setFilespec($filespec);
        try {
            $theFile = P4_File::fetch($filespec);
            $this->fail('Unexpected success fetching a non-existant file.');
        } catch (P4_File_NotFoundException $e) {
            $this->assertSame(
                "Cannot fetch file '$filespec'. File does not exist.",
                $e->getMessage(),
                'Expected error while fetching a non-existant file.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception fetching a non-existant file: '
                . $e->getMessage()
            );
        }

        // now make the file exist
        $file->add();
        $contents = 'This file has some content.';
        $file->setLocalContents($contents);
        $file->submit('Make the file available for fetching.');

        $theFile = P4_File::fetch($filespec);
        $this->assertSame($contents, $theFile->getDepotContents(), 'Expected content.');

        // manipulate the content cache
        $newContents = 'And now for something comepletely different.';
        $theFile->setContentCache($newContents);
        $this->assertSame($newContents, $theFile->getDepotContents(), 'Expected new content.');

        // clear the content cache, original content should then be available
        $theFile->clearContentCache();
        $this->assertSame($contents, $theFile->getDepotContents(), 'Expected content.');

        // test manipulation of client content
        $theFile->open();
        $clientContent = 'Different client content.';
        file_put_contents($theFile->getLocalFilename(), $clientContent);
        $this->assertSame($clientContent, $theFile->getLocalContents(), 'Expected client content.');
        $this->assertSame($contents, $theFile->getDepotContents(), 'Expected depot content to be unchanged.');
    }

    /**
     * Test getLocalContent.
     */
    public function testGetLocalContents()
    {
        $tests = array(
            array(
                'label'     => __LINE__ .': no setup',
                'filespec'  => null,
                'add'       => false,
                'content'   => null,
                'error'     => new P4_File_Exception(
                    'Cannot complete operation, no filespec has been specified'
                ),
            ),
            array(
                'label'     => __LINE__ .': only filespec',
                'filespec'  => '//depot/file',
                'add'       => false,
                'content'   => null,
                'error'     => new P4_File_Exception(
                    'Cannot get local file contents. Local file does not exist.'
                ),
            ),
            array(
                'label'     => __LINE__ .': filespec + content',
                'filespec'  => '//depot/file',
                'add'       => true,
                'content'   => 'Some content.',
                'error'     => false,
            ),
        );

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

            // prep test conditions
            $file = new P4_File;
            if (isset($test['filespec'])) {
                $file->setFilespec($test['filespec']);
            }
            if (isset($test['content'])) {
                $file->add();
                $file->setLocalContents($test['content']);
            }

            try {
                $content = $file->getLocalContents();
                if ($test['error']) {
                    $this->fail("$label - Unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (Exception $e) {
                if (!$test['error']) {
                    $this->fail(
                        "$label - Unexpected exception ("
                        . get_class($e) .') '. $e->getMessage()
                    );
                } else {
                    $this->assertSame(
                        get_class($test['error']),
                        get_class($e),
                        "$label - Expected error class"
                    );
                    $this->assertSame(
                        $test['error']->getMessage(),
                        $e->getMessage(),
                        "$label - Expected error message"
                    );
                }
            }

            if (!$test['error']) {
                $this->assertSame(
                    $test['content'],
                    $content,
                    "$label - Expected content"
                );
            }
        }
    }

    /**
     * Test annotated content.
     */
    public function testAnnotatedContent()
    {
        // make a file with several lines
        $lines = array(
            "The first line in the file." . PHP_EOL,
            "The second line for testing." . PHP_EOL,
            "And another for completion.",
        );
        $content = join('', $lines);

        /**
         * Becuase we are using p4 print instead of syncing down the
         * file, the newline replacement will not happen on Windows, causing
         * the test to fail on Windows but pass on Linux.
         */
        $expectedContent = str_replace("\r", '', $content);

        $file = new P4_File;
        $file->setFilespec('//depot/annotated_file.txt')
             ->add(null, 'text')
             ->setLocalContents($content, $lines)
             ->submit('Add the first version');
        $this->assertSame($expectedContent, $file->getDepotContents(), 'Expected content #1');

        $annotations = array();
        foreach ($lines as $line) {
            $annotations[] = array(
                'upper' => '1',
                'lower' => '1',
                'data'  => $line,
            );
        }
        $this->assertSame($annotations, $file->getAnnotateContent(), 'Expected annotate #1');

        // exercise the cache
        $this->assertSame($annotations, $file->getAnnotateContent(), 'Expected annotate #2');
        $file->clearAnnotateCache();
        $this->assertSame($annotations, $file->getAnnotateContent(), 'Expected annotate #2');
    }

    /**
     * Test reopen.
     */
    public function testReopen()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/file.txt')
             ->add(null, 'binary')
             ->setLocalContents('Content.');

        $this->assertSame(
            'binary',
            $file->getStatus('type'),
            'Expect the type explicitly set.'
        );

        $tests = array(
            array(
                'label'     => __LINE__ .': no params',
                'change'    => null,
                'type'      => null,
                'error'     => new InvalidArgumentException(
                    'Cannot reopen file. You must provide a change and/or a filetype.'
                ),
                'expect'    => false,
            ),
            array(
                'label'     => __LINE__ .': default change',
                'change'    => 'default',
                'type'      => null,
                'error'     => false,
                'expect'    => 'binary',
            ),
            array(
                'label'     => __LINE__ .': bogus change',
                'change'    => 'bogus',
                'type'      => null,
                'error'     => new P4_Connection_CommandException(
                    "Command failed: Invalid changelist number 'bogus'."
                ),
                'expect'    => 'binary',
            ),
            array(
                'label'     => __LINE__ .': binary type',
                'change'    => null,
                'type'      => 'binary',
                'error'     => false,
                'expect'    => 'binary',
            ),
            array(
                'label'     => __LINE__ .': text type',
                'change'    => null,
                'type'      => 'text',
                'error'     => false,
                'expect'    => 'text',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            try {
                $file->reopen($test['change'], $test['type']);
                if ($test['error']) {
                    $this->fail("$label - unexpected success");
                }
            } catch (Exception $e) {
                if ($test['error']) {
                    $this->assertSame(
                        get_class($test['error']),
                        get_class($e),
                        "$label - Expected exception class: ". $e->getMessage()
                    );
                    $this->assertSame(
                        $test['error']->getMessage(),
                        rtrim($e->getMessage()),
                        "$label - Expected exception message."
                    );
                } else {
                    $this->fail(
                        "$label - unexpected exception ("
                        . get_class($e) .'): '. $e->getMessage()
                    );
                }
            }

            if (!$test['error']) {
                $this->assertSame(
                    $test['expect'],
                    $file->getStatus('type'),
                    "$label - expected type"
                );
            }
        }


    }

    /**
     * Test where functionality, with getDepotPath/getDepotFilename.
     */
    public function testWhere()
    {
        $file = new P4_File;
        $filename = 'a_file.txt';
        $file->setFilespec("//depot/$filename")
             ->add()
             ->setLocalContents('This file has some content.')
             ->submit('Make the file available.');

        $where = $file->where();
        $this->assertSame(
            array(
                0   => "//depot/$filename",
                1   => "//test-client/$filename",
                2   => $file->getLocalFilename(),
            ),
            $where,
            'Expected where values'
        );

        // test getDepotPath and getDepotFilename
        $this->assertSame($file->getDepotPath(), dirname($where[0]), 'Expected depot path');
        $this->assertSame($file->getDepotFilename(), $where[0], 'Expected depot filename');
    }

    /**
     * Test hasRevspec.
     */
    public function testHasRevspec()
    {
        $tests = array(
            '//depot/file.txt'      => false,
            '//depot/file.txt#head' => true,
            '//depot/file.txt@1'    => true,
        );

        foreach ($tests as $filespec => $expected) {
            $result = P4_File::hasRevspec($filespec);
            $this->assertSame($expected, $result, "Expected result for '$filespec'");
        }
    }

    /**
     * Test _validateFilespec via exists()
     */
    public function test_ValidateFilespec()
    {
        $tests = array(
            array(
                'label'     => __LINE__ .': valid filespec',
                'filespec'  => '//depot/file.txt',
                'error'     => false,
            ),
            array(
                'label'     => __LINE__ .': non-string filespec',
                'filespec'  => false,
                'error'     => true,
            ),
            array(
                'label'     => __LINE__ .': wildcard filespec',
                'filespec'  => '//depot/file*',
                'error'     => true,
            ),
            array(
                'label'     => __LINE__ .': multi-file filespec',
                'filespec'  => '//depot/...',
                'error'     => true,
            ),
        );

        foreach ($tests as $test) {
            try {
                $result = P4_File::exists($test['filespec']);
                if ($test['error']) {
                    $this->fail($test['label'] .' - unexpected success');
                }
            } catch (P4_File_Exception $e) {
                if ($test['error']) {
                    $this->assertSame(
                        'Invalid filespec provided. In this context, filespecs'
                        . ' must be a reference to a single file.',
                        $e->getMessage(),
                        $test['label'] .' - expected error message'
                    );
                } else {
                    $this->fail($test['label'] .' - unexpected failure');
                }
            } catch (Exception $e) {
                $this->fail($test['label'] .' - unexpected exception: '.  $e->getMessage());
            }
        }
    }

    /**
     * Test lock/unlock.
     */
    public function testLockUnlock()
    {
        // In order to test lock/unlock properly, we need to create another user.
        $password = 'testUser1pass';
        $user2 = new P4_User;
        $user2->setValues(
            array(
                'User'      => 'testUser1',
                'Email'     => 'testUser1@testhost',
                'FullName'  => 'Test User 1',
                'Password'  => $password,
            )
        )->save();

        // create the user's client spec
        $client = new P4_Client;
        $client->setId($this->utility->getP4Params('client') .'-'. $user2->getId());
        $client->setRoot($this->utility->getP4Params('clientRoot') .'/'. $user2->getId())
               ->setView(array('//depot/... //'. $client->getId() .'/...'))
               ->setOwner($user2->getId())
               ->save();

        // create a connection for the new user
        $userP4 = P4_Connection::factory(
            $this->utility->getP4Params('port'), $user2->getId(), $client->getId(), $password, null, null
        );

        // and now the tests
        // have testuser create a file, submit it, and then open for edit and lock it
        $testFile = new P4_File($userP4);
        $testFile->setFilespec('//depot/test_user_file.txt')
                 ->add()
                 ->setLocalContents('Test user content')
                 ->submit('Adding test user file.')
                 ->open()
                 ->lock();

        // have superuser attempt to open/submit the file, expect failure
        $superFile = new P4_File($this->p4);
        $superFile->setFilespec('//depot/test_user_file.txt')
                  ->open()
                  ->setLocalContents('Super user content.');
        try {
            $superFile->submit('Place super content in file.');
            $this->fail('Unexpected success submitting to a locked file.');
        } catch (P4_Connection_CommandException $e) {
            $this->assertRegExp(
                "/Command failed: No files to submit.*"
                . "File\(s\) couldn't be locked.*"
                . "Submit failed -- fix problems above then use 'p4 submit -c 2'./s",
                $e->getMessage(),
                'Expected error submitting to a locked file.'
            );
        } catch (Exception $e) {
            $this->fail('Unexpected exception after submit to a locked file: '.  $e->getMessage());
        }

        // have the testuser unlock the file
        $testFile->unlock();

        // have the superuser attempt to open/submit the file, expect success
        $superFile = new P4_File($this->p4);
        $superFile->setFilespec('//depot/test_user_file.txt')
                  ->edit()
                  ->setLocalContents('Super user content.')
                  ->submit('Place super content in file.');
        $this->assertTrue(true, 'Expect submit to succeed');
        $testFile->revert();
    }

    /**
     * Prepare files for fetchAll testing.
     *
     * @param   string  $basePath  base depot path for file creation; default='//depot/'
     * @param   bool    $sleep     Sleep between file creation to create unique timestamps,
     *                             true = 1 second sleep, false (default) no sleep.
     * @return  array  The list of P4_File objects created.
     */
    protected function _prepareFetchAllTests($basePath = '//depot/', $sleep = false)
    {
        $files = array();
        // make a small file
        $file = new P4_File;
        $file->setFilespec($basePath .'small.txt')
             ->open()
             ->setLocalContents('A small file.')
             ->setAttribute('foo', 'small')
             ->submit('Add a small file.');
        $files[] = $file;
        if ($sleep) {
            sleep(1); // guarantee next file has different timestamp
        }

        // make a medium-sized file
        $file = new P4_File;
        $file->setFilespec($basePath .'medium.jpg')
             ->add(null, 'binary')
             ->setLocalContents('This is a medium-sized file for testing.')
             ->setAttribute('foo', 'medium')
             ->submit('Add a medium file.');
        $files[] = $file;
        if ($sleep) {
            sleep(1); // guarantee next file has different timestamp
        }

        // make a large-sized file
        $largeContent = array();
        for ($i = 0; $i < 100; $i++) {
            $largeContent[] = 'Large files have plenty of content.';
        }
        $file = new P4_File;
        $file->setFilespec($basePath .'large.txt')
             ->open()
             ->setLocalContents(join(' ', $largeContent))
             ->setAttribute('foo', 'large')
             ->submit('Add a large file.');
        $files[] = $file;
        if ($sleep) {
            sleep(1); // guarantee next file has different timestamp
        }

        // make another small file
        $file = new P4_File;
        $file->setFilespec($basePath .'another_small.txt')
             ->open()
             ->setLocalContents('A small file.')
             ->setAttribute('foo', 'small')
             ->submit('Add another small file.');
        $files[] = $file;
        if ($sleep) {
            sleep(1); // guarantee next file has different timestamp
        }

        // make another medium file
        $file = new P4_File;
        $file->setFilespec($basePath .'ze_medium_2.jpg')
             ->add(null, 'binary')
             ->setLocalContents('This is a medium-sized file for testing.')
             ->setAttribute('foo', 'another medium')
             ->submit('Add another medium file.');
        $files[] = $file;
        if ($sleep) {
            sleep(1); // guarantee next file has different timestamp
        }

        // make a pending file
        $file = new P4_File;
        $file->setFilespec($basePath .'opened.txt')
             ->add()
             ->setLocalContents('opened.')
             ->setAttribute('foo', 'opened');
        $this->assertTrue(
            file_put_contents($file->getLocalFilename(), 'opened.') !== false,
            'Should be able to write opened file to client workspace.'
        );
        $files[] = $file;
        // the final sleep is not necessary here.

        // add keys by filename for easier lookups
        foreach ($files as $file) {
            $files[basename($file->getFilespec())] = $file;
        }
        return $files;
    }

    /**
     * Test stripRevspec.
     */
    public function testStripRevspec()
    {
        $tests = array(
            array(
                'label'     => __LINE__ .': null',
                'revspec'   => null,
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': empty string',
                'revspec'   => '',
                'expect'    => '',
            ),
            array(
                'label'     => __LINE__ .': numeric',
                'revspec'   => 123,
                'expect'    => 123,
            ),
            array(
                'label'     => __LINE__ .': string',
                'revspec'   => 'abc',
                'expect'    => 'abc',
            ),
            array(
                'label'     => __LINE__ .': #',
                'revspec'   => '#',
                'expect'    => '',
            ),
            array(
                'label'     => __LINE__ .': string#',
                'revspec'   => 'abc#',
                'expect'    => 'abc',
            ),
            array(
                'label'     => __LINE__ .': #string',
                'revspec'   => '#abc',
                'expect'    => '',
            ),
            array(
                'label'     => __LINE__ .': @',
                'revspec'   => '@',
                'expect'    => '',
            ),
            array(
                'label'     => __LINE__ .': string@',
                'revspec'   => 'abc@',
                'expect'    => 'abc',
            ),
            array(
                'label'     => __LINE__ .': @string',
                'revspec'   => '@abc',
                'expect'    => '',
            ),

            array(
                'label'     => __LINE__ .': depot file',
                'revspec'   => '//depot/file.txt',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file #have',
                'revspec'   => '//depot/file.txt#have',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file #head',
                'revspec'   => '//depot/file.txt#head',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file #none',
                'revspec'   => '//depot/file.txt#none',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file #123',
                'revspec'   => '//depot/file.txt#123',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file @123',
                'revspec'   => '//depot/file.txt@123',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file @test-ws',
                'revspec'   => '//depot/file.txt@test-ws',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file @2009-12-31',
                'revspec'   => '//depot/file.txt@2009-12-31',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file #head@2009-12-31',
                'revspec'   => '//depot/file.txt#head@2009-12-31',
                'expect'    => '//depot/file.txt',
            ),
            array(
                'label'     => __LINE__ .': depot file @2009-12-31#head',
                'revspec'   => '//depot/file.txt@2009-12-31#head',
                'expect'    => '//depot/file.txt',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $actual = P4_File::stripRevspec($test['revspec']);
            $this->assertSame(
                $test['expect'],
                $actual,
                "$label - Expected result"
            );
        }
    }

    /**
     * Ensure that binary data in files and attributes is handled safely.
     */
    public function testBinarySafeness()
    {
        // make test data that contains null bytes.
        $data = str_repeat("deadbeefcafe\0", 1000);

        // make file obj to stick data in.
        $file = new P4_File;
        $file->setFilespec('//depot/test-file')
             ->setLocalContents($data);

        // make sure we wrote the local data correctly.
        $this->assertSame(
            $data,
            $file->getLocalContents(),
            'Expected matching data.'
        );

        // open for add and ensure type is binary.
        $file->add();
        $this->assertTrue($file->isOpened(), 'Expected file to be open.');
        $this->assertSame(
            'binary',
            $file->getStatus('type'),
            'Expected binary file type.'
        );
        $file->submit('Test of binary data.');

        // ensure depot contents match test data.
        $this->assertSame(
            $data,
            $file->getDepotContents(),
            'Expected matching depot vs. in-memory data post submit.'
        );
        $this->assertSame(
            $file->getLocalContents(),
            $file->getDepotContents(),
            'Expected matching depot vs. local data post submit.'
        );

        // try again with fresh objects.
        $file = P4_File::fetch('//depot/test-file');
        $this->assertSame(
            $data,
            $file->getDepotContents(),
            'Expected matching data w. fresh file obj.'
        );
        $query = P4_File_Query::create()->addFilespec('//depot/...');
        $files = P4_File::fetchAll($query);
        $this->assertSame(
            $data,
            $files->current()->getDepotContents(),
            'Expected matching data via fetch all'
        );

        // re-type to text and try again (should still be binary safe).
        $file->edit(null, 'text')->submit('now text');
        $this->assertSame(
            'text',
            $file->getStatus('headType'),
            'Expected text file type.'
        );
        $this->assertSame(
            $data,
            $file->getDepotContents(),
            'Expected matching data even w. text file type.'
        );
        $file = P4_File::fetch('//depot/test-file');
        $this->assertSame(
            $data,
            $file->getDepotContents(),
            'Expected matching data w. text type and fresh file obj.'
        );

        // try binary data in attributes.
        $file = new P4_File;
        $file->setFilespec('//depot/file-w-attr')
             ->touchLocalFile()
             ->add()
             ->setAttribute('foo', $data);
        $this->assertSame(
            $data,
            $file->getOpenAttribute('foo'),
            'Expected matching data in attribute.'
        );

        // submit and check again.
        $file->submit('binary attr.');
        $this->assertSame(
            $data,
            $file->getAttribute('foo'),
            'Expected matching data in attribute post submit.'
        );

        // fetch and check again.
        $file = P4_File::fetch('//depot/file-w-attr');
        $this->assertSame(
            $data,
            $file->getAttribute('foo'),
            'Expected matching data in attribute post submit w. fresh file obj.'
        );
    }

    /**
     * Test retrieving changes related to a given file
     */
    public function testGetChanges()
    {
        $file = new P4_File;

        try {
            $file->getChanges();
            $this->fail('expected exception when no filespec is set');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Cannot complete operation, no filespec has been specified",
                $e->getMessage()
            );
        }

        $file->setFilespec("//depot/testFile2")
             ->add()
             ->setLocalContents('should not see me')
             ->submit('create a different file');

        $file = new P4_File;
        $file->setFilespec("//depot/testFile")
             ->add()
             ->setLocalContents('0')
             ->submit('create file');

        for ($i=1; $i <= 10; $i++) {
            $file->edit()
                 ->setLocalContents($i)
                 ->submit('Test save ' . $i);
        }

        $this->assertSame(
            array (
                0  => 'Test save 10',
                1  => 'Test save 9',
                2  => 'Test save 8',
                3  => 'Test save 7',
                4  => 'Test save 6',
                5  => 'Test save 5',
                6  => 'Test save 4',
                7  => 'Test save 3',
                8  => 'Test save 2',
                9  => 'Test save 1',
                10 => 'create file',
            ),
            array_map('trim', $file->getChanges()->invoke('getDescription'))
        );
    }

    /**
     * Test retrieving the change related to a given file
     */
    public function testGetChange()
    {
        $file = new P4_File;

        try {
            $file->getChange();
            $this->fail('expected exception when no filespec is set');
        } catch (P4_File_Exception $e) {
            $this->assertSame(
                "Cannot complete operation, no filespec has been specified",
                $e->getMessage()
            );
        }

        $file->setFilespec("//depot/testFile2")
             ->add()
             ->setLocalContents('should not see me')
             ->submit('create a different file');

        $file = new P4_File;
        $file->setFilespec("//depot/testFile")
             ->add()
             ->setLocalContents('0')
             ->submit('create file');

        for ($i=1; $i <= 10; $i++) {
            $file->edit()
                 ->setLocalContents($i)
                 ->submit('Test save ' . $i);
        }

        $file = P4_File::fetch('//depot/testFile#2');
        $this->assertEquals('Test save 1', trim($file->getChange()->getDescription()), 'Expected change description');
    }

    /**
     * Test retrieving previor revisions of a given file
     */
    public function testFetchRevision()
    {
        $file = new P4_File;
        $file->setFilespec("//depot/testFile")
             ->add()
             ->setLocalContents('0')
             ->submit('create file');

        for ($i=1; $i <= 10; $i++) {
            $file->edit()
                 ->setLocalContents($i)
                 ->submit('Test save ' . $i);
        }

        $this->assertSame(11, count($file->getChanges()), 'expected matching number of changes');

        for ($i=1; $i <= 11; $i++) {
            $file = P4_File::fetch("//depot/testFile#$i");

            $this->assertSame(
                (string)($i-1),
                $file->getDepotContents(),
                'expected matching value for revision '.$i
            );
        }
    }

    /**
     * Test rolling back a files content
     */
    public function testRollback()
    {
        $file = new P4_File;
        $file->setFilespec("//depot/testFile")
             ->add()
             ->setLocalContents('0')
             ->submit('create file');

        for ($i=1; $i <= 10; $i++) {
            $file->edit()
                 ->setLocalContents($i)
                 ->setAttribute('number', (string)$i)
                 ->submit('Test save ' . $i);
        }

        $this->assertSame(11, count($file->getChanges()), 'expected matching number of changes');

        $file = P4_File::fetch('//depot/testFile#2');
        $this->assertSame(
            '//depot/testFile#2',
            $file->getFilespec(),
            'expected rev in filespec pre rollback'
        );

        $file->sync()
             ->edit()
             ->submit('rollin rollin rollin', P4_File::RESOLVE_ACCEPT_YOURS);

        $this->assertSame(
            '//depot/testFile',
            $file->getFilespec(),
            'expected updated filespec post rollback'
        );
        $this->assertSame(
            '1',
            P4_File::fetch('//depot/testFile')->getDepotContents(),
            'expected matching content post rollback'
        );
        $this->assertSame(
            '1',
            P4_File::fetch('//depot/testFile')->getAttribute('number'),
            'expected matching attribute post rollback'
        );
    }

    /**
     * Test competing adds
     *
     * @expectedException P4_Connection_ConflictException
     */
    public function testCompetingAdds()
    {
        // create a second workspace.
        $clientOne = P4_Client::fetch($this->p4->getClient());
        $clientTwo = new P4_Client;
        $clientTwo->setId($clientOne->getId() . "-2")
                  ->setRoot($clientOne->getRoot() . "-2")
                  ->setView(array('//depot/... //' . $clientOne->getId() . '-2/...'))
                  ->save();

        // create a second connection.
        $p4One = $this->p4;
        $p4Two = P4_Connection::factory(
            $p4One->getPort(),
            $p4One->getUser(),
            $clientTwo->getId(),
            $p4One->getPassword()
        )->connect();

        $fileOne = new P4_File($p4One);
        $fileOne->setFilespec('//depot/foo')
                ->setLocalContents('test one')
                ->add();

        $fileTwo = new P4_File($p4Two);
        $fileTwo->setFilespec('//depot/foo')
                ->setLocalContents('test two')
                ->add();

        // conflict (can't help the user!)
        $fileOne->submit('test one');
        $fileTwo->submit('test two', P4_File::RESOLVE_ACCEPT_YOURS);
    }

    /**
     * Test competing edits
     */
    public function testCompetingEdits()
    {
        // create a second workspace.
        $clientOne = P4_Client::fetch($this->p4->getClient());
        $clientTwo = new P4_Client;
        $clientTwo->setId($clientOne->getId() . "-2")
                  ->setRoot($clientOne->getRoot() . "-2")
                  ->setView(array('//depot/... //' . $clientOne->getId() . '-2/...'))
                  ->save();

        // create a second connection.
        $p4One = $this->p4;
        $p4Two = P4_Connection::factory(
            $p4One->getPort(),
            $p4One->getUser(),
            $clientTwo->getId(),
            $p4One->getPassword()
        )->connect();

        $fileOne = new P4_File($p4One);
        $fileOne->setFilespec('//depot/foo')
                ->setLocalContents('test one')
                ->add()
                ->submit('inital add');

        $fileOne = P4_File::fetch('//depot/foo', $p4One);
        $fileOne->edit()->setLocalContents('test two');

        $fileTwo = P4_File::fetch('//depot/foo', $p4Two);
        $fileTwo->sync()->edit()->setLocalContents('test three');

        // conflict
        $fileOne->submit('test two')->edit()->setLocalContents('test laksdfj')->submit('lkasdfj');
        $fileTwo->submit('test three', P4_File::RESOLVE_ACCEPT_YOURS);
    }

    /**
     * Test edit of deleted file.
     *
     * @expectedException  P4_File_Exception
     */
    public function testEditOfDeleted()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/foo')
             ->setLocalContents('one')
             ->add()
             ->submit('one');

        $file->edit()
             ->setLocalContents('two')
             ->submit('two');

        $file->delete()
             ->submit('three');

        $file = P4_File::fetch('//depot/foo#1');

        // should throw.
        $file->sync()->edit();
    }

    /**
     * Test delete of deleted file.
     *
     * @expectedException  P4_File_Exception
     */
    public function testDeleteOfDeleted()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/foo')
             ->setLocalContents('one')
             ->add()
             ->submit('one');

        $file->edit()
             ->setLocalContents('two')
             ->submit('two');

        $file->delete()
             ->submit('three');

        $file = P4_File::fetch('//depot/foo#1');

        // should throw.
        $file->sync()->delete();
    }

    /**
     * Test a corner case -- sync, edit and submit a file after removing it
     * from the client workspace and opening it for delete with -v.
     */
    public function testDeleteSyncEdit()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/foo')
             ->touchLocalFile()
             ->add()
             ->submit('test');

        $file->setFilespec('//depot/foo#0')->sync();

        $file = new P4_File;
        $file->setFilespec('//depot/foo')
             ->delete();

        $file->sync()->edit()->submit('test');
    }

    /**
     * Verify attribute setting doesn't exceed p4d's N_OPTS limit.
     */
    public function testOptionLimit()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/foo');
        $file->open();

        $limit      = P4_Connection_Abstract::OPTION_LIMIT + 1;
        $attributes = array_fill(0, $limit, 'attribute');
        $file->clearAttributes($attributes);
    }

    /**
     * Test revert and revert unchanged.
     */
    public function testRevert()
    {
        $file = new P4_File;
        $file->setFilespec('//depot/foo')
             ->setLocalContents('test content')
             ->add()
             ->submit('adding file');

        // no change, should revert.
        $file->edit()
             ->revert(P4_File::REVERT_UNCHANGED);
        $this->assertFalse($file->isOpened());

        // content change, should not revert.
        $file->edit()
             ->setLocalContents('new content')
             ->revert(P4_File::REVERT_UNCHANGED);
        $this->assertTrue($file->isOpened());

        // should revert regardless.
        $file->revert();
        $this->assertFalse($file->isOpened());
    }
}
# 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/File/Test.php
#1 8972 Matt Attaway Initial add of the Chronicle source code