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 $Indexer = Indexer::getInstance(); 164 $Indexer->clear(); 165 TestUtils::rdelete(DOKU_TMP_DATA); 166 } 167 168 // populate default dirs 169 TestUtils::rcopy(TMP_DIR, __DIR__ . '/../data/'); 170 } 171 172 /** 173 * Reinitialize the conf directory for this class run 174 */ 175 public static function setupConfDir() 176 { 177 $defaults = [ 178 'acronyms.conf', 179 'dokuwiki.php', 180 'entities.conf', 181 'interwiki.conf', 182 'license.php', 183 'manifest.json', 184 'mediameta.php', 185 'mime.conf', 186 'plugins.php', 187 'plugins.required.php', 188 'scheme.conf', 189 'smileys.conf', 190 'wordblock.conf' 191 ]; 192 193 // clear any leftovers 194 if (is_dir(DOKU_CONF)) { 195 TestUtils::rdelete(DOKU_CONF); 196 } 197 mkdir(DOKU_CONF); 198 199 // copy defaults 200 foreach ($defaults as $file) { 201 copy(DOKU_INC . '/conf/' . $file, DOKU_CONF . $file); 202 } 203 204 // copy test files 205 TestUtils::rcopy(TMP_DIR, __DIR__ . '/../conf'); 206 } 207 208 /** 209 * Waits until a new second has passed 210 * 211 * This tried to be clever about the passing of time and return early if possible. Unfortunately 212 * this never worked reliably fo unknown reasons. To avoid flaky tests, this now always simply 213 * sleeps for a full second on every call. 214 * 215 * @param bool $init no longer used 216 * @return int new timestamp 217 */ 218 protected function waitForTick($init = false) 219 { 220 sleep(1); 221 return time(); 222 } 223 224 /** 225 * Allow for testing inaccessible methods (private or protected) 226 * 227 * This makes it easier to test protected methods without needing to create intermediate 228 * classes inheriting and changing the access. 229 * 230 * @link https://stackoverflow.com/a/8702347/172068 231 * @param object $obj Object in which to call the method 232 * @param string $func The method to call 233 * @param array $args The arguments to call the method with 234 * @return mixed 235 * @throws ReflectionException when the given obj/func does not exist 236 */ 237 protected static function callInaccessibleMethod($obj, $func, array $args) 238 { 239 $class = new \ReflectionClass($obj); 240 $method = $class->getMethod($func); 241 $method->setAccessible(true); 242 return $method->invokeArgs($obj, $args); 243 } 244 245 /** 246 * Allow for reading inaccessible properties (private or protected) 247 * 248 * This makes it easier to check internals of tested objects. This should generally 249 * be avoided. 250 * 251 * @param object $obj Object on which to access the property 252 * @param string $prop name of the property to access 253 * @return mixed 254 * @throws ReflectionException when the given obj/prop does not exist 255 */ 256 protected static function getInaccessibleProperty($obj, $prop) 257 { 258 $class = new \ReflectionClass($obj); 259 $property = $class->getProperty($prop); 260 $property->setAccessible(true); 261 return $property->getValue($obj); 262 } 263 264 /** 265 * Allow for reading inaccessible properties (private or protected) 266 * 267 * This makes it easier to set internals of tested objects. This should generally 268 * be avoided. 269 * 270 * @param object $obj Object on which to access the property 271 * @param string $prop name of the property to access 272 * @param mixed $value new value to set the property to 273 * @return void 274 * @throws ReflectionException when the given obj/prop does not exist 275 */ 276 protected static function setInaccessibleProperty($obj, $prop, $value) 277 { 278 $class = new \ReflectionClass($obj); 279 $property = $class->getProperty($prop); 280 $property->setAccessible(true); 281 $property->setValue($obj, $value); 282 } 283} 284