1<?php 2 3use dokuwiki\Extension\Event; 4use dokuwiki\Extension\EventHandler; 5 6if(!class_exists('PHPUnit_Framework_TestCase')) { 7 /** 8 * phpunit 5/6 compatibility 9 */ 10 class PHPUnit_Framework_TestCase extends PHPUnit\Framework\TestCase { 11 /** 12 * setExpectedException is deprecated in PHPUnit 6 13 * 14 * @param string $class 15 * @param null|string $message 16 */ 17 public function setExpectedException($class, $message=null) { 18 $this->expectException($class); 19 if(!is_null($message)) { 20 $this->expectExceptionMessage($message); 21 } 22 } 23 } 24} 25 26/** 27 * Helper class to provide basic functionality for tests 28 * 29 * @uses PHPUnit_Framework_TestCase and thus PHPUnit 5.7+ is required 30 */ 31abstract class DokuWikiTest extends PHPUnit_Framework_TestCase { 32 33 /** 34 * tests can override this 35 * 36 * @var array plugins to enable for test class 37 */ 38 protected $pluginsEnabled = array(); 39 40 /** 41 * tests can override this 42 * 43 * @var array plugins to disable for test class 44 */ 45 protected $pluginsDisabled = array(); 46 47 /** 48 * Setup the data directory 49 * 50 * This is ran before each test class 51 */ 52 public static function setUpBeforeClass() { 53 // just to be safe not to delete something undefined later 54 if(!defined('TMP_DIR')) die('no temporary directory'); 55 if(!defined('DOKU_TMP_DATA')) die('no temporary data directory'); 56 57 self::setupDataDir(); 58 self::setupConfDir(); 59 } 60 61 /** 62 * Reset the DokuWiki environment before each test run. Makes sure loaded config, 63 * language and plugins are correct. 64 * 65 * @throws Exception if plugin actions fail 66 * @return void 67 */ 68 public function setUp() { 69 70 // reload config 71 global $conf, $config_cascade; 72 $conf = array(); 73 foreach (array('default','local','protected') as $config_group) { 74 if (empty($config_cascade['main'][$config_group])) continue; 75 foreach ($config_cascade['main'][$config_group] as $config_file) { 76 if (file_exists($config_file)) { 77 include($config_file); 78 } 79 } 80 } 81 82 // reload license config 83 global $license; 84 $license = array(); 85 86 // load the license file(s) 87 foreach (array('default','local') as $config_group) { 88 if (empty($config_cascade['license'][$config_group])) continue; 89 foreach ($config_cascade['license'][$config_group] as $config_file) { 90 if(file_exists($config_file)){ 91 include($config_file); 92 } 93 } 94 } 95 // reload some settings 96 $conf['gzip_output'] &= (strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip') !== false); 97 98 if($conf['compression'] == 'bz2' && !DOKU_HAS_BZIP) { 99 $conf['compression'] = 'gz'; 100 } 101 if($conf['compression'] == 'gz' && !DOKU_HAS_GZIP) { 102 $conf['compression'] = 0; 103 } 104 // make real paths and check them 105 init_paths(); 106 init_files(); 107 108 // reset loaded plugins 109 global $plugin_controller_class, $plugin_controller; 110 /** @var Doku_Plugin_Controller $plugin_controller */ 111 $plugin_controller = new $plugin_controller_class(); 112 113 // disable all non-default plugins 114 global $default_plugins; 115 foreach ($plugin_controller->getList() as $plugin) { 116 if (!in_array($plugin, $default_plugins)) { 117 if (!$plugin_controller->disable($plugin)) { 118 throw new Exception('Could not disable plugin "'.$plugin.'"!'); 119 } 120 } 121 } 122 123 // disable and enable configured plugins 124 foreach ($this->pluginsDisabled as $plugin) { 125 if (!$plugin_controller->disable($plugin)) { 126 throw new Exception('Could not disable plugin "'.$plugin.'"!'); 127 } 128 } 129 foreach ($this->pluginsEnabled as $plugin) { 130 /* enable() returns false but works... 131 if (!$plugin_controller->enable($plugin)) { 132 throw new Exception('Could not enable plugin "'.$plugin.'"!'); 133 } 134 */ 135 $plugin_controller->enable($plugin); 136 } 137 138 // reset event handler 139 global $EVENT_HANDLER; 140 $EVENT_HANDLER = new EventHandler(); 141 142 // reload language 143 $local = $conf['lang']; 144 Event::createAndTrigger('INIT_LANG_LOAD', $local, 'init_lang', true); 145 146 global $INPUT; 147 $INPUT = new \dokuwiki\Input\Input(); 148 } 149 150 /** 151 * Reinitialize the data directory for this class run 152 */ 153 public static function setupDataDir() { 154 // remove any leftovers from the last run 155 if(is_dir(DOKU_TMP_DATA)) { 156 // clear indexer data and cache 157 idx_get_indexer()->clear(); 158 TestUtils::rdelete(DOKU_TMP_DATA); 159 } 160 161 // populate default dirs 162 TestUtils::rcopy(TMP_DIR, __DIR__ . '/../data/'); 163 } 164 165 /** 166 * Reinitialize the conf directory for this class run 167 */ 168 public static function setupConfDir() { 169 $defaults = [ 170 'acronyms.conf', 171 'dokuwiki.php', 172 'entities.conf', 173 'interwiki.conf', 174 'license.php', 175 'manifest.json', 176 'mediameta.php', 177 'mime.conf', 178 'plugins.php', 179 'plugins.required.php', 180 'scheme.conf', 181 'smileys.conf', 182 'wordblock.conf' 183 ]; 184 185 // clear any leftovers 186 if(is_dir(DOKU_CONF)) { 187 TestUtils::rdelete(DOKU_CONF); 188 } 189 mkdir(DOKU_CONF); 190 191 // copy defaults 192 foreach($defaults as $file) { 193 copy(DOKU_INC . '/conf/' . $file, DOKU_CONF . $file); 194 } 195 196 // copy test files 197 TestUtils::rcopy(TMP_DIR, __DIR__ . '/../conf'); 198 } 199 200 /** 201 * Waits until a new second has passed 202 * 203 * The very first call will return immeadiately, proceeding calls will return 204 * only after at least 1 second after the last call has passed. 205 * 206 * When passing $init=true it will not return immeadiately but use the current 207 * second as initialization. It might still return faster than a second. 208 * 209 * @param bool $init wait from now on, not from last time 210 * @return int new timestamp 211 */ 212 protected function waitForTick($init = false) { 213 static $last = 0; 214 if($init) $last = time(); 215 while($last === $now = time()) { 216 usleep(100000); //recheck in a 10th of a second 217 } 218 $last = $now; 219 return $now; 220 } 221 222 /** 223 * Allow for testing inaccessible methods (private or protected) 224 * 225 * This makes it easier to test protected methods without needing to create intermediate 226 * classes inheriting and changing the access. 227 * 228 * @link https://stackoverflow.com/a/8702347/172068 229 * @param object $obj Object in which to call the method 230 * @param string $func The method to call 231 * @param array $args The arguments to call the method with 232 * @return mixed 233 * @throws ReflectionException when the given obj/func does not exist 234 */ 235 protected static function callInaccessibleMethod($obj, $func, array $args) { 236 $class = new \ReflectionClass($obj); 237 $method = $class->getMethod($func); 238 $method->setAccessible(true); 239 return $method->invokeArgs($obj, $args); 240 } 241 242 /** 243 * Allow for reading inaccessible properties (private or protected) 244 * 245 * This makes it easier to check internals of tested objects. This should generally 246 * be avoided. 247 * 248 * @param object $obj Object on which to access the property 249 * @param string $prop name of the property to access 250 * @return mixed 251 * @throws ReflectionException when the given obj/prop does not exist 252 */ 253 protected static function getInaccessibleProperty($obj, $prop) { 254 $class = new \ReflectionClass($obj); 255 $property = $class->getProperty($prop); 256 $property->setAccessible(true); 257 return $property->getValue($obj); 258 } 259 260 /** 261 * Allow for reading inaccessible properties (private or protected) 262 * 263 * This makes it easier to set internals of tested objects. This should generally 264 * be avoided. 265 * 266 * @param object $obj Object on which to access the property 267 * @param string $prop name of the property to access 268 * @param mixed $value new value to set the property to 269 * @return void 270 * @throws ReflectionException when the given obj/prop does not exist 271 */ 272 protected static function setInaccessibleProperty($obj, $prop, $value) { 273 $class = new \ReflectionClass($obj); 274 $property = $class->getProperty($prop); 275 $property->setAccessible(true); 276 $property->setValue($obj, $value); 277 } 278} 279