Files
dualfroz 811416bf63 fix: distinguish PCRE failure from a mismatch in Assert::regex()
preg_match() has three outcomes -- 1, 0 and false -- but regex() folded
false into the "no match" branch. An uncompilable pattern or an exhausted
PCRE limit therefore leaked a raw PHP warning out of the library and then
reported the misleading "The value ... does not match the expected pattern."

Check for false separately and report the pattern together with
preg_last_error_msg(), following the existing false === $x = nativeCall()
handling in Assert::strlen(). The caller's custom message is intentionally
not reused for that branch, since it describes a mismatch that never
happened. Matching values and genuine mismatches are unaffected.
2026-09-03 14:28:48 +02:00

1191 lines
51 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the webmozart/assert package.
*
* (c) Bernhard Schussek <bschussek@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Webmozart\Assert\Tests;
use ArrayIterator;
use ArrayObject;
use DateTime;
use DateTimeImmutable;
use Error;
use Exception;
use InvalidArgumentException;
use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use SimpleXMLElement;
use stdClass;
use Webmozart\Assert\Assert;
/**
* @since 1.0
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class AssertTest extends TestCase
{
private const SKIP_MIXIN_ASSERTION_TESTS = [
'isInitialized',
];
private const SKIP_CUSTOM_MESSAGE_TESTS = [
'isAOf',
'isAnyOf',
'isNotA',
'startsWithLetter',
];
public static function getResource()
{
static $resource;
return $resource ??= tmpfile();
}
public static function getTests(): array
{
$nanList = ['key' => 2, NAN];
unset($nanList['key']);
$normalList = ['foo' => 'b', 3];
unset($normalList['foo']);
$a = new ToStringClass('testString');
$b = new ToStringClass('testString');
$c = new ToStringClass('otherString');
return [
['string', ['value'], true],
['string', [''], true],
['string', [1234], false],
['stringNotEmpty', ['value'], true],
['stringNotEmpty', ['0'], true],
['stringNotEmpty', [''], false],
['stringNotEmpty', [1234], false],
['integer', [123], true],
['integer', ['123'], false],
['integer', [1.0], false],
['integer', [1.23], false],
['integerish', [1.0], true],
['integerish', [1.23], false],
['integerish', [123], true],
['integerish', ['123'], true],
['positiveInteger', [123], true],
['positiveInteger', [1], true],
['positiveInteger', [-123], false],
['positiveInteger', [0], false],
['positiveInteger', [0.0], false],
['positiveInteger', ['123'], false],
['positiveInteger', ['-123'], false],
['positiveInteger', ['0'], false],
['positiveInteger', [1.0], false],
['positiveInteger', [1.23], false],
['notNegativeInteger', [123], true],
['notNegativeInteger', [1], true],
['notNegativeInteger', [-123], false],
['notNegativeInteger', [0], true],
['notNegativeInteger', [0.0], false],
['notNegativeInteger', ['123'], false],
['notNegativeInteger', ['-123'], false],
['notNegativeInteger', ['0'], false],
['notNegativeInteger', [1.0], false],
['notNegativeInteger', [1.23], false],
['negativeInteger', [123], false],
['negativeInteger', [1], false],
['negativeInteger', [-123], true],
['negativeInteger', [0], false],
['negativeInteger', [0.0], false],
['negativeInteger', ['123'], false],
['negativeInteger', ['-123'], false],
['negativeInteger', ['0'], false],
['negativeInteger', [1.0], false],
['negativeInteger', [1.23], false],
['float', [1.0], true],
['float', [1.23], true],
['float', [123], false],
['float', ['123'], false],
['numeric', [1.0], true],
['numeric', [1.23], true],
['numeric', [123], true],
['numeric', ['123'], true],
['numeric', ['foo'], false],
['natural', [0], true],
['natural', [1], true],
['natural', [-1], false],
['natural', ['1'], false],
['natural', [1.0], false],
['natural', [1.23], false],
['boolean', [true], true],
['boolean', [false], true],
['boolean', [1], false],
['boolean', ['1'], false],
['scalar', ['1'], true],
['scalar', [123], true],
['scalar', [true], true],
['scalar', [null], false],
['scalar', [[]], false],
['scalar', [new stdClass()], false],
['object', [new stdClass()], true],
['object', [new RuntimeException()], true],
['object', [null], false],
['object', [true], false],
['object', [1], false],
['object', [[]], false],
['objectish', [new stdClass()], true],
['objectish', [new RuntimeException()], true],
['objectish', [null], false],
['objectish', [true], false],
['objectish', [1], false],
['objectish', [[]], false],
['resource', [self::getResource()], true],
['resource', [self::getResource(), 'stream'], true],
['resource', [self::getResource(), 'other'], false],
['resource', [1], false],
['isInitialized', [new class {
public mixed $a = null;
}, 'a'], true],
['isInitialized', [new class {
public mixed $a;
}, 'a'], false],
['isInitialized', [new class {
public mixed $a;
public mixed $b = true;
}, 'a'], false],
['isInitialized', [new class {
public mixed $a;
public mixed $b = true;
}, 'b'], true],
['isCallable', ['strlen'], true],
['isCallable', [[self::class, 'getTests']], true],
['isCallable', [function () {}], true],
['isCallable', [1234], false],
['isCallable', ['foobar'], false],
['isArray', [[]], true],
['isArray', [[1, 2, 3]], true],
['isArray', [new ArrayIterator([])], false],
['isArray', [123], false],
['isArray', [new stdClass()], false],
['isArrayAccessible', [[]], true],
['isArrayAccessible', [[1, 2, 3]], true],
['isArrayAccessible', [new ArrayObject([])], true],
['isArrayAccessible', [123], false],
['isArrayAccessible', [new stdClass()], false],
['isCountable', [[]], true],
['isCountable', [[1, 2]], true],
['isCountable', [new ArrayIterator([])], true],
['isCountable', [new stdClass()], false],
['isCountable', [new SimpleXMLElement('<foo>bar</foo>')], true],
['isCountable', ['abcd'], false],
['isCountable', [123], false],
['isIterable', [[]], true],
['isIterable', [[1, 2, 3]], true],
['isIterable', [new ArrayIterator([])], true],
['isIterable', [123], false],
['isIterable', [new stdClass()], false],
['isInstanceOf', [new stdClass(), 'stdClass'], true],
['isInstanceOf', [new Exception(), 'stdClass'], false],
['isInstanceOf', [123, 'stdClass'], false],
['isInstanceOf', [[], 'stdClass'], false],
['isInstanceOf', [null, 'stdClass'], false],
['notInstanceOf', [new stdClass(), 'stdClass'], false],
['notInstanceOf', [new Exception(), 'stdClass'], true],
['notInstanceOf', [123, 'stdClass'], false],
['notInstanceOf', [[], 'stdClass'], false],
['notInstanceOf', [null, 'stdClass'], false],
['isInstanceOfAny', [new ArrayIterator(), ['Iterator', 'ArrayAccess']], true],
['isInstanceOfAny', [new Exception(), ['Exception', 'Countable']], true],
['isInstanceOfAny', [new Exception(), ['ArrayAccess', 'Countable']], false],
['isInstanceOfAny', [123, ['stdClass']], false],
['isInstanceOfAny', [[], ['stdClass']], false],
['isNotInstanceOfAny', [new ArrayIterator(), ['Exception', 'Countable']], false],
['isNotInstanceOfAny', [new Exception(), ['ArrayAccess', 'Countable']], true],
['isNotInstanceOfAny', [new Exception(), ['Exception', 'Countable']], false],
['isNotInstanceOfAny', [123, ['stdClass']], true],
['isNotInstanceOfAny', [[], ['stdClass']], true],
['isAOf', ['stdClass', 'stdClass'], true],
['isAOf', ['stdClass', 123], false],
['isAOf', ['Iterator', 'ArrayIterator'], false],
['isAOf', [123, 'Iterator'], false],
['isAOf', [[], 'Iterator'], false],
['isAnyOf', ['ArrayIterator', ['Iterator', 'ArrayAccess']], true],
['isAnyOf', ['ArrayIterator', [123]], false],
['isAnyOf', ['Exception', ['Exception', 'Countable']], true],
['isAnyOf', ['Exception', ['ArrayAccess', 'Countable']], false],
['isAnyOf', [123, ['stdClass']], false],
['isAnyOf', [[], ['stdClass']], false],
['isNotA', ['stdClass', 'stdClass'], false],
['isNotA', ['stdClass', 123], false],
['isNotA', [[], 'Iterator'], false],
['true', [true], true],
['true', [false], false],
['true', [1], false],
['true', [null], false],
['false', [false], true],
['false', [true], false],
['false', [1], false],
['false', [0], false],
['false', [null], false],
['notFalse', [false], false],
['notFalse', [true], true],
['notFalse', [1], true],
['notFalse', [0], true],
['notFalse', [null], true],
['null', [null], true],
['null', [false], false],
['null', [0], false],
['notNull', [false], true],
['notNull', [0], true],
['notNull', [null], false],
['isEmpty', [null], true],
['isEmpty', [false], true],
['isEmpty', [0], true],
['isEmpty', [''], true],
['isEmpty', [1], false],
['isEmpty', ['a'], false],
['notEmpty', [1], true],
['notEmpty', ['a'], true],
['notEmpty', [null], false],
['notEmpty', [false], false],
['notEmpty', [0], false],
['notEmpty', [''], false],
['eq', [1, 1], true],
['eq', [1, '1'], true],
['eq', [1, true], true],
['eq', [1, 0], false],
['eq', [$a, $a], true],
['eq', [$a, $b], true],
['eq', [$a, $c], false],
['notEq', [1, 0], true],
['notEq', [1, 1], false],
['notEq', [1, '1'], false],
['notEq', [1, true], false],
['same', [1, 1], true],
['same', [1, '1'], false],
['same', [1, true], false],
['same', [1, 0], false],
['same', [$a, $b], false],
['same', [$a, $a], true],
['notSame', [1, 0], true],
['notSame', [1, 1], false],
['notSame', [1, '1'], true],
['notSame', [1, true], true],
['notSame', [$a, $b], true],
['notSame', [$a, $a], false],
['greaterThan', [1, 0], true],
['greaterThan', [0, 0], false],
['greaterThanEq', [2, 1], true],
['greaterThanEq', [1, 1], true],
['greaterThanEq', [0, 1], false],
['lessThan', [0, 1], true],
['lessThan', [1, 1], false],
['lessThanEq', [0, 1], true],
['lessThanEq', [1, 1], true],
['lessThanEq', [2, 1], false],
['range', [1, 1, 2], true],
['range', [2, 1, 2], true],
['range', [0, 1, 2], false],
['range', [3, 1, 2], false],
['oneOf', [1, [1, 2, 3]], true],
['oneOf', [1, ['1', '2', '3']], false],
['notOneOf', [1, [1, 2, 3]], false],
['notOneOf', [1, ['1', '2', '3']], true],
['inArray', [1, [1, 2, 3]], true],
['inArray', [1, ['1', '2', '3']], false],
['inArray', [$a, [$a]], true],
['inArray', [$a, [(string) $a]], false],
['inArray', [$a, [$b]], false],
['inArray', [$a, [$c]], false],
['notInArray', [1, [1, 2, 3]], false],
['notInArray', [1, ['1', '2', '3']], true],
['notInArray', [$a, [$a]], false],
['notInArray', [$a, [$b]], true],
['notInArray', [$a, [$c]], true],
['contains', ['abcd', 'ab'], true],
['contains', ['abcd', 'bc'], true],
['contains', ['abcd', 'cd'], true],
['contains', ['abcd', 'de'], false],
['contains', ['', 'de'], false],
['contains', ['äþçð', 'äþ'], true],
['contains', ['äþçð', 'þç'], true],
['contains', ['äþçð', 'çð'], true],
['contains', ['äþçð', 'ðé'], false],
['contains', ['', 'ðé'], false],
['contains', ['あいうえ', 'あい'], true],
['contains', ['あいうえ', 'いう'], true],
['contains', ['あいうえ', 'うえ'], true],
['contains', ['あいうえ', 'えお'], false],
['contains', ['', 'えお'], false],
['contains', ['😄😑☹️', '😄'], true],
['contains', ['😄😑☹️', '😑'], true],
['contains', ['😄😑☹️', '☹️'], true],
['contains', ['😄😑☹️', '😄☹️'], false],
['contains', ['', '😑'], false],
['notContains', ['abcd', 'ab'], false],
['notContains', ['abcd', 'bc'], false],
['notContains', ['abcd', 'cd'], false],
['notContains', ['abcd', 'de'], true],
['notContains', ['', 'de'], true],
['notContains', ['äþçð', 'äþ'], false],
['notContains', ['äþçð', 'þç'], false],
['notContains', ['äþçð', 'çð'], false],
['notContains', ['äþçð', 'ðé'], true],
['notContains', ['', 'ðé'], true],
['notContains', ['あいうえ', 'あい'], false],
['notContains', ['あいうえ', 'いう'], false],
['notContains', ['あいうえ', 'うえ'], false],
['notContains', ['あいうえ', 'えお'], true],
['notContains', ['', 'えお'], true],
['notContains', ['😄😑☹️', '😄'], false],
['notContains', ['😄😑☹️', '😑'], false],
['notContains', ['😄😑☹️', '☹️'], false],
['notContains', ['😄😑☹️', '😄☹️'], true],
['notContains', ['', '😑'], true],
['notWhitespaceOnly', ['abc'], true],
['notWhitespaceOnly', ['123'], true],
['notWhitespaceOnly', [' abc '], true],
['notWhitespaceOnly', ['a b c'], true],
['notWhitespaceOnly', [''], false],
['notWhitespaceOnly', [' '], false],
['notWhitespaceOnly', ["\t"], false],
['notWhitespaceOnly', ["\n"], false],
['notWhitespaceOnly', ["\r"], false],
['notWhitespaceOnly', ["\r\n\t "], false],
['startsWith', ['abcd', 'ab'], true],
['startsWith', ['abcd', 'bc'], false],
['startsWith', ['', 'bc'], false],
['startsWith', ['äþçð', 'äþ'], true],
['startsWith', ['äþçð', 'þç'], false],
['startsWith', ['', 'þç'], false],
['startsWith', ['あいうえ', 'あい'], true],
['startsWith', ['あいうえ', 'いう'], false],
['startsWith', ['', 'いう'], false],
['startsWith', ['😄😑☹️', '😄'], true],
['startsWith', ['😄😑☹️', '😑'], false],
['startsWith', ['', '😑'], false],
['startsWithLetter', ['abcd'], true],
['startsWithLetter', [[66]], false],
['startsWithLetter', ['a'], true],
['startsWithLetter', ['a1'], true],
['startsWithLetter', ['1abcd'], false],
['startsWithLetter', ['1'], false],
['startsWithLetter', [''], false],
['startsWithLetter', [null], false],
['startsWithLetter', [66], false],
['notStartsWith', ['abcd', 'ab'], false],
['notStartsWith', ['abcd', 'bc'], true],
['notStartsWith', ['', 'bc'], true],
['notStartsWith', ['äþçð', 'äþ'], false],
['notStartsWith', ['äþçð', 'þç'], true],
['notStartsWith', ['', 'þç'], true],
['notStartsWith', ['あいうえ', 'あい'], false],
['notStartsWith', ['あいうえ', 'いう'], true],
['notStartsWith', ['', 'いう'], true],
['notStartsWith', ['😄😑☹️', '😄'], false],
['notStartsWith', ['😄😑☹️', '😑'], true],
['notStartsWith', ['', '😑'], true],
['endsWith', ['abcd', 'cd'], true],
['endsWith', ['abcd', 'bc'], false],
['endsWith', ['', 'bc'], false],
['endsWith', ['äþçð', 'çð'], true],
['endsWith', ['äþçð', 'þç'], false],
['endsWith', ['', 'þç'], false],
['endsWith', ['あいうえ', 'うえ'], true],
['endsWith', ['あいうえ', 'いう'], false],
['endsWith', ['', 'いう'], false],
['endsWith', ['😄😑☹️', '☹️'], true],
['endsWith', ['😄😑☹️', '😑'], false],
['endsWith', ['', '😑'], false],
['notEndsWith', ['abcd', 'cd'], false],
['notEndsWith', ['abcd', 'bc'], true],
['notEndsWith', ['', 'bc'], true],
['notEndsWith', ['äþçð', 'çð'], false],
['notEndsWith', ['äþçð', 'þç'], true],
['notEndsWith', ['', 'þç'], true],
['notEndsWith', ['あいうえ', 'うえ'], false],
['notEndsWith', ['あいうえ', 'いう'], true],
['notEndsWith', ['', 'いう'], true],
['notEndsWith', ['😄😑☹️', '☹️'], false],
['notEndsWith', ['😄😑☹️', '😑'], true],
['notEndsWith', ['', '😑'], true],
['regex', ['abcd', '~^ab~'], true],
['regex', ['abcd', '~^bc~'], false],
['regex', ['', '~^bc~'], false],
['notRegex', ['abcd', '{^ab}'], false],
['notRegex', ['abcd', '{^bc}'], true],
['notRegex', ['', '{^bc}'], true],
['unicodeLetters', ['abcd'], true],
['unicodeLetters', ['ᴁڅਘธブ乶'], true],
['unicodeLetters', ['ȁȄ'], true],
['unicodeLetters', ['ȁ1Ȅ'], false],
['unicodeLetters', ['©'], false],
['unicodeLetters', ['🙁'], false],
['unicodeLetters', [''], false],
['alpha', ['abcd'], true],
['alpha', ['ab1cd'], false],
['alpha', [''], false],
['alpha', [66], false],
['alpha', [[]], false],
['digits', ['1234'], true],
['digits', ['12a34'], false],
['digits', [''], false],
['digits', [1234], false],
['alnum', ['ab12'], true],
['alnum', ['ab12$'], false],
['alnum', [''], false],
['alnum', [1234], false],
['lower', ['abcd'], true],
['lower', ['abCd'], false],
['lower', ['ab_d'], false],
['lower', [''], false],
['lower', [1234], false],
['upper', ['ABCD'], true],
['upper', ['ABcD'], false],
['upper', ['AB_D'], false],
['upper', [''], false],
['upper', [1234], false],
['length', ['abcd', 4], true],
['length', ['abc', 4], false],
['length', ['abcde', 4], false],
['length', ['äbcd', 4], true, true],
['length', ['äbc', 4], false, true],
['length', ['äbcde', 4], false, true],
['length', ['あbcd', 4], true, true], // 'HIRAGANA LETTER A' (U+3042)
['length', ['あbc', 4], false, true],
['length', ['あbcde', 4], false, true],
['minLength', ['abcd', 4], true],
['minLength', ['abcde', 4], true],
['minLength', ['abc', 4], false],
['minLength', ['äbcd', 4], true, true],
['minLength', ['äbcde', 4], true, true],
['minLength', ['äbc', 4], false, true],
['minLength', ['あbcd', 4], true, true],
['minLength', ['あbcde', 4], true, true],
['minLength', ['あbc', 4], false, true],
['maxLength', ['abcd', 4], true],
['maxLength', ['abc', 4], true],
['maxLength', ['abcde', 4], false],
['maxLength', ['äbcd', 4], true, true],
['maxLength', ['äbc', 4], true, true],
['maxLength', ['äbcde', 4], false, true],
['maxLength', ['あbcd', 4], true, true],
['maxLength', ['あbc', 4], true, true],
['maxLength', ['あbcde', 4], false, true],
['lengthBetween', ['abcd', 3, 5], true],
['lengthBetween', ['abc', 3, 5], true],
['lengthBetween', ['abcde', 3, 5], true],
['lengthBetween', ['ab', 3, 5], false],
['lengthBetween', ['abcdef', 3, 5], false],
['lengthBetween', ['äbcd', 3, 5], true, true],
['lengthBetween', ['äbc', 3, 5], true, true],
['lengthBetween', ['äbcde', 3, 5], true, true],
['lengthBetween', ['äb', 3, 5], false, true],
['lengthBetween', ['äbcdef', 3, 5], false, true],
['lengthBetween', ['あbcd', 3, 5], true, true],
['lengthBetween', ['あbc', 3, 5], true, true],
['lengthBetween', ['あbcde', 3, 5], true, true],
['lengthBetween', ['あb', 3, 5], false, true],
['lengthBetween', ['あbcdef', 3, 5], false, true],
['fileExists', [__FILE__], true],
['fileExists', [__DIR__], true],
['fileExists', [__DIR__.'/foobar'], false],
['file', [__FILE__], true],
['file', [__DIR__], false],
['file', [__DIR__.'/foobar'], false],
['directory', [__DIR__], true],
['directory', [__FILE__], false],
['directory', [__DIR__.'/foobar'], false],
// no tests for readable()/writable() for now
['classExists', [__CLASS__], true],
['classExists', [__NAMESPACE__.'\Foobar'], false],
['subclassOf', [__CLASS__, 'PHPUnit\Framework\TestCase'], true],
['subclassOf', [__CLASS__, 'stdClass'], false],
['interfaceExists', ['\Countable'], true],
['interfaceExists', [__CLASS__], false],
['implementsInterface', ['ArrayIterator', 'Traversable'], true],
['implementsInterface', [__CLASS__, 'Traversable'], false],
['implementsInterface', [new DateTimeImmutable(), 'DateTimeInterface'], true],
['implementsInterface', [new DateTimeImmutable(), 'Traversable'], false],
['implementsInterface', [new ArrayIterator([]), 'DateTimeInterface'], false],
['implementsInterface', [new ArrayIterator([]), 'Traversable'], true],
['propertyExists', [(object) ['property' => 0], 'property'], true],
['propertyExists', [(object) ['property' => null], 'property'], true],
['propertyExists', [(object) ['property' => null], 'foo'], false],
['propertyNotExists', [(object) ['property' => 0], 'property'], false],
['propertyNotExists', [(object) ['property' => null], 'property'], false],
['methodExists', ['RuntimeException', 'getMessage'], true],
['methodExists', [new RuntimeException(), 'getMessage'], true],
['methodExists', ['stdClass', 'getMessage'], false],
['methodExists', [new stdClass(), 'getMessage'], false],
['methodNotExists', ['RuntimeException', 'getMessage'], false],
['methodNotExists', [new RuntimeException(), 'getMessage'], false],
['methodNotExists', ['stdClass', 'getMessage'], true],
['methodNotExists', [new stdClass(), 'getMessage'], true],
['keyExists', [['key' => 0], 'key'], true],
['keyExists', [['key' => null], 'key'], true],
['keyExists', [['key' => null], 'foo'], false],
['keyNotExists', [['key' => 0], 'key'], false],
['keyNotExists', [['key' => null], 'key'], false],
['keyNotExists', [['key' => null], 'foo'], true],
['validArrayKey', ['abcd'], true],
['validArrayKey', [1], true],
['validArrayKey', [false], false],
['validArrayKey', [true], false],
['validArrayKey', [new stdClass()], false],
['validArrayKey', [new ToStringClass('testString')], false],
['validArrayKey', [self::getResource()], false],
['count', [[0, 1, 2], 3], true],
['count', [[0, 1, 2], 2], false],
['minCount', [[0], 2], false],
['minCount', [[0, 1], 2], true],
['minCount', [[0, 1, 2], 2], true],
['maxCount', [[0, 1, 2], 2], false],
['maxCount', [[0, 1], 2], true],
['maxCount', [[0], 2], true],
['countBetween', [[0, 1, 2], 4, 5], false],
['countBetween', [[0, 1, 2], 3, 5], true],
['countBetween', [[0, 1, 2], 1, 2], false],
['countBetween', [[0, 1, 2], 2, 5], true],
['countBetween', [[0, 1, 2], 2, 3], true],
['isList', [[1, 2, 3]], true],
['isList', [[]], true],
['isList', [[0 => 1, 2 => 3]], false],
['isList', [['key' => 1, 'foo' => 2]], false],
['isList', [true], false],
['isList', [false], false],
['isList', [[true]], true],
['isList', [[false]], true],
['isList', [[[1], [2]]], true],
['isList', [[['foo' => 'bar'], ['baz' => 'tab']]], true],
['isList', [$nanList], true],
['isList', [$normalList], true],
['isNonEmptyList', [[1, 2, 3]], true],
['isNonEmptyList', [[]], false],
['isNonEmptyList', [[0 => 1, 2 => 3]], false],
['isNonEmptyList', [['key' => 1, 'foo' => 2]], false],
['isNonEmptyList', [true], false],
['isNonEmptyList', [false], false],
['isNonEmptyList', [[true]], true],
['isNonEmptyList', [[false]], true],
['isNonEmptyList', [[[1], [2]]], true],
['isNonEmptyList', [[['foo' => 'bar'], ['baz' => 'tab']]], true],
['isMap', [['key' => 1, 'foo' => 2]], true],
['isMap', [[0 => 1, 2 => 3]], true],
['isMap', [[]], true],
['isMap', [[1, 2, 3]], false],
['isNonEmptyMap', [['key' => 1, 'foo' => 2]], true],
['isNonEmptyMap', [[0 => 1, 2 => 3]], true],
['isNonEmptyMap', [[]], false],
['isNonEmptyMap', [[1, 2, 3]], false],
['uuid', ['00000000-0000-0000-0000-000000000000'], true],
['uuid', ['urn:uuid:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true],
['uuid', ['uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true],
['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], true],
['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], true],
['uuid', ['ff6f8cb0-c57d-11e1-9b21-0800200c9a66'], true],
['uuid', ['ff6f8cb0-c57d-31e1-9b21-0800200c9a66'], true],
['uuid', ['ff6f8cb0-c57d-41e1-9b21-0800200c9a66'], true],
['uuid', ['ff6f8cb0-c57d-51e1-9b21-0800200c9a66'], true],
['uuid', ['FF6F8CB0-C57D-11E1-9B21-0800200C9A66'], true],
['uuid', ["urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66\n"], false],
['uuid', ["ff6f8cb0-c57d-21e1-9b21-0800200c9a66\n"], false],
['uuid', ["FF6F8CB0-C57D-11E1-9B21-0800200C9A66\n"], false],
['uuid', ['zf6f8cb0-c57d-11e1-9b21-0800200c9a66'], false],
['uuid', ['af6f8cb0c57d11e19b210800200c9a66'], false],
['uuid', ['ff6f8cb0-c57da-51e1-9b21-0800200c9a66'], false],
['uuid', ['af6f8cb-c57d-11e1-9b21-0800200c9a66'], false],
['uuid', ['3f6f8cb0-c57d-11e1-9b21-0800200c9a6'], false],
['uuid', ['f{}f6f8cb0-c57d-21e1-9b21-0800200c9a66'], false],
['uuid', ['ff6f8cb0-c57d-21e1-9b21-08002urn:00c9a66'], false],
['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66uuid:'], false],
['uuid', ['urn:ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false],
['uuid', ['urn:uuid:{ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false],
['uuid', ['{}{}ff6f8cb0-c57d-21e1-9b21-0800200c9a66{}{}'], false],
['uuid', ['{ff6f8cb0-c57d-21e1-9b21-0800200c9a66'], false],
['uuid', ['ff6f8cb0-c57d-21e1-9b21-0800200c9a66}'], false],
['throws', [function () { throw new LogicException('test'); }, 'LogicException'], true],
['throws', [function () { throw new LogicException('test'); }, 'IllogicException'], false],
['throws', [function () { throw new Exception('test'); }], true],
['throws', [function () { trigger_error('test'); }, 'Throwable'], false, false],
['throws', [function () { trigger_error('test'); }, 'Unthrowable'], false, false],
['throws', [function () { throw new Error(); }, 'Throwable'], true, true],
['ip', ['192.168.0.1'], true],
['ip', ['255.255.255.255'], true],
['ip', ['0.0.0.0'], true],
['ip', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], true],
['ip', ['::ffff:192.0.2.1'], true],
['ip', ['::1'], true],
['ip', ['::'], true],
['ip', ['foo'], false],
['ip', [123], false],
['ip', [[]], false],
['ip', [null], false],
['ip', [false], false],
['ipv4', ['192.168.0.1'], true],
['ipv4', ['255.255.255.255'], true],
['ipv4', ['0.0.0.0'], true],
['ipv4', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], false],
['ipv4', ['::ffff:192.0.2.1'], false],
['ipv4', ['::1'], false],
['ipv4', ['::'], false],
['ipv4', ['foo'], false],
['ipv4', [123], false],
['ipv4', [[]], false],
['ipv4', [null], false],
['ipv4', [false], false],
['ipv6', ['192.168.0.1'], false],
['ipv6', ['255.255.255.255'], false],
['ipv6', ['0.0.0.0'], false],
['ipv6', ['2001:0db8:0000:0042:0000:8a2e:0370:7334'], true],
['ipv6', ['::ffff:192.0.2.1'], true],
['ipv6', ['::1'], true],
['ipv6', ['::'], true],
['ipv6', ['foo'], false],
['ipv6', [123], false],
['ipv6', [[]], false],
['ipv6', [null], false],
['ipv6', [false], false],
['email', ['foo'], false],
['email', [123], false],
['email', ['foo.com'], false],
['email', ['foo@bar.com'], true],
['email', ['föö@bar.com'], true],
['uniqueValues', [['qwerty', 'qwerty']], false],
['uniqueValues', [['asdfg', 'qwerty']], true],
['uniqueValues', [[123, '123']], false],
['uniqueValues', [[$a, $a]], false],
['uniqueValues', [[$a, $b]], false],
['uniqueValues', [[$a, $c]], true],
['isStatic', [static function () {}], true],
['isStatic', [function () {}], false],
['notStatic', [static function () {}], false],
['notStatic', [function () {}], true],
];
}
public static function getMethods(): array
{
$methods = [];
foreach (self::getTests() as $params) {
$methods[$params[0]] = [$params[0]];
}
return array_values($methods);
}
#[DataProvider('getTests')]
public function testAssert(string $method, array $args, bool $success, bool $multibyte = false): void
{
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
}
$result = call_user_func_array(['Webmozart\Assert\Assert', $method], $args);
$this->assertSame($args[array_key_first($args)], $result);
}
#[DataProvider('getTests')]
public function testCustomMessage(string $method, array $args, bool $success, bool $multibyte = false): void
{
if (in_array($method, self::SKIP_CUSTOM_MESSAGE_TESTS)) {
$this->markTestSkipped("The method $method could have specific message handling instead of custom message.");
}
if ($args === []) {
$this->addToAssertionCount(1);
return;
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('Custom error message');
}
$args['message'] = 'Custom error message';
$result = Assert::$method(...$args);
$this->assertSame($args[array_key_first($args)], $result);
}
#[DataProvider('getTests')]
public function testLazyMessageCallbackCalled(string $method, array $args, bool $success, bool $multibyte = false): void
{
if ($args === []) {
$this->addToAssertionCount(1);
return;
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
}
$called = 0;
$args['message'] = function () use (&$called) {
++$called;
return 'Custom error message number';
};
Assert::$method(...$args);
$expectedCalled = $success ? 0 : 1;
$this->assertSame($expectedCalled, $called, sprintf('The lazy message callback should be called exactly %d times.', $expectedCalled));
}
#[DataProvider('getTests')]
public function testNullOr(string $method, array $args, bool $success, bool $multibyte = false): void
{
if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) {
$this->markTestSkipped("The method $method does not have nullOr Mixin.");
}
if (in_array($method, ['null', 'notNull'])) {
$this->markTestSkipped('Meaningless test of '.$method);
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success && null !== reset($args)) {
$this->expectException('\InvalidArgumentException');
}
$result = call_user_func_array(['Webmozart\Assert\Assert', 'nullOr'.ucfirst($method)], $args);
$this->assertSame($args[array_key_first($args)], $result);
}
#[DataProvider('getMethods')]
public function testNullOrAcceptsNull(string $method): void
{
if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) {
$this->markTestSkipped("The method $method does not have nullOr Mixin.");
}
if (in_array($method, ['null', 'notNull'])) {
$this->markTestSkipped('Meaningless test of '.$method);
}
$result = call_user_func(['Webmozart\Assert\Assert', 'nullOr'.ucfirst($method)], null, '', '');
$this->assertNull($result);
}
#[DataProvider('getTests')]
public function testAllArray(string $method, array $args, bool $success, bool $multibyte = false): void
{
if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) {
$this->markTestSkipped("The method $method does not have all Mixin.");
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
}
$arg = array_shift($args);
array_unshift($args, [$arg]);
$result = call_user_func_array(['Webmozart\Assert\Assert', 'all'.ucfirst($method)], $args);
$this->assertSame($args[array_key_first($args)], $result);
}
#[DataProvider('getTests')]
public function testAllNullOrArray(string $method, array $args, bool $success, bool $multibyte = false): void
{
if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) {
$this->markTestSkipped("The method $method does not have allNullOr Mixin.");
}
if (in_array($method, ['null', 'notNull'])) {
$this->markTestSkipped('Meaningless test of '.$method);
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
$arg = array_shift($args);
if ($arg === null) {
$this->addToAssertionCount(1);
return;
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
}
array_unshift($args, [$arg, null]);
$result = call_user_func_array(['Webmozart\Assert\Assert', 'allNullOr'.ucfirst($method)], $args);
$this->assertSame($args[array_key_first($args)], $result);
}
#[DataProvider('getTests')]
public function testAllTraversable(string $method, array $args, bool $success, bool $multibyte = false): void
{
if (in_array($method, self::SKIP_MIXIN_ASSERTION_TESTS)) {
$this->markTestSkipped("The method $method does not have all Mixin.");
}
if ($multibyte && !function_exists('mb_strlen')) {
$this->markTestSkipped('The function mb_strlen() is not available');
}
if (!$success) {
$this->expectException('\InvalidArgumentException');
}
$arg = array_shift($args);
array_unshift($args, new ArrayIterator([$arg]));
$result = call_user_func_array(['Webmozart\Assert\Assert', 'all'.ucfirst($method)], $args);
$this->assertSame($args[array_key_first($args)], $result);
}
public static function getStringConversions(): array
{
return [
['integer', ['foobar'], 'Expected an integer. Got: string'],
['string', [1], 'Expected a string. Got: integer'],
['string', [true], 'Expected a string. Got: boolean'],
['string', [null], 'Expected a string. Got: NULL'],
['string', [[]], 'Expected a string. Got: array'],
['string', [new stdClass()], 'Expected a string. Got: stdClass'],
['string', [self::getResource()], 'Expected a string. Got: resource'],
['eq', ['1', '2'], 'Expected a value equal to "2". Got: "1"'],
['eq', [new ToStringClass('XXX'), new ToStringClass('YYY')], 'Expected a value equal to Webmozart\Assert\Tests\ToStringClass: "YYY". Got: Webmozart\Assert\Tests\ToStringClass: "XXX"'],
['eq', [1, 2], 'Expected a value equal to 2. Got: 1'],
['eq', [true, false], 'Expected a value equal to false. Got: true'],
['eq', [true, null], 'Expected a value equal to null. Got: true'],
['eq', [null, true], 'Expected a value equal to true. Got: null'],
['eq', [[1], [2]], 'Expected a value equal to array. Got: array'],
['eq', [new ArrayIterator([]), new stdClass()], 'Expected a value equal to stdClass. Got: ArrayIterator'],
['eq', [1, self::getResource()], 'Expected a value equal to resource. Got: 1'],
['lessThan', [new DateTime('2020-01-01 00:00:00+00:00'), new DateTime('1999-01-01 00:00:00+00:00')], 'Expected a value less than DateTime: "1999-01-01T00:00:00+00:00". Got: DateTime: "2020-01-01T00:00:00+00:00"'],
];
}
#[DataProvider('getStringConversions')]
public function testConvertValuesToStrings(string $method, array $args, string $exceptionMessage): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage($exceptionMessage);
call_user_func_array(['Webmozart\Assert\Assert', $method], $args);
}
public static function getInvalidInstanceOfCases(): iterable
{
yield [
[null, 'stdClass'],
'Expected an instance of stdClass. Got: NULL',
];
yield [
[123, 'stdClass'],
'Expected an instance of stdClass. Got: integer',
];
yield [
[[], 'Iterator'],
'Expected an instance of Iterator. Got: array',
];
yield [
[new stdClass(), 'Iterator'],
'Expected an instance of Iterator. Got: stdClass',
];
}
#[DataProvider('getInvalidInstanceOfCases')]
public function testInstanceOfExceptionMessages(array $args, string $exceptionMessage): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage($exceptionMessage);
call_user_func_array(['Webmozart\Assert\Assert', 'isInstanceOf'], $args);
}
public static function getInvalidIsAOfCases(): iterable
{
yield [
['stdClass', 123],
'Expected class as a string. Got: integer',
];
yield [
['Iterator', 'ArrayIterator'],
'Expected an instance of this class or to this class among its parents "ArrayIterator". Got: "Iterator"',
];
yield [
[123, 'Iterator'],
'Expected an instance of this class or to this class among its parents "Iterator". Got: 123',
];
yield [
[[], 'Iterator'],
'Expected an instance of this class or to this class among its parents "Iterator". Got: array',
];
yield [
[new stdClass(), 'Iterator'],
'Expected an instance of this class or to this class among its parents "Iterator". Got: stdClass',
];
}
#[DataProvider('getInvalidIsAOfCases')]
public function testIsAOfExceptionMessages(array $args, string $exceptionMessage): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage($exceptionMessage);
call_user_func_array(['Webmozart\Assert\Assert', 'isAOf'], $args);
}
public function testResourceOfTypeCustomMessage(): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('I want a resource of type curl. Got: NULL');
Assert::resource(null, 'curl', 'I want a resource of type %2$s. Got: %s');
}
public function testEnumAssertionErrorMessage(): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('Expected null. Got: Webmozart\Assert\Tests\DummyEnum::CaseName');
Assert::null(DummyEnum::CaseName, 'Expected null. Got: %s');
}
public function testRegexRejectsAnUncompilablePattern(): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('The pattern "/(/" could not be evaluated: Internal error.');
Assert::regex('abc', '/(/');
}
public function testRegexSuppressesTheWarningForAnUncompilablePattern(): void
{
$reported = [];
// PHPUnit masks E_WARNING out of error_reporting(), so restore it for this test.
$errorReporting = error_reporting(E_ALL);
set_error_handler(static function (int $errno, string $message) use (&$reported): bool {
// This is what a well behaved error handler does with a suppressed diagnostic.
if (0 !== (error_reporting() & $errno)) {
$reported[] = $message;
}
return true;
});
try {
Assert::regex('abc', '/(/');
} catch (InvalidArgumentException) {
// The assertion is expected to fail; this test only inspects the reported errors.
} finally {
restore_error_handler();
error_reporting($errorReporting);
}
$this->assertSame([], $reported);
}
public function testRegexDoesNotUseTheCustomMessageForAnUncompilablePattern(): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('The pattern "/(/" could not be evaluated: Internal error.');
Assert::regex('abc', '/(/', 'The value %s is not a valid slug.');
}
public function testRegexRejectsAPatternExceedingTheBacktrackLimit(): void
{
$backtrackLimit = ini_get('pcre.backtrack_limit');
ini_set('pcre.backtrack_limit', '100');
try {
Assert::regex(str_repeat('a', 30).'c', '/^(a+)+$/');
$this->fail('Expected an InvalidArgumentException to be thrown.');
} catch (InvalidArgumentException $e) {
$this->assertSame(
'The pattern "/^(a+)+$/" could not be evaluated: Backtrack limit exhausted.',
$e->getMessage()
);
} finally {
ini_set('pcre.backtrack_limit', $backtrackLimit);
}
}
#[DataProvider('getMethodsThatUseOtherMethods')]
public function testMessageIsPassedToInternalCalls(string $method, array $args, string $exceptionMessage): void
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage($exceptionMessage);
call_user_func_array(['Webmozart\Assert\Assert', $method], $args);
}
public static function getMethodsThatUseOtherMethods(): array
{
return [
[
'method' => 'positiveInteger',
'args' => ['not-integer', 'Value must be a positive integer. Got: %s'],
'exceptionMessage' => 'Value must be a positive integer. Got: string',
],
[
'method' => 'notNegativeInteger',
'args' => ['not-integer', 'Value must be a non-negative integer. Got: %s'],
'exceptionMessage' => 'Value must be a non-negative integer. Got: string',
],
[
'method' => 'negativeInteger',
'args' => ['not-integer', 'Value must be a negative integer. Got: %s'],
'exceptionMessage' => 'Value must be a negative integer. Got: string',
],
[
'method' => 'ip',
'args' => [127001, 'Value must be a valid IP. Got: %s'],
'exceptionMessage' => 'Value must be a valid IP. Got: integer',
],
[
'method' => 'ipv4',
'args' => [127001, 'Value must be a valid IPv4. Got: %s'],
'exceptionMessage' => 'Value must be a valid IPv4. Got: integer',
],
[
'method' => 'ipv6',
'args' => [127001, 'Value must be a valid IPv6. Got: %s'],
'exceptionMessage' => 'Value must be a valid IPv6. Got: integer',
],
[
'method' => 'email',
'args' => [111111, 'Value must be a valid email. Got: %s'],
'exceptionMessage' => 'Value must be a valid email. Got: integer',
],
[
'method' => 'unicodeLetters',
'args' => [111, 'Value must be a string with valid unicode characters. Got: %s'],
'exceptionMessage' => 'Value must be a string with valid unicode characters. Got: integer',
],
[
'method' => 'alpha',
'args' => [111, 'Value must be a string with only alphabetic characters. Got: %s'],
'exceptionMessage' => 'Value must be a string with only alphabetic characters. Got: integer',
],
[
'method' => 'digits',
'args' => [111, 'Value must be a string with only digits. Got: %s'],
'exceptionMessage' => 'Value must be a string with only digits. Got: integer',
],
[
'method' => 'alnum',
'args' => [111, 'Value must be a string with only alphanumeric characters. Got: %s'],
'exceptionMessage' => 'Value must be a string with only alphanumeric characters. Got: integer',
],
[
'method' => 'lower',
'args' => [111, 'Value must be a string with only lowercase characters. Got: %s'],
'exceptionMessage' => 'Value must be a string with only lowercase characters. Got: integer',
],
[
'method' => 'upper',
'args' => [111, 'Value must be a string with only uppercase characters. Got: %s'],
'exceptionMessage' => 'Value must be a string with only uppercase characters. Got: integer',
],
[
'method' => 'keyExists',
'args' => [111, 'test', 'Value must be an array with key test. Got: %s'],
'exceptionMessage' => 'Value must be an array with key test. Got: integer',
],
[
'method' => 'keyNotExists',
'args' => [111, 'test', 'Value must be an array without key test. Got: %s'],
'exceptionMessage' => 'Value must be an array without key test. Got: integer',
],
[
'method' => 'isInstanceOf',
'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'],
'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer',
],
[
'method' => 'notInstanceOf',
'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'],
'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer',
],
[
'method' => 'isInstanceOfAny',
'args' => [111, ['stdClass'], 'Value must be an instance of stdClass. Got: %s'],
'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer',
],
[
'method' => 'isNotA',
'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'],
'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer',
],
[
'method' => 'isAnyOf',
'args' => [111, ['stdClass'], 'Value must be an instance of stdClass. Got: %s'],
'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer',
],
];
}
}
/**
* @ignore
*/
class ToStringClass
{
/**
* @var string
*/
private $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}