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