From 5ca904ae12132dc385ba80b28645d8a79c6ea6fa Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 18 Oct 2013 17:00:07 +0200 Subject: [PATCH] Initializer and custom namespace support --- README.md | 29 +- .../Compiler/CoreInitializationPass.php | 30 +++ .../Console/Processor/InitProcessor.php | 247 ++++++++++++++++++ .../Console/Processor/LocatorProcessor.php | 23 +- .../ModuleContextClassGuesser.php | 23 +- src/SilverStripe/BehatExtension/Extension.php | 4 +- .../BehatExtension/services/silverstripe.yml | 6 +- 7 files changed, 337 insertions(+), 25 deletions(-) create mode 100644 src/SilverStripe/BehatExtension/Compiler/CoreInitializationPass.php create mode 100644 src/SilverStripe/BehatExtension/Console/Processor/InitProcessor.php diff --git a/README.md b/README.md index ab11a1a..26cebb5 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,34 @@ Example: behat.yml selenium2: browser: firefox -### Available Step Definitions +## Module Initialization + +You're all set to start writing features now! Simply create `*.feature` files +anywhere in your codebase, and run them as shown above. We recommend the folder +structure of `tests/behat/features`, since its consistent with the common location +of SilverStripe's PHPUnit tests. + +Behat tests rely on a `FeatureContext` class which contains step definitions, +and can be composed of other subcontexts, e.g. for SilverStripe-specific CMS steps +(details on [behat.org](http://docs.behat.org/quick_intro.html#the-context-class-featurecontext)). +Since step definitions are quite domain specific, its likely that you'll need your own context. +The SilverStripe Behat extension provides an initializer script which generates a template +in the recommended folder structure: + + vendor/bin/behat --init @mymodule + +You'll now have a class located in `mymodule/tests/behat/features/bootstrap/Context/FeatureContext.php`, +as well as a folder for your features with `mymodule/tests/behat/features`. +The class is namespaced, and defaults to the module name. You can customize this: + + vendor/bin/behat --namespace='MyVendor\MyModule' --init @mymodule + +In this case, you'll need to pass in the namespace when running the features as well +(at least until SilverStripe modules allow declaring a namespace). + + vendor/bin/behat --namespace='MyVendor\MyModule' @mymodule + +## Available Step Definitions The extension comes with several `BehatContext` subclasses come with some extra step defintions. Some of them are just helpful in general website testing, other's are specific to SilverStripe. diff --git a/src/SilverStripe/BehatExtension/Compiler/CoreInitializationPass.php b/src/SilverStripe/BehatExtension/Compiler/CoreInitializationPass.php new file mode 100644 index 0000000..ac3be2f --- /dev/null +++ b/src/SilverStripe/BehatExtension/Compiler/CoreInitializationPass.php @@ -0,0 +1,30 @@ +getParameter('behat.silverstripe_extension.framework_path'); + $_GET['flush'] = 1; + require_once $frameworkPath . '/core/Core.php'; + \TestRunner::use_test_manifest(); + unset($_GET['flush']); + + // Remove the error handler so that PHPUnit can add its own + restore_error_handler(); + } +} \ No newline at end of file diff --git a/src/SilverStripe/BehatExtension/Console/Processor/InitProcessor.php b/src/SilverStripe/BehatExtension/Console/Processor/InitProcessor.php new file mode 100644 index 0000000..8ab91b5 --- /dev/null +++ b/src/SilverStripe/BehatExtension/Console/Processor/InitProcessor.php @@ -0,0 +1,247 @@ +container = $container; + } + + /** + * @param Command $command + */ + public function configure(Command $command) + { + parent::configure($command); + + $command->addOption('--namespace', null, InputOption::VALUE_OPTIONAL, + "Optional namespace for FeatureContext, defaults to \\Test\\Behaviour.\n" + ); + } + + public function process(InputInterface $input, OutputInterface $output) + { + // throw exception if no features argument provided + if (!$input->getArgument('features') && $input->getOption('init')) { + throw new \InvalidArgumentException('Provide features argument in order to init suite.'); + } + + // initialize bundle structure and exit + if ($input->getOption('init')) { + $this->initBundleDirectoryStructure($input, $output); + + exit(0); + } + } + + /** + * Inits bundle directory structure + * + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function initBundleDirectoryStructure(InputInterface $input, OutputInterface $output) + { + // Bootstrap SS so we can use module listing + $frameworkPath = $this->container->getParameter('behat.silverstripe_extension.framework_path'); + $_GET['flush'] = 1; + require_once $frameworkPath . '/core/Core.php'; + unset($_GET['flush']); + + $featuresPath = $input->getArgument('features'); + if(!$featuresPath) { + throw new \InvalidArgumentException('Please specify a module name (e.g. "@mymodule")'); + } + + // Can't use 'behat.paths.base' since that's locked at this point to base folder (not module) + $pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix'); + $currentModuleName = null; + $modules = \SS_ClassLoader::instance()->getManifest()->getModules(); + $currentModuleName = $this->container->getParameter('behat.silverstripe_extension.module'); + + // get module from short notation if path starts from @ + if (preg_match('/^\@([^\/\\\\]+)(.*)$/', $featuresPath, $matches)) { + $currentModuleName = $matches[1]; + // TODO Replace with proper module loader once AJShort's changes are merged into core + if (!array_key_exists($currentModuleName, $modules)) { + throw new \InvalidArgumentException(sprintf('Module "%s" not found', $currentModuleName)); + } + $currentModulePath = $modules[$currentModuleName]; + } + + if (!$currentModuleName) { + throw new \InvalidArgumentException('Can not find module to initialize suite.'); + } + + // TODO Retrieve from module definition once that's implemented + if($input->getOption('namespace')) { + $namespace = $input->getOption('namespace'); + } else { + $namespace = ucfirst($currentModuleName); + } + $namespace .= '\\' . $this->container->getParameter('behat.silverstripe_extension.context.namespace_suffix'); + + $featuresPath = rtrim($currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix,DIRECTORY_SEPARATOR); + $basePath = $this->container->getParameter('behat.paths.base').DIRECTORY_SEPARATOR; + $bootstrapPath = $featuresPath.DIRECTORY_SEPARATOR.'bootstrap'; + $contextPath = $bootstrapPath.DIRECTORY_SEPARATOR.'Context'; + + if (!is_dir($featuresPath)) { + mkdir($featuresPath, 0777, true); + mkdir($bootstrapPath, 0777, true); + // touch($bootstrapPath.DIRECTORY_SEPARATOR.'_manifest_exclude'); + $output->writeln( + '+d ' . + str_replace($basePath, '', realpath($featuresPath)) . + ' - place your *.feature files here' + ); + } + + if (!is_dir($contextPath)) { + mkdir($contextPath, 0777, true); + + $className = $this->container->getParameter('behat.context.class'); + file_put_contents( + $contextPath . DIRECTORY_SEPARATOR . $className . '.php', + strtr($this->getFeatureContextSkelet(), array( + '%NAMESPACE%' => $namespace + )) + ); + + $output->writeln( + '+f ' . + str_replace($basePath, '', realpath($contextPath)) . DIRECTORY_SEPARATOR . + 'FeatureContext.php - place your feature related code here' + ); + } + } + + /** + * {@inheritdoc} + */ + protected function getFeatureContextSkelet() + { +return <<<'PHP' +useContext('BasicContext', new BasicContext($parameters)); + $this->useContext('LoginContext', new LoginContext($parameters)); + $this->useContext('CmsFormsContext', new CmsFormsContext($parameters)); + $this->useContext('CmsUiContext', new CmsUiContext($parameters)); + + $fixtureContext = new FixtureContext($parameters); + $fixtureContext->setFixtureFactory($this->getFixtureFactory()); + $this->useContext('FixtureContext', $fixtureContext); + + // Use blueprints to set user name from identifier + $factory = $fixtureContext->getFixtureFactory(); + $blueprint = \Injector::inst()->create('FixtureBlueprint', 'Member'); + $blueprint->addCallback('beforeCreate', function($identifier, &$data, &$fixtures) { + if(!isset($data['FirstName'])) $data['FirstName'] = $identifier; + }); + $factory->define('Member', $blueprint); + + // Auto-publish pages + foreach(\ClassInfo::subclassesFor('SiteTree') as $id => $class) { + $blueprint = \Injector::inst()->create('FixtureBlueprint', $class); + $blueprint->addCallback('afterCreate', function($obj, $identifier, &$data, &$fixtures) { + $obj->publish('Stage', 'Live'); + }); + $factory->define($class, $blueprint); + } + } + + public function setMinkParameters(array $parameters) { + parent::setMinkParameters($parameters); + + if(isset($parameters['files_path'])) { + $this->getSubcontext('FixtureContext')->setFilesPath($parameters['files_path']); + } + } + + /** + * @return FixtureFactory + */ + public function getFixtureFactory() { + if(!$this->fixtureFactory) { + $this->fixtureFactory = \Injector::inst()->create('BehatFixtureFactory'); + } + + return $this->fixtureFactory; + } + + public function setFixtureFactory(FixtureFactory $factory) { + $this->fixtureFactory = $factory; + } + + // + // Place your definition and hook methods here: + // + // /** + // * @Given /^I have done something with "([^"]*)"$/ + // */ + // public function iHaveDoneSomethingWith($argument) { + // $container = $this->kernel->getContainer(); + // $container->get('some_service')->doSomethingWith($argument); + // } + // +} + +PHP; + } +} \ No newline at end of file diff --git a/src/SilverStripe/BehatExtension/Console/Processor/LocatorProcessor.php b/src/SilverStripe/BehatExtension/Console/Processor/LocatorProcessor.php index 8649801..36c45f6 100644 --- a/src/SilverStripe/BehatExtension/Console/Processor/LocatorProcessor.php +++ b/src/SilverStripe/BehatExtension/Console/Processor/LocatorProcessor.php @@ -53,13 +53,9 @@ class LocatorProcessor extends BaseProcessor */ public function process(InputInterface $input, OutputInterface $output) { - // Bootstrap SS so we can use module listing - $frameworkPath = $this->container->getParameter('behat.silverstripe_extension.framework_path'); - $_GET['flush'] = 1; - require_once $frameworkPath . '/core/Core.php'; - unset($_GET['flush']); - $featuresPath = $input->getArgument('features'); + + // Can't use 'behat.paths.base' since that's locked at this point to base folder (not module) $pathSuffix = $this->container->getParameter('behat.silverstripe_extension.context.path_suffix'); $currentModuleName = null; @@ -84,23 +80,28 @@ class LocatorProcessor extends BaseProcessor foreach ($modules as $moduleName => $modulePath) { if (false !== strpos($path, realpath($modulePath))) { $currentModuleName = $moduleName; + $currentModulePath = realpath($modulePath); break; } } + $featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath; // if module is configured for profile and feature provided } elseif ($currentModuleName && $featuresPath) { $currentModulePath = $modules[$currentModuleName]; $featuresPath = $currentModulePath.DIRECTORY_SEPARATOR.$pathSuffix.DIRECTORY_SEPARATOR.$featuresPath; } + if($input->getOption('namespace')) { + $namespace = $input->getOption('namespace'); + } else { + $namespace = ucfirst($currentModuleName); + } + if ($currentModuleName) { $this->container ->get('behat.silverstripe_extension.context.class_guesser') - ->setModuleNamespace(ucfirst($currentModuleName)); - } - - if (!$featuresPath) { - $featuresPath = $this->container->getParameter('behat.paths.features'); + // TODO Improve once modules can declare their own namespaces consistently + ->setNamespaceBase($namespace); } $this->container diff --git a/src/SilverStripe/BehatExtension/Context/ClassGuesser/ModuleContextClassGuesser.php b/src/SilverStripe/BehatExtension/Context/ClassGuesser/ModuleContextClassGuesser.php index ce988af..9880ef4 100644 --- a/src/SilverStripe/BehatExtension/Context/ClassGuesser/ModuleContextClassGuesser.php +++ b/src/SilverStripe/BehatExtension/Context/ClassGuesser/ModuleContextClassGuesser.php @@ -10,17 +10,19 @@ use Behat\Behat\Context\ClassGuesser\ClassGuesserInterface; */ class ModuleContextClassGuesser implements ClassGuesserInterface { - private $classSuffix; - private $namespace; + private $namespaceSuffix; + private $namespaceBase; + private $contextClass; /** * Initializes guesser. * - * @param string $classSuffix + * @param string $namespaceSuffix */ - public function __construct($classSuffix = 'Test\\Behaviour\\FeatureContext') + public function __construct($namespaceSuffix, $contextClass) { - $this->classSuffix = $classSuffix; + $this->namespaceSuffix = $namespaceSuffix; + $this->contextClass = $contextClass; } /** @@ -28,9 +30,10 @@ class ModuleContextClassGuesser implements ClassGuesserInterface * * @param string $namespace */ - public function setModuleNamespace($namespace) + public function setNamespaceBase($namespaceBase) { - $this->namespace = $namespace; + $this->namespaceBase = $namespaceBase; + return $this; } /** @@ -41,12 +44,12 @@ class ModuleContextClassGuesser implements ClassGuesserInterface public function guess() { // Try fully qualified namespace - if (class_exists($class = $this->namespace.'\\'.$this->classSuffix)) { + if (class_exists($class = $this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) { return $class; - } + } // Fall back to namespace with SilverStripe prefix // TODO Remove once core has namespace capabilities for modules - if (class_exists($class = 'SilverStripe\\'.$this->namespace.'\\'.$this->classSuffix)) { + if (class_exists($class = 'SilverStripe\\'.$this->namespaceBase.'\\'.$this->namespaceSuffix.'\\'.$this->contextClass)) { return $class; } } diff --git a/src/SilverStripe/BehatExtension/Extension.php b/src/SilverStripe/BehatExtension/Extension.php index 85d5d30..e6b437d 100644 --- a/src/SilverStripe/BehatExtension/Extension.php +++ b/src/SilverStripe/BehatExtension/Extension.php @@ -65,7 +65,9 @@ class Extension implements ExtensionInterface */ public function getCompilerPasses() { - return array(); + return array( + new Compiler\CoreInitializationPass() + ); } /** diff --git a/src/SilverStripe/BehatExtension/services/silverstripe.yml b/src/SilverStripe/BehatExtension/services/silverstripe.yml index 9687dff..b33cb57 100644 --- a/src/SilverStripe/BehatExtension/services/silverstripe.yml +++ b/src/SilverStripe/BehatExtension/services/silverstripe.yml @@ -2,7 +2,8 @@ parameters: behat.silverstripe_extension.context.initializer.class: SilverStripe\BehatExtension\Context\Initializer\SilverStripeAwareInitializer behat.silverstripe_extension.context.class_guesser.class: SilverStripe\BehatExtension\Context\ClassGuesser\ModuleContextClassGuesser behat.console.processor.locator.class: SilverStripe\BehatExtension\Console\Processor\LocatorProcessor - behat.silverstripe_extension.context.class_suffix: Test\Behaviour\FeatureContext + behat.console.processor.init.class: SilverStripe\BehatExtension\Console\Processor\InitProcessor + behat.silverstripe_extension.context.namespace_suffix: Test\Behaviour behat.silverstripe_extension.framework_path: framework behat.silverstripe_extension.ajax_steps: ~ behat.silverstripe_extension.ajax_timeout: ~ @@ -27,6 +28,7 @@ services: behat.silverstripe_extension.context.class_guesser: class: %behat.silverstripe_extension.context.class_guesser.class% arguments: - - %behat.silverstripe_extension.context.class_suffix% + - %behat.silverstripe_extension.context.namespace_suffix% + - %behat.context.class% tags: - { name: behat.context.class_guesser, priority: 10 } \ No newline at end of file