Test Failed
Push — master ( bd8013...848506 )
by Dominik
05:53
created

MockByCallsTrait::getStackTrace()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 14
cts 14
cp 1
rs 8.8817
c 0
b 0
f 0
cc 6
nc 7
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Mock;
6
7
use Chubbyphp\Mock\Argument\ArgumentInterface;
8
use PHPUnit\Framework\MockObject\MockObject;
9
use PHPUnit\Framework\TestCase;
10
11
trait MockByCallsTrait
12
{
13
    /**
14
     * @param string $class
15
     * @param Call[] $calls
16
     *
17
     * @return MockObject
18
     */
19 8
    private function getMockByCalls(string $class, array $calls = []): MockObject
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
20
    {
21
        /** @var MockByCallsTrait|TestCase $this */
22 8
        $reflectionClass = new \ReflectionClass($class);
23
24 8
        $mockBuilder = $this->getMockBuilder($class)
0 ignored issues
show
Bug introduced by
It seems like getMockBuilder() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
25 8
            ->disableOriginalConstructor()
26 8
            ->disableOriginalClone();
27
28
        /* @var MockObject $mock */
29 8
        if ($reflectionClass->isAbstract() || $reflectionClass->isInterface()) {
30 7
            $mock = $mockBuilder->getMockForAbstractClass();
31
        } else {
32 1
            $mock = $mockBuilder->getMock();
33
        }
34
35 8
        $callCount = count($calls);
36
37 8
        if (0 === $callCount) {
38 2
            $mock->expects(self::never())->method(self::anything());
39
40 2
            return $mock;
41
        }
42
43 6
        foreach ($calls as $at => $call) {
44 6
            $mock->expects(self::at($at))
45 6
                ->method($call->getMethod())
46 6
                ->willReturnCallback($this->getMockCallback($class, $at, $call, $mock));
47
        }
48
49 6
        $callIndex = 0;
50
51 6
        $mock->expects(self::any())->method(self::anything())->willReturnCallback(
52 6
            function () use ($class, $mock, $callCount, &$callIndex) {
53 6
                if ($callIndex === $callCount) {
54 1
                    $options = JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
55
56 1
                    self::fail(
57 1
                        sprintf('Additional call at index %d on class "%s"', $callIndex, $class)
58 1
                        .PHP_EOL
59 1
                        .json_encode($this->getStackTrace($mock), JSON_PRETTY_PRINT | $options)
60
                    );
61
                }
62
63 6
                ++$callIndex;
64 6
            }
65
        );
66
67 6
        return $mock;
68
    }
69
70
    /**
71
     * @param string     $class
72
     * @param int        $at
73
     * @param Call       $call
74
     * @param MockObject $mock
75
     *
76
     * @return \Closure
77
     */
78
    private function getMockCallback(
79
        string $class,
80
        int $at,
81
        Call $call,
82
        MockObject $mock
83
    ): \Closure {
84 6
        return function () use ($class, $at, $call, $mock) {
85 6
            if ($call->hasWith()) {
86 6
                $this->compareArguments($class, $call->getMethod(), $at, $call->getWith(), func_get_args());
87
            }
88
89 5
            if (null !== $exception = $call->getException()) {
90 1
                throw $exception;
91
            }
92
93 4
            if ($call->hasReturnSelf()) {
94 1
                return $mock;
95
            }
96
97 3
            if ($call->hasReturn()) {
98 2
                return $call->getReturn();
99
            }
100 6
        };
101
    }
102
103
    /**
104
     * @param string $class
105
     * @param string $method
106
     * @param int    $at
107
     * @param array  $expectedArguments
108
     * @param array  $arguments
109
     */
110 6
    private function compareArguments(
111
        string $class,
112
        string $method,
113
        int $at,
114
        array $expectedArguments,
115
        array $arguments
116
    ) {
117 6
        $expectedArgumentsCount = count($expectedArguments);
118 6
        $argumentsCount = count($arguments);
119
120 6
        self::assertSame(
121 6
            $expectedArgumentsCount,
122 6
            $argumentsCount,
123 6
            sprintf(
124 6
                'Method "%s" on class "%s" at call %d, got %d arguments, but %d are expected',
125 6
                $method,
126 6
                $class,
127 6
                $at,
128 6
                $expectedArgumentsCount,
129 6
                $argumentsCount
130
            )
131
        );
132
133 6
        foreach ($expectedArguments as $index => $expectedArgument) {
134 6
            if ($expectedArgument instanceof ArgumentInterface) {
135 2
                $expectedArgument->assert(
136 2
                    $arguments[$index],
137 2
                    ['class' => $class, 'method' => $method, 'at' => $at, 'index' => $index]
138
                );
139
140 1
                continue;
141
            }
142
143 5
            self::assertSame(
144 5
                $expectedArgument,
145 5
                $arguments[$index],
146 5
                sprintf(
147 5
                    'Method "%s" on class "%s" at call %d, argument %d',
148 5
                    $method,
149 5
                    $class,
150 5
                    $at,
151 5
                    $index
152
                )
153
            );
154
        }
155 5
    }
156
157
    /**
158
     * @param MockObject $mock
159
     *
160
     * @return array
161
     */
162 1
    private function getStackTrace(MockObject $mock): array
163
    {
164 1
        $mockName = (new \ReflectionObject($mock))->getShortName();
165
166 1
        $trace = [];
167 1
        $enableTrace = false;
168 1
        foreach (debug_backtrace() as $i => $row) {
169 1
            if (isset($row['class']) && $mockName === $row['class']) {
170 1
                $enableTrace = true;
171
            }
172
173 1
            if ($enableTrace) {
174 1
                $traceRow = $row['class'].$row['type'].$row['function'];
175
176 1
                if (isset($row['file'])) {
177 1
                    $traceRow .= sprintf(' (%s:%d)', $row['file'], $row['line']);
178
                }
179
180 1
                $trace[] = $traceRow;
181
            }
182
        }
183
184 1
        krsort($trace);
185
186 1
        return array_values($trace);
187
    }
188
}
189