Completed
Push — master ( 77bd5e...c97259 )
by Nazar
04:13
created

test.php ➔ run_test()   D

Complexity

Conditions 17
Paths 78

Size

Total Lines 108
Code Lines 86

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 17
eloc 86
nc 78
nop 2
dl 0
loc 108
rs 4.8361

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	if (isset($parsed_test['EXPECT'])) {
111
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECT'], $php_arguments, $script_arguments));
112
		if ($expect === $output) {
113
			line("<g>$base_text SUCCESS</g>", true);
114
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
115
			return 'success';
116
		}
117
		$expect = $parsed_test['EXPECT'];
118
	} elseif (isset($parsed_test['EXPECTF'])) {
119
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECTF'], $php_arguments, $script_arguments));
120
		$regex  = str_replace(
121
			[
122
				'%s',
123
				'%S',
124
				'%a',
125
				'%A',
126
				'%w',
127
				'%i',
128
				'%d',
129
				'%x',
130
				'%f',
131
				'%c'
132
			],
133
			[
134
				'[^\r\n]+',
135
				'[^\r\n]*',
136
				'.+',
137
				'.*',
138
				'\s*',
139
				'[+-]?\d+',
140
				'\d+',
141
				'[0-9a-fA-F]+',
142
				'[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
143
				'.'
144
			],
145
			preg_quote($expect, '/')
146
		);
147
		if (preg_match("/^$regex\$/s", $output)) {
148
			line("<g>$base_text SUCCESS</g>", true);
149
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
150
			return 'success';
151
		}
152
		$expect = $parsed_test['EXPECTF'];
153
	} else {
154
		$expect = rtrim(execute_code($working_dir, $parsed_test['EXPECREGEX'], $php_arguments, $script_arguments));
155
		$regex  = preg_quote($expect, '/');
156
		if (preg_match("/^$regex\$/s", $output)) {
157
			line("<g>$base_text SUCCESS</g>", true);
158
			isset($parsed_test['CLEAN']) && execute_code($working_dir, $parsed_test['CLEAN'], $php_arguments, $script_arguments);;
159
			return 'success';
160
		}
161
	}
162
	line("<r>$base_text ERROR:</r>", true);
163
	$diff = preg_replace_callback(
164
		'/^([-+]).*$/m',
165
		function ($match) {
166
			return $match[1] == '-' ? "<r>$match[0]</r>" : "<g>$match[0]</g>";
167
		},
168
		compute_diff($test_file, $expect, $output)
169
	);
170
	line($diff);
171
	return 'error';
172
}
173
174
/**
175
 * @param string $test_file
176
 *
177
 * @return string[]
178
 */
179
function parse_test ($test_file) {
180
	$result      = [];
181
	$current_key = null;
182
	foreach (file($test_file) as $line) {
183
		if (preg_match(
184
			"/^--(SKIPIF|INI|ARGS|FILE|EXPECT|EXPECTF|EXPECTREGEX|CLEAN)--\n$/",
185
			$line,
186
			$match
187
		)) {
188
			$current_key = $match[1];
189
		} elseif ($current_key) {
190
			if (!isset($result[$current_key])) {
191
				$result[$current_key] = '';
192
			}
193
			$result[$current_key] .= $line;
194
		}
195
	}
196
	return $result;
197
}
198
199
/**
200
 * @param string   $working_dir
201
 * @param string   $code
202
 * @param string[] $php_arguments
203
 * @param string   $script_arguments
204
 *
205
 * @return string
206
 */
207
function execute_code ($working_dir, $code, $php_arguments, $script_arguments) {
208
	$file = "$working_dir/__code.php";
209
	file_put_contents($file, $code);
210
	$output = shell_exec(PHP_BINARY.' '.implode(' ', $php_arguments).' -f='.escapeshellarg($file)." -- $script_arguments 2>&1");
211
	unlink($file);
212
	return $output;
213
}
214
215
function compute_diff ($test_file, $expect, $output) {
216
	$exp_file = "$test_file.exp";
217
	$out_file = "$test_file.out";
218
	file_put_contents($exp_file, $expect);
219
	file_put_contents($out_file, $output);
220
	$diff = shell_exec(
221
		"diff --old-line-format='-%3dn %L' --new-line-format='+%3dn %L' --from-file=".escapeshellarg($exp_file).' '.escapeshellarg($out_file)
222
	);
223
	file_put_contents("$test_file.diff", $diff);
224
	return $diff;
225
}
226
227
/**
228
 * @param string|string[] $target
229
 *
230
 * @return string[]
231
 */
232
function find_tests ($target) {
233
	if (is_array($target)) {
234
		return array_merge(...array_map('find_tests', $target));
235
	}
236
237
	if (is_dir($target)) {
238
		$iterator = new RegexIterator(
239
			new RecursiveIteratorIterator(
240
				new RecursiveDirectoryIterator($target)
241
			),
242
			'/.*\.phpt$/',
243
			RecursiveRegexIterator::GET_MATCH
244
		);
245
		return array_merge(...array_values(iterator_to_array($iterator)));
246
	}
247
248
	return file_exists($target) ? [$target] : [];
249
}
250
251
/** @var bool[] $options */
252
$options = [];
253
$targets = [];
254
foreach (array_slice($argv, 1) as $arg) {
255
	if (strpos($arg, '-') === 0) {
256
		$options[ltrim($arg, '-')] = true;
257
	} else {
258
		$targets[] = $arg;
259
	}
260
}
261
$options += [
262
	'h'         => false,
263
	'skip-slow' => false
264
];
265
$version = json_decode(file_get_contents(__DIR__.'/components/modules/System/meta.json'), true)['version'];
266
267
line("<g>CleverStyle CMS</g> version <y>$version</y>, PHPT Tests runner\n");
268
269
if (!$targets || $options['h']) {
270
	line(
271
		<<<HTML
272
<y>Usage:</y>
273
  php -d variables_order=EGPCS test.php [-h] [files] [directories] 
274
275
<y>Arguments:</y>
276
  <g>h</g> Print this help message  
277
278
<y>Examples:</y>
279
  Execute tests from tests directory:
280
    <g>php -d variables_order=EGPCS test.php tests</g>
281
  Execute tests single test:
282
    <g>php -d variables_order=EGPCS test.php tests/sample.phpt</g>
283
  Execute tests from tests directory, but skip slow tests using environment variable:
284
    <g>SKIP_SLOW_TESTS=1 php -d variables_order=EGPCS test.php tests</g>
285
  Execute tests from tests directory, but only those for MySQLi database engine:
286
    <g>DB=MySQLi php -d variables_order=EGPCS test.php tests</g>
287
  Execute tests from tests directory, but only those for SQLite database engine:
288
    <g>DB=SQLite php -d variables_order=EGPCS test.php tests</g>
289
290
<y>PHPT Format:</y>
291
  This runner uses modification of PHPT format used by PHP itself, so that it can run many original PHPT tests without any changes.
292
  
293
  PHPT test if text file with *.phpt extension.
294
  Each file contains sections followed by section contents, everything before first section is ignored, you can use it for storing test description.
295
  
296
  Required sections are <g>--FILE--</g> and one of [<g>--EXPECT--</g>, <g>--EXPECTF--</g>, <g>--EXPECTREGEX--</g>].
297
298
<y>PHPT sections supported:</y>
299
  <g>--FILE--</g>        The test source code
300
  <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)
301
  <g>--EXPECTF--</g>     Similar to <g>--EXPECT--</g>, but it uses substitution tags for strings, spaces, digits, which may vary between test runs
302
    The following is a list of all tags and what they are used to represent:
303
      <g>%s</g> One or more of anything (character or white space) except the end of line character
304
      <g>%S</g> Zero or more of anything (character or white space) except the end of line character
305
      <g>%a</g> One or more of anything (character or white space) including the end of line character
306
      <g>%A</g> Zero or more of anything (character or white space) including the end of line character
307
      <g>%w</g> Zero or more white space characters
308
      <g>%i</g> A signed integer value, for example +3142, -3142
309
      <g>%d</g> An unsigned integer value, for example 123456
310
      <g>%x</g> One or more hexadecimal character. That is, characters in the range 0-9, a-f, A-F
311
      <g>%f</g> A floating point number, for example: 3.142, -3.142, 3.142E-10, 3.142e+10
312
      <g>%c</g> A single character of any sort (.)
313
  <g>--EXPECTREGEX--</g> Similar to <g>--EXPECT--</g>, but is treated as regular expression
314
  <g>--SKIPIF--</g>      If output of execution starts with `skip` then test will be skipped
315
  <g>--INI--</g>         Specific php.ini setting for the test, one per line
316
  <g>--ARGS--</g>        A single line defining the arguments passed to php
317
  <g>--CLEAN--</g>       Code that is executed after a test completes
318
319
<y>PHPT tests examples:</y>
320
  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
321
322
<y>Main differences from original PHPT tests files:</y>
323
  1. <g>--TEST--</g> is not required and not even used (files names are used instead)
324
  2. Only sub-set of sections supported and only sub-set of <g>--EXPECTF--</g> tags
325
  3. <g>--EXPECT*--</g> sections are interpreted as code and its output is used as expected result
326
HTML
327
	);
328
	exit;
329
}
330
331
$tests = find_tests($targets);
332
sort($tests, SORT_NATURAL);
333
$tests_count = count($tests);
334
335
if (!$tests_count) {
336
	line("<r>No tests found, there is nothing to do here</r>");
337
	exit(1);
338
}
339
340
line("<y>$tests_count tests found</y>, running them:");
341
342
$max_length = 0;
343
foreach ($tests as $test_file) {
344
	$max_length = max($max_length, strlen($test_file));
345
}
346
347
$results = [
348
	'skipped' => 0,
349
	'success' => 0,
350
	'error'   => 0
351
];
352
353
foreach ($tests as $index => $test_file) {
354
	$base_text = sprintf("%' 3d/$tests_count %s", $index + 1, str_pad($test_file, $max_length));
355
	$results[run_test($test_file, $base_text)]++;
356
}
357
358
line("\nResults:");
359
if ($results['skipped']) {
360
	line(sprintf("<y>%' 3d/$tests_count tests (%' 6.2f%%) skipped</y>", $results['skipped'], $results['skipped'] / $tests_count * 100));
361
}
362
if ($results['success']) {
363
	line(sprintf("<g>%' 3d/$tests_count tests (%' 6.2f%%) succeed</g>", $results['success'], $results['success'] / $tests_count * 100));
364
}
365
if ($results['error']) {
366
	line(sprintf("<r>%' 3d/$tests_count tests (%' 6.2f%%) failed</r>", $results['error'], $results['error'] / $tests_count * 100));
367
	exit(1);
368
}
369