hanneskod /
readme-tester
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace hanneskod\readmetester; |
||
| 6 | |||
| 7 | use hanneskod\readmetester\Config\Configs; |
||
| 8 | use hanneskod\readmetester\Exception\InvalidInputException; |
||
| 9 | use Crell\Tukio\OrderedProviderInterface; |
||
| 10 | use Psr\EventDispatcher\EventDispatcherInterface; |
||
| 11 | use Symfony\Component\Console\Command\Command; |
||
| 12 | use Symfony\Component\Console\Input\InputArgument; |
||
| 13 | use Symfony\Component\Console\Input\InputOption; |
||
| 14 | use Symfony\Component\Console\Input\InputInterface; |
||
| 15 | use Symfony\Component\Console\Output\OutputInterface; |
||
| 16 | |||
| 17 | final class Application |
||
| 18 | { |
||
| 19 | private const CONFIG_FILE_OPTION = 'config'; |
||
| 20 | private const NO_CONFIG_OPTION = 'no-config'; |
||
| 21 | private const STDIN_OPTION = 'stdin'; |
||
| 22 | private const SUITE_OPTION = 'suite'; |
||
| 23 | private const PATHS_ARGUMENT = 'path'; |
||
| 24 | private const INPUT_OPTION = 'input'; |
||
| 25 | private const OUTPUT_OPTION = 'output'; |
||
| 26 | private const DEBUG_OPTION = 'debug'; |
||
| 27 | private const RUNNER_OPTION = 'runner'; |
||
| 28 | private const FILE_EXTENSIONS_OPTION = 'file-extension'; |
||
| 29 | private const IGNORE_PATHS_OPTION = 'exclude'; |
||
| 30 | private const BOOTSTRAP_OPTION = 'bootstrap'; |
||
| 31 | private const NO_BOOTSTRAP_OPTION = 'no-bootstrap'; |
||
| 32 | private const STOP_ON_FAILURE_OPTION = 'stop-on-failure'; |
||
| 33 | private const FILTER_OPTION = 'filter'; |
||
| 34 | |||
| 35 | public function __construct( |
||
| 36 | private Config\ConfigManager $configManager, |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 37 | private Event\ExitStatusListener $exitStatusListener, |
||
| 38 | private ExampleProviderInterface $exampleProvider, |
||
| 39 | private Runner\RunnerFactory $runnerFactory, |
||
| 40 | private TestMarshal $testMarshal, |
||
| 41 | private EventDispatcherInterface $dispatcher, |
||
| 42 | private OrderedProviderInterface $listenerProvider, |
||
| 43 | ) {} |
||
| 44 | |||
| 45 | public function configure(Command $command): void |
||
| 46 | { |
||
| 47 | $command |
||
| 48 | ->addArgument( |
||
| 49 | self::PATHS_ARGUMENT, |
||
| 50 | InputArgument::OPTIONAL | InputArgument::IS_ARRAY, |
||
| 51 | 'One or more paths to scan for test files' |
||
| 52 | ) |
||
| 53 | ->addOption( |
||
| 54 | self::CONFIG_FILE_OPTION, |
||
| 55 | 'c', |
||
| 56 | InputOption::VALUE_REQUIRED, |
||
| 57 | 'Read configurations from file' |
||
| 58 | ) |
||
| 59 | ->addOption( |
||
| 60 | self::NO_CONFIG_OPTION, |
||
| 61 | null, |
||
| 62 | InputOption::VALUE_NONE, |
||
| 63 | 'Do not load a configuration file' |
||
| 64 | ) |
||
| 65 | ->addOption( |
||
| 66 | self::SUITE_OPTION, |
||
| 67 | null, |
||
| 68 | InputOption::VALUE_REQUIRED, |
||
| 69 | 'Execute named suite' |
||
| 70 | ) |
||
| 71 | ->addOption( |
||
| 72 | self::FILE_EXTENSIONS_OPTION, |
||
| 73 | null, |
||
| 74 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
||
| 75 | 'File extension to use while scanning for test files' |
||
| 76 | ) |
||
| 77 | ->addOption( |
||
| 78 | self::IGNORE_PATHS_OPTION, |
||
| 79 | null, |
||
| 80 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
||
| 81 | 'Path to ignore while scanning for test files' |
||
| 82 | ) |
||
| 83 | ->addOption( |
||
| 84 | self::STDIN_OPTION, |
||
| 85 | null, |
||
| 86 | InputOption::VALUE_NONE, |
||
| 87 | 'Read from stdin instead of scaning the filesystem' |
||
| 88 | ) |
||
| 89 | ->addOption( |
||
| 90 | self::OUTPUT_OPTION, |
||
| 91 | null, |
||
| 92 | InputOption::VALUE_REQUIRED, |
||
| 93 | 'Set output format (' . Configs::describe(Configs::OUTPUT_ID) . ')' |
||
| 94 | ) |
||
| 95 | ->addOption( |
||
| 96 | self::DEBUG_OPTION, |
||
| 97 | null, |
||
| 98 | InputOption::VALUE_NONE, |
||
| 99 | 'View debug info on generated examples (shorthand for --output debug)' |
||
| 100 | ) |
||
| 101 | ->addOption( |
||
| 102 | self::INPUT_OPTION, |
||
| 103 | null, |
||
| 104 | InputOption::VALUE_REQUIRED, |
||
| 105 | 'Set input format (' . Configs::describe(Configs::INPUT_ID) . ')' |
||
| 106 | ) |
||
| 107 | ->addOption( |
||
| 108 | self::RUNNER_OPTION, |
||
| 109 | null, |
||
| 110 | InputOption::VALUE_REQUIRED, |
||
| 111 | 'Set example runner (' . Configs::describe(Configs::RUNNER_ID) . ')' |
||
| 112 | ) |
||
| 113 | ->addOption( |
||
| 114 | self::BOOTSTRAP_OPTION, |
||
| 115 | null, |
||
| 116 | InputOption::VALUE_REQUIRED, |
||
| 117 | 'A "bootstrap" PHP file that is included before testing' |
||
| 118 | ) |
||
| 119 | ->addOption( |
||
| 120 | self::NO_BOOTSTRAP_OPTION, |
||
| 121 | null, |
||
| 122 | InputOption::VALUE_NONE, |
||
| 123 | "Ignore bootstrapping" |
||
| 124 | ) |
||
| 125 | ->addOption( |
||
| 126 | self::STOP_ON_FAILURE_OPTION, |
||
| 127 | 's', |
||
| 128 | InputOption::VALUE_NONE, |
||
| 129 | "Stop processing on first failed test" |
||
| 130 | ) |
||
| 131 | ->addOption( |
||
| 132 | self::FILTER_OPTION, |
||
| 133 | null, |
||
| 134 | InputOption::VALUE_REQUIRED, |
||
| 135 | "Filter which examples to test" |
||
| 136 | ) |
||
| 137 | ; |
||
| 138 | } |
||
| 139 | |||
| 140 | public function __invoke(InputInterface $input, OutputInterface $output): int |
||
| 141 | { |
||
| 142 | // Setup configuration from config file |
||
| 143 | |||
| 144 | if (!$input->getOption(self::NO_CONFIG_OPTION)) { |
||
| 145 | if ($input->getOption(self::CONFIG_FILE_OPTION)) { |
||
| 146 | $this->configManager->loadRepository( |
||
| 147 | // @phpstan-ignore-next-line |
||
| 148 | new Config\YamlRepository((string)$input->getOption(self::CONFIG_FILE_OPTION)) |
||
| 149 | ); |
||
| 150 | } else { |
||
| 151 | $this->configManager->loadRepository(new Config\UserConfigRepository()); |
||
| 152 | } |
||
| 153 | } |
||
| 154 | |||
| 155 | // Setup configuration from command line |
||
| 156 | |||
| 157 | $this->configManager->loadRepository( |
||
| 158 | new Config\ArrayRepository([Configs::CLI => array_filter([ |
||
| 159 | Configs::INCLUDE_PATHS => (array)$input->getArgument(self::PATHS_ARGUMENT), |
||
| 160 | Configs::FILE_EXTENSIONS => (array)$input->getOption(self::FILE_EXTENSIONS_OPTION), |
||
| 161 | Configs::EXCLUDE_PATHS => (array)$input->getOption(self::IGNORE_PATHS_OPTION), |
||
| 162 | Configs::BOOTSTRAP => (string)$input->getOption(self::BOOTSTRAP_OPTION), // @phpstan-ignore-line |
||
| 163 | Configs::OUTPUT => $input->getOption(self::DEBUG_OPTION) |
||
| 164 | ? 'debug' |
||
| 165 | : (string)$input->getOption(self::OUTPUT_OPTION), // @phpstan-ignore-line |
||
| 166 | Configs::INPUT_LANGUAGE => (string)$input->getOption(self::INPUT_OPTION), // @phpstan-ignore-line |
||
| 167 | Configs::RUNNER => (string)$input->getOption(self::RUNNER_OPTION), // @phpstan-ignore-line |
||
| 168 | Configs::STOP_ON_FAILURE => (bool)$input->getOption(self::STOP_ON_FAILURE_OPTION), |
||
| 169 | Configs::FILTER => (string)$input->getOption(self::FILTER_OPTION), // @phpstan-ignore-line |
||
| 170 | Configs::STDIN => (bool)$input->getOption(self::STDIN_OPTION), |
||
| 171 | ])]) |
||
| 172 | ); |
||
| 173 | |||
| 174 | // Create bootstrap |
||
| 175 | |||
| 176 | $bootstrap = ''; |
||
| 177 | |||
| 178 | if (!$input->getOption(self::NO_BOOTSTRAP_OPTION)) { |
||
| 179 | $bootstrap = $this->configManager->getBootstrap(); |
||
| 180 | |||
| 181 | if ($bootstrap) { |
||
| 182 | require_once $bootstrap; |
||
| 183 | } |
||
| 184 | } |
||
| 185 | |||
| 186 | // Setup event subscribers |
||
| 187 | |||
| 188 | foreach ($this->configManager->getSubscribers() as $subscriber) { |
||
| 189 | if (!$subscriber) { |
||
| 190 | continue; |
||
| 191 | } |
||
| 192 | |||
| 193 | if (!class_exists($subscriber)) { |
||
| 194 | throw new \RuntimeException("Unknown subscriber '$subscriber', class does not exist"); |
||
| 195 | } |
||
| 196 | |||
| 197 | $this->listenerProvider->addSubscriber($subscriber, $subscriber); |
||
| 198 | } |
||
| 199 | |||
| 200 | // From here convert RuntimeExceptions to ErrorEvents (after subscribers have been loaded) |
||
| 201 | |||
| 202 | try { |
||
| 203 | return $this->execute($input, $output, $bootstrap); |
||
| 204 | } catch (\RuntimeException $exception) { |
||
| 205 | $this->dispatcher->dispatch(new Event\ErrorEvent($exception)); |
||
| 206 | } |
||
| 207 | |||
| 208 | return 1; |
||
| 209 | } |
||
| 210 | |||
| 211 | private function execute(InputInterface $input, OutputInterface $output, string $bootstrap): int |
||
| 212 | { |
||
| 213 | // Dispatch events |
||
| 214 | |||
| 215 | $this->dispatcher->dispatch(new Event\ExecutionStarted($output)); |
||
| 216 | |||
| 217 | if ($bootstrap) { |
||
| 218 | $this->dispatcher->dispatch(new Event\BootstrapIncluded($bootstrap)); |
||
| 219 | } |
||
| 220 | |||
| 221 | foreach ($this->configManager->getLoadedRepositoryNames() as $name) { |
||
| 222 | $this->dispatcher->dispatch(new Event\ConfigurationIncluded($name)); |
||
| 223 | } |
||
| 224 | |||
| 225 | // Execute suites |
||
| 226 | |||
| 227 | if ($input->getOption(self::SUITE_OPTION)) { |
||
| 228 | $this->executeSuite( |
||
| 229 | // @phpstan-ignore-next-line |
||
| 230 | $this->configManager->getSuite((string)$input->getOption(self::SUITE_OPTION)), |
||
| 231 | $bootstrap |
||
| 232 | ); |
||
| 233 | } else { |
||
| 234 | foreach ($this->configManager->getAllSuites() as $suite) { |
||
| 235 | $this->executeSuite($suite, $bootstrap); |
||
| 236 | } |
||
| 237 | } |
||
| 238 | |||
| 239 | // Done |
||
| 240 | |||
| 241 | $this->dispatcher->dispatch(new Event\ExecutionStopped()); |
||
| 242 | |||
| 243 | return $this->exitStatusListener->getStatusCode(); |
||
| 244 | } |
||
| 245 | |||
| 246 | private function executeSuite(Config\Suite $suite, string $bootstrap): void |
||
| 247 | { |
||
| 248 | $this->dispatcher->dispatch(new Event\SuiteStarted($suite)); |
||
| 249 | |||
| 250 | // Create runner |
||
| 251 | |||
| 252 | $runner = $this->runnerFactory->createRunner($suite->getRunner()); |
||
| 253 | |||
| 254 | $runner->setBootstrap($bootstrap); |
||
| 255 | |||
| 256 | $this->dispatcher->dispatch( |
||
| 257 | new Event\DebugEvent("Using runner {$suite->getRunner()}") |
||
| 258 | ); |
||
| 259 | |||
| 260 | // Execute tests |
||
| 261 | |||
| 262 | try { |
||
| 263 | $this->testMarshal->test( |
||
| 264 | $this->exampleProvider->getExamplesForSuite($suite), |
||
| 265 | $runner, |
||
| 266 | $suite->stopOnFailure() |
||
| 267 | ); |
||
| 268 | } catch (InvalidInputException $exception) { |
||
| 269 | $this->dispatcher->dispatch(new Event\InvalidInput($exception)); |
||
| 270 | } |
||
| 271 | |||
| 272 | $this->dispatcher->dispatch(new Event\SuiteDone($suite)); |
||
| 273 | } |
||
| 274 | } |
||
| 275 |