Passed
Push — master ( c32af1...923c61 )
by ANTHONIUS
04:29
created

ApplicationContext   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 463
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 52
eloc 124
dl 0
loc 463
rs 7.44
c 0
b 0
f 0

How to fix   Complexity   

Complex Class

Complex classes like ApplicationContext often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApplicationContext, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the doyo/code-coverage project.
5
 *
6
 * (c) Anthonius Munthi <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Doyo\PhpSpec\CodeCoverage\Context;
15
16
if (version_compare(PHP_VERSION, '7.1', '>=')) {
17
    include __DIR__.'/../Resources/Fake/ReRunner.php';
18
    include __DIR__.'/../Resources/Fake/Prompter.php';
19
} else {
20
    include __DIR__.'/../Resources/Fake70/ReRunner.php';
21
    include __DIR__.'/../Resources/Fake70/Prompter.php';
22
}
23
24
use Behat\Behat\Context\Context;
25
use Behat\Behat\Hook\Scope\ScenarioScope;
26
use Behat\Gherkin\Node\PyStringNode;
27
use Doyo\Bridge\CodeCoverage\Context\ContainerContext;
28
use Fake\Prompter;
29
use Fake\ReRunner;
30
use PhpSpec\Console\Application;
31
use PhpSpec\Loader\StreamWrapper;
32
use Symfony\Component\Console\Tester\ApplicationTester;
33
34
/**
35
 * Defines application features from the specific context.
36
 */
37
class ApplicationContext implements Context
38
{
39
    const JUNIT_XSD_PATH = '/src/PhpSpec/Resources/schema/junit.xsd';
40
41
    /**
42
     * @var Application
43
     */
44
    private $application;
45
46
    /**
47
     * @var int
48
     */
49
    private $lastExitCode;
50
51
    /**
52
     * @var ApplicationTester
53
     */
54
    private $tester;
55
56
    /**
57
     * @var Prompter
58
     */
59
    private $prompter;
60
61
    /**
62
     * @var ReRunner
63
     */
64
    private $reRunner;
65
66
    /**
67
     * @var ContainerContext
68
     */
69
    private $containerContext;
70
71
    /**
72
     * @beforeScenario
73
     */
74
    public function setupApplication(ScenarioScope $scope)
75
    {
76
        StreamWrapper::register();
77
78
        $this->application = new Application('2.1-dev');
79
        $this->application->setAutoExit(false);
80
        $this->setFixedTerminalDimensions();
81
82
        $this->tester = new ApplicationTester($this->application);
83
84
        $this->setupReRunner();
85
        $this->setupPrompter();
86
87
        $this->containerContext = $scope->getEnvironment()->getContext(ContainerContext::class);
88
    }
89
90
    private function setFixedTerminalDimensions()
91
    {
92
        putenv('COLUMNS=130');
93
        putenv('LINES=30');
94
    }
95
96
    private function setupPrompter()
97
    {
98
        $this->prompter = new Prompter();
99
100
        $this->application->getContainer()->set('console.prompter', $this->prompter);
101
    }
102
103
    private function setupReRunner()
104
    {
105
        $this->reRunner = new ReRunner();
106
        $this->application->getContainer()->set('process.rerunner.platformspecific', $this->reRunner);
107
    }
108
109
    /**
110
     * @When I run phpspec (non interactively)
111
     * @When I run phpspec using the :formatter format
112
     * @When I run phpspec with the :option option
113
     * @When I run phpspec with :spec specs to run
114
     * @When /I run phpspec with option (?P<option>.*)/
115
     * @When /I run phpspec (?P<interactive>interactively)$/
116
     * @When /I run phpspec (?P<interactive>interactively) with the (?P<option>.*) option/
117
     *
118
     * @param mixed|null $formatter
119
     * @param mixed|null $option
120
     * @param mixed|null $interactive
121
     * @param mixed|null $spec
122
     */
123
    public function iRunPhpspec($formatter = null, $option = null, $interactive = null, $spec = null)
124
    {
125
        $arguments = [
126
            'command' => 'run',
127
            'spec'    => $spec,
128
        ];
129
130
        if ($formatter) {
131
            $arguments['--format'] = $formatter;
132
        }
133
134
        $this->addOptionToArguments($option, $arguments);
135
136
        $this->lastExitCode = $this->tester->run($arguments, [
137
            'interactive' => (bool) $interactive,
138
            'decorated'   => false,
139
        ]);
140
141
        $container = $this->application->getContainer();
142
        $this->containerContext->setContainer($container);
143
    }
144
145
    /**
146
     * @Given I have started describing the :class class
147
     * @Given I start describing the :class class
148
     *
149
     * @param mixed $class
150
     */
151
    public function iDescribeTheClass($class)
152
    {
153
        $arguments = [
154
            'command' => 'describe',
155
            'class'   => $class,
156
        ];
157
158
        if (0 !== $this->tester->run($arguments, ['interactive' => false])) {
159
            throw new \Exception('Test runner exited with an error');
160
        }
161
    }
162
163
    /**
164
     * @When I run phpspec and answer :answer when asked if I want to generate the code
165
     * @When I run phpspec with the option :option and (I) answer :answer when asked if I want to generate the code
166
     *
167
     * @param mixed      $answer
168
     * @param mixed|null $option
169
     */
170
    public function iRunPhpspecAndAnswerWhenAskedIfIWantToGenerateTheCode($answer, $option=null)
171
    {
172
        $this->runPhpSpecAndAnswerQuestions($answer, 1, $option);
173
    }
174
175
    /**
176
     * @When I run phpspec and answer :answer to (the) :amount questions
177
     *
178
     * @param mixed $amount
179
     * @param mixed $answer
180
     */
181
    public function iRunPhpspecAndAnswerToBothQuestions($amount, $answer)
182
    {
183
        $this->runPhpSpecAndAnswerQuestions($answer, ('both' === $amount ? 2 : 3));
184
    }
185
186
    /**
187
     * @param string $answer
188
     * @param int    $times
189
     * @param string $option
190
     */
191
    private function runPhpSpecAndAnswerQuestions($answer, $times, $option = null)
192
    {
193
        $arguments = [
194
            'command' => 'run',
195
        ];
196
197
        $this->addOptionToArguments($option, $arguments);
198
199
        $i = 0;
200
        while ($i++ < $times) {
201
            $this->prompter->setAnswer('y' === $answer);
202
        }
203
204
        $this->lastExitCode = $this->tester->run($arguments, ['interactive' => true]);
205
    }
206
207
    /**
208
     * @param string $option
209
     * @param array  $arguments
210
     */
211
    private function addOptionToArguments($option, array &$arguments)
212
    {
213
        if ($option) {
214
            if (preg_match('/(?P<option>[a-z-]+)=(?P<value>[a-z.]+)/', $option, $matches)) {
215
                $arguments[$matches['option']] = $matches['value'];
216
            } else {
217
                $arguments['--'.trim($option, '"')] = true;
218
            }
219
        }
220
    }
221
222
    /**
223
     * @Then I should see :output
224
     * @Then I should see:
225
     *
226
     * @param mixed $output
227
     */
228
    public function iShouldSee($output)
229
    {
230
        $this->checkApplicationOutput((string) $output);
231
    }
232
233
    /**
234
     * @Then I should be prompted for code generation
235
     */
236
    public function iShouldBePromptedForCodeGeneration()
237
    {
238
        if (!$this->prompter->hasBeenAsked()) {
239
            throw new \Exception('There was a missing prompt for code generation');
240
        }
241
    }
242
243
    /**
244
     * @Then I should see the error that :methodCall was not expected on :class
245
     *
246
     * @param mixed $methodCall
247
     * @param mixed $class
248
     */
249
    public function iShouldSeeTheErrorThatWasNotExpectedOn($methodCall, $class)
250
    {
251
        $this->checkApplicationOutput((string) $methodCall);
252
        $this->checkApplicationOutput((string) $this->normalize($class));
253
254
        $output = $this->tester->getDisplay();
255
256
        $containsOldProphecyMessage = false !== strpos($output, 'was not expected');
257
        $containsNewProphecyMessage = false !== strpos($output, 'Unexpected method call');
258
259
        if (!$containsOldProphecyMessage && !$containsNewProphecyMessage) {
260
            throw new \Exception('Was expecting error message about an unexpected method call');
261
        }
262
    }
263
264
    /**
265
     * @Then I should not be prompted for code generation
266
     */
267
    public function iShouldNotBePromptedForCodeGeneration()
268
    {
269
        if ($this->prompter->hasBeenAsked()) {
270
            throw new \Exception('There was an unexpected prompt for code generation');
271
        }
272
    }
273
274
    /**
275
     * @Then the suite should pass
276
     */
277
    public function theSuiteShouldPass()
278
    {
279
        $this->theExitCodeShouldBe(0);
280
    }
281
282
    /**
283
     * @Then the suite should not pass
284
     */
285
    public function theSuiteShouldNotPass()
286
    {
287
        if (0 === $this->lastExitCode) {
288
            throw new \Exception('The application did not exit with an error code');
289
        }
290
    }
291
292
    /**
293
     * @Then :number example(s) should have been skipped
294
     *
295
     * @param mixed $number
296
     */
297
    public function exampleShouldHaveBeenSkipped($number)
298
    {
299
        $this->checkApplicationOutput("($number skipped)");
300
    }
301
302
    /**
303
     * @Then :number example(s) should have been run
304
     *
305
     * @param mixed $number
306
     */
307
    public function examplesShouldHaveBeenRun($number)
308
    {
309
        $this->checkApplicationOutput("$number examples");
310
    }
311
312
    /**
313
     * @Then the exit code should be :code
314
     *
315
     * @param mixed $code
316
     */
317
    public function theExitCodeShouldBe($code)
318
    {
319
        if ($this->lastExitCode !== (int) $code) {
320
            throw new \Exception(sprintf(
321
                'The application existed with an unexpected code: expected: %s, actual: %s',
322
                $code,
323
                $this->lastExitCode
324
            ));
325
        }
326
    }
327
328
    /**
329
     * @Then I should see valid junit output
330
     */
331
    public function iShouldSeeValidJunitOutput()
332
    {
333
        $dom = new \DOMDocument();
334
        $dom->loadXML($this->tester->getDisplay());
335
        if (!$dom->schemaValidate(__DIR__.'/../..'.self::JUNIT_XSD_PATH)) {
336
            throw new \Exception(sprintf(
337
                'Output was not valid JUnit XML'
338
            ));
339
        }
340
    }
341
342
    /**
343
     * @Then the tests should be rerun
344
     */
345
    public function theTestsShouldBeRerun()
346
    {
347
        if (!$this->reRunner->hasBeenReRun()) {
348
            throw new \Exception('The tests should have been rerun');
349
        }
350
    }
351
352
    /**
353
     * @Then the tests should not be rerun
354
     */
355
    public function theTestsShouldNotBeRerun()
356
    {
357
        if ($this->reRunner->hasBeenReRun()) {
358
            throw new \Exception('The tests should not have been rerun');
359
        }
360
    }
361
362
    /**
363
     * @Then I should be prompted with:
364
     */
365
    public function iShouldBePromptedWith(PyStringNode $question)
366
    {
367
        $stringQuestion = (string) $question;
368
        if (!$this->prompter->hasBeenAsked($stringQuestion)) {
369
            throw new \Exception("The prompt was not shown: $stringQuestion");
370
        }
371
    }
372
373
    /**
374
     * @Given I have started describing the :class class with the :config (custom) config
375
     * @Given I start describing the :class class with the :config (custom) config
376
     *
377
     * @param mixed $class
378
     * @param mixed $config
379
     */
380
    public function iDescribeTheClassWithTheConfig($class, $config)
381
    {
382
        $arguments = [
383
            'command'  => 'describe',
384
            'class'    => $class,
385
            '--config' => $config,
386
        ];
387
388
        if (0 !== $this->tester->run($arguments, ['interactive' => false])) {
389
            throw new \Exception('Test runner exited with an error');
390
        }
391
    }
392
393
    /**
394
     * @Given there is a PSR-:namespaceType namespace :namespace configured for the :source folder
395
     *
396
     * @param mixed $namespaceType
397
     * @param mixed $namespace
398
     * @param mixed $source
399
     */
400
    public function thereIsAPsrNamespaceConfiguredForTheFolder($namespaceType, $namespace, $source)
401
    {
402
        if (!is_dir(__DIR__.'/src')) {
403
            mkdir(__DIR__.'/src');
404
        }
405
        require_once __DIR__.'/autoloader/fake_autoload.php';
406
    }
407
408
    /**
409
     * @When I run phpspec with the :config (custom) config and answer :answer when asked if I want to generate the code
410
     *
411
     * @param mixed $config
412
     * @param mixed $answer
413
     */
414
    public function iRunPhpspecWithConfigAndAnswerIfIWantToGenerateTheCode($config, $answer)
415
    {
416
        $arguments = [
417
            'command'  => 'run',
418
            '--config' => $config,
419
        ];
420
421
        $this->prompter->setAnswer('y' === $answer);
422
423
        $this->lastExitCode = $this->tester->run($arguments, ['interactive' => true]);
424
    }
425
426
    /**
427
     * @When I run phpspec with the spec :spec
428
     *
429
     * @param mixed $spec
430
     */
431
    public function iRunPhpspecWithTheSpec($spec)
432
    {
433
        $arguments = [
434
            'command' => 'run',
435
            1         => $spec,
436
        ];
437
438
        $this->lastExitCode = $this->tester->run($arguments, ['interactive' => false]);
439
    }
440
441
    /**
442
     * @When I run phpspec with the spec :spec and the config :config
443
     *
444
     * @param mixed $spec
445
     * @param mixed $config
446
     */
447
    public function iRunPhpspecWithTheSpecAndTheConfig($spec, $config)
448
    {
449
        $arguments = [
450
            'command'  => 'run',
451
            1          => $spec,
452
            '--config' => $config,
453
        ];
454
455
        $this->lastExitCode = $this->tester->run($arguments, ['interactive' => false]);
456
    }
457
458
    private function checkApplicationOutput($output)
459
    {
460
        $expected = $this->normalize($output);
461
        $actual   = $this->normalize($this->tester->getDisplay(true));
462
        if (false === strpos($actual, $expected)) {
463
            throw new \Exception(sprintf(
464
                "Application output did not contain expected '%s'. Actual output:\n'%s'",
465
                $expected,
466
                $this->tester->getDisplay()
467
            ));
468
        }
469
    }
470
471
    private function normalize($string)
472
    {
473
        $string = preg_replace('/\([0-9]+ms\)/', '', $string);
474
        $string = str_replace("\r", '', $string);
475
476
        return preg_replace('#(Double\\\\.+?\\\\P)\d+#u', '$1', $string);
477
    }
478
479
    /**
480
     * @Then I should not be prompted for more questions
481
     */
482
    public function iShouldNotBePromptedForMoreQuestions()
483
    {
484
        if ($this->prompter->hasUnansweredQuestions()) {
485
            throw new \Exception(
486
                'Not all questions were answered. This might lead into further code generation not reflected in the scenario.'
487
            );
488
        }
489
    }
490
491
    /**
492
     * @Then I should an error about invalid class name :className to generate spec for
493
     *
494
     * @param mixed $className
495
     */
496
    public function iShouldAnErrorAboutImpossibleSpecGenerationForClass($className)
497
    {
498
        $this->checkApplicationOutput("I cannot generate spec for '$className' because class");
499
        $this->checkApplicationOutput('name contains reserved keyword');
500
    }
501
}
502