Version: current

      1 <?php
      2 require_once 'TestRunner.php';
      3 if(hasPhpUnit()) {
      4 require_once 'PHPUnit/Framework.php';
      5 }
      6
      7 /**
      8  * Test case class for the Sapphire framework.
      9  * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier to work with.
     10  *
     11  * @package sapphire
     12  * @subpackage testing
     13  */
     14 class SapphireTest extends PHPUnit_Framework_TestCase {
     15     /**
     16      * Path to fixture data for this test run.
     17      * If passed as an array, multiple fixture files will be loaded.
     18      * Please note that you won't be able to refer with "=>" notation
     19      * between the fixtures, they act independent of each other.
     20      *
     21      * @var string|array
     22      */
     23     static $fixture_file = null;
     24
     25     protected $originalMailer;
     26     protected $originalMemberPasswordValidator;
     27     protected $originalRequirements;
     28     protected $originalIsRunningTest;
     29     protected $originalTheme;
     30     protected $originalNestedURLsState;
     31
     32     protected $mailer;
     33
     34     protected static $is_running_test = false;
     35
     36     /**
     37      * By default, setUp() does not require default records. Pass
     38      * class names in here, and the require/augment default records
     39      * function will be called on them.
     40      */
     41     protected $requireDefaultRecordsFrom = array();
     42
     43
     44     /**
     45      * A list of extensions that can't be applied during the execution of this run.  If they are
     46      * applied, they will be temporarily removed and a database migration called.
     47      *
     48      * The keys of the are the classes that the extensions can't be applied the extensions to, and
     49      * the values are an array of illegal extensions on that class.
     50      */
     51     protected $illegalExtensions = array(
     52     );
     53
     54     /**
     55      * A list of extensions that must be applied during the execution of this run.  If they are
     56      * not applied, they will be temporarily added and a database migration called.
     57      *
     58      * The keys of the are the classes to apply the extensions to, and the values are an array
     59      * of illegal required extensions on that class.
     60      */
     61     protected $requiredExtensions = array(
     62     );
     63
     64     /**
     65      * By default, the test database won't contain any DataObjects that have the interface TestOnly.
     66      * This variable lets you define additional TestOnly DataObjects to set up for this test.
     67      * Set it to an array of DataObject subclass names.
     68      */
     69     protected $extraDataObjects = array();
     70
     71     /**
     72      * We need to disabling backing up of globals to avoid overriding
     73      * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
     74      *
     75      * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
     76      */
     77     protected $backupGlobals = FALSE;
     78
     79     /**
     80      * Helper arrays for illegalExtensions/requiredExtensions code
     81      */
     82     private $extensionsToReapply = array(), $extensionsToRemove = array();
     83
     84     public static function is_running_test() {
     85         return self::$is_running_test;
     86     }
     87
     88     /**
     89      * @var array $fixtures Array of {@link YamlFixture} instances
     90      */
     91     protected $fixtures;
     92
     93     function setUp() {
     94         // Mark test as being run
     95         $this->originalIsRunningTest = self::$is_running_test;
     96         self::$is_running_test = true;
     97
     98         // Remove password validation
     99         $this->originalMemberPasswordValidator = Member::password_validator();
    100         $this->originalRequirements = Requirements::backend();
    101         Member::set_password_validator(null);
    102         Cookie::set_report_errors(false);
    103
    104         RootURLController::reset();
    105         Translatable::reset();
    106         Versioned::reset();
    107         DataObject::reset();
    108         SiteTree::reset();
    109         Controller::curr()->setSession(new Session(array()));
    110
    111         $this->originalTheme = SSViewer::current_theme();
    112
    113         // Save nested_urls state, so we can restore it later
    114         $this->originalNestedURLsState = SiteTree::nested_urls();
    115
    116         $className = get_class($this);
    117         $fixtureFile = eval("return {$className}::\$fixture_file;");
    118
    119         // Set up fixture
    120         if($fixtureFile || !self::using_temp_db()) {
    121             if(substr(DB::getConn()->currentDatabase(),0,5) != 'tmpdb') {
    122                 //echo "Re-creating temp database... ";
    123                 self::create_temp_db();
    124                 //echo "done.\n";
    125             }
    126
    127             singleton('DataObject')->flushCache();
    128
    129             self::empty_temp_db();
    130
    131             foreach($this->requireDefaultRecordsFrom as $className) {
    132                 $instance = singleton($className);
    133                 if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
    134                 if (method_exists($instance, 'augmentDefaultRecords')) $instance->augmentDefaultRecords();
    135             }
    136
    137             if($fixtureFile) {
    138                 $fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : array($fixtureFile);
    139
    140                 $i = 0;
    141                 foreach($fixtureFiles as $fixtureFilePath) {
    142                     $fixture = new YamlFixture($fixtureFilePath);
    143                     $fixture->saveIntoDatabase();
    144                     $this->fixtures[] = $fixture;
    145
    146                     // backwards compatibility: Load first fixture into $this->fixture
    147                     if($i == 0) $this->fixture = $fixture;
    148                     $i++;
    149                 }
    150             }
    151
    152             $this->logInWithPermission("ADMIN");
    153         }
    154
    155         // Set up email
    156         $this->originalMailer = Email::mailer();
    157         $this->mailer = new TestMailer();
    158         Email::set_mailer($this->mailer);
    159         Email::send_all_emails_to(null);
    160     }
    161
    162     /**
    163      * Called once per test case ({@link SapphireTest} subclass).
    164      * This is different to {@link setUp()}, which gets called once
    165      * per method. Useful to initialize expensive operations which
    166      * don't change state for any called method inside the test,
    167      * e.g. dynamically adding an extension. See {@link tearDownOnce()}
    168      * for tearing down the state again.
    169      */
    170     function setUpOnce() {
    171         // Remove any illegal extensions that are present
    172         foreach($this->illegalExtensions as $class => $extensions) {
    173             foreach($extensions as $extension) {
    174                 if (Object::has_extension($class, $extension)) {
    175                     if(!isset($this->extensionsToReapply[$class])) $this->extensionsToReapply[$class] = array();
    176                     $this->extensionsToReapply[$class][] = $extension;
    177                     Object::remove_extension($class, $extension);
    178                     $isAltered = true;
    179                 }
    180             }
    181         }
    182
    183         // Add any required extensions that aren't present
    184         foreach($this->requiredExtensions as $class => $extensions) {
    185             $this->extensionsToRemove[$class] = array();
    186             foreach($extensions as $extension) {
    187                 if(!Object::has_extension($class, $extension)) {
    188                     if(!isset($this->extensionsToRemove[$class])) $this->extensionsToReapply[$class] = array();
    189                     $this->extensionsToRemove[$class][] = $extension;
    190                     Object::add_extension($class, $extension);
    191                     $isAltered = true;
    192                 }
    193             }
    194         }
    195
    196         // If we have made changes to the extensions present, then migrate the database schema.
    197         if($this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
    198             if(!self::using_temp_db()) self::create_temp_db();
    199             $this->resetDBSchema(true);
    200         }
    201         // clear singletons, they're caching old extension info
    202         // which is used in DatabaseAdmin->doBuild()
    203         global $_SINGLETONS;
    204         $_SINGLETONS = array();
    205     }
    206
    207     /**
    208      * tearDown method that's called once per test class rather once per test method.
    209      */
    210     function tearDownOnce() {
    211         // If we have made changes to the extensions present, then migrate the database schema.
    212         if($this->extensionsToReapply || $this->extensionsToRemove) {
    213             // Remove extensions added for testing
    214             foreach($this->extensionsToRemove as $class => $extensions) {
    215                 foreach($extensions as $extension) {
    216                     Object::remove_extension($class, $extension);
    217                 }
    218             }
    219
    220             // Reapply ones removed
    221             foreach($this->extensionsToReapply as $class => $extensions) {
    222                 foreach($extensions as $extension) {
    223                     Object::add_extension($class, $extension);
    224                 }
    225             }
    226         }
    227
    228         if($this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
    229             $this->resetDBSchema();
    230         }
    231     }
    232
    233     /**
    234      * Array
    235      */
    236     protected $fixtureDictionary;
    237
    238
    239     /**
    240      * Get the ID of an object from the fixture.
    241      * @param $className The data class, as specified in your fixture file.  Parent classes won't work
    242      * @param $identifier The identifier string, as provided in your fixture file
    243      * @return int
    244      */
    245     protected function idFromFixture($className, $identifier) {
    246         if(!$this->fixtures) {
    247             user_error("You've called idFromFixture() but you haven't specified static \$fixture_file.\n", E_USER_WARNING);
    248             return;
    249         }
    250
    251         foreach($this->fixtures as $fixture) {
    252             $match = $fixture->idFromFixture($className, $identifier);
    253             if($match) return $match;
    254         }
    255
    256         $fixtureFiles = Object::get_static(get_class($this), 'fixture_file');
    257         user_error(sprintf(
    258             "Couldn't find object '%s' (class: %s) in files %s",
    259             $identifier,
    260             $className,
    261             (is_array($fixtureFiles)) ? implode(',', $fixtureFiles) : $fixtureFiles
    262         ), E_USER_ERROR);
    263
    264         return false;
    265     }
    266
    267     /**
    268      * Return all of the IDs in the fixture of a particular class name.
    269      * Will collate all IDs form all fixtures if multiple fixtures are provided.
    270      *
    271      * @param string $className
    272      * @return A map of fixture-identifier => object-id
    273      */
    274     protected function allFixtureIDs($className) {
    275         if(!$this->fixtures) {
    276             user_error("You've called allFixtureIDs() but you haven't specified static \$fixture_file.\n", E_USER_WARNING);
    277             return;
    278         }
    279
    280         $ids = array();
    281         foreach($this->fixtures as $fixture) {
    282             $ids += $fixture->allFixtureIDs($className);
    283         }
    284
    285         return $ids;
    286     }
    287
    288     /**
    289      * Get an object from the fixture.
    290      * @param $className The data class, as specified in your fixture file.  Parent classes won't work
    291      * @param $identifier The identifier string, as provided in your fixture file
    292      */
    293     protected function objFromFixture($className, $identifier) {
    294         if(!$this->fixtures) {
    295             user_error("You've called objFromFixture() but you haven't specified static \$fixture_file.\n", E_USER_WARNING);
    296             return;
    297         }
    298
    299         foreach($this->fixtures as $fixture) {
    300             $match = $fixture->objFromFixture($className, $identifier);
    301             if($match) return $match;
    302         }
    303
    304         $fixtureFiles = Object::get_static(get_class($this), 'fixture_file');
    305         user_error(sprintf(
    306             "Couldn't find object '%s' (class: %s) in files %s",
    307             $identifier,
    308             $className,
    309             (is_array($fixtureFiles)) ? implode(',', $fixtureFiles) : $fixtureFiles
    310         ), E_USER_ERROR);
    311
    312         return false;
    313     }
    314
    315     /**
    316      * Load a YAML fixture file into the database.
    317      * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
    318      * Doesn't clear existing fixtures.
    319      *
    320      * @param $fixtureFile The location of the .yml fixture file, relative to the site base dir
    321      */
    322     function loadFixture($fixtureFile) {
    323         $parser = new Spyc();
    324         $fixtureContent = $parser->load(Director::baseFolder().'/'.$fixtureFile);
    325
    326         $fixture = new YamlFixture($fixtureFile);
    327         $fixture->saveIntoDatabase();
    328         $this->fixtures[] = $fixture;
    329     }
    330
    331     /**
    332      * Clear all fixtures which were previously loaded through
    333      * {@link loadFixture()}.
    334      */
    335     function clearFixtures() {
    336         $this->fixtures = array();
    337     }
    338
    339     function tearDown() {
    340         // Restore email configuration
    341         Email::set_mailer($this->originalMailer);
    342         $this->originalMailer = null;
    343         $this->mailer = null;
    344
    345         // Restore password validation
    346         Member::set_password_validator($this->originalMemberPasswordValidator);
    347
    348         // Restore requirements
    349         Requirements::set_backend($this->originalRequirements);
    350
    351         // Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
    352         self::$is_running_test = $this->originalIsRunningTest;
    353         $this->originalIsRunningTest = null;
    354
    355         // Reset theme setting
    356         SSViewer::set_theme($this->originalTheme);
    357
    358         // Reset mocked datetime
    359         SS_Datetime::clear_mock_now();
    360
    361         // Restore nested_urls state
    362         if ( $this->originalNestedURLsState )
    363             SiteTree::enable_nested_urls();
    364         else
    365             SiteTree::disable_nested_urls();
    366
    367         // Stop the redirection that might have been requested in the test.
    368         // Note: Ideally a clean Controller should be created for each test.
    369         // Now all tests executed in a batch share the same controller.
    370         $controller = Controller::curr();
    371         if ( $controller && $controller->response && $controller->response->getHeader('Location') ) {
    372             $controller->response->setStatusCode(200);
    373             $controller->response->removeHeader('Location');
    374         }
    375     }
    376     /**
    377      * Clear the log of emails sent
    378      */
    379     function clearEmails() {
    380         return $this->mailer->clearEmails();
    381     }
    382
    383     /**
    384      * Search for an email that was sent.
    385      * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
    386      * @param $to
    387      * @param $from
    388      * @param $subject
    389      * @param $content
    390      * @return An array containing the keys: 'type','to','from','subject','content', 'plainContent','attachedFiles','customHeaders','htmlContent',inlineImages'
    391      */
    392     function findEmail($to, $from = null, $subject = null, $content = null) {
    393         return $this->mailer->findEmail($to, $from, $subject, $content);
    394     }
    395
    396     /**
    397      * Assert that the matching email was sent since the last call to clearEmails()
    398      * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
    399      * @param $to
    400      * @param $from
    401      * @param $subject
    402      * @param $content
    403      * @return An array containing the keys: 'type','to','from','subject','content', 'plainContent','attachedFiles','customHeaders','htmlContent',inlineImages'
    404      */
    405     function assertEmailSent($to, $from = null, $subject = null, $content = null) {
    406         // To do - this needs to be turned into a "real" PHPUnit ass
    407         if(!$this->findEmail($to, $from, $subject, $content)) {
    408
    409             $infoParts = "";
    410             $withParts = array();
    411             if($to) $infoParts .= " to '$to'";
    412             if($from) $infoParts .= " from '$from'";
    413             if($subject) $withParts[] = "subject '$subject'";
    414             if($content) $withParts[] = "content '$content'";
    415             if($withParts) $infoParts .= " with " . implode(" and ", $withParts);
    416
    417             throw new PHPUnit_Framework_AssertionFailedError(
    418                 "Failed asserting that an email was sent$infoParts."
    419             );
    420         }
    421     }
    422
    423
    424     /**
    425      * Assert that the given {@link DataObjectSet} includes DataObjects matching the given key-value
    426      * pairs.  Each match must correspond to 1 distinct record.
    427      *
    428      * @param $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
    429      * either pass a single pattern or an array of patterns.
    430      * @param $dataObjectSet The {@link DataObjectSet} to test.
    431      *
    432      * Examples
    433      * --------
    434      * Check that $members includes an entry with Email = sam@example.com:
    435      *      $this->assertDOSContains(array('Email' => '...@example.com'), $members);
    436      *
    437      * Check that $members includes entries with Email = sam@example.com and with
    438      * Email = ingo@example.com:
    439      *      $this->assertDOSContains(array(
    440      *         array('Email' => '...@example.com'),
    441      *         array('Email' => 'i...@example.com'),
    442      *      ), $members);
    443      */
    444     function assertDOSContains($matches, $dataObjectSet) {
    445         $extracted = array();
    446         foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
    447
    448         foreach($matches as $match) {
    449             $matched = false;
    450             foreach($extracted as $i => $item) {
    451                 if($this->dataObjectArrayMatch($item, $match)) {
    452                     // Remove it from $extracted so that we don't get duplicate mapping.
    453                     unset($extracted[$i]);
    454                     $matched = true;
    455                     break;
    456                 }
    457             }
    458
    459             // We couldn't find a match - assertion failed
    460             if(!$matched) {
    461                 throw new PHPUnit_Framework_AssertionFailedError(
    462                     "Failed asserting that the DataObjectSet contains an item matching "
    463                         . var_export($match, true) . "\n\nIn the following DataObjectSet:\n"
    464                         . $this->DOSSummaryForMatch($dataObjectSet, $match)
    465                 );
    466             }
    467
    468         }
    469     }
    470
    471     /**
    472      * Assert that the given {@link DataObjectSet} includes only DataObjects matching the given
    473      * key-value pairs.  Each match must correspond to 1 distinct record.
    474      *
    475      * @param $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
    476      * either pass a single pattern or an array of patterns.
    477      * @param $dataObjectSet The {@link DataObjectSet} to test.
    478      *
    479      * Example
    480      * --------
    481      * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
    482      * matter:
    483      *     $this->assertDOSEquals(array(
    484      *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
    485      *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
    486      *      ), $members);
    487      */
    488     function assertDOSEquals($matches, $dataObjectSet) {
    489         if(!$dataObjectSet) return false;
    490
    491         $extracted = array();
    492         foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
    493
    494         foreach($matches as $match) {
    495             $matched = false;
    496             foreach($extracted as $i => $item) {
    497                 if($this->dataObjectArrayMatch($item, $match)) {
    498                     // Remove it from $extracted so that we don't get duplicate mapping.
    499                     unset($extracted[$i]);
    500                     $matched = true;
    501                     break;
    502                 }
    503             }
    504
    505             // We couldn't find a match - assertion failed
    506             if(!$matched) {
    507                 throw new PHPUnit_Framework_AssertionFailedError(
    508                     "Failed asserting that the DataObjectSet contains an item matching "
    509                         . var_export($match, true) . "\n\nIn the following DataObjectSet:\n"
    510                         . $this->DOSSummaryForMatch($dataObjectSet, $match)
    511                 );
    512             }
    513         }
    514
    515         // If we have leftovers than the DOS has extra data that shouldn't be there
    516         if($extracted) {
    517             // If we didn't break by this point then we couldn't find a match
    518             throw new PHPUnit_Framework_AssertionFailedError(
    519                 "Failed asserting that the DataObjectSet contained only the given items, the "
    520                     . "following items were left over:\n" . var_export($extracted, true)
    521             );
    522         }
    523     }
    524
    525     /**
    526      * Assert that the every record in the given {@link DataObjectSet} matches the given key-value
    527      * pairs.
    528      *
    529      * @param $match The pattern to match.  The pattern is a map of key-value pairs.
    530      * @param $dataObjectSet The {@link DataObjectSet} to test.
    531      *
    532      * Example
    533      * --------
    534      * Check that every entry in $members has a Status of 'Active':
    535      *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
    536      */
    537     function assertDOSAllMatch($match, $dataObjectSet) {
    538         $extracted = array();
    539         foreach($dataObjectSet as $item) $extracted[] = $item->toMap();
    540
    541         foreach($extracted as $i => $item) {
    542             if(!$this->dataObjectArrayMatch($item, $match)) {
    543                 throw new PHPUnit_Framework_AssertionFailedError(
    544                     "Failed asserting that the the following item matched "
    545                     . var_export($match, true) . ": " . var_export($item, true)
    546                 );
    547             }
    548         }
    549     }
    550
    551     /**
    552      * Helper function for the DOS matchers
    553      */
    554     private function dataObjectArrayMatch($item, $match) {
    555         foreach($match as $k => $v) {
    556             if(!isset($item[$k]) || $item[$k] != $v) return false;
    557         }
    558         return true;
    559     }
    560
    561     /**
    562      * Helper function for the DOS matchers
    563      */
    564     private function DOSSummaryForMatch($dataObjectSet, $match) {
    565         $extracted = array();
    566         foreach($dataObjectSet as $item) $extracted[] = array_intersect_key($item->toMap(), $match);
    567         return var_export($extracted, true);
    568     }
    569
    570     /**
    571      * Returns true if we are currently using a temporary database
    572      */
    573     static function using_temp_db() {
    574         $dbConn = DB::getConn();
    575         return $dbConn && (substr($dbConn->currentDatabase(),0,5) == 'tmpdb');
    576     }
    577
    578     /**
    579      * @todo Make this db agnostic
    580      */
    581     static function kill_temp_db() {
    582         // Delete our temporary database
    583         if(self::using_temp_db()) {
    584             $dbConn = DB::getConn();
    585             $dbName = $dbConn->currentDatabase();
    586             if($dbName && DB::getConn()->databaseExists($dbName)) {
    587                 // Some DataObjectsDecorators keep a static cache of information that needs to
    588                 // be reset whenever the database is killed
    589                 foreach(ClassInfo::subclassesFor('DataObjectDecorator') as $class) {
    590                     $toCall = array($class, 'on_db_reset');
    591                     if(is_callable($toCall)) call_user_func($toCall);
    592                 }
    593
    594                 // echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
    595                 $dbConn->dropDatabase();
    596             }
    597         }
    598     }
    599
    600     /**
    601      * Remove all content from the temporary database.
    602      */
    603     static function empty_temp_db() {
    604         if(self::using_temp_db()) {
    605             $dbadmin = new DatabaseAdmin();
    606             $dbadmin->clearAllData();
    607
    608             // Some DataObjectsDecorators keep a static cache of information that needs to
    609             // be reset whenever the database is cleaned out
    610             foreach(array_merge(ClassInfo::subclassesFor('DataObjectDecorator'), ClassInfo::subclassesFor('DataObject')) as $class) {
    611                 $toCall = array($class, 'on_db_reset');
    612                 if(is_callable($toCall)) call_user_func($toCall);
    613             }
    614         }
    615     }
    616
    617     /**
    618      * @todo Make this db agnostic
    619      */
    620     static function create_temp_db() {
    621         // Create a temporary database
    622         $dbConn = DB::getConn();
    623         $dbname = 'tmpdb' . rand(1000000,9999999);
    624         while(!$dbname || $dbConn->databaseExists($dbname)) {
    625             $dbname = 'tmpdb' . rand(1000000,9999999);
    626         }
    627
    628         $dbConn->selectDatabase($dbname);
    629         $dbConn->createDatabase();
    630
    631         $st = new SapphireTest();
    632         $st->resetDBSchema();
    633
    634         return $dbname;
    635     }
    636
    637     static function delete_all_temp_dbs() {
    638         foreach(DB::getConn()->allDatabaseNames() as $dbName) {
    639             if(preg_match('/^tmpdb[0-9]+$/', $dbName)) {
    640                 DB::getConn()->dropDatabaseByName($dbName);
    641                 echo "<li>Dropped databse \"$dbName\"\n";
    642                 flush();
    643             }
    644         }
    645     }
    646
    647     /**
    648      * Reset the testing database's schema.
    649      * @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included
    650      */
    651     function resetDBSchema($includeExtraDataObjects = false) {
    652         if(self::using_temp_db()) {
    653             // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
    654             global $_SINGLETONS;
    655             $_SINGLETONS = array();
    656
    657             $dataClasses = ClassInfo::subclassesFor('DataObject');
    658             array_shift($dataClasses);
    659
    660             $conn = DB::getConn();
    661             $conn->beginSchemaUpdate();
    662             DB::quiet();
    663
    664             foreach($dataClasses as $dataClass) {
    665                 // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
    666                 if(class_exists($dataClass)) {
    667                     $SNG = singleton($dataClass);
    668                     if(!($SNG instanceof TestOnly)) $SNG->requireTable();
    669                 }
    670             }
    671
    672             // If we have additional dataobjects which need schema, do so here:
    673             if($includeExtraDataObjects && $this->extraDataObjects) {
    674                 foreach($this->extraDataObjects as $dataClass) {
    675                     $SNG = singleton($dataClass);
    676                     if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
    677                 }
    678             }
    679
    680             $conn->endSchemaUpdate();
    681
    682             ClassInfo::reset_db_cache();
    683             singleton('DataObject')->flushCache();
    684         }
    685     }
    686
    687     /**
    688      * Create a member and group with the given permission code, and log in with it.
    689      * Returns the member ID.
    690      */
    691     function logInWithPermission($permCode = "ADMIN") {
    692         if(!isset($this->cache_generatedMembers[$permCode])) {
    693             $group = new Group();
    694             $group->Title = "$permCode group";
    695             $group->write();
    696
    697             $permission = new Permission();
    698             $permission->Code = $permCode;
    699             $permission->write();
    700             $group->Permissions()->add($permission);
    701
    702             $member = DataObject::get_one('Member', sprintf('"Email" = \'%s\'', "$permCode@example.org"));
    703             if(!$member) $member = new Member();
    704
    705             $member->FirstName = $permCode;
    706             $member->Surname = "User";
    707             $member->Email = "$permCode@example.org";
    708             $member->write();
    709             $group->Members()->add($member);
    710
    711             $this->cache_generatedMembers[$permCode] = $member;
    712         }
    713
    714         $this->cache_generatedMembers[$permCode]->logIn();
    715     }
    716
    717     /**
    718      * Cache for logInWithPermission()
    719      */
    720     protected $cache_generatedMembers = array();
    721 }
    722
    723 ?>
    724

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -