diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..dfc486e --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,34 @@ +name: PHP Unitary + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + env: + COMPOSER_ROOT_VERSION: 2.x-dev + + steps: + - uses: actions/checkout@v4 + + - name: Cache Composer packages + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: php bin/unitary \ No newline at end of file diff --git a/bin/unitary b/bin/unitary index 236af2d..6e2491f 100755 --- a/bin/unitary +++ b/bin/unitary @@ -28,4 +28,4 @@ $app = (new Application()) ->boot([ "argv" => $argv, "dir" => getcwd() - ]); + ]); \ No newline at end of file diff --git a/composer.json b/composer.json index 7553e02..78b84a1 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ "homepage": "https://maplephp.github.io/Unitary/" } ], + "scripts": { + "test": "php bin/unitary" + }, "require": { "php": ">=8.0", "composer/semver": "^3.4", @@ -45,5 +48,12 @@ "bin": [ "bin/unitary" ], - "minimum-stability": "stable" + "extra": { + "branch-alias": { + "dev-main": "2.x-dev", + "dev-develop": "2.x-dev" + } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/src/Config/ConfigProps.php b/src/Config/ConfigProps.php index b6c5e10..780bd3c 100644 --- a/src/Config/ConfigProps.php +++ b/src/Config/ConfigProps.php @@ -19,13 +19,13 @@ */ class ConfigProps extends AbstractConfigProps { - public ?string $path = null; public ?string $discoverPattern = null; public ?string $exclude = null; public ?string $show = null; public ?string $timezone = null; public ?string $locale = null; public ?string $type = null; + public ?string $path = null; public ?int $exitCode = null; public ?bool $verbose = null; public ?bool $alwaysShowFiles = null; @@ -33,6 +33,7 @@ class ConfigProps extends AbstractConfigProps public ?bool $smartSearch = null; public ?bool $failFast = null; public ?string $helpController = null; + public ?array $configuration = null; /** * Hydrate the properties/object with expected data, and handle unexpected data @@ -93,6 +94,9 @@ protected function propsHydration(string|bool $key, mixed $value): void case 'failFast': $this->failFast = $this->dataToBool($value); break; + case 'configuration': + $this->configuration = (array)$value; + break; } } diff --git a/src/Console/Application.php b/src/Console/Application.php index 032ce34..a8badcb 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -16,6 +16,14 @@ final class Application { + private array $middlewares = [ + AddCommandMiddleware::class, + ConfigPropsMiddleware::class, + CheckAllowedProps::class, + LocalMiddleware::class, + CliInitMiddleware::class + ]; + public function __construct() { // Default config @@ -29,6 +37,49 @@ public function __construct() EmitronKernel::setRouterFilePath(__DIR__ . "/ConsoleRouter.php"); } + /** + * Clear the default middlewares, be careful with this + * + * @return $this + */ + public function clearDefaultMiddleware(): self + { + $inst = clone $this; + $inst->middlewares = []; + return $inst; + } + + /** + * Clear the default middlewares, be careful with this + * + * @return $this + */ + public function unsetMiddleware(string $class): self + { + + $inst = clone $this; + foreach($inst->middlewares as $key => $middleware) { + if($middleware === $class) { + unset($inst->middlewares[$key]); + break; + } + } + return $inst; + } + + /** + * Add custom middlewares, follow PSR convention + * + * @param array $middleware + * @return $this + */ + public function withMiddleware(array $middleware): self + { + $inst = clone $this; + $inst->middlewares = array_merge($inst->middlewares, $middleware); + return $inst; + } + /** * Change router file * @@ -84,13 +135,7 @@ public function boot(array $parts): Kernel { $env = new Environment(); $request = new ServerRequest(new Uri($env->getUriParts($parts)), $env); - $kernel = new Kernel(new Container(), [ - AddCommandMiddleware::class, - ConfigPropsMiddleware::class, - CheckAllowedProps::class, - LocalMiddleware::class, - CliInitMiddleware::class - ]); + $kernel = new Kernel(new Container(), $this->middlewares); $kernel->run($request); return $kernel; } diff --git a/src/Console/Controllers/DefaultController.php b/src/Console/Controllers/DefaultController.php index 496411b..47a6e0c 100644 --- a/src/Console/Controllers/DefaultController.php +++ b/src/Console/Controllers/DefaultController.php @@ -2,6 +2,7 @@ namespace MaplePHP\Unitary\Console\Controllers; +use MaplePHP\Emitron\Contracts\ConfigPropsInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -10,7 +11,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use MaplePHP\Prompts\Command; -use MaplePHP\Unitary\Config\ConfigProps; use MaplePHP\Validate\Validator; abstract class DefaultController @@ -20,7 +20,7 @@ abstract class DefaultController protected Command $command; protected DispatchConfigInterface $configs; protected array $args; - protected ?ConfigProps $props = null; + protected ?ConfigPropsInterface $props = null; protected string|bool $path; /** diff --git a/src/Console/Kernel.php b/src/Console/Kernel.php index ea8b36a..e7845d9 100644 --- a/src/Console/Kernel.php +++ b/src/Console/Kernel.php @@ -17,6 +17,7 @@ use Exception; use MaplePHP\Emitron\Contracts\DispatchConfigInterface; use MaplePHP\Emitron\DispatchConfig; +use MaplePHP\Unitary\Config\ConfigProps; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use MaplePHP\Unitary\Support\Router; @@ -72,7 +73,7 @@ public function run(ServerRequestInterface $request, ?StreamInterface $stream = */ private function configuration(ServerRequestInterface $request): DispatchConfigInterface { - $config = new DispatchConfig(EmitronKernel::getConfigFilePath()); + $config = new DispatchConfig(EmitronKernel::getConfigFilePath(), ConfigProps::class); return $config ->setRouter(function ($routerFile) use ($request) { $router = new Router($request->getCliKeyword(), $request->getCliArgs()); diff --git a/src/Console/Middlewares/ConfigPropsMiddleware.php b/src/Console/Middlewares/ConfigPropsMiddleware.php index bb9eb14..ff319c2 100644 --- a/src/Console/Middlewares/ConfigPropsMiddleware.php +++ b/src/Console/Middlewares/ConfigPropsMiddleware.php @@ -2,6 +2,8 @@ namespace MaplePHP\Unitary\Console\Middlewares; +use MaplePHP\Emitron\Configs\ConfigPropsFactory; +use MaplePHP\Emitron\Contracts\ConfigPropsInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -15,7 +17,7 @@ class ConfigPropsMiddleware implements MiddlewareInterface { - protected ?ConfigProps $props = null; + protected ?ConfigPropsInterface $props = null; private ContainerInterface $container; /** @@ -56,7 +58,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - private function getInitProps(): ConfigProps + private function getInitProps(): ConfigPropsInterface { if ($this->props === null) { $args = $this->container->get("args"); @@ -65,14 +67,13 @@ private function getInitProps(): ConfigProps try { $props = array_merge($configs->getProps()->toArray(), $args); - $this->props = new ConfigProps($props); - + $this->props = ConfigPropsFactory::create($props, $configs->getConfigPropsClass()); if ($this->props->hasMissingProps() !== [] && isset($args['verbose'])) { $command->error('The properties (' . - implode(", ", $this->props->hasMissingProps()) . ') is not exist in config props'); + implode(", ", $this->props->hasMissingProps()) . ') is not exist in ' . get_class($this->props)); $command->message( "One or more arguments you passed are not recognized as valid options.\n" . - "Check your command syntax or configuration." + "Check your command parameter syntax for spellings or configuration." ); } diff --git a/src/Console/Middlewares/LocalMiddleware.php b/src/Console/Middlewares/LocalMiddleware.php index 7909030..1be2c3e 100644 --- a/src/Console/Middlewares/LocalMiddleware.php +++ b/src/Console/Middlewares/LocalMiddleware.php @@ -39,8 +39,12 @@ public function __construct(ContainerInterface $container) public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $props = $this->container->get("props"); - Clock::setDefaultLocale($props->locale); - Clock::setDefaultTimezone($props->timezone); + if($props->locale !== null) { + Clock::setDefaultLocale($props->locale); + } + if($props->timezone !== null) { + Clock::setDefaultTimezone($props->timezone); + } return $handler->handle($request); } } diff --git a/src/Console/Services/AbstractMainService.php b/src/Console/Services/AbstractMainService.php index f8ade42..94cfb0b 100644 --- a/src/Console/Services/AbstractMainService.php +++ b/src/Console/Services/AbstractMainService.php @@ -2,6 +2,7 @@ namespace MaplePHP\Unitary\Console\Services; +use MaplePHP\Emitron\Contracts\ConfigPropsInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -10,7 +11,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use MaplePHP\Prompts\Command; -use MaplePHP\Unitary\Config\ConfigProps; abstract class AbstractMainService { @@ -20,7 +20,7 @@ abstract class AbstractMainService protected Command $command; protected DispatchConfigInterface $configs; protected ServerRequestInterface|RequestInterface $request; - protected ?ConfigProps $props = null; + protected ?ConfigPropsInterface $props = null; /** * @throws NotFoundExceptionInterface diff --git a/src/Discovery/TestDiscovery.php b/src/Discovery/TestDiscovery.php index 9243a32..d115000 100755 --- a/src/Discovery/TestDiscovery.php +++ b/src/Discovery/TestDiscovery.php @@ -202,14 +202,11 @@ private function executeUnitFile(string $file): void $ok = self::$unitary->execute(); if (!$ok && $verbose) { - trigger_error( - "\n\nCould not find any tests inside the test file:\n$file\n\nPossible causes:\n" . - " • There are no test in test group/case.\n" . - " • Unitary could not locate the Unit instance.\n" . - " • You did not use the `group()` function.\n" . - " • You created a new Unit in the test file but did not return it at the end.\n\n", - E_USER_WARNING - ); + throw new BlunderSoftException("Could not find any tests inside the test file: $file\n\nPossible causes:\n" . + " • There are no test in test group/case.\n" . + " • Unitary could not locate the Unit instance.\n" . + " • You did not use the `group()` function.\n" . + " • You created a new Unit in the test file but did not return it at the end.\n\n", 0, 0, $file); } } diff --git a/src/Expect.php b/src/Expect.php index 860320c..7af64fc 100644 --- a/src/Expect.php +++ b/src/Expect.php @@ -37,6 +37,12 @@ public static function value(mixed $value): self return new self($value); } + + public function expect(mixed $value): self + { + return $this->setValue($value); + } + /** * We need to pass a test case to Exception to create one loop * diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index 862a7df..62a909b 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -20,7 +20,6 @@ final class Helpers { - /** * Convert bytes to megabytes and return as a string with fixed precision. * diff --git a/src/TestCase.php b/src/TestCase.php index dc0e49f..357d6b6 100755 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -355,12 +355,26 @@ public function validate(mixed $expect, Closure $validation): TestUnit * * @param mixed $value * @return Expect + * @throws ErrorException */ public function expect(mixed $value): Expect { - $this->value = $value; - $this->expect = new Expect($value); - $this->expect->setTestCase($this, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[0] ?? []); + + if(is_callable($value)) { + $validation = $value; + $expectInst = null; + $this->testUnit = $this->expectAndValidate(null, function (mixed $value, Expect $inst) use ($validation, &$expectInst) { + $expectInst = $inst; + return $validation($inst, new Traverse($value)); + }, $this->error); + + $this->expect = $expectInst; + $this->testUnit->setTestValue($this->expect->getValue()); + } else { + $this->value = $value; + $this->expect = new Expect($value); + $this->expect->setTestCase($this, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[0] ?? []); + } return $this->expect; } diff --git a/src/TestItem.php b/src/TestItem.php index fc1ae16..5ba2dd6 100755 --- a/src/TestItem.php +++ b/src/TestItem.php @@ -208,7 +208,8 @@ public function getStringifyArgs(): string { if ($this->hasArgs) { $args = array_map(fn ($value) => Helpers::stringifyArgs($value), $this->args); - return "(" . implode(", ", $args) . ")"; + $args = preg_replace('/\R+/', '', implode(", ", $args)); + return "(" . $args . ")"; } return ""; } diff --git a/tests/unitary-mock.php b/tests/unitary-mock.php index 59bd54d..efd79d4 100755 --- a/tests/unitary-mock.php +++ b/tests/unitary-mock.php @@ -90,7 +90,7 @@ $method->method("addFromEmail") ->withArguments("john.doe@gmail.com", "John Doe") ->withArgumentsForCalls(["john.doe@gmail.com", "John Doe"], ["jane.doe@gmail.com", "Jane Doe"]) - ->willThrowOnce(new InvalidArgumentException("Lowrem ipsum")) + ->willThrowOnce(new InvalidArgumentException("Lorem ipsum")) ->called(2); $method->method("addBCC") @@ -105,11 +105,10 @@ ->called(0); }); - $case->validate(fn() => $mail->addFromEmail("john.doe@gmail.com", "John Doe"), function(Expect $inst) { - $inst->isThrowable(InvalidArgumentException::class); + $case->expect(function(Expect $inst) use($mail) { + $inst->expect(fn() => $mail->addFromEmail("john.doe@gmail.com", "John Doe"))->isThrowable(InvalidArgumentException::class); }); - $mail->addFromEmail("jane.doe@gmail.com", "Jane Doe"); $case->error("Test all exception validations")