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