1
0
Fork 0

Overhaul VcsDrivers, introduce TransportException for remote filesystem errors

pull/414/head
Jordi Boggiano 2012-03-08 21:59:02 +01:00
parent 53ab5011f0
commit 3e22084ea4
11 changed files with 73 additions and 117 deletions

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Downloader;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class TransportException extends \Exception
{
}

View File

@ -49,7 +49,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getRootIdentifier() public function getRootIdentifier()
{ {
if (null === $this->rootIdentifier) { if (null === $this->rootIdentifier) {
$repoData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository), true); $repoData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository));
$this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master'; $this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master';
} }
@ -93,13 +93,13 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
if (!isset($this->infoCache[$identifier])) { if (!isset($this->infoCache[$identifier])) {
$composer = $this->getContents($this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'); $composer = $this->getContents($this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json');
if (!$composer) { if (!$composer) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
if (!isset($composer['time'])) { if (!isset($composer['time'])) {
$changeset = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier), true); $changeset = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier));
$composer['time'] = $changeset['timestamp']; $composer['time'] = $changeset['timestamp'];
} }
$this->infoCache[$identifier] = $composer; $this->infoCache[$identifier] = $composer;
@ -114,7 +114,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getTags() public function getTags()
{ {
if (null === $this->tags) { if (null === $this->tags) {
$tagsData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); $tagsData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'));
$this->tags = array(); $this->tags = array();
foreach ($tagsData as $tag => $data) { foreach ($tagsData as $tag => $data) {
$this->tags[$tag] = $data['raw_node']; $this->tags[$tag] = $data['raw_node'];
@ -130,7 +130,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getBranches() public function getBranches()
{ {
if (null === $this->branches) { if (null === $this->branches) {
$branchData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'), true); $branchData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'));
$this->branches = array(); $this->branches = array();
foreach ($branchData as $branch => $data) { foreach ($branchData as $branch => $data) {
$this->branches[$branch] = $data['raw_node']; $this->branches[$branch] = $data['raw_node'];
@ -140,20 +140,6 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
return $this->branches; return $this->branches;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -9,7 +9,7 @@ use Composer\IO\IOInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
class GitDriver extends VcsDriver implements VcsDriverInterface class GitDriver extends VcsDriver
{ {
protected $tags; protected $tags;
protected $branches; protected $branches;
@ -117,7 +117,7 @@ class GitDriver extends VcsDriver implements VcsDriverInterface
$this->process->execute(sprintf('cd %s && git show %s:composer.json', escapeshellarg($this->repoDir), escapeshellarg($identifier)), $composer); $this->process->execute(sprintf('cd %s && git show %s:composer.json', escapeshellarg($this->repoDir), escapeshellarg($identifier)), $composer);
if (!trim($composer)) { if (!trim($composer)) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
@ -173,20 +173,6 @@ class GitDriver extends VcsDriver implements VcsDriverInterface
return $this->branches; return $this->branches;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -8,7 +8,7 @@ use Composer\IO\IOInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
class GitHubDriver extends VcsDriver implements VcsDriverInterface class GitHubDriver extends VcsDriver
{ {
protected $owner; protected $owner;
protected $repository; protected $repository;
@ -39,7 +39,7 @@ class GitHubDriver extends VcsDriver implements VcsDriverInterface
public function getRootIdentifier() public function getRootIdentifier()
{ {
if (null === $this->rootIdentifier) { if (null === $this->rootIdentifier) {
$repoData = json_decode($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository), true); $repoData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository));
$this->rootIdentifier = $repoData['master_branch'] ?: 'master'; $this->rootIdentifier = $repoData['master_branch'] ?: 'master';
} }
@ -83,13 +83,13 @@ class GitHubDriver extends VcsDriver implements VcsDriverInterface
if (!isset($this->infoCache[$identifier])) { if (!isset($this->infoCache[$identifier])) {
$composer = $this->getContents($this->getScheme() . '://raw.github.com/'.$this->owner.'/'.$this->repository.'/'.$identifier.'/composer.json'); $composer = $this->getContents($this->getScheme() . '://raw.github.com/'.$this->owner.'/'.$this->repository.'/'.$identifier.'/composer.json');
if (!$composer) { if (!$composer) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
if (!isset($composer['time'])) { if (!isset($composer['time'])) {
$commit = json_decode($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.$identifier), true); $commit = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.$identifier));
$composer['time'] = $commit['commit']['committer']['date']; $composer['time'] = $commit['commit']['committer']['date'];
} }
$this->infoCache[$identifier] = $composer; $this->infoCache[$identifier] = $composer;
@ -104,7 +104,7 @@ class GitHubDriver extends VcsDriver implements VcsDriverInterface
public function getTags() public function getTags()
{ {
if (null === $this->tags) { if (null === $this->tags) {
$tagsData = json_decode($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags'), true); $tagsData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags'));
$this->tags = array(); $this->tags = array();
foreach ($tagsData as $tag) { foreach ($tagsData as $tag) {
$this->tags[$tag['name']] = $tag['commit']['sha']; $this->tags[$tag['name']] = $tag['commit']['sha'];
@ -120,7 +120,7 @@ class GitHubDriver extends VcsDriver implements VcsDriverInterface
public function getBranches() public function getBranches()
{ {
if (null === $this->branches) { if (null === $this->branches) {
$branchData = json_decode($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/branches'), true); $branchData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/branches'));
$this->branches = array(); $this->branches = array();
foreach ($branchData as $branch) { foreach ($branchData as $branch) {
$this->branches[$branch['name']] = $branch['commit']['sha']; $this->branches[$branch['name']] = $branch['commit']['sha'];
@ -130,20 +130,6 @@ class GitHubDriver extends VcsDriver implements VcsDriverInterface
return $this->branches; return $this->branches;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -18,7 +18,7 @@ use Composer\IO\IOInterface;
/** /**
* @author Per Bernhardt <plb@webfactory.de> * @author Per Bernhardt <plb@webfactory.de>
*/ */
class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface class HgBitbucketDriver extends VcsDriver
{ {
protected $owner; protected $owner;
protected $repository; protected $repository;
@ -49,7 +49,7 @@ class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getRootIdentifier() public function getRootIdentifier()
{ {
if (null === $this->rootIdentifier) { if (null === $this->rootIdentifier) {
$repoData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); $repoData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'));
$this->rootIdentifier = $repoData['tip']['raw_node']; $this->rootIdentifier = $repoData['tip']['raw_node'];
} }
@ -93,13 +93,13 @@ class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface
if (!isset($this->infoCache[$identifier])) { if (!isset($this->infoCache[$identifier])) {
$composer = $this->getContents($this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'); $composer = $this->getContents($this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json');
if (!$composer) { if (!$composer) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
if (!isset($composer['time'])) { if (!isset($composer['time'])) {
$changeset = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier), true); $changeset = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier));
$composer['time'] = $changeset['timestamp']; $composer['time'] = $changeset['timestamp'];
} }
$this->infoCache[$identifier] = $composer; $this->infoCache[$identifier] = $composer;
@ -114,7 +114,7 @@ class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getTags() public function getTags()
{ {
if (null === $this->tags) { if (null === $this->tags) {
$tagsData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); $tagsData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'));
$this->tags = array(); $this->tags = array();
foreach ($tagsData as $tag => $data) { foreach ($tagsData as $tag => $data) {
$this->tags[$tag] = $data['raw_node']; $this->tags[$tag] = $data['raw_node'];
@ -130,7 +130,7 @@ class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface
public function getBranches() public function getBranches()
{ {
if (null === $this->branches) { if (null === $this->branches) {
$branchData = json_decode($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'), true); $branchData = JsonFile::parseJson($this->getContents($this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'));
$this->branches = array(); $this->branches = array();
foreach ($branchData as $branch => $data) { foreach ($branchData as $branch => $data) {
$this->branches[$branch] = $data['raw_node']; $this->branches[$branch] = $data['raw_node'];
@ -140,25 +140,11 @@ class HgBitbucketDriver extends VcsDriver implements VcsDriverInterface
return $this->branches; return $this->branches;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public static function supports($url, $deep = false) public static function supports($url, $deep = false)
{ {
return preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url); return extension_loaded('openssl') && preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url);
} }
} }

View File

@ -19,7 +19,7 @@ use Composer\IO\IOInterface;
/** /**
* @author Per Bernhardt <plb@webfactory.de> * @author Per Bernhardt <plb@webfactory.de>
*/ */
class HgDriver extends VcsDriver implements VcsDriverInterface class HgDriver extends VcsDriver
{ {
protected $tags; protected $tags;
protected $branches; protected $branches;
@ -100,7 +100,7 @@ class HgDriver extends VcsDriver implements VcsDriverInterface
$this->process->execute(sprintf('cd %s && hg cat -r %s composer.json', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $composer); $this->process->execute(sprintf('cd %s && hg cat -r %s composer.json', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $composer);
if (!trim($composer)) { if (!trim($composer)) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier ' . $identifier . ' in ' . $this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
@ -159,20 +159,6 @@ class HgDriver extends VcsDriver implements VcsDriverInterface
return $this->branches; return $this->branches;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -9,7 +9,7 @@ use Composer\IO\IOInterface;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
*/ */
class SvnDriver extends VcsDriver implements VcsDriverInterface class SvnDriver extends VcsDriver
{ {
protected $baseUrl; protected $baseUrl;
protected $tags; protected $tags;
@ -108,7 +108,7 @@ class SvnDriver extends VcsDriver implements VcsDriverInterface
); );
if (!trim($composer)) { if (!trim($composer)) {
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); return;
} }
$composer = JsonFile::parseJson($composer); $composer = JsonFile::parseJson($composer);
@ -226,20 +226,6 @@ class SvnDriver extends VcsDriver implements VcsDriverInterface
); );
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
$this->getComposerInformation($identifier);
return true;
} catch (\Exception $e) {
}
return false;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -12,6 +12,7 @@
namespace Composer\Repository\Vcs; namespace Composer\Repository\Vcs;
use Composer\Downloader\TransportException;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
@ -21,7 +22,7 @@ use Composer\Util\RemoteFilesystem;
* *
* @author François Pluchino <francois.pluchino@opendisplay.com> * @author François Pluchino <francois.pluchino@opendisplay.com>
*/ */
abstract class VcsDriver abstract class VcsDriver implements VcsDriverInterface
{ {
protected $url; protected $url;
protected $io; protected $io;
@ -41,6 +42,20 @@ abstract class VcsDriver
$this->process = $process ?: new ProcessExecutor; $this->process = $process ?: new ProcessExecutor;
} }
/**
* {@inheritDoc}
*/
public function hasComposerFile($identifier)
{
try {
return (Boolean) $this->getComposerInformation($identifier);
} catch (TransportException $e) {
}
return false;
}
/** /**
* Get the https or http protocol depending on SSL support. * Get the https or http protocol depending on SSL support.
* *

View File

@ -2,6 +2,7 @@
namespace Composer\Repository; namespace Composer\Repository;
use Composer\Downloader\TransportException;
use Composer\Repository\Vcs\VcsDriverInterface; use Composer\Repository\Vcs\VcsDriverInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
@ -90,11 +91,14 @@ class VcsRepository extends ArrayRepository
if ($parsedTag && $driver->hasComposerFile($identifier)) { if ($parsedTag && $driver->hasComposerFile($identifier)) {
try { try {
$data = $driver->getComposerInformation($identifier); $data = $driver->getComposerInformation($identifier);
} catch (\Exception $e) { } catch (TransportException $e) {
if ($debug) { if ($debug) {
$this->io->write('Skipped tag '.$tag.', '.$e->getMessage()); $this->io->write('Skipped tag '.$tag.', '.$e->getMessage());
} }
continue; continue;
} catch (\Exception $e) {
$this->io->write('Skipped tag '.$tag.', '.$e->getMessage());
continue;
} }
// manually versioned package // manually versioned package

View File

@ -13,6 +13,7 @@
namespace Composer\Util; namespace Composer\Util;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
/** /**
* @author François Pluchino <francois.pluchino@opendisplay.com> * @author François Pluchino <francois.pluchino@opendisplay.com>
@ -81,7 +82,7 @@ class RemoteFilesystem
* @param boolean $progress Display the progression * @param boolean $progress Display the progression
* @param boolean $firstCall Whether this is the first attempt at fetching this resource * @param boolean $firstCall Whether this is the first attempt at fetching this resource
* *
* @throws \RuntimeException When the file could not be downloaded * @throws TransportException When the file could not be downloaded
*/ */
protected function get($originUrl, $fileUrl, $fileName = null, $progress = true, $firstCall = true) protected function get($originUrl, $fileUrl, $fileName = null, $progress = true, $firstCall = true)
{ {
@ -117,7 +118,7 @@ class RemoteFilesystem
} }
if (false === $this->result) { if (false === $this->result) {
throw new \RuntimeException("The '$fileUrl' file could not be downloaded"); throw new TransportException("The '$fileUrl' file could not be downloaded");
} }
} }
@ -137,7 +138,7 @@ class RemoteFilesystem
case STREAM_NOTIFY_AUTH_REQUIRED: case STREAM_NOTIFY_AUTH_REQUIRED:
case STREAM_NOTIFY_FAILURE: case STREAM_NOTIFY_FAILURE:
if (404 === $messageCode && !$this->firstCall) { if (404 === $messageCode && !$this->firstCall) {
throw new \RuntimeException("The '" . $this->fileUrl . "' URL not found"); throw new TransportException("The '" . $this->fileUrl . "' URL not found", 404);
} }
// for private repository returning 404 error when the authorization is incorrect // for private repository returning 404 error when the authorization is incorrect
@ -149,9 +150,9 @@ class RemoteFilesystem
// get authorization informations // get authorization informations
if (401 === $messageCode || $attemptAuthentication) { if (401 === $messageCode || $attemptAuthentication) {
if (!$this->io->isInteractive()) { if (!$this->io->isInteractive()) {
$mess = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console"; $message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console";
throw new \RuntimeException($mess); throw new TransportException($message, 401);
} }
$this->io->overwrite(' Authentication required (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):'); $this->io->overwrite(' Authentication required (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):');

View File

@ -111,7 +111,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
$this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0); $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0);
$this->fail(); $this->fail();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertInstanceOf('RuntimeException', $e); $this->assertInstanceOf('Composer\Downloader\TransportException', $e);
$this->assertContains('URL not found', $e->getMessage()); $this->assertContains('URL not found', $e->getMessage());
} }
} }
@ -137,7 +137,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
$this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0); $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0);
$this->fail(); $this->fail();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertInstanceOf('RuntimeException', $e); $this->assertInstanceOf('Composer\Downloader\TransportException', $e);
$this->assertContains('URL required authentication', $e->getMessage()); $this->assertContains('URL required authentication', $e->getMessage());
$this->assertAttributeEquals(false, 'firstCall', $fs); $this->assertAttributeEquals(false, 'firstCall', $fs);
} }