Completed
Push — master ( 9121c0...5838a7 )
by Hannes
02:03
created

TestCommand::execute()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 17
nc 12
nop 2
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace hanneskod\readmetester\Command;
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
    protected function configure()
22
    {
23
        $this->setName('test')
24
            ->setDescription('Test examples in readme file')
25
            ->addArgument(
26
                'filename',
27
                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
28
                'One or more files or directories to test',
29
                ['README.md']
30
            )
31
            ->addOption(
32
                'filter',
33
                null,
34
                InputOption::VALUE_REQUIRED,
35
                'Filter which examples to test using a regular expression'
36
            )
37
            ->addOption(
38
                'bootstrap',
39
                null,
40
                InputOption::VALUE_REQUIRED,
41
                'A "bootstrap" PHP file that is run before testing'
42
            )
43
            ->addOption(
44
                'no-auto-bootstrap',
45
                null,
46
                InputOption::VALUE_NONE,
47
                "Don't try to load a local composer autoloader when boostrap is not definied"
48
            )
49
        ;
50
    }
51
52
    protected function execute(InputInterface $input, OutputInterface $output): int
53
    {
54
        $this->bootstrap($input, $output);
55
56
        $tester = new ReadmeTester;
57
        $exitStatus = 0;
58
59
        $filter = $input->getOption('filter') ? new Regexp($input->getOption('filter')) : null;
60
61
        foreach ($input->getArgument('filename') as $source) {
62
            foreach (new SourceFileIterator($source) as $filename => $contents) {
63
                $output->writeln("Testing examples in <comment>$filename</comment>");
64
65
                foreach ($tester->test($contents) as $exampleName => $returnObj) {
66
                    if ($filter && !$filter->isMatch($exampleName)) {
67
                        continue;
68
                    }
69
70
                    if ($returnObj->isSuccess()) {
71
                        $output->writeln(" <info>Example $exampleName: {$returnObj->getMessage()}</info>");
72
                        continue;
73
                    }
74
75
                    $output->writeln(" <error>Example $exampleName: {$returnObj->getMessage()}</error>");
76
                    $exitStatus = 1;
77
                }
78
            }
79
        }
80
81
        return $exitStatus;
82
    }
83
84
    private function bootstrap(InputInterface $input, OutputInterface $output)
85
    {
86
        if ($filename = $input->getOption('bootstrap')) {
87
            if (!file_exists($filename) || !is_readable($filename)) {
88
                throw new \RuntimeException("Unable to bootstrap $filename");
89
            }
90
91
            if ($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...
92
                $output->writeln("Loading bootstrap <comment>$filename</comment>");
93
            }
94
95
            return require_once $filename;
96
        }
97
98
        if (!$input->getOption('no-auto-bootstrap') && is_readable('vendor/autoload.php')) {
99
            if ($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...
100
                $output->writeln("Loading bootstrap <comment>vendor/autoload.php</comment>");
101
            }
102
103
            return require_once 'vendor/autoload.php';
104
        }
105
    }
106
}
107