1<?php
2/*
3 * This file is part of PHPUnit.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Runner for PHPT test cases.
13 */
14class PHPUnit_Extensions_PhptTestCase implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing
15{
16    /**
17     * @var string
18     */
19    private $filename;
20
21    /**
22     * @var PHPUnit_Util_PHP
23     */
24    private $phpUtil;
25
26    /**
27     * @var array
28     */
29    private $settings = [
30        'allow_url_fopen=1',
31        'auto_append_file=',
32        'auto_prepend_file=',
33        'disable_functions=',
34        'display_errors=1',
35        'docref_root=',
36        'docref_ext=.html',
37        'error_append_string=',
38        'error_prepend_string=',
39        'error_reporting=-1',
40        'html_errors=0',
41        'log_errors=0',
42        'magic_quotes_runtime=0',
43        'output_handler=',
44        'open_basedir=',
45        'output_buffering=Off',
46        'report_memleaks=0',
47        'report_zend_debug=0',
48        'safe_mode=0',
49        'xdebug.default_enable=0'
50    ];
51
52    /**
53     * Constructs a test case with the given filename.
54     *
55     * @param string           $filename
56     * @param PHPUnit_Util_PHP $phpUtil
57     *
58     * @throws PHPUnit_Framework_Exception
59     */
60    public function __construct($filename, $phpUtil = null)
61    {
62        if (!is_string($filename)) {
63            throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string');
64        }
65
66        if (!is_file($filename)) {
67            throw new PHPUnit_Framework_Exception(
68                sprintf(
69                    'File "%s" does not exist.',
70                    $filename
71                )
72            );
73        }
74
75        $this->filename = $filename;
76        $this->phpUtil  = $phpUtil ?: PHPUnit_Util_PHP::factory();
77    }
78
79    /**
80     * Counts the number of test cases executed by run(TestResult result).
81     *
82     * @return int
83     */
84    public function count()
85    {
86        return 1;
87    }
88
89    /**
90     * @param array  $sections
91     * @param string $output
92     */
93    private function assertPhptExpectation(array $sections, $output)
94    {
95        $assertions = [
96            'EXPECT'      => 'assertEquals',
97            'EXPECTF'     => 'assertStringMatchesFormat',
98            'EXPECTREGEX' => 'assertRegExp',
99        ];
100
101        $actual = preg_replace('/\r\n/', "\n", trim($output));
102
103        foreach ($assertions as $sectionName => $sectionAssertion) {
104            if (isset($sections[$sectionName])) {
105                $sectionContent = preg_replace('/\r\n/', "\n", trim($sections[$sectionName]));
106                $assertion      = $sectionAssertion;
107                $expected       = $sectionName == 'EXPECTREGEX' ? "/{$sectionContent}/" : $sectionContent;
108
109                break;
110            }
111        }
112
113        PHPUnit_Framework_Assert::$assertion($expected, $actual);
114    }
115
116    /**
117     * Runs a test and collects its result in a TestResult instance.
118     *
119     * @param PHPUnit_Framework_TestResult $result
120     *
121     * @return PHPUnit_Framework_TestResult
122     */
123    public function run(PHPUnit_Framework_TestResult $result = null)
124    {
125        $sections = $this->parse();
126        $code     = $this->render($sections['FILE']);
127
128        if ($result === null) {
129            $result = new PHPUnit_Framework_TestResult;
130        }
131
132        $skip     = false;
133        $xfail    = false;
134        $time     = 0;
135        $settings = $this->settings;
136
137        $result->startTest($this);
138
139        if (isset($sections['INI'])) {
140            $settings = array_merge($settings, $this->parseIniSection($sections['INI']));
141        }
142
143        if (isset($sections['ENV'])) {
144            $env = $this->parseEnvSection($sections['ENV']);
145            $this->phpUtil->setEnv($env);
146        }
147
148        // Redirects STDERR to STDOUT
149        $this->phpUtil->setUseStderrRedirection(true);
150
151        if ($result->enforcesTimeLimit()) {
152            $this->phpUtil->setTimeout($result->getTimeoutForLargeTests());
153        }
154
155        if (isset($sections['SKIPIF'])) {
156            $jobResult = $this->phpUtil->runJob($sections['SKIPIF'], $settings);
157
158            if (!strncasecmp('skip', ltrim($jobResult['stdout']), 4)) {
159                if (preg_match('/^\s*skip\s*(.+)\s*/i', $jobResult['stdout'], $message)) {
160                    $message = substr($message[1], 2);
161                } else {
162                    $message = '';
163                }
164
165                $result->addFailure($this, new PHPUnit_Framework_SkippedTestError($message), 0);
166
167                $skip = true;
168            }
169        }
170
171        if (isset($sections['XFAIL'])) {
172            $xfail = trim($sections['XFAIL']);
173        }
174
175        if (!$skip) {
176            if (isset($sections['STDIN'])) {
177                $this->phpUtil->setStdin($sections['STDIN']);
178            }
179
180            if (isset($sections['ARGS'])) {
181                $this->phpUtil->setArgs($sections['ARGS']);
182            }
183
184            PHP_Timer::start();
185
186            $jobResult = $this->phpUtil->runJob($code, $settings);
187            $time      = PHP_Timer::stop();
188
189            try {
190                $this->assertPhptExpectation($sections, $jobResult['stdout']);
191            } catch (PHPUnit_Framework_AssertionFailedError $e) {
192                if ($xfail !== false) {
193                    $result->addFailure(
194                        $this,
195                        new PHPUnit_Framework_IncompleteTestError(
196                            $xfail,
197                            0,
198                            $e
199                        ),
200                        $time
201                    );
202                } else {
203                    $result->addFailure($this, $e, $time);
204                }
205            } catch (Throwable $t) {
206                $result->addError($this, $t, $time);
207            } catch (Exception $e) {
208                $result->addError($this, $e, $time);
209            }
210
211            if ($result->allCompletelyImplemented() && $xfail !== false) {
212                $result->addFailure(
213                    $this,
214                    new PHPUnit_Framework_IncompleteTestError(
215                        'XFAIL section but test passes'
216                    ),
217                    $time
218                );
219            }
220
221            $this->phpUtil->setStdin('');
222            $this->phpUtil->setArgs('');
223
224            if (isset($sections['CLEAN'])) {
225                $cleanCode = $this->render($sections['CLEAN']);
226
227                $this->phpUtil->runJob($cleanCode, $this->settings);
228            }
229        }
230
231        $result->endTest($this, $time);
232
233        return $result;
234    }
235
236    /**
237     * Returns the name of the test case.
238     *
239     * @return string
240     */
241    public function getName()
242    {
243        return $this->toString();
244    }
245
246    /**
247     * Returns a string representation of the test case.
248     *
249     * @return string
250     */
251    public function toString()
252    {
253        return $this->filename;
254    }
255
256    /**
257     * @return array
258     *
259     * @throws PHPUnit_Framework_Exception
260     */
261    private function parse()
262    {
263        $sections = [];
264        $section  = '';
265
266        $allowExternalSections = [
267            'FILE',
268            'EXPECT',
269            'EXPECTF',
270            'EXPECTREGEX'
271        ];
272
273        $requiredSections = [
274            'FILE',
275            [
276                'EXPECT',
277                'EXPECTF',
278                'EXPECTREGEX'
279            ]
280        ];
281
282        $unsupportedSections = [
283            'REDIRECTTEST',
284            'REQUEST',
285            'POST',
286            'PUT',
287            'POST_RAW',
288            'GZIP_POST',
289            'DEFLATE_POST',
290            'GET',
291            'COOKIE',
292            'HEADERS',
293            'CGI',
294            'EXPECTHEADERS',
295            'EXTENSIONS',
296            'PHPDBG'
297        ];
298
299        foreach (file($this->filename) as $line) {
300            if (preg_match('/^--([_A-Z]+)--/', $line, $result)) {
301                $section            = $result[1];
302                $sections[$section] = '';
303
304                continue;
305            } elseif (empty($section)) {
306                throw new PHPUnit_Framework_Exception('Invalid PHPT file');
307            }
308
309            $sections[$section] .= $line;
310        }
311
312        if (isset($sections['FILEEOF'])) {
313            $sections['FILE'] = rtrim($sections['FILEEOF'], "\r\n");
314            unset($sections['FILEEOF']);
315        }
316
317        $testDirectory = dirname($this->filename) . DIRECTORY_SEPARATOR;
318
319        foreach ($allowExternalSections as $section) {
320            if (isset($sections[$section . '_EXTERNAL'])) {
321                // do not allow directory traversal
322                $externalFilename = str_replace('..', '', trim($sections[$section . '_EXTERNAL']));
323
324                // only allow files from the test directory
325                if (!is_file($testDirectory . $externalFilename) || !is_readable($testDirectory . $externalFilename)) {
326                    throw new PHPUnit_Framework_Exception(
327                        sprintf(
328                            'Could not load --%s-- %s for PHPT file',
329                            $section . '_EXTERNAL',
330                            $testDirectory . $externalFilename
331                        )
332                    );
333                }
334
335                $sections[$section] = file_get_contents($testDirectory . $externalFilename);
336
337                unset($sections[$section . '_EXTERNAL']);
338            }
339        }
340
341        $isValid = true;
342
343        foreach ($requiredSections as $section) {
344            if (is_array($section)) {
345                $foundSection = false;
346
347                foreach ($section as $anySection) {
348                    if (isset($sections[$anySection])) {
349                        $foundSection = true;
350
351                        break;
352                    }
353                }
354
355                if (!$foundSection) {
356                    $isValid = false;
357
358                    break;
359                }
360            } else {
361                if (!isset($sections[$section])) {
362                    $isValid = false;
363
364                    break;
365                }
366            }
367        }
368
369        if (!$isValid) {
370            throw new PHPUnit_Framework_Exception('Invalid PHPT file');
371        }
372
373        foreach ($unsupportedSections as $section) {
374            if (isset($sections[$section])) {
375                throw new PHPUnit_Framework_Exception(
376                    'PHPUnit does not support this PHPT file'
377                );
378            }
379        }
380
381        return $sections;
382    }
383
384    /**
385     * @param string $code
386     *
387     * @return string
388     */
389    private function render($code)
390    {
391        return str_replace(
392            [
393                '__DIR__',
394                '__FILE__'
395            ],
396            [
397                "'" . dirname($this->filename) . "'",
398                "'" . $this->filename . "'"
399            ],
400            $code
401        );
402    }
403
404    /**
405     * Parse --INI-- section key value pairs and return as array.
406     *
407     * @param string
408     *
409     * @return array
410     */
411    protected function parseIniSection($content)
412    {
413        return preg_split('/\n|\r/', $content, -1, PREG_SPLIT_NO_EMPTY);
414    }
415
416    protected function parseEnvSection($content)
417    {
418        $env = [];
419
420        foreach (explode("\n", trim($content)) as $e) {
421            $e = explode('=', trim($e), 2);
422
423            if (!empty($e[0]) && isset($e[1])) {
424                $env[$e[0]] = $e[1];
425            }
426        }
427
428        return $env;
429    }
430}
431