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