1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Tests finder
19 *
20 * @package    core
21 * @category   test
22 * @copyright  2012 Petr Skoda {@link http://skodak.org}
23 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26/**
27 * Finds components and plugins with tests
28 *
29 * @package    core
30 * @category   test
31 * @copyright  2012 Petr Skoda {@link http://skodak.org}
32 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 */
34class tests_finder {
35
36    /**
37     * Returns all the components with tests of the specified type
38     * @param string $testtype The kind of test we are looking for
39     * @return array
40     */
41    public static function get_components_with_tests($testtype) {
42
43        // Get all the components
44        $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype);
45
46        // Get all the directories having tests
47        $directories = self::get_all_directories_with_tests($testtype);
48
49        // Find any directory not covered by proper components
50        $remaining = array_diff($directories, $components);
51
52        // Add them to the list of components
53        $components += $remaining;
54
55        return $components;
56    }
57
58    /**
59     * Returns all the plugins having tests
60     * @param string $testtype The kind of test we are looking for
61     * @return array  all the plugins having tests
62     */
63    private static function get_all_plugins_with_tests($testtype) {
64        $pluginswithtests = array();
65
66        $plugintypes = core_component::get_plugin_types();
67        ksort($plugintypes);
68        foreach ($plugintypes as $type => $unused) {
69            $plugs = core_component::get_plugin_list($type);
70            ksort($plugs);
71            foreach ($plugs as $plug => $fullplug) {
72                // Look for tests recursively
73                if (self::directory_has_tests($fullplug, $testtype)) {
74                    $pluginswithtests[$type . '_' . $plug] = $fullplug;
75                }
76            }
77        }
78        return $pluginswithtests;
79    }
80
81    /**
82     * Returns all the subsystems having tests
83     *
84     * Note we are hacking here the list of subsystems
85     * to cover some well-known subsystems that are not properly
86     * returned by the {@link get_core_subsystems()} function.
87     *
88     * @param string $testtype The kind of test we are looking for
89     * @return array all the subsystems having tests
90     */
91    private static function get_all_subsystems_with_tests($testtype) {
92        global $CFG;
93
94        $subsystemswithtests = array();
95
96        $subsystems = core_component::get_core_subsystems();
97
98        // Hack the list a bit to cover some well-known ones
99        $subsystems['backup'] = $CFG->dirroot.'/backup';
100        $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml';
101        $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl';
102
103        ksort($subsystems);
104        foreach ($subsystems as $subsys => $fullsubsys) {
105            if ($fullsubsys === null) {
106                continue;
107            }
108            if (!is_dir($fullsubsys)) {
109                continue;
110            }
111            // Look for tests recursively
112            if (self::directory_has_tests($fullsubsys, $testtype)) {
113                $subsystemswithtests['core_' . $subsys] = $fullsubsys;
114            }
115        }
116        return $subsystemswithtests;
117    }
118
119    /**
120     * Returns all the directories having tests
121     *
122     * @param string $testtype The kind of test we are looking for
123     * @return array all directories having tests
124     */
125    private static function get_all_directories_with_tests($testtype) {
126        global $CFG;
127
128        // List of directories to exclude from test file searching.
129        $excludedir = array('node_modules', 'vendor');
130
131        // Get first level directories in which tests should be searched.
132        $directoriestosearch = array();
133        $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
134        foreach ($alldirs as $dir) {
135            if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) {
136                $directoriestosearch[] = $dir;
137            }
138        }
139
140        // Search for tests in valid directories.
141        $dirs = array();
142        foreach ($directoriestosearch as $dir) {
143            $dirite = new RecursiveDirectoryIterator($dir);
144            $iteite = new RecursiveIteratorIterator($dirite);
145            $regexp = self::get_regexp($testtype);
146            $regite = new RegexIterator($iteite, $regexp);
147            foreach ($regite as $path => $element) {
148                $key = dirname(dirname($path));
149                $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
150                $dirs[$key] = $value;
151            }
152        }
153        ksort($dirs);
154        return array_flip($dirs);
155    }
156
157    /**
158     * Returns if a given directory has tests (recursively)
159     *
160     * @param string $dir full path to the directory to look for phpunit tests
161     * @param string $testtype phpunit|behat
162     * @return bool if a given directory has tests (true) or no (false)
163     */
164    private static function directory_has_tests($dir, $testtype) {
165        if (!is_dir($dir)) {
166            return false;
167        }
168
169        $dirite = new RecursiveDirectoryIterator($dir);
170        $iteite = new RecursiveIteratorIterator($dirite);
171        $regexp = self::get_regexp($testtype);
172        $regite = new RegexIterator($iteite, $regexp);
173        $regite->rewind();
174        if ($regite->valid()) {
175            return true;
176        }
177        return false;
178    }
179
180
181    /**
182     * Returns the regular expression to match by the test files
183     * @param string $testtype
184     * @return string
185     */
186    private static function get_regexp($testtype) {
187
188        $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
189
190        switch ($testtype) {
191            case 'phpunit':
192                $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|';
193                break;
194            case 'features':
195                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|';
196                break;
197            case 'stepsdefinitions':
198                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|';
199                break;
200            case 'behat':
201                $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!';
202                break;
203        }
204
205        return $regexp;
206    }
207}
208