Completed
Pull Request — master (#4852)
by
unknown
15:00
created

LocalServer::startCoverageCollection()   C

Complexity

Conditions 12
Paths 64

Size

Total Lines 48
Code Lines 27

Duplication

Lines 4
Ratio 8.33 %

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 64
nop 1
dl 4
loc 48
rs 5.1266
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Codeception\Coverage\Subscriber;
3
4
use Codeception\Configuration;
5
use Codeception\Coverage\SuiteSubscriber;
6
use Codeception\Event\StepEvent;
7
use Codeception\Event\SuiteEvent;
8
use Codeception\Event\TestEvent;
9
use Codeception\Events;
10
use Codeception\Exception\ModuleException;
11
use Codeception\Exception\RemoteException;
12
13
/**
14
 * When collecting code coverage data from local server HTTP requests are sent to c3.php file.
15
 * Coverage Collection is started by sending cookies/headers.
16
 * Result is taken from the local file and merged with local code coverage results.
17
 *
18
 * Class LocalServer
19
 * @package Codeception\Coverage\Subscriber
20
 */
21
class LocalServer extends SuiteSubscriber
22
{
23
    // headers
24
    const COVERAGE_HEADER = 'X-Codeception-CodeCoverage';
25
    const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error';
26
    const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config';
27
    const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite';
28
29
    // cookie names
30
    const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE';
31
    const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR';
32
33
    protected $suiteName;
34
    protected $c3Access = [
35
        'http' => [
36
            'method' => "GET",
37
            'header' => ''
38
        ]
39
    ];
40
41
    /**
42
     * @var \Codeception\Lib\Interfaces\Web
43
     */
44
    protected $module;
45
46
    public static $events = [
47
        Events::SUITE_BEFORE => 'beforeSuite',
48
        Events::TEST_BEFORE  => 'beforeTest',
49
        Events::STEP_AFTER   => 'afterStep',
50
        Events::SUITE_AFTER  => 'afterSuite',
51
    ];
52
53
    protected function isEnabled()
54
    {
55
        return $this->module && !$this->settings['remote'] && $this->settings['enabled'];
56
    }
57
58
    public function beforeSuite(SuiteEvent $e)
59
    {
60
        $this->module = $this->getServerConnectionModule($e->getSuite()->getModules());
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getServerConnecti...tSuite()->getModules()) can also be of type object<Codeception\Lib\Interfaces\Remote>. However, the property $module is declared as type object<Codeception\Lib\Interfaces\Web>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
61
        $this->applySettings($e->getSettings());
62
        if (!$this->isEnabled()) {
63
            return;
64
        }
65
66
        $this->suiteName = $e->getSuite()->getBaseName();
67
68
        if ($this->settings['remote_config']) {
69
            $this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']);
70
        }
71
72
        $knock = $this->c3Request('clear');
73
        if ($knock === false) {
74
            throw new RemoteException(
75
                '
76
                CodeCoverage Error.
77
                Check the file "c3.php" is included in your application.
78
                We tried to access "/c3/report/clear" but this URI was not accessible.
79
                You can review actual error messages in c3tmp dir.
80
                '
81
            );
82
        }
83
    }
84
85
    public function beforeTest(TestEvent $e)
86
    {
87
        if (!$this->isEnabled()) {
88
            return;
89
        }
90
        $this->startCoverageCollection($e->getTest()->getName());
91
    }
92
93
    public function afterStep(StepEvent $e)
0 ignored issues
show
Unused Code introduced by
The parameter $e is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
94
    {
95
        if (!$this->isEnabled()) {
96
            return;
97
        }
98
        $this->fetchErrors();
99
    }
100
101
    public function afterSuite(SuiteEvent $e)
102
    {
103
        if (!$this->isEnabled()) {
104
            return;
105
        }
106
        $coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized';
107
108
        $retries = 5;
109
        while (!file_exists($coverageFile) && --$retries >= 0) {
110
            usleep(0.5 * 1000000); // 0.5 sec
111
        }
112
113
        if (!file_exists($coverageFile)) {
114
            if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) {
115
                throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt'));
116
            }
117
            return;
118
        }
119
120
        $contents = file_get_contents($coverageFile);
121
        $coverage = @unserialize($contents);
122
        if ($coverage === false) {
123
            return;
124
        }
125
        $this->mergeToPrint($coverage);
126
    }
127
128
    protected function c3Request($action)
129
    {
130
        $this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access');
131
        $context = stream_context_create($this->c3Access);
132
        $c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Codeception\Lib\Interfaces\Web as the method _getUrl() does only exist in the following implementations of said interface: Codeception\Module\AngularJS, Codeception\Module\PhpBrowser, Codeception\Module\WebDriver.

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...
133
        $contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context);
134
135
        $okHeaders = array_filter(
136
            $http_response_header,
137
            function ($h) {
138
                return preg_match('~^HTTP(.*?)\s200~', $h);
139
            }
140
        );
141
        if (empty($okHeaders)) {
142
            throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]);
143
        }
144
        if ($contents === false) {
145
            $this->getRemoteError($http_response_header);
146
        }
147
        return $contents;
148
    }
149
150
    protected function startCoverageCollection($testName)
151
    {
152
        $value = [
153
            'CodeCoverage'        => $testName,
154
            'CodeCoverage_Suite'  => $this->suiteName,
155
            'CodeCoverage_Config' => $this->settings['remote_config']
156
        ];
157
        $value = json_encode($value);
158
159
        if ($this->module instanceof \Codeception\Module\WebDriver) {
160
            $this->module->amOnPage('/');
161
        }
162
163
        $c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Codeception\Lib\Interfaces\Web as the method _getUrl() does only exist in the following implementations of said interface: Codeception\Module\AngularJS, Codeception\Module\PhpBrowser, Codeception\Module\WebDriver.

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...
164
165
        // we need to separate coverage cookies by host; we can't separate cookies by port.
166
        $c3Host = isset($c3Url['host']) ? $c3Url['host'] : 'localhost';
167
168
        $this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $c3Host]);
169
170
        // putting in configuration ensures the cookie is used for all sessions of a MultiSession test
171
172
        $cookies = $this->module->_getConfig('cookies');
173
        if (!$cookies || !is_array($cookies)) {
174
            $cookies = [];
175
        }
176
177
        $found = false;
178
        foreach ($cookies as &$cookie) {
179 View Code Duplication
            if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
180
                // \Codeception\Lib\InnerBrowser will complain about this
181
                continue;
182
            }
183
            if ($cookie['Name'] === self::COVERAGE_COOKIE) {
184
                $found = true;
185
                $cookie['Value'] = $value;
186
                break;
187
            }
188
        }
189
        if (!$found) {
190
            $cookies[] = [
191
                'Name' => self::COVERAGE_COOKIE,
192
                'Value' => $value
193
            ];
194
        }
195
196
        $this->module->_setConfig(['cookies' => $cookies]);
197
    }
198
199
    protected function fetchErrors()
200
    {
201
        try {
202
            $error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR);
203
        } catch (ModuleException $e) {
204
            // when a new session is started we can't get cookies because there is no
205
            // current page, but there can be no code coverage error either
206
            $error = null;
207
        }
208
        if (!empty($error)) {
209
            $this->module->resetCookie(self::COVERAGE_COOKIE_ERROR);
210
            throw new RemoteException($error);
211
        }
212
    }
213
214
    protected function getRemoteError($headers)
215
    {
216
        foreach ($headers as $header) {
217
            if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) {
218
                throw new RemoteException($header);
219
            }
220
        }
221
    }
222
223
    protected function addC3AccessHeader($header, $value)
224
    {
225
        $headerString = "$header: $value\r\n";
226
        if (strpos($this->c3Access['http']['header'], $headerString) === false) {
227
            $this->c3Access['http']['header'] .= $headerString;
228
        }
229
    }
230
231
    protected function applySettings($settings)
232
    {
233
        parent::applySettings($settings);
234
        if (isset($settings['coverage']['remote_context_options'])) {
235
            $this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']);
236
        }
237
    }
238
}
239