관리-도구
편집 파일: TestSuite.php
<?php defined('PHPREDIS_TESTRUN') or die("Use TestRedis.php to run tests!\n"); /* A specific exception for when we skip a test */ class TestSkippedException extends Exception {} // phpunit is such a pain to install, we're going with pure-PHP here. class TestSuite { /* Host and port the unit tests will use */ private string $host; private ?int $port = 6379; /* Redis authentication we'll use */ private $auth; /* Redis server version */ protected $version; protected bool $is_keydb; private static bool $colorize = false; private static $BOLD_ON = "\033[1m"; private static $BOLD_OFF = "\033[0m"; private static $BLACK = "\033[0;30m"; private static $DARKGRAY = "\033[1;30m"; private static $BLUE = "\033[0;34m"; private static $PURPLE = "\033[0;35m"; private static $GREEN = "\033[0;32m"; private static $YELLOW = "\033[0;33m"; private static $RED = "\033[0;31m"; public static array $errors = []; public static array $warnings = []; public function __construct(string $host, ?int $port, $auth) { $this->host = $host; $this->port = $port; $this->auth = $auth; } public function getHost() { return $this->host; } public function getPort() { return $this->port; } public function getAuth() { return $this->auth; } public static function errorMessage(string $fmt, ...$args) { $msg = vsprintf($fmt . "\n", $args); if (defined('STDERR')) { fwrite(STDERR, $msg); } else { echo $msg; } } public static function make_bold(string $msg) { return self::$colorize ? self::$BOLD_ON . $msg . self::$BOLD_OFF : $msg; } public static function make_success(string $msg) { return self::$colorize ? self::$GREEN . $msg . self::$BOLD_OFF : $msg; } public static function make_fail(string $msg) { return self::$colorize ? self::$RED . $msg . self::$BOLD_OFF : $msg; } public static function make_warning(string $msg) { return self::$colorize ? self::$YELLOW . $msg . self::$BOLD_OFF : $msg; } protected function printArg($v) { if (is_null($v)) return '(null)'; else if ($v === false || $v === true) return $v ? '(true)' : '(false)'; else if (is_string($v)) return "'$v'"; else return print_r($v, true); } protected function findTestFunction($bt) { $i = 0; while (isset($bt[$i])) { if (substr($bt[$i]['function'], 0, 4) == 'test') return $bt[$i]['function']; $i++; } return NULL; } protected function assertionTrace(?string $fmt = NULL, ...$args) { $prefix = 'Assertion failed:'; $lines = []; $bt = debug_backtrace(); $msg = $fmt ? vsprintf($fmt, $args) : NULL; $fn = $this->findTestFunction($bt); $lines []= sprintf("%s %s - %s", $prefix, self::make_bold($fn), $msg ? $msg : '(no message)'); array_shift($bt); for ($i = 0; $i < count($bt); $i++) { $file = $bt[$i]['file']; $line = $bt[$i]['line']; $fn = $bt[$i+1]['function'] ?? $bt[$i]['function']; $lines []= sprintf("%s %s:%d (%s)%s", str_repeat(' ', strlen($prefix)), $file, $line, $fn, $msg ? " $msg" : ''); if (substr($fn, 0, 4) == 'test') break; } return implode("\n", $lines) . "\n"; } protected function assert($fmt, ...$args) { self::$errors []= $this->assertionTrace($fmt, ...$args); } protected function assertKeyEquals($expected, $key, $redis = NULL): bool { $actual = ($redis ??= $this->redis)->get($key); if ($actual === $expected) return true; self::$errors []= $this->assertionTrace("%s !== %s", $this->printArg($actual), $this->printArg($expected)); return false; } protected function assertKeyEqualsWeak($expected, $key, $redis = NULL): bool { $actual = ($redis ??= $this->redis)->get($key); if ($actual == $expected) return true; self::$errors []= $this->assertionTrace("%s != %s", $this->printArg($actual), $this->printArg($expected)); return false; } protected function assertKeyExists($key, $redis = NULL): bool { if (($redis ??= $this->redis)->exists($key)) return true; self::$errors []= $this->assertionTrace("Key '%s' does not exist.", $key); return false; } protected function assertKeyMissing($key, $redis = NULL): bool { if ( ! ($redis ??= $this->redis)->exists($key)) return true; self::$errors []= $this->assertionTrace("Key '%s' exists but shouldn't.", $key); return false; } protected function assertTrue($value): bool { if ($value === true) return true; self::$errors []= $this->assertionTrace("%s !== %s", $this->printArg($value), $this->printArg(true)); return false; } protected function assertFalse($value): bool { if ($value === false) return true; self::$errors []= $this->assertionTrace("%s !== %s", $this->printArg($value), $this->printArg(false)); return false; } protected function assertNull($value): bool { if ($value === NULL) return true; self::$errors []= $this->assertionTrace("%s !== %s", $this->printArg($value), $this->printArg(NULL)); return false; } protected function assertInArray($ele, $arr, ?callable $cb = NULL): bool { $cb ??= function ($v) { return true; }; $key = array_search($ele, $arr); if ($key !== false && ($valid = $cb($ele))) return true; self::$errors []= $this->assertionTrace("%s %s %s", $this->printArg($ele), $key === false ? 'missing from' : 'is invalid in', $this->printArg($arr)); return false; } protected function assertIsString($v): bool { if (is_string($v)) return true; self::$errors []= $this->assertionTrace("%s is not a string", $this->printArg($v)); return false; } protected function assertIsBool($v): bool { if (is_bool($v)) return true; self::$errors []= $this->assertionTrace("%s is not a boolean", $this->printArg($v)); return false; } protected function assertIsInt($v): bool { if (is_int($v)) return true; self::$errors []= $this->assertionTrace("%s is not an integer", $this->printArg($v)); return false; } protected function assertIsFloat($v): bool { if (is_float($v)) return true; self::$errors []= $this->assertionTrace("%s is not a float", $this->printArg($v)); return false; } protected function assertIsObject($v, ?string $type = NULL): bool { if ( ! is_object($v)) { self::$errors []= $this->assertionTrace("%s is not an object", $this->printArg($v)); return false; } else if ( $type !== NULL && !($v InstanceOf $type)) { self::$errors []= $this->assertionTrace("%s is not an instance of %s", $this->printArg($v), $type); return false; } return true; } protected function assertSameType($expected, $actual): bool { if (gettype($expected) === gettype($actual)) return true; self::$errors []= $this->assertionTrace("%s is not the same type as %s", $this->printArg($actual), $this->printArg($expected)); return false; } protected function assertIsArray($v, ?int $size = null): bool { if ( ! is_array($v)) { self::$errors []= $this->assertionTrace("%s is not an array", $this->printArg($v)); return false; } if ( ! is_null($size) && count($v) != $size) { self::$errors []= $this->assertionTrace("Array size %d != %d", count($v), $size); return false; } return true; } protected function assertArrayKey($arr, $key, ?callable $cb = NULL): bool { $cb ??= function ($v) { return true; }; if (($exists = isset($arr[$key])) && $cb($arr[$key])) return true; if ($exists) { $msg = sprintf("%s is invalid in %s", $this->printArg($arr[$key]), $this->printArg($arr)); } else { $msg = sprintf("%s is not a key in %s", $this->printArg($key), $this->printArg($arr)); } self::$errors []= $this->assertionTrace($msg); return false; } protected function assertArrayKeyEquals($arr, $key, $value): bool { if ( ! isset($arr[$key])) { self::$errors []= $this->assertionTrace( "Key '%s' not found in %s", $key, $this->printArg($arr)); return false; } if ($arr[$key] !== $value) { self::$errors []= $this->assertionTrace( "Value '%s' != '%s' for key '%s' in %s", $arr[$key], $value, $key, $this->printArg($arr)); return false; } return true; } protected function assertValidate($val, callable $cb): bool { if ($cb($val)) return true; self::$errors []= $this->assertionTrace("%s is invalid.", $this->printArg($val)); return false; } protected function assertThrowsMatch($arg, callable $cb, $regex = NULL): bool { $threw = $match = false; try { $cb($arg); } catch (Exception $ex) { $threw = true; $match = !$regex || preg_match($regex, $ex->getMessage()); } if ($threw && $match) return true; $ex = !$threw ? 'no exception' : "no match '$regex'"; self::$errors []= $this->assertionTrace("[$ex]"); return false; } protected function assertLTE($maximum, $value): bool { if ($value <= $maximum) return true; self::$errors []= $this->assertionTrace("%s > %s", $value, $maximum); return false; } protected function assertLT($minimum, $value): bool { if ($value < $minimum) return true; self::$errors []= $this->assertionTrace("%s >= %s", $value, $minimum); return false; } protected function assertGT($maximum, $value): bool { if ($value > $maximum) return true; self::$errors [] = $this->assertionTrace("%s <= %s", $maximum, $value); return false; } protected function assertGTE($minimum, $value): bool { if ($value >= $minimum) return true; self::$errors [] = $this->assertionTrace("%s < %s", $minimum, $value); return false; } protected function externalCmdFailure($cmd, $output, $msg = NULL, $exit_code = NULL) { $bt = debug_backtrace(false); $lines[] = sprintf("Assertion failed: %s:%d (%s)", $bt[0]['file'], $bt[0]['line'], self::make_bold($bt[0]['function'])); if ($msg) $lines[] = sprintf(" Message: %s", $msg); if ($exit_code !== NULL) $lines[] = sprintf(" Exit code: %d", $exit_code); $lines[] = sprintf( " Command: %s", $cmd); if ($output) $lines[] = sprintf(" Output: %s", $output); self::$errors[] = implode("\n", $lines) . "\n"; } protected function assertBetween($value, $min, $max, bool $exclusive = false): bool { if ($min > $max) [$max, $min] = [$min, $max]; if ($exclusive) { if ($value > $min && $value < $max) return true; } else { if ($value >= $min && $value <= $max) return true; } self::$errors []= $this->assertionTrace(sprintf("'%s' not between '%s' and '%s'", $value, $min, $max)); return false; } /* Replica of PHPUnit's assertion. Basically are two arrays the same without ' respect to order. */ protected function assertEqualsCanonicalizing($expected, $actual, $keep_keys = false): bool { if ($expected InstanceOf Traversable) $expected = iterator_to_array($expected); if ($actual InstanceOf Traversable) $actual = iterator_to_array($actual); if ($keep_keys) { asort($expected); asort($actual); } else { sort($expected); sort($actual); } if ($expected === $actual) return true; self::$errors []= $this->assertionTrace("%s !== %s", $this->printArg($actual), $this->printArg($expected)); return false; } protected function assertEqualsWeak($expected, $actual): bool { if ($expected == $actual) return true; self::$errors []= $this->assertionTrace("%s != %s", $this->printArg($actual), $this->printArg($expected)); return false; } protected function assertEquals($expected, $actual): bool { if ($expected === $actual) return true; self::$errors[] = $this->assertionTrace("%s !== %s", $this->printArg($actual), $this->printArg($expected)); return false; } public function assertNotEquals($wrong_value, $test_value): bool { if ($wrong_value !== $test_value) return true; self::$errors []= $this->assertionTrace("%s === %s", $this->printArg($wrong_value), $this->printArg($test_value)); return false; } protected function assertStringContains(string $needle, $haystack): bool { if ( ! is_string($haystack)) { self::$errors []= $this->assertionTrace("'%s' is not a string", $this->printArg($haystack)); return false; } if (strstr($haystack, $needle) !== false) return true; self::$errors []= $this->assertionTrace("'%s' not found in '%s'", $needle, $haystack); } protected function assertPatternMatch(string $pattern, string $value): bool { if (preg_match($pattern, $value)) return true; self::$errors []= $this->assertionTrace("'%s' doesnt match '%s'", $value, $pattern); return false; } protected function markTestSkipped(string $msg = '') { $bt = debug_backtrace(false); self::$warnings []= sprintf("Skipped test: %s:%d (%s) %s\n", $bt[0]["file"], $bt[0]["line"], $bt[1]["function"], $msg); throw new TestSkippedException($msg); } private static function getMaxTestLen(array $methods, ?string $limit): int { $result = 0; foreach ($methods as $obj_method) { $name = strtolower($obj_method->name); if (substr($name, 0, 4) != 'test') continue; if ($limit && !strstr($name, $limit)) continue; if (strlen($name) > $result) { $result = strlen($name); } } return $result; } private static function findFile($path, $file) { $files = glob($path . '/*', GLOB_NOSORT); foreach ($files as $test) { $test = basename($test); if (strcasecmp($file, $test) == 0) return $path . '/' . $test; } return NULL; } /* Small helper method that tries to load a custom test case class */ public static function loadTestClass($class) { $filename = "{$class}.php"; if (($sp = getenv('PHPREDIS_TEST_SEARCH_PATH'))) { $fullname = self::findFile($sp, $filename); } else { $fullname = self::findFile(__DIR__, $filename); } if ( ! $fullname) die("Fatal: Couldn't find $filename\n"); require_once($fullname); if ( ! class_exists($class)) die("Fatal: Loaded '$filename' but didn't find class '$class'\n"); /* Loaded the file and found the class, return it */ return $class; } /* Flag colorization */ public static function flagColorization(bool $override) { self::$colorize = $override && function_exists('posix_isatty') && defined('STDOUT') && posix_isatty(STDOUT); } public static function run($class_name, ?string $limit = NULL, ?string $host = NULL, ?int $port = NULL, $auth = NULL) { if ($limit) $limit = strtolower($limit); $rc = new ReflectionClass($class_name); $methods = $rc->GetMethods(ReflectionMethod::IS_PUBLIC); $max_test_len = self::getMaxTestLen($methods, $limit); foreach($methods as $m) { $name = $m->name; if (substr($name, 0, 4) !== 'test') continue; /* If we're trying to limit to a specific test and can't match the * substring, skip */ if ($limit && stristr($name, $limit) === false) { continue; } $padded_name = str_pad($name, $max_test_len + 1); echo self::make_bold($padded_name); $count = count($class_name::$errors); $rt = new $class_name($host, $port, $auth); try { $rt->setUp(); $rt->$name(); if ($count === count($class_name::$errors)) { $result = self::make_success('PASSED'); } else { $result = self::make_fail('FAILED'); } } catch (Exception $e) { /* We may have simply skipped the test */ if ($e instanceof TestSkippedException) { $result = self::make_warning('SKIPPED'); } else { $class_name::$errors[] = "Uncaught exception '".$e->getMessage()."' ($name)\n"; $result = self::make_fail('FAILED'); } } echo "[" . $result . "]\n"; } echo "\n"; echo implode('', $class_name::$warnings) . "\n"; if (empty($class_name::$errors)) { echo "All tests passed. \o/\n"; return 0; } echo implode('', $class_name::$errors); return 1; } } ?>