├── test ├── Fixtures │ ├── LoadingFiles │ │ ├── file1 │ │ ├── file2 │ │ ├── file3 │ │ └── IgnoreMe │ │ │ ├── file1 │ │ │ ├── file2 │ │ │ └── file3 │ ├── TestException.php │ ├── ValidSuites │ │ ├── AsyncData.php │ │ ├── Setup.php │ │ ├── TearDown.php │ │ ├── ExternalTest.php │ │ ├── Test.php │ │ ├── Factories.php │ │ └── Data.php │ └── InvalidSuites │ │ ├── Test.php │ │ ├── SuiteUpDown.php │ │ ├── TestUpDown.php │ │ ├── Factory.php │ │ └── Data.php ├── self-test.ini ├── Doubles │ ├── Util │ │ └── Assertion.php │ ├── AsyncSuite.php │ └── SpySuite.php ├── Assertion │ ├── AssertionTest.php │ ├── BoolAssertion.php │ ├── StringAssertion.php │ ├── MixedAssertion.php │ ├── NumericAssertion.php │ ├── CallableAssertion.php │ └── ContainerAssertion.php ├── Test │ ├── Async.php │ ├── ParserTest.php │ ├── Loader.php │ ├── InvalidParsing.php │ ├── ValidParsing.php │ ├── Runner.php │ └── Suite.php ├── Assert.php ├── TraceItemTest.php ├── Report │ ├── Status.php │ └── SummaryBuilder.php ├── self-test.php └── Util │ └── TraceTest.php ├── .hhconfig ├── .gitignore ├── src ├── Event │ ├── Interruption.php │ ├── SuiteStart.php │ ├── TestStart.php │ ├── BuildFailure.php │ ├── Pass.php │ ├── MalformedSuite.php │ ├── FailureEmitter.php │ ├── Success.php │ ├── SuccessEmitter.php │ ├── Listeners.php │ ├── Skip.php │ └── Failure.php ├── Exception │ └── InterruptTest.php ├── Report │ ├── Format.php │ ├── Status.php │ ├── Format │ │ └── Cli.php │ └── SummaryBuilder.php ├── Contract │ ├── Assertion │ │ ├── BoolAssertion.php │ │ ├── Assertion.php │ │ ├── KeyedContainerAssertion.php │ │ ├── StringAssertion.php │ │ ├── NumericAssertion.php │ │ ├── MixedAssertion.php │ │ ├── CallableAssertion.php │ │ └── ContainerAssertion.php │ ├── Test │ │ ├── Loader.php │ │ ├── Suite.php │ │ ├── Runner.php │ │ └── Parser.php │ └── Assert.php ├── Test │ ├── SkippedSuite.php │ ├── Suite.php │ ├── Loader.php │ ├── Runner.php │ └── SuiteBuilder.php ├── Assertion │ ├── BoolAssertion.php │ ├── NumericAssertion.php │ ├── MixedAssertion.php │ ├── StringAssertion.php │ ├── CallableAssertion.php │ ├── ContainerAssertion.php │ └── KeyedContainerAssertion.php ├── Util │ ├── Builder.php │ ├── Options.php │ └── Trace.php ├── HackUnit.php └── Assert.php ├── docker-test.sh ├── circle.yml ├── bin └── hackunit ├── LICENSE └── composer.json /test/Fixtures/LoadingFiles/file1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Fixtures/LoadingFiles/file2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Fixtures/LoadingFiles/file3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Fixtures/LoadingFiles/IgnoreMe/file1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Fixtures/LoadingFiles/IgnoreMe/file2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Fixtures/LoadingFiles/IgnoreMe/file3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/self-test.ini: -------------------------------------------------------------------------------- 1 | hhvm.hack.lang.look_for_typechecker = false 2 | hhvm.jit = false 3 | -------------------------------------------------------------------------------- /.hhconfig: -------------------------------------------------------------------------------- 1 | assume_php = false 2 | user_attributes = Test Setup SuiteProvider TearDown Skip Data DataProvider 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .idea 3 | vendor/ 4 | Vagrantfile 5 | provisioning/ 6 | .vagrant/ 7 | hhi/ 8 | composer.lock 9 | -------------------------------------------------------------------------------- /src/Event/Interruption.php: -------------------------------------------------------------------------------- 1 | suiteName; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/Doubles/Util/Assertion.php: -------------------------------------------------------------------------------- 1 | ; 11 | } 12 | -------------------------------------------------------------------------------- /src/Contract/Assertion/Assertion.php: -------------------------------------------------------------------------------- 1 | suiteName; 13 | } 14 | 15 | public function testName(): string { 16 | return $this->testName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Event/BuildFailure.php: -------------------------------------------------------------------------------- 1 | path; 13 | } 14 | 15 | public function exception(): \Exception { 16 | return $this->exception; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/AsyncData.php: -------------------------------------------------------------------------------- 1 | > 10 | public static async function asyncDataProvider(): AsyncIterator { 11 | yield 1; 12 | } 13 | 14 | <> 15 | public function asyncConsumer(Assert $assert, int $value): void {} 16 | } 17 | -------------------------------------------------------------------------------- /src/Contract/Assertion/KeyedContainerAssertion.php: -------------------------------------------------------------------------------- 1 | extends Assertion { 6 | public function containsKey(Tkey $expected): void; 7 | public function contains(Tkey $key, Tval $val): void; 8 | public function containsAll(KeyedContainer $expected): void; 9 | public function containsAny(KeyedContainer $expected): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/Contract/Assertion/StringAssertion.php: -------------------------------------------------------------------------------- 1 | extends Assertion { 6 | public function not(): this; 7 | public function eq(Tcontext $expected): void; 8 | public function gt(Tcontext $expected): void; 9 | public function gte(Tcontext $expected): void; 10 | public function lt(Tcontext $expected): void; 11 | public function lte(Tcontext $expected): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/Event/Pass.php: -------------------------------------------------------------------------------- 1 | testCallSite; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Event/MalformedSuite.php: -------------------------------------------------------------------------------- 1 | reason; 16 | } 17 | 18 | public function traceItem(): TraceItem { 19 | return $this->item; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | 5 | dependencies: 6 | override: 7 | - docker info 8 | - docker pull hhvm/hhvm:3.15-lts-latest 9 | - docker pull kilahm/composer-hhvm:3.15.1 10 | - docker run --rm -v "$(pwd)":"$(pwd)" --workdir="$(pwd)" kilahm/composer-hhvm:3.15.1 install 11 | 12 | test: 13 | override: 14 | - docker run -v "$(pwd)":"$(pwd)" --workdir="$(pwd)" -e "CIRCLE_TEST_REPORTS=$CIRCLE_TEST_REPORTS" hhvm/hhvm:3.15-lts-latest hhvm -c test/self-test.ini test/self-test.php 15 | -------------------------------------------------------------------------------- /src/Contract/Test/Suite.php: -------------------------------------------------------------------------------- 1 | ; 10 | public function run( 11 | Assert $assert, 12 | (function(): void) $testPassed, 13 | \ConstVector $testStartListeners, 14 | ): Awaitable; 15 | public function down(): Awaitable; 16 | public function name(): string; 17 | } 18 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/Setup.php: -------------------------------------------------------------------------------- 1 | > 7 | public static function suiteOnly(): void {} 8 | 9 | <> 10 | public static function nonRequiredParam(int $param = 0): void {} 11 | 12 | <> 13 | public static function both(): void {} 14 | 15 | <> 16 | public function testOnlyExplicit(): void {} 17 | 18 | <> 19 | public function testOnlyImplicit(): void {} 20 | } 21 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/TearDown.php: -------------------------------------------------------------------------------- 1 | > 7 | public static function suiteOnly(): void {} 8 | 9 | <> 10 | public static function nonRequiredParam(int $param = 0): void {} 11 | 12 | <> 13 | public static function both(): void {} 14 | 15 | <> 16 | public function testOnlyExplicit(): void {} 17 | 18 | <> 19 | public function testOnlyImplicit(): void {} 20 | } 21 | -------------------------------------------------------------------------------- /test/Fixtures/InvalidSuites/Test.php: -------------------------------------------------------------------------------- 1 | > 9 | public function __construct(Assert $assert) {} 10 | 11 | <> 12 | public function __destruct() {} 13 | } 14 | 15 | class TestParams { 16 | <> 17 | public function noParams(): void {} 18 | 19 | <> 20 | public function wrongParam(int $wrong): void {} 21 | 22 | <> 23 | public function tooManyParams(Assert $assert, int $wrong): void {} 24 | } 25 | -------------------------------------------------------------------------------- /test/Fixtures/InvalidSuites/SuiteUpDown.php: -------------------------------------------------------------------------------- 1 | > 7 | public static function invalid(int $requiredParam): void {} 8 | } 9 | 10 | class SuiteUpNonStatic { 11 | <> 12 | public function invalid(): void {} 13 | } 14 | 15 | class SuiteDownParams { 16 | <> 17 | public static function invalid(int $requiredParam): void {} 18 | } 19 | 20 | class SuiteDownNonStatic { 21 | <> 22 | public function invalid(): void {} 23 | } 24 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/ExternalTest.php: -------------------------------------------------------------------------------- 1 | > 13 | public function testInsideTrait(Assert $assert): void {} 14 | } 15 | 16 | abstract class AbstractSuite { 17 | <> 18 | public function testInsideAbstractSuite(Assert $assert): void {} 19 | } 20 | 21 | class BaseSuite extends AbstractSuite { 22 | <> 23 | public function testInsideBaseSuite(Assert $assert): void {} 24 | } 25 | -------------------------------------------------------------------------------- /src/Contract/Assertion/MixedAssertion.php: -------------------------------------------------------------------------------- 1 | > 9 | public static function namedBuilder(): this { 10 | return new static(); 11 | } 12 | 13 | <> 14 | public function defaultSuiteProvider(Assert $assert): void {} 15 | 16 | <> 17 | public function namedSuiteProvider(Assert $assert): void {} 18 | 19 | <> 20 | public static function staticTest(Assert $assert): void {} 21 | 22 | <> 23 | public static function skippedTest(Assert $assert): void {} 24 | } 25 | -------------------------------------------------------------------------------- /test/Fixtures/InvalidSuites/TestUpDown.php: -------------------------------------------------------------------------------- 1 | > 7 | public function invalid(int $requiredParam): void {} 8 | } 9 | 10 | class TestUpConstructDestruct { 11 | <> 12 | public function __construct() {} 13 | 14 | <> 15 | public function __destruct() {} 16 | } 17 | 18 | class TestDownParams { 19 | <> 20 | public function invalid(int $requiredParam): void {} 21 | } 22 | 23 | class TestDownConstructDestruct { 24 | <> 25 | public function __construct() {} 26 | 27 | <> 28 | public function __destruct() {} 29 | } 30 | -------------------------------------------------------------------------------- /src/Event/FailureEmitter.php: -------------------------------------------------------------------------------- 1 | $failureListeners = Vector {}; 7 | 8 | public function onFailure(FailureListener $l): this { 9 | $this->failureListeners->add($l); 10 | return $this; 11 | } 12 | 13 | public function setFailureListeners( 14 | Traversable $listeners, 15 | ): this { 16 | $this->failureListeners->clear()->addAll($listeners); 17 | return $this; 18 | } 19 | 20 | private function emitFailure(Failure $event): void { 21 | foreach ($this->failureListeners as $l) { 22 | $l($event); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contract/Assertion/CallableAssertion.php: -------------------------------------------------------------------------------- 1 | assertionCallSite; 19 | } 20 | 21 | public function testMethodTraceItem(): TraceItem { 22 | return $this->testCallSite; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Event/SuccessEmitter.php: -------------------------------------------------------------------------------- 1 | $successListeners = Vector {}; 7 | 8 | public function onSuccess(SuccessListener $l): this { 9 | $this->successListeners->add($l); 10 | return $this; 11 | } 12 | 13 | public function setSuccessListeners( 14 | Traversable $listeners, 15 | ): this { 16 | $this->successListeners->clear()->addAll(new Vector($listeners)); 17 | return $this; 18 | } 19 | 20 | private function emitSuccess(): void { 21 | $e = Success::fromCallStack(); 22 | foreach ($this->successListeners as $l) { 23 | $l($e); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Event/Listeners.php: -------------------------------------------------------------------------------- 1 | $failEvents = Vector {}; 13 | 14 | private function successListeners(): Vector { 15 | return Vector { 16 | ($e) ==> { 17 | $this->successCount++; 18 | }, 19 | }; 20 | } 21 | 22 | private function failListeners(): Vector { 23 | return Vector { 24 | ($e) ==> { 25 | $this->failEvents->add($e); 26 | }, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/Doubles/AsyncSuite.php: -------------------------------------------------------------------------------- 1 | {} 13 | public async function down(): Awaitable {} 14 | 15 | public async function run( 16 | Assert $assert, 17 | (function(): void) $testPassed, 18 | \ConstVector $testStartListeners, 19 | ): Awaitable { 20 | await \HH\Asio\usleep($this->sleepTime); 21 | } 22 | 23 | public function name(): string { 24 | return 'Async test suite'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Contract/Assertion/ContainerAssertion.php: -------------------------------------------------------------------------------- 1 | extends Assertion { 6 | 7 | public function contains( 8 | Tval $expected, 9 | ?(function(Tval, Tval): bool) $comparitor = null, 10 | ): void; 11 | 12 | public function containsAll( 13 | Container $expected, 14 | ?(function(Tval, Tval): bool) $comparitor = null, 15 | ): void; 16 | 17 | public function containsAny( 18 | Container $expected, 19 | ?(function(Tval, Tval): bool) $comparitor = null, 20 | ): void; 21 | 22 | public function containsOnly( 23 | Container $expected, 24 | ?(function(Tval, Tval): bool) $comparitor = null, 25 | ): void; 26 | 27 | public function isEmpty(): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/Event/Skip.php: -------------------------------------------------------------------------------- 1 | reason; 22 | } 23 | 24 | public function skipCallSite(): TraceItem { 25 | return $this->callSite; 26 | } 27 | 28 | public function testMethodTraceItem(): TraceItem { 29 | return $this->testSite; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Test/SkippedSuite.php: -------------------------------------------------------------------------------- 1 | {} 13 | 14 | public async function down(): Awaitable {} 15 | 16 | public async function run( 17 | Assert $assert, 18 | (function(): void) $testPassed, 19 | \ConstVector $testStartListeners, 20 | ): Awaitable { 21 | $assert->skip('Class '.$this->name.' marked "Skipped"', $this->trace); 22 | } 23 | 24 | public function name(): string { 25 | return $this->name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/Failure.php: -------------------------------------------------------------------------------- 1 | assertionCallSite; 25 | } 26 | 27 | public function testMethodTraceItem(): TraceItem { 28 | return $this->testCallSite; 29 | } 30 | 31 | public function getMessage(): string { 32 | return $this->message; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Contract/Assert.php: -------------------------------------------------------------------------------- 1 | ; 10 | public function float(float $context): Assertion\NumericAssertion; 11 | public function string(string $context): Assertion\StringAssertion; 12 | public function whenCalled( 13 | (function(): void) $context, 14 | ): Assertion\CallableAssertion; 15 | public function mixed(mixed $context): Assertion\MixedAssertion; 16 | public function container( 17 | Container $context, 18 | ): Assertion\ContainerAssertion; 19 | public function keyedContainer( 20 | KeyedContainer $context, 21 | ): Assertion\KeyedContainerAssertion; 22 | public function skip(string $reason, ?TraceItem $traceItem = null): void; 23 | } 24 | -------------------------------------------------------------------------------- /bin/hackunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | build()->run(); 35 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/Factories.php: -------------------------------------------------------------------------------- 1 | > 9 | public static function factory(): this { 10 | return new static(); 11 | } 12 | } 13 | 14 | final class ConstructorIsDefaultWithParams { 15 | public function __construct(string $notRequired = '') {} 16 | 17 | <> 18 | public static function factory(): this { 19 | return new static(); 20 | } 21 | } 22 | 23 | final class ConstructorIsNotDefault { 24 | public function __construct() {} 25 | 26 | <> 27 | public static function factory(): this { 28 | return new static(); 29 | } 30 | } 31 | 32 | <<__ConsistentConstruct>> 33 | abstract class AbstractFactory { 34 | <> 35 | public static function factory(): DerivedFactory { 36 | return new DerivedFactory(); 37 | } 38 | } 39 | 40 | class DerivedFactory extends AbstractFactory {} 41 | -------------------------------------------------------------------------------- /test/Fixtures/InvalidSuites/Factory.php: -------------------------------------------------------------------------------- 1 | > 7 | public static function one(): this { 8 | return new static(); 9 | } 10 | 11 | <> 12 | public static function two(): this { 13 | return new static(); 14 | } 15 | } 16 | 17 | final class FactoryParams { 18 | <> 19 | public static function factory(int $required): this { 20 | return new static(); 21 | } 22 | } 23 | 24 | final class NonStaticFactory { 25 | <> 26 | public function factory(): this { 27 | return new static(); 28 | } 29 | } 30 | 31 | final class FactoryReturnType { 32 | <> 33 | public static function factory(): int { 34 | return 0; 35 | } 36 | } 37 | 38 | abstract class AbstractFactory { 39 | <> 40 | public static function factory(): AbstractFactory { 41 | return new InvalidDerivedFactory(); 42 | } 43 | } 44 | 45 | class InvalidDerivedFactory extends AbstractFactory {} 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Brian Scaturro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/Test/Async.php: -------------------------------------------------------------------------------- 1 | $finished = Set {}; 9 | 10 | <> 11 | public async function testOne(Assert $assert): Awaitable { 12 | $assert->container(self::$finished)->isEmpty(); 13 | await \HH\Asio\later(); 14 | self::$finished->add('testOne'); 15 | } 16 | 17 | <> 18 | public async function testTwo(Assert $assert): Awaitable { 19 | $assert->container(self::$finished)->isEmpty(); 20 | await \HH\Asio\later(); 21 | self::$finished->add('testTwo'); 22 | } 23 | 24 | <> 25 | public static async function threeValues(): AsyncIterator { 26 | yield '1'; 27 | yield '2'; 28 | yield '3'; 29 | } 30 | 31 | <> 32 | public async function dataConsumer( 33 | Assert $assert, 34 | string $value, 35 | ): Awaitable { 36 | $assert->container(self::$finished)->isEmpty(); 37 | await \HH\Asio\later(); 38 | self::$finished->add($value); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackpack/hackunit", 3 | "description": "An xUnit testing framework for Hack", 4 | "type": "library", 5 | "keywords": [ 6 | "testing", 7 | "xunit", 8 | "hack", 9 | "hhvm", 10 | "hacklang" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Brian Scaturro", 16 | "email": "scaturrob@gmail.com", 17 | "role": "Creator" 18 | }, 19 | { 20 | "name": "Isaac Leinweber", 21 | "email": "brotchen@gmail.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "require": { 27 | "hhvm": "^3.11", 28 | "facebook/definition-finder": "^1.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "HackPack\\HackUnit\\": "src/" 33 | }, 34 | "files": [ 35 | "src/Event/Listeners.php" 36 | ] 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "HackPack\\HackUnit\\Tests\\": "test" 41 | } 42 | }, 43 | "bin": [ 44 | "bin/hackunit" 45 | ], 46 | "scripts": { 47 | "test": [ 48 | "hhvm test/self-test.php" 49 | ], 50 | "format": "hh_format src; hh_format test;" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Assert.php: -------------------------------------------------------------------------------- 1 | $skips = Vector {}; 13 | 14 | <> 15 | public function clearEvents(): void { 16 | $this->skips->clear(); 17 | } 18 | 19 | <> 20 | public function skipIdentifiesCaller(iAssert $assert): void { 21 | $sut = new Assert( 22 | Vector {}, 23 | Vector { 24 | $skip ==> { 25 | $this->skips->add($skip); 26 | }, 27 | }, 28 | Vector {}, 29 | ); 30 | $line = __LINE__ + 1; 31 | $sut->skip('testing'); 32 | 33 | $assert->int($this->skips->count())->eq(1); 34 | $skip = $this->skips->at(0); 35 | 36 | $this->checkTrace( 37 | $skip->skipCallSite(), 38 | shape( 39 | 'line' => $line, 40 | 'function' => __FUNCTION__, 41 | 'class' => __CLASS__, 42 | 'file' => __FILE__, 43 | ), 44 | $assert, 45 | ); 46 | 47 | $assert->string($skip->message())->is('testing'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Test/ParserTest.php: -------------------------------------------------------------------------------- 1 | $parsersBySuiteName = Map {}; 15 | 16 | <> 17 | public static function buildParsers(): void { 18 | self::$parsersBySuiteName->clear()->addAll( 19 | TreeParser::FromPath(static::basePath()) 20 | ->getClasses() 21 | ->map( 22 | $class ==> Pair { 23 | $class->getName(), 24 | new Parser($class->getName(), $class->getFileName()), 25 | }, 26 | ), 27 | ); 28 | } 29 | 30 | protected function parserFromSuiteName(string $name): Parser { 31 | $parser = self::$parsersBySuiteName->get(static::fullName($name)); 32 | 33 | if ($parser === null) { 34 | throw new \RuntimeException('Unable to locate suite '.$name); 35 | } 36 | return $parser; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Contract/Test/Runner.php: -------------------------------------------------------------------------------- 1 | $suites): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Assertion/BoolAssertion.php: -------------------------------------------------------------------------------- 1 | $failureListeners, 20 | Vector $successListeners, 21 | ) { 22 | $this->setFailureListeners($failureListeners); 23 | $this->setSuccessListeners($successListeners); 24 | } 25 | 26 | public function is(bool $expected): void { 27 | if ($this->context === $expected) { 28 | $this->emitSuccess(); 29 | return; 30 | } 31 | $this->emitFailure( 32 | Failure::fromCallStack( 33 | 'Expected '. 34 | ($expected ? 'true' : 'false'). 35 | ', value was '. 36 | ($expected ? 'false' : 'true'). 37 | '.', 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/Fixtures/ValidSuites/Data.php: -------------------------------------------------------------------------------- 1 | > 9 | public static function consumesVector( 10 | Assert $assert, 11 | Vector $data, 12 | ): void {} 13 | 14 | <> 15 | public static function consumesMap( 16 | Assert $assert, 17 | Map $data, 18 | ): void {} 19 | 20 | <> 21 | public static function consumesString(Assert $assert, string $data): void {} 22 | 23 | <> 24 | public static function notRecognized(Assert $assert): void {} 25 | 26 | <> 27 | public static function vectorProvider(): Traversable> { 28 | return Vector {}; 29 | } 30 | 31 | <> 32 | public static function mapProvider(): Traversable> { 33 | return Map {}; 34 | } 35 | 36 | <> 37 | public static function stringProvider(): Traversable { 38 | return Map {}; 39 | } 40 | 41 | <> 42 | public static function dataProviderWithParam(int $i = 1): Traversable { 43 | return []; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Report/Status.php: -------------------------------------------------------------------------------- 1 | out, 21 | sprintf( 22 | "\nHackUnit by HackPack version %s\nHHVM version %s\n", 23 | Options::VERSION, 24 | $hhvmVersion, 25 | ), 26 | ); 27 | } 28 | 29 | public function handlePass(Pass $event): void { 30 | fwrite($this->out, '.'); 31 | } 32 | 33 | public function handleFailure(Failure $event): void { 34 | fwrite($this->out, 'F'); 35 | } 36 | 37 | public function handleSkip(Skip $event): void { 38 | fwrite($this->out, 'S'); 39 | } 40 | 41 | public function handleBuildFailure(BuildFailure $event): void { 42 | sprintf( 43 | "\n~*~*~*~ Build Failure ~*~*~*~\nFile: %s\nMessage: %s\n", 44 | $event->filePath(), 45 | $event->exception()->getMessage(), 46 | ) 47 | |> fwrite($this->out, $$); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/TraceItemTest.php: -------------------------------------------------------------------------------- 1 | mixed($actual['line'])->isInt(); 21 | $assert->int((int) $actual['line'])->eq($line); 22 | } else { 23 | $assert->mixed($actual['line'])->isNull(); 24 | } 25 | 26 | if (is_string($method)) { 27 | $assert->mixed($actual['function'])->isString(); 28 | $assert->string((string) $actual['function'])->is($method); 29 | } else { 30 | $assert->mixed($actual['function'])->isNull(); 31 | } 32 | 33 | if (is_string($class)) { 34 | $assert->mixed($actual['class'])->isString(); 35 | $assert->string((string) $actual['class'])->is($class); 36 | } else { 37 | $assert->mixed($actual['class'])->isNull(); 38 | } 39 | 40 | if (is_string($file)) { 41 | $assert->mixed($actual['file'])->isString(); 42 | $assert->string((string) $actual['file'])->is($file); 43 | } else { 44 | $assert->mixed($actual['file'])->isNull(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Report/Status.php: -------------------------------------------------------------------------------- 1 | > 15 | public static function provider(): this { 16 | $out = fopen('php://memory', 'w+'); 17 | return new static($out, new Status($out)); 18 | } 19 | 20 | public function __destruct() { 21 | fclose($this->out); 22 | } 23 | 24 | <> 25 | public function passShowsDot(Assert $assert): void { 26 | $this->status->handlePass(Pass::fromCallStack()); 27 | $this->assertOutput($assert, '.'); 28 | } 29 | 30 | <> 31 | public function failureShowsF(Assert $assert): void { 32 | $this->status->handleFailure(Failure::fromCallStack('testing')); 33 | $this->assertOutput($assert, 'F'); 34 | } 35 | 36 | <> 37 | public function skipShowsS(Assert $assert): void { 38 | $this->status->handleSkip(Skip::fromCallStack('testing')); 39 | $this->assertOutput($assert, 'S'); 40 | } 41 | 42 | private function assertOutput(Assert $assert, string $expected): void { 43 | rewind($this->out); 44 | $actual = stream_get_contents($this->out); 45 | $assert->string($actual)->is($expected); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Doubles/SpySuite.php: -------------------------------------------------------------------------------- 1 | int, 11 | 'down' => int, 12 | 'run' => int, 13 | ); 14 | 15 | class SpySuite implements Suite { 16 | 17 | public RunCounts $counts = shape('up' => 0, 'down' => 0, 'run' => 0); 18 | public Vector $asserts = Vector {}; 19 | public Vector<(function(): void)> $passCallbacks = Vector {}; 20 | 21 | private (function(): void) $runAction; 22 | 23 | public function __construct(?(function(): void) $runAction = null) { 24 | $this->runAction = 25 | $runAction === null 26 | ? () ==> { 27 | } 28 | : $runAction; 29 | } 30 | 31 | public function name(): string { 32 | return 'Spy Suite'; 33 | } 34 | 35 | public async function up(): Awaitable { 36 | $this->counts['up']++; 37 | } 38 | 39 | public async function down(): Awaitable { 40 | $this->counts['down']++; 41 | } 42 | 43 | public async function run( 44 | Assert $assert, 45 | (function(): void) $testPassed, 46 | \ConstVector $testStartListeners, 47 | ): Awaitable { 48 | $this->asserts->add($assert); 49 | $this->passCallbacks->add($testPassed); 50 | $this->counts['run']++; 51 | $action = $this->runAction; 52 | $action(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /test/self-test.php: -------------------------------------------------------------------------------- 1 | add(JUnitFormat::build($circleReportDir.'/report.xml')); 24 | // } 25 | $status = new Status(STDOUT); 26 | 27 | $suiteBuilder = new SuiteBuilder( 28 | ($className, $fileName) ==> new Parser($className, $fileName), 29 | ); 30 | 31 | $include = Set {$root.'/test'}; 32 | $exclude = Set { 33 | $root.'/test/self-test.php', 34 | $root.'/test/self-test.ini', 35 | $root.'/test/Fixtures', 36 | $root.'/test/Doubles', 37 | }; 38 | $loader = new Loader( 39 | $class ==> $suiteBuilder->buildSuites($class), 40 | $include, 41 | $exclude, 42 | ); 43 | 44 | $runner = new Runner(class_meth(Assert::class, 'build')); 45 | 46 | $hackunit = 47 | new HackUnit($reportFormatters, $status, $suiteBuilder, $loader, $runner); 48 | $hackunit->run(); 49 | -------------------------------------------------------------------------------- /src/Contract/Test/Parser.php: -------------------------------------------------------------------------------- 1 | ; 12 | 13 | /** 14 | * Return a list of public static methods to run before the suite starts 15 | */ 16 | public function suiteUp(): \ConstVector; 17 | 18 | /** 19 | * Return a list of public static methods to run after the suite ends 20 | */ 21 | public function suiteDown(): \ConstVector; 22 | 23 | /** 24 | * Return a list of public methods (static or not) to run just before each test 25 | */ 26 | public function testUp(): \ConstVector; 27 | 28 | /** 29 | * Return a list of public methods (static or not) to run just after each test 30 | */ 31 | public function testDown(): \ConstVector; 32 | 33 | /** 34 | * Return enough data to define a test method 35 | * 36 | * The factory name SHOULD be contained in the list of factories returned above, 37 | * but this is not required. 38 | */ 39 | public function tests( 40 | ): \ConstVector string, 42 | 'method' => string, 43 | 'skip' => bool, 44 | 'data provider' => string, 45 | )>; 46 | 47 | /** 48 | * List of malformed suite events to broadcast 49 | */ 50 | public function errors(): \ConstVector; 51 | } 52 | -------------------------------------------------------------------------------- /src/Util/Builder.php: -------------------------------------------------------------------------------- 1 | $args): this { 17 | return new self(Options::fromCli($args)); 18 | } 19 | 20 | public function __construct(private Options $options) {} 21 | 22 | public function build(): HackUnit { 23 | $suiteBuilder = new SuiteBuilder( 24 | ($className, $fileName) ==> new Parser($className, $fileName), 25 | ); 26 | 27 | $loader = new Loader( 28 | $class ==> $suiteBuilder->buildSuites($class), 29 | $this->options->includes->toSet(), 30 | $this->options->excludes->toSet(), 31 | ); 32 | 33 | $runner = new Runner(class_meth(Assert::class, 'build')); 34 | 35 | return new HackUnit( 36 | $this->buildReportFormatters(), 37 | $this->buildStatus(), 38 | $suiteBuilder, 39 | $loader, 40 | $runner, 41 | ); 42 | } 43 | 44 | private function buildReportFormatters(): Vector { 45 | return Vector {new Cli(STDOUT)}; 46 | } 47 | 48 | private function buildStatus(): Status { 49 | return new Status(STDOUT); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/Assertion/BoolAssertion.php: -------------------------------------------------------------------------------- 1 | failListeners(), 17 | $this->successListeners(), 18 | ); 19 | } 20 | 21 | <> 22 | public function triggersSuccessWhenBoolsMatch(Assert $assert): void { 23 | $a = $this->makeAssertion(true); 24 | $a->is(true); 25 | $assert->int($this->successCount)->eq(1); 26 | $assert->int($this->failEvents->count())->eq(0); 27 | 28 | $a = $this->makeAssertion(false); 29 | $a->is(false); 30 | $assert->int($this->successCount)->eq(2); 31 | $assert->int($this->failEvents->count())->eq(0); 32 | } 33 | 34 | <> 35 | public function triggersFailureWhenBoolDoesNotMatch(Assert $assert): void { 36 | $a = $this->makeAssertion(false); 37 | $line = __LINE__ + 1; 38 | $a->is(true); 39 | 40 | $assert->int($this->successCount)->eq(0); 41 | $assert->int($this->failEvents->count())->eq(1); 42 | $e = $this->failEvents->at(0); 43 | $this->checkTrace( 44 | $e->assertionTraceItem(), 45 | shape( 46 | 'line' => $line, 47 | 'function' => __FUNCTION__, 48 | 'class' => __CLASS__, 49 | 'file' => __FILE__, 50 | ), 51 | $assert, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/Fixtures/InvalidSuites/Data.php: -------------------------------------------------------------------------------- 1 | > 9 | public static function provider(): Traversable { 10 | return []; 11 | } 12 | } 13 | 14 | class DataProviderWithParams { 15 | <> 16 | public static function provider(int $i): Traversable { 17 | return []; 18 | } 19 | } 20 | 21 | class InstanceDataProvider { 22 | <> 23 | public function provider(int $i): Traversable { 24 | return []; 25 | } 26 | } 27 | 28 | class NonTraversableDataProvider { 29 | <> 30 | public static function provider(): void {} 31 | 32 | <> 33 | public static function intProvider(): int { 34 | return 1; 35 | } 36 | 37 | <> 38 | public static function vectorProvider(): Vector { 39 | return Vector {}; 40 | } 41 | } 42 | 43 | class DataConsumerMismatchParams { 44 | <> 45 | public static function provider(): Traversable { 46 | return []; 47 | } 48 | 49 | <> 50 | public function consumer(Assert $assert, string $value): void {} 51 | } 52 | 53 | class DataConsumerMismatchGeneric { 54 | <> 55 | public static function mapProvider(): Traversable> { 56 | return Map {}; 57 | } 58 | 59 | <> 60 | public static function consumesMap( 61 | Assert $assert, 62 | Map $data, 63 | ): void {} 64 | 65 | <> 66 | public static function consumesMapAgain( 67 | Assert $assert, 68 | Map $data, 69 | ): void {} 70 | } 71 | 72 | class DataConsumerMissingName { 73 | <> 74 | public function consumer(Assert $assert): void {} 75 | } 76 | -------------------------------------------------------------------------------- /src/Util/Options.php: -------------------------------------------------------------------------------- 1 | $includes, 10 | public \ConstSet $excludes, 11 | ) {} 12 | 13 | public static function fromCli(Traversable $args): Options { 14 | $includes = []; 15 | $excludes = []; 16 | 17 | $arglist = new Vector($args); 18 | $arglist->reverse(); 19 | 20 | // first arg is always path to executable 21 | $arglist->pop(); 22 | 23 | $addPathToArray = ($path, $array) ==> { 24 | $realpath = realpath($path); 25 | if (is_string($realpath)) { 26 | $array[] = $realpath; 27 | } 28 | return $array; 29 | }; 30 | 31 | while ($arglist) { 32 | $arg = $arglist->pop(); 33 | if (substr($arg, 0, 2) === '--') { 34 | $path = self::handleLongOption(substr($arg, 2), $arglist); 35 | if ($path !== '') { 36 | $excludes = $addPathToArray($path, $excludes); 37 | } 38 | continue; 39 | } 40 | if (substr($arg, 0, 1) === '-') { 41 | $path = self::handleShortOption(substr($arg, 1), $arglist); 42 | if ($path !== '') { 43 | $excludes = $addPathToArray($path, $excludes); 44 | } 45 | continue; 46 | } 47 | $includes = $addPathToArray($arg, $includes); 48 | } 49 | 50 | return new Options(new ImmSet($includes), new ImmSet($excludes)); 51 | } 52 | 53 | private static function handleLongOption( 54 | string $arg, 55 | Vector $args, 56 | ): string { 57 | $parts = new Vector(explode('=', $arg, 2)); 58 | if ($parts->at(0) !== 'exclude') { 59 | return ''; 60 | } 61 | $value = $parts->get(1); 62 | if ($value === null) { 63 | $value = self::tryNext($args); 64 | } 65 | return $value; 66 | } 67 | 68 | private static function handleShortOption( 69 | string $arg, 70 | Vector $args, 71 | ): string { 72 | if (substr($arg, 0, 1) !== 'e') { 73 | return ''; 74 | } 75 | $value = substr($arg, 1); 76 | if ($value === false) { 77 | $value = self::tryNext($args); 78 | } 79 | return $value; 80 | } 81 | private static function tryNext(Vector $args): string { 82 | if ($args->isEmpty() || substr($args->at(0), 0, 1) === '-') { 83 | return ''; 84 | } 85 | return $args->pop(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/HackUnit.php: -------------------------------------------------------------------------------- 1 | $reportFormatters, 12 | private Report\Status $status, 13 | private Test\SuiteBuilder $suiteBuilder, 14 | private Test\Loader $loader, 15 | private Test\Runner $runner, 16 | ) { 17 | $this->summaryBuilder = new Report\SummaryBuilder(); 18 | } 19 | 20 | public function run(): void { 21 | $this->loader->onBuildFailure( 22 | ($event) ==> { 23 | $this->failures = true; 24 | $this->status->handleBuildFailure($event); 25 | }, 26 | ); 27 | $this->suiteBuilder->onMalformedSuite( 28 | inst_meth($this->summaryBuilder, 'handleMalformedSuite'), 29 | ); 30 | 31 | $this->runner->onRunStart( 32 | () ==> { 33 | $this->status->handleRunStart(); 34 | $this->summaryBuilder->startTiming(); 35 | }, 36 | ); 37 | $this->runner->onFailure( 38 | ($e) ==> { 39 | // Allow us to set the exit code 40 | $this->failures = true; 41 | $this->status->handleFailure($e); 42 | $this->summaryBuilder->handleFailure($e); 43 | 44 | }, 45 | ); 46 | $this->runner->onSkip( 47 | ($e) ==> { 48 | $this->status->handleSkip($e); 49 | $this->summaryBuilder->handleSkip($e); 50 | }, 51 | ); 52 | 53 | $this->runner->onSuccess( 54 | ($e) ==> { 55 | $this->summaryBuilder->handleSuccess($e); 56 | }, 57 | ); 58 | 59 | $this->runner->onPass( 60 | ($e) ==> { 61 | $this->status->handlePass($e); 62 | $this->summaryBuilder->handlePass($e); 63 | }, 64 | ); 65 | 66 | $this->runner->onUncaughtException( 67 | ($exception) ==> { 68 | $this->summaryBuilder->handleUntestedException($exception); 69 | }, 70 | ); 71 | $this->runner->onRunEnd( 72 | () ==> { 73 | $this->summaryBuilder->stopTiming(); 74 | $summary = $this->summaryBuilder->getSummary(); 75 | foreach ($this->reportFormatters as $formatter) { 76 | $formatter->writeReport($summary); 77 | } 78 | }, 79 | ); 80 | // LET'S DO THIS! 81 | $this->runner->run($this->loader->testSuites()); 82 | 83 | // Exit codes FTW 84 | if ($this->failures) { 85 | exit(1); 86 | } 87 | exit(0); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Assertion/NumericAssertion.php: -------------------------------------------------------------------------------- 1 | 13 | implements 14 | \HackPack\HackUnit\Contract\Assertion\NumericAssertion { 15 | use FailureEmitter; 16 | use SuccessEmitter; 17 | 18 | private bool $negate = false; 19 | 20 | public function __construct( 21 | private Tcontext $context, 22 | Vector $failureListeners, 23 | Vector $successListeners, 24 | ) { 25 | $this->setFailureListeners($failureListeners); 26 | $this->setSuccessListeners($successListeners); 27 | } 28 | 29 | public function not(): this { 30 | $this->negate = true; 31 | return $this; 32 | } 33 | 34 | public function eq(Tcontext $expected): void { 35 | if ($this->context === $expected) { 36 | $this->negate ? $this->fail('!==', $expected) : $this->emitSuccess(); 37 | return; 38 | } 39 | $this->negate ? $this->emitSuccess() : $this->fail('===', $expected); 40 | } 41 | 42 | public function gt(Tcontext $expected): void { 43 | if ($this->context > $expected) { 44 | $this->negate ? $this->fail('!>', $expected) : $this->emitSuccess(); 45 | return; 46 | } 47 | $this->negate ? $this->emitSuccess() : $this->fail('>', $expected); 48 | } 49 | 50 | public function gte(Tcontext $expected): void { 51 | if ($this->context >= $expected) { 52 | $this->negate ? $this->fail('!>=', $expected) : $this->emitSuccess(); 53 | return; 54 | } 55 | $this->negate ? $this->emitSuccess() : $this->fail('>=', $expected); 56 | } 57 | 58 | public function lt(Tcontext $expected): void { 59 | if ($this->context < $expected) { 60 | $this->negate ? $this->fail('!<', $expected) : $this->emitSuccess(); 61 | return; 62 | } 63 | $this->negate ? $this->emitSuccess() : $this->fail('<', $expected); 64 | } 65 | 66 | public function lte(Tcontext $expected): void { 67 | if ($this->context <= $expected) { 68 | $this->negate ? $this->fail('!<=', $expected) : $this->emitSuccess(); 69 | return; 70 | } 71 | $this->negate ? $this->emitSuccess() : $this->fail('<=', $expected); 72 | } 73 | 74 | private function fail(string $comparison, Tcontext $expected): void { 75 | if (is_int($this->context)) { 76 | $this->emitFailure( 77 | Failure::fromCallStack( 78 | sprintf( 79 | 'Integer assertion failed. Expected %d %s %d', 80 | $this->context, 81 | $comparison, 82 | $expected, 83 | ), 84 | ), 85 | ); 86 | return; 87 | } 88 | $this->emitFailure( 89 | Failure::fromCallStack( 90 | sprintf( 91 | 'Float assertion failed. Expected %f %s %f', 92 | $this->context, 93 | $comparison, 94 | $expected, 95 | ), 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Assert.php: -------------------------------------------------------------------------------- 1 | $failureListeners, 8 | Vector $skipListeners, 9 | Vector $successListeners, 10 | ): this { 11 | return new static($failureListeners, $skipListeners, $successListeners); 12 | } 13 | 14 | public function __construct( 15 | private Vector $failureListeners, 16 | private Vector $skipListeners, 17 | private Vector $successListeners, 18 | ) {} 19 | 20 | public function bool(bool $context): Contract\Assertion\BoolAssertion { 21 | return new Assertion\BoolAssertion( 22 | $context, 23 | $this->failureListeners, 24 | $this->successListeners, 25 | ); 26 | } 27 | 28 | public function int(int $context): Contract\Assertion\NumericAssertion { 29 | return new Assertion\NumericAssertion( 30 | $context, 31 | $this->failureListeners, 32 | $this->successListeners, 33 | ); 34 | } 35 | 36 | public function float( 37 | float $context, 38 | ): Contract\Assertion\NumericAssertion { 39 | return new Assertion\NumericAssertion( 40 | $context, 41 | $this->failureListeners, 42 | $this->successListeners, 43 | ); 44 | } 45 | 46 | public function string(string $context): Contract\Assertion\StringAssertion { 47 | return new Assertion\StringAssertion( 48 | $context, 49 | $this->failureListeners, 50 | $this->successListeners, 51 | ); 52 | } 53 | 54 | public function whenCalled( 55 | (function(): void) $context, 56 | ): Contract\Assertion\CallableAssertion { 57 | return new Assertion\CallableAssertion( 58 | $context, 59 | $this->failureListeners, 60 | $this->successListeners, 61 | ); 62 | } 63 | 64 | public function mixed(mixed $context): Contract\Assertion\MixedAssertion { 65 | return new Assertion\MixedAssertion( 66 | $context, 67 | $this->failureListeners, 68 | $this->successListeners, 69 | ); 70 | } 71 | 72 | public function container( 73 | Container $context, 74 | ): Contract\Assertion\ContainerAssertion { 75 | return new Assertion\ContainerAssertion( 76 | $context, 77 | $this->failureListeners, 78 | $this->successListeners, 79 | ); 80 | } 81 | 82 | public function keyedContainer( 83 | KeyedContainer $context, 84 | ): Contract\Assertion\KeyedContainerAssertion { 85 | return new Assertion\KeyedContainerAssertion( 86 | $context, 87 | $this->failureListeners, 88 | $this->successListeners, 89 | ); 90 | } 91 | 92 | public function skip( 93 | string $reason, 94 | ?Util\TraceItem $traceItem = null, 95 | ): void { 96 | if ($traceItem === null) { 97 | $stack = Util\Trace::generate(); 98 | $traceItem = shape( 99 | 'file' => $stack[0]['file'], 100 | 'line' => $stack[0]['line'], 101 | 'function' => $stack[1]['function'], 102 | 'class' => $stack[1]['class'], 103 | ); 104 | } 105 | $skip = new Event\Skip($reason, $traceItem, Util\Trace::findTestMethod()); 106 | foreach ($this->skipListeners as $l) { 107 | $l($skip); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Test/Suite.php: -------------------------------------------------------------------------------- 1 | string, 16 | 'suite name' => string, 17 | 'factory' => (function(): Awaitable), 18 | 'method' => InvokerWithParams, 19 | 'trace item' => TraceItem, 20 | 'skip' => bool, 21 | 'data provider' => (function(): AsyncIterator>), 22 | ); 23 | 24 | class Suite implements \HackPack\HackUnit\Contract\Test\Suite { 25 | 26 | public function __construct( 27 | private string $name, 28 | private \ConstVector $tests = Vector {}, 29 | private \ConstVector $suiteup = Vector {}, 30 | private \ConstVector $suitedown = Vector {}, 31 | private \ConstVector $testup = Vector {}, 32 | private \ConstVector $testdown = Vector {}, 33 | ) {} 34 | 35 | public function name(): string { 36 | return $this->name; 37 | } 38 | 39 | public async function run( 40 | Assert $assert, 41 | (function(): void) $testPassed, 42 | \ConstVector $testStartListeners, 43 | ): Awaitable { 44 | await (async (Test $test) ==> { 45 | 46 | $testStartEvent = 47 | new TestStart($test['suite name'], $test['name']); 48 | foreach ($testStartListeners as $testStartListener) { 49 | $testStartListener($testStartEvent); 50 | } 51 | 52 | if ($test['skip']) { 53 | try { 54 | $assert->skip('Test marked <>', $test['trace item']); 55 | } catch (Interruption $e) { 56 | // any listeners should have been notified by now 57 | } 58 | return; 59 | } 60 | 61 | $instance = await $test['factory'](); 62 | 63 | await ($this->testup->map($pretest ==> $pretest($instance, [])) 64 | |> Asio\v($$)); 65 | 66 | $results = Vector {}; 67 | foreach ($test['data provider']() await as $data) { 68 | array_unshift($data, $assert); 69 | $results->add($test['method']($instance, $data)); 70 | } 71 | $results = await Asio\vw($results); 72 | foreach ($results as $result) { 73 | if ($result->isSucceeded()) { 74 | $testPassed(); 75 | } 76 | 77 | if ($result->isFailed()) { 78 | $exception = $result->getException(); 79 | if (!($exception instanceof Interruption)) { 80 | throw $exception; 81 | } 82 | } 83 | } 84 | 85 | await $this->testdown->map( 86 | $posttest ==> $posttest($instance, []), 87 | ) 88 | |> Asio\v($$); 89 | }) 90 | |> $this->tests->map($$) 91 | |> Asio\v($$); 92 | } 93 | 94 | public async function up(): Awaitable { 95 | await \HH\Asio\v($this->suiteup->map($f ==> $f(null, []))); 96 | } 97 | 98 | public async function down(): Awaitable { 99 | await \HH\Asio\v($this->suitedown->map($f ==> $f(null, []))); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Test/Loader.php: -------------------------------------------------------------------------------- 1 | $fileNames = Vector {}; 12 | 13 | private Loader $loader; 14 | 15 | public function __construct() { 16 | $this->fileDir = dirname(__DIR__).'/Fixtures/LoadingFiles'; 17 | $this->loader = new Loader( 18 | $filename ==> { 19 | $this->fileNames->add($filename); 20 | return Vector {}; 21 | }, 22 | ); 23 | } 24 | 25 | <> 26 | public function ignoresMultipleFiles(Assert $assert): void { 27 | $this->loader 28 | ->including($this->fileDir) 29 | ->excluding($this->fileDir.'/IgnoreMe/file1') 30 | ->excluding($this->fileDir.'/IgnoreMe/file2'); 31 | $this->assertExpectedFiles( 32 | $assert, 33 | Vector { 34 | $this->fileDir.'/file1', 35 | $this->fileDir.'/file2', 36 | $this->fileDir.'/file3', 37 | $this->fileDir.'/IgnoreMe/file3', 38 | }, 39 | ); 40 | } 41 | 42 | <> 43 | public function ignoresFile(Assert $assert): void { 44 | $this->loader 45 | ->including($this->fileDir) 46 | ->excluding($this->fileDir.'/IgnoreMe/file1'); 47 | $this->assertExpectedFiles( 48 | $assert, 49 | Vector { 50 | $this->fileDir.'/file1', 51 | $this->fileDir.'/file2', 52 | $this->fileDir.'/file3', 53 | $this->fileDir.'/IgnoreMe/file2', 54 | $this->fileDir.'/IgnoreMe/file3', 55 | }, 56 | ); 57 | } 58 | 59 | <> 60 | public function ignoresFolder(Assert $assert): void { 61 | $this->loader 62 | ->including($this->fileDir) 63 | ->excluding($this->fileDir.'/IgnoreMe'); 64 | $this->assertExpectedFiles( 65 | $assert, 66 | Vector { 67 | $this->fileDir.'/file1', 68 | $this->fileDir.'/file2', 69 | $this->fileDir.'/file3', 70 | }, 71 | ); 72 | } 73 | 74 | <> 75 | public function loadsAllFiles(Assert $assert): void { 76 | $this->loader->including($this->fileDir); 77 | $this->assertExpectedFiles( 78 | $assert, 79 | Vector { 80 | $this->fileDir.'/file1', 81 | $this->fileDir.'/file2', 82 | $this->fileDir.'/file3', 83 | $this->fileDir.'/IgnoreMe/file1', 84 | $this->fileDir.'/IgnoreMe/file2', 85 | $this->fileDir.'/IgnoreMe/file3', 86 | }, 87 | ); 88 | } 89 | 90 | <> 91 | public function loadsSingleFile(Assert $assert): void { 92 | $this->loader->including($this->fileDir.'/file1'); 93 | $this->assertExpectedFiles($assert, Vector {$this->fileDir.'/file1'}); 94 | } 95 | 96 | <> 97 | public function loadsMultipleFiles(Assert $assert): void { 98 | $this->loader 99 | ->including($this->fileDir.'/file1') 100 | ->including($this->fileDir.'/file2'); 101 | $this->assertExpectedFiles( 102 | $assert, 103 | Vector {$this->fileDir.'/file1', $this->fileDir.'/file2'}, 104 | ); 105 | } 106 | 107 | private function assertExpectedFiles( 108 | Assert $assert, 109 | Vector $expectedFiles, 110 | ): void { 111 | // This actually hits the filesystem 112 | $this->loader->testSuites(); 113 | 114 | $missing = array_diff($expectedFiles, $this->fileNames); 115 | $extra = array_diff($this->fileNames, $expectedFiles); 116 | 117 | $assert->int(count($missing))->eq(0); 118 | $assert->int(count($extra))->eq(0); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Test/Loader.php: -------------------------------------------------------------------------------- 1 | $buildFailureListeners = Vector {}; 14 | 15 | public function __construct( 16 | private (function(string): Traversable) $suiteBuilder, 17 | private Set $includes = Set {}, 18 | private Set $excludes = Set {}, 19 | ) {} 20 | 21 | public function onBuildFailure(BuildFailureListener $l): this { 22 | $this->buildFailureListeners->add($l); 23 | return $this; 24 | } 25 | 26 | public function including(string $path): this { 27 | $this->includes->add($path); 28 | return $this; 29 | } 30 | 31 | public function excluding(string $path): this { 32 | $fullPath = realpath($path); 33 | if (is_string($fullPath)) { 34 | $this->excludes->add($fullPath); 35 | } 36 | return $this; 37 | } 38 | 39 | public function testSuites(): Vector { 40 | $suites = Vector {}; 41 | $builder = $this->suiteBuilder; 42 | 43 | foreach ($this->pathsToScan() as $path) { 44 | try { 45 | $suite = $builder($path); 46 | } catch (\Exception $e) { 47 | $this->emitBuildFailure($path, $e); 48 | $suite = null; 49 | } 50 | 51 | if ($suite !== null) { 52 | $suites->addAll($suite); 53 | } 54 | } 55 | 56 | return $suites; 57 | } 58 | 59 | private function pathsToScan(): \Generator { 60 | foreach ($this->includes as $includeBase) { 61 | $info = new SplFileInfo($includeBase); 62 | $rp = $info->getRealPath(); 63 | if (!is_string($rp) || !$info->isReadable()) { 64 | echo 'skipping '.$rp.PHP_EOL; 65 | continue; 66 | } 67 | 68 | if ($info->isFile() && $this->isPathIncluded($rp)) { 69 | yield $rp; 70 | } 71 | 72 | if ($info->isDir()) { 73 | $rdi = new \RecursiveDirectoryIterator( 74 | $rp, 75 | FilesystemIterator::CURRENT_AS_FILEINFO | 76 | FilesystemIterator::UNIX_PATHS | 77 | FilesystemIterator::SKIP_DOTS, 78 | ); 79 | /* HH_FIXME[4105] */ 80 | $rfi = new \RecursiveCallbackFilterIterator( 81 | $rdi, 82 | $pathInfo ==> { 83 | $realPath = $pathInfo->getRealPath(); 84 | return 85 | $pathInfo->isReadable() && 86 | is_string($realPath) && 87 | $this->isPathIncluded($realPath); 88 | }, 89 | ); 90 | $rii = new \RecursiveIteratorIterator($rfi); 91 | foreach ($rii as $fileInfo) { 92 | $realPath = $fileInfo->getRealPath(); 93 | if (is_string($realPath)) { 94 | yield $realPath; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | private function isPathIncluded(string $path): bool { 102 | foreach ($this->excludes as $exclude) { 103 | if ($path === $exclude || strpos($path, $exclude.'/') === 0) { 104 | return false; 105 | } 106 | } 107 | return true; 108 | } 109 | 110 | private function emitBuildFailure(string $path, \Exception $e): void { 111 | $event = new BuildFailure($path, $e); 112 | foreach ($this->buildFailureListeners as $l) { 113 | $l($event); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/Assertion/StringAssertion.php: -------------------------------------------------------------------------------- 1 | failListeners(), 24 | $this->successListeners(), 25 | ); 26 | } 27 | 28 | <> 29 | public function expectSameToPass(Assert $assert): void { 30 | $a = $this->makeAssertion(); 31 | $a->is(self::test); 32 | 33 | $assert->int($this->successCount)->eq(1); 34 | $assert->int($this->failEvents->count())->eq(0); 35 | } 36 | 37 | <> 38 | public function expectSameToFail(Assert $assert): void { 39 | $a = $this->makeAssertion(); 40 | $a->is(self::superString); 41 | 42 | $assert->int($this->successCount)->eq(0); 43 | $assert->int($this->failEvents->count())->eq(1); 44 | } 45 | 46 | <> 47 | public function expectDifferentToPass(Assert $assert): void { 48 | $a = $this->makeAssertion(); 49 | $a->not()->is(self::superString); 50 | 51 | $assert->int($this->successCount)->eq(1); 52 | $assert->int($this->failEvents->count())->eq(0); 53 | } 54 | 55 | <> 56 | public function expectDifferentToFail(Assert $assert): void { 57 | $a = $this->makeAssertion(); 58 | $a->not()->is(self::test); 59 | 60 | $assert->int($this->successCount)->eq(0); 61 | $assert->int($this->failEvents->count())->eq(1); 62 | } 63 | 64 | <> 65 | public function expectContainsToPass(Assert $assert): void { 66 | $a = $this->makeAssertion(); 67 | $a->contains(self::subString); 68 | $a->contains(self::test); 69 | 70 | $assert->int($this->successCount)->eq(2); 71 | $assert->int($this->failEvents->count())->eq(0); 72 | } 73 | 74 | <> 75 | public function expectContainsToFail(Assert $assert): void { 76 | $a = $this->makeAssertion(); 77 | $a->contains(self::superString); 78 | 79 | $assert->int($this->successCount)->eq(0); 80 | $assert->int($this->failEvents->count())->eq(1); 81 | } 82 | 83 | <> 84 | public function expectContainedByToPass(Assert $assert): void { 85 | $a = $this->makeAssertion(); 86 | $a->containedBy(self::superString); 87 | $a->containedBy(self::test); 88 | 89 | $assert->int($this->successCount)->eq(2); 90 | $assert->int($this->failEvents->count())->eq(0); 91 | } 92 | 93 | <> 94 | public function expectContainedByToFail(Assert $assert): void { 95 | $a = $this->makeAssertion(); 96 | $a->containedBy(self::subString); 97 | 98 | $assert->int($this->successCount)->eq(0); 99 | $assert->int($this->failEvents->count())->eq(1); 100 | } 101 | 102 | <> 103 | public function expectMatchesToPass(Assert $assert): void { 104 | $a = $this->makeAssertion(); 105 | $a->matches(self::matchingPattern); 106 | 107 | $assert->int($this->successCount)->eq(1); 108 | $assert->int($this->failEvents->count())->eq(0); 109 | } 110 | 111 | <> 112 | public function expectMatchesToFail(Assert $assert): void { 113 | $a = $this->makeAssertion(); 114 | $a->matches(self::nonMatchingPattern); 115 | 116 | $assert->int($this->successCount)->eq(0); 117 | $assert->int($this->failEvents->count())->eq(1); 118 | } 119 | 120 | <> 121 | public function expectHasLengthToPass(Assert $assert): void { 122 | $a = $this->makeAssertion(); 123 | $a->hasLength(self::realLen); 124 | 125 | $assert->int($this->successCount)->eq(1); 126 | $assert->int($this->failEvents->count())->eq(0); 127 | } 128 | 129 | <> 130 | public function expectHasLengthToFail(Assert $assert): void { 131 | $a = $this->makeAssertion(); 132 | $a->hasLength(self::lessLen); 133 | $a->hasLength(self::moreLen); 134 | 135 | $assert->int($this->successCount)->eq(0); 136 | $assert->int($this->failEvents->count())->eq(2); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Assertion/MixedAssertion.php: -------------------------------------------------------------------------------- 1 | $failureListeners, 22 | Vector $successListeners, 23 | ) { 24 | $this->setFailureListeners($failureListeners); 25 | $this->setSuccessListeners($successListeners); 26 | } 27 | 28 | public function not(): this { 29 | $this->negate = true; 30 | return $this; 31 | } 32 | 33 | public function isNull(): void { 34 | if ($this->context === null) { 35 | $this->negate 36 | ? $this->fail('Expected context to be non-null.') 37 | : $this->emitSuccess(); 38 | return; 39 | } 40 | $this->negate 41 | ? $this->emitSuccess() 42 | : $this->fail('Expected context to be null.'); 43 | } 44 | 45 | public function isBool(): void { 46 | if (is_bool($this->context)) { 47 | $this->negate 48 | ? $this->fail('Expected context to not be a bool.') 49 | : $this->emitSuccess(); 50 | return; 51 | } 52 | $this->negate 53 | ? $this->emitSuccess() 54 | : $this->fail('Expected context to be a bool.'); 55 | } 56 | 57 | public function isInt(): void { 58 | if (is_int($this->context)) { 59 | $this->negate 60 | ? $this->fail('Expected context to not be an integer.') 61 | : $this->emitSuccess(); 62 | return; 63 | } 64 | $this->negate 65 | ? $this->emitSuccess() 66 | : $this->fail('Expected context to be an integer.'); 67 | } 68 | 69 | public function isFloat(): void { 70 | if (is_float($this->context)) { 71 | $this->negate 72 | ? $this->fail('Expected context to not be an float.') 73 | : $this->emitSuccess(); 74 | return; 75 | } 76 | $this->negate 77 | ? $this->emitSuccess() 78 | : $this->fail('Expected context to be an float.'); 79 | } 80 | 81 | public function isString(): void { 82 | if (is_string($this->context)) { 83 | $this->negate 84 | ? $this->fail('Expected context to not be an string.') 85 | : $this->emitSuccess(); 86 | return; 87 | } 88 | $this->negate 89 | ? $this->emitSuccess() 90 | : $this->fail('Expected context to be an string.'); 91 | } 92 | 93 | public function isArray(): void { 94 | if (is_array($this->context)) { 95 | $this->negate 96 | ? $this->fail('Expected context to not be an array.') 97 | : $this->emitSuccess(); 98 | return; 99 | } 100 | $this->negate 101 | ? $this->emitSuccess() 102 | : $this->fail('Expected context to be an array.'); 103 | } 104 | 105 | public function looselyEquals(mixed $expected): void { 106 | if ($this->context == $expected) { 107 | $this->negate 108 | ? $this->fail('Items expected to not be equal.') 109 | : $this->emitSuccess(); 110 | return; 111 | } 112 | $this->negate 113 | ? $this->emitSuccess() 114 | : $this->fail('Items expected to be equal.'); 115 | } 116 | 117 | public function identicalTo(mixed $expected): void { 118 | if ($this->context === $expected) { 119 | $this->negate 120 | ? $this->fail('Items expected to not be identical.') 121 | : $this->emitSuccess(); 122 | return; 123 | } 124 | $this->negate 125 | ? $this->emitSuccess() 126 | : $this->fail('Items expected to be identical.'); 127 | } 128 | 129 | public function isObject(): void { 130 | if (is_object($this->context)) { 131 | $this->negate 132 | ? $this->fail('Expected context to not be an object.') 133 | : $this->emitSuccess(); 134 | return; 135 | } 136 | $this->negate 137 | ? $this->emitSuccess() 138 | : $this->fail('Expected context to be an object.'); 139 | } 140 | 141 | public function isTypeOf(string $className): void { 142 | if (is_a($this->context, $className)) { 143 | $this->negate 144 | ? $this->fail( 145 | 'Expected context to not be an instance of '.$className.'.', 146 | ) 147 | : $this->emitSuccess(); 148 | return; 149 | } 150 | $this->negate 151 | ? $this->emitSuccess() 152 | : $this->fail('Expected context to be an instance of '.$className.'.'); 153 | } 154 | 155 | private function fail(string $message): void { 156 | $this->emitFailure(Failure::fromCallStack($message)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/Util/TraceTest.php: -------------------------------------------------------------------------------- 1 | { 11 | return Vector { 12 | 1, 13 | 0, 14 | -1, 15 | true, 16 | false, 17 | 0.1, 18 | 0.0, 19 | -1.1, 20 | [], 21 | ['a'], 22 | new TraceTest(), 23 | }; 24 | } 25 | 26 | private function nonInts(): Vector { 27 | return Vector { 28 | 'a', 29 | 'longer string?', 30 | true, 31 | false, 32 | 0.1, 33 | 0.0, 34 | -1.1, 35 | [], 36 | ['a'], 37 | new TraceTest(), 38 | }; 39 | } 40 | 41 | <> 42 | public function missingParamsResultsInNull(Assert $assert): void { 43 | $raw = []; 44 | $items = Trace::convert([$raw]); 45 | $assert->int($items->count())->eq(1); 46 | 47 | $item = $items->at(0); 48 | 49 | $assert->mixed($item['line'])->isNull(); 50 | $assert->mixed($item['function'])->isNull(); 51 | $assert->mixed($item['class'])->isNull(); 52 | $assert->mixed($item['file'])->isNull(); 53 | } 54 | 55 | <> 56 | public function lineMustBeInteger(Assert $assert): void { 57 | foreach ($this->nonInts() as $mixed) { 58 | $item = Trace::convert([['line' => $mixed]])->at(0); 59 | $assert->mixed($item['line'])->isNull(); 60 | } 61 | foreach ([-1, 0, 1, 3] as $int) { 62 | $item = Trace::convert([['line' => $int]])->at(0); 63 | $assert->mixed($item['line'])->identicalTo($int); 64 | } 65 | } 66 | 67 | <> 68 | public function functionMustBeString(Assert $assert): void { 69 | foreach ($this->nonStrings() as $mixed) { 70 | $item = Trace::convert([['function' => $mixed]])->at(0); 71 | $assert->mixed($item['function'])->isNull(); 72 | } 73 | foreach (['', '0', '1e10', 'normal string'] as $string) { 74 | $item = Trace::convert([['function' => $string]])->at(0); 75 | $assert->mixed($item['function'])->identicalTo($string); 76 | } 77 | } 78 | 79 | <> 80 | public function classMustBeString(Assert $assert): void { 81 | foreach ($this->nonStrings() as $mixed) { 82 | $item = Trace::convert([['class' => $mixed]])->at(0); 83 | $assert->mixed($item['class'])->isNull(); 84 | } 85 | foreach (['', '0', '1e10', 'normal string'] as $string) { 86 | $item = Trace::convert([['class' => $string]])->at(0); 87 | $assert->mixed($item['class'])->identicalTo($string); 88 | } 89 | } 90 | 91 | <> 92 | public function fileMustBeString(Assert $assert): void { 93 | foreach ($this->nonStrings() as $mixed) { 94 | $item = Trace::convert([['file' => $mixed]])->at(0); 95 | $assert->mixed($item['file'])->isNull(); 96 | } 97 | foreach (['', '0', '1e10', 'normal string'] as $string) { 98 | $item = Trace::convert([['file' => $string]])->at(0); 99 | $assert->mixed($item['file'])->identicalTo($string); 100 | } 101 | } 102 | 103 | <> 104 | public function assertionCallSearchFailsWhenSearchingWithoutAssertion( 105 | Assert $assert, 106 | ): void { 107 | $item = Trace::findAssertionCall(); 108 | $assert->mixed($item['line'])->isNull(); 109 | $assert->mixed($item['function'])->isNull(); 110 | $assert->mixed($item['class'])->isNull(); 111 | $assert->mixed($item['file'])->isNull(); 112 | } 113 | 114 | <> 115 | public function assertionCallSearchSucceedsWhenSearchingWithAssertion( 116 | Assert $assert, 117 | ): void { 118 | $mockAssert = new \HackPack\HackUnit\Tests\Doubles\Util\Assertion(); 119 | $items = Vector {}; 120 | $line = __LINE__ + 5; 121 | $mockAssert->run( 122 | () ==> { 123 | $items->add(Trace::findAssertionCall()); 124 | }, 125 | ); 126 | 127 | $assert->int($items->count())->eq(1); 128 | $item = $items->at(0); 129 | 130 | $assert->mixed($item['line'])->isInt(); 131 | $assert->mixed($item['function'])->isString(); 132 | $assert->mixed($item['class'])->isString(); 133 | $assert->mixed($item['file'])->isString(); 134 | 135 | $assert->int((int) $item['line'])->eq($line); 136 | $assert->string((string) $item['function'])->is(__FUNCTION__); 137 | $assert->string((string) $item['class'])->is(__CLASS__); 138 | $assert->string((string) $item['file'])->is(__FILE__); 139 | } 140 | 141 | private function levelOne(): TraceItem { 142 | return $this->levelTwo(); 143 | } 144 | 145 | private function levelTwo(): TraceItem { 146 | return Trace::findTestMethod(); 147 | } 148 | 149 | <> 150 | public function findTestMethod(Assert $assert): void { 151 | $line = __LINE__ + 1; 152 | $item = $this->levelOne(); 153 | 154 | $assert->mixed($item['line'])->isInt(); 155 | $assert->mixed($item['function'])->isString(); 156 | $assert->mixed($item['class'])->isString(); 157 | $assert->mixed($item['file'])->isString(); 158 | 159 | $assert->int((int) $item['line'])->eq($line); 160 | $assert->string((string) $item['function'])->is(__FUNCTION__); 161 | $assert->string((string) $item['class'])->is(__CLASS__); 162 | $assert->string((string) $item['file'])->is(__FILE__); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/Test/InvalidParsing.php: -------------------------------------------------------------------------------- 1 | string, 11 | 'errors' => int, 12 | ); 13 | 14 | class InvalidParsing extends ParserTest { 15 | 16 | protected static function basePath(): string { 17 | return dirname(__DIR__).'/Fixtures/InvalidSuites'; 18 | } 19 | 20 | protected static function fullName(string $name): string { 21 | return 'HackPack\\HackUnit\\Tests\\Fixtures\\InvalidSuites\\'.$name; 22 | } 23 | 24 | <> 25 | public function factoryParsing(Assert $assert): void { 26 | $factoryList = $this->parserFromSuiteName('AbstractFactory')->factories(); 27 | $assert->int($factoryList->count())->eq(0); 28 | } 29 | 30 | <> 31 | public static function invalidSuiteUpDown(): Traversable { 32 | return [ 33 | 'SuiteUpParams', 34 | 'SuiteUpNonStatic', 35 | 'SuiteDownParams', 36 | 'SuiteDownNonStatic', 37 | ]; 38 | } 39 | 40 | <> 41 | public function SuiteUpDownParseErrors( 42 | Assert $assert, 43 | string $suiteName, 44 | ): void { 45 | $parser = $this->parserFromSuiteName($suiteName); 46 | $assert->int($parser->errors()->count())->eq(1); 47 | $assert->int($parser->suiteUp()->count())->eq(0); 48 | } 49 | 50 | <> 51 | public static function invalidUpDownSuites(): Traversable { 52 | return [ 53 | shape('errors' => 1, 'name' => 'TestUpParams'), 54 | shape('errors' => 2, 'name' => 'TestUpConstructDestruct'), 55 | shape('errors' => 2, 'name' => 'TestDownConstructDestruct'), 56 | shape('errors' => 1, 'name' => 'TestUpParams'), 57 | shape('errors' => 1, 'name' => 'TestDownParams'), 58 | ]; 59 | } 60 | <> 61 | public function TestUpDownParseErrors( 62 | Assert $assert, 63 | ErrorSuite $suiteData, 64 | ): void { 65 | $parser = $this->parserFromSuiteName($suiteData['name']); 66 | $assert->int($parser->errors()->count())->eq($suiteData['errors']); 67 | $assert->int($parser->testUp()->count())->eq(0); 68 | } 69 | 70 | <> 71 | public function TestParseErrors(Assert $assert): void { 72 | $parser = $this->parserFromSuiteName('TestConstructDestruct'); 73 | $assert->int($parser->errors()->count())->eq(2); 74 | $assert->int($parser->tests()->count())->eq(0); 75 | 76 | $parser = $this->parserFromSuiteName('TestParams'); 77 | $assert->int($parser->errors()->count())->eq(3); 78 | $assert->int($parser->tests()->count())->eq(0); 79 | } 80 | 81 | <> 82 | public static function invalidFactories(): Traversable { 83 | return [ 84 | 'FactoryParams', 85 | 'NonStaticFactory', 86 | 'FactoryReturnType', 87 | 'InvalidDerivedFactory', 88 | ]; 89 | } 90 | 91 | <> 92 | public function FactoryParseErrors(Assert $assert, string $suiteName): void { 93 | $parser = $this->parserFromSuiteName($suiteName); 94 | $assert->int($parser->errors()->count())->eq(1); 95 | $assert->int($parser->factories()->count())->eq(0); 96 | } 97 | 98 | <> 99 | public function duplicateFactories(Assert $assert): void { 100 | $parser = $this->parserFromSuiteName('DuplicateFactories'); 101 | $assert->int($parser->errors()->count())->eq(1); 102 | $assert->int($parser->factories()->count())->eq(1); 103 | } 104 | 105 | <> 106 | public static function invalidDataProviders(): Traversable { 107 | return [ 108 | shape('errors' => 1, 'name' => 'DataProviderMissingName'), 109 | shape('errors' => 1, 'name' => 'DataProviderWithParams'), 110 | shape('errors' => 1, 'name' => 'InstanceDataProvider'), 111 | shape('errors' => 3, 'name' => 'NonTraversableDataProvider'), 112 | ]; 113 | } 114 | 115 | <> 116 | public function invalidDataProvider( 117 | Assert $assert, 118 | ErrorSuite $suiteData, 119 | ): void { 120 | $parser = $this->parserFromSuiteName($suiteData['name']); 121 | $assert->int($parser->errors()->count())->eq($suiteData['errors']); 122 | $assert->int($parser->tests()->count())->eq(0); 123 | } 124 | 125 | <> 126 | public static function invalidDataConsumers(): Traversable { 127 | return [ 128 | shape('errors' => 1, 'name' => 'DataConsumerMismatchParams'), 129 | shape('errors' => 1, 'name' => 'DataConsumerMissingName'), 130 | shape('errors' => 2, 'name' => 'DataConsumerMismatchGeneric'), 131 | ]; 132 | } 133 | 134 | <> 135 | public function invalidDataConsumer( 136 | Assert $assert, 137 | ErrorSuite $suiteData, 138 | ): void { 139 | $parser = $this->parserFromSuiteName($suiteData['name']); 140 | if ($parser->errors()->count() !== $suiteData['errors']) { 141 | var_dump($suiteData, $parser->errors()); 142 | } 143 | $assert->int($parser->errors()->count())->eq($suiteData['errors']); 144 | $assert->int($parser->tests()->count())->eq(0); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Assertion/StringAssertion.php: -------------------------------------------------------------------------------- 1 | $failureListeners, 22 | Vector $successListeners, 23 | ) { 24 | $this->setFailureListeners($failureListeners); 25 | $this->setSuccessListeners($successListeners); 26 | } 27 | 28 | public function not(): this { 29 | $this->negate = true; 30 | return $this; 31 | } 32 | 33 | public function is(string $expected): this { 34 | if ($this->context === $expected) { 35 | $this->negate 36 | ? $this->fail(Vector {'Strings expected to be non-identical.'}) 37 | : $this->emitSuccess(); 38 | return $this; 39 | } 40 | $this->negate 41 | ? $this->emitSuccess() 42 | : $this->fail( 43 | Vector { 44 | 'Strings expected to be identical:', 45 | 'Context ('.strlen($this->context).'):', 46 | substr($this->context, 0, 250), 47 | '', 48 | 'Expected ('.strlen($expected).')', 49 | substr($expected, 0, 250), 50 | }, 51 | ); 52 | return $this; 53 | } 54 | 55 | public function hasLength(int $length): this { 56 | if (strlen($this->context) === $length) { 57 | $this->negate 58 | ? $this->fail( 59 | Vector { 60 | 'Expected length: '.$length, 61 | 'Context ('.strlen($this->context).'):', 62 | substr($this->context, 0, 250), 63 | }, 64 | ) 65 | : $this->emitSuccess(); 66 | return $this; 67 | } 68 | $this->negate 69 | ? $this->emitSuccess() 70 | : $this->fail( 71 | Vector {'Expected string to have length different than '.$length}, 72 | ); 73 | return $this; 74 | } 75 | 76 | public function matches(string $pattern): this { 77 | $result = preg_match($pattern, $this->context); 78 | /* HH_FIXME[4118] */ 79 | if ($result === false) { 80 | $this->fail( 81 | Vector { 82 | 'Error when matching pattern '.$pattern, 83 | $this->lastPregError(), 84 | }, 85 | ); 86 | return $this; 87 | } 88 | 89 | if ($result === 1) { 90 | $this->negate 91 | ? $this->fail( 92 | Vector {'String expected to not match '.$pattern, $this->context}, 93 | ) 94 | : $this->emitSuccess(); 95 | return $this; 96 | } 97 | 98 | $this->negate 99 | ? $this->emitSuccess() 100 | : $this->fail( 101 | Vector {'String expected to match '.$pattern, $this->context}, 102 | ); 103 | return $this; 104 | } 105 | 106 | private function lastPregError(): string { 107 | switch (preg_last_error()) { 108 | case PREG_NO_ERROR: 109 | return 'No error'; 110 | case PREG_INTERNAL_ERROR: 111 | return 'Internal preg error'; 112 | case PREG_BACKTRACK_LIMIT_ERROR: 113 | return 'Backtrack limit'; 114 | case PREG_RECURSION_LIMIT_ERROR: 115 | return 'Recursion limit'; 116 | case PREG_BAD_UTF8_ERROR: 117 | return 'Invalid UTF8'; 118 | case PREG_BAD_UTF8_OFFSET_ERROR: 119 | return 'Matched to middle of UTF8 character.'; 120 | } 121 | return 'Unknown error'; 122 | } 123 | 124 | public function contains(string $needle): this { 125 | if (strpos($this->context, $needle) === false) { 126 | $this->negate 127 | ? $this->emitSuccess() 128 | : $this->fail( 129 | Vector { 130 | 'Expected context to contain substring.', 131 | 'Context:', 132 | $this->context, 133 | 'Substring:', 134 | $needle, 135 | }, 136 | ); 137 | return $this; 138 | } 139 | $this->negate 140 | ? $this->fail( 141 | Vector { 142 | 'Expected context to not contain substring.', 143 | 'Context:', 144 | $this->context, 145 | 'Substring:', 146 | $needle, 147 | }, 148 | ) 149 | : $this->emitSuccess(); 150 | return $this; 151 | } 152 | 153 | public function containedBy(string $haystack): this { 154 | if (strpos($haystack, $this->context) === false) { 155 | $this->negate 156 | ? $this->emitSuccess() 157 | : $this->fail( 158 | Vector { 159 | 'Context:', 160 | $this->context, 161 | 'Expected to be contained by:', 162 | $haystack, 163 | }, 164 | ); 165 | return $this; 166 | } 167 | $this->negate 168 | ? $this->fail( 169 | Vector { 170 | 'Context:', 171 | $this->context, 172 | 'Expected to not be contained by:', 173 | $haystack, 174 | }, 175 | ) 176 | : $this->emitSuccess(); 177 | return $this; 178 | } 179 | 180 | private function fail(Vector $lines): void { 181 | $this->emitFailure(Failure::fromCallStack(implode(PHP_EOL, $lines))); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Assertion/CallableAssertion.php: -------------------------------------------------------------------------------- 1 | $failureListeners, 20 | Vector $successListeners, 21 | ) { 22 | $this->setFailureListeners($failureListeners); 23 | $this->setSuccessListeners($successListeners); 24 | } 25 | 26 | public function willThrow(): void { 27 | try { 28 | $c = $this->context; 29 | $c(); 30 | } catch (\Exception $e) { 31 | $this->emitSuccess(); 32 | return; 33 | } 34 | $this->missingException(); 35 | } 36 | 37 | public function willThrowClass(string $className): void { 38 | try { 39 | $c = $this->context; 40 | $c(); 41 | } catch (\Exception $e) { 42 | if (is_a($e, $className)) { 43 | $this->emitSuccess(); 44 | return; 45 | } 46 | $this->wrongClass($className, get_class($e)); 47 | return; 48 | } 49 | $this->missingException(); 50 | } 51 | 52 | public function willThrowMessage(string $message): void { 53 | try { 54 | $c = $this->context; 55 | $c(); 56 | } catch (\Exception $e) { 57 | if ($e->getMessage() === $message) { 58 | $this->emitSuccess(); 59 | return; 60 | } 61 | $this->wrongMessage($message, $e->getMessage()); 62 | return; 63 | } 64 | $this->missingException(); 65 | } 66 | 67 | public function willThrowMessageContaining(string $needle): void { 68 | try { 69 | $c = $this->context; 70 | $c(); 71 | } catch (\Exception $e) { 72 | if (strpos($e->getMessage(), $needle) !== false) { 73 | $this->emitSuccess(); 74 | return; 75 | } 76 | $this->messageDoesNotContain($needle, $e->getMessage()); 77 | return; 78 | } 79 | $this->missingException(); 80 | } 81 | 82 | public function willThrowClassWithMessage( 83 | string $className, 84 | string $message, 85 | ): void { 86 | try { 87 | $c = $this->context; 88 | $c(); 89 | } catch (\Exception $e) { 90 | if ($e->getMessage() !== $message) { 91 | $this->wrongMessage($message, $e->getMessage()); 92 | return; 93 | } 94 | if (!is_a($e, $className)) { 95 | $this->wrongClass($className, get_class($e)); 96 | return; 97 | } 98 | $this->emitSuccess(); 99 | return; 100 | } 101 | $this->missingException(); 102 | } 103 | 104 | public function willThrowClassWithMessageContaining( 105 | string $className, 106 | string $needle, 107 | ): void { 108 | try { 109 | $c = $this->context; 110 | $c(); 111 | } catch (\Exception $e) { 112 | if (strpos($e->getMessage(), $needle) === false) { 113 | $this->messageDoesNotContain($needle, $e->getMessage()); 114 | return; 115 | } 116 | if (!is_a($e, $className)) { 117 | $this->wrongClass($className, get_class($e)); 118 | return; 119 | } 120 | $this->emitSuccess(); 121 | return; 122 | } 123 | $this->missingException(); 124 | } 125 | 126 | public function willNotThrow(): void { 127 | try { 128 | $c = $this->context; 129 | $c(); 130 | } catch (\Exception $e) { 131 | $this->emitFailure( 132 | Failure::fromCallStack( 133 | implode( 134 | PHP_EOL, 135 | [ 136 | 'Unexpected exception thrown.', 137 | get_class($e), 138 | $e->getMessage(), 139 | ], 140 | ), 141 | ), 142 | ); 143 | return; 144 | } 145 | $this->emitSuccess(); 146 | } 147 | 148 | private function missingException(): void { 149 | $this->emitFailure( 150 | Failure::fromCallStack('Expected exception to be thrown.'), 151 | ); 152 | } 153 | 154 | private function wrongClass(string $expected, string $actual): void { 155 | $this->emitFailure( 156 | Failure::fromCallStack( 157 | 'Expected exception of type '. 158 | $expected. 159 | ' but '. 160 | $actual. 161 | ' was thrown.', 162 | ), 163 | ); 164 | } 165 | 166 | private function wrongMessage(string $expected, string $actual): void { 167 | $this->emitFailure( 168 | Failure::fromCallStack( 169 | implode( 170 | PHP_EOL, 171 | [ 172 | 'Expected exception message:', 173 | $expected, 174 | 'Actual message:', 175 | $actual, 176 | ], 177 | ), 178 | ), 179 | ); 180 | } 181 | 182 | private function messageDoesNotContain( 183 | string $expected, 184 | string $actual, 185 | ): void { 186 | $this->emitFailure( 187 | Failure::fromCallStack( 188 | implode( 189 | PHP_EOL, 190 | [ 191 | 'Expected exception message to contain:', 192 | $expected, 193 | 'Actual message:', 194 | $actual, 195 | ], 196 | ), 197 | ), 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /test/Assertion/MixedAssertion.php: -------------------------------------------------------------------------------- 1 | 12 | $truish = Vector { 13 | '1', 14 | '-1', 15 | '-1.1', 16 | '0.0', 17 | Vector {''}, 18 | [''], 19 | 'a', 20 | 1, 21 | -1, 22 | 1.1, 23 | -0.1, 24 | }; 25 | 26 | private static Vector 27 | $falsish = Vector {'0', Vector {}, [], '', 0, 0.0}; 28 | 29 | private static Vector 30 | $allTypes = Vector {true, 0, 1.0, 'a', [], Vector {}}; 31 | 32 | private function buildAssertion(mixed $context): MixedAssertion { 33 | return new MixedAssertion( 34 | $context, 35 | $this->failListeners(), 36 | $this->successListeners(), 37 | ); 38 | } 39 | 40 | <> 41 | public function looseComparisonToTrue(Assert $assert): void { 42 | $assertion = $this->buildAssertion(true); 43 | $expectedSuccess = 0; 44 | foreach (self::$truish as $truish) { 45 | $expectedSuccess++; 46 | $assertion->looselyEquals($truish); 47 | $assert->int($this->successCount)->eq($expectedSuccess); 48 | $assert->int($this->failEvents->count())->eq(0); 49 | } 50 | } 51 | 52 | <> 53 | public function strictComparisonToTrue(Assert $assert): void { 54 | $assertion = $this->buildAssertion(true); 55 | $expectedFailures = 0; 56 | foreach (self::$truish as $truish) { 57 | $expectedFailures++; 58 | $assertion->identicalTo($truish); 59 | $assert->int($this->failEvents->count())->eq($expectedFailures); 60 | $assert->int($this->successCount)->eq(0); 61 | } 62 | } 63 | 64 | <> 65 | public function looseComparisonToFalse(Assert $assert): void { 66 | $assertion = $this->buildAssertion(false); 67 | $expectedSuccess = 0; 68 | foreach (self::$falsish as $falsish) { 69 | $expectedSuccess++; 70 | $assertion->looselyEquals($falsish); 71 | $assert->int($this->successCount)->eq($expectedSuccess); 72 | $assert->int($this->failEvents->count())->eq(0); 73 | } 74 | } 75 | 76 | <> 77 | public function strictComparisonToFalse(Assert $assert): void { 78 | $assertion = $this->buildAssertion(false); 79 | $expectedFailures = 0; 80 | foreach (self::$falsish as $falsish) { 81 | $expectedFailures++; 82 | $assertion->identicalTo($falsish); 83 | $assert->int($this->failEvents->count())->eq($expectedFailures); 84 | $assert->int($this->successCount)->eq(0); 85 | } 86 | } 87 | 88 | <> 89 | public function isBool(Assert $assert): void { 90 | foreach (self::$allTypes as $item) { 91 | $this->buildAssertion($item)->isBool(); 92 | } 93 | $assert->int($this->failEvents->count()) 94 | ->eq(self::$allTypes->count() - 1); 95 | $assert->int($this->successCount)->eq(1); 96 | } 97 | 98 | <> 99 | public function isInt(Assert $assert): void { 100 | foreach (self::$allTypes as $item) { 101 | $this->buildAssertion($item)->isInt(); 102 | } 103 | $assert->int($this->failEvents->count()) 104 | ->eq(self::$allTypes->count() - 1); 105 | $assert->int($this->successCount)->eq(1); 106 | } 107 | 108 | <> 109 | public function isFloat(Assert $assert): void { 110 | foreach (self::$allTypes as $item) { 111 | $this->buildAssertion($item)->isFloat(); 112 | } 113 | $assert->int($this->failEvents->count()) 114 | ->eq(self::$allTypes->count() - 1); 115 | $assert->int($this->successCount)->eq(1); 116 | } 117 | 118 | <> 119 | public function isString(Assert $assert): void { 120 | foreach (self::$allTypes as $item) { 121 | $this->buildAssertion($item)->isString(); 122 | } 123 | $assert->int($this->failEvents->count()) 124 | ->eq(self::$allTypes->count() - 1); 125 | $assert->int($this->successCount)->eq(1); 126 | } 127 | 128 | <> 129 | public function isArray(Assert $assert): void { 130 | foreach (self::$allTypes as $item) { 131 | $this->buildAssertion($item)->isArray(); 132 | } 133 | $assert->int($this->failEvents->count()) 134 | ->eq(self::$allTypes->count() - 1); 135 | $assert->int($this->successCount)->eq(1); 136 | } 137 | 138 | <> 139 | public function isObject(Assert $assert): void { 140 | foreach (self::$allTypes as $item) { 141 | $this->buildAssertion($item)->isObject(); 142 | } 143 | $assert->int($this->failEvents->count()) 144 | ->eq(self::$allTypes->count() - 1); 145 | $assert->int($this->successCount)->eq(1); 146 | } 147 | 148 | <> 149 | public function isTypeOf(Assert $assert): void { 150 | $this->buildAssertion(Vector {})->isTypeOf(Vector::class); 151 | $assert->int($this->successCount)->eq(1); 152 | $assert->int($this->failEvents->count())->eq(0); 153 | } 154 | 155 | <> 156 | public function isNotTypeOf(Assert $assert): void { 157 | $this->buildAssertion(Vector {})->isTypeOf(Map::class); 158 | $assert->int($this->successCount)->eq(0); 159 | $assert->int($this->failEvents->count())->eq(1); 160 | } 161 | 162 | <> 163 | public function isSubTypeOf(Assert $assert): void { 164 | $this->buildAssertion(Vector {})->isTypeOf(\ConstVector::class); 165 | $assert->int($this->successCount)->eq(1); 166 | $assert->int($this->failEvents->count())->eq(0); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Util/Trace.php: -------------------------------------------------------------------------------- 1 | ?string, 11 | 'line' => ?int, 12 | 'function' => ?string, 13 | 'class' => ?string, 14 | ); 15 | 16 | class Trace { 17 | public static function fromScannedMethod( 18 | ScannedBasicClass $class, 19 | ScannedMethod $method, 20 | ): TraceItem { 21 | return self::buildItem( 22 | [ 23 | 'line' => Shapes::idx($method->getPosition(), 'line'), 24 | 'class' => $class->getName(), 25 | 'file' => $method->getFileName(), 26 | 'function' => $method->getName(), 27 | ], 28 | ); 29 | } 30 | 31 | public static function fromReflectionClass( 32 | \ReflectionClass $classMirror, 33 | ): TraceItem { 34 | return self::buildItem( 35 | [ 36 | 'line' => $classMirror->getStartLine(), 37 | 'class' => $classMirror->getName(), 38 | 'file' => $classMirror->getFileName(), 39 | ], 40 | ); 41 | } 42 | 43 | public static function fromReflectionMethod( 44 | \ReflectionMethod $methodMirror, 45 | ): TraceItem { 46 | return self::buildItem( 47 | [ 48 | 'line' => $methodMirror->getStartLine(), 49 | 'function' => $methodMirror->name, 50 | 'class' => $methodMirror->class, 51 | 'file' => $methodMirror->getFileName(), 52 | ], 53 | ); 54 | } 55 | 56 | public static function generate(): Vector { 57 | return 58 | self::convert((new Vector(debug_backtrace()))->removeKey(0)->toArray()); 59 | } 60 | 61 | public static function convert( 62 | array> $trace, 63 | ): Vector { 64 | return (new Vector($trace))->map($t ==> self::buildItem($t)); 65 | } 66 | 67 | public static function buildItem(array $item): TraceItem { 68 | return shape( 69 | 'line' => 70 | array_key_exists('line', $item) && is_int($item['line']) 71 | ? (int) $item['line'] 72 | : null, 73 | 'function' => 74 | array_key_exists('function', $item) && is_string($item['function']) 75 | ? (string) $item['function'] 76 | : null, 77 | 'class' => 78 | array_key_exists('class', $item) && is_string($item['class']) 79 | ? (string) $item['class'] 80 | : null, 81 | 'file' => 82 | array_key_exists('file', $item) && is_string($item['file']) 83 | ? (string) $item['file'] 84 | : null, 85 | ); 86 | } 87 | 88 | public static function findAssertionCallFromStack( 89 | Vector $trace, 90 | ): TraceItem { 91 | foreach ($trace as $idx => $item) { 92 | // Always switching classes when making an assertion 93 | if (($trace->containsKey($idx + 1) && 94 | $item['class'] === $trace->at($idx + 1)['class']) || 95 | $item['class'] === null) { 96 | continue; 97 | } 98 | 99 | // See if current item implements the assertion interface 100 | $implements = class_implements($item['class']); 101 | if (is_array($implements) && 102 | array_key_exists(Assertion::class, $implements)) { 103 | // Next item in the stack was the actual caller 104 | if ($trace->containsKey($idx + 1)) { 105 | return shape( 106 | 'line' => $trace->at($idx)['line'], 107 | 'function' => $trace->at($idx + 1)['function'], 108 | 'class' => $trace->at($idx + 1)['class'], 109 | 'file' => $trace->at($idx)['file'], 110 | ); 111 | } 112 | return self::emptyTraceItem(); 113 | } 114 | } 115 | return self::emptyTraceItem(); 116 | } 117 | 118 | public static function findTestMethodFromStack( 119 | Vector $trace, 120 | ): TraceItem { 121 | foreach ($trace as $idx => $item) { 122 | // Make sure we know the class and method names 123 | if ($item['class'] === null || $item['function'] === null) { 124 | continue; 125 | } 126 | try { 127 | $class = new \ReflectionClass($item['class']); 128 | $method = $class->getMethod((string) $item['function']); 129 | } catch (\ReflectionException $e) { 130 | // Either class or function were not defined 131 | continue; 132 | } 133 | 134 | // See if the class is a suite and the method is a test 135 | $classAttributes = new Map($class->getAttributes()); 136 | $methodAttributes = new Map($method->getAttributes()); 137 | 138 | if ($methodAttributes->get('Test') !== null) { 139 | // Found the marked test method 140 | return shape( 141 | 'line' => $trace->at($idx - 1)['line'], 142 | 'function' => $trace->at($idx)['function'], 143 | 'class' => $trace->at($idx)['class'], 144 | 'file' => $trace->at($idx - 1)['file'], 145 | ); 146 | } 147 | } 148 | 149 | return self::emptyTraceItem(); 150 | } 151 | 152 | public static function findAssertionCall(): TraceItem { 153 | return self::findAssertionCallFromStack(self::generate()); 154 | } 155 | 156 | public static function findTestMethod(): TraceItem { 157 | return self::findTestMethodFromStack(self::generate()); 158 | } 159 | 160 | private static function emptyTraceItem(): TraceItem { 161 | return shape( 162 | 'line' => null, 163 | 'function' => null, 164 | 'class' => null, 165 | 'file' => null, 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Report/Format/Cli.php: -------------------------------------------------------------------------------- 1 | line(PHP_EOL); 15 | $this->line($this->timeReport($status)); 16 | $this->line($this->testSummary($status)); 17 | $this->show($this->malformedReport($status)); 18 | $this->show($this->skipReport($status)); 19 | $this->show($this->errorReport($status)); 20 | $this->show($this->untestedExceptionReport($status)); 21 | } 22 | public function untestedExceptionReport(Summary $summary): string { 23 | $entries = Vector {}; 24 | foreach ($summary['untested exceptions'] as $e) { 25 | 26 | $message = 27 | 'Fatal exception thrown in '. 28 | $e->getFile(). 29 | ' on line '. 30 | $e->getLine(). 31 | '.'; 32 | 33 | $entries->add( 34 | implode( 35 | PHP_EOL, 36 | [ 37 | $message, 38 | 'Exception message:', 39 | $e->getMessage(), 40 | 'Trace:', 41 | $e->getTraceAsString(), 42 | ], 43 | ), 44 | ); 45 | } 46 | 47 | return PHP_EOL.implode(PHP_EOL, $entries).PHP_EOL; 48 | } 49 | 50 | public function testSummary(Summary $summary): string { 51 | return sprintf( 52 | 'Assertions: %s/%d Tests: %s/%d Failed: %s Skipped %s', 53 | $summary['success count'], 54 | $summary['assert count'], 55 | $summary['pass count'], 56 | $summary['test count'], 57 | $summary['fail count'], 58 | $summary['skip count'], 59 | ); 60 | } 61 | 62 | public function skipReport(Summary $summary): string { 63 | if ($summary['skip events']->isEmpty()) { 64 | return ''; 65 | } 66 | return 67 | PHP_EOL. 68 | 'Skipped tests:'. 69 | PHP_EOL. 70 | implode( 71 | PHP_EOL.PHP_EOL, 72 | $summary['skip events']->mapWithKey( 73 | ($idx, $e) ==> { 74 | return implode( 75 | PHP_EOL, 76 | [ 77 | '-*-*-*- Test Skip '.($idx + 1).' -*-*-*-', 78 | $this->buildMethodCall($e->testMethodTraceItem()), 79 | ' In file '.$e->testMethodTraceItem()['file'], 80 | $e->message(), 81 | ], 82 | ); 83 | }, 84 | ), 85 | ). 86 | PHP_EOL; 87 | 88 | } 89 | 90 | public function errorReport(Summary $summary): string { 91 | if ($summary['fail events']->isEmpty()) { 92 | return ''; 93 | } 94 | $report = ''; 95 | foreach ($summary['fail events'] as $idx => $e) { 96 | $assertionTraceItem = $e->assertionTraceItem(); 97 | $testTraceItem = $e->testMethodTraceItem(); 98 | $assertionCall = $this->buildMethodCall($assertionTraceItem); 99 | $testMethod = $this->buildMethodCall($testTraceItem); 100 | $report .= 101 | implode( 102 | PHP_EOL, 103 | [ 104 | '', 105 | '-*-*-*- Test Failure '.($idx + 1).' -*-*-*-', 106 | 'Test failure - '.$testMethod, 107 | 'Assertion failed in '.$assertionCall, 108 | 'On line '. 109 | $assertionTraceItem['line']. 110 | ' of '. 111 | $assertionTraceItem['file'], 112 | $e->getMessage(), 113 | ], 114 | ). 115 | PHP_EOL; 116 | } 117 | return $report.PHP_EOL; 118 | } 119 | 120 | private function timeReport(Summary $summary): string { 121 | $elapsedTime = $summary['end time'] - $summary['start time']; 122 | if ($elapsedTime < 0.000000001) { 123 | return 'Finished testing.'; 124 | } 125 | return sprintf('Finished testing in %.2f seconds.', $elapsedTime); 126 | } 127 | 128 | private function malformedReport(Summary $summary): string { 129 | if ($summary['malformed events']->isEmpty()) { 130 | return ''; 131 | } 132 | 133 | $report = 'Some test suites were malformed:'; 134 | 135 | foreach ($summary['malformed events'] as $idx => $event) { 136 | $report .= implode( 137 | PHP_EOL, 138 | [ 139 | PHP_EOL, 140 | '-*-*-*- Malformed Error '.($idx + 1).' -*-*-*-', 141 | $this->buildMethodCall($event->traceItem()), 142 | $this->buildLineReference($event->traceItem()), 143 | $event->message(), 144 | ], 145 | ); 146 | } 147 | 148 | return $report.PHP_EOL; 149 | } 150 | 151 | private function buildLineReference(TraceItem $item): string { 152 | $lineNumber = $item['line']; 153 | $fileName = $item['file']; 154 | $lineNumber = $lineNumber === null ? '??' : (string) $lineNumber; 155 | $fileName = $fileName === null ? 'Unknown file' : (string) $fileName; 156 | return 'On line '.$lineNumber.' in file '.$fileName; 157 | } 158 | 159 | private function buildMethodCall(TraceItem $item): string { 160 | $className = $item['class']; 161 | $methodName = $item['function']; 162 | if ($className === null) { 163 | $className = 'Unknown class'; 164 | } 165 | if ($methodName === null) { 166 | $methodName = 'Unknown method'; 167 | } 168 | $out = $className.'->'.$methodName.'()'; 169 | return $out; 170 | } 171 | 172 | private function line(string $message): void { 173 | fwrite($this->out, $message.PHP_EOL); 174 | } 175 | 176 | private function show(string $message): void { 177 | fwrite($this->out, $message); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Test/Runner.php: -------------------------------------------------------------------------------- 1 | $exceptionListeners = Vector {}; 26 | private Vector $failureListeners = Vector {}; 27 | private Vector $passListeners = Vector {}; 28 | private Vector $runEndListeners = Vector {}; 29 | private Vector $runStartListeners = Vector {}; 30 | private Vector $skipListeners = Vector {}; 31 | private Vector $successListeners = Vector {}; 32 | private Vector $suiteEndListeners = Vector {}; 33 | private Vector $suiteStartListeners = Vector {}; 34 | private Vector $testStartListeners = Vector {}; 35 | 36 | public function __construct( 37 | private (function(Vector, 38 | Vector, 39 | Vector, 40 | ): Assert) $assertBuilder, 41 | ) {} 42 | 43 | public function onFailure(FailureListener $l): this { 44 | $this->failureListeners->add($l); 45 | return $this; 46 | } 47 | 48 | public function onUncaughtException(ExceptionListener $l): this { 49 | $this->exceptionListeners->add($l); 50 | return $this; 51 | } 52 | 53 | public function onRunEnd(RunEndListener $l): this { 54 | $this->runEndListeners->add($l); 55 | return $this; 56 | } 57 | 58 | public function onRunStart(RunStartListener $l): this { 59 | $this->runStartListeners->add($l); 60 | return $this; 61 | } 62 | 63 | public function onSkip(SkipListener $l): this { 64 | $this->skipListeners->add($l); 65 | return $this; 66 | } 67 | 68 | public function onSuccess(SuccessListener $l): this { 69 | $this->successListeners->add($l); 70 | return $this; 71 | } 72 | 73 | public function onSuiteEnd(SuiteEndListener $l): this { 74 | $this->suiteEndListeners->add($l); 75 | return $this; 76 | } 77 | 78 | public function onSuiteStart(SuiteStartListener $l): this { 79 | $this->suiteStartListeners->add($l); 80 | return $this; 81 | } 82 | 83 | public function onTestStart(TestStartListener $l): this { 84 | $this->testStartListeners->add($l); 85 | return $this; 86 | } 87 | 88 | public function onPass(PassListener $l): this { 89 | $this->passListeners->add($l); 90 | return $this; 91 | } 92 | 93 | public function run(Vector $suites): void { 94 | 95 | // Throw an interruption after all other handlers 96 | $this->failureListeners->add( 97 | $failure ==> { 98 | throw new Interruption(); 99 | }, 100 | ); 101 | 102 | $this->skipListeners->add( 103 | $skip ==> { 104 | throw new Interruption(); 105 | }, 106 | ); 107 | $this->emitRunStart(); 108 | 109 | $builder = $this->assertBuilder; 110 | 111 | $awaitable = Asio\vw( 112 | $suites->map( 113 | async ($s) ==> { 114 | $this->emitSuiteStart(new SuiteStart($s->name())); 115 | await $s->up(); 116 | 117 | $testResult = await $s->run( 118 | $builder( 119 | $this->failureListeners, 120 | $this->skipListeners, 121 | $this->successListeners, 122 | ), 123 | () ==> { 124 | $this->emitPass(); 125 | }, 126 | $this->testStartListeners, 127 | ) |> Asio\wrap($$); 128 | 129 | await $s->down(); 130 | $this->emitSuiteEnd(); 131 | 132 | if ($testResult->isFailed()) { 133 | throw $testResult->getException(); 134 | } 135 | }, 136 | ), 137 | ); 138 | 139 | foreach (Asio\join($awaitable) as $result) { 140 | if ($result->isFailed()) { 141 | $this->emitException($result->getException()); 142 | } 143 | } 144 | 145 | $this->emitRunEnd(); 146 | } 147 | 148 | private function emitSuiteEnd(): void { 149 | foreach ($this->suiteEndListeners as $l) { 150 | $l(); 151 | } 152 | } 153 | 154 | private function emitSuiteStart(SuiteStart $e): void { 155 | foreach ($this->suiteStartListeners as $l) { 156 | $l($e); 157 | } 158 | } 159 | 160 | private function emitTestStart(TestStart $e): void { 161 | foreach ($this->testStartListeners as $l) { 162 | $l($e); 163 | } 164 | } 165 | 166 | private function emitRunEnd(): void { 167 | foreach ($this->runEndListeners as $l) { 168 | $l(); 169 | } 170 | } 171 | 172 | private function emitRunStart(): void { 173 | foreach ($this->runStartListeners as $l) { 174 | $l(); 175 | } 176 | } 177 | 178 | private function emitPass(): void { 179 | $event = Pass::fromCallStack(); 180 | foreach ($this->passListeners as $l) { 181 | $l($event); 182 | } 183 | } 184 | 185 | private function emitException(\Exception $e): void { 186 | foreach ($this->exceptionListeners as $l) { 187 | $l($e); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/Test/ValidParsing.php: -------------------------------------------------------------------------------- 1 | > 20 | public function validSuitesParseWithoutError(Assert $assert): void { 21 | foreach (self::$parsersBySuiteName as $parser) { 22 | $assert->bool($parser->errors()->isEmpty())->is(true); 23 | } 24 | } 25 | 26 | <> 27 | public function factoryParsing(Assert $assert): void { 28 | $factoryList = 29 | $this->parserFromSuiteName('ConstructorIsDefaultWithNoParams') 30 | ->factories(); 31 | 32 | $assert->int($factoryList->count())->eq(2); 33 | $assert->bool($factoryList->containsKey(''))->is(true); 34 | $assert->bool($factoryList->containsKey('named'))->is(true); 35 | $assert->string($factoryList->at(''))->is('__construct'); 36 | $assert->string($factoryList->at('named'))->is('factory'); 37 | 38 | $factoryList = 39 | $this->parserFromSuiteName('ConstructorIsDefaultWithParams') 40 | ->factories(); 41 | 42 | $assert->int($factoryList->count())->eq(2); 43 | $assert->bool($factoryList->containsKey(''))->is(true); 44 | $assert->bool($factoryList->containsKey('named'))->is(true); 45 | $assert->string($factoryList->at(''))->is('__construct'); 46 | $assert->string($factoryList->at('named'))->is('factory'); 47 | 48 | $factoryList = 49 | $this->parserFromSuiteName('ConstructorIsNotDefault')->factories(); 50 | $assert->bool($factoryList->containsKey(''))->is(true); 51 | $assert->string($factoryList->at(''))->is('factory'); 52 | 53 | $factoryList = $this->parserFromSuiteName('DerivedFactory')->factories(); 54 | $assert->bool($factoryList->containsKey(''))->is(true); 55 | $assert->string($factoryList->at(''))->is('factory'); 56 | 57 | $factoryList = $this->parserFromSuiteName('AbstractFactory')->factories(); 58 | $assert->int($factoryList->count())->eq(0); 59 | } 60 | 61 | <> 62 | public function setupParsing(Assert $assert): void { 63 | $this->updownParsing('Setup', $assert); 64 | } 65 | 66 | <> 67 | public function teardownParsing(Assert $assert): void { 68 | $this->updownParsing('TearDown', $assert); 69 | } 70 | 71 | private function updownParsing(string $type, Assert $assert): void { 72 | $parser = $this->parserFromSuiteName($type); 73 | $suiteUp = $type === 'Setup' ? $parser->suiteUp() : $parser->suiteDown(); 74 | $assert->container($suiteUp) 75 | ->containsOnly(['suiteOnly', 'both', 'nonRequiredParam']); 76 | 77 | $expectedTestUp = Vector {}; 78 | $testUp = $type === 'Setup' ? $parser->testUp() : $parser->testDown(); 79 | $assert->container($testUp) 80 | ->containsOnly(['both', 'testOnlyExplicit', 'testOnlyImplicit']); 81 | } 82 | 83 | <> 84 | public function testParsing(Assert $assert): void { 85 | $parser = $this->parserFromSuiteName('Test'); 86 | 87 | $tests = Map::fromItems( 88 | $parser->tests()->map($test ==> Pair {$test['method'], $test}), 89 | ); 90 | 91 | $expectedTestNames = Vector { 92 | 'defaultSuiteProvider', 93 | 'namedSuiteProvider', 94 | 'staticTest', 95 | 'skippedTest', 96 | }; 97 | 98 | $assert->container($tests->keys())->containsOnly($expectedTestNames); 99 | 100 | $defaultProvider = $tests->at('defaultSuiteProvider'); 101 | $assert->string($defaultProvider['factory name'])->is(''); 102 | $assert->bool($defaultProvider['skip'])->is(false); 103 | 104 | $defaultProvider = $tests->at('namedSuiteProvider'); 105 | $assert->string($defaultProvider['factory name'])->is('named'); 106 | $assert->bool($defaultProvider['skip'])->is(false); 107 | 108 | $defaultProvider = $tests->at('skippedTest'); 109 | $assert->string($defaultProvider['factory name'])->is(''); 110 | $assert->bool($defaultProvider['skip'])->is(true); 111 | } 112 | 113 | <> 114 | public function inheritedTests(Assert $assert): void { 115 | $parser = $this->parserFromSuiteName('BaseSuite'); 116 | $testNames = $parser->tests()->map($test ==> $test['method']); 117 | $assert->container($testNames) 118 | ->containsOnly(['testInsideBaseSuite', 'testInsideAbstractSuite']); 119 | 120 | $parser = $this->parserFromSuiteName('HasExternalSuite'); 121 | $testNames = $parser->tests()->map($test ==> $test['method']); 122 | $assert->container($testNames)->containsOnly( 123 | ['testInsideBaseSuite', 'testInsideAbstractSuite', 'testInsideTrait'], 124 | ); 125 | } 126 | 127 | <> 128 | public function abstractClassesDoNotHaveTests(Assert $assert): void { 129 | $parser = $this->parserFromSuiteName('AbstractSuite'); 130 | 131 | $assert->int($parser->tests()->count())->eq(0); 132 | } 133 | 134 | <> 135 | public function validDataProvider(Assert $assert): void { 136 | $tests = Map::fromItems( 137 | $this->parserFromSuiteName('Data') 138 | ->tests() 139 | ->map($test ==> Pair {$test['method'], $test}), 140 | ); 141 | $assert->container($tests->keys()) 142 | ->containsOnly(['consumesVector', 'consumesMap', 'consumesString']); 143 | 144 | $assert->string($tests->at('consumesVector')['data provider']) 145 | ->is('vectorProvider'); 146 | $assert->string($tests->at('consumesMap')['data provider']) 147 | ->is('mapProvider'); 148 | $assert->string($tests->at('consumesString')['data provider']) 149 | ->is('stringProvider'); 150 | } 151 | 152 | <> 153 | public function asyncDataProvider(Assert $assert): void { 154 | $tests = Map::fromItems( 155 | $this->parserFromSuiteName('AsyncData') 156 | ->tests() 157 | ->map($test ==> Pair {$test['method'], $test}), 158 | ); 159 | $assert->container($tests->keys())->containsOnly(['asyncConsumer']); 160 | 161 | $assert->string($tests->at('asyncConsumer')['data provider']) 162 | ->is('asyncDataProvider'); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/Test/Runner.php: -------------------------------------------------------------------------------- 1 | $failureListeners = Vector {}; 19 | private Vector $skipListeners = Vector {}; 20 | private Vector $successListeners = Vector {}; 21 | private Vector $asserts = Vector {}; 22 | private Vector<\Exception> $uncaughtExceptions = Vector {}; 23 | private Runner $runner; 24 | private int $testsPassed = 0; 25 | 26 | public function __construct() { 27 | $this->runner = new Runner(inst_meth($this, 'assertionBuilder')); 28 | } 29 | 30 | public function assertionBuilder( 31 | Vector $failures, 32 | Vector $skips, 33 | Vector $successes, 34 | ): Assert { 35 | $this->failureListeners->addAll($failures); 36 | $this->skipListeners->addAll($skips); 37 | $this->successListeners->addAll($successes); 38 | 39 | $assert = new \HackPack\HackUnit\Assert($failures, $skips, $successes); 40 | 41 | $this->asserts->add($assert); 42 | return $assert; 43 | } 44 | 45 | <> 46 | public function allSuitesAreRun(Assert $assert): void { 47 | $suites = Vector {}; 48 | for ($i = 0; $i < 3; $i++) { 49 | $suites->add(new SpySuite()); 50 | } 51 | 52 | $this->runner->run($suites); 53 | 54 | foreach ($suites as $index => $suite) { 55 | 56 | // Tell the typechecker we know what's going on 57 | invariant($suite instanceof SpySuite, ''); 58 | 59 | // Ensure each suite is run once 60 | $assert->int($suite->counts['up'])->eq(1); 61 | $assert->int($suite->counts['run'])->eq(1); 62 | $assert->int($suite->counts['down'])->eq(1); 63 | 64 | // Ensure the suites are passed the generated assert objects 65 | $assert->mixed($suite->asserts->at(0)) 66 | ->identicalTo($this->asserts->at($index)); 67 | } 68 | } 69 | 70 | <> 71 | public function suitesAreRunAsync(Assert $assert): void { 72 | // 0.01 second per suite 73 | $sleepTime = 10000; 74 | $suites = Vector {}; 75 | for ($i = 0; $i < 3; $i++) { 76 | $suites->add(new AsyncSuite($sleepTime)); 77 | } 78 | 79 | $start = microtime(true); 80 | $this->runner->run($suites); 81 | $end = microtime(true); 82 | 83 | // Convert delta time to microseconds 84 | $deltaTime = ($end - $start) * 1000000; 85 | 86 | // Total run time should be less than twice the sleep time 87 | // since all are running in async 88 | $assert->float($deltaTime)->lt(2.0 * $sleepTime); 89 | } 90 | 91 | <> 92 | public function interruptionEventHandlersAreAdded(Assert $assert): void { 93 | $this->runner->run(Vector {new SpySuite()}); 94 | 95 | $assert->int($this->failureListeners->count())->eq(1); 96 | $assert->int($this->skipListeners->count())->eq(1); 97 | $assert->int($this->successListeners->count())->eq(0); 98 | 99 | // Ensure the handlers called throw an Interruption exception 100 | $assert->whenCalled( 101 | () ==> { 102 | $listener = $this->failureListeners->at(0); 103 | $listener(Failure::fromCallStack('fake failure')); 104 | }, 105 | )->willThrowClass(Interruption::class); 106 | 107 | $assert->whenCalled( 108 | () ==> { 109 | $listener = $this->skipListeners->at(0); 110 | $listener(Skip::fromCallStack('fake failure')); 111 | }, 112 | )->willThrowClass(Interruption::class); 113 | } 114 | 115 | <> 116 | public function listenersArePassedToAssertBuilder(Assert $assert): void { 117 | $failure = ($event) ==> { 118 | }; 119 | $skip = ($event) ==> { 120 | }; 121 | $success = ($event) ==> { 122 | }; 123 | 124 | $this->runner->onFailure($failure); 125 | $this->runner->onSkip($skip); 126 | $this->runner->onSuccess($success); 127 | 128 | $this->runner->run(Vector {new SpySuite()}); 129 | 130 | $assert->int($this->failureListeners->count())->eq(2); 131 | $assert->int($this->skipListeners->count())->eq(2); 132 | $assert->int($this->successListeners->count())->eq(1); 133 | 134 | // This also implicitly tests that the interruption handlers are added at the end 135 | $assert->mixed($this->failureListeners->at(0))->identicalTo($failure); 136 | $assert->mixed($this->skipListeners->at(0))->identicalTo($skip); 137 | $assert->mixed($this->successListeners->at(0))->identicalTo($success); 138 | } 139 | 140 | <> 141 | public function testPassListenersAreRun(Assert $assert): void { 142 | $this->runner->onPass( 143 | ($e) ==> { 144 | $this->testsPassed++; 145 | }, 146 | ); 147 | 148 | $suite = new SpySuite(); 149 | $this->runner->run(Vector {$suite}); 150 | 151 | $assert->int($suite->passCallbacks->count())->eq(1); 152 | $passCallback = $suite->passCallbacks->at(0); 153 | 154 | $assert->int($this->testsPassed)->eq(0); 155 | $passCallback(); 156 | $assert->int($this->testsPassed)->eq(1); 157 | } 158 | 159 | <> 160 | public function unexpectedExceptionIsHandled(Assert $assert): void { 161 | $this->runner->onUncaughtException( 162 | $e ==> { 163 | $this->uncaughtExceptions->add($e); 164 | }, 165 | ); 166 | 167 | $exception = new \Exception('With a message'); 168 | $suite = new SpySuite( 169 | () ==> { 170 | throw $exception; 171 | }, 172 | ); 173 | 174 | $assert->whenCalled( 175 | () ==> { 176 | $this->runner->run(Vector {$suite}); 177 | }, 178 | )->willNotThrow(); 179 | 180 | $assert->container($this->uncaughtExceptions) 181 | ->containsOnly(Vector {$exception}); 182 | $assert->int($suite->counts['up'])->eq(1); 183 | $assert->int($suite->counts['run'])->eq(1); 184 | $assert->int($suite->counts['down'])->eq(1); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Assertion/ContainerAssertion.php: -------------------------------------------------------------------------------- 1 | 13 | implements \HackPack\HackUnit\Contract\Assertion\ContainerAssertion { 14 | 15 | use FailureEmitter; 16 | use SuccessEmitter; 17 | 18 | private bool $negate = false; 19 | private \ConstVector $context; 20 | 21 | public function __construct( 22 | Container $context, 23 | Vector $failureListeners, 24 | Vector $successListeners, 25 | ) { 26 | $this->context = new Vector($context); 27 | $this->setFailureListeners($failureListeners); 28 | $this->setSuccessListeners($successListeners); 29 | } 30 | 31 | public function not(): this { 32 | $this->negate = true; 33 | return $this; 34 | } 35 | 36 | public function contains( 37 | Tval $expected, 38 | ?(function(Tval, Tval): bool) $comparitor = null, 39 | ): void { 40 | 41 | if ($this->context->isEmpty()) { 42 | if ($this->negate) { 43 | $this->emitSuccess(); 44 | return; 45 | } 46 | $this->emitFailure(Failure::fromCallStack('The Container is empty.')); 47 | return; 48 | } 49 | 50 | if ($comparitor === null) { 51 | $comparitor = self::identityComparitor(); 52 | } 53 | 54 | foreach ($this->context as $value) { 55 | if ($comparitor($expected, $value)) { 56 | 57 | if ($this->negate) { 58 | $this->emitFailure( 59 | Failure::fromCallStack( 60 | 'Expected Container to not contain '. 61 | var_export($expected, true), 62 | ), 63 | ); 64 | return; 65 | } 66 | 67 | $this->emitSuccess(); 68 | return; 69 | } 70 | } 71 | 72 | if ($this->negate) { 73 | $this->emitSuccess(); 74 | return; 75 | } 76 | 77 | $this->emitFailure( 78 | Failure::fromCallStack( 79 | 'Expected Container to contain '.var_export($expected, true), 80 | ), 81 | ); 82 | } 83 | 84 | public function containsAll( 85 | Container $expected, 86 | ?(function(Tval, Tval): bool) $comparitor = null, 87 | ): void { 88 | if ($this->context->isEmpty()) { 89 | if ($this->negate) { 90 | $this->emitSuccess(); 91 | return; 92 | } 93 | $this->emitFailure(Failure::fromCallStack('The Container is empty.')); 94 | return; 95 | } 96 | 97 | if ($comparitor === null) { 98 | $comparitor = self::identityComparitor(); 99 | } 100 | 101 | foreach ($expected as $other) { 102 | 103 | $otherIsContained = array_reduce( 104 | $this->context->toArray(), 105 | ($result, $contextVal) ==> $result || 106 | $comparitor($contextVal, $other), 107 | false, 108 | ); 109 | 110 | if ($otherIsContained) { 111 | continue; 112 | } 113 | 114 | if ($this->negate) { 115 | $this->emitSuccess(); 116 | return; 117 | } 118 | $this->emitFailure( 119 | Failure::fromCallStack( 120 | 'Container expected to contain '.var_export($other, true), 121 | ), 122 | ); 123 | return; 124 | } 125 | 126 | if ($this->negate) { 127 | $this->emitFailure( 128 | Failure::fromCallStack( 129 | 'Container expected to not contain all of the given list.', 130 | ), 131 | ); 132 | return; 133 | } 134 | $this->emitSuccess(); 135 | 136 | } 137 | 138 | public function containsAny( 139 | Container $expected, 140 | ?(function(Tval, Tval): bool) $comparitor = null, 141 | ): void { 142 | if ($this->context->isEmpty()) { 143 | if ($this->negate) { 144 | $this->emitSuccess(); 145 | return; 146 | } 147 | $this->emitFailure(Failure::fromCallStack('The Container is empty.')); 148 | return; 149 | } 150 | 151 | if ($comparitor === null) { 152 | $comparitor = self::identityComparitor(); 153 | } 154 | 155 | foreach ($expected as $otherValue) { 156 | 157 | $otherIsContained = array_reduce( 158 | $this->context->toArray(), 159 | ($result, $contextVal) ==> $result || 160 | $comparitor($contextVal, $otherValue), 161 | false, 162 | ); 163 | 164 | if ($otherIsContained) { 165 | if ($this->negate) { 166 | $this->emitFailure( 167 | Failure::fromCallStack( 168 | 'Container expected to not contain '. 169 | var_export($otherValue, true), 170 | ), 171 | ); 172 | return; 173 | } 174 | $this->emitSuccess(); 175 | return; 176 | } 177 | } 178 | 179 | if ($this->negate) { 180 | $this->emitSuccess(); 181 | return; 182 | } 183 | 184 | $this->emitFailure( 185 | Failure::fromCallStack( 186 | 'Container expected to contain at least one item from the list.', 187 | ), 188 | ); 189 | } 190 | 191 | public function containsOnly( 192 | Container $expected, 193 | ?(function(Tval, Tval): bool) $comparitor = null, 194 | ): void { 195 | $expected = new Vector($expected); 196 | 197 | if ($this->context->count() !== $expected->count()) { 198 | if ($this->negate) { 199 | $this->emitSuccess(); 200 | return; 201 | } 202 | $message = 203 | $this->context->count() > $expected->count() 204 | ? 'Container contains more elements than expected.' 205 | : 'Container contains fewer elements than expected.'; 206 | $this->emitFailure(Failure::fromCallStack($message)); 207 | return; 208 | } 209 | 210 | if ($this->context->isEmpty()) { 211 | if ($this->negate) { 212 | $this->emitFailure( 213 | Failure::fromCallStack('Traverseable expected to not be empty.'), 214 | ); 215 | return; 216 | } 217 | $this->emitSuccess(); 218 | return; 219 | } 220 | 221 | $this->containsAll($expected, $comparitor); 222 | 223 | } 224 | 225 | public function isEmpty(): void { 226 | if ($this->context->isEmpty()) { 227 | if ($this->negate) { 228 | $this->emitFailure( 229 | Failure::fromCallStack('Container expected not to be empty.'), 230 | ); 231 | return; 232 | } 233 | $this->emitSuccess(); 234 | return; 235 | } 236 | 237 | if ($this->negate) { 238 | $this->emitSuccess(); 239 | return; 240 | } 241 | $this->emitFailure( 242 | Failure::fromCallStack('Container expected to be empty.'), 243 | ); 244 | } 245 | 246 | <<__Memoize>> 247 | private static function identityComparitor(): (function(T, T): bool) { 248 | return ($a, $b) ==> $a === $b; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /test/Assertion/NumericAssertion.php: -------------------------------------------------------------------------------- 1 | { 14 | return new NumericAssertion( 15 | $context, 16 | $this->failListeners(), 17 | $this->successListeners(), 18 | ); 19 | } 20 | 21 | <> 22 | public function expectEqualIsEqual(Assert $assert): void { 23 | $a = $this->makeAssertion(1); 24 | $a->eq(1); 25 | 26 | $assert->int($this->successCount)->eq(1); 27 | $assert->int($this->failEvents->count())->eq(0); 28 | } 29 | 30 | <> 31 | public function expectEqualIsGreater(Assert $assert): void { 32 | $line = __LINE__ + 2; 33 | $a = $this->makeAssertion(1); 34 | $a->eq(0); 35 | 36 | $assert->int($this->successCount)->eq(0); 37 | $assert->int($this->failEvents->count())->eq(1); 38 | 39 | $this->checkTrace( 40 | $this->failEvents->at(0)->assertionTraceItem(), 41 | shape( 42 | 'line' => $line, 43 | 'function' => __FUNCTION__, 44 | 'class' => __CLASS__, 45 | 'file' => __FILE__, 46 | ), 47 | $assert, 48 | ); 49 | } 50 | 51 | <> 52 | public function expectEqualIsLess(Assert $assert): void { 53 | $line = __LINE__ + 2; 54 | $a = $this->makeAssertion(1); 55 | $a->eq(2); 56 | 57 | $assert->int($this->successCount)->eq(0); 58 | $assert->int($this->failEvents->count())->eq(1); 59 | 60 | $this->checkTrace( 61 | $this->failEvents->at(0)->assertionTraceItem(), 62 | shape( 63 | 'line' => $line, 64 | 'function' => __FUNCTION__, 65 | 'class' => __CLASS__, 66 | 'file' => __FILE__, 67 | ), 68 | $assert, 69 | ); 70 | } 71 | 72 | <> 73 | public function expectGreaterIsEqual(Assert $assert): void { 74 | $line = __LINE__ + 2; 75 | $a = $this->makeAssertion(1); 76 | $a->gt(1); 77 | 78 | $assert->int($this->successCount)->eq(0); 79 | $assert->int($this->failEvents->count())->eq(1); 80 | 81 | $this->checkTrace( 82 | $this->failEvents->at(0)->assertionTraceItem(), 83 | shape( 84 | 'line' => $line, 85 | 'function' => __FUNCTION__, 86 | 'class' => __CLASS__, 87 | 'file' => __FILE__, 88 | ), 89 | $assert, 90 | ); 91 | } 92 | 93 | <> 94 | public function expectGreaterIsGreater(Assert $assert): void { 95 | $a = $this->makeAssertion(1); 96 | $a->gt(0); 97 | 98 | $assert->int($this->successCount)->eq(1); 99 | $assert->int($this->failEvents->count())->eq(0); 100 | } 101 | 102 | <> 103 | public function expectGreaterIsLess(Assert $assert): void { 104 | $line = __LINE__ + 2; 105 | $a = $this->makeAssertion(1); 106 | $a->gt(2); 107 | 108 | $assert->int($this->successCount)->eq(0); 109 | $assert->int($this->failEvents->count())->eq(1); 110 | 111 | $this->checkTrace( 112 | $this->failEvents->at(0)->assertionTraceItem(), 113 | shape( 114 | 'line' => $line, 115 | 'function' => __FUNCTION__, 116 | 'class' => __CLASS__, 117 | 'file' => __FILE__, 118 | ), 119 | $assert, 120 | ); 121 | } 122 | 123 | <> 124 | public function expectGreaterOrEqualIsEqual(Assert $assert): void { 125 | $a = $this->makeAssertion(1); 126 | $a->gte(1); 127 | 128 | $assert->int($this->successCount)->eq(1); 129 | $assert->int($this->failEvents->count())->eq(0); 130 | } 131 | 132 | <> 133 | public function expectGreaterOrEqualIsGreater(Assert $assert): void { 134 | $a = $this->makeAssertion(1); 135 | $a->gte(0); 136 | 137 | $assert->int($this->successCount)->eq(1); 138 | $assert->int($this->failEvents->count())->eq(0); 139 | } 140 | 141 | <> 142 | public function expectGreaterOrEqualIsLess(Assert $assert): void { 143 | $line = __LINE__ + 2; 144 | $a = $this->makeAssertion(1); 145 | $a->gte(2); 146 | 147 | $assert->int($this->successCount)->eq(0); 148 | $assert->int($this->failEvents->count())->eq(1); 149 | 150 | $this->checkTrace( 151 | $this->failEvents->at(0)->assertionTraceItem(), 152 | shape( 153 | 'line' => $line, 154 | 'function' => __FUNCTION__, 155 | 'class' => __CLASS__, 156 | 'file' => __FILE__, 157 | ), 158 | $assert, 159 | ); 160 | } 161 | 162 | <> 163 | public function expectLessIsEqual(Assert $assert): void { 164 | $line = __LINE__ + 2; 165 | $a = $this->makeAssertion(1); 166 | $a->lt(1); 167 | 168 | $assert->int($this->successCount)->eq(0); 169 | $assert->int($this->failEvents->count())->eq(1); 170 | 171 | $this->checkTrace( 172 | $this->failEvents->at(0)->assertionTraceItem(), 173 | shape( 174 | 'line' => $line, 175 | 'function' => __FUNCTION__, 176 | 'class' => __CLASS__, 177 | 'file' => __FILE__, 178 | ), 179 | $assert, 180 | ); 181 | } 182 | 183 | <> 184 | public function expectLessIsGreater(Assert $assert): void { 185 | $line = __LINE__ + 2; 186 | $a = $this->makeAssertion(1); 187 | $a->lt(0); 188 | 189 | $assert->int($this->successCount)->eq(0); 190 | $assert->int($this->failEvents->count())->eq(1); 191 | 192 | $this->checkTrace( 193 | $this->failEvents->at(0)->assertionTraceItem(), 194 | shape( 195 | 'line' => $line, 196 | 'function' => __FUNCTION__, 197 | 'class' => __CLASS__, 198 | 'file' => __FILE__, 199 | ), 200 | $assert, 201 | ); 202 | } 203 | 204 | <> 205 | public function expectLessIsLess(Assert $assert): void { 206 | $a = $this->makeAssertion(1); 207 | $a->lt(2); 208 | 209 | $assert->int($this->successCount)->eq(1); 210 | $assert->int($this->failEvents->count())->eq(0); 211 | } 212 | 213 | <> 214 | public function expectLessOrEqualIsEqual(Assert $assert): void { 215 | $a = $this->makeAssertion(1); 216 | $a->lte(1); 217 | 218 | $assert->int($this->successCount)->eq(1); 219 | $assert->int($this->failEvents->count())->eq(0); 220 | } 221 | 222 | <> 223 | public function expectLessOrEqualIsGreater(Assert $assert): void { 224 | $line = __LINE__ + 2; 225 | $a = $this->makeAssertion(1); 226 | $a->lte(0); 227 | 228 | $assert->int($this->successCount)->eq(0); 229 | $assert->int($this->failEvents->count())->eq(1); 230 | 231 | $this->checkTrace( 232 | $this->failEvents->at(0)->assertionTraceItem(), 233 | shape( 234 | 'line' => $line, 235 | 'function' => __FUNCTION__, 236 | 'class' => __CLASS__, 237 | 'file' => __FILE__, 238 | ), 239 | $assert, 240 | ); 241 | } 242 | 243 | <> 244 | public function expectLessOrEqualIsLess(Assert $assert): void { 245 | $a = $this->makeAssertion(1); 246 | $a->lte(2); 247 | 248 | $assert->int($this->successCount)->eq(1); 249 | $assert->int($this->failEvents->count())->eq(0); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/Assertion/KeyedContainerAssertion.php: -------------------------------------------------------------------------------- 1 | implements IAssertion { 16 | 17 | use FailureEmitter; 18 | use SuccessEmitter; 19 | 20 | private bool $negate = false; 21 | private \ConstMap $context; 22 | 23 | public function __construct( 24 | KeyedContainer $context, 25 | Vector $failureListeners, 26 | Vector $successListeners, 27 | ) { 28 | $this->context = new Map($context); 29 | $this->setFailureListeners($failureListeners); 30 | $this->setSuccessListeners($successListeners); 31 | } 32 | 33 | public function not(): this { 34 | $this->negate = true; 35 | return $this; 36 | } 37 | 38 | public function containsKey(Tkey $expected): void { 39 | if ($this->context->containsKey($expected)) { 40 | if ($this->negate) { 41 | $this->emitFailure( 42 | Failure::fromCallStack( 43 | 'Expected Keyed Container to not have key '. 44 | var_export($expected, true), 45 | ), 46 | ); 47 | return; 48 | } 49 | $this->emitSuccess(); 50 | return; 51 | } 52 | 53 | if ($this->negate) { 54 | $this->emitSuccess(); 55 | return; 56 | } 57 | 58 | $this->emitFailure( 59 | Failure::fromCallStack( 60 | 'Expected Keyed Container to have key '.var_export($expected, true), 61 | ), 62 | ); 63 | return; 64 | } 65 | 66 | public function contains( 67 | Tkey $key, 68 | Tval $val, 69 | ?(function(Tkey, Tval, Tval): bool) $comparitor = null, 70 | ): void { 71 | if ($comparitor === null) { 72 | $comparitor = self::identityComparitor(); 73 | } 74 | 75 | if (!$this->context->containsKey($key)) { 76 | if ($this->negate) { 77 | $this->emitSuccess(); 78 | return; 79 | } 80 | $this->emitFailure( 81 | Failure::fromCallStack( 82 | 'Expected Keyed Container to have key '.var_export($key, true), 83 | ), 84 | ); 85 | return; 86 | } 87 | 88 | if ($comparitor($key, $this->context->at($key), $val)) { 89 | if ($this->negate) { 90 | $this->emitFailure( 91 | Failure::fromCallStack( 92 | 'Expected Keyed Container to not contain a matching value at key '. 93 | var_export($key, true), 94 | ), 95 | ); 96 | return; 97 | } 98 | $this->emitSuccess(); 99 | return; 100 | } 101 | 102 | if ($this->negate) { 103 | $this->emitSuccess(); 104 | return; 105 | } 106 | 107 | $this->emitFailure( 108 | Failure::fromCallStack( 109 | 'Expected Keyed Container to contain a matching value at key '. 110 | var_export($key, true), 111 | ), 112 | ); 113 | } 114 | 115 | public function containsAll( 116 | KeyedContainer $expected, 117 | ?(function(Tkey, Tval, Tval): bool) $comparitor = null, 118 | ): void { 119 | if ($comparitor === null) { 120 | $comparitor = self::identityComparitor(); 121 | } 122 | $filtered = 123 | (new Map($expected))->filterWithKey( 124 | ($k, $v) ==> { 125 | if ($this->context->containsKey($k)) { 126 | return !$comparitor($k, $this->context->at($k), $v); 127 | } 128 | return true; 129 | }, 130 | ); 131 | 132 | if ($filtered->count() === 0) { 133 | if ($this->negate) { 134 | $this->emitFailure( 135 | Failure::fromCallStack( 136 | 'Expected Keyed Container to not contain all elements of given list.', 137 | ), 138 | ); 139 | return; 140 | } 141 | $this->emitSuccess(); 142 | return; 143 | } 144 | 145 | if ($this->negate) { 146 | $this->emitSuccess(); 147 | return; 148 | } 149 | $this->emitFailure( 150 | Failure::fromCallStack( 151 | 'Expected Keyed Container to contain all elements of given list.', 152 | ), 153 | ); 154 | } 155 | 156 | public function containsOnly( 157 | KeyedContainer $expected, 158 | ?(function(Tkey, Tval, Tval): bool) $comparitor = null, 159 | ): void { 160 | $comparitor = 161 | $comparitor === null ? self::identityComparitor() : $comparitor; 162 | $filter = ($a, $b) ==> { 163 | 164 | return $a->filterWithKey( 165 | ($k, $v) ==> { 166 | if ($b->containsKey($k)) { 167 | return !$comparitor($k, $b->at($k), $v); 168 | } 169 | return true; 170 | }, 171 | ); 172 | }; 173 | 174 | $expected = new Map($expected); 175 | $filteredContext = $filter($this->context, $expected); 176 | $filteredExpected = $filter($expected, $this->context); 177 | 178 | if ($filteredContext->isEmpty() && $filteredExpected->isEmpty()) { 179 | if ($this->negate) { 180 | $this->emitFailure( 181 | Failure::fromCallStack( 182 | 'Expected Keyed Container to not contain only the elements of the given list', 183 | ), 184 | ); 185 | return; 186 | } 187 | $this->emitSuccess(); 188 | return; 189 | } 190 | 191 | if ($this->negate) { 192 | $this->emitSuccess(); 193 | return; 194 | } 195 | 196 | $this->emitFailure( 197 | Failure::fromCallStack( 198 | 'Expected Keyed Container to contain only the elements of the given list', 199 | ), 200 | ); 201 | } 202 | 203 | public function containsAny( 204 | KeyedContainer $expected, 205 | ?(function(Tkey, Tval, Tval): bool) $comparitor = null, 206 | ): void { 207 | if ($comparitor === null) { 208 | $comparitor = self::identityComparitor(); 209 | } 210 | 211 | foreach ($expected as $expectedKey => $expectedValue) { 212 | if (!$this->context->containsKey($expectedKey)) { 213 | continue; 214 | } 215 | 216 | if ($comparitor( 217 | $expectedKey, 218 | $this->context->at($expectedKey), 219 | $expectedValue, 220 | )) { 221 | if ($this->negate) { 222 | $this->emitFailure( 223 | Failure::fromCallStack( 224 | 'Expected Keyed Container to not contain any elements of the given list.', 225 | ), 226 | ); 227 | return; 228 | } 229 | $this->emitSuccess(); 230 | return; 231 | } 232 | } 233 | 234 | if ($this->negate) { 235 | $this->emitSuccess(); 236 | return; 237 | } 238 | $this->emitFailure( 239 | Failure::fromCallStack( 240 | 'Expected Keyed Container to contain any elements of the given list.', 241 | ), 242 | ); 243 | } 244 | 245 | <<__Memoize>> 246 | private static function identityComparitor( 247 | ): (function(Tkey, Tval, Tval): bool) { 248 | return ($key, $a, $b) ==> $a === $b; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /test/Assertion/CallableAssertion.php: -------------------------------------------------------------------------------- 1 | { 20 | }, 21 | $this->failListeners(), 22 | $this->successListeners(), 23 | ); 24 | } 25 | 26 | private function buildThrowingAssertion(): CallableAssertion { 27 | return new CallableAssertion( 28 | () ==> { 29 | throw new TestException(self::message); 30 | }, 31 | $this->failListeners(), 32 | $this->successListeners(), 33 | ); 34 | } 35 | 36 | <> 37 | public function expectedMissingException(Assert $assert): void { 38 | $this->buildNonThrowingAssertion()->willNotThrow(); 39 | 40 | $assert->int($this->successCount)->eq(1); 41 | $assert->int($this->failEvents->count())->eq(0); 42 | } 43 | 44 | <> 45 | public function unexpectedException(Assert $assert): void { 46 | $this->buildThrowingAssertion()->willNotThrow(); 47 | 48 | $assert->int($this->successCount)->eq(0); 49 | $assert->int($this->failEvents->count())->eq(1); 50 | } 51 | 52 | <> 53 | public function expectedException(Assert $assert): void { 54 | $this->buildThrowingAssertion()->willThrow(); 55 | 56 | $assert->int($this->successCount)->eq(1); 57 | $assert->int($this->failEvents->count())->eq(0); 58 | } 59 | 60 | <> 61 | public function expectedExceptionWithMessage(Assert $assert): void { 62 | $this->buildThrowingAssertion()->willThrowMessage(self::message); 63 | 64 | $assert->int($this->successCount)->eq(1); 65 | $assert->int($this->failEvents->count())->eq(0); 66 | } 67 | 68 | <> 69 | public function expectedExceptionWithMessageContaining( 70 | Assert $assert, 71 | ): void { 72 | $this->buildThrowingAssertion() 73 | ->willThrowMessageContaining(self::message); 74 | $this->buildThrowingAssertion() 75 | ->willThrowMessageContaining(self::subMessage); 76 | 77 | $assert->int($this->successCount)->eq(2); 78 | $assert->int($this->failEvents->count())->eq(0); 79 | } 80 | 81 | <> 82 | public function expectedExceptionClass(Assert $assert): void { 83 | $this->buildThrowingAssertion()->willThrowClass(self::exceptionClass); 84 | 85 | $assert->int($this->successCount)->eq(1); 86 | $assert->int($this->failEvents->count())->eq(0); 87 | } 88 | 89 | <> 90 | public function expectedExceptionClassWithMessage(Assert $assert): void { 91 | $this->buildThrowingAssertion() 92 | ->willThrowClassWithMessage(self::exceptionClass, self::message); 93 | 94 | $assert->int($this->successCount)->eq(1); 95 | $assert->int($this->failEvents->count())->eq(0); 96 | } 97 | 98 | <> 99 | public function expectedExceptionClassWithMessageContaining( 100 | Assert $assert, 101 | ): void { 102 | $this->buildThrowingAssertion()->willThrowClassWithMessageContaining( 103 | self::exceptionClass, 104 | self::message, 105 | ); 106 | $this->buildThrowingAssertion()->willThrowClassWithMessageContaining( 107 | self::exceptionClass, 108 | self::subMessage, 109 | ); 110 | 111 | $assert->int($this->successCount)->eq(2); 112 | $assert->int($this->failEvents->count())->eq(0); 113 | } 114 | 115 | <> 116 | public function missingException(Assert $assert): void { 117 | $this->buildNonThrowingAssertion()->willThrow(); 118 | 119 | $assert->int($this->successCount)->eq(0); 120 | $assert->int($this->failEvents->count())->eq(1); 121 | } 122 | 123 | <> 124 | public function missingExceptionWithMessage(Assert $assert): void { 125 | $this->buildNonThrowingAssertion()->willThrowMessage(self::message); 126 | 127 | $assert->int($this->successCount)->eq(0); 128 | $assert->int($this->failEvents->count())->eq(1); 129 | } 130 | 131 | <> 132 | public function missingExceptionClass(Assert $assert): void { 133 | $this->buildNonThrowingAssertion()->willThrowClass(self::class); 134 | 135 | $assert->int($this->successCount)->eq(0); 136 | $assert->int($this->failEvents->count())->eq(1); 137 | } 138 | 139 | <> 140 | public function missingExceptionClassWithMessage(Assert $assert): void { 141 | $this->buildNonThrowingAssertion() 142 | ->willThrowClassWithMessage(self::class, self::message); 143 | 144 | $assert->int($this->successCount)->eq(0); 145 | $assert->int($this->failEvents->count())->eq(1); 146 | } 147 | 148 | <> 149 | public function missingExceptionClassWithMessageContaining( 150 | Assert $assert, 151 | ): void { 152 | $this->buildNonThrowingAssertion() 153 | ->willThrowClassWithMessageContaining(self::class, self::message); 154 | 155 | $assert->int($this->successCount)->eq(0); 156 | $assert->int($this->failEvents->count())->eq(1); 157 | } 158 | 159 | <> 160 | public function wrongMessage(Assert $assert): void { 161 | $this->buildThrowingAssertion()->willThrowMessage(self::notSubMessage); 162 | 163 | $assert->int($this->successCount)->eq(0); 164 | $assert->int($this->failEvents->count())->eq(1); 165 | } 166 | 167 | <> 168 | public function messageDoesNotContain(Assert $assert): void { 169 | $this->buildThrowingAssertion() 170 | ->willThrowMessageContaining(self::notSubMessage); 171 | 172 | $assert->int($this->successCount)->eq(0); 173 | $assert->int($this->failEvents->count())->eq(1); 174 | } 175 | 176 | <> 177 | public function wrongClass(Assert $assert): void { 178 | $this->buildThrowingAssertion()->willThrowClass(self::class); 179 | 180 | $assert->int($this->successCount)->eq(0); 181 | $assert->int($this->failEvents->count())->eq(1); 182 | } 183 | 184 | <> 185 | public function wrongClassRightMessage(Assert $assert): void { 186 | $this->buildThrowingAssertion() 187 | ->willThrowClassWithMessage(self::class, self::message); 188 | 189 | $assert->int($this->successCount)->eq(0); 190 | $assert->int($this->failEvents->count())->eq(1); 191 | } 192 | 193 | <> 194 | public function wrongClassMessageDoesContain(Assert $assert): void { 195 | $this->buildThrowingAssertion() 196 | ->willThrowClassWithMessageContaining(self::class, self::message); 197 | $this->buildThrowingAssertion() 198 | ->willThrowClassWithMessageContaining(self::class, self::subMessage); 199 | 200 | $assert->int($this->successCount)->eq(0); 201 | $assert->int($this->failEvents->count())->eq(2); 202 | } 203 | 204 | <> 205 | public function rightClassWrongMessage(Assert $assert): void { 206 | $this->buildThrowingAssertion() 207 | ->willThrowClassWithMessage(self::exceptionClass, self::subMessage); 208 | 209 | $assert->int($this->successCount)->eq(0); 210 | $assert->int($this->failEvents->count())->eq(1); 211 | } 212 | 213 | <> 214 | public function rightClassMessageDoesNotContain(Assert $assert): void { 215 | $this->buildThrowingAssertion()->willThrowClassWithMessageContaining( 216 | self::exceptionClass, 217 | self::notSubMessage, 218 | ); 219 | 220 | $assert->int($this->successCount)->eq(0); 221 | $assert->int($this->failEvents->count())->eq(1); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /test/Report/SummaryBuilder.php: -------------------------------------------------------------------------------- 1 | builder = new SummaryBuilder(); 20 | 21 | } 22 | 23 | private function buildStackTraces(): (TraceItem, TraceItem) { 24 | return tuple( 25 | shape( 26 | 'file' => 'test', 27 | 'line' => 10, 28 | 'function' => 'assertionFunction', 29 | 'class' => 'TestClass', 30 | ), 31 | shape( 32 | 'file' => 'test', 33 | 'line' => 10, 34 | 'function' => 'testFunction', 35 | 'class' => 'TestClass', 36 | ), 37 | ); 38 | } 39 | 40 | private function buildSuccessEvent(): Success { 41 | return new Success(...$this->buildStackTraces()); 42 | } 43 | 44 | private function buildFailEvent(): Failure { 45 | return new Failure('failure message', ...$this->buildStackTraces()); 46 | } 47 | 48 | private function buildSkipEvent(): Skip { 49 | return new Skip('skip message', ...$this->buildStackTraces()); 50 | } 51 | 52 | <> 53 | public function successIncrementsAppropriateCounts(Assert $assert): void { 54 | $this->builder->handleSuccess($this->buildSuccessEvent()); 55 | $actualSummary = $this->builder->getSummary(); 56 | 57 | $expectedTestSummary = SummaryBuilder::emptyTestSummary(); 58 | $expectedTestSummary['assert count'] = 1; 59 | $expectedTestSummary['success count'] = 1; 60 | 61 | $expectedSuiteSummary = SummaryBuilder::emptySuiteSummary(); 62 | $expectedSuiteSummary['assert count'] = 1; 63 | $expectedSuiteSummary['success count'] = 1; 64 | $expectedSuiteSummary['test summaries'] = Map { 65 | 'testFunction' => $expectedTestSummary, 66 | }; 67 | 68 | $expectedSummary = SummaryBuilder::emptySummary(); 69 | $expectedSummary['assert count'] = 1; 70 | $expectedSummary['success count'] = 1; 71 | $expectedSummary['suite summaries'] = Map { 72 | 'TestClass' => $expectedSuiteSummary, 73 | }; 74 | 75 | $this->compareSummaries($assert, $actualSummary, $expectedSummary); 76 | } 77 | 78 | <> 79 | public function failIncrementsAppropriateCounts(Assert $assert): void { 80 | $event = $this->buildFailEvent(); 81 | $this->builder->handleFailure($event); 82 | $actualSummary = $this->builder->getSummary(); 83 | 84 | $expectedTestSummary = SummaryBuilder::emptyTestSummary(); 85 | $expectedTestSummary['assert count'] = 1; 86 | $expectedTestSummary['result'] = TestResult::Fail; 87 | $expectedTestSummary['fail event'] = $event; 88 | 89 | $expectedSuiteSummary = SummaryBuilder::emptySuiteSummary(); 90 | $expectedSuiteSummary['assert count'] = 1; 91 | $expectedSuiteSummary['test count'] = 1; 92 | $expectedSuiteSummary['fail count'] = 1; 93 | $expectedSuiteSummary['test summaries'] = Map { 94 | 'testFunction' => $expectedTestSummary, 95 | }; 96 | 97 | $expectedSummary = SummaryBuilder::emptySummary(); 98 | $expectedSummary['assert count'] = 1; 99 | $expectedSummary['test count'] = 1; 100 | $expectedSummary['fail count'] = 1; 101 | $expectedSummary['fail events'] = Vector {$event}; 102 | $expectedSummary['suite summaries'] = Map { 103 | 'TestClass' => $expectedSuiteSummary, 104 | }; 105 | 106 | $this->compareSummaries($assert, $actualSummary, $expectedSummary); 107 | } 108 | 109 | <> 110 | public function skipIncrementsAppropriateCounts(Assert $assert): void { 111 | $event = $this->buildSkipEvent(); 112 | $this->builder->handleSkip($event); 113 | $actualSummary = $this->builder->getSummary(); 114 | 115 | $expectedTestSummary = SummaryBuilder::emptyTestSummary(); 116 | $expectedTestSummary['test count'] = 1; 117 | $expectedTestSummary['result'] = TestResult::Skip; 118 | $expectedTestSummary['skip event'] = $event; 119 | 120 | $expectedSuiteSummary = SummaryBuilder::emptySuiteSummary(); 121 | $expectedSuiteSummary['skip count'] = 1; 122 | $expectedSuiteSummary['test count'] = 1; 123 | $expectedSuiteSummary['test summaries'] = Map { 124 | 'testFunction' => $expectedTestSummary, 125 | }; 126 | 127 | $expectedSummary = SummaryBuilder::emptySummary(); 128 | $expectedSummary['test count'] = 1; 129 | $expectedSummary['skip count'] = 1; 130 | $expectedSummary['skip events'] = Vector {$event}; 131 | $expectedSummary['suite summaries'] = Map { 132 | 'TestClass' => $expectedSuiteSummary, 133 | }; 134 | 135 | $this->compareSummaries($assert, $actualSummary, $expectedSummary); 136 | } 137 | 138 | private function compareSummaries( 139 | Assert $assert, 140 | Summary $actualSummary, 141 | Summary $expectedSummary, 142 | ): void { 143 | $assert->float($actualSummary['start time']) 144 | ->eq($expectedSummary['start time']); 145 | $assert->float($actualSummary['end time']) 146 | ->eq($expectedSummary['end time']); 147 | $assert->int($actualSummary['assert count']) 148 | ->eq($expectedSummary['assert count']); 149 | $assert->int($actualSummary['success count']) 150 | ->eq($expectedSummary['success count']); 151 | $assert->int($actualSummary['pass count']) 152 | ->eq($expectedSummary['pass count']); 153 | $assert->int($actualSummary['fail count']) 154 | ->eq($expectedSummary['fail count']); 155 | $assert->int($actualSummary['skip count']) 156 | ->eq($expectedSummary['skip count']); 157 | $assert->int($actualSummary['test count']) 158 | ->eq($expectedSummary['test count']); 159 | $assert->container($actualSummary['fail events']) 160 | ->containsOnly($expectedSummary['fail events']); 161 | $assert->container($actualSummary['skip events']) 162 | ->containsOnly($expectedSummary['skip events']); 163 | $assert->container($actualSummary['malformed events']) 164 | ->containsOnly($expectedSummary['malformed events']); 165 | $assert->container($actualSummary['untested exceptions']) 166 | ->containsOnly($expectedSummary['untested exceptions']); 167 | 168 | $assert->container($actualSummary['suite summaries']->keys()) 169 | ->containsOnly($expectedSummary['suite summaries']->keys()); 170 | foreach ($expectedSummary['suite summaries'] as 171 | $suiteName => $expectedSuiteSummary) { 172 | $this->compareSuiteSummaries( 173 | $assert, 174 | $actualSummary['suite summaries']->at($suiteName), 175 | $expectedSuiteSummary, 176 | ); 177 | } 178 | } 179 | 180 | private function compareSuiteSummaries( 181 | Assert $assert, 182 | SuiteSummary $actualSummary, 183 | SuiteSummary $expectedSummary, 184 | ): void { 185 | $assert->int($actualSummary['assert count']) 186 | ->eq($expectedSummary['assert count']); 187 | $assert->int($actualSummary['success count']) 188 | ->eq($expectedSummary['success count']); 189 | $assert->int($actualSummary['pass count']) 190 | ->eq($expectedSummary['pass count']); 191 | $assert->int($actualSummary['fail count']) 192 | ->eq($expectedSummary['fail count']); 193 | $assert->int($actualSummary['skip count']) 194 | ->eq($expectedSummary['skip count']); 195 | $assert->int($actualSummary['test count']) 196 | ->eq($expectedSummary['test count']); 197 | 198 | $assert->container($actualSummary['test summaries']->keys()) 199 | ->containsOnly($expectedSummary['test summaries']->keys()); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Test/SuiteBuilder.php: -------------------------------------------------------------------------------- 1 | ): Awaitable); 16 | 17 | final class SuiteBuilder { 18 | public function __construct( 19 | private (function(string, string): Parser) $parserBuilder, 20 | private Vector $malformedListeners = Vector {}, 21 | ) {} 22 | 23 | public function onMalformedSuite(MalformedSuiteListener $listener): this { 24 | $this->malformedListeners->add($listener); 25 | return $this; 26 | } 27 | 28 | public function buildSuites(string $filename): Traversable { 29 | $suites = Vector {}; 30 | $parserBuilder = $this->parserBuilder; 31 | 32 | foreach (FileParser::FromFile($filename)->getClasses() as $scannedClass) { 33 | 34 | if ($this->markedAsSkipped($scannedClass)) { 35 | $pos = $scannedClass->getPosition(); 36 | [ 37 | 'line' => $pos['line'], 38 | 'class' => $scannedClass->getName(), 39 | 'file' => $pos['filename'], 40 | ] |> Trace::buildItem($$) 41 | |> new SkippedSuite($scannedClass->getName(), $$) 42 | |> $suites->add($$); 43 | continue; 44 | } 45 | 46 | $mirror = $this->reflectClass($scannedClass); 47 | if ($mirror === null) { 48 | continue; 49 | } 50 | 51 | $parser = $parserBuilder( 52 | $scannedClass->getName(), 53 | $scannedClass->getFileName(), 54 | ); 55 | if ($parser->tests()->isEmpty()) { 56 | continue; 57 | } 58 | 59 | $suite = $this->buildSuite($mirror, $parser); 60 | if ($suite !== null) { 61 | $suites->add($suite); 62 | } 63 | } 64 | 65 | return $suites; 66 | } 67 | 68 | private function markedAsSkipped(ScannedBasicClass $class): bool { 69 | return $class->getAttributes()->containsKey('Skip'); 70 | } 71 | 72 | private function buildInvoker(ReflectionMethod $method): InvokerWithParams { 73 | return async ($instance, $params) ==> { 74 | 75 | $result = 76 | $method->isStatic() 77 | ? $method->invokeArgs(null, $params) 78 | : $method->invokeArgs($instance, $params); 79 | 80 | if ($method->isAsync()) { 81 | await $result; 82 | } 83 | 84 | }; 85 | } 86 | 87 | private function buildDataProvider( 88 | ?ReflectionMethod $method, 89 | ): (function(): AsyncIterator>) { 90 | if ($method === null) { 91 | return async () ==> { 92 | yield []; 93 | }; 94 | } 95 | 96 | $result = $method->invoke(null); 97 | 98 | if ($method->isAsync()) { 99 | return async () ==> { 100 | foreach ($result await as $data) { 101 | yield [$data]; 102 | } 103 | }; 104 | } 105 | 106 | return async () ==> { 107 | foreach ($result as $data) { 108 | yield [$data]; 109 | } 110 | }; 111 | } 112 | 113 | private function buildSuite( 114 | ReflectionClass $classMirror, 115 | Parser $parser, 116 | ): ?SuiteInterface { 117 | $tests = Vector {}; 118 | $errors = $parser->errors()->toVector(); 119 | 120 | // Convert method names to ReflectionMethods 121 | $getMethod = inst_meth($classMirror, 'getMethod'); 122 | $nameToInvoker = $name ==> $this->buildInvoker($getMethod($name)); 123 | 124 | $factories = 125 | $methodName ==> { 126 | return async () ==> { 127 | 128 | if ($methodName === '__construct') { 129 | return $classMirror->newInstance(); 130 | } 131 | 132 | $method = $classMirror->getMethod($methodName); 133 | $result = $method->invoke(null); 134 | if ($method->isAsync()) { 135 | $result = await $result; 136 | } 137 | return $result; 138 | }; 139 | } |> $parser->factories()->map($$); 140 | 141 | // Set the default constructor if possible 142 | if (!$factories->containsKey('')) { 143 | $default = $this->getDefaultFactory($classMirror); 144 | if ($default !== null) { 145 | $factories = $factories->toMap()->set('', $default); 146 | } 147 | } 148 | 149 | $suiteUp = $parser->suiteUp()->map($nameToInvoker); 150 | $suiteDown = $parser->suiteDown()->map($nameToInvoker); 151 | $testUp = $parser->testUp()->map($nameToInvoker); 152 | $testDown = $parser->testDown()->map($nameToInvoker); 153 | 154 | $nullTests = 155 | $test ==> { 156 | 157 | if (!$factories->containsKey($test['factory name'])) { 158 | 159 | ($test['factory name'] === '' 160 | ? 'You must provide a factory method to construct your test suite and annotate it with <>.' 161 | : 'Suite provider "'.$test['factory name'].'" not found.') 162 | |> $errors->add( 163 | new MalformedSuite( 164 | Trace::fromReflectionMethod($getMethod($test['method'])), 165 | $$, 166 | ), 167 | ); 168 | 169 | return null; 170 | } 171 | 172 | return 173 | $getMethod($test['method']) 174 | |> shape( 175 | 'name' => $$->getName(), 176 | 'suite name' => $classMirror->getName(), 177 | 'factory' => $factories->at($test['factory name']), 178 | 'method' => $this->buildInvoker($$), 179 | 'trace item' => Trace::fromReflectionMethod($$), 180 | 'skip' => $test['skip'], 181 | 'data provider' => 182 | ($test['data provider'] === '' 183 | ? null 184 | : $getMethod($test['data provider'])) 185 | |> $this->buildDataProvider($$), 186 | ); 187 | 188 | } 189 | |> $parser->tests()->map($$); 190 | 191 | if (!$errors->isEmpty()) { 192 | foreach ($errors as $error) { 193 | $this->emitMalformedSuite($error); 194 | } 195 | return null; 196 | } 197 | 198 | $tests = Vector {}; 199 | foreach ($nullTests as $t) { 200 | if ($t !== null) { 201 | $tests->add($t); 202 | } 203 | } 204 | return new Suite( 205 | $classMirror->getName(), 206 | $tests, 207 | $suiteUp, 208 | $suiteDown, 209 | $testUp, 210 | $testDown, 211 | ); 212 | } 213 | 214 | private function getDefaultFactory( 215 | ReflectionClass $classMirror, 216 | ): ?(function(): Awaitable) { 217 | $constructor = $classMirror->getConstructor(); 218 | 219 | if ($constructor === null || 220 | $constructor->getNumberOfRequiredParameters() === 0) { 221 | return async () ==> $classMirror->newInstance(); 222 | } 223 | 224 | return null; 225 | } 226 | 227 | private function emitMalformedSuite(MalformedSuite $event): void { 228 | foreach ($this->malformedListeners as $l) { 229 | $l($event); 230 | } 231 | } 232 | 233 | private function load(string $fileName): void { 234 | // Is there a better way of dynamically including files? 235 | /* HH_FIXME[1002] */ 236 | require_once ($fileName); 237 | } 238 | 239 | private function reflectClass( 240 | ScannedBasicClass $scannedClass, 241 | ): ?ReflectionClass { 242 | if (!class_exists($scannedClass->getName())) { 243 | $this->load($scannedClass->getFileName()); 244 | } 245 | 246 | try { 247 | return new \ReflectionClass($scannedClass->getName()); 248 | } catch (\ReflectionException $e) { 249 | // Unable to load the file, or the map was wrong? 250 | // Should we warn the user? 251 | //echo PHP_EOL . 'Unable to reflect class ' . $scannedClass->getName() . PHP_EOL; 252 | return null; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Report/SummaryBuilder.php: -------------------------------------------------------------------------------- 1 | float, 15 | 'end time' => float, 16 | 'assert count' => int, 17 | 'success count' => int, 18 | 'pass count' => int, 19 | 'fail count' => int, 20 | 'fail events' => Vector, 21 | 'skip count' => int, 22 | 'skip events' => Vector, 23 | 'test count' => int, 24 | 'malformed events' => Vector, 25 | 'untested exceptions' => Vector<\Exception>, 26 | 'suite summaries' => Map, 27 | ); 28 | type Summary = shape( 29 | 'start time' => float, 30 | 'end time' => float, 31 | 'assert count' => int, 32 | 'success count' => int, 33 | 'pass count' => int, 34 | 'fail count' => int, 35 | 'fail events' => \ConstVector, 36 | 'skip count' => int, 37 | 'skip events' => \ConstVector, 38 | 'test count' => int, 39 | 'malformed events' => \ConstVector, 40 | 'untested exceptions' => \ConstVector<\Exception>, 41 | 'suite summaries' => \ConstMap, 42 | ); 43 | 44 | type MutableSuiteSummary = shape( 45 | 'assert count' => int, 46 | 'success count' => int, 47 | 'pass count' => int, 48 | 'fail count' => int, 49 | 'skip count' => int, 50 | 'test count' => int, 51 | 'test summaries' => Map, 52 | ); 53 | 54 | type SuiteSummary = shape( 55 | 'assert count' => int, 56 | 'success count' => int, 57 | 'pass count' => int, 58 | 'fail count' => int, 59 | 'skip count' => int, 60 | 'test count' => int, 61 | 'test summaries' => \ConstMap, 62 | ); 63 | 64 | type TestSummary = shape( 65 | 'assert count' => int, 66 | 'success count' => int, 67 | 'result' => TestResult, 68 | 'skip event' => ?Skip, 69 | 'fail event' => ?Failure, 70 | ); 71 | 72 | type TestInfo = shape( 73 | 'test name' => string, 74 | 'suite name' => string, 75 | ); 76 | 77 | enum TestResult : string { 78 | Pass = 'pass'; 79 | Fail = 'fail'; 80 | Skip = 'skip'; 81 | Error = 'error'; 82 | } 83 | 84 | class SummaryBuilder { 85 | 86 | // Mirror Summary shape, but with mutable collections 87 | private MutableSummary $summary; 88 | public function __construct() { 89 | $this->summary = self::emptyMutableSummary(); 90 | } 91 | 92 | public function startTiming(): void { 93 | $this->summary['start time'] = microtime(true); 94 | } 95 | 96 | public function stopTiming(): void { 97 | $this->summary['end time'] = microtime(true); 98 | } 99 | 100 | public function handleFailure(Failure $event): void { 101 | $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); 102 | $this->ensureTestExists($testInfo); 103 | 104 | $this->summary['test count']++; 105 | $this->summary['fail count']++; 106 | $this->summary['assert count']++; 107 | $this->summary['fail events']->add($event); 108 | 109 | $suiteSummary = 110 | $this->summary['suite summaries']->at($testInfo['suite name']); 111 | $suiteSummary['test count']++; 112 | $suiteSummary['fail count']++; 113 | $suiteSummary['assert count']++; 114 | 115 | $testSummary = 116 | $suiteSummary['test summaries']->at($testInfo['test name']); 117 | $testSummary['fail event'] = $event; 118 | $testSummary['assert count']++; 119 | $testSummary['result'] = TestResult::Skip; 120 | 121 | $suiteSummary['test summaries'] 122 | ->set($testInfo['test name'], $testSummary); 123 | $this->summary['suite summaries'] 124 | ->set($testInfo['suite name'], $suiteSummary); 125 | 126 | } 127 | 128 | public function handleSkip(Skip $event): void { 129 | $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); 130 | $this->ensureTestExists($testInfo); 131 | 132 | $this->summary['test count']++; 133 | $this->summary['skip count']++; 134 | $this->summary['skip events']->add($event); 135 | 136 | $suiteSummary = 137 | $this->summary['suite summaries']->at($testInfo['suite name']); 138 | $suiteSummary['test count']++; 139 | $suiteSummary['skip count']++; 140 | 141 | $testSummary = 142 | $suiteSummary['test summaries']->at($testInfo['test name']); 143 | $testSummary['skip event'] = $event; 144 | $testSummary['result'] = TestResult::Skip; 145 | 146 | $suiteSummary['test summaries'] 147 | ->set($testInfo['test name'], $testSummary); 148 | $this->summary['suite summaries'] 149 | ->set($testInfo['suite name'], $suiteSummary); 150 | } 151 | 152 | public function handleSuccess(Success $event): void { 153 | $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); 154 | $this->ensureTestExists($testInfo); 155 | 156 | $this->summary['assert count']++; 157 | $this->summary['success count']++; 158 | 159 | $suiteSummary = 160 | $this->summary['suite summaries']->at($testInfo['suite name']); 161 | $suiteSummary['assert count']++; 162 | $suiteSummary['success count']++; 163 | 164 | $testSummary = 165 | $suiteSummary['test summaries']->at($testInfo['test name']); 166 | $testSummary['assert count']++; 167 | $testSummary['success count']++; 168 | 169 | $suiteSummary['test summaries'] 170 | ->set($testInfo['test name'], $testSummary); 171 | $this->summary['suite summaries'] 172 | ->set($testInfo['suite name'], $suiteSummary); 173 | } 174 | 175 | public function handlePass(Pass $event): void { 176 | $testInfo = $this->determineTestInfo($event->testMethodTraceItem()); 177 | $this->ensureTestExists($testInfo); 178 | 179 | $this->summary['test count']++; 180 | $this->summary['pass count']++; 181 | 182 | $this->summary['suite summaries'] 183 | ->at($testInfo['suite name'])['test count']++; 184 | $this->summary['suite summaries'] 185 | ->at($testInfo['suite name'])['pass count']++; 186 | } 187 | 188 | public function handleUntestedException(\Exception $e): void { 189 | $this->summary['untested exceptions']->add($e); 190 | } 191 | 192 | public function handleMalformedSuite(MalformedSuite $event): void { 193 | $this->summary['malformed events']->add($event); 194 | } 195 | 196 | public function getSummary(): Summary { 197 | return $this->summary; 198 | } 199 | 200 | public static function emptySummary(): Summary { 201 | return self::emptyMutableSummary(); 202 | } 203 | 204 | private static function emptyMutableSummary(): MutableSummary { 205 | return shape( 206 | 'start time' => 0.0, 207 | 'end time' => 0.0, 208 | 'assert count' => 0, 209 | 'success count' => 0, 210 | 'pass count' => 0, 211 | 'fail count' => 0, 212 | 'fail events' => Vector {}, 213 | 'skip count' => 0, 214 | 'skip events' => Vector {}, 215 | 'test count' => 0, 216 | 'malformed events' => Vector {}, 217 | 'untested exceptions' => Vector {}, 218 | 'suite summaries' => Map {}, 219 | ); 220 | } 221 | 222 | public static function emptySuiteSummary(): SuiteSummary { 223 | return self::emptyMutableSuiteSummary(); 224 | } 225 | 226 | private static function emptyMutableSuiteSummary(): MutableSuiteSummary { 227 | 228 | return shape( 229 | 'assert count' => 0, 230 | 'success count' => 0, 231 | 'pass count' => 0, 232 | 'fail count' => 0, 233 | 'skip count' => 0, 234 | 'test count' => 0, 235 | 'test summaries' => Map {}, 236 | ); 237 | } 238 | 239 | public static function emptyTestSummary(): TestSummary { 240 | return shape( 241 | 'assert count' => 0, 242 | 'success count' => 0, 243 | 'message' => '', 244 | 'result' => TestResult::Pass, 245 | ); 246 | } 247 | 248 | private function determineTestInfo(TraceItem $trace): TestInfo { 249 | $testName = Shapes::idx($trace, 'function', '??'); 250 | if ($testName === null) { 251 | $testName = '??'; 252 | } 253 | $suiteName = 254 | Shapes::idx($trace, 'class', Shapes::idx($trace, 'file', '??')); 255 | if ($suiteName === null) { 256 | $suiteName = '??'; 257 | } 258 | 259 | return shape('test name' => $testName, 'suite name' => $suiteName); 260 | } 261 | 262 | private function ensureTestExists(TestInfo $testInfo): void { 263 | if (!$this->summary['suite summaries'] 264 | ->containsKey($testInfo['suite name'])) { 265 | $this->summary['suite summaries'] 266 | ->set($testInfo['suite name'], self::emptyMutableSuiteSummary()); 267 | } 268 | if (!$this->summary['suite summaries'] 269 | ->at($testInfo['suite name'])['test summaries'] 270 | ->containsKey($testInfo['test name'])) { 271 | $this->summary['suite summaries'] 272 | ->at($testInfo['suite name'])['test summaries'] 273 | ->set($testInfo['test name'], self::emptyTestSummary()); 274 | } 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /test/Test/Suite.php: -------------------------------------------------------------------------------- 1 | $skipEvents = Vector {}; 26 | private Vector $testStartEvents = Vector {}; 27 | 28 | private (function(): Awaitable) $factory; 29 | private TraceItem $traceItem; 30 | 31 | public function __construct() { 32 | $this->factory = async () ==> { 33 | $this->factoryRuns++; 34 | return $this; 35 | }; 36 | $this->traceItem = Trace::buildItem([]); 37 | } 38 | 39 | <> 40 | public function dataConsumerTest(Assert $assert): void { 41 | $testMethod = async ($instance, $args) ==> { 42 | $this->testRuns++; 43 | $assert->mixed($instance)->identicalTo($this); 44 | $assert->int(count($args))->eq(2); 45 | $assert->mixed($args[0])->isTypeOf(Assert::class); 46 | $assert->mixed($args[1])->identicalTo($this->testRuns); 47 | }; 48 | $test = shape( 49 | 'name' => '', 50 | 'suite name' => '', 51 | 'factory' => $this->factory, 52 | 'method' => $testMethod, 53 | 'trace item' => $this->traceItem, 54 | 'skip' => false, 55 | 'data provider' => async () ==> { 56 | yield [1]; 57 | yield [2]; 58 | }, 59 | ); 60 | $suite = new Suite( 61 | '', 62 | Vector {$test}, 63 | Vector {}, 64 | Vector {}, 65 | Vector {}, 66 | Vector {}, 67 | ); 68 | 69 | $assert->whenCalled(() ==> $this->runSuite($suite))->willNotThrow(); 70 | $assert->int($this->passedEvents)->eq(2); 71 | $assert->int($this->testRuns)->eq(2); 72 | } 73 | 74 | <> 75 | public function unexpectedExceptionTest(Assert $assert): void { 76 | $tests = Vector { 77 | $this->makePassingTest($assert), 78 | $this->makeUnexpectedExceptionTest(), 79 | $this->makePassingTest($assert), 80 | }; 81 | $suite = new Suite( 82 | '', 83 | $tests, 84 | Vector {}, 85 | Vector {}, 86 | Vector {$this->makeTestUp($assert)}, 87 | Vector {$this->makeTestDown($assert)}, 88 | ); 89 | 90 | $assert->whenCalled(() ==> $this->runSuite($suite)) 91 | ->willThrowMessage('This is the message'); 92 | $assert->int($this->passedEvents)->eq(2); 93 | $assert->int($this->testUpRuns)->eq(3); 94 | $assert->int($this->testDownRuns)->eq(2); 95 | } 96 | 97 | <> 98 | public function suiteTests(Assert $assert): void { 99 | foreach (range(0, 3) as $thirdTestCount) { 100 | 101 | $tests = Vector {}; 102 | for ($i = 0; $i < $thirdTestCount; $i++) { 103 | $tests->add($this->makeInterruptedTest()); 104 | $tests->add($this->makeSkippedTest($assert)); 105 | $tests->add($this->makePassingTest($assert)); 106 | } 107 | $this->runTests($assert, $tests); 108 | } 109 | } 110 | 111 | private function runTests(Assert $assert, Vector $tests): void { 112 | $thirdTestCount = (int) floor($tests->count() / 3); 113 | 114 | foreach (range(0, 2) as $upDownCount) { 115 | $suite = new Suite( 116 | '', 117 | $tests, 118 | $this->repeat($upDownCount, $this->makeSuiteUp($assert)), 119 | $this->repeat($upDownCount, $this->makeSuiteDown($assert)), 120 | $this->repeat($upDownCount, $this->makeTestUp($assert)), 121 | $this->repeat($upDownCount, $this->makeTestDown($assert)), 122 | ); 123 | 124 | $assert->whenCalled(() ==> $this->runSuite($suite))->willNotThrow(); 125 | 126 | // Test start events triggered 127 | $assert->int($this->testStartEvents->count())->eq($tests->count()); 128 | 129 | // Skipped tests shouldn't run the factory 130 | $assert->int($this->factoryRuns)->eq(2 * $thirdTestCount); 131 | 132 | // Skipped tests shouldn't be run 133 | $assert->int($this->testRuns)->eq(2 * $thirdTestCount); 134 | 135 | // Interrupted and skipped tests shouldn't be passed 136 | $assert->int($this->passedEvents)->eq($thirdTestCount); 137 | 138 | // Make sure the skip event is triggered for skipped tests 139 | $assert->int($this->skipEvents->count())->eq($thirdTestCount); 140 | 141 | // Make sure the test trace item is passed to the skip event 142 | if ($thirdTestCount > 0) { 143 | $event = $this->skipEvents->at(0); 144 | $assert->mixed($event->skipCallSite())->identicalTo($this->traceItem); 145 | } 146 | 147 | // Test up/down should not run skipped tests, should run for interrupted tests 148 | $assert->int($this->testUpRuns)->eq(2 * $thirdTestCount * $upDownCount); 149 | $assert->int($this->testDownRuns) 150 | ->eq(2 * $thirdTestCount * $upDownCount); 151 | 152 | // Running shouldn't trigger suite up/down 153 | $assert->int($this->suiteUpRuns)->eq(0); 154 | $assert->int($this->suiteDownRuns)->eq(0); 155 | 156 | // Should be independent of test count, and only ups are run 157 | Asio\join($suite->up()); 158 | $assert->int($this->suiteUpRuns)->eq($upDownCount); 159 | $assert->int($this->suiteDownRuns)->eq(0); 160 | 161 | // Should be independent of test count, and only downs are run 162 | Asio\join($suite->down()); 163 | $assert->int($this->suiteUpRuns)->eq($upDownCount); 164 | $assert->int($this->suiteDownRuns)->eq($upDownCount); 165 | 166 | $this->resetCounts(); 167 | } 168 | } 169 | 170 | private function resetCounts(): void { 171 | $this->factoryRuns = 0; 172 | $this->suiteUpRuns = 0; 173 | $this->suiteDownRuns = 0; 174 | $this->testUpRuns = 0; 175 | $this->testDownRuns = 0; 176 | $this->testRuns = 0; 177 | $this->passedEvents = 0; 178 | $this->skipEvents->clear(); 179 | $this->testStartEvents->clear(); 180 | } 181 | 182 | private function repeat(int $count, T $item): Vector { 183 | $list = Vector {}; 184 | $list->resize($count, $item); 185 | return $list; 186 | } 187 | 188 | private function makeTestMethod(Assert $assert): InvokerWithParams { 189 | return async ($instance, $args) ==> { 190 | $assert->mixed($instance)->identicalTo($this); 191 | $assert->int(count($args))->eq(1); 192 | $assert->mixed($args[0])->isTypeOf(Assert::class); 193 | $this->testRuns++; 194 | }; 195 | } 196 | 197 | private function makePassingTest(Assert $assert): TestShape { 198 | return shape( 199 | 'name' => 'passing', 200 | 'suite name' => '', 201 | 'factory' => $this->factory, 202 | 'method' => $this->makeTestMethod($assert), 203 | 'trace item' => $this->traceItem, 204 | 'skip' => false, 205 | 'data provider' => async () ==> { 206 | yield []; 207 | }, 208 | ); 209 | } 210 | 211 | private function makeSkippedTest(Assert $assert): TestShape { 212 | return shape( 213 | 'name' => 'skipped', 214 | 'suite name' => '', 215 | 'factory' => $this->factory, 216 | 'method' => $this->makeTestMethod($assert), 217 | 'trace item' => $this->traceItem, 218 | 'skip' => true, 219 | 'data provider' => async () ==> { 220 | yield []; 221 | }, 222 | ); 223 | } 224 | 225 | private function makeUnexpectedExceptionTest(): TestShape { 226 | return shape( 227 | 'name' => 'unexpected exception', 228 | 'suite name' => '', 229 | 'factory' => $this->factory, 230 | 'method' => async ($instance, $args) ==> { 231 | $this->testRuns++; 232 | throw new \Exception('This is the message'); 233 | }, 234 | 'trace item' => $this->traceItem, 235 | 'skip' => false, 236 | 'data provider' => async () ==> { 237 | yield []; 238 | }, 239 | ); 240 | } 241 | 242 | private function makeInterruptedTest(): TestShape { 243 | return shape( 244 | 'name' => 'interrupted', 245 | 'suite name' => '', 246 | 'factory' => $this->factory, 247 | 'method' => async ($instance, $args) ==> { 248 | $this->testRuns++; 249 | throw new Interruption(); 250 | }, 251 | 'trace item' => $this->traceItem, 252 | 'skip' => false, 253 | 'data provider' => async () ==> { 254 | yield []; 255 | }, 256 | ); 257 | } 258 | 259 | private function makeSuiteUp(Assert $assert): InvokerWithParams { 260 | return async ($instance, $params) ==> { 261 | $this->suiteUpRuns++; 262 | // All suite up methods are static, so no instance should be passed 263 | $assert->mixed($instance)->isNull(); 264 | }; 265 | } 266 | 267 | private function makeSuiteDown(Assert $assert): InvokerWithParams { 268 | return async ($instance, $params) ==> { 269 | $this->suiteDownRuns++; 270 | // All suite down methods are static, so no instance should be passed 271 | $assert->mixed($instance)->isNull(); 272 | }; 273 | } 274 | 275 | private function makeTestUp(Assert $assert): InvokerWithParams { 276 | return 277 | async ($instance, $params) ==> { 278 | $this->testUpRuns++; 279 | // The test factories all return $this. Ensure it is passed to the test up methods. 280 | $assert->mixed($instance)->identicalTo($this); 281 | }; 282 | } 283 | 284 | private function makeTestDown(Assert $assert): InvokerWithParams { 285 | return 286 | async ($instance, $params) ==> { 287 | $this->testDownRuns++; 288 | // The test factories all return $this. Ensure it is passed to the test up methods. 289 | $assert->mixed($instance)->identicalTo($this); 290 | }; 291 | } 292 | 293 | private function runSuite(Suite $suite): void { 294 | Asio\join( 295 | $suite->run( 296 | $this->makeAssert(), 297 | () ==> { 298 | $this->passedEvents++; 299 | }, 300 | Vector { 301 | (TestStart $e) ==> { 302 | $this->testStartEvents->add($e); 303 | }, 304 | }, 305 | ), 306 | ); 307 | } 308 | 309 | private function makeAssert(): Assert { 310 | $skipListener = ($skipEvent) ==> { 311 | $this->skipEvents->add($skipEvent); 312 | }; 313 | 314 | return new \HackPack\HackUnit\Assert( 315 | Vector {}, 316 | Vector {$skipListener}, 317 | Vector {}, 318 | ); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /test/Assertion/ContainerAssertion.php: -------------------------------------------------------------------------------- 1 | ( 14 | Container $actual, 15 | ): ContainerAssertion { 16 | return new ContainerAssertion( 17 | $actual, 18 | $this->failListeners(), 19 | $this->successListeners(), 20 | ); 21 | } 22 | 23 | <> 24 | public function doesContain(Assert $assert): void { 25 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 26 | 27 | $assertion->contains('b'); 28 | $assertion->contains('a'); 29 | 30 | $assert->int($this->successCount)->eq(2); 31 | $assert->int($this->failEvents->count())->eq(0); 32 | } 33 | 34 | <> 35 | public function doesContainCustom(Assert $assert): void { 36 | $assertion = $this->makeAssertion(Vector {1, 3}); 37 | 38 | $assertion->contains(2, ($a, $b) ==> true); 39 | 40 | $assert->int($this->successCount)->eq(1); 41 | $assert->int($this->failEvents->count())->eq(0); 42 | } 43 | 44 | <> 45 | public function emptyDoesNotContain(Assert $assert): void { 46 | $assertion = $this->makeAssertion(Vector {}); 47 | 48 | $assertion->not()->contains('c'); 49 | 50 | $assert->int($this->successCount)->eq(1); 51 | $assert->int($this->failEvents->count())->eq(0); 52 | } 53 | 54 | <> 55 | public function doesNotContain(Assert $assert): void { 56 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 57 | 58 | $assertion->not()->contains('c'); 59 | 60 | $assert->int($this->successCount)->eq(1); 61 | $assert->int($this->failEvents->count())->eq(0); 62 | } 63 | 64 | <> 65 | public function doesNotContainCustom(Assert $assert): void { 66 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 67 | 68 | $assertion->not()->contains('a', ($a, $b) ==> false); 69 | 70 | $assert->int($this->successCount)->eq(1); 71 | $assert->int($this->failEvents->count())->eq(0); 72 | } 73 | 74 | <> 75 | public function failsToContain(Assert $assert): void { 76 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 77 | 78 | $assertion->contains(1); 79 | 80 | $assert->int($this->successCount)->eq(0); 81 | $assert->int($this->failEvents->count())->eq(1); 82 | } 83 | 84 | <> 85 | public function emptyFailsToContain(Assert $assert): void { 86 | $assertion = $this->makeAssertion(Vector {}); 87 | 88 | $assertion->contains(1); 89 | 90 | $assert->int($this->successCount)->eq(0); 91 | $assert->int($this->failEvents->count())->eq(1); 92 | $assert->string($this->failEvents->at(0)->getMessage()) 93 | ->is('The Container is empty.'); 94 | } 95 | 96 | <> 97 | public function failsToContainCustom(Assert $assert): void { 98 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 99 | 100 | $assertion->contains('a', ($a, $b) ==> false); 101 | 102 | $assert->int($this->successCount)->eq(0); 103 | $assert->int($this->failEvents->count())->eq(1); 104 | } 105 | 106 | <> 107 | public function failsToNotContain(Assert $assert): void { 108 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 109 | 110 | $assertion->not()->contains('b'); 111 | 112 | $assert->int($this->successCount)->eq(0); 113 | $assert->int($this->failEvents->count())->eq(1); 114 | } 115 | 116 | <> 117 | public function failsToNotContainCustom(Assert $assert): void { 118 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 119 | 120 | $assertion->not()->contains('c', ($a, $b) ==> true); 121 | 122 | $assert->int($this->successCount)->eq(0); 123 | $assert->int($this->failEvents->count())->eq(1); 124 | } 125 | 126 | <> 127 | public function doesContainAll(Assert $assert): void { 128 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 129 | 130 | $assertion->containsAll(Vector {'a', 'c'}); 131 | 132 | $assert->int($this->successCount)->eq(1); 133 | $assert->int($this->failEvents->count())->eq(0); 134 | } 135 | 136 | <> 137 | public function doesContainAllCustom(Assert $assert): void { 138 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 139 | 140 | $assertion->containsAll(Vector {'a', 'c', 'e'}, ($a, $b) ==> true); 141 | 142 | $assert->int($this->successCount)->eq(1); 143 | $assert->int($this->failEvents->count())->eq(0); 144 | } 145 | 146 | <> 147 | public function doesNotContainAll(Assert $assert): void { 148 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 149 | 150 | $assertion->not()->containsAll(Vector {'c', 'a'}); 151 | 152 | $assert->int($this->successCount)->eq(1); 153 | $assert->int($this->failEvents->count())->eq(0); 154 | } 155 | 156 | <> 157 | public function doesNotContainAllCustom(Assert $assert): void { 158 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 159 | 160 | $assertion->not()->containsAll(Vector {'a', 'c'}, ($a, $b) ==> false); 161 | 162 | $assert->int($this->successCount)->eq(1); 163 | $assert->int($this->failEvents->count())->eq(0); 164 | } 165 | 166 | <> 167 | public function failsToContainAll(Assert $assert): void { 168 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 169 | 170 | $assertion->containsAll(Vector {'c', 'a'}); 171 | 172 | $assert->int($this->successCount)->eq(0); 173 | $assert->int($this->failEvents->count())->eq(1); 174 | } 175 | 176 | <> 177 | public function failsToContainAllCustom(Assert $assert): void { 178 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 179 | 180 | $assertion->containsAll(Vector {'b', 'a'}, ($a, $b) ==> false); 181 | 182 | $assert->int($this->successCount)->eq(0); 183 | $assert->int($this->failEvents->count())->eq(1); 184 | } 185 | 186 | <> 187 | public function failsToNotContainAll(Assert $assert): void { 188 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 189 | 190 | $assertion->not()->containsAll(Vector {'a', 'b'}); 191 | 192 | $assert->int($this->successCount)->eq(0); 193 | $assert->int($this->failEvents->count())->eq(1); 194 | } 195 | 196 | <> 197 | public function failsToNotContainAllCustom(Assert $assert): void { 198 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 199 | 200 | $assertion->not()->containsAll(Vector {'a', 'd'}, ($a, $b) ==> true); 201 | 202 | $assert->int($this->successCount)->eq(0); 203 | $assert->int($this->failEvents->count())->eq(1); 204 | } 205 | 206 | <> 207 | public function doesContainAny(Assert $assert): void { 208 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 209 | 210 | $assertion->containsAny(Vector {'a', 'd'}); 211 | 212 | $assert->int($this->successCount)->eq(1); 213 | $assert->int($this->failEvents->count())->eq(0); 214 | } 215 | 216 | <> 217 | public function doesContainAnyCustom(Assert $assert): void { 218 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 219 | 220 | $assertion->containsAny(Vector {'d'}, ($a, $b) ==> true); 221 | 222 | $assert->int($this->successCount)->eq(1); 223 | $assert->int($this->failEvents->count())->eq(0); 224 | } 225 | 226 | <> 227 | public function doesNotContainAny(Assert $assert): void { 228 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 229 | 230 | $assertion->not()->containsAny(Vector {'f', 'd'}); 231 | 232 | $assert->int($this->successCount)->eq(1); 233 | $assert->int($this->failEvents->count())->eq(0); 234 | } 235 | 236 | <> 237 | public function emptyDoesNotContainAny(Assert $assert): void { 238 | $assertion = $this->makeAssertion(Vector {}); 239 | 240 | $assertion->not()->containsAny(Vector {'f', 'd'}); 241 | 242 | $assert->int($this->successCount)->eq(1); 243 | $assert->int($this->failEvents->count())->eq(0); 244 | } 245 | 246 | <> 247 | public function doesNotContainAnyCustom(Assert $assert): void { 248 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 249 | 250 | $assertion->not()->containsAny(Vector {'a', 'd'}, ($a, $b) ==> false); 251 | 252 | $assert->int($this->successCount)->eq(1); 253 | $assert->int($this->failEvents->count())->eq(0); 254 | } 255 | 256 | <> 257 | public function failsToContainAny(Assert $assert): void { 258 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 259 | 260 | $assertion->containsAny(Vector {'f', 'd'}); 261 | 262 | $assert->int($this->successCount)->eq(0); 263 | $assert->int($this->failEvents->count())->eq(1); 264 | } 265 | 266 | <> 267 | public function emptyFailsToContainAny(Assert $assert): void { 268 | $assertion = $this->makeAssertion(Vector {}); 269 | 270 | $assertion->containsAny(Vector {'f', 'd'}); 271 | 272 | $assert->int($this->successCount)->eq(0); 273 | $assert->int($this->failEvents->count())->eq(1); 274 | $assert->string($this->failEvents->at(0)->getMessage()) 275 | ->is('The Container is empty.'); 276 | } 277 | 278 | <> 279 | public function failsToNotContainAny(Assert $assert): void { 280 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 281 | 282 | $assertion->not()->containsAny(Vector {'f', 'b'}); 283 | 284 | $assert->int($this->successCount)->eq(0); 285 | $assert->int($this->failEvents->count())->eq(1); 286 | } 287 | 288 | <> 289 | public function failsToNotContainAnyCustom(Assert $assert): void { 290 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 291 | 292 | $assertion->not()->containsAny(Vector {'d', 'f'}, ($a, $b) ==> true); 293 | 294 | $assert->int($this->successCount)->eq(0); 295 | $assert->int($this->failEvents->count())->eq(1); 296 | } 297 | 298 | <> 299 | public function doesContainOnly(Assert $assert): void { 300 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 301 | 302 | $assertion->containsOnly(Vector {'b', 'c', 'a'}); 303 | 304 | $assert->int($this->successCount)->eq(1); 305 | $assert->int($this->failEvents->count())->eq(0); 306 | } 307 | 308 | <> 309 | public function doesContainOnlyCustom(Assert $assert): void { 310 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 311 | 312 | $assertion->containsOnly(Vector {'d', 'c', 'a'}, ($a, $b) ==> true); 313 | 314 | $assert->int($this->successCount)->eq(1); 315 | $assert->int($this->failEvents->count())->eq(0); 316 | } 317 | 318 | <> 319 | public function doesNotContainOnly(Assert $assert): void { 320 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 321 | 322 | $assertion->not(); 323 | $assertion->containsOnly(Vector {}); 324 | $assertion->containsOnly(Vector {'g', 'a'}); 325 | $assertion->containsOnly(Vector {'g', 'c', 'a'}); 326 | $assertion->containsOnly(Vector {'b', 'g', 'c', 'a'}); 327 | 328 | $assert->int($this->successCount)->eq(4); 329 | $assert->int($this->failEvents->count())->eq(0); 330 | } 331 | 332 | <> 333 | public function doesNotContainOnlyCustom(Assert $assert): void { 334 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 335 | $assertion->not() 336 | ->containsOnly(Vector {'a', 'b', 'c'}, ($a, $b) ==> false); 337 | 338 | $assert->int($this->successCount)->eq(1); 339 | $assert->int($this->failEvents->count())->eq(0); 340 | } 341 | 342 | <> 343 | public function containsTooMany(Assert $assert): void { 344 | $assertion = $this->makeAssertion(Vector {'a', 'b', 'c'}); 345 | 346 | $assertion->containsOnly(Vector {'g', 'a'}); 347 | 348 | $assert->int($this->successCount)->eq(0); 349 | $assert->int($this->failEvents->count())->eq(1); 350 | $assert->string($this->failEvents->at(0)->getMessage()) 351 | ->is('Container contains more elements than expected.'); 352 | } 353 | 354 | <> 355 | public function containsTooFew(Assert $assert): void { 356 | $assertion = $this->makeAssertion(Vector {'a'}); 357 | 358 | $assertion->containsOnly(Vector {'g', 'a'}); 359 | 360 | $assert->int($this->successCount)->eq(0); 361 | $assert->int($this->failEvents->count())->eq(1); 362 | $assert->string($this->failEvents->at(0)->getMessage()) 363 | ->is('Container contains fewer elements than expected.'); 364 | } 365 | 366 | <> 367 | public function containsWrongElement(Assert $assert): void { 368 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 369 | 370 | $assertion->containsOnly(Vector {'c', 'a'}); 371 | 372 | $assert->int($this->successCount)->eq(0); 373 | $assert->int($this->failEvents->count())->eq(1); 374 | $assert->string($this->failEvents->at(0)->getMessage()) 375 | ->is('Container expected to contain '.var_export('c', true)); 376 | } 377 | 378 | <> 379 | public function failsToNotContainOnly(Assert $assert): void { 380 | $assertion = $this->makeAssertion(Vector {'a', 'b'}); 381 | 382 | $assertion->not()->containsOnly(Vector {'b', 'a'}); 383 | 384 | $assert->int($this->successCount)->eq(0); 385 | $assert->int($this->failEvents->count())->eq(1); 386 | } 387 | 388 | <> 389 | public function isEmpty(Assert $assert): void { 390 | $assertion = $this->makeAssertion([]); 391 | 392 | $assertion->isEmpty(); 393 | 394 | $assert->int($this->successCount)->eq(1); 395 | $assert->int($this->failEvents->count())->eq(0); 396 | } 397 | 398 | <> 399 | public function isNotEmpty(Assert $assert): void { 400 | $assertion = $this->makeAssertion([1]); 401 | 402 | $assertion->not()->isEmpty(); 403 | 404 | $assert->int($this->successCount)->eq(1); 405 | $assert->int($this->failEvents->count())->eq(0); 406 | } 407 | 408 | <> 409 | public function failsToBeEmpty(Assert $assert): void { 410 | $assertion = $this->makeAssertion([1]); 411 | 412 | $assertion->isEmpty(); 413 | 414 | $assert->int($this->successCount)->eq(0); 415 | $assert->int($this->failEvents->count())->eq(1); 416 | } 417 | 418 | <> 419 | public function failsToNotBeEmpty(Assert $assert): void { 420 | $assertion = $this->makeAssertion([]); 421 | 422 | $assertion->not()->isEmpty(); 423 | 424 | $assert->int($this->successCount)->eq(0); 425 | $assert->int($this->failEvents->count())->eq(1); 426 | } 427 | } 428 | --------------------------------------------------------------------------------