Completed
Pull Request — master (#5247)
by Damian
11:17
created

SapphireTest   F

Complexity

Total Complexity 152

Size/Duplication

Total Lines 1056
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 30

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 152
c 3
b 1
f 0
lcom 2
cbo 30
dl 0
loc 1056
rs 0.7513

41 Methods

Rating   Name   Duplication   Size   Complexity  
A clearEmails() 0 3 1
A findEmail() 0 3 1
A is_running_test() 0 3 1
A set_is_running_test() 0 3 1
A set_test_class_manifest() 0 3 1
A get_test_class_manifest() 0 3 1
A get_fixture_file() 0 3 1
D setUpOnce() 0 48 15
D tearDownOnce() 0 31 10
A getFixtureFactory() 0 4 2
A setFixtureFactory() 0 4 1
A idFromFixture() 0 13 2
A allFixtureIDs() 0 3 1
A objFromFixture() 0 13 2
A loadFixture() 0 5 1
A clearFixtures() 0 4 1
A getCurrentAbsolutePath() 0 5 2
A getCurrentRelativePath() 0 6 2
C tearDown() 0 40 8
A assertContains() 0 11 2
A assertNotContains() 0 11 2
B assertEmailSent() 0 16 6
B assertDOSContains() 0 24 5
A assertDOSAllMatch() 0 12 3
A normaliseSQL() 0 3 1
A assertSQLEquals() 0 9 1
A assertSQLContains() 0 8 1
A assertSQLNotContains() 0 8 1
A dataObjectArrayMatch() 0 6 4
A DOSSummaryForMatch() 0 5 2
A using_temp_db() 0 6 3
B kill_temp_db() 0 18 6
A empty_temp_db() 0 13 4
B create_temp_db() 0 25 4
B delete_all_temp_dbs() 0 14 5
A useTestTheme() 0 22 3
C assertDOSEquals() 0 41 7
D resetDBSchema() 0 35 9
B logInWithPermission() 0 43 5
D setUp() 0 117 21
B use_test_manifest() 0 25 3

How to fix   Complexity   

Complex Class

Complex classes like SapphireTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SapphireTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use SilverStripe\Model\FieldType\DBField;
4
use SilverStripe\Model\FieldType\DBDatetime;
5
6
/**
7
 * Test case class for the Sapphire framework.
8
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
9
 * to work with.
10
 *
11
 * @package framework
12
 * @subpackage testing
13
 */
14
class SapphireTest extends PHPUnit_Framework_TestCase {
15
16
	/** @config */
17
	private static $dependencies = array(
18
		'fixtureFactory' => '%$FixtureFactory',
19
	);
20
21
	/**
22
	 * Path to fixture data for this test run.
23
	 * If passed as an array, multiple fixture files will be loaded.
24
	 * Please note that you won't be able to refer with "=>" notation
25
	 * between the fixtures, they act independent of each other.
26
	 *
27
	 * @var string|array
28
	 */
29
	protected static $fixture_file = null;
30
31
	/**
32
	 * @var FixtureFactory
33
	 */
34
	protected $fixtureFactory;
35
36
	/**
37
	 * @var Boolean If set to TRUE, this will force a test database to be generated
38
	 * in {@link setUp()}. Note that this flag is overruled by the presence of a
39
	 * {@link $fixture_file}, which always forces a database build.
40
	 */
41
	protected $usesDatabase = null;
42
43
	/**
44
	 * @deprecated since version 4.0
45
	 */
46
	protected $originalMailer;
47
48
	protected $originalMemberPasswordValidator;
49
	protected $originalRequirements;
50
	protected $originalIsRunningTest;
51
	protected $originalTheme;
52
	protected $originalNestedURLsState;
53
	protected $originalMemoryLimit;
54
55
	protected $mailer;
56
57
	/**
58
	 * Pointer to the manifest that isn't a test manifest
59
	 */
60
	protected static $regular_manifest;
61
62
	/**
63
	 * @var boolean
64
	 */
65
	protected static $is_running_test = false;
66
67
	protected static $test_class_manifest;
68
69
	/**
70
	 * By default, setUp() does not require default records. Pass
71
	 * class names in here, and the require/augment default records
72
	 * function will be called on them.
73
	 */
74
	protected $requireDefaultRecordsFrom = array();
75
76
77
	/**
78
	 * A list of extensions that can't be applied during the execution of this run.  If they are
79
	 * applied, they will be temporarily removed and a database migration called.
80
	 *
81
	 * The keys of the are the classes that the extensions can't be applied the extensions to, and
82
	 * the values are an array of illegal extensions on that class.
83
	 */
84
	protected $illegalExtensions = array(
85
	);
86
87
	/**
88
	 * A list of extensions that must be applied during the execution of this run.  If they are
89
	 * not applied, they will be temporarily added and a database migration called.
90
	 *
91
	 * The keys of the are the classes to apply the extensions to, and the values are an array
92
	 * of required extensions on that class.
93
	 *
94
	 * Example:
95
	 * <code>
96
	 * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
97
	 * </code>
98
	 */
99
	protected $requiredExtensions = array(
100
	);
101
102
	/**
103
	 * By default, the test database won't contain any DataObjects that have the interface TestOnly.
104
	 * This variable lets you define additional TestOnly DataObjects to set up for this test.
105
	 * Set it to an array of DataObject subclass names.
106
	 */
107
	protected $extraDataObjects = array();
108
109
	/**
110
	 * We need to disabling backing up of globals to avoid overriding
111
	 * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
112
	 *
113
	 * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
114
	 */
115
	protected $backupGlobals = FALSE;
116
117
	/**
118
	 * Helper arrays for illegalExtensions/requiredExtensions code
119
	 */
120
	private $extensionsToReapply = array(), $extensionsToRemove = array();
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
121
122
123
	/**
124
	 * Determines if unit tests are currently run, flag set during test bootstrap.
125
	 * This is used as a cheap replacement for fully mockable state
126
	 * in certain contiditions (e.g. access checks).
127
	 * Caution: When set to FALSE, certain controllers might bypass
128
	 * access checks, so this is a very security sensitive setting.
129
	 *
130
	 * @return boolean
131
	 */
132
	public static function is_running_test() {
133
		return self::$is_running_test;
134
	}
135
136
	public static function set_is_running_test($bool) {
137
		self::$is_running_test = $bool;
138
	}
139
140
	/**
141
	 * Set the manifest to be used to look up test classes by helper functions
142
	 */
143
	public static function set_test_class_manifest($manifest) {
144
		self::$test_class_manifest = $manifest;
145
	}
146
147
	/**
148
	 * Return the manifest being used to look up test classes by helper functions
149
	 */
150
	public static function get_test_class_manifest() {
151
		return self::$test_class_manifest;
152
	}
153
154
	/**
155
	 * @return String
156
	 */
157
	public static function get_fixture_file() {
158
		return static::$fixture_file;
159
	}
160
161
	/**
162
	 * @var array $fixtures Array of {@link YamlFixture} instances
163
	 * @deprecated 3.1 Use $fixtureFactory instad
164
	 */
165
	protected $fixtures = array();
166
167
	protected $model;
168
169
	/**
170
	 * State of Versioned before this test is run
171
	 *
172
	 * @var string
173
	 */
174
	protected $originalReadingMode = null;
175
176
	public function setUp() {
177
178
		//nest config and injector for each test so they are effectively sandboxed per test
179
		Config::nest();
180
		Injector::nest();
181
182
		$this->originalReadingMode = \Versioned::get_reading_mode();
183
184
		// We cannot run the tests on this abstract class.
185
		if(get_class($this) == "SapphireTest") {
186
			$this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
187
			return;
188
		}
189
190
		// Mark test as being run
191
		$this->originalIsRunningTest = self::$is_running_test;
192
		self::$is_running_test = true;
193
194
		// i18n needs to be set to the defaults or tests fail
195
		i18n::set_locale(i18n::default_locale());
0 ignored issues
show
Deprecated Code introduced by
The method i18n::default_locale() has been deprecated with message: since version 4.0; Use the "i18n.default_locale" config setting instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
196
		i18n::config()->date_format = null;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
197
		i18n::config()->time_format = null;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
198
199
		// Set default timezone consistently to avoid NZ-specific dependencies
200
		date_default_timezone_set('UTC');
201
202
		// Remove password validation
203
		$this->originalMemberPasswordValidator = Member::password_validator();
204
		$this->originalRequirements = Requirements::backend();
205
		Member::set_password_validator(null);
206
		Config::inst()->update('Cookie', 'report_errors', false);
207
208
		if(class_exists('RootURLController')) RootURLController::reset();
209
		if(class_exists('Translatable')) Translatable::reset();
210
		Versioned::reset();
211
		DataObject::reset();
212
		if(class_exists('SiteTree')) SiteTree::reset();
213
		Hierarchy::reset();
214
		if(Controller::has_curr()) Controller::curr()->setSession(Injector::inst()->create('Session', array()));
215
		Security::$database_is_ready = null;
0 ignored issues
show
Documentation introduced by
The property $database_is_ready is declared private in Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
216
217
		// Add controller-name auto-routing
218
		Config::inst()->update('Director', 'rules', array(
219
			'$Controller//$Action/$ID/$OtherID' => '*'
220
		));
221
222
		$fixtureFile = static::get_fixture_file();
223
224
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
225
226
		// Set up email
227
		$this->originalMailer = Email::mailer();
0 ignored issues
show
Deprecated Code introduced by
The property SapphireTest::$originalMailer has been deprecated with message: since version 4.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
228
		$this->mailer = new TestMailer();
229
		Injector::inst()->registerService($this->mailer, 'Mailer');
230
		Config::inst()->remove('Email', 'send_all_emails_to');
231
232
		// Todo: this could be a special test model
233
		$this->model = DataModel::inst();
234
235
		// Set up fixture
236
		if($fixtureFile || $this->usesDatabase || !self::using_temp_db()) {
237
			if(substr(DB::get_conn()->getSelectedDatabase(), 0, strlen($prefix) + 5)
238
					!= strtolower(sprintf('%stmpdb', $prefix))) {
239
240
				//echo "Re-creating temp database... ";
241
				self::create_temp_db();
242
				//echo "done.\n";
243
			}
244
245
			singleton('DataObject')->flushCache();
246
247
			self::empty_temp_db();
248
249
			foreach($this->requireDefaultRecordsFrom as $className) {
250
				$instance = singleton($className);
251
				if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
252
				if (method_exists($instance, 'augmentDefaultRecords')) $instance->augmentDefaultRecords();
253
			}
254
255
			if($fixtureFile) {
256
				$pathForClass = $this->getCurrentAbsolutePath();
257
				$fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : array($fixtureFile);
258
259
				$i = 0;
260
				foreach($fixtureFiles as $fixtureFilePath) {
261
					// Support fixture paths relative to the test class, rather than relative to webroot
262
					// String checking is faster than file_exists() calls.
263
					$isRelativeToFile = (strpos('/', $fixtureFilePath) === false
264
						|| preg_match('/^\.\./', $fixtureFilePath));
265
266
					if($isRelativeToFile) {
267
						$resolvedPath = realpath($pathForClass . '/' . $fixtureFilePath);
268
						if($resolvedPath) $fixtureFilePath = $resolvedPath;
269
					}
270
271
					$fixture = Injector::inst()->create('YamlFixture', $fixtureFilePath);
272
					$fixture->writeInto($this->getFixtureFactory());
273
					$this->fixtures[] = $fixture;
0 ignored issues
show
Deprecated Code introduced by
The property SapphireTest::$fixtures has been deprecated with message: 3.1 Use $fixtureFactory instad

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
274
275
					// backwards compatibility: Load first fixture into $this->fixture
276
					if($i == 0) $this->fixture = $fixture;
0 ignored issues
show
Bug introduced by
The property fixture does not seem to exist. Did you mean fixture_file?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
277
					$i++;
278
				}
279
			}
280
281
			$this->logInWithPermission("ADMIN");
282
		}
283
284
		// Preserve memory settings
285
		$this->originalMemoryLimit = ini_get('memory_limit');
286
287
		// turn off template debugging
288
		Config::inst()->update('SSViewer', 'source_file_comments', false);
289
290
		// Clear requirements
291
		Requirements::clear();
292
	}
293
294
	/**
295
	 * Called once per test case ({@link SapphireTest} subclass).
296
	 * This is different to {@link setUp()}, which gets called once
297
	 * per method. Useful to initialize expensive operations which
298
	 * don't change state for any called method inside the test,
299
	 * e.g. dynamically adding an extension. See {@link tearDownOnce()}
300
	 * for tearing down the state again.
301
	 */
302
	public function setUpOnce() {
303
304
		//nest config and injector for each suite so they are effectively sandboxed
305
		Config::nest();
306
		Injector::nest();
307
		$isAltered = false;
308
309
		if(!Director::isDev()) {
310
			user_error('Tests can only run in "dev" mode', E_USER_ERROR);
311
		}
312
313
		// Remove any illegal extensions that are present
314
		foreach($this->illegalExtensions as $class => $extensions) {
315
			foreach($extensions as $extension) {
316
				if ($class::has_extension($extension)) {
317
					if(!isset($this->extensionsToReapply[$class])) $this->extensionsToReapply[$class] = array();
318
					$this->extensionsToReapply[$class][] = $extension;
319
					$class::remove_extension($extension);
320
					$isAltered = true;
321
				}
322
			}
323
		}
324
325
		// Add any required extensions that aren't present
326
		foreach($this->requiredExtensions as $class => $extensions) {
327
			$this->extensionsToRemove[$class] = array();
328
			foreach($extensions as $extension) {
329
				if(!$class::has_extension($extension)) {
330
					if(!isset($this->extensionsToRemove[$class])) $this->extensionsToReapply[$class] = array();
331
					$this->extensionsToRemove[$class][] = $extension;
332
					$class::add_extension($extension);
333
					$isAltered = true;
334
				}
335
			}
336
		}
337
338
		// If we have made changes to the extensions present, then migrate the database schema.
339
		if($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply 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...
Bug Best Practice introduced by
The expression $this->extensionsToRemove 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...
Bug Best Practice introduced by
The expression $this->extraDataObjects 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...
340
			if(!self::using_temp_db()) self::create_temp_db();
341
			$this->resetDBSchema(true);
342
		}
343
		// clear singletons, they're caching old extension info
344
		// which is used in DatabaseAdmin->doBuild()
345
		Injector::inst()->unregisterAllObjects();
346
347
		// Set default timezone consistently to avoid NZ-specific dependencies
348
		date_default_timezone_set('UTC');
349
	}
350
351
	/**
352
	 * tearDown method that's called once per test class rather once per test method.
353
	 */
354
	public function tearDownOnce() {
355
		// If we have made changes to the extensions present, then migrate the database schema.
356
		if($this->extensionsToReapply || $this->extensionsToRemove) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply 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...
Bug Best Practice introduced by
The expression $this->extensionsToRemove 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...
357
			// @todo: This isn't strictly necessary to restore extensions, but only to ensure that
358
			// Object::$extra_methods is properly flushed. This should be replaced with a simple
359
			// flush mechanism for each $class.
360
			//
361
			// Remove extensions added for testing
362
			foreach($this->extensionsToRemove as $class => $extensions) {
363
				foreach($extensions as $extension) {
364
					$class::remove_extension($extension);
365
				}
366
			}
367
368
			// Reapply ones removed
369
			foreach($this->extensionsToReapply as $class => $extensions) {
370
				foreach($extensions as $extension) {
371
					$class::add_extension($extension);
372
				}
373
			}
374
		}
375
376
		//unnest injector / config now that the test suite is over
377
		// this will reset all the extensions on the object too (see setUpOnce)
378
		Injector::unnest();
379
		Config::unnest();
380
381
		if(!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($this->extraDataObjects)) {
382
			$this->resetDBSchema();
383
		}
384
	}
385
386
	/**
387
	 * @return FixtureFactory
388
	 */
389
	public function getFixtureFactory() {
390
		if(!$this->fixtureFactory) $this->fixtureFactory = Injector::inst()->create('FixtureFactory');
391
		return $this->fixtureFactory;
392
	}
393
394
	public function setFixtureFactory(FixtureFactory $factory) {
395
		$this->fixtureFactory = $factory;
396
		return $this;
397
	}
398
399
	/**
400
	 * Get the ID of an object from the fixture.
401
	 *
402
	 * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
403
	 * @param string $identifier The identifier string, as provided in your fixture file
404
	 * @return int
405
	 */
406
	protected function idFromFixture($className, $identifier) {
407
		$id = $this->getFixtureFactory()->getId($className, $identifier);
408
409
		if(!$id) {
410
			user_error(sprintf(
411
				"Couldn't find object '%s' (class: %s)",
412
				$identifier,
413
				$className
414
			), E_USER_ERROR);
415
		}
416
417
		return $id;
418
	}
419
420
	/**
421
	 * Return all of the IDs in the fixture of a particular class name.
422
	 * Will collate all IDs form all fixtures if multiple fixtures are provided.
423
	 *
424
	 * @param string $className
425
	 * @return array A map of fixture-identifier => object-id
426
	 */
427
	protected function allFixtureIDs($className) {
428
		return $this->getFixtureFactory()->getIds($className);
429
	}
430
431
	/**
432
	 * Get an object from the fixture.
433
	 *
434
	 * @param string $className The data class, as specified in your fixture file. Parent classes won't work
435
	 * @param string $identifier The identifier string, as provided in your fixture file
436
	 *
437
	 * @return DataObject
438
	 */
439
	protected function objFromFixture($className, $identifier) {
440
		$obj = $this->getFixtureFactory()->get($className, $identifier);
441
442
		if(!$obj) {
443
			user_error(sprintf(
444
				"Couldn't find object '%s' (class: %s)",
445
				$identifier,
446
				$className
447
			), E_USER_ERROR);
448
		}
449
450
		return $obj;
451
	}
452
453
	/**
454
	 * Load a YAML fixture file into the database.
455
	 * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
456
	 * Doesn't clear existing fixtures.
457
	 *
458
	 * @param $fixtureFile The location of the .yml fixture file, relative to the site base dir
459
	 */
460
	public function loadFixture($fixtureFile) {
461
		$fixture = Injector::inst()->create('YamlFixture', $fixtureFile);
462
		$fixture->writeInto($this->getFixtureFactory());
463
		$this->fixtures[] = $fixture;
0 ignored issues
show
Deprecated Code introduced by
The property SapphireTest::$fixtures has been deprecated with message: 3.1 Use $fixtureFactory instad

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
464
	}
465
466
	/**
467
	 * Clear all fixtures which were previously loaded through
468
	 * {@link loadFixture()}
469
	 */
470
	public function clearFixtures() {
471
		$this->fixtures = array();
0 ignored issues
show
Deprecated Code introduced by
The property SapphireTest::$fixtures has been deprecated with message: 3.1 Use $fixtureFactory instad

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
472
		$this->getFixtureFactory()->clear();
473
	}
474
475
	/**
476
	 * Useful for writing unit tests without hardcoding folder structures.
477
	 *
478
	 * @return String Absolute path to current class.
479
	 */
480
	protected function getCurrentAbsolutePath() {
481
		$filename = self::$test_class_manifest->getItemPath(get_class($this));
482
		if(!$filename) throw new LogicException("getItemPath returned null for " . get_class($this));
483
		return dirname($filename);
484
	}
485
486
	/**
487
	 * @return String File path relative to webroot
488
	 */
489
	protected function getCurrentRelativePath() {
490
		$base = Director::baseFolder();
491
		$path = $this->getCurrentAbsolutePath();
492
		if(substr($path,0,strlen($base)) == $base) $path = preg_replace('/^\/*/', '', substr($path,strlen($base)));
493
		return $path;
494
	}
495
496
	public function tearDown() {
497
		// Preserve memory settings
498
		ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
499
500
		// Restore email configuration
501
		$this->originalMailer = null;
0 ignored issues
show
Deprecated Code introduced by
The property SapphireTest::$originalMailer has been deprecated with message: since version 4.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
502
		$this->mailer = null;
503
504
		// Restore password validation
505
		if($this->originalMemberPasswordValidator) {
506
			Member::set_password_validator($this->originalMemberPasswordValidator);
507
		}
508
509
		// Restore requirements
510
		if($this->originalRequirements) {
511
			Requirements::set_backend($this->originalRequirements);
512
		}
513
514
		// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
515
		self::$is_running_test = $this->originalIsRunningTest;
516
		$this->originalIsRunningTest = null;
517
518
		// Reset mocked datetime
519
		DBDatetime::clear_mock_now();
520
521
		// Stop the redirection that might have been requested in the test.
522
		// Note: Ideally a clean Controller should be created for each test.
523
		// Now all tests executed in a batch share the same controller.
524
		$controller = Controller::has_curr() ? Controller::curr() : null;
525
		if ( $controller && $controller->response && $controller->response->getHeader('Location') ) {
0 ignored issues
show
Documentation introduced by
The property $response is declared protected in Controller. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
526
			$controller->response->setStatusCode(200);
0 ignored issues
show
Documentation introduced by
The property $response is declared protected in Controller. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
527
			$controller->response->removeHeader('Location');
0 ignored issues
show
Documentation introduced by
The property $response is declared protected in Controller. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
528
		}
529
530
		\Versioned::set_reading_mode($this->originalReadingMode);
531
532
		//unnest injector / config now that tests are over
533
		Injector::unnest();
534
		Config::unnest();
535
	}
536
537
	public static function assertContains(
538
		$needle,
539
		$haystack,
540
		$message = '',
541
		$ignoreCase = FALSE,
542
		$checkForObjectIdentity = TRUE,
543
		$checkForNonObjectIdentity = false
544
	) {
545
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
546
		parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
547
	}
548
549
	public static function assertNotContains(
550
		$needle,
551
		$haystack,
552
		$message = '',
553
		$ignoreCase = FALSE,
554
		$checkForObjectIdentity = TRUE,
555
		$checkForNonObjectIdentity = false
556
	) {
557
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
558
		parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
559
	}
560
561
	/**
562
	 * Clear the log of emails sent
563
	 */
564
	public function clearEmails() {
565
		return $this->mailer->clearEmails();
566
	}
567
568
	/**
569
	 * Search for an email that was sent.
570
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
571
	 * @param $to
572
	 * @param $from
573
	 * @param $subject
574
	 * @param $content
575
	 * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
576
	 *               'customHeaders', 'htmlContent', 'inlineImages'
577
	 */
578
	public function findEmail($to, $from = null, $subject = null, $content = null) {
579
		return $this->mailer->findEmail($to, $from, $subject, $content);
580
	}
581
582
	/**
583
	 * Assert that the matching email was sent since the last call to clearEmails()
584
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
585
	 * @param $to
586
	 * @param $from
587
	 * @param $subject
588
	 * @param $content
589
	 * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
590
	 *               'customHeaders', 'htmlContent', inlineImages'
591
	 */
592
	public function assertEmailSent($to, $from = null, $subject = null, $content = null) {
593
		$found = (bool)$this->findEmail($to, $from, $subject, $content);
594
595
		$infoParts = "";
596
		$withParts = array();
597
		if($to) $infoParts .= " to '$to'";
598
		if($from) $infoParts .= " from '$from'";
599
		if($subject) $withParts[] = "subject '$subject'";
600
		if($content) $withParts[] = "content '$content'";
601
		if($withParts) $infoParts .= " with " . implode(" and ", $withParts);
0 ignored issues
show
Bug Best Practice introduced by
The expression $withParts 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...
602
603
		$this->assertTrue(
604
			$found,
605
			"Failed asserting that an email was sent$infoParts."
606
		);
607
	}
608
609
610
	/**
611
	 * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
612
	 * pairs.  Each match must correspond to 1 distinct record.
613
	 *
614
	 * @param $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
615
	 * either pass a single pattern or an array of patterns.
616
	 * @param $dataObjectSet The {@link SS_List} to test.
617
	 *
618
	 * Examples
619
	 * --------
620
	 * Check that $members includes an entry with Email = [email protected]:
621
	 *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
622
	 *
623
	 * Check that $members includes entries with Email = [email protected] and with
624
	 * Email = [email protected]:
625
	 *      $this->assertDOSContains(array(
626
	 *         array('Email' => '[email protected]'),
627
	 *         array('Email' => '[email protected]'),
628
	 *      ), $members);
629
	 */
630
	public function assertDOSContains($matches, $dataObjectSet) {
631
		$extracted = array();
632
		foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
633
634
		foreach($matches as $match) {
635
			$matched = false;
636
			foreach($extracted as $i => $item) {
637
				if($this->dataObjectArrayMatch($item, $match)) {
638
					// Remove it from $extracted so that we don't get duplicate mapping.
639
					unset($extracted[$i]);
640
					$matched = true;
641
					break;
642
				}
643
			}
644
645
			// We couldn't find a match - assertion failed
646
			$this->assertTrue(
647
				$matched,
648
				"Failed asserting that the SS_List contains an item matching "
649
				. var_export($match, true) . "\n\nIn the following SS_List:\n"
650
				. $this->DOSSummaryForMatch($dataObjectSet, $match)
651
			);
652
		}
653
	}
654
655
	/**
656
	 * Assert that the given {@link SS_List} includes only DataObjects matching the given
657
	 * key-value pairs.  Each match must correspond to 1 distinct record.
658
	 *
659
	 * Example
660
	 * --------
661
	 * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
662
	 * matter:
663
	 *     $this->assertDOSEquals(array(
664
	 *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
665
	 *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
666
	 *      ), $members);
667
	 *
668
	 * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
669
	 * either pass a single pattern or an array of patterns.
670
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
671
	 */
672
	public function assertDOSEquals($matches, $dataObjectSet) {
673
		// Extract dataobjects
674
		$extracted = array();
675
		if($dataObjectSet) {
676
			foreach ($dataObjectSet as $item) {
677
				/** @var DataObject $item */
678
				$extracted[] = $item->toMap();
679
			}
680
		}
681
682
		// Check all matches
683
		if($matches) {
684
			foreach ($matches as $match) {
685
				$matched = false;
686
				foreach ($extracted as $i => $item) {
687
					if ($this->dataObjectArrayMatch($item, $match)) {
688
						// Remove it from $extracted so that we don't get duplicate mapping.
689
						unset($extracted[$i]);
690
						$matched = true;
691
						break;
692
					}
693
				}
694
695
				// We couldn't find a match - assertion failed
696
				$this->assertTrue(
697
					$matched,
698
					"Failed asserting that the SS_List contains an item matching "
699
					. var_export($match, true) . "\n\nIn the following SS_List:\n"
700
					. $this->DOSSummaryForMatch($dataObjectSet, $match)
701
				);
702
			}
703
		}
704
705
		// If we have leftovers than the DOS has extra data that shouldn't be there
706
		$this->assertTrue(
707
			(count($extracted) == 0),
708
			// If we didn't break by this point then we couldn't find a match
709
			"Failed asserting that the SS_List contained only the given items, the "
710
			. "following items were left over:\n" . var_export($extracted, true)
711
		);
712
	}
713
714
	/**
715
	 * Assert that the every record in the given {@link SS_List} matches the given key-value
716
	 * pairs.
717
	 *
718
	 * Example
719
	 * --------
720
	 * Check that every entry in $members has a Status of 'Active':
721
	 *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
722
	 *
723
	 * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
724
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
725
	 */
726
	public function assertDOSAllMatch($match, $dataObjectSet) {
727
		$extracted = array();
728
		foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
729
730
		foreach($extracted as $i => $item) {
731
			$this->assertTrue(
732
				$this->dataObjectArrayMatch($item, $match),
733
				"Failed asserting that the the following item matched "
734
				. var_export($match, true) . ": " . var_export($item, true)
735
			);
736
		}
737
	}
738
739
	/**
740
	 * Removes sequences of repeated whitespace characters from SQL queries
741
	 * making them suitable for string comparison
742
	 *
743
	 * @param string $sql
744
	 * @return string The cleaned and normalised SQL string
745
	 */
746
	protected function normaliseSQL($sql) {
747
		return trim(preg_replace('/\s+/m', ' ', $sql));
748
	}
749
750
	/**
751
	 * Asserts that two SQL queries are equivalent
752
	 *
753
	 * @param string $expectedSQL
754
	 * @param string $actualSQL
755
	 * @param string $message
756
	 * @param float $delta
757
	 * @param integer $maxDepth
758
	 * @param boolean $canonicalize
759
	 * @param boolean $ignoreCase
760
	 */
761
	public function assertSQLEquals($expectedSQL, $actualSQL, $message = '', $delta = 0, $maxDepth = 10,
762
		$canonicalize = false, $ignoreCase = false
763
	) {
764
		// Normalise SQL queries to remove patterns of repeating whitespace
765
		$expectedSQL = $this->normaliseSQL($expectedSQL);
766
		$actualSQL = $this->normaliseSQL($actualSQL);
767
768
		$this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
769
	}
770
771
	/**
772
	 * Asserts that a SQL query contains a SQL fragment
773
	 *
774
	 * @param string $needleSQL
775
	 * @param string $haystackSQL
776
	 * @param string $message
777
	 * @param boolean $ignoreCase
778
	 * @param boolean $checkForObjectIdentity
779
	 */
780
	public function assertSQLContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
781
		$checkForObjectIdentity = true
782
	) {
783
		$needleSQL = $this->normaliseSQL($needleSQL);
784
		$haystackSQL = $this->normaliseSQL($haystackSQL);
785
786
		$this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
787
	}
788
789
	/**
790
	 * Asserts that a SQL query contains a SQL fragment
791
	 *
792
	 * @param string $needleSQL
793
	 * @param string $haystackSQL
794
	 * @param string $message
795
	 * @param boolean $ignoreCase
796
	 * @param boolean $checkForObjectIdentity
797
	 */
798
	public function assertSQLNotContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
799
		$checkForObjectIdentity = true
800
	) {
801
		$needleSQL = $this->normaliseSQL($needleSQL);
802
		$haystackSQL = $this->normaliseSQL($haystackSQL);
803
804
		$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
805
	}
806
	/**
807
	 * Helper function for the DOS matchers
808
	 */
809
	private function dataObjectArrayMatch($item, $match) {
810
		foreach($match as $k => $v) {
811
			if(!array_key_exists($k, $item) || $item[$k] != $v) return false;
812
		}
813
		return true;
814
	}
815
816
	/**
817
	 * Helper function for the DOS matchers
818
	 */
819
	private function DOSSummaryForMatch($dataObjectSet, $match) {
820
		$extracted = array();
821
		foreach($dataObjectSet as $item) $extracted[] = array_intersect_key($item->toMap(), $match);
822
		return var_export($extracted, true);
823
	}
824
825
	/**
826
	 * Pushes a class and template manifest instance that include tests onto the
827
	 * top of the loader stacks.
828
	 */
829
	public static function use_test_manifest() {
0 ignored issues
show
Coding Style introduced by
use_test_manifest uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
830
		$flush = true;
831
		if(isset($_GET['flush']) && $_GET['flush'] === '0') {
832
			$flush = false;
833
		}
834
835
		$classManifest = new SS_ClassManifest(
836
			BASE_PATH, true, $flush
837
		);
838
839
		SS_ClassLoader::instance()->pushManifest($classManifest, false);
840
		SapphireTest::set_test_class_manifest($classManifest);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
841
842
		SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
843
			BASE_PATH, project(), true, $flush
844
		));
845
846
		Config::inst()->pushConfigStaticManifest(new SS_ConfigStaticManifest(
847
			BASE_PATH, true, $flush
0 ignored issues
show
Unused Code introduced by
The call to SS_ConfigStaticManifest::__construct() has too many arguments starting with BASE_PATH.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
848
		));
849
850
		// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
851
		// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
852
		DataObject::reset();
853
	}
854
855
	/**
856
	 * Returns true if we are currently using a temporary database
857
	 */
858
	public static function using_temp_db() {
859
		$dbConn = DB::get_conn();
860
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
861
		return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
862
			== strtolower(sprintf('%stmpdb', $prefix)));
863
	}
864
865
	public static function kill_temp_db() {
866
		// Delete our temporary database
867
		if(self::using_temp_db()) {
868
			$dbConn = DB::get_conn();
869
			$dbName = $dbConn->getSelectedDatabase();
870
			if($dbName && DB::get_conn()->databaseExists($dbName)) {
871
				// Some DataExtensions keep a static cache of information that needs to
872
				// be reset whenever the database is killed
873
				foreach(ClassInfo::subclassesFor('DataExtension') as $class) {
0 ignored issues
show
Bug introduced by
The expression \ClassInfo::subclassesFor('DataExtension') of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
874
					$toCall = array($class, 'on_db_reset');
875
					if(is_callable($toCall)) call_user_func($toCall);
876
				}
877
878
				// echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
879
				$dbConn->dropSelectedDatabase();
880
			}
881
		}
882
	}
883
884
	/**
885
	 * Remove all content from the temporary database.
886
	 */
887
	public static function empty_temp_db() {
888
		if(self::using_temp_db()) {
889
			DB::get_conn()->clearAllData();
890
891
			// Some DataExtensions keep a static cache of information that needs to
892
			// be reset whenever the database is cleaned out
893
			$classes = array_merge(ClassInfo::subclassesFor('DataExtension'), ClassInfo::subclassesFor('DataObject'));
894
			foreach($classes as $class) {
895
				$toCall = array($class, 'on_db_reset');
896
				if(is_callable($toCall)) call_user_func($toCall);
897
			}
898
		}
899
	}
900
901
	public static function create_temp_db() {
902
		// Disable PHPUnit error handling
903
		restore_error_handler();
904
905
		// Create a temporary database, and force the connection to use UTC for time
906
		global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
907
		$databaseConfig['timezone'] = '+0:00';
908
		DB::connect($databaseConfig);
909
		$dbConn = DB::get_conn();
910
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
911
		$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
912
		while(!$dbname || $dbConn->databaseExists($dbname)) {
913
			$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
914
		}
915
916
		$dbConn->selectDatabase($dbname, true);
917
918
		$st = Injector::inst()->create('SapphireTest');
919
		$st->resetDBSchema();
920
921
		// Reinstate PHPUnit error handling
922
		set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
923
924
		return $dbname;
925
	}
926
927
	public static function delete_all_temp_dbs() {
928
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
929
		foreach(DB::get_schema()->databaseList() as $dbName) {
930
			if(preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
931
				DB::get_schema()->dropDatabase($dbName);
932
				if(Director::is_cli()) {
933
					echo "Dropped database \"$dbName\"" . PHP_EOL;
934
				} else {
935
					echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
936
				}
937
				flush();
938
			}
939
		}
940
	}
941
942
	/**
943
	 * Reset the testing database's schema.
944
	 * @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included
945
	 */
946
	public function resetDBSchema($includeExtraDataObjects = false) {
947
		if(self::using_temp_db()) {
948
			DataObject::reset();
949
950
			// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
951
			Injector::inst()->unregisterAllObjects();
952
953
			$dataClasses = ClassInfo::subclassesFor('DataObject');
954
			array_shift($dataClasses);
955
956
			DB::quiet();
957
			$schema = DB::get_schema();
958
			$extraDataObjects = $includeExtraDataObjects ? $this->extraDataObjects : null;
959
			$schema->schemaUpdate(function() use($dataClasses, $extraDataObjects){
960
				foreach($dataClasses as $dataClass) {
961
					// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
962
					if(class_exists($dataClass)) {
963
						$SNG = singleton($dataClass);
964
						if(!($SNG instanceof TestOnly)) $SNG->requireTable();
965
					}
966
				}
967
968
				// If we have additional dataobjects which need schema, do so here:
969
				if($extraDataObjects) {
970
					foreach($extraDataObjects as $dataClass) {
971
						$SNG = singleton($dataClass);
972
						if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
973
					}
974
				}
975
			});
976
977
			ClassInfo::reset_db_cache();
978
			singleton('DataObject')->flushCache();
979
		}
980
	}
981
982
	/**
983
	 * Create a member and group with the given permission code, and log in with it.
984
	 * Returns the member ID.
985
	 *
986
	 * @param string|array $permCode Either a permission, or list of permissions
987
	 * @return int Member ID
988
	 */
989
	public function logInWithPermission($permCode = "ADMIN") {
990
		if(is_array($permCode)) {
991
			$permArray = $permCode;
992
			$permCode = implode('.', $permCode);
993
		} else {
994
			$permArray = array($permCode);
995
		}
996
997
		// Check cached member
998
		if(isset($this->cache_generatedMembers[$permCode])) {
999
			$member = $this->cache_generatedMembers[$permCode];
1000
		} else {
1001
			// Generate group with these permissions
1002
			$group = Group::create();
1003
			$group->Title = "$permCode group";
1004
			$group->write();
1005
1006
			// Create each individual permission
1007
			foreach($permArray as $permArrayItem) {
1008
				$permission = Permission::create();
1009
				$permission->Code = $permArrayItem;
1010
				$permission->write();
1011
				$group->Permissions()->add($permission);
1012
			}
1013
1014
			$member = DataObject::get_one('Member', array(
1015
				'"Member"."Email"' => "[email protected]"
1016
			));
1017
			if (!$member) {
1018
				$member = Member::create();
1019
			}
1020
1021
			$member->FirstName = $permCode;
0 ignored issues
show
Documentation introduced by
The property FirstName does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1022
			$member->Surname = "User";
0 ignored issues
show
Documentation introduced by
The property Surname does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1023
			$member->Email = "[email protected]";
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1024
			$member->write();
1025
			$group->Members()->add($member);
1026
1027
			$this->cache_generatedMembers[$permCode] = $member;
1028
		}
1029
		$member->logIn();
1030
		return $member->ID;
1031
	}
1032
1033
	/**
1034
	 * Cache for logInWithPermission()
1035
	 */
1036
	protected $cache_generatedMembers = array();
1037
1038
1039
	/**
1040
	 * Test against a theme.
1041
	 *
1042
	 * @param $themeBaseDir string - themes directory
1043
	 * @param $theme string - theme name
1044
	 * @param $callback Closure
1045
	 */
1046
	protected function useTestTheme($themeBaseDir, $theme, $callback) {
1047
		Config::nest();
1048
		global $project;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1049
1050
		$manifest = new SS_TemplateManifest($themeBaseDir, $project, true, true);
1051
1052
		SS_TemplateLoader::instance()->pushManifest($manifest);
1053
1054
		Config::inst()->update('SSViewer', 'theme', $theme);
1055
1056
		$e = null;
1057
1058
		try { $callback(); }
1059
		catch (Exception $e) { /* NOP for now, just save $e */ }
1060
1061
		// Remove all the test themes we created
1062
		SS_TemplateLoader::instance()->popManifest();
1063
1064
		Config::unnest();
1065
1066
		if ($e) throw $e;
1067
	}
1068
1069
}
1070
1071
1072