Issues (9)

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