Issues (7)

src/TestCase.php (2 issues)

Severity
1
<?php
2
declare(strict_types=1);
3
4
namespace MyTester;
5
6
use MyTester\Annotations\Reader;
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use ReflectionClass;
9
use ReflectionMethod;
10
11
/**
12
 * One test suite
13
 *
14
 * @author Jakub Konečný
15
 * @property-read Job[] $jobs @internal
16
 */
17
abstract class TestCase
18
{
19
    use \Nette\SmartObject;
20
    use TAssertions;
21
22
    protected const string METHOD_PATTERN = '#^test[A-Z0-9_]#';
23
24
    /** @internal */
25
    public const string ANNOTATION_TEST = "test";
26
    /** @internal */
27
    public const string ANNOTATION_TEST_SUITE = "testSuite";
28
    /** @internal */
29
    public const string ANNOTATION_IGNORE_DEPRECATIONS = "ignoreDeprecations";
30
    /** @internal */
31
    public const string ANNOTATION_NO_ASSERTIONS = "noAssertions";
32
33
    protected ISkipChecker $skipChecker;
34
    protected IDataProvider $dataProvider;
35
    protected Reader $annotationsReader;
36
37
    /** @var Job[] */
38
    private array $jobs = [];
39
40
    private EventDispatcherInterface $eventDispatcher;
41
42
    public function __construct()
43
    {
44 1
        $this->annotationsReader = Reader::create();
45 1
        $this->skipChecker = new AnnotationsSkipChecker($this->annotationsReader);
46 1
        $this->dataProvider = new AnnotationsDataProvider($this->annotationsReader);
47 1
    }
48
49
    /**
50
     * @internal
51
     */
52
    final public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
53
    {
54 1
        $this->eventDispatcher = $eventDispatcher;
55 1
    }
56
57
    /**
58
     * Get list of test methods in current test suite
59
     *
60
     * @return string[]
61
     */
62
    protected function getTestMethodsNames(): array
63
    {
64 1
        $r = new ReflectionClass(static::class);
65
        /** @var string[] $result */
66 1
        $result = array_values(
67 1
            (array) preg_grep(
68 1
                static::METHOD_PATTERN,
69 1
                array_map(
70 1
                    function (ReflectionMethod $rm) {
71 1
                        return $rm->getName();
72 1
                    },
73 1
                    $r->getMethods(ReflectionMethod::IS_PUBLIC)
74
                )
75
            )
76
        );
77 1
        return $result;
78
    }
79
80
    /**
81
     * Get list of callbacks that should be called after a job finishes
82
     *
83
     * @deprecated
84
     * @return callable[]
85
     */
86
    protected function getJobAfterExecuteCallbacks(string $methodName): array
87
    {
88 1
        return [];
89
    }
90
91
    /**
92
     * @internal
93
     */
94
    public function shouldCheckAssertions(string $methodName): bool
95
    {
96 1
        return !$this->annotationsReader->hasAnnotation(static::ANNOTATION_NO_ASSERTIONS, static::class) &&
97 1
            !$this->annotationsReader->hasAnnotation(
98 1
                static::ANNOTATION_NO_ASSERTIONS,
99 1
                static::class,
100
                $methodName
101
            );
102
    }
103
104
    protected function shouldReportDeprecations(string $methodName): bool
105
    {
106 1
        $reportDeprecationsClass = !$this->annotationsReader->hasAnnotation(
107 1
            static::ANNOTATION_IGNORE_DEPRECATIONS,
108 1
            static::class
109
        );
110 1
        $reportDeprecationsMethod = !$this->annotationsReader->hasAnnotation(
111 1
            static::ANNOTATION_IGNORE_DEPRECATIONS,
112 1
            static::class,
113
            $methodName
114
        );
115 1
        return $reportDeprecationsClass && $reportDeprecationsMethod;
116
    }
117
118
    protected function shouldSkip(string $methodName): bool|string
119
    {
120 1
        return $this->skipChecker->shouldSkip(static::class, $methodName);
121
    }
122
123
    /**
124
     * Get list of jobs with parameters for current test suite
125
     *
126
     * @return Job[]
127
     */
128
    protected function getJobs(): array
0 ignored issues
show
Function's cyclomatic complexity (11) exceeds 10; consider refactoring the function
Loading history...
129
    {
130 1
        if (count($this->jobs) === 0) {
131 1
            $methods = $this->getTestMethodsNames();
132 1
            foreach ($methods as $method) {
133
                /** @var callable $callback */
134 1
                $callback = [$this, $method];
135 1
                $job = [
136 1
                    "name" => $this->getJobName(static::class, $method),
137 1
                    "callback" => $callback,
138
                    "params" => [],
139 1
                    "skip" => $this->shouldSkip($method),
140 1
                    "onAfterExecute" => $this->getJobAfterExecuteCallbacks($method), // @phpstan-ignore method.deprecated
0 ignored issues
show
Line exceeds 120 characters; contains 121 characters
Loading history...
141 1
                    "dataSetName" => "",
142 1
                    "reportDeprecations" => $this->shouldReportDeprecations($method),
143
                ];
144
145 1
                $requiredParameters = (new ReflectionMethod($this, $method))->getNumberOfParameters();
146 1
                if ($requiredParameters === 0) {
147 1
                    $this->jobs[] = new Job(... $job);
148 1
                    continue;
149
                }
150
151 1
                $data = $this->dataProvider->getData($this, $method);
152 1
                if (!is_array($data)) {
153 1
                    $data = iterator_to_array($data);
154
                }
155 1
                if (count($data) === 0) {
156 1
                    $job["skip"] = "Method requires at least 1 parameter but data provider does not provide any.";
157 1
                    $this->jobs[] = new Job(... $job);
158 1
                    continue;
159
                }
160
161 1
                foreach ($data as $dataSetName => $value) {
162 1
                    if (!is_array($value) || count($value) < $requiredParameters) {
163 1
                        $job["skip"] = sprintf(
164 1
                            "Method requires at least %d parameter(s) but data provider provides only %d.",
165
                            $requiredParameters,
166 1
                            is_array($value) ? count($value) : 0
167
                        );
168 1
                        $this->jobs[] = new Job(... $job);
169 1
                        $job["params"] = [];
170 1
                        break;
171
                    } else {
172 1
                        $job["params"] = $value;
173
                    }
174 1
                    if (is_string($dataSetName)) {
175 1
                        $job["dataSetName"] = $dataSetName;
176
                    }
177 1
                    $this->jobs[] = new Job(... $job);
178 1
                    $job["params"] = [];
179 1
                    $job["dataSetName"] = "";
180
                }
181
            }
182
        }
183
184 1
        foreach ($this->jobs as $job) {
185 1
            $job->setEventDispatcher($this->eventDispatcher);
186
        }
187
188 1
        return $this->jobs;
189
    }
190
191
    /**
192
     * Get name of a test suite
193
     *
194
     * @param class-string|object $class
195
     * @internal
196
     */
197
    public function getSuiteName(string|object|null $class = null): string
198
    {
199 1
        $class = $class ?? static::class;
200
        /** @var string|null $annotation */
201 1
        $annotation = $this->annotationsReader->getAnnotation(static::ANNOTATION_TEST_SUITE, $class);
202 1
        if ($annotation !== null) {
203 1
            return $annotation;
204
        }
205 1
        return is_object($class) ? get_class($class) : $class;
206
    }
207
208
    /**
209
     * Get name for a job
210
     *
211
     * @param class-string|object $class
212
     */
213
    protected function getJobName(string|object $class, string $method): string
214
    {
215 1
        $annotation = $this->annotationsReader->getAnnotation(static::ANNOTATION_TEST, $class, $method);
216
        /** @var string|null $annotation */
217 1
        if ($annotation !== null) {
218 1
            return $annotation;
219
        }
220 1
        return $this->getSuiteName($class) . "::" . $method;
221
    }
222
223
    /**
224
     * Called at start of the suite
225
     *
226
     * @deprecated This method will not be run automatically in the next major version
227
     */
228
    public function startUp(): void
229
    {
230 1
    }
231
232
    /**
233
     * Called at end of the suite
234
     *
235
     * @deprecated This method will not be run automatically in the next major version
236
     */
237
    public function shutDown(): void
238
    {
239 1
    }
240
241
    /**
242
     * Called before each job
243
     *
244
     * @deprecated This method will not be run automatically in the next major version
245
     */
246
    public function setUp(): void
247
    {
248 1
    }
249
250
    /**
251
     * Called after each job
252
     *
253
     * @deprecated This method will not be run automatically in the next major version
254
     */
255
    public function tearDown(): void
256
    {
257 1
    }
258
259
    /**
260
     * Interrupts the job's run, it is reported as passed with warning
261
     */
262
    protected function markTestIncomplete(string $message = ""): void
263
    {
264 1
        throw new IncompleteTestException($message);
265
    }
266
267
    /**
268
     * Interrupts the job's run, it is reported as skipped
269
     */
270
    protected function markTestSkipped(string $message = ""): void
271
    {
272 1
        throw new SkippedTestException($message);
273
    }
274
275
    protected function runJob(Job $job): string
276
    {
277 1
        $this->resetCounter();
278 1
        if ($job->skip === false) {
279 1
            $this->eventDispatcher->dispatch(new Events\TestStarted($job));
280
        }
281 1
        $job->execute();
282 1
        if ($job->skip === false) {
283 1
            $this->eventDispatcher->dispatch(new Events\TestFinished($job));
284
        }
285 1
        $this->eventDispatcher->dispatch(match ($job->result) {
286 1
            JobResult::PASSED => new Events\TestPassed($job),
287 1
            JobResult::WARNING => new Events\TestPassedWithWarning($job),
288 1
            JobResult::FAILED => new Events\TestFailed($job),
289 1
            JobResult::SKIPPED => new Events\TestSkipped($job),
290
        });
291 1
        $this->resetCounter();
292 1
        return $job->result->output();
293
    }
294
295
    /**
296
     * Runs the test suite
297
     */
298
    public function run(): bool
299
    {
300 1
        $this->eventDispatcher->dispatch(new Events\TestSuiteStarted($this));
301 1
        $jobs = $this->getJobs();
302 1
        $passed = true;
303 1
        foreach ($jobs as $job) {
304 1
            $this->runJob($job);
305 1
            $passed = $passed && $job->result !== JobResult::FAILED;
306
        }
307 1
        $this->eventDispatcher->dispatch(new Events\TestSuiteFinished($this));
308 1
        return $passed;
309
    }
310
}
311