├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── hacktest └── hacktest.hack ├── composer.json ├── example.txt ├── hh_autoload.json ├── hhast-lint.json └── src ├── Exception ├── ExpectationFailedException.hack ├── InvalidDataProviderException.hack ├── InvalidTestClassException.hack ├── InvalidTestFileException.hack ├── InvalidTestMethodException.hack └── SkippedTestException.hack ├── Framework ├── DataProvider.hack ├── HackTest.hack └── TestGroup.hack ├── HackTestCLI.hack ├── ProgressEvent.hack ├── Retriever ├── ClassRetriever.hack └── FileRetriever.hack ├── Runner └── HackTestRunner.hack ├── _Private ├── CLIOutputHandler.hack ├── ConciseCLIOutput.hack ├── OnScopeExitAsync.hack ├── Progress.hack ├── Result.hack └── VerboseCLIOutput.hack └── enum ├── ExitCode.hack └── TestResult.hack /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. 2 | { 3 | "name": "Hack", 4 | "runArgs": [ 5 | "--init" 6 | ], 7 | "image": "hhvm/hhvm:latest", 8 | 9 | // Set *default* container specific settings.json values on container create. 10 | "userEnvProbe": "loginShell", 11 | 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": [ 14 | "pranayagarwal.vscode-hack" 15 | ], 16 | 17 | // Use 'postCreateCommand' to run commands after the container is created. 18 | "postCreateCommand": "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && composer install" 19 | 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | .hhconfig export-ignore 3 | .hhvmconfig.hdf export-ignore 4 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '42 15 * * *' 7 | jobs: 8 | build: 9 | name: HHVM ${{matrix.hhvm}} - ${{matrix.os}} 10 | strategy: 11 | # Run tests on all OS's and HHVM versions, even if one fails 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu ] 15 | hhvm: 16 | - '4.168' 17 | - latest 18 | - nightly 19 | runs-on: ${{matrix.os}}-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Create branch for version alias 23 | run: git checkout -b CI_current_pull_request 24 | - uses: hhvm/actions/hack-lint-test@master 25 | with: 26 | hhvm: ${{matrix.hhvm}} 27 | skip_tests: true 28 | - name: Run tests 29 | run: bin/hacktest tests/clean/ 30 | - id: create-repo-dir 31 | name: Create repository directory 32 | run: echo "::set-output name=path::$(mktemp -d)" 33 | - name: Run HHBC 34 | run: | 35 | # Exclude vendor/bin/ to work around issue in HHVM 4.62 36 | # https://github.com/facebook/hhvm/issues/8719 37 | hhvm --hphp -l 3 \ 38 | ${{ matrix.hhvm == '4.168' && '--module' || '--dir' }} bin \ 39 | ${{ matrix.hhvm == '4.168' && '--module' || '--dir' }} src \ 40 | ${{ matrix.hhvm == '4.168' && '--module' || '--dir' }} tests \ 41 | ${{ matrix.hhvm == '4.168' && '--module' || '--dir' }} vendor \ 42 | --inputs bin/hacktest \ 43 | --exclude-dir vendor/bin \ 44 | --output-dir ${{ steps.create-repo-dir.outputs.path }} 45 | - name: Run tests in repo-authoritative mode 46 | working-directory: ${{ steps.create-repo-dir.outputs.path }} 47 | run: | 48 | hhvm --no-config \ 49 | -d hhvm.repo.authoritative=true \ 50 | -d hhvm.repo.central.path=$(pwd)/hhvm.hhbc \ 51 | bin/hacktest tests/clean/ 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | *.swp 4 | *~ 5 | .*.hhast.*cache 6 | .var/ 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to HackTest 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | 7 | This project is developed in the 'master' branch on GitHub. Changes should be submitted as pull requests. 8 | 9 | Usually, versions will be tagged directly from master; if changes are needed to 10 | an old release, a branch will be cut from the previous release in that series - e.g. 11 | v1.2.3 might be tagged from a `1.2.x` branch. 12 | 13 | ## Pull Requests 14 | We actively welcome your pull requests. 15 | 1. Fork the repo and create your branch from `master`. 16 | 2. If you've added code that should be tested, add tests 17 | 3. Ensure the test suite passes on `tests/clean/` and the example still works 18 | 4. If you haven't already, complete the Contributor License Agreement ("CLA"). 19 | 20 | ## Contributor License Agreement ("CLA") 21 | In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. 22 | 23 | Complete your CLA here: 24 | 25 | ## Issues 26 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 27 | 28 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. 29 | 30 | ## Core Components 31 | 32 | - `Facebook\HackTest\HackTestCLI`: Kicks off the test runner and writes the results to `STDOUT`. Currently, only the verbose flag is supported. We hope to support different options as well as CLI modes in the future. 33 | - `Facebook\HackTest\HackTestRunner`: Creates and executes test cases given `PATH` arguments, returning the results to the CLI. 34 | - `Facebook\HackTest\HackTestCase`: An individual test case that uses `ReflectionClass` to retrieve and run all the test methods in a file. 35 | - `Facebook\HackTest/Retriever`: Classes that retrieve and validate test files and class names. 36 | 37 | ## Coding Style 38 | * 2 spaces for indentation rather than tabs 39 | * 80 character line length 40 | * Please be consistent with the existing code style 41 | 42 | `hackfmt` or in-IDE code formatting (in 3.27+) and `hhast-lint` will 43 | enforce most of the rules. 44 | 45 | ## License 46 | By contributing to HackTest, you agree that your contributions will be licensed under its MIT license. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2004-present, Facebook, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackTest 2 | 3 | [![Build Status](https://travis-ci.org/hhvm/hacktest.svg?branch=master)](https://travis-ci.org/hhvm/hacktest) 4 | 5 | HackTest is a unit test runner and base class. Assertions are provided 6 | by separate libraries, such [fbexpect](https://github.com/hhvm/fbexpect). 7 | 8 | ## Installation 9 | 10 | ``` 11 | php /path/to/composer.phar require --dev hhvm/hacktest facebook/fbexpect 12 | ``` 13 | 14 | ## Usage 15 | 16 | To run tests: 17 | 18 | ``` 19 | vendor/bin/hacktest [OPTIONS] tests/ 20 | ``` 21 | 22 | Tests are methods in classes, where: 23 | - the class name matches the file name 24 | - the class name ends with 'Test' 25 | - the method is public 26 | - the method name begins with 'test' 27 | 28 | Test methods can be async, and will automatically be awaited. 29 | 30 | Additionally, classes can implement several special methods: 31 | 32 | - `public static function beforeFirstTestAsync(): Awaitable` 33 | - `public static function afterLastTestAsync(): Awaitable` 34 | - `public function beforeEachTestAsync(): Awaitable` 35 | - `public function afterEachTestAsync(): Awaitable` 36 | 37 | Finally, for data-driven tests, the `<>` attribute can be used: 38 | 39 | ```Hack 40 | public function provideFoos(): vec<(string, int)> { 41 | return vec[ 42 | tuple('foo', 123), 43 | tuple('bar', 456), 44 | ]; 45 | } 46 | 47 | <> 48 | public function testFoos(string $a, int $b): void { 49 | .... 50 | } 51 | ``` 52 | 53 | ## Examples 54 | 55 | ### "I want to test all files in a directory" 56 | ``` 57 | $ vendor/bin/hacktest tests/clean/exit/ 58 | 59 | ... 60 | 61 | Summary: 3 test(s), 3 passed, 0 failed, 0 skipped, 0 error(s). 62 | ``` 63 | 64 | ### "I want to run all tests in a specific file" 65 | 66 | ``` 67 | $ vendor/bin/hacktest tests/dirty/DirtyAsyncTest.php 68 | 69 | FFF 70 | 71 | 1) DirtyAsyncTest::testWithNonNullableTypesAsync 72 | Failed asserting that Array &0 ( 73 | 0 => 1 74 | 1 => 'foo' 75 | ) is not identical to Array &0 ( 76 | 0 => 1 77 | 1 => 'foo' 78 | ). 79 | 80 | /fakepath/hacktest/tests/dirty/DirtyAsyncTest.php(22): Facebook\FBExpect\ExpectObj->toNotBeSame() 81 | /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyAsyncTest->testWithNonNullableTypesAsync() 82 | 83 | 2)... 84 | 85 | Summary: 3 test(s), 0 passed, 3 failed, 0 skipped, 0 error(s). 86 | ``` 87 | 88 | For an example in verbose mode, see [example.txt](example.txt) 89 | 90 | ## Contributing 91 | 92 | See [CONTRIBUTING.md](CONTRIBUTING.md). 93 | 94 | ## License 95 | 96 | The HackTest framework is MIT-licensed. 97 | -------------------------------------------------------------------------------- /bin/hacktest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | /* 3 | * Copyright (c) 2018-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | namespace Facebook\HackTest; 12 | 13 | <<__EntryPoint>> 14 | async function hack_test_main_async_UNSAFE(): Awaitable { 15 | (() ==> { 16 | // HHAST-generated to avoid pseudomain local leaks 17 | require_once(__FILE__.'.hack'); 18 | })(); 19 | await hack_test_main_async(); 20 | } 21 | -------------------------------------------------------------------------------- /bin/hacktest.hack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | /* 3 | * Copyright (c) 2018-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | namespace Facebook\HackTest; 12 | 13 | <<__EntryPoint>> 14 | async function hack_test_main_async(): Awaitable { 15 | $root = \dirname(__DIR__); 16 | $found_autoloader = false; 17 | while (true) { 18 | $file = $root.'/vendor/autoload.hack'; 19 | if (\HH\could_include($file)) { 20 | require_once($root.'/vendor/autoload.hack'); 21 | $found_autoloader = true; 22 | \Facebook\AutoloadMap\initialize(); 23 | break; 24 | } 25 | if ($root === '/') { 26 | break; 27 | } 28 | $root = \dirname($root); 29 | } 30 | 31 | if (!$found_autoloader) { 32 | \fprintf(\STDERR, "Failed to find autoloader.\n"); 33 | exit(1); 34 | } 35 | 36 | $exit_code = await HackTestCLI::runAsync(); 37 | exit($exit_code); 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hhvm/hacktest", 3 | "bin": ["bin/hacktest", "bin/hacktest.hack"], 4 | "description": "The Hack Test Library", 5 | "license": "MIT", 6 | "extra": { 7 | "branch-alias": { 8 | "dev-master": "2.x-dev", 9 | "dev-main": "2.x-dev", 10 | "dev-CI_current_pull_request": "2.x-dev" 11 | } 12 | }, 13 | "require-dev": { 14 | "facebook/fbexpect": "^2.8.1", 15 | "hhvm/hhvm-autoload": "^2.0.2|^3.0", 16 | "hhvm/hhast": "^4.0" 17 | }, 18 | "require": { 19 | "hhvm": "^4.168", 20 | "facebook/hh-clilib": "^2.5.0rc1", 21 | "hhvm/type-assert": "^3.0|^4.0" 22 | }, 23 | "config": { 24 | "allow-plugins": { 25 | "hhvm/hhvm-autoload": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example.txt: -------------------------------------------------------------------------------- 1 | $ bin/hacktest -v tests/ 2 | 3 | SS...........................................................................................................................................................................................................................................................................................................................................SS...........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................EEFFFEEEEFFFFFFFF 4 | 5 | 1) DataProviderTest::testProviderSkip 6 | Skipped: This test depends on a data provider that is not ready yet. 7 | 8 | 2) DataProviderTest::testProviderSkipDup 9 | Skipped: This test depends on a data provider that is not ready yet. 10 | 11 | 3) VecOrderTest::testShuffle with data set #1 (vec [ 12 | 8, 13 | 6, 14 | 7, 15 | 5, 16 | 3, 17 | 0, 18 | 9, 19 | ], vec [ 20 | 0, 21 | 3, 22 | 5, 23 | 6, 24 | 7, 25 | 8, 26 | 9, 27 | ]) 28 | Skipped: Mocking is not supported externally 29 | 30 | 4) VecOrderTest::testShuffle with data set #2 (HackLibTestForwardOnlyIterator::__set_state(array( 31 | 'used' => false, 32 | 'keyIdx' => 0, 33 | 'keys' => 34 | array ( 35 | 0 => 0, 36 | 1 => 1, 37 | 2 => 2, 38 | 3 => 3, 39 | 4 => 4, 40 | 5 => 5, 41 | 6 => 6, 42 | ), 43 | 'data' => 44 | dict [ 45 | 0 => 8, 46 | 1 => 6, 47 | 2 => 7, 48 | 3 => 5, 49 | 4 => 3, 50 | 5 => 0, 51 | 6 => 9, 52 | ], 53 | )), vec [ 54 | 0, 55 | 3, 56 | 5, 57 | 6, 58 | 7, 59 | 8, 60 | 9, 61 | ]) 62 | Skipped: Mocking is not supported externally 63 | 64 | 5) DirtyErrorTest::testInvariantException 65 | This should count as an error rather than a test failure 66 | 67 | #0 /fakepath/hacktest/tests/dirty/DirtyErrorTest.php(19): HH\invariant_violation() 68 | #1 /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyErrorTest->testInvariantException() 69 | #2 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync() 70 | #3 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 71 | #4 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 72 | #5 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 73 | #6 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 74 | #7 {main} 75 | 76 | 6) DirtyErrorTest::testArgumentCountError 77 | Too few arguments to function testArgumentCountError(), 0 passed and exactly 1 expected 78 | 79 | #0 /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyErrorTest->testArgumentCountError() 80 | #1 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync() 81 | #2 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 82 | #3 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 83 | #4 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 84 | #5 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 85 | #6 {main} 86 | 87 | 7) DirtyAsyncTest::testWithNonNullableTypesAsync 88 | Failed asserting that Array &0 ( 89 | 0 => 1 90 | 1 => 'foo' 91 | ) is not identical to Array &0 ( 92 | 0 => 1 93 | 1 => 'foo' 94 | ). 95 | 96 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/Not.php(98): PHPUnit_Framework_Constraint->fail() 97 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_Not->evaluate() 98 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1419): PHPUnit_Framework_Assert::assertThat() 99 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(352): PHPUnit_Framework_Assert::assertNotSame() 100 | #4 /fakepath/hacktest/tests/dirty/DirtyAsyncTest.php(22): Facebook\FBExpect\ExpectObj->toNotBeSame() 101 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyAsyncTest->testWithNonNullableTypesAsync() 102 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync() 103 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 104 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 105 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 106 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 107 | #11 {main} 108 | 109 | 8) DirtyAsyncTest::testWithNullLiteralsAsync 110 | Failed asserting that Array &0 ( 111 | 0 => 1 112 | 1 => null 113 | 2 => null 114 | ) is true. 115 | 116 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint.php(56): PHPUnit_Framework_Constraint->fail() 117 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint->evaluate() 118 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1094): PHPUnit_Framework_Assert::assertThat() 119 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(106): PHPUnit_Framework_Assert::assertTrue() 120 | #4 /fakepath/hacktest/tests/dirty/DirtyAsyncTest.php(35): Facebook\FBExpect\ExpectObj->toBeTrue() 121 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyAsyncTest->testWithNullLiteralsAsync() 122 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync() 123 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 124 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 125 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 126 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 127 | #11 {main} 128 | 129 | 9) DirtyAsyncTest::testWithNullableTypesAsync 130 | Failed asserting that Array &0 ( 131 | 0 => 1 132 | 1 => 'foo' 133 | ) is null. 134 | 135 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint.php(56): PHPUnit_Framework_Constraint->fail() 136 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint->evaluate() 137 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1144): PHPUnit_Framework_Assert::assertThat() 138 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(118): PHPUnit_Framework_Assert::assertNull() 139 | #4 /fakepath/hacktest/tests/dirty/DirtyAsyncTest.php(44): Facebook\FBExpect\ExpectObj->toBeNull() 140 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(43): DirtyAsyncTest->testWithNullableTypesAsync() 141 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync() 142 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 143 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 144 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 145 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 146 | #11 {main} 147 | 148 | 10) DirtyProviderTest::testNoData 149 | This test depends on a provider (provideNoData) that returns no data. 150 | 151 | #0 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 152 | #1 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 153 | #2 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 154 | #3 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 155 | #4 {main} 156 | 157 | 11) DirtyProviderTest::testNoDataDup 158 | This test depends on a provider (provideNoData) that returns no data. 159 | 160 | #0 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 161 | #1 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 162 | #2 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 163 | #3 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 164 | #4 {main} 165 | 166 | 12) DirtyProviderTest::testProviderError 167 | This test depends on a provider that throws an error. 168 | 169 | #0 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(54): HH\invariant_violation() 170 | #1 /fakepath/hacktest/src/Framework/HackTestCase.php(54): DirtyProviderTest->provideError() 171 | #2 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 172 | #3 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 173 | #4 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 174 | #5 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 175 | #6 {main} 176 | 177 | 13) DirtyProviderTest::testProviderErrorDup 178 | This test depends on a provider that throws an error. 179 | 180 | #0 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(54): HH\invariant_violation() 181 | #1 /fakepath/hacktest/src/Framework/HackTestCase.php(54): DirtyProviderTest->provideError() 182 | #2 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 183 | #3 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 184 | #4 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 185 | #5 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 186 | #6 {main} 187 | 188 | 14) DirtyProviderTest::testDirtyData with data set #1 vec [ 189 | 'the', 190 | 'quicky', 191 | 'brown', 192 | 'fox', 193 | 1, 194 | ] 195 | Failed asserting that two strings are identical. 196 | 197 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 198 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 199 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 200 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 201 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 202 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 203 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 204 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 205 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 206 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 207 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 208 | #11 {main} 209 | 210 | 15) DirtyProviderTest::testDirtyData with data set #2 HH\Vector { 211 | 'the', 212 | 'quicky', 213 | 'brown', 214 | 'fox', 215 | 1, 216 | } 217 | Failed asserting that two strings are identical. 218 | 219 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 220 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 221 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 222 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 223 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 224 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 225 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 226 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 227 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 228 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 229 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 230 | #11 {main} 231 | 232 | 16) DirtyProviderTest::testDirtyData with data set #3 HH\Set { 233 | 'the', 234 | 'quicky', 235 | 'brown', 236 | 'fox', 237 | 1, 238 | } 239 | Failed asserting that two strings are identical. 240 | 241 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 242 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 243 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 244 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 245 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 246 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 247 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 248 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 249 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 250 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 251 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 252 | #11 {main} 253 | 254 | 17) DirtyProviderTest::testDirtyData with data set #4 HH\Map { 255 | 0 => 'the', 256 | 1 => 'quicky', 257 | 2 => 'brown', 258 | 3 => 'fox', 259 | 4 => 1, 260 | } 261 | Failed asserting that two strings are identical. 262 | 263 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 264 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 265 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 266 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 267 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 268 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 269 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 270 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 271 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 272 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 273 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 274 | #11 {main} 275 | 276 | 18) DirtyProviderTest::testDirtyData with data set #5 vec [ 277 | 'the', 278 | 'quicky', 279 | 'brown', 280 | 'fox', 281 | 1, 282 | ] 283 | Failed asserting that two strings are identical. 284 | 285 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 286 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 287 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 288 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 289 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 290 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 291 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 292 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 293 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 294 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 295 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 296 | #11 {main} 297 | 298 | 19) DirtyProviderTest::testDirtyData with data set #6 keyset [ 299 | 'the', 300 | 'quicky', 301 | 'brown', 302 | 'fox', 303 | 1, 304 | ] 305 | Failed asserting that two strings are identical. 306 | 307 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 308 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 309 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 310 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 311 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 312 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 313 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 314 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 315 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 316 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 317 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 318 | #11 {main} 319 | 320 | 20) DirtyProviderTest::testDirtyData with data set #7 dict [ 321 | 0 => 'the', 322 | 1 => 'quicky', 323 | 2 => 'brown', 324 | 3 => 'fox', 325 | 4 => 1, 326 | ] 327 | Failed asserting that two strings are identical. 328 | 329 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 330 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 331 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 332 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 333 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 334 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 335 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 336 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 337 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 338 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 339 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 340 | #11 {main} 341 | 342 | 21) DirtyProviderTest::testDirtyData with data set #8 HackLibTestForwardOnlyIterator::__set_state(array( 343 | 'used' => false, 344 | 'keyIdx' => 0, 345 | 'keys' => 346 | array ( 347 | 0 => 0, 348 | 1 => 1, 349 | 2 => 2, 350 | 3 => 3, 351 | 4 => 4, 352 | ), 353 | 'data' => 354 | dict [ 355 | 0 => 'the', 356 | 1 => 'quicky', 357 | 2 => 'brown', 358 | 3 => 'fox', 359 | 4 => 1, 360 | ], 361 | )) 362 | Failed asserting that two strings are identical. 363 | 364 | #0 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php(88): PHPUnit_Framework_Constraint->fail() 365 | #1 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(2060): PHPUnit_Framework_Constraint_IsIdentical->evaluate() 366 | #2 /fakepath/hacktest/vendor/phpunit/phpunit/src/Framework/Assert.php(1379): PHPUnit_Framework_Assert::assertThat() 367 | #3 /fakepath/hacktest/vendor/facebook/fbexpect/src/ExpectObj.php(100): PHPUnit_Framework_Assert::assertSame() 368 | #4 /fakepath/hacktest/tests/dirty/DirtyProviderTest.php(36): Facebook\FBExpect\ExpectObj->toBeSame() 369 | #5 /fakepath/hacktest/src/Framework/HackTestCase.php(78): DirtyProviderTest->testDirtyData() 370 | #6 /fakepath/hacktest/src/Framework/HackTestCase.php(84): Closure$Facebook\HackTest\HackTestCase::runAsync#2() 371 | #7 /fakepath/hacktest/src/Runner/HackTestRunner.php(29): Facebook\HackTest\HackTestCase->runAsync() 372 | #8 /fakepath/hacktest/src/HackTestCLI.php(48): Facebook\HackTest\HackTestRunner::runAsync() 373 | #9 /fakepath/hacktest/vendor/facebook/hh-clilib/src/CLIBase.hh(168): Facebook\HackTest\HackTestCLI->mainAsync() 374 | #10 /fakepath/hacktest/bin/hacktest(8): Facebook\CLILib\CLIBase::main() 375 | #11 {main} 376 | 377 | Summary: 955 test(s), 934 passed, 11 failed, 4 skipped, 6 error(s). 378 | -------------------------------------------------------------------------------- /hh_autoload.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "src/" 4 | ], 5 | "devRoots": [ 6 | "tests/" 7 | ], 8 | "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler", 9 | "useFactsIfAvailable": true 10 | } 11 | -------------------------------------------------------------------------------- /hhast-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ "src/", "tests/" ], 3 | "builtinLinters": "all", 4 | "disabledLinters": [ 5 | "Facebook\\HHAST\\UseStatementWithAsLinter", 6 | "Facebook\\HHAST\\Linters\\UseStatementWithAsLinter" 7 | ], 8 | "overrides": [ 9 | { 10 | "patterns": ["tests/clean/hsl/*"], 11 | "disableAllLinters": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/ExpectationFailedException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class ExpectationFailedException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Exception/InvalidDataProviderException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class InvalidDataProviderException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Exception/InvalidTestClassException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class InvalidTestClassException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Exception/InvalidTestFileException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class InvalidTestFileException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Exception/InvalidTestMethodException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class InvalidTestMethodException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Exception/SkippedTestException.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | final class SkippedTestException extends \RuntimeException {} 13 | -------------------------------------------------------------------------------- /src/Framework/DataProvider.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | // @lint-ignore-every NAMESPACES 11 | 12 | namespace Facebook\HackTest; 13 | 14 | /** 15 | * Identifies the method that provides data to a test method. 16 | * 17 | * Used on 'testFoo' methods in HackTest classes. 18 | * 19 | * @example 20 | * 21 | * class MyClassTest extends HackTest { 22 | * public function fooData(): vec<(string, int)> { 23 | * return vec[ 24 | * tuple('foo', 123), 25 | * tuple('bar', 456), 26 | * ]; 27 | * } 28 | * 29 | * <> 30 | * public function testFoo(string $arg1, int $arg2) { 31 | * // code 32 | * } 33 | * } 34 | * 35 | */ 36 | final class DataProvider implements \HH\MethodAttribute { 37 | public function __construct( 38 | /** The name of a public method providing parameters for a test */ 39 | public string $provider, 40 | ) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Framework/HackTest.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | // @lint-ignore-every NAMESPACES 11 | // @lint-ignore-every HackLint5583 await in loop for tests 12 | /* HHAST_IGNORE_ALL[DontAwaitInALoop] */ 13 | 14 | namespace Facebook\HackTest; 15 | 16 | use namespace HH\Lib\{C, Str, Vec}; 17 | 18 | <<__ConsistentConstruct>> 19 | abstract class HackTest { 20 | 21 | private vec<\ReflectionMethod> $methods = vec[]; 22 | 23 | private ?string $expectedException = null; 24 | private ?string $expectedExceptionMessage = null; 25 | private ?int $expectedExceptionCode = null; 26 | private bool $setUpNeeded = true; 27 | const bool ALLOW_STATIC_TEST_METHODS = false; 28 | 29 | <<__LateInit>> private string $filename; 30 | 31 | public final function __construct() { 32 | $class = new \ReflectionClass($this); 33 | $this->filename = $class->getFileName() as string; 34 | $this->methods = Vec\filter( 35 | $class->getMethods(), 36 | $method ==> Str\starts_with($method->getName(), 'test'), 37 | ); 38 | $this->filterTestMethods(); 39 | $this->validateTestMethods(); 40 | } 41 | 42 | final public function getTestMethods(): vec<\ReflectionMethod> { 43 | return $this->methods; 44 | } 45 | 46 | const type TFilters = shape( 47 | 'methods' => (function(string): string), 48 | // TODO: dataproviders 49 | ); 50 | 51 | public final async function runTestsAsync( 52 | (function(\ReflectionMethod): bool) $method_filter, 53 | (function(ProgressEvent): Awaitable) $progress_callback, 54 | ): Awaitable { 55 | $progress = 56 | new _Private\Progress($progress_callback, $this->filename, static::class); 57 | 58 | await static::beforeFirstTestAsync(); 59 | await using new _Private\OnScopeExitAsync(static::afterLastTestAsync<>); 60 | 61 | foreach ($this->methods as $method) { 62 | $to_run = vec[]; 63 | if (!$method_filter($method)) { 64 | continue; 65 | } 66 | 67 | $this->clearExpectedException(); 68 | $exception = $method->getAttribute('ExpectedException'); 69 | if ($exception !== null) { 70 | $exception_message = $method->getAttribute('ExpectedExceptionMessage'); 71 | $msg = null; 72 | $code = null; 73 | if ($exception_message !== null) { 74 | $msg = (string)C\onlyx($exception_message); 75 | } 76 | $exception_code = $method->getAttribute('ExpectedExceptionCode'); 77 | if ($exception_code !== null) { 78 | $code = (string)C\onlyx($exception_code); 79 | } 80 | $this->setExpectedException((string)C\onlyx($exception), $msg, $code); 81 | } 82 | 83 | $providers = vec[]; 84 | $provider = $method->getAttributeClass(DataProvider::class)?->provider; 85 | if ($provider is nonnull) { 86 | $providers[] = $provider; 87 | } 88 | 89 | $method_name = $method->getName(); 90 | if (C\is_empty($providers)) { 91 | /* HH_IGNORE_ERROR[2011] this is unsafe */ 92 | $to_run[] = tuple($method_name, null, () ==> $this->$method_name()); 93 | } else { 94 | if (C\count($providers) > 1) { 95 | throw new InvalidTestMethodException( 96 | Str\format( 97 | 'There can only be one data provider in %s', 98 | $method_name, 99 | ), 100 | ); 101 | } 102 | await $progress->invokingDataProviderAsync($method_name); 103 | $provider = C\onlyx($providers); 104 | await $this->beforeEachTestAsync(); 105 | $this->setUpNeeded = false; 106 | try { 107 | if (Str\contains($provider, '::')) { 108 | list($class, $method) = Str\split($provider, '::', 2); 109 | $rm = new \ReflectionMethod($class, $method); 110 | $tuples = $rm->invoke(null); 111 | } else { 112 | $rm = new \ReflectionMethod($this, $provider); 113 | if ($rm->isStatic()) { 114 | $tuples = $rm->invoke(null); 115 | } else { 116 | $tuples = $rm->invoke($this); 117 | } 118 | } 119 | if ($tuples is Awaitable<_>) { 120 | $tuples = await $tuples; 121 | } 122 | $tuples = $tuples as KeyedContainer<_, _>; 123 | if (C\is_empty($tuples)) { 124 | throw new InvalidDataProviderException( 125 | Str\format( 126 | 'This test depends on a provider (%s) that returns no data.', 127 | $provider, 128 | ), 129 | ); 130 | } 131 | } catch (\Throwable $e) { 132 | await $this->afterEachTestAsync(); 133 | await $progress->testFinishedWithExceptionAsync($provider, null, $e); 134 | continue; 135 | } 136 | 137 | foreach ($tuples as $idx => $tuple) { 138 | $tuple = vec($tuple as Traversable<_>); 139 | $to_run[] = tuple( 140 | $method_name, 141 | tuple($idx as arraykey, $tuple as Container<_>), 142 | /* HH_IGNORE_ERROR[2011] this is unsafe */ 143 | () ==> $this->$method_name(...$tuple), 144 | ); 145 | } 146 | } 147 | 148 | foreach ($to_run as list($method, $data_provider_row, $runnable)) { 149 | await $progress->testStartingAsync($method, $data_provider_row); 150 | if ($this->setUpNeeded) { 151 | await $this->beforeEachTestAsync(); 152 | } else { 153 | $this->setUpNeeded = true; 154 | } 155 | if ($exception === null) { 156 | $this->clearExpectedException(); 157 | } 158 | $clean = false; 159 | try { 160 | $res = $runnable(); 161 | if ($res is Awaitable<_>) { 162 | await $res; 163 | } 164 | /* HH_IGNORE_ERROR[6002] this is used in catch block */ 165 | $clean = true; 166 | await $this->afterEachTestAsync(); 167 | if ($this->expectedException !== null) { 168 | throw new ExpectationFailedException( 169 | Str\format( 170 | 'Failed asserting that %s was thrown', 171 | $this->expectedException, 172 | ), 173 | ); 174 | } 175 | await $progress->testPassedAsync($method, $data_provider_row); 176 | } catch (\Throwable $e) { 177 | if (!$clean) { 178 | await $this->afterEachTestAsync(); 179 | } 180 | $pass = false; 181 | if ( 182 | $this->expectedException !== null && 183 | !($e is SkippedTestException) && 184 | \is_a($e, $this->expectedException) 185 | ) { 186 | $pass = true; 187 | $message = (string)$e->getMessage(); 188 | $expected_message = (string)$this->expectedExceptionMessage; 189 | if (!Str\contains($message, $expected_message)) { 190 | $e = new ExpectationFailedException( 191 | Str\format( 192 | 'Failed asserting that the exception message \'%s\' contains \'%s\'', 193 | $e->getMessage(), 194 | $expected_message, 195 | ), 196 | ); 197 | $pass = false; 198 | } else if ( 199 | $this->expectedExceptionCode !== null && 200 | $this->expectedExceptionCode !== $e->getCode() 201 | ) { 202 | $exception_code = (int)$this->expectedExceptionCode; 203 | $e = new ExpectationFailedException( 204 | Str\format( 205 | 'Failed asserting that the exception code %d is equal to %d', 206 | (int)$e->getCode(), 207 | $exception_code, 208 | ), 209 | ); 210 | $pass = false; 211 | } 212 | } 213 | if ($pass) { 214 | await $progress->testPassedAsync($method, $data_provider_row); 215 | } else { 216 | await $progress->testFinishedWithExceptionAsync( 217 | $method, 218 | $data_provider_row, 219 | $e, 220 | ); 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | private function filterTestMethods(): void { 228 | $methods = vec[]; 229 | foreach ($this->methods as $method) { 230 | $type_text = $method->getReturnTypeText(); 231 | if ($type_text === false) { 232 | // nothing we can really do if a method begins with 'test' and has no return type hint 233 | $methods[] = $method; 234 | continue; 235 | } 236 | $type = Str\replace($type_text, 'HH\\', ''); 237 | if ($type === 'void' || $type === 'Awaitable') { 238 | $methods[] = $method; 239 | } 240 | } 241 | $this->methods = $methods; 242 | } 243 | 244 | private function validateTestMethods(): void { 245 | foreach ($this->methods as $method) { 246 | $method_name = $method->getName(); 247 | if (!$method->isPublic()) { 248 | throw new InvalidTestMethodException( 249 | Str\format('Test method (%s) must be public', $method_name), 250 | ); 251 | } 252 | if (!static::ALLOW_STATIC_TEST_METHODS && $method->isStatic()) { 253 | throw new InvalidTestMethodException( 254 | Str\format('Test method (%s) cannot be static', $method_name), 255 | ); 256 | } 257 | } 258 | } 259 | 260 | /** 261 | * Preferred over markTestSkipped(), because the typechecker considers 262 | * code past markTestSkipped() to be unreachable. This triggers inference 263 | * bugs when dealing with coeffects @see tests/ShowCoeffectViolationTest.php. 264 | * If you ever want to make your test not be skipped anymore, the code will 265 | * be reachable again, which may reveal previously hidden errors introduced 266 | * by code changes made whilst the test was skipped. 267 | * 268 | * You can use `return static::markAsSkipped('skipping');` to match the old 269 | * `static::markTestSkipped('skipping')` behavior. 270 | */ 271 | public static final function markAsSkipped(string $message): nothing { 272 | self::markTestSkipped($message); 273 | } 274 | 275 | /** 276 | * Code below this invocation will become unreachable for the typechecker. 277 | * This reduces the strength of typechecking and may raise baseless type errors. 278 | * @see markAsSkipped() 279 | */ 280 | public static final function markTestSkipped(string $message): noreturn { 281 | throw new SkippedTestException($message); 282 | } 283 | 284 | /** 285 | * Preferred over markTestIncomplete(), @see markAsSkipped() for rationale. 286 | */ 287 | public static final function markAsIncomplete(string $message): nothing { 288 | self::markTestIncomplete($message); 289 | } 290 | 291 | /** 292 | * Code below this invocation will become unreachable for the typechecker. 293 | * This reduces the strength of typechecking and may raise baseless type errors. 294 | * @see markAsIncomplete() 295 | */ 296 | public static function markTestIncomplete(string $message): noreturn { 297 | throw new SkippedTestException($message); 298 | } 299 | 300 | /** 301 | * Preferred over fail(), @see markAsSkipped() for rationale. 302 | */ 303 | public static final function markAsFailed(string $message = ''): nothing { 304 | self::fail($message); 305 | } 306 | 307 | /** 308 | * Code below this invocation will become unreachable for the typechecker. 309 | * This reduces the strength of typechecking and may raise baseless type errors. 310 | * @see markAsFailed() 311 | */ 312 | public static final function fail(string $message = ''): noreturn { 313 | throw new \RuntimeException($message); 314 | } 315 | 316 | public final function setExpectedException( 317 | string $exception, 318 | ?string $exception_message = '', 319 | mixed $exception_code = null, 320 | ): void { 321 | $this->expectedException = $exception; 322 | $this->expectedExceptionMessage = $exception_message; 323 | $this->expectedExceptionCode = 324 | static::computeExpectedExceptionCode($exception_code); 325 | } 326 | 327 | private function clearExpectedException(): void { 328 | $this->expectedException = null; 329 | $this->expectedExceptionMessage = null; 330 | $this->expectedExceptionCode = null; 331 | } 332 | 333 | public static function computeExpectedExceptionCode( 334 | mixed $exception_code, 335 | ): ?int { 336 | if ($exception_code is int) { 337 | return $exception_code; 338 | } 339 | if (!($exception_code is string)) { 340 | return null; 341 | } 342 | $int = Str\to_int($exception_code); 343 | if ($int !== null) { 344 | return $int; 345 | } 346 | 347 | // can't handle arbitrary enums for open source 348 | return null; 349 | } 350 | 351 | public async function beforeEachTestAsync(): Awaitable {} 352 | public async function afterEachTestAsync(): Awaitable {} 353 | public static async function beforeFirstTestAsync(): Awaitable {} 354 | public static async function afterLastTestAsync(): Awaitable {} 355 | } 356 | -------------------------------------------------------------------------------- /src/Framework/TestGroup.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | use namespace HH\Lib\C; 13 | 14 | /** Mark a test as a member of a particular group. 15 | * 16 | * 17 | * @example 18 | * 19 | * <> 20 | * public function testFoo(): void { 21 | * } 22 | */ 23 | final class TestGroup implements \HH\MethodAttribute { 24 | private keyset $groups; 25 | 26 | public function __construct(string ...$groups) { 27 | $this->groups = keyset($groups); 28 | } 29 | 30 | public function contains(string $group): bool { 31 | return C\contains_key($this->groups, $group); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/HackTestCLI.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | use type Facebook\CLILib\CLIWithArguments; 13 | use namespace Facebook\CLILib\CLIOptions; 14 | use namespace HH\Lib\{C, Math, Str}; 15 | 16 | /** The main `hacktest` CLI */ 17 | final class HackTestCLI extends CLIWithArguments { 18 | private bool $verbose = false; 19 | private ?string $classFilter = null; 20 | private ?HackTestRunner::TMethodFilter $methodFilter = null; 21 | 22 | <<__Override>> 23 | public static function getHelpTextForOptionalArguments(): string { 24 | return 'SOURCE_PATH'; 25 | } 26 | 27 | <<__Override>> 28 | protected function getSupportedOptions(): vec { 29 | return vec[ 30 | CLIOptions\with_required_string( 31 | $f ==> { 32 | $this->classFilter = $f; 33 | }, 34 | 'Filter test class names with the specified glob pattern', 35 | '--filter-classes', 36 | ), 37 | CLIOptions\with_required_string( 38 | $f ==> { 39 | $mf = $this->methodFilter; 40 | $impl = (mixed $_class, \ReflectionMethod $method) ==> 41 | \fnmatch($f, $method->getName()); 42 | $this->methodFilter = $mf 43 | ? ( 44 | (classname $c, \ReflectionMethod $m) ==> 45 | $mf($c, $m) && $impl($c, $m) 46 | ) 47 | : $impl; 48 | }, 49 | 'Filter test method names with the specified glob pattern', 50 | '--filter-methods', 51 | ), 52 | CLIOptions\with_required_string( 53 | $groups ==> { 54 | $groups = Str\split($groups, ',') |> keyset($$); 55 | $mf = $this->methodFilter; 56 | $impl = (mixed $_class, \ReflectionMethod $method) ==> { 57 | $attr = $method->getAttributeClass(TestGroup::class); 58 | if ($attr === null) { 59 | return false; 60 | } 61 | return C\any($groups, $group ==> $attr->contains($group)); 62 | }; 63 | $this->methodFilter = $mf 64 | ? ( 65 | (classname $c, \ReflectionMethod $m) ==> 66 | $mf($c, $m) && $impl($c, $m) 67 | ) 68 | : $impl; 69 | }, 70 | 'Only run tests with a specified <> (comma-separated)', 71 | '--filter-groups', 72 | '-g', 73 | ), 74 | CLIOptions\flag( 75 | () ==> { 76 | $this->verbose = true; 77 | }, 78 | 'Increase output verbosity', 79 | '--verbose', 80 | '-v', 81 | ), 82 | ]; 83 | } 84 | 85 | <<__Override>> 86 | public async function mainAsync(): Awaitable { 87 | $cf = $this->classFilter; 88 | $mf = $this->methodFilter; 89 | $output = $this->verbose 90 | ? new _Private\VerboseCLIOutput($this->getTerminal()) 91 | : new _Private\ConciseCLIOutput($this->getTerminal()); 92 | $stdout = $this->getStdout(); 93 | 94 | $arguments = $this->getArguments(); 95 | if (C\is_empty($arguments)) { 96 | if (!\is_dir('tests')) { 97 | await $this->getStderr() 98 | ->writeAllAsync("SOURCE_PATH must be specified.\n"); 99 | return ExitCode::FAILURE; 100 | } 101 | await $this->getStderr()->writeAllAsync( 102 | Str\format( 103 | "No SOURCE_PATH was provided, but tests/ was found.\n". 104 | "Executing all tests found in the %s directory.\n", 105 | \getcwd().'/tests', 106 | ), 107 | ); 108 | $arguments = vec['tests/']; 109 | } 110 | 111 | await HackTestRunner::runAsync( 112 | $arguments, 113 | shape( 114 | 'classes' => ($cf is null ? ($_ ==> true) : ($c ==> \fnmatch($cf, $c))), 115 | 'methods' => ($mf ?? ($_class, $_method) ==> true), 116 | ), 117 | async $event ==> await $output->writeProgressAsync($stdout, $event), 118 | ); 119 | 120 | $result_counts = $output->getResultCounts(); 121 | 122 | if (Math\sum($result_counts) === 0) { 123 | await $this->getStderr()->writeAllAsync("No tests found.\n"); 124 | return ExitCode::ERROR; 125 | } 126 | 127 | if (($result_counts[TestResult::ERROR] ?? 0) > 0) { 128 | return ExitCode::ERROR; 129 | } 130 | if (($result_counts[TestResult::FAILED] ?? 0) > 0) { 131 | return ExitCode::FAILURE; 132 | } 133 | return ExitCode::SUCCESS; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ProgressEvent.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | abstract class ProgressEvent { 13 | <<__ReturnDisposable>> 14 | final public function onScopeExit( 15 | (function(this): Awaitable) $callback, 16 | ): \IAsyncDisposable { 17 | return new _Private\OnScopeExitAsync(async () ==> await $callback($this)); 18 | } 19 | } 20 | 21 | final class TestRunStartedProgressEvent extends ProgressEvent {} 22 | final class TestRunFinishedProgressEvent extends ProgressEvent {} 23 | 24 | abstract class FileProgressEvent extends ProgressEvent { 25 | public function __construct(private string $path) { 26 | } 27 | 28 | final public function getPath(): string { 29 | return $this->path; 30 | } 31 | } 32 | 33 | interface ErrorProgressEvent { 34 | require extends ProgressEvent; 35 | public function getException(): \Throwable; 36 | } 37 | 38 | final class FileErrorProgressEvent 39 | extends FileProgressEvent 40 | implements ErrorProgressEvent { 41 | public function __construct(string $path, private \Throwable $ex) { 42 | parent::__construct($path); 43 | } 44 | 45 | public function getException(): \Throwable { 46 | return $this->ex; 47 | } 48 | } 49 | 50 | abstract class ClassProgressEvent extends FileProgressEvent { 51 | public function __construct( 52 | string $path, 53 | private classname $class, 54 | ) { 55 | parent::__construct($path); 56 | } 57 | 58 | final public function getClassname(): classname { 59 | return $this->class; 60 | } 61 | } 62 | 63 | final class StartingTestClassEvent extends ClassProgressEvent {} 64 | final class FinishedTestClassEvent extends ClassProgressEvent {} 65 | 66 | abstract class TestProgressEvent extends ClassProgressEvent { 67 | public function __construct( 68 | string $path, 69 | classname $class, 70 | private string $testMethod, 71 | ) { 72 | parent::__construct($path, $class); 73 | } 74 | 75 | public function getTestMethod(): string { 76 | return $this->testMethod; 77 | } 78 | } 79 | 80 | final class InvokingDataProvidersProgressEvent extends TestProgressEvent {} 81 | 82 | abstract class TestInstanceProgressEvent extends TestProgressEvent { 83 | public function __construct( 84 | string $path, 85 | classname $class, 86 | string $testMethod, 87 | private ?(arraykey, Container) $dataProviderRow, 88 | ) { 89 | parent::__construct($path, $class, $testMethod); 90 | } 91 | 92 | final public function getDataProviderRow(): ?(arraykey, Container) { 93 | return $this->dataProviderRow; 94 | } 95 | } 96 | 97 | final class TestStartingProgressEvent extends TestInstanceProgressEvent {} 98 | 99 | abstract class TestFinishedProgressEvent extends TestInstanceProgressEvent { 100 | abstract public function getResult(): TestResult; 101 | } 102 | 103 | final class TestPassedProgressEvent extends TestFinishedProgressEvent { 104 | <<__Override>> 105 | public function getResult(): TestResult { 106 | return TestResult::PASSED; 107 | } 108 | } 109 | 110 | final class TestSkippedProgressEvent extends TestFinishedProgressEvent { 111 | <<__Override>> 112 | public function getResult(): TestResult { 113 | return TestResult::SKIPPED; 114 | } 115 | } 116 | 117 | abstract class TestFinishedWithExceptionProgressEvent 118 | extends TestFinishedProgressEvent 119 | implements ErrorProgressEvent { 120 | public function __construct( 121 | string $path, 122 | classname $class, 123 | string $testMethod, 124 | ?(arraykey, Container) $dataProviderRow, 125 | private \Throwable $ex, 126 | ) { 127 | parent::__construct($path, $class, $testMethod, $dataProviderRow); 128 | } 129 | 130 | public function getException(): \Throwable { 131 | return $this->ex; 132 | } 133 | } 134 | 135 | final class TestFailedProgressEvent 136 | extends TestFinishedWithExceptionProgressEvent { 137 | <<__Override>> 138 | public function getResult(): TestResult { 139 | return TestResult::FAILED; 140 | } 141 | } 142 | 143 | final class TestErroredProgressEvent 144 | extends TestFinishedWithExceptionProgressEvent { 145 | <<__Override>> 146 | public function getResult(): TestResult { 147 | return TestResult::ERROR; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Retriever/ClassRetriever.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | use namespace Facebook\TypeAssert; 13 | use namespace HH\Lib\{C, Dict, Keyset, Str, Vec}; 14 | use type Facebook\HackTest\_Private\{ 15 | ResultOrException, 16 | WrappedException, 17 | WrappedResult, 18 | }; 19 | 20 | final class ClassRetriever { 21 | const type TFacts = shape( 22 | 'types' => vec string, 24 | ?'baseTypes' => vec, 25 | 'kindOf' => string, 26 | ... 27 | )>, 28 | ... 29 | ); 30 | 31 | public function __construct( 32 | private string $filename, 33 | private keyset $caseInsensitiveClassnames, 34 | ) { 35 | } 36 | 37 | public static function forFile(string $path): ClassRetriever { 38 | $res = C\onlyx(self::forFiles(keyset[$path])); 39 | if ($res is WrappedResult<_>) { 40 | return $res->getResult(); 41 | } 42 | throw ($res as WrappedException<_>)->getException(); 43 | } 44 | 45 | public static function forFiles( 46 | keyset $paths, 47 | ): dict> { 48 | if (\ini_get('hhvm.repo.authoritative')) { 49 | return Dict\map( 50 | $paths, 51 | $path ==> \Facebook\AutoloadMap\Generated\map()['class'] 52 | |> Dict\filter($$, $class_path ==> $class_path === $path) 53 | |> Keyset\keys($$) 54 | |> new WrappedResult(new self($path, $$)), 55 | ); 56 | } 57 | 58 | $all_facts = \HH\facts_parse( 59 | /* root = */ '/', 60 | vec($paths), 61 | /* force_hh = */ false, 62 | /* multithreaded = */ true, 63 | ); 64 | return Dict\map( 65 | $paths, 66 | $path ==> { 67 | try { 68 | $file_facts = TypeAssert\matches_type_structure( 69 | type_structure(self::class, 'TFacts'), 70 | $all_facts[$path], 71 | ); 72 | return new WrappedResult(new self( 73 | $path, 74 | Keyset\map($file_facts['types'], $type ==> $type['name']), 75 | )); 76 | } catch (TypeAssert\IncorrectTypeException $e) { 77 | return new WrappedException( 78 | new InvalidTestFileException('Could not parse file.'), 79 | ); 80 | } 81 | }, 82 | ); 83 | } 84 | 85 | public function getTestClassName(): ?classname { 86 | $test_classes = $this->caseInsensitiveClassnames 87 | |> Vec\filter( 88 | $$, 89 | $name ==> \is_subclass_of($name, HackTest::class, true), 90 | ); 91 | 92 | $count = C\count($test_classes); 93 | if ($count !== 1) { 94 | $all_classes = ''; 95 | if (!C\is_empty($this->caseInsensitiveClassnames)) { 96 | $all_classes = ':'; 97 | foreach ($this->caseInsensitiveClassnames as $cn) { 98 | $rc = new \ReflectionClass($cn); 99 | $all_classes .= "\n - ".$rc->getName(); 100 | if ($rc->isSubclassOf(HackTest::class)) { 101 | $all_classes .= ' (is a test class)'; 102 | } else { 103 | $all_classes .= ' (is not a subclass of '.HackTest::class.')'; 104 | } 105 | } 106 | } 107 | throw new InvalidTestClassException( 108 | Str\format( 109 | 'There must be exactly one test class in %s; found %d%s', 110 | $this->filename, 111 | $count, 112 | $all_classes, 113 | ), 114 | ); 115 | } 116 | 117 | $rc = new \ReflectionClass(C\onlyx($test_classes)); 118 | if ($rc->isAbstract()) { 119 | return null; 120 | } 121 | $name = $rc->getName(); // fixes capitalization 122 | 123 | $class_name = $name 124 | |> Str\split($$, '\\') 125 | |> C\lastx($$); 126 | $filename = $this->filename 127 | |> Str\split($$, '/') 128 | |> C\lastx($$) 129 | |> Str\split($$, '.') 130 | |> C\firstx($$); 131 | 132 | if ($class_name !== $filename) { 133 | throw new InvalidTestClassException( 134 | Str\format( 135 | 'Class name (%s) must match filename (%s)', 136 | $class_name, 137 | $filename, 138 | ), 139 | ); 140 | } 141 | if (!Str\ends_with($class_name, 'Test')) { 142 | throw new InvalidTestClassException( 143 | Str\format('Class name (%s) must end with Test', $class_name), 144 | ); 145 | } 146 | $classname = $this->convertToClassname($name); 147 | if ($classname === null) { 148 | throw new InvalidTestClassException( 149 | Str\format('%s does not extend %s', $name, HackTest::class), 150 | ); 151 | } 152 | 153 | return $classname; 154 | } 155 | 156 | private function convertToClassname(string $name): ?classname { 157 | try { 158 | return TypeAssert\classname_of(HackTest::class, $name); 159 | } catch (TypeAssert\IncorrectTypeException $_) { 160 | return null; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Retriever/FileRetriever.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | use namespace HH\Lib\{Keyset, Regex, Str}; 13 | 14 | final class FileRetriever { 15 | 16 | public function __construct(private string $path) { 17 | $this->path = Str\strip_suffix($this->path, '/'); 18 | } 19 | 20 | public function getTestFiles(): keyset { 21 | if (\ini_get('hhvm.repo.authoritative')) { 22 | return \Facebook\AutoloadMap\Generated\map()['class'] 23 | |> Keyset\filter( 24 | $$, 25 | $filename ==> ( 26 | $filename === $this->path || 27 | Str\starts_with($filename, $this->path.'/') 28 | ) && 29 | $this->isTestFile($filename), 30 | ); 31 | } 32 | 33 | $path = \realpath($this->path); 34 | if (!$path) { 35 | throw new InvalidTestFileException( 36 | Str\format('File or directory (%s) not found', $this->path), 37 | ); 38 | } 39 | $files = keyset[]; 40 | if (!\is_dir($path)) { 41 | $file = $path; 42 | if (!\is_file($file)) { 43 | throw new InvalidTestFileException( 44 | Str\format('File (%s) not found', $file), 45 | ); 46 | } 47 | if ($this->isTestFile($file)) { 48 | return keyset[$file]; 49 | } 50 | throw new InvalidTestFileException( 51 | Str\format( 52 | "Asked to run tests in %s, but it does not end in 'Test.hack' or ". 53 | 'or a legacy extension.', 54 | $file, 55 | ), 56 | ); 57 | } 58 | $rii = new \RecursiveIteratorIterator( 59 | new \RecursiveDirectoryIterator($path), 60 | ); 61 | 62 | foreach ($rii as $file) { 63 | $filename = $file->getPathname(); 64 | if (!$file->isDir() && $this->isTestFile($filename)) { 65 | $files[] = $filename; 66 | } 67 | } 68 | 69 | return $files; 70 | } 71 | 72 | private function isTestFile(string $filename): bool { 73 | return Regex\matches($filename, re"/Test(\.php|\.hh|\.hack|\.hck)$/"); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Runner/HackTestRunner.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | use namespace HH\Lib\Keyset; 13 | 14 | /* HHAST_IGNORE_ALL[DontAwaitInALoop] */ 15 | 16 | abstract final class HackTestRunner { 17 | const type TMethodFilter = (function( 18 | classname, 19 | \ReflectionMethod, 20 | ): bool); 21 | const type TFilters = shape( 22 | 'classes' => (function(classname): bool), 23 | 'methods' => this::TMethodFilter, 24 | ); 25 | 26 | public static async function runAsync( 27 | vec $paths, 28 | this::TFilters $filters, 29 | (function(ProgressEvent): Awaitable) $progress_callback, 30 | ): Awaitable { 31 | await $progress_callback(new TestRunStartedProgressEvent()); 32 | await using ( 33 | (new TestRunFinishedProgressEvent())->onScopeExit($progress_callback) 34 | ); 35 | 36 | $files = keyset[]; 37 | foreach ($paths as $path) { 38 | $files = Keyset\union($files, (new FileRetriever($path))->getTestFiles()); 39 | } 40 | 41 | $classes_or_exceptions = ClassRetriever::forFiles($files); 42 | $classes = vec[]; 43 | foreach ($classes_or_exceptions as $path => $coe) { 44 | if ($coe is _Private\WrappedResult<_>) { 45 | try { 46 | $classes[] = tuple($path, $coe->getResult()->getTestClassName()); 47 | } catch (InvalidTestClassException $ex) { 48 | await $progress_callback(new FileErrorProgressEvent($path, $ex)); 49 | } 50 | continue; 51 | } 52 | $wex = $coe as _Private\WrappedException<_>; 53 | await $progress_callback(new FileErrorProgressEvent( 54 | $path, 55 | $wex->getException() as InvalidTestFileException, 56 | )); 57 | } 58 | 59 | $class_filter = $filters['classes']; 60 | $method_filter = $filters['methods']; 61 | foreach ($classes as list($path, $classname)) { 62 | if ($classname === null) { 63 | continue; 64 | } 65 | if (!$class_filter($classname)) { 66 | continue; 67 | } 68 | await $progress_callback(new StartingTestClassEvent($path, $classname)); 69 | await using ( 70 | (new FinishedTestClassEvent($path, $classname))->onScopeExit( 71 | $progress_callback, 72 | ) 73 | ) { 74 | $test_case = new $classname(); 75 | await $test_case->runTestsAsync( 76 | $method ==> $method_filter($classname, $method), 77 | $progress_callback, 78 | ); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/_Private/CLIOutputHandler.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest\_Private; 11 | 12 | use namespace HH\Lib\{C, Dict, IO, Math, Str, Vec}; 13 | use namespace Facebook\HackTest; 14 | 15 | abstract class CLIOutputHandler { 16 | <<__LateInit>> private dict $resultCounts; 17 | <<__LateInit>> private vec $errors; 18 | 19 | const int CONTEXT_LINES = 3; 20 | 21 | public function __construct(private \Facebook\CLILib\ITerminal $terminal) { 22 | } 23 | 24 | final public async function writeProgressAsync( 25 | <<__AcceptDisposable>> IO\WriteHandle $handle, 26 | \Facebook\HackTest\ProgressEvent $e, 27 | ): Awaitable { 28 | if ($e is HackTest\TestRunStartedProgressEvent) { 29 | $this->reset(); 30 | return; 31 | } 32 | 33 | $this->logEvent($e); 34 | 35 | await $this->writeProgressImplAsync($handle, $e); 36 | } 37 | 38 | abstract protected function writeProgressImplAsync( 39 | <<__AcceptDisposable>> IO\WriteHandle $handle, 40 | \Facebook\HackTest\ProgressEvent $e, 41 | ): Awaitable; 42 | 43 | private function reset(): void { 44 | $this->resultCounts = Dict\fill_keys(HackTest\TestResult::getValues(), 0); 45 | $this->errors = vec[]; 46 | } 47 | 48 | private function logEvent(HackTest\ProgressEvent $e): void { 49 | if ($e is HackTest\TestFinishedProgressEvent) { 50 | $this->resultCounts[$e->getResult()]++; 51 | } 52 | 53 | if ($e is HackTest\ErrorProgressEvent) { 54 | $this->errors[] = $e; 55 | if (!$e is HackTest\TestFinishedProgressEvent) { 56 | $this->resultCounts[HackTest\TestResult::ERROR]++; 57 | } 58 | } 59 | } 60 | 61 | final protected function getErrors(): vec { 62 | return $this->errors; 63 | } 64 | 65 | final protected function getMessageHeaderForErrorDetails( 66 | int $message_num, 67 | HackTest\ErrorProgressEvent $ev, 68 | ): string { 69 | if (!$ev is HackTest\TestFinishedWithExceptionProgressEvent) { 70 | if ($ev is HackTest\ClassProgressEvent) { 71 | return Str\format("\n\n%d) %s\n", $message_num, $ev->getClassname()); 72 | } 73 | if ($ev is HackTest\FileProgressEvent) { 74 | return Str\format("\n\n%d) %s\n", $message_num, $ev->getPath()); 75 | } 76 | return "\n\n".$message_num.")\n"; 77 | } 78 | 79 | $row = $ev->getDataProviderRow(); 80 | if ($row is nonnull) { 81 | return Str\format( 82 | "\n\n%d) %s::%s with data set #%s\n", 83 | $message_num, 84 | $ev->getClassname(), 85 | $ev->getTestMethod(), 86 | (string)$row[0], 87 | ); 88 | } else { 89 | return Str\format( 90 | "\n\n%d) %s::%s\n", 91 | $message_num, 92 | $ev->getClassname(), 93 | $ev->getTestMethod(), 94 | ); 95 | } 96 | } 97 | 98 | final public function getResultCounts(): dict { 99 | return $this->resultCounts; 100 | } 101 | 102 | final protected async function writeSummaryAsync( 103 | <<__AcceptDisposable>> IO\WriteHandle $handle, 104 | ): Awaitable { 105 | $result_counts = $this->getResultCounts(); 106 | $num_tests = Math\sum($result_counts); 107 | 108 | await $handle->writeAllAsync(Str\format( 109 | "\n\nSummary: %d test(s), %d passed, %d failed, %d skipped, %d error(s).\n", 110 | $num_tests, 111 | $result_counts[HackTest\TestResult::PASSED] ?? 0, 112 | $result_counts[HackTest\TestResult::FAILED] ?? 0, 113 | $result_counts[HackTest\TestResult::SKIPPED] ?? 0, 114 | $result_counts[HackTest\TestResult::ERROR] ?? 0, 115 | )); 116 | } 117 | 118 | final protected function getPrettyContext( 119 | \Throwable $ex, 120 | string $file, 121 | ): ?string { 122 | if (!\file_exists($file)) { 123 | // Possibly running in repo-authoritative mode 124 | return null; 125 | } 126 | 127 | $frame = $ex->getTrace() 128 | |> Vec\filter( 129 | $$, 130 | $row ==> 131 | (($row as KeyedContainer<_, _>)['file'] ?? null) as ?string === $file, 132 | ) 133 | |> C\last($$); 134 | 135 | if (!$frame is KeyedContainer<_, _>) { 136 | return null; 137 | } 138 | $colors = $this->terminal->supportsColors(); 139 | $c_light = $colors ? "\e[2m" : ''; 140 | $c_bold = $colors ? "\e[1m" : ''; 141 | $c_red = $colors ? "\e[31m" : ''; 142 | $c_reset = $colors ? "\e[0m" : ''; 143 | 144 | $line = $frame['line'] as int; 145 | $line_number_width = Str\length((string)$line) + 2; 146 | 147 | $first_line = Math\maxva(1, $line - self::CONTEXT_LINES); 148 | $all_lines = \file_get_contents($file) 149 | |> Str\split($$, "\n"); 150 | 151 | $context_lines = Vec\slice( 152 | $all_lines, 153 | $first_line - 1, 154 | ($line - $first_line), 155 | ) 156 | |> Vec\map_with_key( 157 | $$, 158 | ($n, $content) ==> Str\format( 159 | '%s| %s%s%s', 160 | Str\pad_left((string)($n + $first_line), $line_number_width, ' '), 161 | $c_light, 162 | $content, 163 | $c_reset, 164 | ), 165 | ); 166 | 167 | $blame_line = $all_lines[$line - 1]; 168 | $fun = $frame['function'] as string; 169 | $fun_offset = Str\search($blame_line, $fun.'('); 170 | if ($fun_offset is null && Str\contains($fun, '\\')) { 171 | $fun = Str\split($fun, '\\') |> C\lastx($$); 172 | $fun_offset = Str\search($blame_line, $fun.'('); 173 | } 174 | if ( 175 | $fun_offset is null && $frame['function'] === 'HH\\invariant_violation' 176 | ) { 177 | $fun = 'invariant'; 178 | $fun_offset = Str\search($blame_line, 'invariant('); 179 | } 180 | 181 | if ($fun_offset is null) { 182 | $context_lines[] = Str\format( 183 | '%s%s>%s %s%s', 184 | Str\pad_left((string)$line, $line_number_width), 185 | $c_red, 186 | $c_reset.$c_bold, 187 | $blame_line, 188 | $c_reset, 189 | ); 190 | } else { 191 | $context_lines[] = Str\format( 192 | '%s%s>%s %s%s%s%s%s%s', 193 | Str\pad_left((string)$line, $line_number_width), 194 | $c_red, 195 | $c_reset.$c_bold, 196 | Str\slice($blame_line, 0, $fun_offset), 197 | $c_red, 198 | $fun, 199 | $c_reset.$c_bold, 200 | Str\slice($blame_line, $fun_offset + Str\length($fun)), 201 | $c_reset, 202 | ); 203 | 204 | $context_lines[] = Str\format( 205 | '%s%s%s%s', 206 | Str\repeat(' ', $line_number_width + $fun_offset + 2), 207 | $c_red, 208 | Str\repeat('^', Str\length($fun)), 209 | $c_reset, 210 | ); 211 | } 212 | return $file.':'.$line."\n".Str\join($context_lines, "\n"); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/_Private/ConciseCLIOutput.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | /* HHAST_IGNORE_ALL[DontAwaitInALoop] */ 11 | 12 | namespace Facebook\HackTest\_Private; 13 | 14 | use namespace HH\Lib\{IO, Str, Vec}; 15 | use namespace Facebook\HackTest; 16 | use type Facebook\HackTest\TestResult; 17 | 18 | final class ConciseCLIOutput extends CLIOutputHandler { 19 | 20 | <<__Override>> 21 | protected async function writeProgressImplAsync( 22 | <<__AcceptDisposable>> IO\WriteHandle $handle, 23 | HackTest\ProgressEvent $e, 24 | ): Awaitable { 25 | if ($e is HackTest\TestRunFinishedProgressEvent) { 26 | await $this->writeFailureDetailsAsync($handle); 27 | await $this->writeSummaryAsync($handle); 28 | return; 29 | } 30 | 31 | if (!$e is HackTest\TestFinishedProgressEvent) { 32 | return; 33 | } 34 | 35 | switch ($e->getResult()) { 36 | case TestResult::PASSED: 37 | await $handle->writeAllAsync('.'); 38 | break; 39 | case TestResult::SKIPPED: 40 | await $handle->writeAllAsync('S'); 41 | break; 42 | case TestResult::FAILED: 43 | await $handle->writeAllAsync('F'); 44 | break; 45 | case TestResult::ERROR: 46 | await $handle->writeAllAsync('E'); 47 | break; 48 | } 49 | } 50 | 51 | private async function writeFailureDetailsAsync( 52 | <<__AcceptDisposable>> IO\WriteHandle $handle, 53 | ): Awaitable { 54 | $error_id = 0; 55 | foreach ($this->getErrors() as $event) { 56 | $ex = $event->getException(); 57 | $error_id++; 58 | 59 | $header = $this->getMessageHeaderForErrorDetails($error_id, $event); 60 | 61 | $message = $ex->getMessage(); 62 | if ($event is HackTest\TestSkippedProgressEvent) { 63 | $message = 'Skipped: '.$ex->getMessage(); 64 | } else if ($event is HackTest\FileProgressEvent) { 65 | $file = $event->getPath(); 66 | 67 | $context = $this->getPrettyContext($ex, $file) ?? 68 | $ex->getTraceAsString() 69 | |> Str\split($$, '#') 70 | |> Vec\filter($$, $line ==> Str\contains($line, $file)) 71 | |> Vec\map($$, $line ==> Str\strip_prefix($line, ' ')) 72 | |> Str\join($$, "\n"); 73 | 74 | if ($context !== '') { 75 | $message .= "\n\n".$context; 76 | } 77 | } 78 | await $handle->writeAllAsync($header.$message); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/_Private/OnScopeExitAsync.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest\_Private; 11 | 12 | final class OnScopeExitAsync implements \IAsyncDisposable { 13 | public function __construct( 14 | private (function(): Awaitable) $callback, 15 | ) { 16 | } 17 | 18 | public async function __disposeAsync(): Awaitable { 19 | $cb = $this->callback; 20 | await $cb(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/_Private/Progress.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest\_Private; 11 | 12 | use namespace Facebook\HackTest; 13 | 14 | final class Progress { 15 | private vec $errors = vec[]; 16 | 17 | public function __construct( 18 | private (function(HackTest\ProgressEvent): Awaitable) $callback, 19 | private string $path, 20 | private classname $class, 21 | ) { 22 | } 23 | 24 | public async function invokingDataProviderAsync( 25 | string $test_method, 26 | ): Awaitable { 27 | $ev = new HackTest\InvokingDataProvidersProgressEvent( 28 | $this->path, 29 | $this->class, 30 | $test_method, 31 | ); 32 | $cb = $this->callback; 33 | await $cb($ev); 34 | } 35 | 36 | public async function testStartingAsync( 37 | string $test_method, 38 | ?(arraykey, Container) $data_provider_row, 39 | ): Awaitable { 40 | $ev = new HackTest\TestStartingProgressEvent( 41 | $this->path, 42 | $this->class, 43 | $test_method, 44 | $data_provider_row, 45 | ); 46 | $cb = $this->callback; 47 | await $cb($ev); 48 | } 49 | 50 | public async function testPassedAsync( 51 | string $test_method, 52 | ?(arraykey, Container) $data_provider_row, 53 | ): Awaitable { 54 | $ev = new HackTest\TestPassedProgressEvent( 55 | $this->path, 56 | $this->class, 57 | $test_method, 58 | $data_provider_row, 59 | ); 60 | $cb = $this->callback; 61 | await $cb($ev); 62 | } 63 | 64 | public function getErrors(): vec { 65 | return $this->errors; 66 | } 67 | 68 | /** Return a Skipped/Errored/Failed as appropriate */ 69 | public async function testFinishedWithExceptionAsync( 70 | string $test_method, 71 | ?(arraykey, Container) $data_provider_row, 72 | \Throwable $ex, 73 | ): Awaitable { 74 | if ($ex is HackTest\SkippedTestException) { 75 | $ev = new HackTest\TestSkippedProgressEvent( 76 | $this->path, 77 | $this->class, 78 | $test_method, 79 | $data_provider_row, 80 | ); 81 | } else if ($ex is HackTest\ExpectationFailedException) { 82 | $ev = new HackTest\TestFailedProgressEvent( 83 | $this->path, 84 | $this->class, 85 | $test_method, 86 | $data_provider_row, 87 | $ex, 88 | ); 89 | } else { 90 | $ev = new HackTest\TestErroredProgressEvent( 91 | $this->path, 92 | $this->class, 93 | $test_method, 94 | $data_provider_row, 95 | $ex, 96 | ); 97 | } 98 | if ($ev is HackTest\ErrorProgressEvent) { 99 | $this->errors[] = $ev; 100 | } 101 | $cb = $this->callback; 102 | await $cb($ev); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/_Private/Result.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest\_Private; 11 | 12 | <<__Sealed(WrappedResult::class, WrappedException::class)>> 13 | interface ResultOrException { 14 | } 15 | 16 | final class WrappedResult implements ResultOrException { 17 | public function __construct(private T $result) { 18 | } 19 | 20 | public function getResult(): T { 21 | return $this->result; 22 | } 23 | } 24 | 25 | final class WrappedException implements ResultOrException { 26 | public function __construct(private \Throwable $ex) { 27 | } 28 | 29 | public function getException(): \Throwable { 30 | return $this->ex; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/_Private/VerboseCLIOutput.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | j 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | * 9 | */ 10 | 11 | /* HHAST_IGNORE_ALL[DontAwaitInALoop] */ 12 | 13 | namespace Facebook\HackTest\_Private; 14 | 15 | use namespace HH\Lib\{IO, Str}; 16 | 17 | use namespace Facebook\HackTest; 18 | use type Facebook\HackTest\TestResult; 19 | 20 | final class VerboseCLIOutput extends CLIOutputHandler { 21 | <<__Override>> 22 | protected async function writeProgressImplAsync( 23 | <<__AcceptDisposable>> IO\WriteHandle $handle, 24 | HackTest\ProgressEvent $e, 25 | ): Awaitable { 26 | if ($e is HackTest\TestRunFinishedProgressEvent) { 27 | await $this->writeFailureDetailsAsync($handle); 28 | await $this->writeSummaryAsync($handle); 29 | return; 30 | } 31 | 32 | if ($e is HackTest\TestFinishedProgressEvent) { 33 | switch ($e->getResult()) { 34 | case TestResult::PASSED: 35 | await $handle->writeAllAsync("PASS\n"); 36 | break; 37 | case TestResult::SKIPPED: 38 | await $handle->writeAllAsync("SKIP\n"); 39 | break; 40 | case TestResult::FAILED: 41 | await $handle->writeAllAsync("FAIL\n"); 42 | break; 43 | case TestResult::ERROR: 44 | await $handle->writeAllAsync("ERROR\n"); 45 | break; 46 | } 47 | } 48 | 49 | if ($e is HackTest\InvokingDataProvidersProgressEvent) { 50 | $message = 'calling data providers...'; 51 | } else if ($e is HackTest\TestStartingProgressEvent) { 52 | $message = 'starting...'; 53 | } else if ($e is HackTest\TestFinishedProgressEvent) { 54 | $message = '...complete.'; 55 | } else { 56 | return; 57 | } 58 | 59 | if ($e is HackTest\TestProgressEvent) { 60 | $scope = ' ::'.$e->getTestMethod(); 61 | if ($e is HackTest\TestInstanceProgressEvent) { 62 | $dp = $e->getDataProviderRow(); 63 | if ($dp is nonnull) { 64 | $scope .= '['.(string)$dp[0].']'; 65 | } 66 | } 67 | } else if ($e is HackTest\ClassProgressEvent) { 68 | $scope = $e->getClassname(); 69 | } else { 70 | $scope = $e->getPath(); 71 | } 72 | 73 | await $handle->writeAllAsync($scope.'> '.$message."\n"); 74 | } 75 | 76 | private async function writeFailureDetailsAsync( 77 | <<__AcceptDisposable>> IO\WriteHandle $handle, 78 | ): Awaitable { 79 | $error_id = 0; 80 | foreach ($this->getErrors() as $event) { 81 | $ex = $event->getException(); 82 | $error_id++; 83 | $header = $this->getMessageHeaderForErrorDetails($error_id, $event); 84 | 85 | if ($event is HackTest\TestSkippedProgressEvent) { 86 | await $handle->writeAllAsync($header.'Skipped: '.$ex->getMessage()); 87 | return; 88 | } 89 | 90 | $it = $ex; 91 | $message = ''; 92 | while ($it) { 93 | $message .= Str\format( 94 | "%s\n\n@ %s(%d)\n%s", 95 | $it->getMessage(), 96 | $it->getFile(), 97 | $it->getLine(), 98 | $it->getTraceAsString(), 99 | ); 100 | $it = $it->getPrevious(); 101 | if ($it !== null) { 102 | $message .= "\n\nPrevious exception:\n\n"; 103 | } 104 | } 105 | if ($event is HackTest\FileProgressEvent) { 106 | $file = $event->getPath(); 107 | $context = $this->getPrettyContext($ex, $file); 108 | if ($context is nonnull) { 109 | $message .= "\n\n".$context; 110 | } 111 | } 112 | await $handle->writeAllAsync($header.$message); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/enum/ExitCode.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | enum ExitCode: int as int { 13 | SUCCESS = 0; 14 | FAILURE = 1; 15 | ERROR = 2; 16 | } 17 | -------------------------------------------------------------------------------- /src/enum/TestResult.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackTest; 11 | 12 | enum TestResult: int { 13 | PASSED = 0; 14 | FAILED = 1; 15 | ERROR = 2; 16 | SKIPPED = 3; 17 | } 18 | --------------------------------------------------------------------------------