Merge pull request #21164 from civicrm/5.41
[civicrm-core.git] / Civi / Test / Legacy / CiviTestListener.php
CommitLineData
a6439b6a
SL
1<?php
2
3namespace Civi\Test\Legacy;
4
5/**
6 * Class CiviTestListener
7 * @package Civi\Test
8 *
9 * CiviTestListener participates in test-execution, looking for test-classes
10 * which have certain tags. If the tags are found, the listener will perform
11 * additional setup/teardown logic.
12 *
13 * @see EndToEndInterface
14 * @see HeadlessInterface
15 * @see HookInterface
16 */
17class CiviTestListener extends \PHPUnit_Framework_BaseTestListener {
a6439b6a
SL
18
19 /**
20 * @var array
21 * Ex: $cache['Some_Test_Class']['civicrm_foobar'] = 'hook_civicrm_foobar';
22 * Array(string $testClass => Array(string $hookName => string $methodName)).
23 */
24 private $cache = [];
25
26 /**
15478faa 27 * @var \CRM_Core_Transaction|null
a6439b6a
SL
28 */
29 private $tx;
30
31 public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) {
32 $byInterface = $this->indexTestsByInterface($suite->tests());
33 $this->validateGroups($byInterface);
34 $this->autoboot($byInterface);
35 }
36
37 public function endTestSuite(\PHPUnit_Framework_TestSuite $suite) {
38 $this->cache = [];
39 }
40
41 public function startTest(\PHPUnit_Framework_Test $test) {
42 if ($this->isCiviTest($test)) {
43 error_reporting(E_ALL);
1b64d7cb 44 $GLOBALS['CIVICRM_TEST_CASE'] = $test;
a6439b6a
SL
45 }
46
47 if ($test instanceof \Civi\Test\HeadlessInterface) {
48 $this->bootHeadless($test);
49 }
50
a6439b6a
SL
51 if ($test instanceof \Civi\Test\TransactionalInterface) {
52 $this->tx = new \CRM_Core_Transaction(TRUE);
53 $this->tx->rollback();
54 }
55 else {
56 $this->tx = NULL;
57 }
58 }
59
60 public function endTest(\PHPUnit_Framework_Test $test, $time) {
61 if ($test instanceof \Civi\Test\TransactionalInterface) {
62 $this->tx->rollback()->commit();
63 $this->tx = NULL;
64 }
65 if ($test instanceof \Civi\Test\HookInterface) {
66 \CRM_Utils_Hook::singleton()->reset();
67 }
fcd647c0 68 \CRM_Utils_Time::resetTime();
a6439b6a 69 if ($this->isCiviTest($test)) {
1b64d7cb 70 unset($GLOBALS['CIVICRM_TEST_CASE']);
a6439b6a
SL
71 error_reporting(E_ALL & ~E_NOTICE);
72 $this->errorScope = NULL;
73 }
74 }
75
76 /**
77 * @param HeadlessInterface|\PHPUnit_Framework_Test $test
78 */
79 protected function bootHeadless($test) {
80 if (CIVICRM_UF !== 'UnitTests') {
81 throw new \RuntimeException('HeadlessInterface requires CIVICRM_UF=UnitTests');
82 }
83
84 // Hrm, this seems wrong. Shouldn't we be resetting the entire session?
85 $session = \CRM_Core_Session::singleton();
86 $session->set('userID', NULL);
87
88 $test->setUpHeadless();
89
90 \CRM_Utils_System::flushCache();
91 \Civi::reset();
92 \CRM_Core_Session::singleton()->set('userID', NULL);
93 // ugh, performance
94 $config = \CRM_Core_Config::singleton(TRUE, TRUE);
95
96 if (property_exists($config->userPermissionClass, 'permissions')) {
97 $config->userPermissionClass->permissions = NULL;
98 }
99 }
100
a6439b6a
SL
101 /**
102 * @param \PHPUnit_Framework_Test $test
103 * @return bool
104 */
105 protected function isCiviTest(\PHPUnit_Framework_Test $test) {
106 return $test instanceof \Civi\Test\HookInterface || $test instanceof \Civi\Test\HeadlessInterface;
107 }
108
a6439b6a
SL
109 /**
110 * The first time we come across HeadlessInterface or EndToEndInterface, we'll
111 * try to autoboot.
112 *
113 * Once the system is booted, there's nothing we can do -- we're stuck with that
114 * environment. (Thank you, prolific define()s!) If there's a conflict between a
115 * test-class and the active boot-level, then we'll have to bail.
116 *
117 * @param array $byInterface
118 * List of test classes, keyed by major interface (HeadlessInterface vs EndToEndInterface).
119 */
120 protected function autoboot($byInterface) {
121 if (defined('CIVICRM_UF')) {
122 // OK, nothing we can do. System has booted already.
123 }
124 elseif (!empty($byInterface['HeadlessInterface'])) {
125 putenv('CIVICRM_UF=UnitTests');
126 // phpcs:disable
127 eval($this->cv('php:boot --level=full', 'phpcode'));
128 // phpcs:enable
129 }
130 elseif (!empty($byInterface['EndToEndInterface'])) {
131 putenv('CIVICRM_UF=');
132 // phpcs:disable
133 eval($this->cv('php:boot --level=full', 'phpcode'));
134 // phpcs:enable
135 }
136
137 $blurb = "Tip: Run the headless tests and end-to-end tests separately, e.g.\n"
138 . " $ phpunit5 --group headless\n"
139 . " $ phpunit5 --group e2e \n";
140
141 if (!empty($byInterface['HeadlessInterface']) && CIVICRM_UF !== 'UnitTests') {
142 $testNames = implode(', ', array_keys($byInterface['HeadlessInterface']));
143 throw new \RuntimeException("Suite includes headless tests ($testNames) which require CIVICRM_UF=UnitTests.\n\n$blurb");
144 }
145 if (!empty($byInterface['EndToEndInterface']) && CIVICRM_UF === 'UnitTests') {
146 $testNames = implode(', ', array_keys($byInterface['EndToEndInterface']));
147 throw new \RuntimeException("Suite includes end-to-end tests ($testNames) which do not support CIVICRM_UF=UnitTests.\n\n$blurb");
148 }
149 }
150
151 /**
152 * Call the "cv" command.
153 *
154 * This duplicates the standalone `cv()` wrapper that is recommended in bootstrap.php.
155 * This duplication is necessary because `cv()` is optional, and downstream implementers
156 * may alter, rename, or omit the wrapper, and (by virtue of its role in bootstrap) there
157 * it is impossible to define it centrally.
158 *
159 * @param string $cmd
160 * The rest of the command to send.
161 * @param string $decode
162 * Ex: 'json' or 'phpcode'.
163 * @return string
164 * Response output (if the command executed normally).
165 * @throws \RuntimeException
166 * If the command terminates abnormally.
167 */
168 protected function cv($cmd, $decode = 'json') {
169 $cmd = 'cv ' . $cmd;
170 $descriptorSpec = [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => STDERR];
171 $oldOutput = getenv('CV_OUTPUT');
172 putenv("CV_OUTPUT=json");
173 $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
174 putenv("CV_OUTPUT=$oldOutput");
175 fclose($pipes[0]);
176 $result = stream_get_contents($pipes[1]);
177 fclose($pipes[1]);
178 if (proc_close($process) !== 0) {
179 throw new \RuntimeException("Command failed ($cmd):\n$result");
180 }
181 switch ($decode) {
182 case 'raw':
183 return $result;
184
185 case 'phpcode':
186 // If the last output is /*PHPCODE*/, then we managed to complete execution.
187 if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
188 throw new \RuntimeException("Command failed ($cmd):\n$result");
189 }
190 return $result;
191
192 case 'json':
193 return json_decode($result, 1);
194
195 default:
196 throw new \RuntimeException("Bad decoder format ($decode)");
197 }
198 }
199
200 /**
201 * @param $tests
202 * @return array
203 */
204 protected function indexTestsByInterface($tests) {
205 $byInterface = ['HeadlessInterface' => [], 'EndToEndInterface' => []];
206 foreach ($tests as $test) {
207 /** @var \PHPUnit_Framework_Test $test */
208 if ($test instanceof \Civi\Test\HeadlessInterface) {
209 $byInterface['HeadlessInterface'][get_class($test)] = 1;
210 }
211 if ($test instanceof \Civi\Test\EndToEndInterface) {
212 $byInterface['EndToEndInterface'][get_class($test)] = 1;
213 }
214 }
215 return $byInterface;
216 }
217
218 /**
219 * Ensure that any tests have sensible groups, e.g.
220 *
221 * `HeadlessInterface` ==> `group headless`
222 * `EndToEndInterface` ==> `group e2e`
223 *
224 * @param array $byInterface
225 */
226 protected function validateGroups($byInterface) {
227 foreach ($byInterface['HeadlessInterface'] as $className => $nonce) {
228 $clazz = new \ReflectionClass($className);
229 $docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
230 if (strpos($docComment, "@group headless\n") === FALSE) {
231 echo "WARNING: Class $className implements HeadlessInterface. It should declare \"@group headless\".\n";
232 }
233 if (strpos($docComment, "@group e2e\n") !== FALSE) {
234 echo "WARNING: Class $className implements HeadlessInterface. It should not declare \"@group e2e\".\n";
235 }
236 }
237 foreach ($byInterface['EndToEndInterface'] as $className => $nonce) {
238 $clazz = new \ReflectionClass($className);
239 $docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
240 if (strpos($docComment, "@group e2e\n") === FALSE) {
241 echo "WARNING: Class $className implements EndToEndInterface. It should declare \"@group e2e\".\n";
242 }
243 if (strpos($docComment, "@group headless\n") !== FALSE) {
244 echo "WARNING: Class $className implements EndToEndInterface. It should not declare \"@group headless\".\n";
245 }
246 }
247 }
248
249}