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) { |
|
|
|
|
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]; |
|
|
|
|
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))); |
|
|
|
|
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
|
|
|
|
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.