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