xref: /dokuwiki/_test/core/DokuWikiTest.php (revision 1ca67924afdba7cda93bb9fbf36899df6f3723ef)
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