--- /dev/null
+<?php
+
+/*
+ * This file is part of the Behat Testwork.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Behat\Testwork\Tester\Cli;
+
+use Behat\Testwork\Cli\Controller;
+use Behat\Testwork\Specification\SpecificationFinder;
+use Behat\Testwork\Specification\SpecificationIterator;
+use Behat\Testwork\Suite\Suite;
+use Behat\Testwork\Suite\SuiteRepository;
+use Behat\Testwork\Tester\Exception\WrongPathsException;
+use Behat\Testwork\Tester\Exercise;
+use Behat\Testwork\Tester\Result\IntegerTestResult;
+use Behat\Testwork\Tester\Result\ResultInterpreter;
+use Behat\Testwork\Tester\Result\TestResult;
+use Behat\Testwork\Tester\Result\TestResults;
+use Behat\Testwork\Tester\Result\TestWithSetupResult;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Executes exercise.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+final class ExerciseController implements Controller
+{
+ /**
+ * @var SuiteRepository
+ */
+ private $suiteRepository;
+ /**
+ * @var SpecificationFinder
+ */
+ private $specificationFinder;
+ /**
+ * @var Exercise
+ */
+ private $exercise;
+ /**
+ * @var ResultInterpreter
+ */
+ private $resultInterpreter;
+ /**
+ * @var Boolean
+ */
+ private $skip;
+
+ /**
+ * Initializes controller.
+ *
+ * @param SuiteRepository $suiteRepository
+ * @param SpecificationFinder $specificationFinder
+ * @param Exercise $exercise
+ * @param ResultInterpreter $resultInterpreter
+ * @param Boolean $skip
+ */
+ public function __construct(
+ SuiteRepository $suiteRepository,
+ SpecificationFinder $specificationFinder,
+ Exercise $exercise,
+ ResultInterpreter $resultInterpreter,
+ $skip = false
+ ) {
+ $this->suiteRepository = $suiteRepository;
+ $this->specificationFinder = $specificationFinder;
+ $this->exercise = $exercise;
+ $this->resultInterpreter = $resultInterpreter;
+ $this->skip = $skip;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configure(Command $command)
+ {
+ $locatorsExamples = implode(PHP_EOL, array_map(
+ function ($locator) {
+ return '- ' . $locator;
+ }, $this->specificationFinder->getExampleLocators()
+ ));
+
+ $command
+ ->addArgument('paths', InputArgument::OPTIONAL,
+ 'Optional path(s) to execute. Could be:' . PHP_EOL . $locatorsExamples
+ )
+ ->addOption('--dry-run', null, InputOption::VALUE_NONE,
+ 'Invokes formatters without executing the tests and hooks.'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute(InputInterface $input, OutputInterface $output)
+ {
+ $specs = $this->findSpecifications($input);
+ $result = $this->testSpecifications($input, $specs);
+
+ if ($input->getArgument('paths') && TestResults::NO_TESTS === $result->getResultCode()) {
+ throw new WrongPathsException(
+ sprintf(
+ 'No specifications found at path(s) `%s`. This might be because of incorrect paths configuration in your `suites`.',
+ $input->getArgument('paths')
+ ),
+ $input->getArgument('paths')
+ );
+ }
+
+ return $this->resultInterpreter->interpretResult($result);
+ }
+
+ /**
+ * Finds exercise specifications.
+ *
+ * @param InputInterface $input
+ *
+ * @return SpecificationIterator[]
+ */
+ private function findSpecifications(InputInterface $input)
+ {
+ return $this->findSuitesSpecifications($this->getAvailableSuites(), $input->getArgument('paths'));
+ }
+
+ /**
+ * Tests exercise specifications.
+ *
+ * @param InputInterface $input
+ * @param SpecificationIterator[] $specifications
+ *
+ * @return TestResult
+ */
+ private function testSpecifications(InputInterface $input, array $specifications)
+ {
+ $skip = $input->getOption('dry-run') || $this->skip;
+
+ $setup = $this->exercise->setUp($specifications, $skip);
+ $skip = !$setup->isSuccessful() || $skip;
+ $testResult = $this->exercise->test($specifications, $skip);
+ $teardown = $this->exercise->tearDown($specifications, $skip, $testResult);
+
+ $result = new IntegerTestResult($testResult->getResultCode());
+
+ return new TestWithSetupResult($setup, $result, $teardown);
+ }
+
+ /**
+ * Returns all currently available suites.
+ *
+ * @return Suite[]
+ */
+ private function getAvailableSuites()
+ {
+ return $this->suiteRepository->getSuites();
+ }
+
+ /**
+ * Finds specification iterators for all provided suites using locator.
+ *
+ * @param Suite[] $suites
+ * @param null|string $locator
+ *
+ * @return SpecificationIterator[]
+ */
+ private function findSuitesSpecifications($suites, $locator)
+ {
+ return $this->specificationFinder->findSuitesSpecifications($suites, $locator);
+ }
+}