Issues (83)

src/Application.php (1 issue)

Labels
Severity
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
A parse error occurred: Syntax error, unexpected T_PRIVATE, expecting T_VARIABLE on line 36 at column 8
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