Completed
Push — 4.2.0 ( f03d5e...24bd0a )
by Daniel
12:22 queued 05:26
created

FixtureTestState::getIsLoaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev\State;
4
5
use LogicException;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Core\Manifest\ClassLoader;
9
use SilverStripe\Dev\FixtureFactory;
10
use SilverStripe\Dev\SapphireTest;
11
use SilverStripe\Dev\YamlFixture;
12
use SilverStripe\ORM\DataObject;
13
14
class FixtureTestState implements TestState
15
{
16
17
    /**
18
     * @var FixtureFactory[]
19
     */
20
    private $fixtureFactories = [];
21
22
    /**
23
     * Set if fixtures have been loaded
24
     *
25
     * @var bool
26
     */
27
    protected $loaded = [];
28
29
    /**
30
     * Called on setup
31
     *
32
     * @param SapphireTest $test
33
     */
34
    public function setUp(SapphireTest $test)
35
    {
36
        if (!$this->testNeedsDB($test)) {
37
            return;
38
        }
39
40
        // Ensure DB is built
41
        $tmpDB = $test::tempDB();
42
        if (!$tmpDB->isUsed()) {
43
            // Build base db
44
            $tmpDB->build();
45
46
            // Reset schema
47
            $extraObjects = $test->getExtraDataObjects();
48
            if ($extraObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extraObjects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
49
                $tmpDB->resetDBSchema($extraObjects);
50
            }
51
        }
52
53
        DataObject::singleton()->flushCache();
54
55
        // Ensure DB is built and populated
56
        if (!$this->getIsLoaded(get_class($test))) {
57
            foreach ($test->getRequireDefaultRecordsFrom() as $className) {
58
                $instance = singleton($className);
59
                if (method_exists($instance, 'requireDefaultRecords')) {
60
                    $instance->requireDefaultRecords();
61
                }
62
                if (method_exists($instance, 'augmentDefaultRecords')) {
63
                    $instance->augmentDefaultRecords();
64
                }
65
            }
66
            $this->loadFixtures($test);
67
        }
68
69
        // Begin transactions if enabled
70
        if ($test->getUsesTransactions()) {
71
            $tmpDB->startTransaction();
72
        }
73
    }
74
75
    /**
76
     * Called on tear down
77
     *
78
     * @param SapphireTest $test
79
     */
80
    public function tearDown(SapphireTest $test)
81
    {
82
        if (!$this->testNeedsDB($test)) {
83
            return;
84
        }
85
86
        // For transactional states, rollback if possible
87
        if ($test->getUsesTransactions()) {
88
            $success = $test::tempDB()->rollbackTransaction();
89
            if ($success) {
90
                return;
91
            }
92
        }
93
94
        // Force reset if transaction failed, or disabled
95
        $test::tempDB()->kill();
96
        $this->resetFixtureFactory(get_class($test));
97
    }
98
99
    /**
100
     * Called once on setup
101
     *
102
     * @param string $class Class being setup
103
     */
104
    public function setUpOnce($class)
105
    {
106
        $this->resetFixtureFactory($class);
107
    }
108
109
    /**
110
     * Called once on tear down
111
     *
112
     * @param string $class Class being torn down
113
     */
114
    public function tearDownOnce($class)
115
    {
116
        unset($this->fixtureFactories[strtolower($class)]);
117
        $class::tempDB()->clearAllData();
118
    }
119
120
    /**
121
     * @param string $class
122
     *
123
     * @return bool|FixtureFactory
124
     */
125
    public function getFixtureFactory($class)
126
    {
127
        $testClass = strtolower($class);
128
        if (array_key_exists($testClass, $this->fixtureFactories)) {
129
            return $this->fixtureFactories[$testClass];
130
        }
131
        return false;
132
    }
133
134
    /**
135
     * @param FixtureFactory $factory
136
     * @param string $class
137
     */
138
    public function setFixtureFactory(FixtureFactory $factory, $class)
139
    {
140
        $this->fixtureFactories[strtolower($class)] = $factory;
141
    }
142
143
    /**
144
     * @param array $fixtures
145
     *
146
     * @param SapphireTest $test
147
     *
148
     * @return array
149
     */
150
    protected function getFixturePaths($fixtures, SapphireTest $test)
151
    {
152
        return array_map(function ($fixtureFilePath) use ($test) {
153
            return $this->resolveFixturePath($fixtureFilePath, $test);
154
        }, $fixtures);
155
    }
156
157
    /**
158
     * @param SapphireTest $test
159
     */
160
    protected function loadFixtures(SapphireTest $test)
161
    {
162
        $fixtures = $test::get_fixture_file();
163
        $fixtures = is_array($fixtures) ? $fixtures : [$fixtures];
0 ignored issues
show
introduced by
The condition is_array($fixtures) is always false.
Loading history...
164
        $paths = $this->getFixturePaths($fixtures, $test);
165
        foreach ($paths as $fixtureFile) {
166
            $this->loadFixture($fixtureFile, $test);
167
        }
168
        // Flag as loaded
169
        $this->loaded[strtolower(get_class($test))] = true;
170
    }
171
172
    /**
173
     * @param string $fixtureFile
174
     * @param SapphireTest $test
175
     */
176
    protected function loadFixture($fixtureFile, SapphireTest $test)
177
    {
178
        /** @var YamlFixture $fixture */
179
        $fixture = Injector::inst()->create(YamlFixture::class, $fixtureFile);
180
        $fixture->writeInto($this->getFixtureFactory(get_class($test)));
0 ignored issues
show
Bug introduced by
It seems like $this->getFixtureFactory(get_class($test)) can also be of type false; however, parameter $factory of SilverStripe\Dev\YamlFixture::writeInto() does only seem to accept SilverStripe\Dev\FixtureFactory, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

180
        $fixture->writeInto(/** @scrutinizer ignore-type */ $this->getFixtureFactory(get_class($test)));
Loading history...
181
    }
182
183
    /**
184
     * Map a fixture path to a physical file
185
     *
186
     * @param string $fixtureFilePath
187
     * @param SapphireTest $test
188
     *
189
     * @return string
190
     */
191
    protected function resolveFixturePath($fixtureFilePath, SapphireTest $test)
192
    {
193
        // Support fixture paths relative to the test class, rather than relative to webroot
194
        // String checking is faster than file_exists() calls.
195
        $resolvedPath = realpath($this->getTestAbsolutePath($test) . '/' . $fixtureFilePath);
196
        if ($resolvedPath) {
197
            return $resolvedPath;
198
        }
199
200
        // Check if file exists relative to base dir
201
        $resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
202
        if ($resolvedPath) {
203
            return $resolvedPath;
204
        }
205
206
        return $fixtureFilePath;
207
    }
208
209
    /**
210
     * Useful for writing unit tests without hardcoding folder structures.
211
     *
212
     * @param SapphireTest $test
213
     *
214
     * @return string Absolute path to current class.
215
     */
216
    protected function getTestAbsolutePath(SapphireTest $test)
217
    {
218
        $filename = ClassLoader::inst()->getItemPath(get_class($test));
219
        if (!$filename) {
220
            throw new LogicException('getItemPath returned null for ' . static::class
221
                . '. Try adding flush=1 to the test run.');
222
        }
223
        return dirname($filename);
224
    }
225
226
    /**
227
     * @param SapphireTest $test
228
     *
229
     * @return bool
230
     */
231
    protected function testNeedsDB(SapphireTest $test)
232
    {
233
        // test class explicitly enables DB
234
        if ($test->getUsesDatabase()) {
235
            return true;
236
        }
237
238
        // presence of fixture file implicitly enables DB
239
        $fixtures = $test::get_fixture_file();
240
        if (!empty($fixtures)) {
241
            return true;
242
        }
243
244
        $annotations = $test->getAnnotations();
245
246
        // annotation explicitly disables the DB
247
        if (array_key_exists('useDatabase', $annotations['method'])
248
            && $annotations['method']['useDatabase'][0] === 'false') {
249
            return false;
250
        }
251
252
        // annotation explicitly enables the DB
253
        if (array_key_exists('useDatabase', $annotations['method'])
254
            && $annotations['method']['useDatabase'][0] !== 'false') {
255
            return true;
256
        }
257
258
        return false;
259
    }
260
261
    /**
262
     * Bootstrap a clean fixture factory for the given class
263
     *
264
     * @param string $class
265
     */
266
    protected function resetFixtureFactory($class)
267
    {
268
        $class = strtolower($class);
269
        $this->fixtureFactories[$class] = Injector::inst()->create(FixtureFactory::class);
270
        $this->loaded[$class] = false;
271
    }
272
273
    /**
274
     * Check if fixtures need to be loaded for this class
275
     *
276
     * @param string $class Name of test to check
277
     * @return bool
278
     */
279
    protected function getIsLoaded($class)
280
    {
281
        return !empty($this->loaded[strtolower($class)]);
282
    }
283
}
284