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