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