* integrating UniversalEditButton extension into core (enabled by default, set $wgUni...
[mediawiki.git] / t / Test.php
blob7904f3ba426904e4938beae543371713583f1942
1 <?php
2 # See the end of this file for documentation
4 # The latest release of this test framework can always be found on CPAN:
5 # http://search.cpan.org/search?query=Test.php
7 register_shutdown_function('_test_ends');
9 $__Test = array(
10 # How many tests are planned
11 'planned' => null,
13 # How many tests we've run, if 'planned' is still null by the time we're
14 # done we report the total count at the end
15 'run' => 0,
17 # Are are we currently within todo_start()/todo_end() ?
18 'todo' => array(),
21 function plan($plan, $why = '')
23 global $__Test;
25 $__Test['planned'] = true;
27 switch ($plan)
29 case 'no_plan':
30 $__Test['planned'] = false;
31 break;
32 case 'skip_all';
33 printf("1..0%s\n", $why ? " # Skip $why" : '');
34 exit;
35 default:
36 printf("1..%d\n", $plan);
37 break;
41 function pass($desc = '')
43 return _proclaim(true, $desc);
46 function fail($desc = '')
48 return _proclaim(false, $desc);
51 function ok($cond, $desc = '') {
52 return _proclaim($cond, $desc);
55 function is($got, $expected, $desc = '') {
56 $pass = $got == $expected;
57 return _proclaim($pass, $desc, /* todo */ false, $got, $expected);
60 function isnt($got, $expected, $desc = '') {
61 $pass = $got != $expected;
62 return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true);
65 function like($got, $expected, $desc = '') {
66 $pass = preg_match($expected, $got);
67 return _proclaim($pass, $desc, /* todo */ false, $got, $expected);
70 function unlike($got, $expected, $desc = '') {
71 $pass = !preg_match($expected, $got);
72 return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true);
75 function cmp_ok($got, $op, $expected, $desc = '')
77 $pass = null;
79 # See http://www.php.net/manual/en/language.operators.comparison.php
80 switch ($op)
82 case '==':
83 $pass = $got == $expected;
84 break;
85 case '===':
86 $pass = $got === $expected;
87 break;
88 case '!=':
89 case '<>':
90 $pass = $got != $expected;
91 break;
92 case '!==':
93 $pass = $got !== $expected;
94 break;
95 case '<':
96 $pass = $got < $expected;
97 break;
98 case '>':
99 $pass = $got > $expected;
100 break;
101 case '<=':
102 $pass = $got <= $expected;
103 break;
104 case '>=':
105 $pass = $got >= $expected;
106 break;
107 default:
108 if (function_exists($op)) {
109 $pass = $op($got, $expected);
110 } else {
111 die("No such operator or function $op\n");
115 return _proclaim($pass, $desc, /* todo */ false, $got, "$got $op $expected");
118 function diag($message)
120 if (is_array($message))
122 $message = implode("\n", $message);
125 foreach (explode("\n", $message) as $line)
127 echo "# $line\n";
131 function include_ok($file, $desc = '')
133 $pass = include $file;
134 return _proclaim($pass, $desc == '' ? "include $file" : $desc);
137 function require_ok($file, $desc = '')
139 $pass = require $file;
140 return _proclaim($pass, $desc == '' ? "require $file" : $desc);
143 function is_deeply($got, $expected, $desc = '')
145 $diff = _cmp_deeply($got, $expected);
146 $pass = is_null($diff);
148 if (!$pass) {
149 $got = strlen($diff['gpath']) ? ($diff['gpath'] . ' = ' . $diff['got'])
150 : _repl($got);
151 $expected = strlen($diff['epath']) ? ($diff['epath'] . ' = ' . $diff['expected'])
152 : _repl($expected);
155 _proclaim($pass, $desc, /* todo */ false, $got, $expected);
158 function isa_ok($obj, $expected, $desc = '')
160 $pass = is_a($obj, $expected);
161 _proclaim($pass, $desc, /* todo */ false, $name, $expected);
164 function todo_start($why = '')
166 global $__Test;
168 $__Test['todo'][] = $why;
171 function todo_end()
173 global $__Test;
175 if (count($__Test['todo']) == 0) {
176 die("todo_end() called without a matching todo_start() call");
177 } else {
178 array_pop($__Test['todo']);
183 # The code below consists of private utility functions for the above functions
186 function _proclaim(
187 $cond, # bool
188 $desc = '',
189 $todo = false,
190 $got = null,
191 $expected = null,
192 $negate = false) {
194 global $__Test;
196 $__Test['run'] += 1;
198 # We're in a TODO block via todo_start()/todo_end(). TODO via specific
199 # functions is currently unimplemented and will probably stay that way
200 if (count($__Test['todo'])) {
201 $todo = true;
204 # Everything after the first # is special, so escape user-supplied messages
205 $desc = str_replace('#', '\\#', $desc);
206 $desc = str_replace("\n", '\\n', $desc);
208 $ok = $cond ? "ok" : "not ok";
209 $directive = '';
211 if ($todo) {
212 $todo_idx = count($__Test['todo']) - 1;
213 $directive .= ' # TODO ' . $__Test['todo'][$todo_idx];
216 printf("%s %d %s%s\n", $ok, $__Test['run'], $desc, $directive);
218 # report a failure
219 if (!$cond) {
220 # Every public function in this file calls _proclaim so our culprit is
221 # the second item in the stack
222 $caller = debug_backtrace();
223 $call = $caller['1'];
225 diag(
226 sprintf(" Failed%stest '%s'\n in %s at line %d\n got: %s\n expected: %s",
227 $todo ? ' TODO ' : ' ',
228 $desc,
229 $call['file'],
230 $call['line'],
231 $got,
232 $expected
237 return $cond;
240 function _test_ends()
242 global $__Test;
244 if (count($__Test['todo']) != 0) {
245 $todos = join("', '", $__Test['todo']);
246 die("Missing todo_end() for '$todos'");
249 if (!$__Test['planned']) {
250 printf("1..%d\n", $__Test['run']);
255 # All of the below is for is_deeply()
258 function _repl($obj, $deep = true) {
259 if (is_string($obj)) {
260 return "'" . $obj . "'";
261 } else if (is_numeric($obj)) {
262 return $obj;
263 } else if (is_null($obj)) {
264 return 'null';
265 } else if (is_bool($obj)) {
266 return $obj ? 'true' : 'false';
267 } else if (is_array($obj)) {
268 return _repl_array($obj, $deep);
269 }else {
270 return gettype($obj);
274 function _diff($gpath, $got, $epath, $expected) {
275 return array(
276 'gpath' => $gpath,
277 'got' => $got,
278 'epath' => $epath,
279 'expected' => $expected
283 function _idx($obj, $path = '') {
284 return $path . '[' . _repl($obj) . ']';
287 function _cmp_deeply($got, $exp, $path = '') {
288 if (is_array($exp)) {
290 if (!is_array($got)) {
291 return _diff($path, _repl($got), $path, _repl($exp));
294 $gk = array_keys($got);
295 $ek = array_keys($exp);
296 $mc = max(count($gk), count($ek));
298 for ($el = 0; $el < $mc; $el++) {
299 # One array shorter than the other?
300 if ($el >= count($ek)) {
301 return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]),
302 'missing', 'nothing');
303 } else if ($el >= count($gk)) {
304 return _diff('missing', 'nothing',
305 _idx($ek[$el], $path), _repl($exp[$ek[$el]]));
308 # Keys differ?
309 if ($gk[$el] != $ek[$el]) {
310 return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]),
311 _idx($ek[$el], $path), _repl($exp[$ek[$el]]));
314 # Recurse
315 $rc = _cmp_deeply($got[$gk[$el]], $exp[$ek[$el]], _idx($gk[$el], $path));
316 if (!is_null($rc)) {
317 return $rc;
321 else {
322 # Default to serialize hack
323 if (serialize($got) != serialize($exp)) {
324 return _diff($path, _repl($got), $path, _repl($exp));
328 return null;
331 function _plural($n, $singular, $plural = null) {
332 if (is_null($plural)) {
333 $plural = $singular . 's';
335 return $n == 1 ? "$n $singular" : "$n $plural";
338 function _repl_array($obj, $deep) {
339 if ($deep) {
340 $slice = array_slice($obj, 0, 3); # Increase from 3 to show more
341 $repl = array();
342 $next = 0;
343 foreach ($slice as $idx => $el) {
344 $elrep = _repl($el, false);
345 if (is_numeric($idx) && $next == $idx) {
346 // Numeric index
347 $next++;
348 } else {
349 // Out of sequence or non-numeric
350 $elrep = _repl($idx, false) . ' => ' . $elrep;
352 $repl[] = $elrep;
354 $more = count($obj) - count($slice);
355 if ($more > 0) {
356 $repl[] = '... ' . _plural($more, 'more element') . ' ...';
358 return 'array(' . join(', ', $repl) . ')';
360 else {
361 return 'array(' . count($obj) . ')';
367 =head1 NAME
369 Test.php - TAP test framework for PHP with a L<Test::More>-like interface
371 =head1 SYNOPSIS
373 #!/usr/bin/env php
374 <?php
375 require 'Test.php';
377 plan($num); # plan $num tests
378 # or
379 plan('no_plan'); # We don't know how many
380 # or
381 plan('skip_all'); # Skip all tests
382 # or
383 plan('skip_all', $reason); # Skip all tests with a reason
385 diag('message in test output') # Trailing \n not required
387 # $test_name is always optional and should be a short description of
388 # the test, e.g. "some_function() returns an integer"
390 # Various ways to say "ok"
391 ok($got == $expected, $test_name);
393 # Compare with == and !=
394 is($got, $expected, $test_name);
395 isnt($got, $expected, $test_name);
397 # Run a preg regex match on some data
398 like($got, $regex, $test_name);
399 unlike($got, $regex, $test_name);
401 # Compare something with a given comparison operator
402 cmp_ok($got, '==', $expected, $test_name);
403 # Compare something with a comparison function (should return bool)
404 cmp_ok($got, $func, $expected, $test_name);
406 # Recursively check datastructures for equalness
407 is_deeply($got, $expected, $test_name);
409 # Always pass or fail a test under an optional name
410 pass($test_name);
411 fail($test_name);
413 # TODO tests, these are expected to fail but won't fail the test run,
414 # unexpected success will be reported
415 todo_start("integer arithmetic still working");
416 ok(1 + 2 == 3);
418 # TODOs can be nested
419 todo_start("string comparison still working")
420 is("foo", "bar");
421 todo_end();
423 todo_end();
426 =head1 DESCRIPTION
428 F<Test.php> is an implementation of Perl's L<Test::More> for PHP. Like
429 Test::More it produces language agnostic TAP output (see L<TAP>) which
430 can then be gathered, formatted and summarized by a program that
431 understands TAP such as prove(1).
433 =head1 HOWTO
435 First place the F<Test.php> in the project root or somewhere else in
436 the include path where C<require> and C<include> will find it.
438 Then make a place to put your tests in, it's customary to place TAP
439 tests in a directory named F<t> under the root but they can be
440 anywhere you like. Make a test in this directory or one of its subdirs
441 and try running it with php(1):
443 $ php t/pass.t
444 1..1
445 ok 1 This dummy test passed
447 The TAP output consists of very simple output, of course reading
448 larger output is going to be harder which is where prove(1) comes
449 in. prove is a harness program that reads test output and produces
450 reports based on it:
452 $ prove t/pass.t
453 t/pass....ok
454 All tests successful.
455 Files=1, Tests=1, 0 wallclock secs ( 0.03 cusr + 0.02 csys = 0.05 CPU)
457 To run all the tests in the F<t> directory recursively use C<prove -r
458 t>. This can be put in a F<Makefile> under a I<test> target, for
459 example:
461 test: Test.php
462 prove -r t
464 For reference the example test file above looks like this, the shebang
465 on the first line is needed so that prove(1) and other test harness
466 programs know they're dealing with a PHP file.
468 #!/usr/bin/env php
469 <?php
471 require 'Test.php';
473 plan(1);
474 pass('This dummy test passed');
477 =head1 SEE ALSO
479 L<TAP> - The TAP protocol
481 =head1 AUTHOR
483 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avar@cpan.org> and Andy Armstrong <andy@hexten.net>
485 =head1 LICENSING
487 The author or authors of this code dedicate any and all copyright
488 interest in this code to the public domain. We make this dedication
489 for the benefit of the public at large and to the detriment of our
490 heirs and successors. We intend this dedication to be an overt act of
491 relinquishment in perpetuity of all present and future rights this
492 code under copyright law.
494 =cut