diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1014,6 +1014,9 @@ 'phutil_load_library' => 'init/lib/moduleutils.php', 'phutil_loggable_string' => 'utils/utils.php', 'phutil_microseconds_since' => 'utils/utils.php', + 'phutil_nonempty_scalar' => 'utils/utils.php', + 'phutil_nonempty_string' => 'utils/utils.php', + 'phutil_nonempty_stringlike' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', 'phutil_partition' => 'utils/utils.php', 'phutil_passthru' => 'future/exec/execx.php', diff --git a/src/unit/engine/phutil/PhutilTestCase.php b/src/unit/engine/phutil/PhutilTestCase.php --- a/src/unit/engine/phutil/PhutilTestCase.php +++ b/src/unit/engine/phutil/PhutilTestCase.php @@ -154,6 +154,147 @@ throw new PhutilTestSkippedException($message); } + final protected function assertCaught( + $expect, + $actual, + $message = null) { + + if ($message !== null) { + $message = phutil_string_cast($message); + } + + if ($actual === null) { + // This is okay: no exception. + } else if ($actual instanceof Exception) { + // This is also okay. + } else if ($actual instanceof Throwable) { + // And this is okay too. + } else { + // Anything else is no good. + + if ($message !== null) { + $output = pht( + 'Call to "assertCaught(..., , ...)" for test case "%s" '. + 'passed bad value for test result. Expected null, Exception, '. + 'or Throwable; got: %s.', + $message, + phutil_describe_type($actual)); + } else { + $output = pht( + 'Call to "assertCaught(..., , ...)" passed bad value for '. + 'test result. Expected null, Exception, or Throwable; got: %s.', + phutil_describe_type($actual)); + } + + $this->failTest($output); + + throw new PhutilTestTerminatedException($output); + } + + $expect_list = null; + + if ($expect === false) { + $expect_list = array(); + } else if ($expect === true) { + $expect_list = array( + 'Exception', + 'Throwable', + ); + } else if (is_string($expect) || is_array($expect)) { + $list = (array)$expect; + + $items_ok = true; + foreach ($list as $key => $item) { + if (!phutil_nonempty_stringlike($item)) { + $items_ok = false; + break; + } + + $list[$key] = phutil_string_cast($item); + } + + if ($items_ok) { + $expect_list = $list; + } + } + + if ($expect_list === null) { + if ($message !== null) { + $output = pht( + 'Call to "assertCaught(, ...)" for test case "%s" '. + 'passed bad expected value. Expected bool, class name as a string, '. + 'or a list of class names. Got: %s.', + $message, + phutil_describe_type($expect)); + } else { + $output = pht( + 'Call to "assertCaught(, ...)" passed bad expected value. '. + 'expected result. Expected null, Exception, or Throwable; got: %s.', + phutil_describe_type($expect)); + } + + $this->failTest($output); + + throw new PhutilTestTerminatedException($output); + } + + if ($actual === null) { + $is_match = !$expect_list; + } else { + $is_match = false; + foreach ($expect_list as $exception_class) { + if ($actual instanceof $exception_class) { + $is_match = true; + break; + } + } + } + + if ($is_match) { + $this->assertions++; + return; + } + + $caller = self::getCallerInfo(); + $file = $caller['file']; + $line = $caller['line']; + + $output = array(); + + if ($message !== null) { + $output[] = pht( + 'Assertion of caught exception failed (at %s:%d in test case "%s").', + $file, + $line, + $message); + } else { + $output[] = pht( + 'Assertion of caught exception failed (at %s:%d).', + $file, + $line); + } + + if ($actual === null) { + $output[] = pht('Expected any exception, got no exception.'); + } else if (!$expect_list) { + $output[] = pht( + 'Expected no exception, got exception of class "%s".', + get_class($actual)); + } else { + $expected_classes = implode(', ', $expect_list); + $output[] = pht( + 'Expected exception (in class(es): %s), got exception of class "%s".', + $expected_classes, + get_class($actual)); + } + + $output = implode("\n\n", $output); + + $this->failTest($output); + + throw new PhutilTestTerminatedException($output); + } + /* -( Exception Handling )------------------------------------------------- */ diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -1000,4 +1000,74 @@ } } + public function testEmptyStringMethods() { + + $uri = new PhutilURI('http://example.org/'); + + $map = array( + array(null, false, false, false, 'literal null'), + array('', false, false, false, 'empty string'), + array('x', true, true, true, 'nonempty string'), + array(false, null, null, null, 'bool'), + array(1, null, null, true, 'integer'), + array($uri, null, true, true, 'uri object'), + array(2.5, null, null, true, 'float'), + array(array(), null, null, null, 'array'), + array((object)array(), null, null, null, 'object'), + ); + + foreach ($map as $test_case) { + $input = $test_case[0]; + + $expect_string = $test_case[1]; + $expect_stringlike = $test_case[2]; + $expect_scalar = $test_case[3]; + + $test_name = $test_case[4]; + + $this->executeEmptyStringTest( + $input, + $expect_string, + 'phutil_nonempty_string', + $test_name); + + $this->executeEmptyStringTest( + $input, + $expect_stringlike, + 'phutil_nonempty_stringlike', + $test_name); + + $this->executeEmptyStringTest( + $input, + $expect_scalar, + 'phutil_nonempty_scalar', + $test_name); + } + + } + + private function executeEmptyStringTest($input, $expect, $call, $name) { + $name = sprintf('%s(<%s>)', $call, $name); + + $caught = null; + try { + $actual = call_user_func($call, $input); + } catch (Exception $ex) { + $caught = $ex; + } catch (Throwable $ex) { + $caught = $ex; + } + + if ($expect === null) { + $expect_exceptions = array('InvalidArgumentException'); + } else { + $expect_exceptions = false; + } + + $this->assertCaught($expect_exceptions, $caught, $name); + if (!$caught) { + $this->assertEqual($expect, $actual, $name); + } + } + } diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -2094,3 +2094,128 @@ throw new PhutilRegexException($message); } + + +/** + * Test if a value is a nonempty string. + * + * The value "null" and the empty string are considered empty; all other + * strings are considered nonempty. + * + * This method raises an exception if passed a value which is neither null + * nor a string. + * + * @param Value to test. + * @return bool True if the parameter is a nonempty string. + */ +function phutil_nonempty_string($value) { + if ($value === null) { + return false; + } + + if ($value === '') { + return false; + } + + if (is_string($value)) { + return true; + } + + throw new InvalidArgumentException( + pht( + 'Call to phutil_nonempty_string() expected null or a string, got: %s.', + phutil_describe_type($value))); +} + + +/** + * Test if a value is a nonempty, stringlike value. + * + * The value "null", the empty string, and objects which have a "__toString()" + * method which returns the empty string are empty. + * + * Other strings, and objects with a "__toString()" method that returns a + * string other than the empty string are considered nonempty. + * + * This method raises an exception if passed any other value. + * + * @param Value to test. + * @return bool True if the parameter is a nonempty, stringlike value. + */ +function phutil_nonempty_stringlike($value) { + if ($value === null) { + return false; + } + + if ($value === '') { + return false; + } + + if (is_string($value)) { + return true; + } + + if (is_object($value)) { + try { + $string = phutil_string_cast($value); + return phutil_nonempty_string($string); + } catch (Exception $ex) { + // Continue below. + } catch (Throwable $ex) { + // Continue below. + } + } + + throw new InvalidArgumentException( + pht( + 'Call to phutil_nonempty_stringlike() expected a string or stringlike '. + 'object, got: %s.', + phutil_describe_type($value))); +} + + +/** + * Test if a value is a nonempty, scalar value. + * + * The value "null", the empty string, and objects which have a "__toString()" + * method which returns the empty string are empty. + * + * Other strings, objects with a "__toString()" method which returns a + * string other than the empty string, integers, and floats are considered + * scalar. + * + * This method raises an exception if passed any other value. + * + * @param Value to test. + * @return bool True if the parameter is a nonempty, scalar value. + */ +function phutil_nonempty_scalar($value) { + if ($value === null) { + return false; + } + + if ($value === '') { + return false; + } + + if (is_string($value) || is_int($value) || is_float($value)) { + return true; + } + + if (is_object($value)) { + try { + $string = phutil_string_cast($value); + return phutil_nonempty_string($string); + } catch (Exception $ex) { + // Continue below. + } catch (Throwable $ex) { + // Continue below. + } + } + + throw new InvalidArgumentException( + pht( + 'Call to phutil_nonempty_scalar() expected: a string; or stringlike '. + 'object; or int; or float. Got: %s.', + phutil_describe_type($value))); +}