Issues (4)

src/TestCase.php (1 issue)

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
    /** @internal */
23
    public const string ANNOTATION_TEST = "test";
24
    /** @internal */
25
    public const string ANNOTATION_TEST_SUITE = "testSuite";
26
    /** @internal */
27
    public const string ANNOTATION_IGNORE_DEPRECATIONS = "ignoreDeprecations";
28
    /** @internal */
29
    public const string ANNOTATION_NO_ASSERTIONS = "noAssertions";
30
    /** @internal */
31
    public const string ANNOTATION_FLAKY_TEST = "flakyTest";
32
33
    protected SkipChecker $skipChecker;
34
    protected DataProvider $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
        return array_values(
74
            array_filter(
75
                array_map(
76
                    static fn(ReflectionMethod $rm) => $rm->getName(),
77
                    $r->getMethods(ReflectionMethod::IS_PUBLIC)
78
                ),
79
                static fn(string $method) => str_starts_with($method, "test")
80
            )
81
        );
82
    }
83
84
    /**
85
     * @internal
86
     */
87
    public function shouldCheckAssertions(string $methodName): bool
88
    {
89
        return !$this->annotationsReader->hasAnnotation(static::ANNOTATION_NO_ASSERTIONS, static::class) &&
90
            !$this->annotationsReader->hasAnnotation(
91
                static::ANNOTATION_NO_ASSERTIONS,
92
                static::class,
93
                $methodName
94
            );
95
    }
96
97
    protected function shouldReportDeprecations(string $methodName): bool
98
    {
99
        $reportDeprecationsClass = !$this->annotationsReader->hasAnnotation(
100
            static::ANNOTATION_IGNORE_DEPRECATIONS,
101
            static::class
102
        );
103
        $reportDeprecationsMethod = !$this->annotationsReader->hasAnnotation(
104
            static::ANNOTATION_IGNORE_DEPRECATIONS,
105
            static::class,
106
            $methodName
107
        );
108
        return $reportDeprecationsClass && $reportDeprecationsMethod;
109
    }
110
111
    protected function shouldSkip(string $methodName): bool|string
112
    {
113
        return $this->skipChecker->shouldSkip(static::class, $methodName);
114
    }
115
116
    protected function getMaxRetries(string $methodName): int
117
    {
118
        $value = $this->annotationsReader->getAnnotation(static::ANNOTATION_FLAKY_TEST, static::class, $methodName);
119
        if (!is_int($value)) {
120
            return 0;
121
        }
122
        return $value;
123
    }
124
125
    /**
126
     * Get list of jobs with parameters for current test suite
127
     *
128
     * @return Job[]
129
     */
130
    protected function getJobs(): array
0 ignored issues
show
Function's cyclomatic complexity (11) exceeds 10; consider refactoring the function
Loading history...
131
    {
132
        if (count($this->jobs) === 0) {
133
            $methods = $this->getTestMethodsNames();
134
            foreach ($methods as $method) {
135
                $job = [
136
                    "name" => $this->getJobName(static::class, $method),
137
                    "callback" => $this->$method(...), // @phpstan-ignore method.dynamicName
138
                    "params" => [],
139
                    "skip" => $this->shouldSkip($method),
140
                    "dataSetName" => "",
141
                    "reportDeprecations" => $this->shouldReportDeprecations($method),
142
                    "maxRetries" => $this->getMaxRetries($method),
143
                ];
144
145
                $requiredParameters = (new ReflectionMethod($this, $method))->getNumberOfParameters();
146
                if ($requiredParameters === 0) {
147
                    $this->jobs[] = new Job(... $job);
148
                    continue;
149
                }
150
151
                $data = $this->dataProvider->getData($this, $method);
152
                if (!is_array($data)) {
153
                    $data = iterator_to_array($data);
154
                }
155
                if (count($data) === 0) {
156
                    $job["skip"] = "Method requires at least 1 parameter but data provider does not provide any.";
157
                    $this->jobs[] = new Job(... $job);
158
                    continue;
159
                }
160
161
                foreach ($data as $dataSetName => $value) {
162
                    if (!is_array($value) || count($value) < $requiredParameters) {
163
                        $job["skip"] = sprintf(
164
                            "Method requires at least %d parameter(s) but data provider provides only %d.",
165
                            $requiredParameters,
166
                            is_array($value) ? count($value) : 0
167
                        );
168
                        $this->jobs[] = new Job(... $job);
169
                        $job["params"] = [];
170
                        break;
171
                    } else {
172
                        $job["params"] = $value;
173
                    }
174
                    if (is_string($dataSetName)) {
175
                        $job["dataSetName"] = $dataSetName;
176
                    }
177
                    $this->jobs[] = new Job(... $job);
178
                    $job["params"] = [];
179
                    $job["dataSetName"] = "";
180
                }
181
            }
182
        }
183
184
        foreach ($this->jobs as $job) {
185
            $job->setEventDispatcher($this->eventDispatcher);
186
        }
187
188
        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
        $class = $class ?? static::class;
200
        /** @var string|null $annotation */
201
        $annotation = $this->annotationsReader->getAnnotation(static::ANNOTATION_TEST_SUITE, $class);
202
        if ($annotation !== null) {
203
            return $annotation;
204
        }
205
        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
        $annotation = $this->annotationsReader->getAnnotation(static::ANNOTATION_TEST, $class, $method);
216
        /** @var string|null $annotation */
217
        if ($annotation !== null) {
218
            return $annotation;
219
        }
220
        return $this->getSuiteName($class) . "::" . $method;
221
    }
222
223
    /**
224
     * Interrupts the job's run, it is reported as passed with warning
225
     */
226
    protected function markTestIncomplete(string $message = ""): void
227
    {
228
        throw new IncompleteTestException($message);
229
    }
230
231
    /**
232
     * Interrupts the job's run, it is reported as skipped
233
     */
234
    protected function markTestSkipped(string $message = ""): void
235
    {
236
        throw new SkippedTestException($message);
237
    }
238
239
    protected function runJob(Job $job): string
240
    {
241
        $this->resetCounter();
242
        if ($job->skip === false) {
243
            $this->eventDispatcher->dispatch(new Events\TestStarted($job));
244
        }
245
        $job->execute();
246
        if ($job->skip === false) {
247
            $this->eventDispatcher->dispatch(new Events\TestFinished($job));
248
        }
249
        $this->eventDispatcher->dispatch(match ($job->result) {
250
            JobResult::PASSED => new Events\TestPassed($job),
251
            JobResult::WARNING => new Events\TestPassedWithWarning($job),
252
            JobResult::FAILED => new Events\TestFailed($job),
253
            JobResult::SKIPPED => new Events\TestSkipped($job),
254
        });
255
        $this->resetCounter();
256
        return $job->result->output();
257
    }
258
259
    /**
260
     * Runs the test suite
261
     */
262
    public function run(): bool
263
    {
264
        $this->eventDispatcher->dispatch(new Events\TestSuiteStarted($this));
265
        $jobs = $this->getJobs();
266
        $passed = true;
267
        foreach ($jobs as $job) {
268
            $this->runJob($job);
269
            $passed = $passed && $job->result !== JobResult::FAILED;
270
        }
271
        $this->eventDispatcher->dispatch(new Events\TestSuiteFinished($this));
272
        return $passed;
273
    }
274
}
275