Repository: Behat/Behat (php)

Issue:
Unrecognized option "extensions" under "testwork".
Hi,

What I'm trying to have is a suite that does not need extensions and executes fast.
My default suite uses Symfony extension and because it's a big app, creating the container takes a lot of time.
I have a suite "domain" of "acceptance tests" that does not rely on symfony in any way and that is really really fast so I'd like to run this suite before the other on CI. 
The fact that my "domain" suite load the extensions makes it slow.

So I tried to create a "domain" profile instead of a suite with this configuration:

```
default:
  suites:
    default: ....

  extensions:
    Behat\Symfony2Extension:
      kernel:
        bootstrap: config/bootstrap.php
        env: behat
        debug: true
        path: src/Kernel.php
        class: Kernel

domain:
  suites:
    default: ....

  extensions: ~
```

But I have this issue:

```
Unrecognized option "extensions" under "testwork". Available options are "argument", "autoload", "calls", "cli", "contexts", "definitions", "environments", "events", "exceptions", "filesystem", "formatters", "gherkin", "gherkin_translations", "helper_container", "hooks", "ordering", "snippets", "specifications", "suites", "testers", "transformations", "translation".
```

A solution I found is to rename the `default` profile to something else and remove the `extensions` key from my `domain` profile but this breaks the `default` profile.

This issue is that the `default` profile is BOTH "the default profile if no profile is specified" AND "the profile that others inherit from" :/

Is there a solution for my need that is not a second configuration file ?

If this is considered a bug that needs to fixed I can make a PR if someone guides me a little on how to implement a fix.

Thanks for your time.

Pull request:
Allow profiles to override extensions
Fixes #1339

Patch:
diff --git a/features/extension_inheritance.feature b/features/extension_inheritance.feature
new file mode 100644
index 00000000..95409849
--- /dev/null
+++ b/features/extension_inheritance.feature
@@ -0,0 +1,184 @@
+Feature: Profile extension overrides
+    In order to organize my profiles
+    As a tester
+    I need to be able to override extensions in non-default profiles
+
+  Background:
+    Given a file named "custom_extension.php" with:
+      """
+      <?php
+
+      use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+      use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+      class CustomInitializer implements Behat\Behat\Context\Initializer\ContextInitializer
+      {
+          public function supportsContext(Behat\Behat\Context\Context $context)
+          {
+              return true;
+          }
+
+          public function initializeContext(Behat\Behat\Context\Context $context)
+          {
+              $context->addExtension('custom extension 1');
+          }
+      }
+
+      class CustomExtension implements Behat\Testwork\ServiceContainer\Extension {
+          public function getConfigKey()
+          {
+              return 'custom_extension';
+          }
+
+          public function configure(ArrayNodeDefinition $builder) {}
+
+          public function initialize(Behat\Testwork\ServiceContainer\ExtensionManager $extensionManager) {}
+
+          public function load(ContainerBuilder $container, array $config)
+          {
+              $definition = $container->register('custom_initializer', 'CustomInitializer');
+              $definition->addTag('context.initializer', array('priority' => 100));
+          }
+
+          public function process(ContainerBuilder $container) {}
+      }
+
+      return new CustomExtension;
+      """
+    And a file named "custom_extension2.php" with:
+      """
+      <?php
+
+      use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+      use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+      class CustomInitializer2 implements Behat\Behat\Context\Initializer\ContextInitializer
+      {
+          public function supportsContext(Behat\Behat\Context\Context $context)
+          {
+              return true;
+          }
+
+          public function initializeContext(Behat\Behat\Context\Context $context)
+          {
+              $context->addExtension('custom extension 2');
+          }
+      }
+
+      class CustomExtension2 implements Behat\Testwork\ServiceContainer\Extension {
+          public function getConfigKey()
+          {
+              return 'custom_extension2';
+          }
+
+          public function configure(ArrayNodeDefinition $builder) {}
+
+          public function initialize(Behat\Testwork\ServiceContainer\ExtensionManager $extensionManager) {}
+
+          public function load(ContainerBuilder $container, array $config)
+          {
+              $definition = $container->register('custom_initializer2', 'CustomInitializer2');
+              $definition->addTag('context.initializer', array('priority' => 100));
+          }
+
+          public function process(ContainerBuilder $container) {}
+      }
+
+      return new CustomExtension2;
+      """
+    And a file named "features/bootstrap/FeatureContext.php" with:
+      """
+      <?php
+
+      use Behat\Behat\Context\Context;
+
+      class FeatureContext implements Context
+      {
+          private $loadedExtensions = array();
+
+          public function addExtension($extensionName) {
+              $this->loadedExtensions[] = $extensionName;
+          }
+
+          /** @When this scenario executes */
+          public function thisScenarioExecutes() {}
+
+          /** @Then the extension :name should be loaded */
+          public function theExtensionLoaded($name) {
+              PHPUnit\Framework\Assert::assertContains($name, $this->loadedExtensions);
+          }
+
+          /** @Then the extension :name should not be loaded */
+          public function theExtensionNotLoaded($name) {
+              PHPUnit\Framework\Assert::assertNotContains($name, $this->loadedExtensions);
+          }
+
+          /** @Then the extension :name should have :key set to :value */
+          public function theExtensionHas($name, $key, $value) {
+              $this->theExtensionLoaded($name);
+              $this->theExtensionNotLoaded($name);
+          }
+      }
+      """
+
+  Scenario: Default profile has extension loaded
+    Given a file named "behat.yml.dist" with:
+      """
+      default:
+        extensions:
+          custom_extension.php: ~
+
+      custom_profile:
+        extensions: ~
+      """
+    And a file named "features/extensions.feature" with:
+      """
+      Feature:
+        Scenario:
+          When this scenario executes
+          Then the extension "custom extension 1" should be loaded
+      """
+    When I run "behat -f progress"
+    Then it should pass
+
+  Scenario: Custom profile disables all extensions
+    Given a file named "behat.yml.dist" with:
+      """
+      default:
+        extensions:
+          custom_extension.php: ~
+
+      custom_profile:
+        extensions: ~
+      """
+    And a file named "features/extensions.feature" with:
+      """
+      Feature:
+        Scenario:
+          When this scenario executes
+          Then the extension "custom extension 1" should not be loaded
+      """
+    When I run "behat -f progress -p custom_profile -vvv"
+    Then it should pass
+
+  Scenario: Custom profile has an additional extension
+    Given a file named "behat.yml.dist" with:
+      """
+      default:
+        extensions:
+          custom_extension.php: ~
+
+      custom_profile:
+        extensions:
+          custom_extension2.php: ~
+      """
+    And a file named "features/extensions.feature" with:
+      """
+      Feature:
+        Scenario:
+          When this scenario executes
+          Then the extension "custom extension 1" should be loaded
+          And the extension "custom extension 2" should be loaded
+      """
+    When I run "behat -f progress -p custom_profile -vvv"
+    Then it should pass


Test patch:
diff --git a/src/Behat/Testwork/ServiceContainer/ContainerLoader.php b/src/Behat/Testwork/ServiceContainer/ContainerLoader.php
index 5c343a6e..0c7b7f24 100644
--- a/src/Behat/Testwork/ServiceContainer/ContainerLoader.php
+++ b/src/Behat/Testwork/ServiceContainer/ContainerLoader.php
@@ -91,14 +91,28 @@ final class ContainerLoader
      */
     private function initializeExtensions(ContainerBuilder $container, array $configs)
     {
+        $extensions = [];
         foreach ($configs as $i => $config) {
-            if (isset($config['extensions'])) {
+            $extensions[$i] = [];
+            if (array_key_exists('extensions', $config)) {
+                if (null === $config['extensions']) {
+                    $extensions = []; // Disable all extensions
+                    break;
+                }
                 foreach ($config['extensions'] as $extensionLocator => $extensionConfig) {
-                    $extension = $this->extensionManager->activateExtension($extensionLocator);
-                    $configs[$i][$extension->getConfigKey()] = $extensionConfig;
+                    $extensions[$i][$extensionLocator] = $extensionConfig;
                 }
+            }
+        }
+
+        foreach ($configs as $i => $config) {
+            unset($configs[$i]['extensions']);
+        }
 
-                unset($configs[$i]['extensions']);
+        foreach ($extensions as $i => $extensionConfigs) {
+            foreach ($extensionConfigs as $extensionLocator => $extensionConfig) {
+                $extension = $this->extensionManager->activateExtension($extensionLocator);
+                $configs[$i][$extension->getConfigKey()] = $extensionConfig;
             }
         }