diff --git a/src/Symfony/Component/Config/Resource/DirectoryResource.php b/src/Symfony/Component/Config/Resource/DirectoryResource.php index 5ccd204ef9334..8530400f7facf 100644 --- a/src/Symfony/Component/Config/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Config/Resource/DirectoryResource.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -class DirectoryResource implements ResourceInterface, \Serializable +class DirectoryResource implements ResourceInterface { private $resource; private $pattern; @@ -33,6 +33,73 @@ public function __construct($resource, $pattern = null) $this->pattern = $pattern; } + /** + * Returns the list of filtered file and directory childs of directory resource. + * + * @return array An array of files + */ + public function getFilteredChilds() + { + $childs = array(); + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) { + // if regex filtering is enabled only return matching files + if ($file->isFile() && !$this->hasFile($file)) { + continue; + } + + // always monitor directories for changes, except the .. entries + // (otherwise deleted files wouldn't get detected) + if ($file->isDir() && '/..' === substr($file, -3)) { + continue; + } + + $childs[] = $file; + } + + return $childs; + } + + /** + * Returns child resources that matches directory filters. + * + * @return array + */ + public function getFilteredResources() + { + if (!$this->exists()) { + return array(); + } + + $iterator = new \DirectoryIterator($this->resource); + + $resources = array(); + foreach ($iterator as $file) { + // if regex filtering is enabled only return matching files + if ($file->isFile() && !$this->hasFile($file)) { + continue; + } + + // always monitor directories for changes, except the .. entries + // (otherwise deleted files wouldn't get detected) + if ($file->isDir() && '/..' === substr($file, -3)) { + continue; + } + + // if file is dot - continue + if ($file->isDot()) { + continue; + } + + if ($file->isFile()) { + $resources[] = new FileResource($file->getRealPath()); + } elseif ($file->isDir()) { + $resources[] = new DirectoryResource($file->getRealPath()); + } + } + + return $resources; + } + /** * Returns a string representation of the Resource. * @@ -53,11 +120,62 @@ public function getResource() return $this->resource; } + /** + * Returns check pattern. + * + * @return mixed + */ public function getPattern() { return $this->pattern; } + /** + * Checks that passed file exists in resource and matches resource filters. + * + * @param SplFileInfo|string $file + * + * @return Boolean + */ + public function hasFile($file) + { + if (!$file instanceof \SplFileInfo) { + $file = new \SplFileInfo($file); + } + + if (0 !== strpos($file->getRealPath(), realpath($this->resource))) { + return false; + } + + if ($this->pattern) { + return (bool) preg_match($this->pattern, $file->getBasename()); + } + + return true; + } + + /** + * Returns resource mtime. + * + * @return integer + */ + public function getModificationTime() + { + if (!$this->exists()) { + return -1; + } + + clearstatcache(true, $this->resource); + $newestMTime = filemtime($this->resource); + + foreach ($this->getFilteredChilds() as $file) { + clearstatcache(true, (string) $file); + $newestMTime = max($file->getMTime(), $newestMTime); + } + + return $newestMTime; + } + /** * Returns true if the resource has not been updated since the given timestamp. * @@ -67,27 +185,31 @@ public function getPattern() */ public function isFresh($timestamp) { - if (!is_dir($this->resource)) { + if (!$this->exists()) { return false; } - $newestMTime = filemtime($this->resource); - foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) { - // if regex filtering is enabled only check matching files - if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) { - continue; - } - - // always monitor directories for changes, except the .. entries - // (otherwise deleted files wouldn't get detected) - if ($file->isDir() && '/..' === substr($file, -3)) { - continue; - } + return $this->getModificationTime() < $timestamp; + } - $newestMTime = max($file->getMTime(), $newestMTime); - } + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + public function exists() + { + return is_dir($this->resource); + } - return $newestMTime < $timestamp; + /** + * Returns unique resource ID. + * + * @return string + */ + public function getId() + { + return md5($this->resource.$this->pattern); } public function serialize() diff --git a/src/Symfony/Component/Config/Resource/FileResource.php b/src/Symfony/Component/Config/Resource/FileResource.php index 619f84bcef2ec..0931112f4debc 100644 --- a/src/Symfony/Component/Config/Resource/FileResource.php +++ b/src/Symfony/Component/Config/Resource/FileResource.php @@ -18,7 +18,7 @@ * * @author Fabien Potencier */ -class FileResource implements ResourceInterface, \Serializable +class FileResource implements ResourceInterface { private $resource; @@ -29,7 +29,7 @@ class FileResource implements ResourceInterface, \Serializable */ public function __construct($resource) { - $this->resource = realpath($resource); + $this->resource = file_exists($resource) ? realpath($resource) : $resource; } /** @@ -52,6 +52,22 @@ public function getResource() return $this->resource; } + /** + * Returns resource mtime. + * + * @return integer + */ + public function getModificationTime() + { + if (!$this->exists()) { + return -1; + } + + clearstatcache(true, $this->resource); + + return filemtime($this->resource); + } + /** * Returns true if the resource has not been updated since the given timestamp. * @@ -61,11 +77,31 @@ public function getResource() */ public function isFresh($timestamp) { - if (!file_exists($this->resource)) { + if (!$this->exists()) { return false; } - return filemtime($this->resource) < $timestamp; + return $this->getModificationTime() <= $timestamp; + } + + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + public function exists() + { + return file_exists($this->resource); + } + + /** + * Returns unique resource ID. + * + * @return string + */ + public function getId() + { + return md5($this->resource); } public function serialize() diff --git a/src/Symfony/Component/Config/Resource/ResourceInterface.php b/src/Symfony/Component/Config/Resource/ResourceInterface.php index 024f2e95f95fc..7febbd81394ab 100644 --- a/src/Symfony/Component/Config/Resource/ResourceInterface.php +++ b/src/Symfony/Component/Config/Resource/ResourceInterface.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -interface ResourceInterface +interface ResourceInterface extends \Serializable { /** * Returns a string representation of the Resource. @@ -34,10 +34,31 @@ function __toString(); */ function isFresh($timestamp); + /** + * Returns resource mtime. + * + * @return integer + */ + function getModificationTime(); + + /** + * Returns true if the resource exists in the filesystem. + * + * @return Boolean + */ + function exists(); + /** * Returns the resource tied to this Resource. * * @return mixed The resource */ function getResource(); + + /** + * Returns unique resource ID. + * + * @return string + */ + function getId(); } diff --git a/src/Symfony/Component/ResourceWatcher/Event/Event.php b/src/Symfony/Component/ResourceWatcher/Event/Event.php new file mode 100644 index 0000000000000..0c1a1ebf9616c --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Event/Event.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Event; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Resource change event. + * + * @author Konstantin Kudryashov + */ +class Event +{ + const CREATED = 1; + const MODIFIED = 2; + const DELETED = 4; + const ALL = 7; + + private $trackingId; + private $resource; + private $type; + private $time; + + /** + * Initializes resource event. + * + * @param mixed $trackingId id of resource inside tracker + * @param ResourceInterface $resource resource instance + * @param integer $type event type bit + */ + public function __construct($trackingId, ResourceInterface $resource, $type) + { + $this->trackingId = $trackingId; + $this->resource = $resource; + $this->type = $type; + $this->time = time(); + } + + /** + * Returns id of resource inside tracker. + * + * @return integer + */ + public function getTrackingId() + { + return $this->trackingId; + } + + /** + * Returns changed resource. + * + * @return ResourceInterface + */ + public function getResource() + { + return $this->resource; + } + + /** + * Returns event type. + * + * @return integer + */ + public function getType() + { + return $this->type; + } + + /** + * Returns time on which event occured. + * + * @return integer + */ + public function getTime() + { + return $this->time; + } +} diff --git a/src/Symfony/Component/ResourceWatcher/Event/EventListener.php b/src/Symfony/Component/ResourceWatcher/Event/EventListener.php new file mode 100644 index 0000000000000..8589bf85c523a --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Event/EventListener.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Event; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException; + +/** + * Resource change listener. + * + * @author Konstantin Kudryashov + */ +class EventListener implements EventListenerInterface +{ + private $resource; + private $callback; + private $eventsMask; + + /** + * Initializes listener. + * + * @param ResourceInterface $resource resource to listen + * @param callable $callback callback to call on event + * @param integer $eventsMask event types to listen + */ + public function __construct(ResourceInterface $resource, $callback, $eventsMask) + { + if (!is_callable($callback)) { + throw new InvalidArgumentException( + 'EventListener\'s second argument should be callable.' + ); + } + + $this->resource = $resource; + $this->callback = $callback; + $this->eventsMask = $eventsMask; + } + + /** + * Returns listening resource. + * + * @return ResourceInterface + */ + public function getResource() + { + return $this->resource; + } + + /** + * Returns callback. + * + * @return callable + */ + public function getCallback() + { + return $this->callback; + } + + /** + * Checks whether listener supports provided resource event. + * + * @param Event $event + */ + public function supports(Event $event) + { + return 0 !== ($this->eventsMask & $event->getType()); + } +} diff --git a/src/Symfony/Component/ResourceWatcher/Event/EventListenerInterface.php b/src/Symfony/Component/ResourceWatcher/Event/EventListenerInterface.php new file mode 100644 index 0000000000000..ff4515dae4383 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Event/EventListenerInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Event; + +/** + * Resource change listener interface. + * + * @author Konstantin Kudryashov + */ +interface EventListenerInterface +{ + /** + * Returns listening resource. + * + * @return ResourceInterface + */ + function getResource(); + + /** + * Returns callback. + * + * @return callable + */ + function getCallback(); + + /** + * Checks whether listener supports provided resource event. + * + * @param Event $event + */ + function supports(Event $event); +} diff --git a/src/Symfony/Component/ResourceWatcher/Exception/ExceptionInterface.php b/src/Symfony/Component/ResourceWatcher/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..620cc434feaa4 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ResourceWatcher/Exception/InvalidArgumentException.php b/src/Symfony/Component/ResourceWatcher/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..8e4575e8c02c1 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Exception/InvalidArgumentException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Exception; + +use \InvalidArgumentException as BaseInvalidArgumentException; + +/** + * InvalidArgumentException + * + * @author Fabien Potencier + */ +class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ResourceWatcher/Exception/RuntimeException.php b/src/Symfony/Component/ResourceWatcher/Exception/RuntimeException.php new file mode 100644 index 0000000000000..f464350fc2bc6 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Exception/RuntimeException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Exception; + +use \RuntimeException as BaseRuntimeException; + +/** + * RuntimeException + * + * @author Fabien Potencier + */ +class RuntimeException extends BaseRuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ResourceWatcher/ResourceWatcher.php b/src/Symfony/Component/ResourceWatcher/ResourceWatcher.php new file mode 100644 index 0000000000000..32492ecb78daf --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/ResourceWatcher.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\ResourceWatcher\Event\EventListener; +use Symfony\Component\ResourceWatcher\Event\EventListenerInterface; +use Symfony\Component\ResourceWatcher\Tracker\TrackerInterface; +use Symfony\Component\ResourceWatcher\Tracker\InotifyTracker; +use Symfony\Component\ResourceWatcher\Tracker\RecursiveIteratorTracker; +use Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException; + +/** + * Resources changes watcher. + * + * @author Konstantin Kudryashov + */ +class ResourceWatcher +{ + private $tracker; + private $watching = true; + private $listeners = array(); + + /** + * Initializes path watcher. + * + * @param TrackerInterface $tracker + */ + public function __construct(TrackerInterface $tracker = null) + { + if (null !== $tracker) { + $this->tracker = $tracker; + } elseif (function_exists('inotify_init')) { + $this->tracker = new InotifyTracker(); + } else { + $this->tracker = new RecursiveIteratorTracker(); + } + } + + /** + * Returns current tracker instance. + * + * @return TrackerInterface + */ + public function getTracker() + { + return $this->tracker; + } + + /** + * Track resource with watcher. + * + * @param ResourceInterface|string $resource resource to track + * @param callable $callback event callback + * @param integer $eventsMask event types bitmask + */ + public function track($resource, $callback, $eventsMask = Event::ALL) + { + if (!$resource instanceof ResourceInterface) { + if (is_file($resource)) { + $resource = new FileResource($resource); + } elseif (is_dir($resource)) { + $resource = new DirectoryResource($resource); + } else { + throw new InvalidArgumentException(sprintf( + 'First argument to track() should be either file or directory resource, but got "%s"', $resource + )); + } + } + + if (!is_callable($callback)) { + throw new InvalidArgumentException('Second argument to track() should be callable.'); + } + + $this->addListener(new EventListener($resource, $callback, $eventsMask)); + } + + /** + * Adds resource event listener to watcher. + * + * @param EventListenerInterface $listener resource event listener + */ + public function addListener(EventListenerInterface $listener) + { + if (!$this->getTracker()->isResourceTracked($listener->getResource())) { + $this->getTracker()->track($listener->getResource()); + } + + $trackingId = $listener->getResource()->getId(); + + $this->listeners[$trackingId][] = $listener; + } + + /** + * Returns true if watcher is currently watching on tracked resources (started). + * + * @return Boolean + */ + public function isWatching() + { + return $this->watching; + } + + /** + * Starts wathing on tracked resources. + * + * @param integer $checkInterval check interval in microseconds + * @param integer $timeLimit maximum watching time limit in microseconds + */ + public function start($checkInterval = 1000000, $timeLimit = null) + { + $totalTime = 0; + $this->watching = true; + + while ($this->watching) { + usleep($checkInterval); + $totalTime += $checkInterval; + + if (null !== $timeLimit && $totalTime > $timeLimit) { + break; + } + + if (count($events = $this->getTracker()->getEvents())) { + $this->notifyListeners($events); + } + } + } + + /** + * Stop watching on tracked resources. + */ + public function stop() + { + $this->watching = false; + } + + /** + * Notifies all registered resource event listeners about their events. + * + * @param array $events array of resource events + */ + private function notifyListeners(array $events) + { + foreach ($events as $event) { + $trackingId = $event->getTrackingId(); + + if (isset($this->listeners[$trackingId])) { + foreach ($this->listeners[$trackingId] as $listener) { + if ($listener->supports($event)) { + call_user_func($listener->getCallback(), $event); + } + } + } + } + } +} diff --git a/src/Symfony/Component/ResourceWatcher/StateChecker/DirectoryStateChecker.php b/src/Symfony/Component/ResourceWatcher/StateChecker/DirectoryStateChecker.php new file mode 100644 index 0000000000000..4570dd13d9ef9 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/StateChecker/DirectoryStateChecker.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\StateChecker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileResource; + +/** + * Recursive directory state checker. + * + * @author Konstantin Kudryashov + */ +class DirectoryStateChecker extends ResourceStateChecker +{ + private $childs = array(); + + /** + * Initializes checker. + * + * @param DirectoryResource $resource + */ + public function __construct(DirectoryResource $resource) + { + parent::__construct($resource); + + foreach ($this->createDirectoryChildCheckers($resource) as $checker) { + $this->childs[$checker->getResource()->getId()] = $checker; + } + } + + /** + * Check tracked resource for changes. + * + * @return array + */ + public function getChangeset() + { + $changeset = parent::getChangeset(); + if (count($changeset) && Event::MODIFIED === $changeset[0]['event']) { + $changeset = array(); + } + + foreach ($this->childs as $id => $checker) { + foreach ($checker->getChangeset() as $change) { + if (Event::DELETED === $change['event'] && $id === $change['resource']->getId()) { + unset($this->childs[$id]); + } + $changeset[] = $change; + } + } + + if ($this->getResource()->exists()) { + foreach ($this->createDirectoryChildCheckers($this->getResource()) as $checker) { + if (!isset($this->childs[$checker->getResource()->getId()])) { + $this->childs[$checker->getResource()->getId()] = $checker; + $changeset[] = array( + 'event' => Event::CREATED, 'resource' => $checker->getResource() + ); + } + } + } + + return $changeset; + } + + /** + * Reads files and subdirectories on provided resource path and transform them to resources. + * + * @param DirectoryResource $resource + * + * @return array + */ + private function createDirectoryChildCheckers(DirectoryResource $resource) + { + $checkers = array(); + foreach ($resource->getFilteredResources() as $resource) { + if ($resource instanceof DirectoryResource) { + $checkers[] = new DirectoryStateChecker($resource); + } else { + $checkers[] = new FileStateChecker($resource); + } + } + + return $checkers; + } +} diff --git a/src/Symfony/Component/ResourceWatcher/StateChecker/FileStateChecker.php b/src/Symfony/Component/ResourceWatcher/StateChecker/FileStateChecker.php new file mode 100644 index 0000000000000..57140a5816ea4 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/StateChecker/FileStateChecker.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\StateChecker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\FileResource; + +/** + * File state checker. + * + * @author Konstantin Kudryashov + */ +class FileStateChecker extends ResourceStateChecker +{ + /** + * Initializes checker. + * + * @param FileResource $resource + */ + public function __construct(FileResource $resource) + { + parent::__construct($resource); + } +} diff --git a/src/Symfony/Component/ResourceWatcher/StateChecker/ResourceStateChecker.php b/src/Symfony/Component/ResourceWatcher/StateChecker/ResourceStateChecker.php new file mode 100644 index 0000000000000..c51f708577ee8 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/StateChecker/ResourceStateChecker.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\StateChecker; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\ResourceWatcher\Event\Event; + +/** + * Abstract resource state checker class. + * + * @author Konstantin Kudryashov + */ +abstract class ResourceStateChecker implements StateCheckerInterface +{ + private $resource; + private $timestamp; + private $deleted = false; + + /** + * Initializes checker. + * + * @param ResourceInterface $resource + */ + public function __construct(ResourceInterface $resource) + { + $this->resource = $resource; + $this->timestamp = $resource->getModificationTime() + 1; + $this->deleted = !$resource->exists(); + } + + /** + * Returns tracked resource. + * + * @return ResourceInterface + */ + public function getResource() + { + return $this->resource; + } + + /** + * Check tracked resource for changes. + * + * @return array + */ + public function getChangeset() + { + if ($this->deleted) { + if (!$this->resource->exists()) { + return array(); + } + + $this->timestamp = $this->resource->getModificationTime() + 1; + $this->deleted = false; + + return array(array('event' => Event::CREATED, 'resource' => $this->resource)); + } elseif (!$this->resource->exists()) { + $this->deleted = true; + + return array(array('event' => Event::DELETED, 'resource' => $this->resource)); + } elseif (!$this->resource->isFresh($this->timestamp)) { + $this->timestamp = $this->resource->getModificationTime() + 1; + + return array(array('event' => Event::MODIFIED, 'resource' => $this->resource)); + } + + return array(); + } + + /** + * Checks whether resource have been previously deleted. + * + * @return Boolean + */ + protected function isDeleted() + { + return $this->deleted; + } +} diff --git a/src/Symfony/Component/ResourceWatcher/StateChecker/StateCheckerInterface.php b/src/Symfony/Component/ResourceWatcher/StateChecker/StateCheckerInterface.php new file mode 100644 index 0000000000000..54494491c6a2a --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/StateChecker/StateCheckerInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\StateChecker; + +/** + * Resource state checker interface. + * + * @author Konstantin Kudryashov + */ +interface StateCheckerInterface +{ + /** + * Returns tracked resource. + * + * @return ResourceInterface + */ + function getResource(); + + /** + * Check tracked resource for changes. + * + * @return array + */ + function getChangeset(); +} diff --git a/src/Symfony/Component/ResourceWatcher/Tracker/InotifyTracker.php b/src/Symfony/Component/ResourceWatcher/Tracker/InotifyTracker.php new file mode 100644 index 0000000000000..4fb1c9e652846 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Tracker/InotifyTracker.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ResourceWatcher\Tracker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\ResourceWatcher\Exception\RuntimeException; +use Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException; + +/** + * Inotify events resources tracker. + * + * @author Konstantin Kudryashov + */ +class InotifyTracker implements TrackerInterface +{ + private $stream; + private $trackedResources = array(); + private $resourcePaths = array(); + + /** + * Initializes tracker. + */ + public function __construct() + { + if (!function_exists('inotify_init')) { + throw new RuntimeException('You must install inotify to be able to use this tracker.'); + } + + $this->stream = inotify_init(); + stream_set_blocking($this->stream, 0); + } + + /** + * Destructs tracker. + */ + public function __destruct() + { + fclose($this->stream); + } + + /** + * Starts to track provided resource for changes. + * + * @param ResourceInterface $resource + */ + public function track(ResourceInterface $resource) + { + if (!$resource->exists()) { + throw new InvalidArgumentException(sprintf('Unable to track a non-existent resource (%s)', $resource)); + } + + $this->watchResource($resource, $resource, realpath($resource->getResource())); + } + + /** + * Checks whether provided resource is tracked by this tracker. + * + * @param ResourceInterface $resource + * + * @return Boolean + */ + public function isResourceTracked(ResourceInterface $resource) + { + return null !== $this->getTrackingId($resource); + } + + /** + * Checks tracked resources for change events. + * + * @return array change events array + */ + public function getEvents() + { + $events = array(); + if ($iEvents = inotify_read($this->stream)) { + foreach ($iEvents as $iEvent) { + $trackingId = $iEvent['wd']; + $resourcePath = $this->resourcePaths[$trackingId].DIRECTORY_SEPARATOR.$iEvent['name']; + $tracked = $this->trackedResources[$trackingId]; + + if ('' == $iEvent['name']) { + continue; + } elseif (is_dir($resourcePath)) { + $resource = new DirectoryResource($resourcePath); + } elseif (is_file($resourcePath)) { + $resource = new FileResource($resourcePath); + } else { + continue; + } + + if ($resource instanceof FileResource) { + $file = new \SplFileInfo($resourcePath); + if ($tracked instanceof DirectoryResource && !$tracked->hasFile($file)) { + continue; + } + } + + if (IN_CREATE === ($iEvent['mask'] & IN_CREATE)) { + if ($resource instanceof DirectoryResource) { + $this->watchResource($resource, $tracked, $resource->getResource()); + } + $event = Event::CREATED; + } elseif (IN_MODIFY === ($iEvent['mask'] & IN_MODIFY)) { + $event = Event::MODIFIED; + } elseif (IN_DELETE === ($iEvent['mask'] & IN_DELETE)) { + $this->unwatchResource($resource); + $event = Event::DELETED; + } + + $events[] = new Event($tracked->getId(), $resource, $event); + } + } + + return $events; + } + + private function watchResource(ResourceInterface $resource, ResourceInterface $parent, $path) + { + $trackingId = inotify_add_watch( + $this->stream, $resource->getResource(), IN_CREATE | IN_MODIFY | IN_DELETE + ); + + $this->trackedResources[$trackingId] = $parent; + + if (!is_dir($path)) { + $path = dirname($path); + } + $this->resourcePaths[$trackingId] = rtrim($path, DIRECTORY_SEPARATOR); + + if ($resource instanceof DirectoryResource) { + foreach ($resource->getFilteredResources() as $child) { + if ($child instanceof DirectoryResource) { + $this->watchResource($child, $parent, realpath($child->getResource())); + } + } + } + } + + private function unwatchResource(ResourceInterface $resource) + { + if ($id = $this->getTrackingId($resource)) { + inotify_rm_watch($this->stream, $id); + unset($this->resourcePaths[$id]); + unset($this->trackedResources[$id]); + } + } + + private function getTrackingId(ResourceInterface $resource) + { + foreach ($this->trackedResources as $trackingId => $trackedResource) { + if ($trackedResource->getId() === $resource->getId()) { + return $trackingId; + } + } + } +} diff --git a/src/Symfony/Component/ResourceWatcher/Tracker/RecursiveIteratorTracker.php b/src/Symfony/Component/ResourceWatcher/Tracker/RecursiveIteratorTracker.php new file mode 100644 index 0000000000000..08014eaae9007 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Tracker/RecursiveIteratorTracker.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\ResourceWatcher\Tracker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\ResourceWatcher\StateChecker\DirectoryStateChecker; +use Symfony\Component\ResourceWatcher\StateChecker\FileStateChecker; +use Symfony\Component\ResourceWatcher\StateChecker\StateCheckerInterface; +use Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException; + +/** + * Recursive iterator resources tracker. + * + * @author Konstantin Kudryashov + */ +class RecursiveIteratorTracker implements TrackerInterface +{ + private $checkers = array(); + + /** + * Starts to track provided resource for changes. + * + * @param ResourceInterface $resource + */ + public function track(ResourceInterface $resource) + { + if (!$resource->exists()) { + throw new InvalidArgumentException(sprintf('Unable to track a non-existent resource (%s)', $resource)); + } + + $checker = $resource instanceof DirectoryResource ? new DirectoryStateChecker($resource) : new FileStateChecker($resource); + + $this->addResourceStateChecker($checker); + } + + /** + * Adds resource state checker. + * + * @param StateCheckerInterface $checker + */ + public function addResourceStateChecker(StateCheckerInterface $checker) + { + $this->checkers[$checker->getResource()->getId()] = $checker; + } + + /** + * Checks whether provided resource is tracked by this tracker. + * + * @param ResourceInterface $resource + * + * @return Boolean + */ + public function isResourceTracked(ResourceInterface $resource) + { + return isset($this->checkers[$resource->getId()]); + } + + /** + * Checks tracked resources for change events. + * + * @return array change events array + */ + public function getEvents() + { + $events = array(); + foreach ($this->checkers as $id => $checker) { + foreach ($checker->getChangeset() as $change) { + $events[] = new Event($id, $change['resource'], $change['event']); + } + } + + return $events; + } +} diff --git a/src/Symfony/Component/ResourceWatcher/Tracker/TrackerInterface.php b/src/Symfony/Component/ResourceWatcher/Tracker/TrackerInterface.php new file mode 100644 index 0000000000000..1497ce029d4e0 --- /dev/null +++ b/src/Symfony/Component/ResourceWatcher/Tracker/TrackerInterface.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\ResourceWatcher\Tracker; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Resources tracker interface. + * + * @author Konstantin Kudryashov + */ +interface TrackerInterface +{ + /** + * Starts to track provided resource for changes. + * + * @param ResourceInterface $resource + */ + function track(ResourceInterface $resource); + + /** + * Checks whether provided resource is tracked by this tracker. + * + * @param ResourceInterface $resource + * + * @return Boolean + */ + function isResourceTracked(ResourceInterface $resource); + + /** + * Checks tracked resources for change events. + * + * @return array change events array + */ + function getEvents(); +} diff --git a/tests/Symfony/Tests/Component/Config/Resource/DirectoryResourceTest.php b/tests/Symfony/Tests/Component/Config/Resource/DirectoryResourceTest.php index 417a433adb8a4..e0f47fb33b1d8 100644 --- a/tests/Symfony/Tests/Component/Config/Resource/DirectoryResourceTest.php +++ b/tests/Symfony/Tests/Component/Config/Resource/DirectoryResourceTest.php @@ -49,6 +49,20 @@ protected function removeDirectory($directory) { rmdir($directory); } + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getId + */ + public function testGetId() + { + $resource1 = new DirectoryResource($this->directory); + $resource2 = new DirectoryResource($this->directory); + $resource3 = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + $this->assertNotNull($resource1->getId()); + $this->assertEquals($resource1->getId(), $resource2->getId()); + $this->assertNotEquals($resource1->getId(), $resource3->getId()); + } + /** * @covers Symfony\Component\Config\Resource\DirectoryResource::getResource */ @@ -167,4 +181,102 @@ public function testFilterRegexListMatch() touch($this->directory.'/new.xml', time() + 20); $this->assertFalse($resource->isFresh(time() + 10), '->isFresh() returns false if an new file matching the filter regex is created '); } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::hasFile + */ + public function testHasFile() + { + $resource = new DirectoryResource($this->directory, '/\.foo$/'); + + touch($this->directory.'/new.foo', time() + 20); + + $this->assertFalse($resource->hasFile($this->directory.'/tmp.xml')); + $this->assertTrue($resource->hasFile($this->directory.'/new.foo')); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getFilteredChilds + */ + public function testGetFilteredChilds() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($file1 = $this->directory.'/new.xml', time() + 20); + touch($file2 = $this->directory.'/old.foo', time() + 20); + touch($this->directory.'/old', time() + 20); + mkdir($dir = $this->directory.'/sub'); + touch($file3 = $this->directory.'/sub/file.foo', time() + 20); + + $childs = $resource->getFilteredChilds(); + $this->assertSame(5, count($childs)); + + $childs = array_map(function($item) { + return (string) $item; + }, $childs); + + $this->assertContains($file1, $childs); + $this->assertContains($file2, $childs); + $this->assertContains($dir, $childs); + $this->assertContains($this->directory.'/tmp.xml', $childs); + $this->assertContains($file3, $childs); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getFilteredResources + */ + public function testGetFilteredResources() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($file1 = $this->directory.'/new.xml', time() + 20); + touch($file2 = $this->directory.'/old.foo', time() + 20); + touch($this->directory.'/old', time() + 20); + mkdir($dir = $this->directory.'/sub'); + touch($file3 = $this->directory.'/sub/file.foo', time() + 20); + + $resources = $resource->getFilteredResources(); + $this->assertSame(4, count($resources)); + + $childs = array_map(function($item) { + return realpath($item->getResource()); + }, $resources); + + $this->assertContains(realpath($file1), $childs); + $this->assertContains(realpath($file2), $childs); + $this->assertContains(realpath($dir), $childs); + $this->assertContains(realpath($this->directory.'/tmp.xml'), $childs); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::exists + */ + public function testDirectoryExists() + { + $resource = new DirectoryResource($this->directory); + + $this->assertTrue($resource->exists(), '->exists() returns true if directory exists '); + + unlink($this->directory.'/tmp.xml'); + rmdir($this->directory); + + $this->assertFalse($resource->exists(), '->exists() returns false if directory does not exists'); + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getModificationTime + */ + public function testGetModificationTime() + { + $resource = new DirectoryResource($this->directory, '/\.(foo|xml)$/'); + + touch($this->directory.'/new.xml', $time = time() + 20); + $this->assertSame($time, $resource->getModificationTime(), '->getModificationTime() returns time of the last modificated resource'); + + touch($this->directory.'/some', time() + 60); + $this->assertSame($time, $resource->getModificationTime(), '->getModificationTime() returns time of last modificated resource, that only matches pattern'); + + touch($this->directory, $time2 = time() + 90); + $this->assertSame($time2, $resource->getModificationTime(), '->getModificationTime() returns modification time of the directory itself'); + } } diff --git a/tests/Symfony/Tests/Component/Config/Resource/FileResourceTest.php b/tests/Symfony/Tests/Component/Config/Resource/FileResourceTest.php index ef8daf593678a..1a6ba15380928 100644 --- a/tests/Symfony/Tests/Component/Config/Resource/FileResourceTest.php +++ b/tests/Symfony/Tests/Component/Config/Resource/FileResourceTest.php @@ -27,7 +27,21 @@ protected function setUp() protected function tearDown() { - unlink($this->file); + if ($this->file) { + unlink($this->file); + } + } + + /** + * @covers Symfony\Component\Config\Resource\DirectoryResource::getId + */ + public function testGetId() + { + $resource1 = new FileResource($this->file); + $resource2 = new FileResource($this->file); + + $this->assertNotNull($resource1->getId()); + $this->assertEquals($resource1->getId(), $resource2->getId()); } /** @@ -49,4 +63,26 @@ public function testIsFresh() $resource = new FileResource('/____foo/foobar'.rand(1, 999999)); $this->assertFalse($resource->isFresh(time()), '->isFresh() returns false if the resource does not exist'); } + + /** + * @covers Symfony\Component\Config\Resource\FileResource::getModificationTime + */ + public function testGetModificationTime() + { + touch($this->file, $time = time() + 100); + $this->assertSame($time, $this->resource->getModificationTime()); + } + + /** + * @covers Symfony\Component\Config\Resource\FileResource::exists + */ + public function testExists() + { + $this->assertTrue($this->resource->exists(), '->exists() returns true if the resource exists'); + + unlink($this->file); + $this->file = null; + + $this->assertFalse($this->resource->exists(), '->exists() returns false if the resource does not exists'); + } } diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventListenerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventListenerTest.php new file mode 100644 index 0000000000000..c3a16a5ca8e74 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventListenerTest.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\Tests\Component\ResourceWatcher\Event; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\ResourceWatcher\Event\EventListener; + +class EventListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructAndGetters() + { + $listener = new EventListener($res = new FileResource(__FILE__), $cb = function(){}, Event::CREATED); + + $this->assertSame($res, $listener->getResource()); + $this->assertSame($cb, $listener->getCallback()); + } + + public function testHandles() + { + $res = new FileResource(__FILE__); + $cb = function(){}; + + $listener = new EventListener($res, $cb, Event::CREATED); + + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::CREATED))); + $this->assertFalse($listener->supports(new Event(1, $res, $type = Event::MODIFIED))); + $this->assertFalse($listener->supports(new Event(1, $res, $type = Event::DELETED))); + + $listener = new EventListener($res, $cb, Event::CREATED | Event::DELETED); + + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::CREATED))); + $this->assertFalse($listener->supports(new Event(1, $res, $type = Event::MODIFIED))); + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::DELETED))); + + $listener = new EventListener($res, $cb, Event::ALL); + + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::CREATED))); + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::MODIFIED))); + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::DELETED))); + + $listener = new EventListener($res, $cb, Event::DELETED); + + $this->assertFalse($listener->supports(new Event(1, $res, $type = Event::CREATED))); + $this->assertFalse($listener->supports(new Event(1, $res, $type = Event::MODIFIED))); + $this->assertTrue($listener->supports(new Event(1, $res, $type = Event::DELETED))); + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventTest.php new file mode 100644 index 0000000000000..799048d5734be --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/Event/EventTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\Event; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\ResourceWatcher\Event\Event; + +class EventTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructAndGetters() + { + $event = new Event($id = 23, $res = new FileResource(__FILE__), $type = Event::MODIFIED); + + $this->assertEquals($id, $event->getTrackingId()); + $this->assertSame($res, $event->getResource()); + $this->assertSame($type, $event->getType()); + $this->assertNotNull($event->getTime()); + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/ResourceWatcherTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/ResourceWatcherTest.php new file mode 100644 index 0000000000000..1290c68860124 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/ResourceWatcherTest.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher; + +use Symfony\Component\ResourceWatcher\ResourceWatcher; +use Symfony\Component\ResourceWatcher\Event\Event; + +class ResourceWatcherTest extends \PHPUnit_Framework_TestCase +{ + public function testUntrackedResourceTrack() + { + $tracker = $this + ->getMockBuilder('Symfony\Component\ResourceWatcher\Tracker\TrackerInterface') + ->getMock(); + + $resource = $this + ->getMockBuilder('Symfony\Component\Config\Resource\ResourceInterface') + ->getMock(); + + $tracker + ->expects($this->once()) + ->method('isResourceTracked') + ->with($resource) + ->will($this->returnValue(false)); + $tracker + ->expects($this->once()) + ->method('track') + ->with($resource) + ->will($this->returnValue(null)); + + $resource + ->expects($this->once()) + ->method('getId'); + + $watcher = new ResourceWatcher($tracker); + $watcher->track($resource, function(){}); + } + + public function testTrackedResourceTrack() + { + $tracker = $this + ->getMockBuilder('Symfony\Component\ResourceWatcher\Tracker\TrackerInterface') + ->getMock(); + + $resource = $this + ->getMockBuilder('Symfony\Component\Config\Resource\ResourceInterface') + ->getMock(); + + $tracker + ->expects($this->once()) + ->method('isResourceTracked') + ->with($resource) + ->will($this->returnValue(true)); + $tracker + ->expects($this->never()) + ->method('track'); + + $resource + ->expects($this->once()) + ->method('getId'); + + $watcher = new ResourceWatcher($tracker); + $watcher->track($resource, function(){}); + } + + public function testWatching() + { + $tracker = $this + ->getMockBuilder('Symfony\Component\ResourceWatcher\Tracker\TrackerInterface') + ->getMock(); + + $resourceMockBuilder = $this + ->getMockBuilder('Symfony\Component\Config\Resource\ResourceInterface'); + + $resource1 = $resourceMockBuilder->getMock(); + $resource2 = $resourceMockBuilder->getMock(); + + $listenerMockBuilder = $this + ->getMockBuilder('Symfony\Component\ResourceWatcher\Event\EventListenerInterface'); + + $listener1 = $listenerMockBuilder->getMock(); + $listener2 = $listenerMockBuilder->getMock(); + $listener3 = $listenerMockBuilder->getMock(); + + $listener1 + ->expects($this->exactly(3)) + ->method('getResource') + ->will($this->returnValue($resource1)); + $listener2 + ->expects($this->exactly(3)) + ->method('getResource') + ->will($this->returnValue($resource2)); + $listener3 + ->expects($this->exactly(2)) + ->method('getResource') + ->will($this->returnValue($resource2)); + + $tracker + ->expects($this->exactly(3)) + ->method('isResourceTracked') + ->will($this->onConsecutiveCalls(false, false, true)); + $tracker + ->expects($this->exactly(2)) + ->method('track'); + + $resource1 + ->expects($this->once()) + ->method('getId') + ->will($this->returnValue(1)); + + $resource2 + ->expects($this->exactly(2)) + ->method('getId') + ->will($this->onConsecutiveCalls(2, 2)); + + $watcher = new ResourceWatcher($tracker); + $watcher->addListener($listener1); + $watcher->addListener($listener2); + $watcher->addListener($listener3); + + $listener1 + ->expects($this->once()) + ->method('supports') + ->will($this->returnValue(true)); + $listener2 + ->expects($this->exactly(2)) + ->method('supports') + ->will($this->onConsecutiveCalls(false, false)); + $listener3 + ->expects($this->exactly(2)) + ->method('supports') + ->will($this->onConsecutiveCalls(true, true)); + + $listener1 + ->expects($this->once()) + ->method('getCallback') + ->will($this->returnValue(function($e){})); + $listener2 + ->expects($this->never()) + ->method('getCallback'); + $listener3 + ->expects($this->exactly(2)) + ->method('getCallback') + ->will($this->returnValue(function($e){})); + + $tracker + ->expects($this->once()) + ->method('getEvents') + ->will($this->returnValue(array( + new Event(1, $resource1, 1), + new Event(2, $resource2, 1), + new Event(2, $resource2, 1), + ))); + + $watcher->start(1, 1); + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/DirectoryStateCheckerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/DirectoryStateCheckerTest.php new file mode 100644 index 0000000000000..301d41efe2ff4 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/DirectoryStateCheckerTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\StateChecker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\ResourceWatcher\StateChecker\DirectoryStateChecker; +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileResource; + +class DirectoryStateCheckerTest extends \PHPUnit_Framework_TestCase +{ + public function testDeepFileChanged() + { + $resource = $this->createDirectoryResourceMock(); + $resource + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foo = $this->createDirectoryResourceMock() + ))); + + $resource + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(11)); + + $foo + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foobar = $this->createFileResourceMock() + ))); + $foo + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(22)); + + $foobar + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(33)); + + $checker = new DirectoryStateChecker($resource); + + $this->touchResource($resource, true, true); + $this->touchResource($foo, true, true); + $this->touchResource($foobar, true, false); + + $this->assertEquals(array( + array('event' => Event::MODIFIED, 'resource' => $foobar) + ), $checker->getChangeset()); + } + + public function testDeepFileDeleted() + { + $resource = $this->createDirectoryResourceMock(); + $resource + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foo = $this->createDirectoryResourceMock() + ))); + $resource + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(11)); + $foo + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foobar = $this->createFileResourceMock(array(true, false)) + ))); + $foo + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(22)); + $foobar + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(33)); + + $checker = new DirectoryStateChecker($resource); + + $this->touchResource($resource, true, true); + $this->touchResource($foo, true, true); + $this->touchResource($foobar, false); + + $this->assertEquals(array( + array('event' => Event::DELETED, 'resource' => $foobar) + ), $checker->getChangeset()); + } + + public function testDeepFileCreated() + { + $resource = $this->createDirectoryResourceMock(); + $resource + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foo = $this->createDirectoryResourceMock() + ))); + $resource + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(11)); + $foo + ->expects($this->any()) + ->method('getFilteredResources') + ->will($this->returnValue(array( + $foobar = $this->createFileResourceMock(array(false, true)) + ))); + $foo + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(22)); + $foobar + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(33)); + + $checker = new DirectoryStateChecker($resource); + + $this->touchResource($resource, true, true); + $this->touchResource($foo, true, true); + $this->touchResource($foobar, false); + + $this->assertEquals(array( + array('event' => Event::DELETED, 'resource' => $foobar) + ), $checker->getChangeset()); + } + + protected function touchResource(ResourceInterface $resource, $exists = true, $fresh = true) + { + if ($exists) { + $resource + ->expects($this->any()) + ->method('isFresh') + ->will($this->returnValue($fresh)); + } + } + + protected function createDirectoryResourceMock($exists = true) + { + $resource = $this->getMockBuilder('Symfony\Component\Config\Resource\DirectoryResource') + ->disableOriginalConstructor() + ->getMock(); + + $this->setResourceExists($resource, $exists); + + return $resource; + } + + protected function createFileResourceMock($exists = true) + { + $resource = $this->getMockBuilder('Symfony\Component\Config\Resource\FileResource') + ->disableOriginalConstructor() + ->getMock(); + + $this->setResourceExists($resource, $exists); + + return $resource; + } + + protected function setResourceExists($resource, $exists) + { + if (is_array($exists)) { + $resource + ->expects($this->any()) + ->method('exists') + ->will($this->onConsecutiveCalls($exists)); + } else { + $resource + ->expects($this->any()) + ->method('exists') + ->will($this->returnValue($exists)); + } + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/FileStateCheckerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/FileStateCheckerTest.php new file mode 100644 index 0000000000000..91d0b011ee8d9 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/StateChecker/FileStateCheckerTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\StateChecker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\ResourceWatcher\StateChecker\FileStateChecker; + +class FileStateCheckerTest extends \PHPUnit_Framework_TestCase +{ + public function testGetResource() + { + $resource = $this->createResource(); + $checker = $this->createChecker($resource); + + $this->assertSame($resource, $checker->getResource()); + } + + public function testNoChanges() + { + $resource = $this->createResource(true); + $checker = $this->createChecker($resource); + + $resource + ->expects($this->once()) + ->method('isFresh') + ->with(12) + ->will($this->returnValue(true)); + + $this->assertEquals(array(), $checker->getChangeset()); + } + + public function testDeleted() + { + $resource = $this->createResource(null); + $resource + ->expects($this->any()) + ->method('exists') + ->will($this->onConsecutiveCalls(true, false)); + + $checker = $this->createChecker($resource); + + $this->assertEquals( + array(array('event' => Event::DELETED, 'resource' => $resource)), + $checker->getChangeset() + ); + } + + public function testModified() + { + $resource = $this->createResource(true); + $checker = $this->createChecker($resource); + + $resource + ->expects($this->once()) + ->method('isFresh') + ->with(12) + ->will($this->returnValue(false)); + + $this->assertEquals( + array(array('event' => Event::MODIFIED, 'resource' => $resource)), + $checker->getChangeset() + ); + } + + public function testConsecutiveChecks() + { + $resource = $this->createResource(null); + $resource + ->expects($this->any()) + ->method('exists') + ->will($this->onConsecutiveCalls(true, true, false)); + $checker = $this->createChecker($resource); + + $resource + ->expects($this->once()) + ->method('isFresh') + ->with(12) + ->will($this->returnValue(false)); + + $this->assertEquals( + array(array('event' => Event::MODIFIED, 'resource' => $resource)), + $checker->getChangeset() + ); + + $this->assertEquals( + array(array('event' => Event::DELETED, 'resource' => $resource)), + $checker->getChangeset() + ); + + $this->assertEquals(array(), $checker->getChangeset()); + } + + protected function createResource($exists = true) + { + $resource = $this + ->getMockBuilder('Symfony\Component\Config\Resource\FileResource') + ->disableOriginalConstructor() + ->getMock(); + + $resource + ->expects($this->any()) + ->method('getModificationTime') + ->will($this->returnValue(11)); + + if (null !== $exists) { + $resource + ->expects($this->any()) + ->method('exists') + ->will($this->returnValue($exists)); + } + + return $resource; + } + + protected function createChecker($resource) + { + return new FileStateChecker($resource); + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/InotifyTrackerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/InotifyTrackerTest.php new file mode 100644 index 0000000000000..5b7e80f6d6a32 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/InotifyTrackerTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\Tracker; + +use Symfony\Component\ResourceWatcher\Tracker\InotifyTracker; + +class InotifyTrackerTest extends TrackerTest +{ + public function setUp() + { + if (!function_exists('inotify_init')) { + $this->markTestSkipped('Inotify is required for this test'); + } + + parent::setUp(); + } + + /** + * @return TrackerInterface + */ + protected function getTracker() + { + return new InotifyTracker(); + } + + protected function getMiminumInterval() + { + return 100; + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/RecursiveIteratorTrackerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/RecursiveIteratorTrackerTest.php new file mode 100644 index 0000000000000..878a1f4ddc036 --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/RecursiveIteratorTrackerTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\Tracker; + +use Symfony\Component\ResourceWatcher\Tracker\RecursiveIteratorTracker; + +class RecursiveIteratorTrackerTest extends TrackerTest +{ + /** + * @return TrackerInterface + */ + protected function getTracker() + { + return new RecursiveIteratorTracker(); + } + + protected function getMiminumInterval() + { + return 2000000; + } +} diff --git a/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/TrackerTest.php b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/TrackerTest.php new file mode 100644 index 0000000000000..d8e7086c32cca --- /dev/null +++ b/tests/Symfony/Tests/Component/ResourceWatcher/Tracker/TrackerTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ResourceWatcher\Tracker; + +use Symfony\Component\ResourceWatcher\Event\Event; +use Symfony\Component\ResourceWatcher\Tracker\TrackerInterface; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileResource; + +abstract class TrackerTest extends \PHPUnit_Framework_TestCase +{ + protected $tmpDir; + + public function setUp() + { + $this->tmpDir = sys_get_temp_dir().'/sf2_resource_watcher_tests'; + if (is_dir($this->tmpDir)) { + $this->cleanDir($this->tmpDir); + } + + mkdir($this->tmpDir); + } + + public function tearDown() + { + $this->cleanDir($this->tmpDir); + } + + /** + * @expectedException Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException + */ + public function testDoesNotTrackMissingFiles() + { + $tracker = $this->getTracker(); + + $tracker->track(new FileResource(__DIR__.'/missingfile')); + } + + /** + * @expectedException Symfony\Component\ResourceWatcher\Exception\InvalidArgumentException + */ + public function testDoesNotTrackMissingDirectories() + { + $tracker = $this->getTracker(); + + $tracker->track(new DirectoryResource(__DIR__.'/missingdir')); + } + + public function testTrackFileChanges() + { + $tracker = $this->getTracker(); + + touch($file = $this->tmpDir.'/foo'); + + $tracker->track($resource = new FileResource($file)); + + usleep($this->getMiminumInterval()); + touch($file); + + $events = $tracker->getEvents(); + $this->assertCount(1, $events); + $this->assertEquals(Event::MODIFIED, $events[0]->getType()); + + usleep($this->getMiminumInterval()); + unlink($file); + + $events = $tracker->getEvents(); + $this->assertCount(1, $events); + $this->assertEquals(Event::DELETED, $events[0]->getType()); + } + + abstract protected function getMiminumInterval(); + + /** + * @return TrackerInterface + */ + abstract protected function getTracker(); + + protected function cleanDir($dir) + { + if (!is_dir($dir)) { + return; + } + + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($dir, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $file) + { + if (is_file($file)) { + unlink($file); + } + } + + rmdir($dir); + } +} 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