Completed
Push — master ( bbfb28...e849e1 )
by Hannes
01:48
created

TestCommand   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 90
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 16
lcom 1
cbo 8
dl 0
loc 90
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
B configure() 0 30 1
D execute() 0 33 9
B bootstrap() 0 17 6
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace hanneskod\readmetester\Console;
6
7
use Symfony\Component\Console\Command\Command;
8
use Symfony\Component\Console\Input\InputArgument;
9
use Symfony\Component\Console\Input\InputOption;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use hanneskod\readmetester\ReadmeTester;
13
use hanneskod\readmetester\SourceFileIterator;
14
use hanneskod\readmetester\Expectation\Regexp;
15
16
/**
17
 * CLI command to run test
18
 */
19
class TestCommand extends Command
20
{
21
    /**
22
     * Path to default bootstrap file
23
     */
24
    const DEFAULT_BOOTSTRAP = 'vendor/autoload.php';
25
26
    protected function configure()
27
    {
28
        $this->setName('test')
29
            ->setDescription('Test examples in readme file')
30
            ->addArgument(
31
                'source',
32
                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
33
                'One or more files or directories to test',
34
                ['README.md']
35
            )
36
            ->addOption(
37
                'filter',
38
                null,
39
                InputOption::VALUE_REQUIRED,
40
                'Filter which examples to test using a regular expression'
41
            )
42
            ->addOption(
43
                'bootstrap',
44
                null,
45
                InputOption::VALUE_REQUIRED,
46
                'A "bootstrap" PHP file that is run before testing'
47
            )
48
            ->addOption(
49
                'no-auto-bootstrap',
50
                null,
51
                InputOption::VALUE_NONE,
52
                "Don't try to load a local composer autoloader when boostrap is not definied"
53
            )
54
        ;
55
    }
56
57
    protected function execute(InputInterface $input, OutputInterface $output): int
58
    {
59
        if ($output->isVeryVerbose()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method isVeryVerbose() does only exist in the following implementations of said interface: Symfony\Component\Console\Output\BufferedOutput, Symfony\Component\Console\Output\ConsoleOutput, Symfony\Component\Console\Output\NullOutput, Symfony\Component\Console\Output\Output, Symfony\Component\Console\Output\StreamOutput, Symfony\Component\Consol...ts\Fixtures\DummyOutput, Symfony\Component\Console\Tests\Output\TestOutput.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
60
            $presenter = new VeryVerbosePresenter($output);
61
        } elseif ($output->isVerbose()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Output\OutputInterface as the method isVerbose() does only exist in the following implementations of said interface: Symfony\Component\Console\Output\BufferedOutput, Symfony\Component\Console\Output\ConsoleOutput, Symfony\Component\Console\Output\NullOutput, Symfony\Component\Console\Output\Output, Symfony\Component\Console\Output\StreamOutput, Symfony\Component\Consol...ts\Fixtures\DummyOutput, Symfony\Component\Console\Tests\Output\TestOutput.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
62
            $presenter = new VerbosePresenter($output);
63
        } else {
64
            $presenter = new Presenter($output);
65
        }
66
67
        $presenter->begin();
68
        $this->bootstrap($input, $presenter);
69
70
        $tester = new ReadmeTester;
71
        $filter = $input->getOption('filter') ? new Regexp($input->getOption('filter')) : null;
72
73
        foreach ($input->getArgument('source') as $source) {
74
            foreach (new SourceFileIterator($source) as $filename => $contents) {
75
                $presenter->beginFile($filename);
76
77
                foreach ($tester->test($contents) as $exampleName => $returnObj) {
78
                    if ($filter && !$filter->isMatch($exampleName)) {
79
                        continue;
80
                    }
81
82
                    $presenter->beginAssertion($exampleName, $returnObj);
83
                }
84
            }
85
        }
86
87
        $presenter->end();
88
        return (int)$presenter->hasFailures();
89
    }
90
91
    private function bootstrap(InputInterface $input, Presenter $presenter)
92
    {
93
        if ($filename = $input->getOption('bootstrap')) {
94
            if (!file_exists($filename) || !is_readable($filename)) {
95
                throw new \RuntimeException("Unable to bootstrap $filename");
96
            }
97
98
            require_once $filename;
99
            $presenter->bootstrap($filename);
100
            return;
101
        }
102
103
        if (!$input->getOption('no-auto-bootstrap') && is_readable(self::DEFAULT_BOOTSTRAP)) {
104
            require_once self::DEFAULT_BOOTSTRAP;
105
            $presenter->bootstrap(self::DEFAULT_BOOTSTRAP);
106
        }
107
    }
108
}
109