Completed
Push — master ( e3bee1...7ca2d5 )
by Nazar
04:36
created

test.php ➔ run_test()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 47
Code Lines 36

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
eloc 36
nc 14
nop 2
dl 0
loc 47
rs 5.7377
1
<?php
2
/**
3
 * @package    CleverStyle CMS
4
 * @subpackage PHPT Tests runner
5
 * @author     Nazar Mokrynskyi <[email protected]>
6
 * @copyright  Copyright (c) 2016, Nazar Mokrynskyi
7
 * @license    MIT License, see license.txt
8
 */
9
/**
10
 * @param string $text
11
 *
12
 * @return string
13
 */
14
function colorize ($text) {
15
	return preg_replace_callback(
16
		'#<([gyr])>(.*)</\1>#Us',
17
		function ($matches) {
18
			$color_codes = [
19
				'g' => 32, // Green
20
				'y' => 33, // Yellow
21
				'r' => 31, // Red
22
			];
23
			$color       = $color_codes[$matches[1]];
24
			$text        = $matches[2];
25
			return "\033[{$color}m$text\033[0m";
26
		},
27
		$text
28
	);
29
}
30
31
/**
32
 * Output something to console
33
 *
34
 * Will colorize stuff in process
35
 *
36
 * @param bool   $clean Clean current line before output
37
 * @param string $text
38
 */
39
function out ($text, $clean = false) {
40
	if ($clean) {
41
		echo "\r";
42
	}
43
	echo colorize($text);
44
}
45
46
/**
47
 * Output something to console and add new line at the end
48
 *
49
 * Will colorize stuff in process
50
 *
51
 * @param bool   $clean Clean current line before output
52
 * @param string $text
53
 */
54
function line ($text = '', $clean = false) {
55
	out($text, $clean);
56
	echo "\n";
57
}
58
59
/**
60
 * @param string $test_file
61
 * @param string $base_text
62
 *
63
 * @return string `skipped`, `success` or `error`
64
 */
65
function run_test ($test_file, $base_text) {
66
	out("<y>$base_text ...</y>");
67
	$test_file = realpath($test_file);
68
	@unlink("$test_file.exp");
69
	@unlink("$test_file.out");
70
	@unlink("$test_file.diff");
71
	$parsed_test = parse_test($test_file);
72
	/**
73
	 * Check required sections
74
	 */
75
	if (!isset($parsed_test['FILE'])) {
76
		line("<r>$base_text ERROR</r>", true);
77
		line("--FILE-- section MUST be present");
78
		return 'error';
79
	}
80
	$output_sections = ['EXPECT', 'EXPECTF', 'EXPECTREGEX'];
81
	if (!array_intersect(array_keys($parsed_test), $output_sections)) {
82
		line("<r>$base_text ERROR</r>", true);
83
		line('One of the following sections MUST be present: '.implode(',', $output_sections));
84
		return 'error';
85
	}
86
	$php_arguments = [
87
		'-d variables_order=EGPCS',
88
		'-d error_reporting='.E_ALL,
89
		'-d display_errors=1',
90
		'-d xdebug.default_enable=0'
91
	];
92
	if (isset($parsed_test['INI'])) {
93
		foreach (explode("\n", trim($parsed_test['INI'])) as $line) {
94
			list($key, $value) = explode('=', $line, 2);
95
			$php_arguments[] = '-d '.trim($key).'='.trim($value);
96
		}
97
		unset($line, $key, $value);
98
	}
99
	$script_arguments = isset($parsed_test['ARGS']) ? $parsed_test['ARGS'] : '';
100
	$working_dir      = dirname($test_file);
101
	if (isset($parsed_test['SKIPIF'])) {
102
		$result = execute_code($working_dir, $parsed_test['SKIPIF'], $php_arguments, $script_arguments);
103
		if (stripos($result, 'skip') === 0) {
104
			line("<y>$base_text SKIPPED</y>", true);
105
			line(ltrim(substr($result, 4)));
106
			return 'skipped';
107
		}
108
	}
109
	$output = rtrim(execute_code($working_dir, $parsed_test['FILE'], $php_arguments, $script_arguments));
110
	return compare_output($output, $base_text, $test_file, $php_arguments, $script_arguments, $parsed_test);
111
}
112
113
/**
114
 * @param string $test_file
115
 *
116
 * @return string[]
117
 */
118
function parse_test ($test_file) {
119
	$result      = [];
120
	$current_key = null;
121
	foreach (file($test_file) as $line) {
122
		if (preg_match(
123
			"/^--(SKIPIF|INI|ARGS|FILE|EXPECT|EXPECTF|EXPECTREGEX|CLEAN)--\n$/",
124
			$line,
125
			$match
126
		)) {
127
			$current_key = $match[1];
128
		} elseif ($current_key) {
129
			if (!isset($result[$current_key])) {
130
				$result[$current_key] = '';
131
			}
132
			$result[$current_key] .= $line;
133
		}
134
	}
135
	return $result;
136
}
137
138
/**
139
 * @param string   $working_dir
140
 * @param string   $code
141
 * @param string[] $php_arguments
142
 * @param string   $script_arguments
143
 *
144
 * @return string
145
 */
146
function execute_code ($working_dir, $code, $php_arguments, $script_arguments) {
147
	$file = "$working_dir/__code.php";
148
	file_put_contents($file, $code);
149
	$output = shell_exec(PHP_BINARY.' '.implode(' ', $php_arguments).' -f='.escapeshellarg($file)." -- $script_arguments 2>&1");
150
	unlink($file);
151
	return $output;
152
}
153
154
/**
155
 * @param string   $output
156
 * @param string   $base_text
157
 * @param string   $test_file
158
 * @param array    $php_arguments
159
 * @param string   $script_arguments
160
 * @param string[] $parsed_test
161
 *
162
 * @return string `skipped`, `success` or `error`
163
 */
164
function compare_output ($output, $base_text, $test_file, $php_arguments, $script_arguments, $parsed_test) {
165
	$working_dir = dirname($test_file);
166
	if (isset($parsed_test['EXPECT'])) {
167
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECT'], $php_arguments, $script_arguments));
168
		if ($expect === $output) {
169
			line("<g>$base_text SUCCESS</g>", true);
170
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
171
			return 'success';
172
		}
173
		$expect = $parsed_test['EXPECT'];
174
	} elseif (isset($parsed_test['EXPECTF'])) {
175
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECTF'], $php_arguments, $script_arguments));
176
		$regex  = str_replace(
177
			[
178
				'%s',
179
				'%S',
180
				'%a',
181
				'%A',
182
				'%w',
183
				'%i',
184
				'%d',
185
				'%x',
186
				'%f',
187
				'%c'
188
			],
189
			[
190
				'[^\r\n]+',
191
				'[^\r\n]*',
192
				'.+',
193
				'.*',
194
				'\s*',
195
				'[+-]?\d+',
196
				'\d+',
197
				'[0-9a-fA-F]+',
198
				'[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
199
				'.'
200
			],
201
			preg_quote($expect, '/')
202
		);
203
		if (preg_match("/^$regex\$/s", $output)) {
204
			line("<g>$base_text SUCCESS</g>", true);
205
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
206
			return 'success';
207
		}
208
		$expect = $parsed_test['EXPECTF'];
209
	} else {
210
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECREGEX'], $php_arguments, $script_arguments));
211
		$regex  = preg_quote($expect, '/');
212
		if (preg_match("/^$regex\$/s", $output)) {
213
			line("<g>$base_text SUCCESS</g>", true);
214
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
215
			return 'success';
216
		}
217
	}
218
	line("<r>$base_text ERROR:</r>", true);
219
	$diff = preg_replace_callback(
220
		'/^([-+]).*$/m',
221
		function ($match) {
222
			return $match[1] == '-' ? "<r>$match[0]</r>" : "<g>$match[0]</g>";
223
		},
224
		compute_diff($test_file, $expect, $output)
225
	);
226
	line($diff);
227
	return 'error';
228
}
229
230
function compute_diff ($test_file, $expect, $output) {
231
	$exp_file = "$test_file.exp";
232
	$out_file = "$test_file.out";
233
	file_put_contents($exp_file, $expect);
234
	file_put_contents($out_file, $output);
235
	$diff = shell_exec(
236
		"diff --old-line-format='-%3dn %L' --new-line-format='+%3dn %L' --from-file=".escapeshellarg($exp_file).' '.escapeshellarg($out_file)
237
	);
238
	file_put_contents("$test_file.diff", $diff);
239
	return $diff;
240
}
241
242
/**
243
 * @param string|string[] $target
244
 *
245
 * @return string[]
246
 */
247
function find_tests ($target) {
248
	if (is_array($target)) {
249
		return array_merge(...array_map('find_tests', $target));
250
	}
251
252
	if (is_dir($target)) {
253
		$iterator = new RegexIterator(
254
			new RecursiveIteratorIterator(
255
				new RecursiveDirectoryIterator($target)
256
			),
257
			'/.*\.phpt$/',
258
			RecursiveRegexIterator::GET_MATCH
259
		);
260
		return array_merge(...array_values(iterator_to_array($iterator)));
261
	}
262
263
	return file_exists($target) ? [$target] : [];
264
}
265
266
/** @var bool[] $options */
267
$options = [];
268
$targets = [];
269
foreach (array_slice($argv, 1) as $arg) {
270
	if (strpos($arg, '-') === 0) {
271
		$options[ltrim($arg, '-')] = true;
272
	} else {
273
		$targets[] = $arg;
274
	}
275
}
276
$options += [
277
	'h'         => false,
278
	'skip-slow' => false
279
];
280
$version = json_decode(file_get_contents(__DIR__.'/components/modules/System/meta.json'), true)['version'];
281
282
line("<g>CleverStyle CMS</g> version <y>$version</y>, PHPT Tests runner\n");
283
284
if (!$targets || $options['h']) {
285
	line(
286
		<<<HTML
287
<y>Usage:</y>
288
  php -d variables_order=EGPCS test.php [-h] [files] [directories] 
289
290
<y>Arguments:</y>
291
  <g>h</g> Print this help message  
292
293
<y>Examples:</y>
294
  Execute tests from tests directory:
295
    <g>php -d variables_order=EGPCS test.php tests</g>
296
  Execute tests single test:
297
    <g>php -d variables_order=EGPCS test.php tests/sample.phpt</g>
298
  Execute tests from tests directory, but skip slow tests using environment variable:
299
    <g>SKIP_SLOW_TESTS=1 php -d variables_order=EGPCS test.php tests</g>
300
  Execute tests from tests directory, but only those for MySQLi database engine:
301
    <g>DB=MySQLi php -d variables_order=EGPCS test.php tests</g>
302
  Execute tests from tests directory, but only those for SQLite database engine:
303
    <g>DB=SQLite php -d variables_order=EGPCS test.php tests</g>
304
305
<y>PHPT Format:</y>
306
  This runner uses modification of PHPT format used by PHP itself, so that it can run many original PHPT tests without any changes.
307
  
308
  PHPT test if text file with *.phpt extension.
309
  Each file contains sections followed by section contents, everything before first section is ignored, you can use it for storing test description.
310
  
311
  Required sections are <g>--FILE--</g> and one of [<g>--EXPECT--</g>, <g>--EXPECTF--</g>, <g>--EXPECTREGEX--</g>].
312
313
<y>PHPT sections supported:</y>
314
  <g>--FILE--</g>        The test source code
315
  <g>--EXPECT--</g>      The expected output from the test script (will be executed as PHP script, so it might be code as well as plain text)
316
  <g>--EXPECTF--</g>     Similar to <g>--EXPECT--</g>, but it uses substitution tags for strings, spaces, digits, which may vary between test runs
317
    The following is a list of all tags and what they are used to represent:
318
      <g>%s</g> One or more of anything (character or white space) except the end of line character
319
      <g>%S</g> Zero or more of anything (character or white space) except the end of line character
320
      <g>%a</g> One or more of anything (character or white space) including the end of line character
321
      <g>%A</g> Zero or more of anything (character or white space) including the end of line character
322
      <g>%w</g> Zero or more white space characters
323
      <g>%i</g> A signed integer value, for example +3142, -3142
324
      <g>%d</g> An unsigned integer value, for example 123456
325
      <g>%x</g> One or more hexadecimal character. That is, characters in the range 0-9, a-f, A-F
326
      <g>%f</g> A floating point number, for example: 3.142, -3.142, 3.142E-10, 3.142e+10
327
      <g>%c</g> A single character of any sort (.)
328
  <g>--EXPECTREGEX--</g> Similar to <g>--EXPECT--</g>, but is treated as regular expression
329
  <g>--SKIPIF--</g>      If output of execution starts with `skip` then test will be skipped
330
  <g>--INI--</g>         Specific php.ini setting for the test, one per line
331
  <g>--ARGS--</g>        A single line defining the arguments passed to php
332
  <g>--CLEAN--</g>       Code that is executed after a test completes
333
334
<y>PHPT tests examples:</y>
335
  Examples can be found at <<g>https://qa.php.net/phpt_details.php</g>> (taking into account differences here) and in <g>tests</g> directory
336
337
<y>Main differences from original PHPT tests files:</y>
338
  1. <g>--TEST--</g> is not required and not even used (files names are used instead)
339
  2. Only sub-set of sections supported and only sub-set of <g>--EXPECTF--</g> tags
340
  3. <g>--EXPECT*--</g> sections are interpreted as code and its output is used as expected result
341
HTML
342
	);
343
	exit;
344
}
345
346
$tests = find_tests($targets);
347
sort($tests, SORT_NATURAL);
348
$tests_count = count($tests);
349
350
if (!$tests_count) {
351
	line("<r>No tests found, there is nothing to do here</r>");
352
	exit(1);
353
}
354
355
line("<y>$tests_count tests found</y>, running them:");
356
357
$max_length = 0;
358
foreach ($tests as $test_file) {
359
	$max_length = max($max_length, strlen($test_file));
360
}
361
362
$results = [
363
	'skipped' => 0,
364
	'success' => 0,
365
	'error'   => 0
366
];
367
368
foreach ($tests as $index => $test_file) {
369
	$base_text = sprintf("%' 3d/$tests_count %s", $index + 1, str_pad($test_file, $max_length));
370
	$results[run_test($test_file, $base_text)]++;
371
}
372
373
line("\nResults:");
374
if ($results['skipped']) {
375
	line(sprintf("<y>%' 3d/$tests_count tests (%' 6.2f%%) skipped</y>", $results['skipped'], $results['skipped'] / $tests_count * 100));
376
}
377
if ($results['success']) {
378
	line(sprintf("<g>%' 3d/$tests_count tests (%' 6.2f%%) succeed</g>", $results['success'], $results['success'] / $tests_count * 100));
379
}
380
if ($results['error']) {
381
	line(sprintf("<r>%' 3d/$tests_count tests (%' 6.2f%%) failed</r>", $results['error'], $results['error'] / $tests_count * 100));
382
	exit(1);
383
}
384