diff --git a/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php b/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php new file mode 100644 index 000000000000..8958571bdff7 --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/AbstractAdapter.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Adapter; + +/** + * Interface for finder engine implementations. + * + * @author Jean-François Simon + */ +abstract class AbstractAdapter implements AdapterInterface +{ + protected $followLinks = false; + protected $mode = 0; + protected $minDepth = 0; + protected $maxDepth = INF; + protected $exclude = array(); + protected $names = array(); + protected $notNames = array(); + protected $contains = array(); + protected $notContains = array(); + protected $sizes = array(); + protected $dates = array(); + protected $filters = array(); + protected $sort = false; + + /** + * {@inheritdoc} + */ + public function setFollowLinks($followLinks) + { + $this->followLinks = $followLinks; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setMode($mode) + { + $this->mode = $mode; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setDepths(array $depths) + { + $this->minDepth = 0; + $this->maxDepth = INF; + + foreach ($depths as $comparator) { + switch ($comparator->getOperator()) { + case '>': + $this->minDepth = $comparator->getTarget() + 1; + break; + case '>=': + $this->minDepth = $comparator->getTarget(); + break; + case '<': + $this->maxDepth = $comparator->getTarget() - 1; + break; + case '<=': + $this->maxDepth = $comparator->getTarget(); + break; + default: + $this->minDepth = $this->maxDepth = $comparator->getTarget(); + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setExclude(array $exclude) + { + $this->exclude = $exclude; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setNames(array $names) + { + $this->names = $names; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setNotNames(array $notNames) + { + $this->notNames = $notNames; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setContains(array $contains) + { + $this->contains = $contains; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setNotContains(array $notContains) + { + $this->notContains = $notContains; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setSizes(array $sizes) + { + $this->sizes = $sizes; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setDates(array $dates) + { + $this->dates = $dates; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setFilters(array $filters) + { + $this->filters = $filters; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setSort($sort) + { + $this->sort = $sort; + + return $this; + } +} diff --git a/src/Symfony/Component/Finder/Adapter/AdapterInterface.php b/src/Symfony/Component/Finder/Adapter/AdapterInterface.php new file mode 100644 index 000000000000..246a26cb06ad --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/AdapterInterface.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Adapter; + +/** + * @author Jean-François Simon + */ +interface AdapterInterface +{ + /** + * @param bool $followLinks + * + * @return AdapterInterface Current instance + */ + function setFollowLinks($followLinks); + + /** + * @param int $mode + * + * @return AdapterInterface Current instance + */ + function setMode($mode); + + /** + * @param array $exclude + * + * @return AdapterInterface Current instance + */ + function setExclude(array $exclude); + + /** + * @param array $depths + * + * @return AdapterInterface Current instance + */ + function setDepths(array $depths); + + /** + * @param array $names + * + * @return AdapterInterface Current instance + */ + function setNames(array $names); + + /** + * @param array $notNames + * + * @return AdapterInterface Current instance + */ + function setNotNames(array $notNames); + + /** + * @param array $contains + * + * @return AdapterInterface Current instance + */ + function setContains(array $contains); + + /** + * @param array $notContains + * + * @return AdapterInterface Current instance + */ + function setNotContains(array $notContains); + + /** + * @param array $sizes + * + * @return AdapterInterface Current instance + */ + function setSizes(array $sizes); + + /** + * @param array $dates + * + * @return AdapterInterface Current instance + */ + function setDates(array $dates); + + /** + * @param array $filters + * + * @return AdapterInterface Current instance + */ + function setFilters(array $filters); + + /** + * @param \Closure|int $sort + * + * @return AdapterInterface Current instance + */ + function setSort($sort); + + /** + * @param string $dir + * + * @return \Iterator Result iterator + */ + function searchInDirectory($dir); + + /** + * Tests adapter support for current platform. + * + * @return bool + */ + function isSupported(); + + /** + * Returns adapter name. + * + * @return string + */ + function getName(); +} diff --git a/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php b/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php new file mode 100644 index 000000000000..98ac30f9b2d0 --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/GnuFindAdapter.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Adapter; + +use Symfony\Component\Finder\Iterator; +use Symfony\Component\Finder\Shell\Shell; +use Symfony\Component\Finder\Expression\Expression; +use Symfony\Component\Finder\Shell\Command; +use Symfony\Component\Finder\Iterator\SortableIterator; +use Symfony\Component\Finder\Comparator\NumberComparator; +use Symfony\Component\Finder\Comparator\DateComparator; + +/** + * Shell engine implementation using GNU find command. + * + * @author Jean-François Simon + */ +class GnuFindAdapter extends AbstractAdapter +{ + /** + * @var Shell + */ + private $shell; + + /** + * Constructor. + */ + public function __construct() + { + $this->shell = new Shell(); + } + + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + // having "/../" in path make find fail + $dir = realpath($dir); + + // searching directories containing or not containing strings leads to no result + if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode && ($this->contains || $this->notContains)) { + return new Iterator\FilePathsIterator(array(), $dir); + } + + $command = Command::create(); + + $find = $command + ->ins('find') + ->add('find ') + ->arg($dir) + ->add('-noleaf') // -noleaf option is required for filesystems who doesn't follow '.' and '..' convention + ->add('-regextype posix-extended'); + + if ($this->followLinks) { + $find->add('-follow'); + } + + $find->add('-mindepth')->add($this->minDepth+1); + // warning! INF < INF => true ; INF == INF => false ; INF === INF => true + // https://bugs.php.net/bug.php?id=9118 + if (INF !== $this->maxDepth) { + $find->add('-maxdepth')->add($this->maxDepth+1); + } + + if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode) { + $find->add('-type d'); + } elseif (Iterator\FileTypeFilterIterator::ONLY_FILES === $this->mode) { + $find->add('-type f'); + } + + $this->buildNamesFiltering($find, $this->names); + $this->buildNamesFiltering($find, $this->notNames, true); + $this->buildSizesFiltering($find, $this->sizes); + $this->buildDatesFiltering($find, $this->dates); + + $useGrep = $this->shell->testCommand('grep') && $this->shell->testCommand('xargs'); + $useSort = is_int($this->sort) && $this->shell->testCommand('sort') && $this->shell->testCommand('awk'); + + if ($useGrep && ($this->contains || $this->notContains)) { + $grep = $command->ins('grep'); + $this->buildContentFiltering($grep, $this->contains); + $this->buildContentFiltering($grep, $this->notContains, true); + } + + if ($useSort) { + $this->buildSorting($command, $this->sort); + } + + $paths = $this->shell->testCommand('uniq') ? $command->add('| uniq')->execute() : array_unique($command->execute()); + $iterator = new Iterator\FilePathsIterator($paths, $dir); + + if ($this->exclude) { + $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); + } + + if (!$useGrep && ($this->contains || $this->notContains)) { + $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); + } + + if ($this->filters) { + $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); + } + + if (!$useSort && $this->sort) { + $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); + $iterator = $iteratorAggregate->getIterator(); + } + + return $iterator; + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return $this->shell->getType() !== Shell::TYPE_WINDOWS + && $this->shell->testCommand('find'); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'gnu_find'; + } + + /** + * @param Command $command + * @param string[] $names + * @param bool $not + */ + private function buildNamesFiltering(Command $command, array $names, $not = false) + { + if (0 === count($names)) { + return; + } + + $command->add($not ? '-not' : null)->cmd('('); + + foreach ($names as $i => $name) { + $expr = Expression::create($name); + + // Fixes 'not search' and 'fuls path matching' regex problems. + // - Jokers '.' are replaced by [^/]. + // - We add '[^/]*' before and after regex (if no ^|$ flags are present). + if ($expr->isRegex()) { + $regex = $expr->getRegex(); + $regex->prepend($regex->hasStartFlag() ? '/' : '/[^/]*') + ->setStartFlag(false) + ->setStartJoker(true) + ->replaceJokers('[^/]'); + if (!$regex->hasEndFlag() || $regex->hasEndJoker()) { + $regex->setEndJoker(false)->append('[^/]*'); + } + } + + $command + ->add($i > 0 ? '-or' : null) + ->add($expr->isRegex() + ? ($expr->isCaseSensitive() ? '-regex' : '-iregex') + : ($expr->isCaseSensitive() ? '-name' : '-iname') + ) + ->arg($expr->renderPattern()); + } + + $command->cmd(')'); + } + + /** + * @param Command $command + * @param NumberComparator[] $sizes + */ + private function buildSizesFiltering(Command $command, array $sizes) + { + foreach ($sizes as $i => $size) { + $command->add($i > 0 ? '-and' : null); + + if ('<=' === $size->getOperator()) { + $command->add('-size -'.($size->getTarget()+1).'c'); + continue; + } + + if ('<' === $size->getOperator()) { + $command->add('-size -'.$size->getTarget().'c'); + continue; + } + + if ('>=' === $size->getOperator()) { + $command->add('-size +'.($size->getTarget()-1).'c'); + continue; + } + + if ('>' === $size->getOperator()) { + $command->add('-size +'.$size->getTarget().'c'); + continue; + } + + if ('!=' === $size->getOperator()) { + $command->add('-size -'.$size->getTarget().'c'); + $command->add('-size +'.$size->getTarget().'c'); + continue; + } + + $command->add('-size '.$size->getTarget().'c'); + } + } + + /** + * @param Command $command + * @param DateComparator[] $dates + */ + private function buildDatesFiltering(Command $command, array $dates) + { + foreach ($dates as $i => $date) { + $command->add($i > 0 ? '-and' : null); + + $mins = (int) round((time()-$date->getTarget())/60); + + if (0 > $mins) { + // mtime is in the future + $command->add(' -mmin -0'); + // we will have no result so we dont need to continue + return; + } + + if ('<=' === $date->getOperator()) { + $command->add('-mmin +'.($mins-1)); + continue; + } + + if ('<' === $date->getOperator()) { + $command->add('-mmin +'.$mins); + continue; + } + + if ('>=' === $date->getOperator()) { + $command->add('-mmin -'.($mins+1)); + continue; + } + + if ('>' === $date->getOperator()) { + $command->add('-mmin -'.$mins); + continue; + } + + if ('!=' === $date->getOperator()) { + $command->add('-mmin +'.$mins.' -or -mmin -'.$mins); + continue; + } + + $command->add('-mmin '.$mins); + } + } + + /** + * @param Command $command + * @param array $contains + * @param bool $not + */ + private function buildContentFiltering(Command $command, array $contains, $not = false) + { + foreach ($contains as $contain) { + $expr = Expression::create($contain); + + // todo: avoid forking process for each $pattern by using multiple -e options + $command + ->add('| xargs -r grep -I') + ->add($expr->isCaseSensitive() ? null : '-i') + ->add($not ? '-L' : '-l') + ->add('-Ee')->arg($expr->renderPattern()); + } + } + + private function buildSorting(Command $command, $sort) + { + switch ($sort) { + case SortableIterator::SORT_BY_NAME: + $format = null; + break; + case SortableIterator::SORT_BY_TYPE: + $format = '%y'; + break; + case SortableIterator::SORT_BY_ACCESSED_TIME: + $format = '%A@'; + break; + case SortableIterator::SORT_BY_CHANGED_TIME: + $format = '%C@'; + break; + case SortableIterator::SORT_BY_MODIFIED_TIME: + $format = '%T@'; + break; + default: + throw new \InvalidArgumentException('Unknown sort options: '.$sort.'.'); + } + + $command->get('find')->add('-printf')->arg($format.' %h/%f\\n'); + $command->ins('sort')->add('| sort'); + $command->ins('awk')->add('| awk')->arg('{ print $'.(null === $format ? '1' : '2').' }'); + } +} diff --git a/src/Symfony/Component/Finder/Adapter/PhpAdapter.php b/src/Symfony/Component/Finder/Adapter/PhpAdapter.php new file mode 100644 index 000000000000..2dbae76a565f --- /dev/null +++ b/src/Symfony/Component/Finder/Adapter/PhpAdapter.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Adapter; + +use Symfony\Component\Finder\Iterator; + +/** + * PHP finder engine implementation. + * + * @author Jean-François Simon + */ +class PhpAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + $flags = \RecursiveDirectoryIterator::SKIP_DOTS; + + if ($this->followLinks) { + $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; + } + + $iterator = new \RecursiveIteratorIterator( + new Iterator\RecursiveDirectoryIterator($dir, $flags), + \RecursiveIteratorIterator::SELF_FIRST + ); + + if ($this->minDepth > 0 || $this->maxDepth < INF) { + $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->minDepth, $this->maxDepth); + } + + if ($this->mode) { + $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); + } + + if ($this->exclude) { + $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); + } + + if ($this->names || $this->notNames) { + $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); + } + + if ($this->contains || $this->notContains) { + $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); + } + + if ($this->sizes) { + $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); + } + + if ($this->dates) { + $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); + } + + if ($this->filters) { + $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); + } + + if ($this->sort) { + $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); + $iterator = $iteratorAggregate->getIterator(); + } + + return $iterator; + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'php'; + } +} diff --git a/src/Symfony/Component/Finder/Exception/AdapterFailureException.php b/src/Symfony/Component/Finder/Exception/AdapterFailureException.php new file mode 100644 index 000000000000..15fa22147d83 --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/AdapterFailureException.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Exception; + +use Symfony\Component\Finder\Adapter\AdapterInterface; + +/** + * Base exception for all adapter failures. + * + * @author Jean-François Simon + */ +class AdapterFailureException extends \RuntimeException implements ExceptionInterface +{ + /** + * @var \Symfony\Component\Finder\Adapter\AdapterInterface + */ + private $adapter; + + /** + * @param AdapterInterface $adapter + * @param string|null $message + * @param \Exception|null $previous + */ + public function __construct(AdapterInterface $adapter, $message = null, \Exception $previous = null) + { + $this->adapter = $adapter; + parent::__construct($message ?: 'Search failed with "'.$adapter->getName().'" adapter.', $previous); + } + + /** + * {@inheritdoc} + */ + public function getAdapter() + { + return $this->adapter; + } +} diff --git a/src/Symfony/Component/Finder/Exception/ExceptionInterface.php b/src/Symfony/Component/Finder/Exception/ExceptionInterface.php new file mode 100644 index 000000000000..86d391b91e5c --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + + */ +interface ExceptionInterface +{ + /** + * @return \Symfony\Component\Finder\Adapter\AdapterInterface + */ + function getAdapter(); +} diff --git a/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php b/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php new file mode 100644 index 000000000000..3663112259c4 --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/OperationNotPermitedException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Exception; + +/** + * @author Jean-François Simon + */ +class OperationNotPermitedException extends AdapterFailureException +{ +} diff --git a/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php b/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php new file mode 100644 index 000000000000..2658f6a508fb --- /dev/null +++ b/src/Symfony/Component/Finder/Exception/ShellCommandFailureException.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Exception; + +use Symfony\Component\Finder\Adapter\AdapterInterface; +use Symfony\Component\Finder\Shell\Command; + +/** + * @author Jean-François Simon + */ +class ShellCommandFailureException extends AdapterFailureException +{ + /** + * @var Command + */ + private $command; + + /** + * @param AdapterInterface $adapter + * @param Command $command + * @param \Exception|null $previous + */ + public function __construct(AdapterInterface $adapter, Command $command, \Exception $previous = null) + { + $this->command = $command; + parent::__construct($adapter, 'Shell command failed: "'.$command->join().'".', $previous); + } + + /** + * @return Command + */ + public function getCommand() + { + return $this->command; + } +} diff --git a/src/Symfony/Component/Finder/Expression/Expression.php b/src/Symfony/Component/Finder/Expression/Expression.php new file mode 100644 index 000000000000..b8124124269d --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Expression.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Expression; + +/** + * @author Jean-François Simon + */ +class Expression implements ValueInterface +{ + const TYPE_REGEX = 1; + const TYPE_GLOB = 2; + + /** + * @var ValueInterface + */ + private $value; + + /** + * @param string $expr + * + * @return Expression + */ + public static function create($expr) + { + return new self($expr); + } + + /** + * @param string $expr + */ + public function __construct($expr) + { + try { + $this->value = Regex::create($expr); + } catch(\InvalidArgumentException $e) { + $this->value = new Glob($expr); + } + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(); + } + + /** + * {@inheritdoc} + */ + public function render() + { + return $this->value->render(); + } + + /** + * {@inheritdoc} + */ + public function renderPattern() + { + return $this->value->renderPattern(); + } + + /** + * @return bool + */ + public function isCaseSensitive() + { + return $this->value->isCaseSensitive(); + } + + /** + * @return int + */ + public function getType() + { + return $this->value->getType(); + } + + /** + * @return bool + */ + public function isRegex() + { + return self::TYPE_REGEX === $this->value->getType(); + } + + /** + * @return bool + */ + public function isGlob() + { + return self::TYPE_GLOB === $this->value->getType(); + } + + /** + * @return Regex + */ + public function getRegex() + { + return self::TYPE_REGEX === $this->value->getType() ? $this->value : $this->value->toRegex(); + } +} diff --git a/src/Symfony/Component/Finder/Expression/Glob.php b/src/Symfony/Component/Finder/Expression/Glob.php new file mode 100644 index 000000000000..5e7b2d01127f --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Glob.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Expression; + +/** + * @author Jean-François Simon + */ +class Glob implements ValueInterface +{ + /** + * @var string + */ + private $pattern; + + /** + * @param string $pattern + */ + public function __construct($pattern) + { + $this->pattern = $pattern; + } + + /** + * {@inheritdoc} + */ + public function render() + { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function renderPattern() + { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function getType() + { + return Expression::TYPE_GLOB; + } + + /** + * {@inheritdoc} + */ + public function isCaseSensitive() + { + return true; + } + + /** + * @param bool $strictLeadingDot + * @param bool $strictWildcardSlash + * + * @return Regex + */ + public function toRegex($strictLeadingDot = true, $strictWildcardSlash = true) + { + $firstByte = true; + $escaping = false; + $inCurlies = 0; + $regex = ''; + $sizeGlob = strlen($this->pattern); + for ($i = 0; $i < $sizeGlob; $i++) { + $car = $this->pattern[$i]; + if ($firstByte) { + if ($strictLeadingDot && '.' !== $car) { + $regex .= '(?=[^\.])'; + } + + $firstByte = false; + } + + if ('/' === $car) { + $firstByte = true; + } + + if ('.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { + $regex .= "\\$car"; + } elseif ('*' === $car) { + $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); + } elseif ('?' === $car) { + $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); + } elseif ('{' === $car) { + $regex .= $escaping ? '\\{' : '('; + if (!$escaping) { + ++$inCurlies; + } + } elseif ('}' === $car && $inCurlies) { + $regex .= $escaping ? '}' : ')'; + if (!$escaping) { + --$inCurlies; + } + } elseif (',' === $car && $inCurlies) { + $regex .= $escaping ? ',' : '|'; + } elseif ('\\' === $car) { + if ($escaping) { + $regex .= '\\\\'; + $escaping = false; + } else { + $escaping = true; + } + + continue; + } else { + $regex .= $car; + } + $escaping = false; + } + + return new Regex('^'.$regex.'$'); + } +} diff --git a/src/Symfony/Component/Finder/Expression/Regex.php b/src/Symfony/Component/Finder/Expression/Regex.php new file mode 100644 index 000000000000..40d776394a89 --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/Regex.php @@ -0,0 +1,315 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Expression; + +/** + * @author Jean-François Simon + */ +class Regex implements ValueInterface +{ + const START_FLAG = '^'; + const END_FLAG = '$'; + const BOUNDARY = '~'; + const JOKER = '.*'; + const ESCAPING = '\\'; + + /** + * @var string + */ + private $pattern; + + /** + * @var array + */ + private $options; + + /** + * @var bool + */ + private $startFlag; + + /** + * @var bool + */ + private $endFlag; + + /** + * @var bool + */ + private $startJoker; + + /** + * @var bool + */ + private $endJoker; + + /** + * @param string $expr + * + * @return Regex + * + * @throws \InvalidArgumentException + */ + public static function create($expr) + { + if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $expr, $m)) { + $start = substr($m[1], 0, 1); + $end = substr($m[1], -1); + + if (($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) { + return new self(substr($m[1], 1, -1), $m[2]); + } + } + + throw new \InvalidArgumentException('Given expression is not a regex.'); + } + + /** + * @param string $pattern + * @param string $options + */ + public function __construct($pattern, $options = '') + { + $this->parsePattern($pattern); + $this->options = $options; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(); + } + + /** + * {@inheritdoc} + */ + public function render() + { + return self::BOUNDARY + .$this->renderPattern() + .self::BOUNDARY + .$this->options; + } + + /** + * {@inheritdoc} + */ + public function renderPattern() + { + return ($this->startFlag ? self::START_FLAG : '') + .($this->startJoker ? self::JOKER : '') + .$this->pattern + .($this->endJoker ? self::JOKER : '') + .($this->endFlag ? self::END_FLAG : ''); + } + + /** + * {@inheritdoc} + */ + public function isCaseSensitive() + { + return !$this->hasOption('i'); + } + + /** + * {@inheritdoc} + */ + public function getType() + { + return Expression::TYPE_REGEX; + } + + /** + * @param string $option + * + * @return bool + */ + public function hasOption($option) + { + return false !== strpos($this->options, $option); + } + + /** + * @param string $option + * + * @return Regex + */ + public function addOption($option) + { + if (!$this->hasOption($option)) { + $this->options.= $option; + } + + return $this; + } + + /** + * @param string $option + * + * @return Regex + */ + public function removeOption($option) + { + $this->options = str_replace($option, '', $this->options); + + return $this; + } + + /** + * @param bool $startFlag + * + * @return Regex + */ + public function setStartFlag($startFlag) + { + $this->startFlag = $startFlag; + + return $this; + } + + /** + * @return bool + */ + public function hasStartFlag() + { + return $this->startFlag; + } + + /** + * @param bool $endFlag + * + * @return Regex + */ + public function setEndFlag($endFlag) + { + $this->endFlag = (bool) $endFlag; + + return $this; + } + + /** + * @return bool + */ + public function hasEndFlag() + { + return $this->endFlag; + } + + /** + * @param bool $startJoker + * + * @return Regex + */ + public function setStartJoker($startJoker) + { + $this->startJoker = $startJoker; + + return $this; + } + + /** + * @return bool + */ + public function hasStartJoker() + { + return $this->startJoker; + } + + /** + * @param bool $endJoker + * + * @return Regex + */ + public function setEndJoker($endJoker) + { + $this->endJoker = (bool) $endJoker; + + return $this; + } + + /** + * @return bool + */ + public function hasEndJoker() + { + return $this->endJoker; + } + + /** + * @param string $expr + * + * @return Regex + */ + public function prepend($expr) + { + $this->pattern = $expr.$this->pattern; + + return $this; + } + + /** + * @param string $expr + * + * @return Regex + */ + public function append($expr) + { + $this->pattern .= $expr; + + return $this; + } + + /** + * @param array $replacements + * + * @return Regex + */ + public function replaceJokers($replacement) + { + $replace = function ($subject) use ($replacement) { + $subject = $subject[0]; + $replace = 0 === substr_count($subject, '\\') % 2; + + return $replace ? str_replace('.', $replacement, $subject) : $subject; + }; + + $this->pattern = preg_replace_callback('~[\\\\]*\\.~', $replace, $this->pattern); + + return $this; + } + + /** + * @param string $pattern + */ + private function parsePattern($pattern) + { + if ($this->startFlag = self::START_FLAG === substr($pattern, 0, 1)) { + $pattern = substr($pattern, 1); + } + + if ($this->startJoker = self::JOKER === substr($pattern, 0, 2)) { + $pattern = substr($pattern, 2); + } + + if ($this->endFlag = (self::END_FLAG === substr($pattern, -1) && self::ESCAPING !== substr($pattern, -2, -1))) { + $pattern = substr($pattern, 0, -1); + } + + if ($this->endJoker = (self::JOKER === substr($pattern, -2) && self::ESCAPING !== substr($pattern, -3, -2))) { + $pattern = substr($pattern, 0, -2); + } + + $this->pattern = $pattern; + } +} diff --git a/src/Symfony/Component/Finder/Expression/ValueInterface.php b/src/Symfony/Component/Finder/Expression/ValueInterface.php new file mode 100644 index 000000000000..aca28f4bad89 --- /dev/null +++ b/src/Symfony/Component/Finder/Expression/ValueInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Expression; + +/** + * @author Jean-François Simon + */ +interface ValueInterface +{ + /** + * Renders string representation of expression. + * + * @return string + */ + function render(); + + /** + * Renders string representation of pattern. + * + * @return string + */ + function renderPattern(); + + /** + * Returns value case sensitivity. + * + * @return bool + */ + function isCaseSensitive(); + + /** + * Returns expression type. + * + * @return int + */ + function getType(); +} diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index 489a6f8b9a98..63fa6e70f7f7 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Finder; +use Symfony\Component\Finder\Adapter\AdapterInterface; +use Symfony\Component\Finder\Adapter\GnuFindAdapter; +use Symfony\Component\Finder\Adapter\PhpAdapter; +use Symfony\Component\Finder\Exception\ExceptionInterface; + /** * Finder allows to build rules to find files and directories. * @@ -46,6 +51,7 @@ class Finder implements \IteratorAggregate, \Countable private $iterators = array(); private $contains = array(); private $notContains = array(); + private $adapters = array(); private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'); @@ -55,6 +61,9 @@ class Finder implements \IteratorAggregate, \Countable public function __construct() { $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; + + $this->register(new GnuFindAdapter()); + $this->register(new PhpAdapter(), -50); } /** @@ -69,6 +78,48 @@ public static function create() return new static(); } + /** + * Registers a finder engine implementation. + * + * @param AdapterInterface $adapter An adapter instance + * @param int $priority Highest is selected first + * + * @return Finder The current Finder instance + */ + public function register(Adapter\AdapterInterface $adapter, $priority = 0) + { + $this->adapters[$adapter->getName()] = array( + 'adapter' => $adapter, + 'priority' => $priority, + ); + + return $this->sortAdapters(); + } + + /** + * Removes all adapters registered in the finder. + * + * @return Finder The current Finder instance + */ + public function removeAdapters() + { + $this->adapters = array(); + + return $this; + } + + /** + * Returns registered adapters ordered by priority without extra information. + * + * @return AdapterInterface[] + */ + public function getAdapters() + { + return array_values(array_map(function(array $adapter) { + return $adapter['adapter']; + }, $this->adapters)); + } + /** * Restricts the matching to directories only. * @@ -569,27 +620,25 @@ public function count() return iterator_count($this->getIterator()); } - private function searchInDirectory($dir) + /* + * @return Finder The current Finder instance + */ + private function sortAdapters() { - $flags = \RecursiveDirectoryIterator::SKIP_DOTS; - - if ($this->followLinks) { - $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - } - - $iterator = new \RecursiveIteratorIterator( - new Iterator\RecursiveDirectoryIterator($dir, $flags), - \RecursiveIteratorIterator::SELF_FIRST - ); - - if ($this->depths) { - $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->depths); - } + uasort($this->adapters, function (array $a, array $b) { + return $a['priority'] > $b['priority'] ? -1 : 1; + }); - if ($this->mode) { - $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); - } + return $this; + } + /** + * @param $dir + * + * @return \Iterator + */ + private function searchInDirectory($dir) + { if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { $this->exclude = array_merge($this->exclude, self::$vcsPatterns); } @@ -598,35 +647,42 @@ private function searchInDirectory($dir) $this->notNames[] = '/^\..+/'; } - if ($this->exclude) { - $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); - } - - if ($this->names || $this->notNames) { - $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); - } - - if ($this->contains || $this->notContains) { - $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); - } - - if ($this->sizes) { - $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); - } - - if ($this->dates) { - $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); - } + foreach ($this->adapters as $adapter) { + if (!$adapter['adapter']->isSupported()) { + continue; + } - if ($this->filters) { - $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); + try { + return $this + ->buildAdapter($adapter['adapter']) + ->searchInDirectory($dir); + } catch(ExceptionInterface $e) { + continue; + } } - if ($this->sort) { - $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); - $iterator = $iteratorAggregate->getIterator(); - } + throw new \RuntimeException('No supported adapter found.'); + } - return $iterator; + /** + * @param AdapterInterface $adapter + * + * @return AdapterInterface + */ + private function buildAdapter(AdapterInterface $adapter) + { + return $adapter + ->setFollowLinks($this->followLinks) + ->setDepths($this->depths) + ->setMode($this->mode) + ->setExclude($this->exclude) + ->setNames($this->names) + ->setNotNames($this->notNames) + ->setContains($this->contains) + ->setNotContains($this->notContains) + ->setSizes($this->sizes) + ->setDates($this->dates) + ->setFilters($this->filters) + ->setSort($this->sort); } } diff --git a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php index 832125393f1a..77a9f45f2dfa 100644 --- a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php @@ -24,31 +24,11 @@ class DepthRangeFilterIterator extends FilterIterator * Constructor. * * @param \RecursiveIteratorIterator $iterator The Iterator to filter - * @param array $comparators An array of \NumberComparator instances + * @param int $minDepth The min depth + * @param int $maxDepth The max depth */ - public function __construct(\RecursiveIteratorIterator $iterator, array $comparators) + public function __construct(\RecursiveIteratorIterator $iterator, $minDepth = 0, $maxDepth = INF) { - $minDepth = 0; - $maxDepth = INF; - foreach ($comparators as $comparator) { - switch ($comparator->getOperator()) { - case '>': - $minDepth = $comparator->getTarget() + 1; - break; - case '>=': - $minDepth = $comparator->getTarget(); - break; - case '<': - $maxDepth = $comparator->getTarget() - 1; - break; - case '<=': - $maxDepth = $comparator->getTarget(); - break; - default: - $minDepth = $maxDepth = $comparator->getTarget(); - } - } - $this->minDepth = $minDepth; $iterator->setMaxDepth(INF === $maxDepth ? -1 : $maxDepth); diff --git a/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php b/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php new file mode 100644 index 000000000000..5153ed301aea --- /dev/null +++ b/src/Symfony/Component/Finder/Iterator/FilePathsIterator.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\SplFileInfo; + +/** + * Iterate over shell command result. + * + * @author Jean-François Simon + */ +class FilePathsIterator extends \ArrayIterator +{ + /** + * @var string + */ + private $baseDir; + + /** + * @var int + */ + private $baseDirLength; + + /** + * @var string + */ + private $subPath; + + /** + * @var string + */ + private $subPathname; + + /** + * @param array $paths List of paths returned by shell command + * @param string $baseDir Base dir for relative path building + */ + public function __construct(array $paths, $baseDir) + { + $this->baseDir = $baseDir; + $this->baseDirLength = strlen($baseDir); + + parent::__construct($paths); + } + + /** + * @param string $name + * @param array $arguments + * + * @return mixed + */ + public function __call($name, array $arguments) + { + return call_user_func_array(array($this->current(), $name), $arguments); + } + + /** + * Return an instance of SplFileInfo with support for relative paths. + * + * @return SplFileInfo File information + */ + public function current() + { + return new SplFileInfo(parent::current(), $this->subPath, $this->subPathname); + } + + public function next() + { + parent::next(); + + $this->buildSubPath(); + } + + public function rewind() + { + parent::rewind(); + + $this->buildSubPath(); + } + + /** + * @return string + */ + public function getSubPath() + { + return $this->subPath; + } + + /** + * @return string + */ + public function getSubPathname() + { + return $this->subPathname; + } + + /** + * @param string $absolutePath + * + * @return null|string + */ + private function buildSubPath() + { + $absolutePath = parent::current(); + + if ($this->baseDir === substr($absolutePath, 0, $this->baseDirLength)) { + $this->subPathname = ltrim(substr($absolutePath, $this->baseDirLength), '/\\'); + $dir = dirname($this->subPathname); + $this->subPath = '.' === $dir ? '' : $dir; + } else { + $this->subPath = $this->subPathname = ''; + } + } +} diff --git a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php index 109b5f00294b..3c0f3aa70ef7 100644 --- a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Finder\Iterator; -use Symfony\Component\Finder\Glob; +use Symfony\Component\Finder\Expression\Expression; /** * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). @@ -63,6 +63,6 @@ public function accept() */ protected function toRegex($str) { - return $this->isRegex($str) ? $str : Glob::toRegex($str); + return Expression::create($str)->getRegex()->render(); } } diff --git a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php index 12584b186e00..3a9dd5558285 100644 --- a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Finder\Iterator; +use Symfony\Component\Finder\Expression\Expression; + /** * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). * @@ -52,20 +54,7 @@ public function __construct(\Iterator $iterator, array $matchPatterns, array $no */ protected function isRegex($str) { - if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) { - $start = substr($m[1], 0, 1); - $end = substr($m[1], -1); - - if ($start === $end) { - return !preg_match('/[*?[:alnum:] \\\\]/', $start); - } - - if ($start === '{' && $end === '}') { - return true; - } - } - - return false; + return Expression::create($str)->isRegex(); } /** diff --git a/src/Symfony/Component/Finder/Shell/Command.php b/src/Symfony/Component/Finder/Shell/Command.php new file mode 100644 index 000000000000..477c664e111f --- /dev/null +++ b/src/Symfony/Component/Finder/Shell/Command.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Shell; + +/** + * @author Jean-François Simon + */ +class Command +{ + /** + * @var Command|null + */ + private $parent; + + /** + * @var array + */ + private $bits; + + /** + * @var array + */ + private $labels; + + /** + * Constructor. + * + * @param Command $parent Parent command + */ + public function __construct(Command $parent = null) + { + $this->parent = $parent; + $this->bits = array(); + $this->labels = array(); + } + + /** + * Returns command as string. + * + * @return string + */ + public function __toString() + { + return $this->join(); + } + + /** + * Creates a new Command instance. + * + * @param Command $parent Parent command + * + * @return Command New Command instance + */ + public static function create(Command $parent = null) + { + return new self($parent); + } + + /** + * Escapes special chars from input. + * + * @param string $input A string to escape + * + * @return string The escaped string + */ + public static function escape($input) + { + return escapeshellcmd($input); + } + + /** + * Quotes input. + * + * @param string $input An argument string + * + * @return string The quoted string + */ + public static function quote($input) + { + return escapeshellarg($input); + } + + /** + * Appends a string or a Command instance. + * + * @param string|Command $bit + * + * @return Command The current Command instance + */ + public function add($bit) + { + $this->bits[] = $bit; + + return $this; + } + + /** + * Prepends a string or a command instance. + * + * @param string|Command $bit + * + * @return Command The current Command instance + */ + public function top($bit) + { + array_unshift($this->bits, $bit); + + foreach ($this->labels as $label => $index) { + $this->labels[$label] += 1; + } + + return $this; + } + + /** + * Appends an argument, will be quoted. + * + * @param string $arg + * + * @return Command The current Command instance + */ + public function arg($arg) + { + $this->bits[] = self::quote($arg); + + return $this; + } + + /** + * Appends escaped special command chars. + * + * @param string $esc + * + * @return Command The current Command instance + */ + public function cmd($esc) + { + $this->bits[] = self::escape($esc); + + return $this; + } + + /** + * Inserts a labeled command to feed later. + * + * @param string $label The unique label + * + * @return Command The current Command instance + * + * @throws \RuntimeException If label already exists + */ + public function ins($label) + { + if (isset($this->labels[$label])) { + throw new \RuntimeException('Label "'.$label.'" already exists.'); + } + + $this->bits[] = self::create($this); + $this->labels[$label] = count($this->bits)-1; + + return $this->bits[$this->labels[$label]]; + } + + /** + * Retrieves a previously labeled command. + * + * @param string $label + * + * @return Command The labeled command + */ + public function get($label) + { + if (!isset($this->labels[$label])) { + throw new \RuntimeException('Label "'.$label.'" does not exists.'); + } + + return $this->bits[$this->labels[$label]]; + } + + /** + * Returns parent command (if any). + * + * @return Command Parent command + * + * @throws \RuntimeException If command has no parent + */ + public function end() + { + if (null === $this->parent) { + throw new \RuntimeException('Calling end on root command dont makes sense.'); + } + + return $this->parent; + } + + /** + * Counts bits stored in command. + * + * @return int The bits count + */ + public function length() + { + return count($this->bits); + } + + /** + * Executes current command. + * + * @return array The command result + */ + public function execute() + { + exec($this->join(), $output, $code); + + if (0 !== $code) { + throw new \RuntimeException('Execution failed with return code: '.$code.'.'); + } + + return $output ?: array(); + } + + /** + * Joins bits. + * + * @return string + */ + public function join() + { + return implode(' ', array_filter( + array_map(function($bit) { + return $bit instanceof Command ? $bit->join() : ($bit ?: null); + }, $this->bits), + function($bit) { return null !== $bit; } + )); + } +} diff --git a/src/Symfony/Component/Finder/Shell/Shell.php b/src/Symfony/Component/Finder/Shell/Shell.php new file mode 100644 index 000000000000..0c8bc7ef03e3 --- /dev/null +++ b/src/Symfony/Component/Finder/Shell/Shell.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Shell; + +/** + * @author Jean-François Simon + */ +class Shell +{ + const TYPE_UNIX = 1; + const TYPE_DARWIN = 2; + const TYPE_CYGWIN = 3; + const TYPE_WINDOWS = 4; + + /** + * @var string|null + */ + private $type; + + /** + * Returns guessed OS type. + * + * @return int + */ + public function getType() + { + if (null === $this->type) { + $this->type = $this->guessType(); + } + + return $this->type; + } + + /** + * Tests if a command is available. + * + * @param string $command + * + * @return bool + */ + public function testCommand($command) + { + if (self::TYPE_WINDOWS === $this->type) { + // todo: find a way to test if windows command exists + return true; + } + + // todo: find a better way (command could not be available) + exec('command -v '.$command, $output, $code); + + return 0 === $code && count($output) > 0; + } + + /** + * Guesses OS type. + * + * @return int + */ + private function guessType() + { + $os = strtolower(PHP_OS); + + if (false !== strpos($os, 'cygwin')) { + return self::TYPE_CYGWIN; + } + + if (false !== strpos($os, 'darwin')) { + return self::TYPE_DARWIN; + } + + if (0 === strpos($os, 'win')) { + return self::TYPE_WINDOWS; + } + + return self::TYPE_UNIX; + } +} diff --git a/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php b/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php new file mode 100644 index 000000000000..c907d6a8d4d4 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Expression/ExpressionTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests; + +use Symfony\Component\Finder\Expression\Expression; + +class ExpressionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getTypeGuesserData + */ + public function testTypeGuesser($expr, $type) + { + $this->assertEquals($type, Expression::create($expr)->getType()); + } + + /** + * @dataProvider getCaseSensitiveData + */ + public function testCaseSensitive($expr, $isCaseSensitive) + { + $this->assertEquals($isCaseSensitive, Expression::create($expr)->isCaseSensitive()); + } + + /** + * @dataProvider getRegexRenderingData + */ + public function testRegexRendering($expr, $body) + { + $this->assertEquals($body, Expression::create($expr)->renderPattern()); + } + + public function getTypeGuesserData() + { + return array( + array('{foo}', Expression::TYPE_REGEX), + array('/foo/', Expression::TYPE_REGEX), + array('foo', Expression::TYPE_GLOB), + array('foo*', Expression::TYPE_GLOB), + ); + } + + public function getCaseSensitiveData() + { + return array( + array('{foo}m', true), + array('/foo/i', false), + array('foo*', true), + ); + } + + public function getRegexRenderingData() + { + return array( + array('{foo}m', 'foo'), + array('/foo/i', 'foo'), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/GlobTest.php b/src/Symfony/Component/Finder/Tests/Expression/GlobTest.php similarity index 77% rename from src/Symfony/Component/Finder/Tests/GlobTest.php rename to src/Symfony/Component/Finder/Tests/Expression/GlobTest.php index 56077a10687c..fbaeb0e237c8 100644 --- a/src/Symfony/Component/Finder/Tests/GlobTest.php +++ b/src/Symfony/Component/Finder/Tests/Expression/GlobTest.php @@ -11,21 +11,21 @@ namespace Symfony\Component\Finder\Tests; -use Symfony\Component\Finder\Glob; +use Symfony\Component\Finder\Expression\Expression; class GlobTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider getToRegexData */ - public function testToRegex($glob, $match, $noMatch) + public function testGlobToRegex($glob, $match, $noMatch) { foreach ($match as $m) { - $this->assertRegExp(Glob::toRegex($glob), $m, '::toRegex() converts a glob to a regexp'); + $this->assertRegExp(Expression::create($glob)->getRegex()->render(), $m, '::toRegex() converts a glob to a regexp'); } foreach ($noMatch as $m) { - $this->assertNotRegExp(Glob::toRegex($glob), $m, '::toRegex() converts a glob to a regexp'); + $this->assertNotRegExp(Expression::create($glob)->getRegex()->render(), $m, '::toRegex() converts a glob to a regexp'); } } diff --git a/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php b/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php new file mode 100644 index 000000000000..f252696a7625 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Expression/RegexTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests; + +use Symfony\Component\Finder\Expression\Expression; + +class RegexTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getHasFlagsData + */ + public function testHasFlags($regex, $start, $end) + { + $expr = new Expression($regex); + + $this->assertEquals($start, $expr->getRegex()->hasStartFlag()); + $this->assertEquals($end, $expr->getRegex()->hasEndFlag()); + } + + /** + * @dataProvider getHasJokersData + */ + public function testHasJokers($regex, $start, $end) + { + $expr = new Expression($regex); + + $this->assertEquals($start, $expr->getRegex()->hasStartJoker()); + $this->assertEquals($end, $expr->getRegex()->hasEndJoker()); + } + + /** + * @dataProvider getSetFlagsData + */ + public function testSetFlags($regex, $start, $end, $expected) + { + $expr = new Expression($regex); + $expr->getRegex()->setStartFlag($start)->setEndFlag($end); + + $this->assertEquals($expected, $expr->render()); + } + + /** + * @dataProvider getSetJokersData + */ + public function testSetJokers($regex, $start, $end, $expected) + { + $expr = new Expression($regex); + $expr->getRegex()->setStartJoker($start)->setEndJoker($end); + + $this->assertEquals($expected, $expr->render()); + } + + public function testOptions() + { + $expr = new Expression('~abc~is'); + $expr->getRegex()->removeOption('i')->addOption('m'); + + $this->assertEquals('~abc~sm', $expr->render()); + } + + public function testMixFlagsAndJokers() + { + $expr = new Expression('~^.*abc.*$~is'); + + $expr->getRegex()->setStartFlag(false)->setEndFlag(false)->setStartJoker(false)->setEndJoker(false); + $this->assertEquals('~abc~is', $expr->render()); + + $expr->getRegex()->setStartFlag(true)->setEndFlag(true)->setStartJoker(true)->setEndJoker(true); + $this->assertEquals('~^.*abc.*$~is', $expr->render()); + } + + /** + * @dataProvider getReplaceJokersTestData + */ + public function testReplaceJokers($regex, $expected) + { + $expr = new Expression($regex); + $expr = $expr->getRegex()->replaceJokers('@'); + + $this->assertEquals($expected, $expr->renderPattern()); + } + + public function getHasFlagsData() + { + return array( + array('~^abc~', true, false), + array('~abc$~', false, true), + array('~abc~', false, false), + array('~^abc$~', true, true), + array('~^abc\\$~', true, false), + ); + } + + public function getHasJokersData() + { + return array( + array('~.*abc~', true, false), + array('~abc.*~', false, true), + array('~abc~', false, false), + array('~.*abc.*~', true, true), + array('~.*abc\\.*~', true, false), + ); + } + + public function getSetFlagsData() + { + return array( + array('~abc~', true, false, '~^abc~'), + array('~abc~', false, true, '~abc$~'), + array('~abc~', false, false, '~abc~'), + array('~abc~', true, true, '~^abc$~'), + ); + } + + public function getSetJokersData() + { + return array( + array('~abc~', true, false, '~.*abc~'), + array('~abc~', false, true, '~abc.*~'), + array('~abc~', false, false, '~abc~'), + array('~abc~', true, true, '~.*abc.*~'), + ); + } + + public function getReplaceJokersTestData() + { + return array( + array('~.abc~', '@abc'), + array('~\\.abc~', '\\.abc'), + array('~\\\\.abc~', '\\\\@abc'), + array('~\\\\\\.abc~', '\\\\\\.abc'), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php new file mode 100644 index 000000000000..26361f803612 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/DummyAdapter.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; +use Symfony\Component\Finder\Exception\AdapterFailureException; + +/** + * @author Jean-François Simon + */ +class DummyAdapter extends AbstractAdapter +{ + /** + * @var \Iterator + */ + private $iterator; + + /** + * @param \Iterator $iterator + */ + public function __construct(\Iterator $iterator) + { + $this->iterator = $iterator; + } + + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return $this->iterator; + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'yes'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php new file mode 100644 index 000000000000..00fc022fdf76 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/FailingAdapter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; +use Symfony\Component\Finder\Exception\AdapterFailureException; + +/** + * @author Jean-François Simon + */ +class FailingAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + throw new AdapterFailureException($this); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return 'failing'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php new file mode 100644 index 000000000000..2321195e94b0 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/NamedAdapter.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; + +/** + * @author Jean-François Simon + */ +class NamedAdapter extends AbstractAdapter +{ + /** + * @var string + */ + private $name; + + /** + * @param string $name + */ + public function __construct($name) + { + $this->name = $name; + } + + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return new \ArrayIterator(array()); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return true; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return $this->name; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php b/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php new file mode 100644 index 000000000000..d02a13e0328b --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/FakeAdapter/UnsupportedAdapter.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\FakeAdapter; + +use Symfony\Component\Finder\Adapter\AbstractAdapter; + +/** + * @author Jean-François Simon + */ +class UnsupportedAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + return new \ArrayIterator(array()); + } + + /** + * {@inheritdoc} + */ + public function isSupported() + { + return false; + } + + /** + * {@inheritdoc} + */ + function getName() + { + return 'unsupported'; + } +} diff --git a/src/Symfony/Component/Finder/Tests/FinderTest.php b/src/Symfony/Component/Finder/Tests/FinderTest.php index ec7db1ec0684..1baf457f7db6 100644 --- a/src/Symfony/Component/Finder/Tests/FinderTest.php +++ b/src/Symfony/Component/Finder/Tests/FinderTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Finder\Tests; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\Adapter; +use Symfony\Component\Finder\Tests\FakeAdapter; class FinderTest extends Iterator\RealIteratorTestCase { @@ -24,80 +26,106 @@ public static function setUpBeforeClass() self::$tmpDir = sys_get_temp_dir().'/symfony2_finder'; } - public function testCreate() + /** + * @dataProvider getAdaptersTestData + */ + public function testCreate($adapter) { $this->assertInstanceOf('Symfony\Component\Finder\Finder', Finder::create()); } - public function testDirectories() + /** + * @dataProvider getAdaptersTestData + */ + public function testDirectories($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->directories()); $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->directories(); $finder->files(); $finder->directories(); $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFiles() + /** + * @dataProvider getAdaptersTestData + */ + public function testFiles($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files(); $finder->directories(); $finder->files(); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testDepth() + /** + * @dataProvider getAdaptersTestData + */ + public function testDepth($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('< 1')); $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('<= 0')); $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->depth('>= 1')); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->depth('< 1')->depth('>= 1'); $this->assertIterator(array(), $finder->in(self::$tmpDir)->getIterator()); } - public function testName() + /** + * @dataProvider getAdaptersTestData + */ + public function testName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->name('*.php')); $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->name('test.ph*'); $finder->name('test.py'); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); + + $finder = $this->buildFinder($adapter); + $finder->name('~^test~i'); + $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); + + $finder = $this->buildFinder($adapter); + $finder->name('~\\.php$~i'); + $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testNotName() + /** + * @dataProvider getAdaptersTestData + */ + public function testNotName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->notName('*.php')); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->notName('*.php'); $finder->notName('*.py'); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->name('test.ph*'); $finder->name('test.py'); $finder->notName('*.php'); @@ -105,46 +133,72 @@ public function testNotName() $this->assertIterator(array(), $finder->in(self::$tmpDir)->getIterator()); } - public function testSize() + /** + * @dataProvider getRegexNameTestData + * + * @group regexName + */ + public function testRegexName($adapter, $regex) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); + $finder->name($regex); + $this->assertIterator($this->toAbsolute(array('test.py', 'test.php')), $finder->in(self::$tmpDir)->getIterator()); + } + + /** + * @dataProvider getAdaptersTestData + */ + public function testSize($adapter) + { + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()->size('< 1K')->size('> 500')); $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testDate() + /** + * @dataProvider getAdaptersTestData + */ + public function testDate($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->files()->date('until last month')); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php')), $finder->in(self::$tmpDir)->getIterator()); } - public function testExclude() + /** + * @dataProvider getAdaptersTestData + */ + public function testExclude($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->exclude('foo')); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testIgnoreVCS() + /** + * @dataProvider getAdaptersTestData + */ + public function testIgnoreVCS($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreVCS(false)->ignoreDotFiles(false)); $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->ignoreVCS(false)->ignoreVCS(false)->ignoreDotFiles(false); $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreVCS(true)->ignoreDotFiles(false)); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar')), $finder->in(self::$tmpDir)->getIterator()); - } - public function testIgnoreDotFiles() + /** + * @dataProvider getAdaptersTestData + */ + public function testIgnoreDotFiles($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreDotFiles(false)->ignoreVCS(false)); $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); @@ -152,75 +206,102 @@ public function testIgnoreDotFiles() $finder->ignoreDotFiles(false)->ignoreDotFiles(false)->ignoreVCS(false); $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->ignoreDotFiles(true)->ignoreVCS(false)); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByName() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByName($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByName()); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByType() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByType($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByType()); $this->assertIterator($this->toAbsolute(array('foo', 'toto', 'foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByAccessedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByAccessedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByAccessedTime()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByChangedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByChangedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByChangedTime()); $this->assertIterator($this->toAbsolute(array('toto', 'test.py', 'test.php', 'foo/bar.tmp', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSortByModifiedTime() + /** + * @dataProvider getAdaptersTestData + */ + public function testSortByModifiedTime($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sortByModifiedTime()); $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo')), $finder->in(self::$tmpDir)->getIterator()); } - public function testSort() + /** + * @dataProvider getAdaptersTestData + */ + public function testSort($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { return strcmp($a->getRealpath(), $b->getRealpath()); })); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFilter() + /** + * @dataProvider getAdaptersTestData + */ + public function testFilter($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->filter(function (\SplFileInfo $f) { return preg_match('/test/', $f) > 0; })); $this->assertIterator($this->toAbsolute(array('test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); } - public function testFollowLinks() + /** + * @dataProvider getAdaptersTestData + */ + public function testFollowLinks($adapter) { if ('\\' == DIRECTORY_SEPARATOR) { return; } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertSame($finder, $finder->followLinks()); $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } - public function testIn() + /** + * @dataProvider getAdaptersTestData + */ + public function testIn($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); try { $finder->in('foobar'); $this->fail('->in() throws a \InvalidArgumentException if the directory does not exist'); @@ -228,15 +309,18 @@ public function testIn() $this->assertInstanceOf('InvalidArgumentException', $e, '->in() throws a \InvalidArgumentException if the directory does not exist'); } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $iterator = $finder->files()->name('*.php')->depth('< 1')->in(array(self::$tmpDir, __DIR__))->getIterator(); - $this->assertIterator(array(self::$tmpDir.DIRECTORY_SEPARATOR.'test.php', __DIR__.DIRECTORY_SEPARATOR.'FinderTest.php', __DIR__.DIRECTORY_SEPARATOR.'bootstrap.php', __DIR__.DIRECTORY_SEPARATOR.'GlobTest.php'), $iterator); + $this->assertIterator(array(self::$tmpDir.DIRECTORY_SEPARATOR.'test.php', __DIR__.DIRECTORY_SEPARATOR.'FinderTest.php', __DIR__.DIRECTORY_SEPARATOR.'bootstrap.php'), $iterator); } - public function testGetIterator() + /** + * @dataProvider getAdaptersTestData + */ + public function testGetIterator($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); try { $finder->getIterator(); $this->fail('->getIterator() throws a \LogicException if the in() method has not been called'); @@ -244,7 +328,7 @@ public function testGetIterator() $this->assertInstanceOf('LogicException', $e, '->getIterator() throws a \LogicException if the in() method has not been called'); } - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $dirs = array(); foreach ($finder->directories()->in(self::$tmpDir) as $dir) { $dirs[] = (string) $dir; @@ -257,19 +341,22 @@ public function testGetIterator() $this->assertEquals($expected, $dirs, 'implements the \IteratorAggregate interface'); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $this->assertEquals(2, iterator_count($finder->directories()->in(self::$tmpDir)), 'implements the \IteratorAggregate interface'); - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $a = iterator_to_array($finder->directories()->in(self::$tmpDir)); $a = array_values(array_map(function ($a) { return (string) $a; }, $a)); sort($a); $this->assertEquals($expected, $a, 'implements the \IteratorAggregate interface'); } - public function testRelativePath() + /** + * @dataProvider getAdaptersTestData + */ + public function testRelativePath($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(self::$tmpDir); @@ -284,12 +371,15 @@ public function testRelativePath() sort($ref); sort($paths); - $this->assertEquals($paths, $ref); + $this->assertEquals($ref, $paths); } - public function testRelativePathname() + /** + * @dataProvider getAdaptersTestData + */ + public function testRelativePathname($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(self::$tmpDir)->sortByName(); @@ -304,15 +394,18 @@ public function testRelativePathname() sort($paths); sort($ref); - $this->assertEquals($paths, $ref); + $this->assertEquals($ref, $paths); } - public function testAppendWithAFinder() + /** + * @dataProvider getAdaptersTestData + */ + public function testAppendWithAFinder($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files()->in(self::$tmpDir.DIRECTORY_SEPARATOR.'foo'); - $finder1 = new Finder(); + $finder1 = $this->buildFinder($adapter); $finder1->directories()->in(self::$tmpDir); $finder->append($finder1); @@ -320,9 +413,12 @@ public function testAppendWithAFinder() $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto')), $finder->getIterator()); } - public function testAppendWithAnArray() + /** + * @dataProvider getAdaptersTestData + */ + public function testAppendWithAnArray($adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->files()->in(self::$tmpDir.DIRECTORY_SEPARATOR.'foo'); $finder->append($this->toAbsolute(array('foo', 'toto'))); @@ -391,10 +487,11 @@ protected function toAbsoluteFixtures($files) /** * @dataProvider getContainsTestData + * @group grep */ - public function testContains($matchPatterns, $noMatchPatterns, $expected) + public function testContains($adapter, $matchPatterns, $noMatchPatterns, $expected) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__.DIRECTORY_SEPARATOR.'Fixtures') ->name('*.txt')->sortByName() ->contains($matchPatterns) @@ -403,26 +500,12 @@ public function testContains($matchPatterns, $noMatchPatterns, $expected) $this->assertIterator($this->toAbsoluteFixtures($expected), $finder); } - public function getContainsTestData() - { - return array( - array('', '', array()), - array('foo', 'bar', array()), - array('', 'foobar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), - array('lorem ipsum dolor sit amet', 'foobar', array('lorem.txt')), - array('sit', 'bar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), - array('dolor sit amet', '@^L@m', array('dolor.txt', 'ipsum.txt')), - array('/^lorem ipsum dolor sit amet$/m', 'foobar', array('lorem.txt')), - array('lorem', 'foobar', array('lorem.txt')), - - array('', 'lorem', array('dolor.txt', 'ipsum.txt')), - array('ipsum dolor sit amet', '/^IPSUM/m', array('lorem.txt')), - ); - } - - public function testContainsOnDirectory() + /** + * @dataProvider getAdaptersTestData + */ + public function testContainsOnDirectory(Adapter\AdapterInterface $adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__) ->directories() ->name('Fixtures') @@ -430,9 +513,12 @@ public function testContainsOnDirectory() $this->assertIterator(array(), $finder); } - public function testNotContainsOnDirectory() + /** + * @dataProvider getAdaptersTestData + */ + public function testNotContainsOnDirectory(Adapter\AdapterInterface $adapter) { - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in(__DIR__) ->directories() ->name('Fixtures') @@ -445,8 +531,10 @@ public function testNotContainsOnDirectory() * with inner FilesystemIterator in an ivalid state. * * @see https://bugs.php.net/bug.php?id=49104 + * + * @dataProvider getAdaptersTestData */ - public function testMultipleLocations() + public function testMultipleLocations(Adapter\AdapterInterface $adapter) { $locations = array( self::$tmpDir.'/', @@ -454,9 +542,108 @@ public function testMultipleLocations() ); // it is expected that there are test.py test.php in the tmpDir - $finder = new Finder(); + $finder = $this->buildFinder($adapter); $finder->in($locations)->depth('< 1')->name('test.php'); $this->assertEquals(1, count($finder)); } + + public function testAdaptersOrdering() + { + $finder = Finder::create() + ->removeAdapters() + ->register(new FakeAdapter\NamedAdapter('a'), 0) + ->register(new FakeAdapter\NamedAdapter('b'), -50) + ->register(new FakeAdapter\NamedAdapter('c'), 50) + ->register(new FakeAdapter\NamedAdapter('d'), -25) + ->register(new FakeAdapter\NamedAdapter('e'), 25); + + $this->assertEquals( + array('c', 'e', 'a', 'd', 'b'), + array_map(function(Adapter\AdapterInterface $adapter) { + return $adapter->getName(); + }, $finder->getAdapters()) + ); + } + + public function testAdaptersChaining() + { + $iterator = new \ArrayIterator(array()); + $filenames = $this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')); + foreach ($filenames as $file) { + $iterator->append(new \Symfony\Component\Finder\SplFileInfo($file, null, null)); + } + + $finder = Finder::create() + ->removeAdapters() + ->register(new FakeAdapter\UnsupportedAdapter(), 3) + ->register(new FakeAdapter\FailingAdapter(), 2) + ->register(new FakeAdapter\DummyAdapter($iterator), 1); + + $this->assertIterator($filenames, $finder->in(sys_get_temp_dir())->getIterator()); + } + + public function getAdaptersTestData() + { + return array_map( + function ($adapter) { return array($adapter); }, + $this->getValidAdapters() + ); + } + + public function getContainsTestData() + { + $tests = array( + array('', '', array()), + array('foo', 'bar', array()), + array('', 'foobar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), + array('lorem ipsum dolor sit amet', 'foobar', array('lorem.txt')), + array('sit', 'bar', array('dolor.txt', 'ipsum.txt', 'lorem.txt')), + array('dolor sit amet', '@^L@m', array('dolor.txt', 'ipsum.txt')), + array('/^lorem ipsum dolor sit amet$/m', 'foobar', array('lorem.txt')), + array('lorem', 'foobar', array('lorem.txt')), + + array('', 'lorem', array('dolor.txt', 'ipsum.txt')), + array('ipsum dolor sit amet', '/^IPSUM/m', array('lorem.txt')), + ); + + return $this->buildTestData($tests); + } + + public function getRegexNameTestData() + { + $tests = array( + array('~.+\\.p.+~i'), + array('~t.*s~i'), + ); + + return $this->buildTestData($tests); + } + + private function buildFinder(Adapter\AdapterInterface $adapter) + { + return Finder::create() + ->removeAdapters() + ->register($adapter); + } + + private function getValidAdapters() + { + return array_filter( + array(new Adapter\GnuFindAdapter(), new Adapter\PhpAdapter()), + function (Adapter\AdapterInterface $adapter) { return $adapter->isSupported(); } + ); + } + + private function buildTestData(array $tests) + { + $data = array(); + foreach ($this->getValidAdapters() as $adapter) { + foreach ($tests as $test) { + $data[] = array_merge(array($adapter), $test); + } + } + + return $data; + } } diff --git a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php index 43ad4599496c..68c81e1168a2 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeIteratorTest.php @@ -19,11 +19,11 @@ class DepthRangeFilterIteratorTest extends RealIteratorTestCase /** * @dataProvider getAcceptData */ - public function testAccept($size, $expected) + public function testAccept($minDepth, $maxDepth, $expected) { $inner = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->getAbsolutePath(''), \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); - $iterator = new DepthRangeFilterIterator($inner, $size); + $iterator = new DepthRangeFilterIterator($inner, $minDepth, $maxDepth); $actual = array_keys(iterator_to_array($iterator)); sort($expected); @@ -34,11 +34,11 @@ public function testAccept($size, $expected) public function getAcceptData() { return array( - array(array(new NumberComparator('< 1')), array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.bar'))), - array(array(new NumberComparator('<= 1')), array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.foo/.bar'), $this->getAbsolutePath('/.bar'))), - array(array(new NumberComparator('> 1')), array()), - array(array(new NumberComparator('>= 1')), array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), - array(array(new NumberComparator('1')), array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), + array(0, 0, array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.bar'))), + array(0, 1, array($this->getAbsolutePath('/.git'), $this->getAbsolutePath('/test.py'), $this->getAbsolutePath('/foo'), $this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/test.php'), $this->getAbsolutePath('/toto'), $this->getAbsolutePath('/.foo'), $this->getAbsolutePath('/.foo/.bar'), $this->getAbsolutePath('/.bar'))), + array(2, INF, array()), + array(1, INF, array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), + array(1, 1, array($this->getAbsolutePath('/foo/bar.tmp'), $this->getAbsolutePath('/.foo/.bar'))), ); } diff --git a/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php new file mode 100644 index 000000000000..c60a90656a55 --- /dev/null +++ b/src/Symfony/Component/Finder/Tests/Iterator/FilePathsIteratorTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Tests\Iterator; + +use Symfony\Component\Finder\Iterator\FilePathsIterator; + +class FilePathsIteratorTest extends RealIteratorTestCase +{ + /** + * @dataProvider getSubPathData + */ + public function testSubPath($baseDir, array $paths, array $subPaths, array $subPathnames) + { + $iterator = new FilePathsIterator($paths, $baseDir); + + foreach ($iterator as $index => $file) { + $this->assertEquals($paths[$index], $file->getPathname()); + $this->assertEquals($subPaths[$index], $iterator->getSubPath()); + $this->assertEquals($subPathnames[$index], $iterator->getSubPathname()); + } + } + + public function getSubPathData() + { + $tmpDir = sys_get_temp_dir().'/symfony2_finder'; + + return array( + array( + $tmpDir, + array( // paths + $tmpDir.DIRECTORY_SEPARATOR.'.git', + $tmpDir.DIRECTORY_SEPARATOR.'test.py', + $tmpDir.DIRECTORY_SEPARATOR.'foo', + $tmpDir.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'bar.tmp', + $tmpDir.DIRECTORY_SEPARATOR.'test.php', + $tmpDir.DIRECTORY_SEPARATOR.'toto' + ), + array( // subPaths + '', + '', + '', + 'foo', + '', + '' + ), + array( // subPathnames + '.git', + 'test.py', + 'foo', + 'foo'.DIRECTORY_SEPARATOR.'bar.tmp', + 'test.php', + 'toto' + ), + ), + ); + } +} diff --git a/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php b/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php index 8810ce757d25..226469b4e7a9 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/IteratorTestCase.php @@ -15,7 +15,9 @@ abstract class IteratorTestCase extends \PHPUnit_Framework_TestCase { protected function assertIterator($expected, \Traversable $iterator) { - $values = array_map(function (\SplFileInfo $fileinfo) { return $fileinfo->getPathname(); }, iterator_to_array($iterator)); + // set iterator_to_array $use_key to false to avoid values merge + // this made FinderTest::testAppendWithAnArray() failed with GnuFinderAdapter + $values = array_map(function (\SplFileInfo $fileinfo) { return $fileinfo->getPathname(); }, iterator_to_array($iterator, false)); sort($values); sort($expected); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy