1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tester;
9:
10:
11: 12: 13: 14: 15:
16: class Assert
17: {
18:
19: const EPSILON = 1e-10;
20:
21:
22: static public $patterns = array(
23: '%a%' => '[^\r\n]+',
24: '%a\?%'=> '[^\r\n]*',
25: '%A%' => '.+',
26: '%A\?%'=> '.*',
27: '%s%' => '[\t ]+',
28: '%s\?%'=> '[\t ]*',
29: '%S%' => '\S+',
30: '%S\?%'=> '\S*',
31: '%c%' => '[^\r\n]',
32: '%d%' => '[0-9]+',
33: '%d\?%'=> '[0-9]*',
34: '%i%' => '[+-]?[0-9]+',
35: '%f%' => '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?',
36: '%h%' => '[0-9a-fA-F]+',
37: '%ds%'=> '[\\\\/]',
38: '%(\[.*\].*)%'=> '$1',
39: );
40:
41:
42: public static $onFailure;
43:
44:
45: 46: 47: 48:
49: public static function same($expected, $actual)
50: {
51: if ($actual !== $expected) {
52: self::fail('%1 should be %2', $actual, $expected);
53: }
54: }
55:
56:
57: 58: 59: 60:
61: public static function notSame($expected, $actual)
62: {
63: if ($actual === $expected) {
64: self::fail('%1 should not be %2', $actual, $expected);
65: }
66: }
67:
68:
69: 70: 71: 72:
73: public static function equal($expected, $actual)
74: {
75: if (!self::isEqual($expected, $actual)) {
76: self::fail('%1 should be equal to %2', $actual, $expected);
77: }
78: }
79:
80:
81: 82: 83: 84:
85: public static function notEqual($expected, $actual)
86: {
87: if (self::isEqual($expected, $actual)) {
88: self::fail('%1 should not be equal to %2', $actual, $expected);
89: }
90: }
91:
92:
93: 94: 95: 96:
97: public static function contains($needle, $actual)
98: {
99: if (is_array($actual)) {
100: if (!in_array($needle, $actual, TRUE)) {
101: self::fail('%1 should contain %2', $actual, $needle);
102: }
103: } elseif (is_string($actual)) {
104: if (strpos($actual, $needle) === FALSE) {
105: self::fail('%1 should contain %2', $actual, $needle);
106: }
107: } else {
108: self::fail('%1 should be string or array', $actual);
109: }
110: }
111:
112:
113: 114: 115: 116:
117: public static function notContains($needle, $actual)
118: {
119: if (is_array($actual)) {
120: if (in_array($needle, $actual, TRUE)) {
121: self::fail('%1 should not contain %2', $actual, $needle);
122: }
123: } elseif (is_string($actual)) {
124: if (strpos($actual, $needle) !== FALSE) {
125: self::fail('%1 should not contain %2', $actual, $needle);
126: }
127: } else {
128: self::fail('%1 should be string or array', $actual);
129: }
130: }
131:
132:
133: 134: 135: 136: 137:
138: public static function true($actual)
139: {
140: if ($actual !== TRUE) {
141: self::fail('%1 should be TRUE', $actual);
142: }
143: }
144:
145:
146: 147: 148: 149: 150:
151: public static function false($actual)
152: {
153: if ($actual !== FALSE) {
154: self::fail('%1 should be FALSE', $actual);
155: }
156: }
157:
158:
159: 160: 161: 162: 163:
164: public static function null($actual)
165: {
166: if ($actual !== NULL) {
167: self::fail('%1 should be NULL', $actual);
168: }
169: }
170:
171:
172: 173: 174: 175: 176:
177: public static function truthy($actual)
178: {
179: if (!$actual) {
180: self::fail('%1 should be truthy', $actual);
181: }
182: }
183:
184:
185: 186: 187: 188: 189:
190: public static function falsey($actual)
191: {
192: if ($actual) {
193: self::fail('%1 should be falsey', $actual);
194: }
195: }
196:
197:
198: 199: 200: 201:
202: public static function type($type, $value)
203: {
204: if (!is_object($type) && !is_string($type)) {
205: throw new \Exception('Type must be a object or a string.');
206:
207: } elseif ($type === 'list') {
208: if (!is_array($value) || ($value && array_keys($value) !== range(0, count($value) - 1))) {
209: self::fail("%1 should be $type", $value);
210: }
211:
212: } elseif (in_array($type, array('array', 'bool', 'callable', 'float',
213: 'int', 'integer', 'null', 'object', 'resource', 'scalar', 'string'), TRUE)
214: ) {
215: if (!call_user_func("is_$type", $value)) {
216: self::fail("%1 should be $type", $value);
217: }
218:
219: } elseif (!$value instanceof $type) {
220: self::fail("%1 should be instance of $type", $value);
221: }
222: }
223:
224:
225: 226: 227: 228: 229: 230: 231:
232: public static function exception($function, $class, $message = NULL)
233: {
234: try {
235: call_user_func($function);
236: } catch (\Exception $e) {
237: }
238: if (!isset($e)) {
239: self::fail("$class was expected, but none was thrown");
240:
241: } elseif (!$e instanceof $class) {
242: self::fail("$class was expected but got " . get_class($e) . ($e->getMessage() ? " ({$e->getMessage()})" : ''));
243:
244: } elseif ($message && !self::isMatching($message, $e->getMessage())) {
245: self::fail("$class with a message matching %2 was expected but got %1", $e->getMessage(), $message);
246: }
247: return $e;
248: }
249:
250:
251: 252: 253: 254:
255: public static function throws($function, $class, $message = NULL)
256: {
257: return self::exception($function, $class, $message);
258: }
259:
260:
261: 262: 263: 264: 265: 266: 267:
268: public static function error($function, $expectedType, $expectedMessage = NULL)
269: {
270: if (is_string($expectedType) && !preg_match('#^E_[A-Z_]+\z#', $expectedType)) {
271: return static::exception($function, $expectedType, $expectedMessage);
272: }
273:
274: $expected = is_array($expectedType) ? $expectedType : array(array($expectedType, $expectedMessage));
275: foreach ($expected as & $item) {
276: list($expectedType, $expectedMessage) = $item;
277: if (is_int($expectedType)) {
278: $item[2] = Helpers::errorTypeToString($expectedType);
279: } elseif (is_string($expectedType)) {
280: $item[0] = constant($item[2] = $expectedType);
281: } else {
282: throw new \Exception('Error type must be E_* constant.');
283: }
284: }
285:
286: set_error_handler(function($severity, $message, $file, $line) use (& $expected) {
287: if (($severity & error_reporting()) !== $severity) {
288: return;
289: }
290:
291: $errorStr = Helpers::errorTypeToString($severity) . ($message ? " ($message)" : '');
292: list($expectedType, $expectedMessage, $expectedTypeStr) = array_shift($expected);
293: if ($expectedType === NULL) {
294: restore_error_handler();
295: Assert::fail("Generated more errors than expected: $errorStr was generated in file $file on line $line");
296:
297: } elseif ($severity !== $expectedType) {
298: restore_error_handler();
299: Assert::fail("$expectedTypeStr was expected, but $errorStr was generated in file $file on line $line");
300:
301: } elseif ($expectedMessage && !Assert::isMatching($expectedMessage, $message)) {
302: restore_error_handler();
303: Assert::fail("$expectedTypeStr with a message matching %2 was expected but got %1", $message, $expectedMessage);
304: }
305: });
306:
307: reset($expected);
308: call_user_func($function);
309: restore_error_handler();
310:
311: if ($expected) {
312: self::fail('Error was expected, but was not generated');
313: }
314: }
315:
316:
317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336:
337: public static function match($pattern, $actual)
338: {
339: if (!is_string($pattern)) {
340: throw new \Exception('Pattern must be a string.');
341:
342: } elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
343: self::fail('%1 should match %2', $actual, rtrim($pattern));
344: }
345: }
346:
347:
348: 349: 350: 351:
352: public static function matchFile($file, $actual)
353: {
354: $pattern = @file_get_contents($file);
355: if ($pattern === FALSE) {
356: throw new \Exception("Unable to read file '$file'.");
357:
358: } elseif (!is_scalar($actual) || !self::isMatching($pattern, $actual)) {
359: self::fail('%1 should match %2', $actual, rtrim($pattern));
360: }
361: }
362:
363:
364: 365: 366: 367:
368: public static function fail($message, $actual = NULL, $expected = NULL)
369: {
370: $e = new AssertException($message);
371: $e->actual = $actual;
372: $e->expected = $expected;
373: if (self::$onFailure) {
374: call_user_func(self::$onFailure, $e);
375: } else {
376: throw $e;
377: }
378: }
379:
380:
381: public static function with($obj, \Closure $closure)
382: {
383: return $closure->bindTo($obj, $obj)->__invoke();
384: }
385:
386:
387:
388:
389:
390: 391: 392: 393: 394:
395: public static function isMatching($pattern, $actual)
396: {
397: if (!is_string($pattern) && !is_scalar($actual)) {
398: throw new \Exception('Value and pattern must be strings.');
399: }
400:
401: $old = ini_set('pcre.backtrack_limit', '10000000');
402:
403: if (!preg_match('/^([~#]).+(\1)[imsxUu]*\z/s', $pattern)) {
404: $utf8 = preg_match('#\x80-\x{10FFFF}]#u', $pattern) ? 'u' : '';
405: $patterns = static::$patterns + array(
406: '[.\\\\+*?[^$(){|\x00\#]' => '\$0',
407: '[\t ]*\r?\n' => "[\\t ]*\n",
408: );
409: $pattern = '#^' . preg_replace_callback('#' . implode('|', array_keys($patterns)) . '#U' . $utf8, function($m) use ($patterns) {
410: foreach ($patterns as $re => $replacement) {
411: $s = preg_replace("#^$re\\z#", str_replace('\\', '\\\\', $replacement), $m[0], 1, $count);
412: if ($count) {
413: return $s;
414: }
415: }
416: }, rtrim($pattern)) . '\s*$#sU' . $utf8;
417: $actual = str_replace("\r\n", "\n", $actual);
418: }
419:
420: $res = preg_match($pattern, $actual);
421: ini_set('pcre.backtrack_limit', $old);
422: if ($res === FALSE || preg_last_error()) {
423: throw new \Exception('Error while executing regular expression. (PREG Error Code ' . preg_last_error() . ')');
424: }
425: return (bool) $res;
426: }
427:
428:
429: 430: 431: 432: 433:
434: public static function isEqual($expected, $actual, $level = 0)
435: {
436: if ($level > 10) {
437: throw new \Exception('Nesting level too deep or recursive dependency.');
438: }
439:
440: if (is_float($expected) && is_float($actual) && is_finite($expected) && is_finite($actual)) {
441: $diff = abs($expected - $actual);
442: return ($diff < self::EPSILON) || ($diff / max(abs($expected), abs($actual)) < self::EPSILON);
443: }
444:
445: if (is_object($expected) && is_object($actual) && get_class($expected) === get_class($actual)) {
446: if ($expected === $actual) {
447: return TRUE;
448: }
449: $expected = (array) $expected;
450: $actual = (array) $actual;
451: }
452:
453: if (is_array($expected) && is_array($actual)) {
454: ksort($expected, SORT_STRING);
455: ksort($actual, SORT_STRING);
456: if (array_keys($expected) !== array_keys($actual)) {
457: return FALSE;
458: }
459:
460: foreach ($expected as $value) {
461: if (!self::isEqual($value, current($actual), $level + 1)) {
462: return FALSE;
463: }
464: next($actual);
465: }
466: return TRUE;
467: }
468:
469: return $expected === $actual;
470: }
471:
472: }
473:
474:
475: 476: 477: 478: 479:
480: class AssertException extends \Exception
481: {
482: public $message;
483:
484: public $actual;
485:
486: public $expected;
487:
488: }
489: