Write some tests!

This commit is contained in:
Daniel Hensby 2018-03-06 15:14:31 +00:00
parent 0d4b4cf0e5
commit d8f9f31b71
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
4 changed files with 485 additions and 15 deletions

18
.travis.yml Normal file
View File

@ -0,0 +1,18 @@
language: php
php:
- 7.1
- 7.2
- nightly
matrix:
fast_finish: true
allow_failures:
- php: nightly
before_script:
- composer validate
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile
script:
- vendor/bin/phpunit tests/

View File

@ -14,6 +14,11 @@
"SilverStripe\\RecipePlugin\\": "src/" "SilverStripe\\RecipePlugin\\": "src/"
} }
}, },
"autoload-dev": {
"psr-4": {
"SilverStripe\\Test\\RecipePlugin\\": "src/"
}
},
"extra": { "extra": {
"class": "SilverStripe\\RecipePlugin\\RecipePlugin", "class": "SilverStripe\\RecipePlugin\\RecipePlugin",
"branch-alias": { "branch-alias": {
@ -24,6 +29,7 @@
"composer-plugin-api": "^1.1" "composer-plugin-api": "^1.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^7",
"composer/composer": "^1.2" "composer/composer": "^1.2"
}, },
"minimum-stability": "dev" "minimum-stability": "dev"

View File

@ -8,6 +8,7 @@ use Composer\Composer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use FilesystemIterator; use FilesystemIterator;
use Iterator; use Iterator;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
@ -16,8 +17,16 @@ use RegexIterator;
class RecipeInstaller extends LibraryInstaller { class RecipeInstaller extends LibraryInstaller {
public function __construct(IOInterface $io, Composer $composer) { /**
parent::__construct($io, $composer, null); * RecipeInstaller constructor.
*
* @param IOInterface $io
* @param Composer $composer
* @param string $type
* @param Filesystem $filesystem
*/
public function __construct(IOInterface $io, Composer $composer, $type = null, Filesystem $filesystem = null) {
parent::__construct($io, $composer, $type, $filesystem);
} }
/** /**
@ -32,23 +41,23 @@ class RecipeInstaller extends LibraryInstaller {
*/ */
protected function installProjectFiles($recipe, $sourceRoot, $destinationRoot, $filePatterns, $registrationKey, $name = 'project') protected function installProjectFiles($recipe, $sourceRoot, $destinationRoot, $filePatterns, $registrationKey, $name = 'project')
{ {
// load composer json data // fetch the installed files from the json data
$composerFile = new JsonFile(Factory::getComposerFile(), null, $this->io); $installedFiles = $this->getInstalledFiles($registrationKey);
$composerData = $composerFile->read();
$installedFiles = isset($composerData['extra'][$registrationKey])
? $composerData['extra'][$registrationKey]
: [];
// Load all project files // Load all project files
$fileIterator = $this->getFileIterator($sourceRoot, $filePatterns); $fileIterator = $this->getFileIterator($sourceRoot, $filePatterns);
$any = false; $any = false;
foreach($fileIterator as $path => $info) { foreach($fileIterator as $path => $info) {
$destination = $destinationRoot . substr($path, strlen($sourceRoot)); $destination = $destinationRoot . substr($path, strlen($sourceRoot));
$extension = pathinfo($destination, PATHINFO_EXTENSION); $destinationExt = pathinfo($destination, PATHINFO_EXTENSION);
if ($extension === 'tmpl') { if ($destinationExt === 'tmpl') {
$destination = substr($destination, -5); $destination = substr($destination, 0, -5);
} }
$relativePath = substr($path, strlen($sourceRoot) + 1); // Name path without leading '/' $relativePath = substr($path, strlen($sourceRoot) + 1); // Name path without leading '/'
$relativePathExt = pathinfo($relativePath, PATHINFO_EXTENSION);
if ($relativePathExt === 'tmpl') {
$relativePath = substr($relativePath, 0, -5);
}
// Write header // Write header
if (!$any) { if (!$any) {
@ -57,8 +66,8 @@ class RecipeInstaller extends LibraryInstaller {
} }
// Check if file exists // Check if file exists
if (file_exists($destination)) { if ($this->fileExists($destination)) {
if (file_get_contents($destination) === file_get_contents($path)) { if ($this->fileGetContents($destination) === $this->fileGetContents($path)) {
$this->io->write( $this->io->write(
" - Skipping <info>$relativePath</info> (<comment>existing, but unchanged</comment>)" " - Skipping <info>$relativePath</info> (<comment>existing, but unchanged</comment>)"
); );
@ -76,7 +85,7 @@ class RecipeInstaller extends LibraryInstaller {
$any++; $any++;
$this->io->write(" - Copying <info>$relativePath</info>"); $this->io->write(" - Copying <info>$relativePath</info>");
$this->filesystem->ensureDirectoryExists(dirname($destination)); $this->filesystem->ensureDirectoryExists(dirname($destination));
copy($path, $destination); $this->filesystem->copy($path, $destination);
} }
// Add file to installed (even if already exists) // Add file to installed (even if already exists)
@ -92,10 +101,35 @@ class RecipeInstaller extends LibraryInstaller {
$composerData['extra'] = []; $composerData['extra'] = [];
} }
$composerData['extra'][$registrationKey] = $installedFiles; $composerData['extra'][$registrationKey] = $installedFiles;
$composerFile->write($composerData); $this->getComposerFile()->write($composerData);
} }
} }
public function fileExists($filename)
{
return file_exists($filename);
}
public function fileGetContents($filename, $use_include_path = false, $context = null, $offset = 0, $maxlen = null)
{
return file_get_contents($filename, $use_include_path, $context, $offset, $maxlen);
}
protected function getComposerFile()
{
return new JsonFile(Factory::getComposerFile(), null, $this->io);
}
protected function getInstalledFiles($registrationKey)
{
// load composer json data
$composerFile = $this->getComposerFile();
$composerData = $composerFile->read();
return isset($composerData['extra'][$registrationKey])
? $composerData['extra'][$registrationKey]
: [];
}
/** /**
* Get iterator of matching source files to copy * Get iterator of matching source files to copy
* *

View File

@ -0,0 +1,412 @@
<?php
namespace SilverStripe\Test\RecipePlugin;
use Composer\Composer;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Util\Filesystem;
use PHPUnit\Framework\TestCase;
use SilverStripe\RecipePlugin\RecipeInstaller;
class RecipeInstallerTest extends TestCase
{
public function testInstallProjectFilesFresh()
{
$recipeName = 'test';
$sourceRoot = '/source';
$destinationRoot = '/destination';
$registrationKey = 'key';
$projectName = 'test project';
$messages = [];
$io = $this->getMockBuilder(IOInterface::class)
->setMethods([])
->getMock();
$io->expects($this->exactly(2))->method('write')->willReturnCallback(function ($message) use (&$messages) {
$messages[] = $message;
});
$composer = $this->getMockBuilder(Composer::class)
->setMethods([
'getConfig',
])->getMock();
$composer->method('getConfig')->willReturn(new Config());
$filesystem = $this->getMockBuilder(Filesystem::class)->setMethods([])->getMock();
$filesystem->expects($this->once())->method('ensureDirectoryExists')->with(
$destinationRoot
);
$filesystem->expects($this->once())->method('copy')->with(
$sourceRoot . '/file.php.tmpl',
$destinationRoot . '/file.php'
);
$mockInstaller = $this->getMockBuilder(RecipeInstaller::class)
->setConstructorArgs([
$io,
$composer,
null,
$filesystem,
])
->setMethods([
'getFileIterator',
'getInstalledFiles',
'fileExists',
'getComposerFile',
])
->getMock();
$mockInstaller->method('getFileIterator')->willReturn([
$sourceRoot . '/file.php.tmpl' => [],
]);
$mockInstaller->method('fileExists')->willReturn(false);
$mockInstaller->method('getInstalledFiles')->willReturn([]);
$mockInstaller->method('getComposerFile')->willReturn(
$jsonFile = $this->getMockBuilder(JsonFile::class)
->disableOriginalConstructor()
->setMethods([])
->getMock()
);
$jsonFile->expects($this->once())->method('write')->willReturnCallback(function ($data) use ($registrationKey) {
$this->assertArrayHasKey('extra', $data);
$this->assertArrayHasKey($registrationKey, $data['extra']);
$this->assertCount(1, $data['extra'][$registrationKey]);
$this->assertContains('file.php', $data['extra'][$registrationKey]);
});
$reflectionClass = new \ReflectionClass($mockInstaller);
$reflectionMethod = $reflectionClass->getMethod('installProjectFiles');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invokeArgs($mockInstaller, [
$recipeName,
$sourceRoot,
$destinationRoot,
'*.php',
$registrationKey,
$projectName,
]);
// perhaps theses tests are needlessly tightly coupled to the output
$this->assertCount(2, $messages);
$this->assertContains(sprintf('Installing %s files for recipe <info>%s</info>', $projectName, $recipeName), $messages[0]);
$this->assertContains('Copying <info>file.php</info>', $messages[1]);
}
public function testInstallProjectFilesExistsSame()
{
$recipeName = 'test';
$sourceRoot = '/source';
$destinationRoot = '/destination';
$registrationKey = 'key';
$projectName = 'test project';
$messages = [];
$io = $this->getMockBuilder(IOInterface::class)
->setMethods([])
->getMock();
$io->expects($this->exactly(2))->method('write')->willReturnCallback(function ($message) use (&$messages) {
$messages[] = $message;
});
$composer = $this->getMockBuilder(Composer::class)
->setMethods([
'getConfig',
])->getMock();
$composer->method('getConfig')->willReturn(new Config());
$filesystem = $this->getMockBuilder(Filesystem::class)->setMethods([])->getMock();
$filesystem->expects($this->never())->method('copy');
$mockInstaller = $this->getMockBuilder(RecipeInstaller::class)
->setConstructorArgs([
$io,
$composer,
null,
$filesystem,
])
->setMethods([
'getFileIterator',
'getInstalledFiles',
'fileExists',
'fileGetContents',
'getComposerFile',
])
->getMock();
$mockInstaller->method('getFileIterator')->willReturn([
$sourceRoot . '/file.php.tmpl' => [],
]);
$mockInstaller->method('fileExists')->willReturn(true);
$mockInstaller->expects($this->exactly(2))->method('fileGetContents')->willReturn('contents');
$mockInstaller->method('getInstalledFiles')->willReturn([]);
$mockInstaller->method('getComposerFile')->willReturn(
$jsonFile = $this->getMockBuilder(JsonFile::class)
->disableOriginalConstructor()
->setMethods([])
->getMock()
);
$jsonFile->expects($this->once())->method('write')->willReturnCallback(function ($data) use ($registrationKey) {
$this->assertArrayHasKey('extra', $data);
$this->assertArrayHasKey($registrationKey, $data['extra']);
$this->assertCount(1, $data['extra'][$registrationKey]);
$this->assertContains('file.php', $data['extra'][$registrationKey]);
});
$reflectionClass = new \ReflectionClass($mockInstaller);
$reflectionMethod = $reflectionClass->getMethod('installProjectFiles');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invokeArgs($mockInstaller, [
$recipeName,
$sourceRoot,
$destinationRoot,
'*.php',
$registrationKey,
$projectName,
]);
// perhaps theses tests are needlessly tightly coupled to the output
$this->assertCount(2, $messages);
$this->assertContains(sprintf('Installing %s files for recipe <info>%s</info>', $projectName, $recipeName), $messages[0]);
$this->assertContains('Skipping <info>file.php</info> (<comment>existing, but unchanged</comment>)', $messages[1]);
}
public function testInstallProjectFilesExistsDifferent()
{
$recipeName = 'test';
$sourceRoot = '/source';
$destinationRoot = '/destination';
$registrationKey = 'key';
$projectName = 'test project';
$messages = [];
$io = $this->getMockBuilder(IOInterface::class)
->setMethods([])
->getMock();
$io->expects($this->exactly(2))->method('write')->willReturnCallback(function ($message) use (&$messages) {
$messages[] = $message;
});
$composer = $this->getMockBuilder(Composer::class)
->setMethods([
'getConfig',
])->getMock();
$composer->method('getConfig')->willReturn(new Config());
$filesystem = $this->getMockBuilder(Filesystem::class)->setMethods([])->getMock();
$filesystem->expects($this->never())->method('copy');
$mockInstaller = $this->getMockBuilder(RecipeInstaller::class)
->setConstructorArgs([
$io,
$composer,
null,
$filesystem,
])
->setMethods([
'getFileIterator',
'getInstalledFiles',
'fileExists',
'fileGetContents',
'getComposerFile',
])
->getMock();
$mockInstaller->method('getFileIterator')->willReturn([
$sourceRoot . '/file.php.tmpl' => [],
]);
$mockInstaller->method('fileExists')->willReturn(true);
$mockInstaller->expects($this->exactly(2))->method('fileGetContents')->willReturnOnConsecutiveCalls(
'contents', 'different contents'
);
$mockInstaller->method('getInstalledFiles')->willReturn([]);
$mockInstaller->method('getComposerFile')->willReturn(
$jsonFile = $this->getMockBuilder(JsonFile::class)
->disableOriginalConstructor()
->setMethods([])
->getMock()
);
$jsonFile->expects($this->once())->method('write')->willReturnCallback(function ($data) use ($registrationKey) {
$this->assertArrayHasKey('extra', $data);
$this->assertArrayHasKey($registrationKey, $data['extra']);
$this->assertCount(1, $data['extra'][$registrationKey]);
$this->assertContains('file.php', $data['extra'][$registrationKey]);
});
$reflectionClass = new \ReflectionClass($mockInstaller);
$reflectionMethod = $reflectionClass->getMethod('installProjectFiles');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invokeArgs($mockInstaller, [
$recipeName,
$sourceRoot,
$destinationRoot,
'*.php',
$registrationKey,
$projectName,
]);
// perhaps theses tests are needlessly tightly coupled to the output
$this->assertCount(2, $messages);
$this->assertContains(sprintf('Installing %s files for recipe <info>%s</info>', $projectName, $recipeName), $messages[0]);
$this->assertContains('Skipping <info>file.php</info> (<comment>existing and modified in project</comment>)', $messages[1]);
}
public function testInstallProjectFilesRemoved()
{
$recipeName = 'test';
$sourceRoot = '/source';
$destinationRoot = '/destination';
$registrationKey = 'key';
$projectName = 'test project';
$messages = [];
$io = $this->getMockBuilder(IOInterface::class)
->setMethods([])
->getMock();
$io->expects($this->exactly(2))->method('write')->willReturnCallback(function ($message) use (&$messages) {
$messages[] = $message;
});
$composer = $this->getMockBuilder(Composer::class)
->setMethods([
'getConfig',
])->getMock();
$composer->method('getConfig')->willReturn(new Config());
$filesystem = $this->getMockBuilder(Filesystem::class)->setMethods([])->getMock();
$filesystem->expects($this->never())->method('copy');
$mockInstaller = $this->getMockBuilder(RecipeInstaller::class)
->setConstructorArgs([
$io,
$composer,
null,
$filesystem,
])
->setMethods([
'getFileIterator',
'getInstalledFiles',
'fileExists',
'fileGetContents',
'getComposerFile',
])
->getMock();
$mockInstaller->method('getFileIterator')->willReturn([
$sourceRoot . '/file.php.tmpl' => [],
]);
$mockInstaller->method('fileExists')->willReturn(false);
$mockInstaller->expects($this->never())->method('fileGetContents');
$mockInstaller->method('getInstalledFiles')->willReturn([
'file.php',
]);
$mockInstaller->method('getComposerFile')->willReturn(
$jsonFile = $this->getMockBuilder(JsonFile::class)
->disableOriginalConstructor()
->setMethods([])
->getMock()
);
$jsonFile->expects($this->once())->method('write')->willReturnCallback(function ($data) use ($registrationKey) {
$this->assertArrayHasKey('extra', $data);
$this->assertArrayHasKey($registrationKey, $data['extra']);
$this->assertCount(1, $data['extra'][$registrationKey]);
$this->assertContains('file.php', $data['extra'][$registrationKey]);
});
$reflectionClass = new \ReflectionClass($mockInstaller);
$reflectionMethod = $reflectionClass->getMethod('installProjectFiles');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invokeArgs($mockInstaller, [
$recipeName,
$sourceRoot,
$destinationRoot,
'*.php',
$registrationKey,
$projectName,
]);
// perhaps theses tests are needlessly tightly coupled to the output
$this->assertCount(2, $messages);
$this->assertContains(sprintf('Installing %s files for recipe <info>%s</info>', $projectName, $recipeName), $messages[0]);
$this->assertContains('Skipping <info>file.php</info> (<comment>previously installed</comment>)', $messages[1]);
}
public function testInstallProjectFilesWithoutTmplExtension()
{
$recipeName = 'test';
$sourceRoot = '/source';
$destinationRoot = '/destination';
$registrationKey = 'key';
$projectName = 'test project';
$messages = [];
$io = $this->getMockBuilder(IOInterface::class)
->setMethods([])
->getMock();
$io->expects($this->exactly(2))->method('write')->willReturnCallback(function ($message) use (&$messages) {
$messages[] = $message;
});
$composer = $this->getMockBuilder(Composer::class)
->setMethods([
'getConfig',
])->getMock();
$composer->method('getConfig')->willReturn(new Config());
$filesystem = $this->getMockBuilder(Filesystem::class)->setMethods([])->getMock();
$filesystem->expects($this->once())->method('ensureDirectoryExists')->with(
$destinationRoot
);
$filesystem->expects($this->once())->method('copy')->with(
$sourceRoot . '/file.php',
$destinationRoot . '/file.php'
);
$mockInstaller = $this->getMockBuilder(RecipeInstaller::class)
->setConstructorArgs([
$io,
$composer,
null,
$filesystem,
])
->setMethods([
'getFileIterator',
'getInstalledFiles',
'fileExists',
'getComposerFile',
])
->getMock();
$mockInstaller->method('getFileIterator')->willReturn([
$sourceRoot . '/file.php' => [],
]);
$mockInstaller->method('fileExists')->willReturn(false);
$mockInstaller->method('getInstalledFiles')->willReturn([]);
$mockInstaller->method('getComposerFile')->willReturn(
$jsonFile = $this->getMockBuilder(JsonFile::class)
->disableOriginalConstructor()
->setMethods([])
->getMock()
);
$jsonFile->expects($this->once())->method('write')->willReturnCallback(function ($data) use ($registrationKey) {
$this->assertArrayHasKey('extra', $data);
$this->assertArrayHasKey($registrationKey, $data['extra']);
$this->assertCount(1, $data['extra'][$registrationKey]);
$this->assertContains('file.php', $data['extra'][$registrationKey]);
});
$reflectionClass = new \ReflectionClass($mockInstaller);
$reflectionMethod = $reflectionClass->getMethod('installProjectFiles');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invokeArgs($mockInstaller, [
$recipeName,
$sourceRoot,
$destinationRoot,
'*.php',
$registrationKey,
$projectName,
]);
// perhaps theses tests are needlessly tightly coupled to the output
$this->assertCount(2, $messages);
$this->assertContains(sprintf('Installing %s files for recipe <info>%s</info>', $projectName, $recipeName), $messages[0]);
$this->assertContains('Copying <info>file.php</info>', $messages[1]);
}
}