xref: /dokuwiki/_test/core/DokuWikiTest.php (revision e05998d5d6388950e9732477c1bca8f3aff6f193)
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        $method->setAccessible(true);
237        return $method->invokeArgs($obj, $args);
238    }
239
240    /**
241     * Allow for reading inaccessible properties (private or protected)
242     *
243     * This makes it easier to check internals of tested objects. This should generally
244     * be avoided.
245     *
246     * @param object $obj Object on which to access the property
247     * @param string $prop name of the property to access
248     * @return mixed
249     * @throws ReflectionException  when the given obj/prop does not exist
250     */
251    protected static function getInaccessibleProperty($obj, $prop)
252    {
253        $class = new \ReflectionClass($obj);
254        $property = $class->getProperty($prop);
255        $property->setAccessible(true);
256        return $property->getValue($obj);
257    }
258
259    /**
260     * Allow for reading inaccessible properties (private or protected)
261     *
262     * This makes it easier to set internals of tested objects. This should generally
263     * be avoided.
264     *
265     * @param object $obj Object on which to access the property
266     * @param string $prop name of the property to access
267     * @param mixed $value new value to set the property to
268     * @return void
269     * @throws ReflectionException when the given obj/prop does not exist
270     */
271    protected static function setInaccessibleProperty($obj, $prop, $value)
272    {
273        $class = new \ReflectionClass($obj);
274        $property = $class->getProperty($prop);
275        $property->setAccessible(true);
276        $property->setValue($obj, $value);
277    }
278
279    /**
280     * Expect the next log message to contain $message
281     *
282     * @param string $facility
283     * @param string $message
284     * @return void
285     */
286    protected function expectLogMessage(string $message, string $facility = Logger::LOG_ERROR): void
287    {
288        $logger = Logger::getInstance($facility);
289        $logger->expect($message);
290    }
291}
292