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
![]() |
|||
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 |