diff --git a/lib/Builder.php b/lib/Builder.php index a4530899..79fccec6 100644 --- a/lib/Builder.php +++ b/lib/Builder.php @@ -13,7 +13,6 @@ use Doctrine\RST\Event\PreBuildParseEvent; use Doctrine\RST\Event\PreBuildRenderEvent; use Doctrine\RST\Event\PreBuildScanEvent; -use Doctrine\RST\Meta\MetaEntry; use Doctrine\RST\Meta\Metas; use Symfony\Component\Filesystem\Filesystem; use function is_dir; @@ -38,12 +37,6 @@ class Builder /** @var Documents */ private $documents; - /** @var ParseQueue */ - private $parseQueue; - - /** @var Scanner */ - private $scanner; - /** @var Copier */ private $copier; @@ -67,10 +60,6 @@ public function __construct(?Kernel $kernel = null) $this->metas ); - $this->parseQueue = new Builder\ParseQueue($this->documents); - - $this->scanner = new Builder\Scanner($this->parseQueue, $this->metas); - $this->copier = new Builder\Copier($this->filesystem); $this->kernel->initBuilder($this); @@ -124,9 +113,9 @@ public function build( $this->loadCachedMetas($targetDirectory); - $this->scan($directory, $targetDirectory); + $parseQueue = $this->scan($directory, $targetDirectory); - $this->parse($directory, $targetDirectory); + $this->parse($directory, $targetDirectory, $parseQueue); $this->render($directory, $targetDirectory); @@ -147,19 +136,23 @@ public function mkdir(string $directory) : self return $this; } - private function scan(string $directory, string $targetDirectory) : void + private function scan(string $directory, string $targetDirectory) : ParseQueue { $this->configuration->dispatchEvent( PreBuildScanEvent::PRE_BUILD_SCAN, new PreBuildScanEvent($this, $directory, $targetDirectory) ); - $this->scanner->scan($directory, $this->getIndexName()); + $scanner = new Scanner( + $this->configuration->getSourceFileExtension(), + $directory, + $this->metas + ); - $this->scanner->scanMetas($directory); + return $scanner->scan($directory, $this->metas); } - private function parse(string $directory, string $targetDirectory) : void + private function parse(string $directory, string $targetDirectory, ParseQueue $parseQueue) : void { $this->configuration->dispatchEvent( PreBuildParseEvent::PRE_BUILD_PARSE, @@ -169,16 +162,14 @@ private function parse(string $directory, string $targetDirectory) : void $parseQueueProcessor = new ParseQueueProcessor( $this->kernel, $this->errorManager, - $this->parseQueue, $this->metas, $this->documents, - $this->scanner, $directory, $targetDirectory, $this->configuration->getFileExtension() ); - $parseQueueProcessor->process(); + $parseQueueProcessor->process($parseQueue); } private function render(string $directory, string $targetDirectory) : void diff --git a/lib/Builder/ParseQueue.php b/lib/Builder/ParseQueue.php index ff7f39d7..a3dfb571 100644 --- a/lib/Builder/ParseQueue.php +++ b/lib/Builder/ParseQueue.php @@ -4,51 +4,46 @@ namespace Doctrine\RST\Builder; -use function array_shift; - -class ParseQueue +final class ParseQueue { - /** @var Documents */ - private $documents; - - /** @var string[] */ - private $parseQueue = []; - - /** @var int[] */ - private $states = []; - - public function __construct(Documents $documents) + /** + * An array where each key is the filename and the value is a + * boolean indicating if the file needs to be parsed or not. + * + * @var bool[] + */ + private $fileStatuses = []; + + public function addFile(string $filename, bool $parseNeeded) : void { - $this->documents = $documents; - } - - public function getState(string $file) : ?int - { - return $this->states[$file] ?? null; + if (isset($this->fileStatuses[$filename])) { + throw new \InvalidArgumentException(sprintf('File "%s" is already in the parse queue', $filename)); + } + + $this->fileStatuses[$filename] = $parseNeeded; } - public function setState(string $file, int $state) : void + public function isFileKnownToParseQueue(string $filename) : bool { - $this->states[$file] = $state; + return array_key_exists($filename, $this->fileStatuses); } - public function getFileToParse() : ?string + public function doesFileRequireParsing(string $filename) : bool { - if ($this->parseQueue !== []) { - return array_shift($this->parseQueue); + if (!$this->isFileKnownToParseQueue($filename)) { + throw new \InvalidArgumentException(sprintf('File "%s" is not known to the parse queue', $filename)); } - return null; + return $this->fileStatuses[$filename]; } - public function addToParseQueue(string $file) : void + /** + * @return string[] + */ + public function getAllFilesThatRequireParsing() : array { - $this->states[$file] = State::PARSE; - - if ($this->documents->hasDocument($file)) { - return; - } - - $this->parseQueue[$file] = $file; + return array_keys(array_filter($this->fileStatuses, function(bool $parseNeeded) { + return $parseNeeded; + })); } } diff --git a/lib/Builder/ParseQueueProcessor.php b/lib/Builder/ParseQueueProcessor.php index 8c766ccc..b8b0b971 100644 --- a/lib/Builder/ParseQueueProcessor.php +++ b/lib/Builder/ParseQueueProcessor.php @@ -7,6 +7,7 @@ use Doctrine\RST\Environment; use Doctrine\RST\ErrorManager; use Doctrine\RST\Kernel; +use Doctrine\RST\Meta\DocumentDependency; use Doctrine\RST\Meta\Metas; use Doctrine\RST\Nodes\DocumentNode; use Doctrine\RST\Parser; @@ -22,18 +23,12 @@ class ParseQueueProcessor /** @var ErrorManager */ private $errorManager; - /** @var ParseQueue */ - private $parseQueue; - /** @var Metas */ private $metas; /** @var Documents */ private $documents; - /** @var Scanner */ - private $scanner; - /** @var string */ private $directory; @@ -46,28 +41,24 @@ class ParseQueueProcessor public function __construct( Kernel $kernel, ErrorManager $errorManager, - ParseQueue $parseQueue, Metas $metas, Documents $documents, - Scanner $scanner, string $directory, string $targetDirectory, string $fileExtension ) { $this->kernel = $kernel; $this->errorManager = $errorManager; - $this->parseQueue = $parseQueue; $this->metas = $metas; $this->documents = $documents; - $this->scanner = $scanner; $this->directory = $directory; $this->targetDirectory = $targetDirectory; $this->fileExtension = $fileExtension; } - public function process() : void + public function process(ParseQueue $parseQueue) : void { - while ($file = $this->parseQueue->getFileToParse()) { + foreach ($parseQueue->getAllFilesThatRequireParsing() as $file) { $this->processFile($file); } } @@ -86,12 +77,6 @@ private function processFile(string $file) : void $this->kernel->postParse($document); - $dependencies = $environment->getDependencies(); - - foreach ($this->buildDependenciesToScan($dependencies) as $dependency) { - $this->scanner->scan($this->directory, $dependency); - } - $this->metas->set( $file, $this->buildDocumentUrl($document), @@ -99,7 +84,7 @@ private function processFile(string $file) : void $document->getTitles(), $document->getTocs(), (int) filectime($fileAbsolutePath), - $dependencies, + $environment->getDependencies(), $environment->getLinks() ); } @@ -119,18 +104,6 @@ private function createFileParser(string $file) : Parser return $parser; } - /** - * @param string[] $dependencies - * - * @return string[] - */ - private function buildDependenciesToScan(array $dependencies) : array - { - return array_filter($dependencies, function (string $dependency) : bool { - return file_exists($this->buildFileAbsolutePath($dependency)); - }); - } - private function buildFileAbsolutePath(string $file) : string { return $this->directory . '/' . $file . '.rst'; diff --git a/lib/Builder/Scanner.php b/lib/Builder/Scanner.php index 7ce3b1c4..0b6ebed1 100644 --- a/lib/Builder/Scanner.php +++ b/lib/Builder/Scanner.php @@ -4,76 +4,178 @@ namespace Doctrine\RST\Builder; -use Doctrine\RST\Meta\MetaEntry; use Doctrine\RST\Meta\Metas; -use function file_exists; -use function filectime; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; class Scanner { - /** @var ParseQueue */ - private $parseQueue; + private $fileExtension; + + private $directory; - /** @var Metas */ private $metas; - public function __construct(ParseQueue $parseQueue, Metas $metas) + private $finder; + + /** @var SplFileInfo[] */ + private $fileInfos = []; + + public function __construct(string $fileExtension, string $directory, Metas $metas, Finder $finder = null) { - $this->parseQueue = $parseQueue; - $this->metas = $metas; + $this->fileExtension = $fileExtension; + $this->directory = $directory; + $this->metas = $metas; + + $this->finder = $finder ?: new Finder(); + $this->finder->in($this->directory) + ->files() + ->name('*.'.$this->fileExtension); } - public function scan(string $directory, string $file) : void + /** + * Scans a directory recursively looking for all files to parse. + * + * This takes into account the presence of cached & fresh MetaEntry + * objects, and avoids adding files to the parse queue that have + * not changed and whose dependencies have not changed. + */ + public function scan() : ParseQueue { - if ($this->parseQueue->getState($file) !== null) { - return; + // completely populate the splFileInfos property + $this->fileInfos = []; + foreach ($this->finder as $fileInfo) { + $relativeFilename = $fileInfo->getRelativePathname(); + // strip off the extension + $documentPath = substr($relativeFilename, 0, -(strlen($this->fileExtension) + 1)); + + $this->fileInfos[$documentPath] = $fileInfo; } - $this->parseQueue->setState($file, State::NO_PARSE); + $parseQueue = new ParseQueue(); + foreach ($this->fileInfos as $filename => $fileInfo) { + $parseQueue->addFile( + $filename, + $this->doesFileRequireParsing($filename, $parseQueue) + ); + } - $entry = $this->metas->get($file); + return $parseQueue; + + /* + * 1) See if meta exists. If it does not, add it to ParseQueue + * as needing to PARSE_NEEDED + * 2) If meta DOES exist + * a) if stale, PARSE_NEEDED + * b) if not stale, loop over each dependency and + * do the exact same check + * -> how can we avoid circular issues? + * -> basically, each iteration of the loop + * going deeper just needs to know that + * if it hits its parent caller/file, it + * should take no information from this, + * but not try to follow its dependencies. + * -> could possibly do this with a flag called + * PARSE_PROCESSING, which is released after + * you follow all of your dependencies. If + * you hit a PARSE_PROCESSING on one of your + * dependencies, you just exist and re-set + * your status back to some unknown. If you + * ARE able to determine from your dependencies + * if you need to be parsed, then you set to whatever + * that status is. + * -> this probably means adding everything to the + * ParseQueue in the beginning. And maybe this + * just becomes a utility class of the Scanner, + * and an array of filenames or array of SourceDocument + * objects is returned. Maybe Scanner can be more + * stateless, and a lot of the recursive logic is + * moved into this temporary, stateful ParseQueue. + * i) if no dependencies need to be re-parsed, don't parse + * ii) if any need re-parsing, re-parse + * + * We will use ParseQueue in an intelligent way - actually asking + * it if it is aware of a file yet, instead of using all this getState() + * garbage + */ + } - $rst = $directory . '/' . $file . '.rst'; + private function doesFileRequireParsing(string $filename, ParseQueue $parseQueue, array $filesAlreadyBeingChecked = []) : bool + { + if (!isset($this->fileInfos[$filename])) { + throw new \InvalidArgumentException(sprintf('No file info found for "%s" - file does not exist.', $filename)); + } + + $file = $this->fileInfos[$filename]; + + $documentFilename = $this->getFilenameFromFile($file); + $entry = $this->metas->get($documentFilename); - if ($entry === null || ! file_exists($rst) || $entry->getCtime() < filectime($rst)) { - // File was never seen or changed and thus need to be parsed - $this->parseQueue->addToParseQueue($file); - } else { - $this->scanMetaEntry($entry, $directory, $file); + if ($entry === null || $entry->getCtime() < $file->getCTime()) { + // File is new or changed and thus need to be parsed + return true; } - } - public function scanMetaEntry(MetaEntry $entry, string $directory, string $file) : void - { - // Have a look to the file dependencies to know if you need to parse - // it or not - $depends = $entry->getDepends(); + // Look to the file's dependencies to know if you need to parse it or not + $dependencies = $entry->getDepends(); $parent = $entry->getParent(); - if ($parent !== null) { - $depends[] = $parent; + $dependencies[] = $parent; } - foreach ($depends as $dependency) { - $this->scan($directory, $dependency); + $filesAlreadyBeingChecked[] = $documentFilename; + + foreach ($dependencies as $dependency) { + if (in_array($dependency, $filesAlreadyBeingChecked, true)) { + /* + * File is already being checked. For example, consider + * this dependency tree: + * + * DocA (depends on)-> + * DocB (depends on)-> + * DocC (depends on)-> DocB & DocD + * + * And assume only DocD has changed. + * The method will be called recursively for DocB, then DocC. + * When that happens, it needs to realize that we're already + * checking to see if DocB has changed. And so, we should not + * recursively check DocB again. It's a no-op: we don't know + * if DocB has changed yet or not. So, we skip, and check DocD. + */ + + continue; + } + + // if the parseQueue already knows about this file, just ask it + if ($parseQueue->isFileKnownToParseQueue($dependency)) { + if ($parseQueue->doesFileRequireParsing($dependency)) { + return true; + } - // If any dependency needs to be parsed, this file needs also to be - // parsed - if ($this->parseQueue->getState($dependency) !== State::PARSE) { continue; } - $this->parseQueue->addToParseQueue($file); + // dependency no longer exists? We should re-parse this file + if (!isset($this->fileInfos[$dependency])) { + return true; + } + + // finally, we need to recursively ask if this file needs parsing + if ($this->doesFileRequireParsing($dependency, $parseQueue, $filesAlreadyBeingChecked)) { + return true; + } } + + // Meta is fresh and no dependencies need parsing + return false; } - public function scanMetas(string $directory) : void + /** + * Converts foo/bar.rst to foo/bar (the document filename) + */ + private function getFilenameFromFile(SplFileInfo $file) : string { - $entries = $this->metas->getAll(); - - foreach ($entries as $file => $infos) { - $this->scan($directory, $file); - } + return substr($file->getRelativePathname(), 0, -(strlen($this->fileExtension) + 1)); } } diff --git a/lib/Builder/State.php b/lib/Builder/State.php deleted file mode 100644 index 3020c5e6..00000000 --- a/lib/Builder/State.php +++ /dev/null @@ -1,15 +0,0 @@ -formats[$this->fileExtension]; } + public function getSourceFileExtension() : string + { + return $this->sourceFileExtension; + } + private function createNodeInstantiator(string $type, string $nodeClassName) : NodeInstantiator { return new NodeInstantiator( diff --git a/tests/Builder/ParseQueueProcessorTest.php b/tests/Builder/ParseQueueProcessorTest.php index eed54c2c..7bfb236f 100644 --- a/tests/Builder/ParseQueueProcessorTest.php +++ b/tests/Builder/ParseQueueProcessorTest.php @@ -24,18 +24,12 @@ class ParseQueueProcessorTest extends TestCase /** @var ErrorManager|MockObject */ private $errorManager; - /** @var ParseQueue|MockObject */ - private $parseQueue; - /** @var Metas|MockObject */ private $metas; /** @var Documents|MockObject */ private $documents; - /** @var Scanner|MockObject */ - private $scanner; - /** @var string */ private $directory; @@ -52,13 +46,8 @@ public function testProcess() : void { touch($this->directory . '/file.rst'); - $this->parseQueue->expects(self::at(0)) - ->method('getFileToParse') - ->willReturn('file'); - - $this->parseQueue->expects(self::at(1)) - ->method('getFileToParse') - ->willReturn(null); + $parseQueue = new ParseQueue(); + $parseQueue->addFile('file', true); $this->documents->expects(self::once()) ->method('addDocument') @@ -70,17 +59,15 @@ public function testProcess() : void $this->metas->expects(self::once()) ->method('set'); - $this->parseQueueProcessor->process(); + $this->parseQueueProcessor->process($parseQueue); } protected function setUp() : void { $this->kernel = $this->createMock(Kernel::class); $this->errorManager = $this->createMock(ErrorManager::class); - $this->parseQueue = $this->createMock(ParseQueue::class); $this->metas = $this->createMock(Metas::class); $this->documents = $this->createMock(Documents::class); - $this->scanner = $this->createMock(Scanner::class); $this->directory = sys_get_temp_dir(); $this->targetDirectory = '/target'; $this->fileExtension = 'rst'; @@ -88,10 +75,8 @@ protected function setUp() : void $this->parseQueueProcessor = new ParseQueueProcessor( $this->kernel, $this->errorManager, - $this->parseQueue, $this->metas, $this->documents, - $this->scanner, $this->directory, $this->targetDirectory, $this->fileExtension diff --git a/tests/Builder/ParseQueueTest.php b/tests/Builder/ParseQueueTest.php index eb107d3e..e361ba61 100644 --- a/tests/Builder/ParseQueueTest.php +++ b/tests/Builder/ParseQueueTest.php @@ -12,49 +12,19 @@ class ParseQueueTest extends TestCase { - /** @var Documents|MockObject */ - private $documents; - - /** @var ParseQueue */ - private $parseQueue; - - public function testGetSetState() : void - { - self::assertNull($this->parseQueue->getState('file')); - - $this->parseQueue->setState('file', State::NO_PARSE); - - self::assertSame(State::NO_PARSE, $this->parseQueue->getState('file')); - } - - public function testGetFileToParse() : void - { - self::assertNull($this->parseQueue->getFileToParse()); - - $this->parseQueue->addToParseQueue('file1'); - $this->parseQueue->addToParseQueue('file2'); - - self::assertSame('file1', $this->parseQueue->getFileToParse()); - self::assertSame('file2', $this->parseQueue->getFileToParse()); - self::assertNull($this->parseQueue->getFileToParse()); - } - - public function testAddToParseQueue() : void + public function testAddingFiles() { - $this->documents->expects(self::once()) - ->method('hasDocument') - ->with('file') - ->willReturn(true); - - $this->parseQueue->addToParseQueue('file'); - - self::assertNull($this->parseQueue->getFileToParse()); - } - - protected function setUp() : void - { - $this->documents = $this->createMock(Documents::class); - - $this->parseQueue = new ParseQueue($this->documents); + $parseQueue = new ParseQueue(); + $parseQueue->addFile('file_needs_parsing1', true); + $parseQueue->addFile('file_no_parsing1', false); + + $this->assertTrue($parseQueue->isFileKnownToParseQueue('file_needs_parsing1')); + $this->assertTrue($parseQueue->isFileKnownToParseQueue('file_no_parsing1')); + $this->assertFalse($parseQueue->isFileKnownToParseQueue('other_file')); + + $this->assertTrue($parseQueue->doesFileRequireParsing('file_needs_parsing1')); + $this->assertFalse($parseQueue->doesFileRequireParsing('file_no_parsing1')); + + $this->assertSame(['file_needs_parsing1'], $parseQueue->getAllFilesThatRequireParsing()); } } diff --git a/tests/Builder/ScannerTest.php b/tests/Builder/ScannerTest.php index 6002b0bf..b4205fd6 100644 --- a/tests/Builder/ScannerTest.php +++ b/tests/Builder/ScannerTest.php @@ -4,18 +4,19 @@ namespace Doctrine\Tests\RST\Builder; -use Doctrine\RST\Builder\ParseQueue; +use ArrayIterator; use Doctrine\RST\Builder\Scanner; -use Doctrine\RST\Builder\State; use Doctrine\RST\Meta\MetaEntry; use Doctrine\RST\Meta\Metas; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; class ScannerTest extends TestCase { - /** @var ParseQueue|MockObject */ - private $parseQueue; + /** @var Finder|MockObject */ + private $finder; /** @var Metas|MockObject */ private $metas; @@ -23,96 +24,219 @@ class ScannerTest extends TestCase /** @var Scanner */ private $scanner; - public function testScan() : void - { - $this->parseQueue->expects(self::once()) - ->method('getState') - ->with('file') - ->willReturn(null); - - $this->parseQueue->expects(self::once()) - ->method('setState') - ->with('file', State::NO_PARSE); + /** @var SplFileInfo[]|MockObject[]|ArrayIterator */ + private $fileMocks = []; - $metaEntry = $this->createMock(MetaEntry::class); + private $metaEntryMocks = []; - $this->metas->expects(self::once()) + public function testScanWithNoMetas() : void + { + $this->metas->expects($this->any()) ->method('get') - ->with('file') - ->willReturn($metaEntry); - - $this->parseQueue->expects(self::once()) - ->method('addToParseQueue') - ->with('file'); + ->willReturn(null); - $this->scanner->scan('/directory', 'file'); + $this->addFileMockToFinder('file1.rst'); + $this->addFileMockToFinder('file2.rst'); + $this->addFileMockToFinder('file3.rst'); + $this->addFileMockToFinder('subdir/file4.rst'); + $this->addFileMockToFinder('subdir/file5.rst'); + + $parseQueue = $this->scanner->scan(); + $this->assertSame([ + 'file1', + 'file2', + 'file3', + 'subdir/file4', + 'subdir/file5' + ], $parseQueue->getAllFilesThatRequireParsing()); } - public function testScanMetaEntry() : void + public function testScanWithNonFreshMetas() { - $metaEntry = $this->createMock(MetaEntry::class); - - $metaEntry->expects(self::once()) + $file1InfoMock = $this->addFileMockToFinder('file1.rst'); + $file1MetaMock = $this->createMetaEntryMock('file1'); + + // file1.rst was modified 50 seconds ago + $file1InfoMock->method('getCTime')->willReturn(time() - 50); + // but file1 MetaEntry was modified 100 seconds ago (is out of date) + $file1MetaMock->method('getCTime')->willReturn(time() - 100); + // should never be called because the meta is definitely not fresh + $file1MetaMock->expects($this->never())->method('getDepends'); + + $file2InfoMock = $this->addFileMockToFinder('file2.rst'); + $file2MetaMock = $this->createMetaEntryMock('file2'); + + // file2.rst was modified 50 seconds ago + $lastModifiedTime = time() - 50; + $file2InfoMock->method('getCTime')->willReturn($lastModifiedTime); + // and file2 MetaEntry was also 50 seconds ago, fresh + $file2MetaMock->method('getCTime')->willReturn($lastModifiedTime); + // ignore dependencies for this test + $file2MetaMock->expects($this->once()) ->method('getDepends') - ->willReturn(['dependency']); - - $metaEntry->expects(self::once()) - ->method('getParent') - ->willReturn('parent'); - - $this->parseQueue->expects(self::at(0)) - ->method('getState') - ->with('dependency') - ->willReturn(State::PARSE); + ->willReturn([]); - $this->parseQueue->expects(self::at(1)) - ->method('getState') - ->with('dependency') - ->willReturn(State::PARSE); - - $this->parseQueue->expects(self::at(2)) - ->method('addToParseQueue') - ->with('file'); - - $this->scanner->scanMetaEntry($metaEntry, '/directory', 'file'); + $parseQueue = $this->scanner->scan(); + $this->assertSame(['file1'], $parseQueue->getAllFilesThatRequireParsing()); + $this->assertFalse($parseQueue->doesFileRequireParsing('file2')); } - public function testScanMetas() : void + public function testScanWithDependencies() { - $metaEntry = $this->createMock(MetaEntry::class); + /* + * Here is the dependency tree and results: + * * file1 (unmodified) + * depends on: file2 + * * file2 (unmodified) + * depends on: file3, file1 + * * file3 (unmodified) + * depends on: file4, file5, file3, file2 + * * file4 (unmodified) + * depends on: nothing + * * file5 (MODIFIED) + * depends on: file3, file6 + * * file6 (unmodified) + * depends on: file4 + * + * Result is that only file4 and file 6 are fresh + */ + + $metaCTime = time() - 50; + + $file1InfoMock = $this->addFileMockToFinder('file1.rst'); + $file1InfoMock->method('getCTime')->willReturn($metaCTime); + $file1MetaMock = $this->createMetaEntryMock('file1'); + $file1MetaMock->method('getDepends') + ->willReturn(['file2']); + $file1MetaMock->method('getCTime')->willReturn($metaCTime); + + $file2InfoMock = $this->addFileMockToFinder('file2.rst'); + $file2InfoMock->method('getCTime')->willReturn($metaCTime); + $file2MetaMock = $this->createMetaEntryMock('file2'); + $file2MetaMock->method('getDepends') + ->willReturn(['file2', 'file3']); + $file2MetaMock->method('getCTime')->willReturn($metaCTime); + + $file3InfoMock = $this->addFileMockToFinder('file3.rst'); + $file3InfoMock->method('getCTime')->willReturn($metaCTime); + $file3MetaMock = $this->createMetaEntryMock('file3'); + $file3MetaMock->method('getDepends') + ->willReturn(['file4', 'file5', 'file3', 'file2']); + $file3MetaMock->method('getCTime')->willReturn($metaCTime); + + $file4InfoMock = $this->addFileMockToFinder('file4.rst'); + $file4InfoMock->method('getCTime')->willReturn($metaCTime); + $file4MetaMock = $this->createMetaEntryMock('file4'); + $file4MetaMock->method('getDepends') + ->willReturn([]); + $file4MetaMock->method('getCTime')->willReturn($metaCTime); + + $file5InfoMock = $this->addFileMockToFinder('file5.rst'); + // THIS file is the one file that's modified + $file5InfoMock->method('getCTime')->willReturn(time() - 10); + $file5MetaMock = $this->createMetaEntryMock('file5'); + $file5MetaMock->method('getDepends') + ->willReturn(['file3', 'file6']); + $file5MetaMock->method('getCTime')->willReturn($metaCTime); + + $file6InfoMock = $this->addFileMockToFinder('file6.rst'); + $file6InfoMock->method('getCTime')->willReturn($metaCTime); + $file6MetaMock = $this->createMetaEntryMock('file6'); + $file6MetaMock->method('getDepends') + ->willReturn(['file4']); + $file6MetaMock->method('getCTime')->willReturn($metaCTime); + + $parseQueue = $this->scanner->scan(); + $this->assertSame([ + 'file1', + 'file2', + 'file3', + 'file5', + ], $parseQueue->getAllFilesThatRequireParsing()); + } - $metaEntries = ['file' => $metaEntry]; + public function testScanWithNonExistentDependency() + { + /* + * * file1 (unmodified) + * depends on: file2 + * * file2 (does not exist) + * depends on: file3, file1 + * + * Result is that file 1 DOES need to be parsed + */ + + $metaCTime = time() - 50; + + $file1InfoMock = $this->addFileMockToFinder('file1.rst'); + $file1InfoMock->method('getCTime')->willReturn($metaCTime); + $file1MetaMock = $this->createMetaEntryMock('file1'); + $file1MetaMock->method('getDepends') + ->willReturn(['file2']); + $file1MetaMock->method('getCTime')->willReturn($metaCTime); + + // no file info made for file2 + + $parseQueue = $this->scanner->scan(); + $this->assertSame(['file1'], $parseQueue->getAllFilesThatRequireParsing()); + } - $this->metas->expects(self::once()) - ->method('getAll') - ->willReturn($metaEntries); + // test dependency does not exist anymore - $this->parseQueue->expects(self::once()) - ->method('getState') - ->with('file') - ->willReturn(null); + protected function setUp() : void + { + $this->fileMocks = new \ArrayIterator(); + $this->finder = $this->createMock(Finder::class); + $this->finder->expects($this->any()) + ->method('getIterator') + ->willReturn($this->fileMocks); + $this->finder->expects($this->once()) + ->method('in') + ->with('/directory') + ->willReturnSelf(); + $this->finder->expects($this->once()) + ->method('files') + ->with() + ->willReturnSelf(); + $this->finder->expects($this->once()) + ->method('name') + ->with('*.rst') + ->willReturnSelf(); + + $this->metas = $this->createMock(Metas::class); + $this->metas->expects($this->any()) + ->method('get') + ->willReturnCallback(function($filename) { + return $this->metaEntryMocks[$filename] ?? null; + }); - $this->parseQueue->expects(self::once()) - ->method('setState') - ->with('file', State::NO_PARSE); + $this->scanner = new Scanner('rst', '/directory', $this->metas, $this->finder); + } - $this->metas->expects(self::once()) - ->method('get') - ->with('file') - ->willReturn($metaEntry); + /** + * @return MockObject|SplFileInfo + */ + private function addFileMockToFinder(string $relativePath) + { + $fileInfo = $this->createMock(SplFileInfo::class); + $fileInfo->expects($this->any()) + ->method('getRelativePathname') + ->willReturn($relativePath); - $this->parseQueue->expects(self::once()) - ->method('addToParseQueue') - ->with('file'); + $this->fileMocks[$relativePath] = $fileInfo; - $this->scanner->scanMetas('/directory'); + return $fileInfo; } - protected function setUp() : void + /** + * @return MockObject|MetaEntry + */ + private function createMetaEntryMock(string $filename) { - $this->parseQueue = $this->createMock(ParseQueue::class); - $this->metas = $this->createMock(Metas::class); + $meta = $this->createMock(MetaEntry::class); + + $this->metaEntryMocks[$filename] = $meta; - $this->scanner = new Scanner($this->parseQueue, $this->metas); + return $meta; } }