From 1699dc4d9bce43f9adeab192731bbc54edf10a99 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:19:23 +0100 Subject: [PATCH 01/38] Updates for PHP >=8.0 --- .github/workflows/tests.yml | 2 +- bin/cigar | 22 +++++++++++++++++++--- composer.json | 6 +++--- spec/AsyncCheckerSpec.php | 7 +++---- spec/CigarCliSpec.php | 26 +++++++++++++------------- spec/stubs/.cigar.insecure.json | 2 +- src/Parser.php | 2 +- 7 files changed, 41 insertions(+), 26 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c83dc2a..e93917c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.1', '7.2', '7.3', '7.4'] + php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: - name: Checkout diff --git a/bin/cigar b/bin/cigar index 13e5a73..2930ed4 100755 --- a/bin/cigar +++ b/bin/cigar @@ -1,7 +1,7 @@ #!/usr/bin/env php check($domains); - $expected = [ - new Result($domain, 200), - ]; + $expected = [new Result($domain, $statusCode)]; expect($results[0]->getStatusCode())->toEqual($expected[0]->getStatusCode()); }); diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php index 97fe9b4..7e4a732 100755 --- a/spec/CigarCliSpec.php +++ b/spec/CigarCliSpec.php @@ -6,19 +6,19 @@ // this is here to clean up config files from each test afterEach(function () { - $process = new Process('cd spec && rm .*.json'); + $process = Process::fromShellCommandline('cd spec && rm .*.json'); $process->run(); }); it('passes if given good URLs to check', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('is quiet if given the option', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar --quiet'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar --quiet'); $process->run(); expect($process->getOutput())->toBe(''); @@ -26,70 +26,70 @@ }); it('fails if given bad URLs to check', function () { - $process = new Process('cd spec && cp stubs/.cigar.fail.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.fail.json .cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); it('can be given an alternative configuration file to load with a short command line flag', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar -c .config.json'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar -c .config.json'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be given an alternative configuration file to load with a long command line flag', function () { - $process = new Process('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar --config .config.json'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar --config .config.json'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('fails with insecure certs', function () { - $process = new Process('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); it('passes with insecure certs if the insecure flag is specified', function () { - $process = new Process('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar --insecure'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar --insecure'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed a base URL as a short command argument', function () { - $process = new Process('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar -u http://httpbin.org'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar -u http://httpbin.org'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed a base URL as a full command argument', function () { - $process = new Process('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar --url=http://httpbin.org'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar --url=http://httpbin.org'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed headers as full arguments', function () { - $process = new Process('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed headers as short arguments', function () { - $process = new Process('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed timeout arguments and it overwrites the configured value in .cigar.json', function () { - $process = new Process('cd spec && cp stubs/.cigar.timeouts.json .cigar.json && ../bin/cigar -t 1'); + $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.timeouts.json .cigar.json && ../bin/cigar -t 1'); $process->run(); expect($process->getExitCode())->toBe(1); diff --git a/spec/stubs/.cigar.insecure.json b/spec/stubs/.cigar.insecure.json index 596ac3f..15a0268 100644 --- a/spec/stubs/.cigar.insecure.json +++ b/spec/stubs/.cigar.insecure.json @@ -1,6 +1,6 @@ [ { "url": "https://cigar-do-not-work.apps.brunty.me", - "status": 200 + "status": 403 } ] diff --git a/src/Parser.php b/src/Parser.php index c89e4b0..4eb369d 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -21,7 +21,7 @@ class Parser public function __construct(string $baseUrl = null, int $connectTimeout = null, int $timeout = null) { - $this->baseUrl = rtrim($baseUrl, '/'); + $this->baseUrl = rtrim((string) $baseUrl, '/'); $this->connectTimeout = $connectTimeout; $this->timeout = $timeout; } From db086521a06ee40bbd189534f28a8fdfa407941c Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:20:04 +0100 Subject: [PATCH 02/38] Remove un-needed dir in psr-4 dev autoloading --- composer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index d14a919..92ef3ff 100755 --- a/composer.json +++ b/composer.json @@ -15,9 +15,7 @@ } }, "autoload-dev": { - "psr-4": { - "Brunty\\Cigar\\Tests\\": "tests/" - } + "psr-4": {} }, "require": { "php": ">=7.0", From 4a29c1122ec60a2bb64084c983c1fea1bb8a6688 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:26:09 +0100 Subject: [PATCH 03/38] Only support PHP 8.1 and PHP 8.2 --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e93917c..a39a812 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-versions: ['8.1', '8.2'] name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: - name: Checkout diff --git a/composer.json b/composer.json index 92ef3ff..b3950cc 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "psr-4": {} }, "require": { - "php": ">=7.0", + "php": "8.1.*, 8.2.*", "ext-curl": "*", "ext-json": "*" }, From a7c99efda5658ae80cfe037887f0f5caf85320b6 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:27:18 +0100 Subject: [PATCH 04/38] Only support PHP 8.1 and PHP 8.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b3950cc..ff07871 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "psr-4": {} }, "require": { - "php": "8.1.*, 8.2.*", + "php": "8.1.*|8.2.*", "ext-curl": "*", "ext-json": "*" }, From f8bdde112cedbad8c3ffe74fc523a42cb927caa9 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:31:22 +0100 Subject: [PATCH 05/38] Coveralls --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a39a812..172b2d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,4 +29,4 @@ jobs: - name: Run Kahlan run: vendor/bin/kahlan --reporter=verbose --clover=coverage.xml - name: Coveralls - run: php vendor/bin/coveralls + run: php vendor/bin/php-coveralls From c241dd4567f55aa03f116ab568b3276bd2193251 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:35:01 +0100 Subject: [PATCH 06/38] WIP: Remove coveralls for now --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 172b2d5..5560687 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,6 +27,6 @@ jobs: - name: Install dependencies run: composer install - name: Run Kahlan - run: vendor/bin/kahlan --reporter=verbose --clover=coverage.xml - - name: Coveralls - run: php vendor/bin/php-coveralls + run: XDEBUG_MODE=coverage vendor/bin/kahlan --reporter=verbose --clover=coverage.xml + #- name: Coveralls + # run: php vendor/bin/php-coveralls From 80503f1dee202fc928cd64f1fff6162ed567a63d Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 13:47:30 +0100 Subject: [PATCH 07/38] Cigar 2.0 Dockerfile --- docker/2.0/Dockerfile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100755 docker/2.0/Dockerfile diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile new file mode 100755 index 0000000..7653d75 --- /dev/null +++ b/docker/2.0/Dockerfile @@ -0,0 +1,24 @@ +# Cigar Docker Container +FROM composer AS composer + +FROM php:8.2-cli-alpine + +LABEL maintainer="matt@brunty.me" + +COPY --from=composer /usr/bin/composer /usr/bin/composer + +# Goto temporary directory +WORKDIR /tmp + +# Run composer and cigar installation. +RUN composer require "brunty/cigar:^2.0" --optimize-autoloader --prefer-dist --no-scripts && \ + ln -s /tmp/vendor/bin/cigar /usr/local/bin/cigar + +# Set up the application directory. +VOLUME ["/app"] +WORKDIR /app + +# Set up the command arguments. +ENTRYPOINT ["/usr/local/bin/cigar"] + + From ad0419fe9a7537d26db1cc0fb5856eb1839f5f6d Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 14:13:35 +0100 Subject: [PATCH 08/38] Move from .cigar.json to just cigar.json default config file --- .gitattributes | 1 + .gitignore | 1 + README.md | 7 ++++- bin/cigar | 4 +-- spec/CigarCliSpec.php | 26 +++++++++---------- spec/ParserSpec.php | 18 ++++++------- .../{.cigar.fail.json => cigar.fail.json} | 0 ....cigar.headers.json => cigar.headers.json} | 0 ...igar.insecure.json => cigar.insecure.json} | 0 .../{.cigar.pass.json => cigar.pass.json} | 0 ...igar.timeouts.json => cigar.timeouts.json} | 0 ...hout-base.json => cigar.without-base.json} | 0 12 files changed, 32 insertions(+), 25 deletions(-) rename spec/stubs/{.cigar.fail.json => cigar.fail.json} (100%) rename spec/stubs/{.cigar.headers.json => cigar.headers.json} (100%) rename spec/stubs/{.cigar.insecure.json => cigar.insecure.json} (100%) rename spec/stubs/{.cigar.pass.json => cigar.pass.json} (100%) rename spec/stubs/{.cigar.timeouts.json => cigar.timeouts.json} (100%) rename spec/stubs/{.cigar.without-base.json => cigar.without-base.json} (100%) diff --git a/.gitattributes b/.gitattributes index f7ed319..f74656e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # Ignoring files for distribution archives spec/ export-ignore .cigar.json.example export-ignore +cigar.json.example export-ignore .coveralls.yml export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/.gitignore b/.gitignore index 85fa163..3cf5c84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /vendor /composer.lock +cigar.json .cigar.json .idea coverage.xml diff --git a/README.md b/README.md index 9c88cd1..1f694c5 100755 --- a/README.md +++ b/README.md @@ -18,9 +18,14 @@ Pull via docker: `docker pull brunty/cigar` +## Upgrading from V1 - Breaking Changes + +- `.cigar.json` default config file name is now just `cigar.json` +- No longer testing against PHP 7.X and PHP 8.0 + ## To use -Create a `.cigar.json` file that contains an array of json objects specifying the `url`, `status`, (optional) `content`, and (optional) `content-type` to check. +Create a `cigar.json` file that contains an array of json objects specifying the `url`, `status`, (optional) `content`, and (optional) `content-type` to check. ``` [ diff --git a/bin/cigar b/bin/cigar index 2930ed4..ad3be80 100755 --- a/bin/cigar +++ b/bin/cigar @@ -43,7 +43,7 @@ if (isset($options['help'])) { \033[33mOptions:\033[0m - \033[32m-c file.json, --config=file.json\033[0m Use the specified config file instead of the default .cigar.json file + \033[32m-c file.json, --config=file.json\033[0m Use the specified config file instead of the default cigar.json file \033[32m-u URL, --url=URL\033[0m Base URL for checks, e.g. https://example.org/ \033[32m-i, --insecure\033[0m Allow invalid SSL certificates \033[32m-a, --auth\033[0m Authorization header "\074type\076 \074credentials\076" @@ -89,7 +89,7 @@ VERSION; $writer = isset($options['j']) || isset($options['json']) ? new JsonWriter() : new EchoWriter(); $outputter = new Outputter(isset($options['quiet']), $writer); -$file = $options['c'] ?? ($options['config'] ?? '.cigar.json'); +$file = $options['c'] ?? ($options['config'] ?? 'cigar.json'); $baseUrl = $options['u'] ?? ($options['url'] ?? null); if ( ! file_exists($file)) { diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php index 7e4a732..7cbff80 100755 --- a/spec/CigarCliSpec.php +++ b/spec/CigarCliSpec.php @@ -11,14 +11,14 @@ }); it('passes if given good URLs to check', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('is quiet if given the option', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .cigar.json && ../bin/cigar --quiet'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar --quiet'); $process->run(); expect($process->getOutput())->toBe(''); @@ -26,70 +26,70 @@ }); it('fails if given bad URLs to check', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.fail.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.fail.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); it('can be given an alternative configuration file to load with a short command line flag', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar -c .config.json'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -c .config.json'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be given an alternative configuration file to load with a long command line flag', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.pass.json .config.json && ../bin/cigar --config .config.json'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar --config .config.json'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('fails with insecure certs', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); it('passes with insecure certs if the insecure flag is specified', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.insecure.json .cigar.json && ../bin/cigar --insecure'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar --insecure'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed a base URL as a short command argument', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar -u http://httpbin.org'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar -u http://httpbin.org'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed a base URL as a full command argument', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.without-base.json .cigar.json && ../bin/cigar --url=http://httpbin.org'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar --url=http://httpbin.org'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed headers as full arguments', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); $process->run(); expect($process->getExitCode())->toBe(0); }); it('can be passed headers as short arguments', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.headers.json .cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed timeout arguments and it overwrites the configured value in .cigar.json', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/.cigar.timeouts.json .cigar.json && ../bin/cigar -t 1'); + it('can be passed timeout arguments and it overwrites the configured value in cigar.json', function () { + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.timeouts.json cigar.json && ../bin/cigar -t 1'); $process->run(); expect($process->getExitCode())->toBe(1); diff --git a/spec/ParserSpec.php b/spec/ParserSpec.php index b106b8d..6e1bcf9 100644 --- a/spec/ParserSpec.php +++ b/spec/ParserSpec.php @@ -7,7 +7,7 @@ describe('Parser', function () { it('parses a file that is correctly formatted', function () { $structure = [ - '.cigar.json' => '[ + 'cigar.json' => '[ { "url": "http://httpbin.org/status/418", "status": 418 @@ -29,7 +29,7 @@ ]; vfsStream::setup('root', null, $structure); - $results = (new Parser)->parse('vfs://root/.cigar.json'); + $results = (new Parser)->parse('vfs://root/cigar.json'); $expected = [ new Url('http://httpbin.org/status/418', 418), @@ -42,20 +42,20 @@ it('lets errors be thrown on parsing a file', function () { $structure = [ - '.cigar.json' => 'http://httpbin.org/status/418', + 'cigar.json' => 'http://httpbin.org/status/418', ]; vfsStream::setup('root', null, $structure); $fn = function () { - (new Parser)->parse('vfs://root/.cigar.json'); + (new Parser)->parse('vfs://root/cigar.json'); }; - expect($fn)->toThrow(new ParseError('Could not parse vfs://root/.cigar.json')); + expect($fn)->toThrow(new ParseError('Could not parse vfs://root/cigar.json')); }); it('parses a file that contains both relative and absolute URLs', function () { $structure = [ - '.cigar.json' => '[ + 'cigar.json' => '[ { "url": "/status/418", "status": 418 @@ -74,7 +74,7 @@ ]; vfsStream::setup('root', null, $structure); - $results = (new Parser('http://httpbin.org'))->parse('vfs://root/.cigar.json'); + $results = (new Parser('http://httpbin.org'))->parse('vfs://root/cigar.json'); $expected = [ new Url('http://httpbin.org/status/418', 418), @@ -87,7 +87,7 @@ it('throws an exception if a URL cannot be parsed', function () { $structure = [ - '.cigar.json' => '[ + 'cigar.json' => '[ { "url": "http://:80", "status": 418, @@ -100,7 +100,7 @@ $fn = function () { - (new Parser)->parse('vfs://root/.cigar.json'); + (new Parser)->parse('vfs://root/cigar.json'); }; expect($fn)->toThrow(new ParseError('Could not parse URL: http://:80')); diff --git a/spec/stubs/.cigar.fail.json b/spec/stubs/cigar.fail.json similarity index 100% rename from spec/stubs/.cigar.fail.json rename to spec/stubs/cigar.fail.json diff --git a/spec/stubs/.cigar.headers.json b/spec/stubs/cigar.headers.json similarity index 100% rename from spec/stubs/.cigar.headers.json rename to spec/stubs/cigar.headers.json diff --git a/spec/stubs/.cigar.insecure.json b/spec/stubs/cigar.insecure.json similarity index 100% rename from spec/stubs/.cigar.insecure.json rename to spec/stubs/cigar.insecure.json diff --git a/spec/stubs/.cigar.pass.json b/spec/stubs/cigar.pass.json similarity index 100% rename from spec/stubs/.cigar.pass.json rename to spec/stubs/cigar.pass.json diff --git a/spec/stubs/.cigar.timeouts.json b/spec/stubs/cigar.timeouts.json similarity index 100% rename from spec/stubs/.cigar.timeouts.json rename to spec/stubs/cigar.timeouts.json diff --git a/spec/stubs/.cigar.without-base.json b/spec/stubs/cigar.without-base.json similarity index 100% rename from spec/stubs/.cigar.without-base.json rename to spec/stubs/cigar.without-base.json From 75cfe0c7abdd63c302ac1ff6601a11dd0bcd31ce Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 14:20:49 +0100 Subject: [PATCH 09/38] Forcing an empty commit to fix github weirdness? From adb132f473294073460deaee565a1a1a3acd1264 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 19 Sep 2023 17:39:54 +0100 Subject: [PATCH 10/38] Rename example config file --- .cigar.json.example => cigar.json.example | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .cigar.json.example => cigar.json.example (100%) diff --git a/.cigar.json.example b/cigar.json.example similarity index 100% rename from .cigar.json.example rename to cigar.json.example From 74ed453d3f2248afe88a43c97c166812b06ed7fa Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 18:38:16 +0100 Subject: [PATCH 11/38] Refactor getopt into Input and InputOption class --- bin/cigar | 95 +++++++++---------- spec/CigarCliSpec.php | 2 +- spec/OutputterSpec.php | 27 +----- src/EchoWriter.php | 6 +- src/Input.php | 73 +++++++++++++++ src/InputOption.php | 39 ++++++++ src/JsonWriter.php | 6 +- src/Output.php | 119 ++++++++++++++++++++++++ src/Outputter.php | 46 --------- src/QuietWriter.php | 14 +++ src/{WriterInterface.php => Writer.php} | 6 +- 11 files changed, 304 insertions(+), 129 deletions(-) create mode 100644 src/Input.php create mode 100644 src/InputOption.php create mode 100644 src/Output.php delete mode 100644 src/Outputter.php create mode 100644 src/QuietWriter.php rename src/{WriterInterface.php => Writer.php} (64%) diff --git a/bin/cigar b/bin/cigar index ad3be80..50f7524 100755 --- a/bin/cigar +++ b/bin/cigar @@ -13,46 +13,44 @@ foreach (['/../../..', '/../..', '/../vendor', '/vendor'] as $autoloadFileDirect use Brunty\Cigar\AsyncChecker; use Brunty\Cigar\EchoWriter; +use Brunty\Cigar\Input; +use Brunty\Cigar\InputOption; use Brunty\Cigar\JsonWriter; -use Brunty\Cigar\Outputter; +use Brunty\Cigar\Output; use Brunty\Cigar\Parser; +use Brunty\Cigar\QuietWriter; use Brunty\Cigar\Result; -$options = getopt( - 'c:ia:u:jh:t:s:', - [ - 'version', - 'help', - 'quiet', - 'config:', - 'insecure', - 'auth:', - 'url:', - 'json', - 'header:', - 'timeout:', - 'connect-timeout:', - ] +$input = new Input(); +$input->configureOptions([ + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message', false), + 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar', false), + 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), + 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), + 'insecure' => InputOption::create('insecure', 'i', InputOption::VALUE_NONE, 'Allow invalid SSL certificates'), + 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, 'Authorization header " "'), + 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, 'Base URL for checks, e.g. https://example.org/'), + 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, 'Custom header ": ", can be passed multiple times to send multiple headers'), + 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_NONE, 'Timeout in seconds'), + 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, 'Connect Timeout in seconds'), + 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), +]); + +$output = new Output( + $input->getOption('quiet') + ? new QuietWriter() + : ($input->getOption('json') ? new JsonWriter() : new EchoWriter()) ); -if (isset($options['help'])) { +$inputOptionsHelpOutput = $output->helpOutputForInputOptions($input); + +if ($input->getOption('help')) { $content = <<getOption('version')) { $version = CIGAR_VERSION; $content = <<getOption('config') ?: 'cigar.json'; +$baseUrl = $input->getOption('url') ?: null; -if ( ! file_exists($file)) { - $outputter->writeErrorLine('Could not find configuration file: ' . $file); +if ( ! file_exists($configFile)) { + $output->writeErrorLine('Could not find configuration file: ' . $configFile); exit(1); } -$secure = ! (isset($options['i']) || isset($options['insecure'])); -$authorization = $options['a'] ?? $options['auth'] ?? null; -$headers = (array) ($options['h'] ?? $options['header'] ?? []); -$connectTimeout = $options['s'] ?? $options['connect-timeout'] ?? null; -$timeout = $options['t'] ?? $options['timeout'] ?? null; +$secure = ! $input->getOption('insecure'); +$authorization = $input->getOption('auth') ?: null; +$headers = $input->getOption('header') ?: []; +$connectTimeout = $input->getOption('connect-timeout') ?: null; +$timeout = $input->getOption('timeout') ?: null; try { - $domains = (new Parser($baseUrl, $connectTimeout, $timeout))->parse($file); -} catch (\Throwable $e) { - $outputter->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); + $domains = (new Parser($baseUrl, $connectTimeout, $timeout))->parse($configFile); +} catch (Throwable $e) { + $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); exit(1); } @@ -115,7 +110,7 @@ $passedResults = array_filter($results, function (Result $result) { return $result->hasPassed(); }); -$outputter->outputResults($passedResults, $results, $start); +$output->outputResults($passedResults, $results, $start); if (count($passedResults) !== count($results)) { exit(1); diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php index 7cbff80..35028d7 100755 --- a/spec/CigarCliSpec.php +++ b/spec/CigarCliSpec.php @@ -33,7 +33,7 @@ }); it('can be given an alternative configuration file to load with a short command line flag', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -c .config.json'); + $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -f .config.json'); $process->run(); expect($process->getExitCode())->toBe(0); diff --git a/spec/OutputterSpec.php b/spec/OutputterSpec.php index e019864..e83ca8e 100644 --- a/spec/OutputterSpec.php +++ b/spec/OutputterSpec.php @@ -1,9 +1,9 @@ domain = new \Brunty\Cigar\Url('url', 418, 'teapot', 'teapot'); $this->results = [ @@ -17,20 +17,12 @@ it('outputs an error line', function () { $fn = function () { - (new Outputter)->writeErrorLine('Error message'); + (new Output())->writeErrorLine('Error message'); }; expect($fn)->toEcho("\033[31mError message\033[0m\n"); }); - it('does not output an error line when run quietly', function () { - $fn = function () { - (new Outputter($quiet = true))->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho(''); - }); - it('outputs results if some results have passed', function () { $results = $this->results; $passedResults = $this->passedResults; @@ -38,7 +30,7 @@ allow('microtime')->toBeCalled()->andReturn(3); $fn = function () use ($passedResults, $results) { - (new Outputter)->outputResults($passedResults, $results, 1.5); + (new Output())->outputResults($passedResults, $results, 1.5); }; $output = <<< OUTPUT @@ -52,15 +44,4 @@ expect($fn)->toEcho($output); }); - - it('outputs results quietly', function () { - $results = $this->results; - $passedResults = $this->passedResults; - - $fn = function () use ($passedResults, $results) { - (new Outputter(true))->outputResults($passedResults, $results, 1.5); - }; - - expect($fn)->toEcho(''); - }); }); diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 12b1ca6..8cab877 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -2,7 +2,7 @@ namespace Brunty\Cigar; -class EchoWriter implements WriterInterface +class EchoWriter implements Writer { const CONSOLE_GREEN = "\033[32m"; const CONSOLE_RED = "\033[31m"; @@ -11,12 +11,12 @@ class EchoWriter implements WriterInterface const SYMBOL_PASSED = '✓'; const SYMBOL_FAILED = '✘'; - public function writeErrorLine(string $message) + public function writeErrorLine(string $message): void { echo self::CONSOLE_RED . $message . self::CONSOLE_RESET . PHP_EOL; } - public function writeResults(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff, Result ...$results) + public function writeResults(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff, Result ...$results): void { ob_start(); diff --git a/src/Input.php b/src/Input.php new file mode 100644 index 0000000..a0fa002 --- /dev/null +++ b/src/Input.php @@ -0,0 +1,73 @@ + */ + private array $options = []; + private array $submittedOptions = []; + + /** + * @param array $options + */ + public function configureOptions(array $options): void + { + ksort($options); + $this->options = $options; + foreach ($options as $inputOption) { + $this->optionShortCodes .= $inputOption->fullShortCode(); + $this->optionLongCodes[] = $inputOption->fullLongCode(); + } + } + + /** + * @return array + */ + public function options(): array + { + return $this->options; + } + + /** + * If the option was configured with {@see InputOption::VALUE_REQUIRED} then it will be the string value of what was + * submitted via the option on the command line, if it's set to {@see InputOption::VALUE_NONE} then it'll be just + * true / false based on if the option was set on the command line or not + * + * @param string $optionName + * @return array|string|bool + */ + public function getOption(string $optionName): array|string|bool + { + if (array_key_exists($optionName, $this->options) === false) { + throw new InvalidArgumentException("Could not find option with {$optionName}"); + } + + if ($this->submittedOptions === []) { + $this->submittedOptions = getopt($this->optionShortCodes, $this->optionLongCodes); + } + + $option = $this->options[$optionName]; + + $longOptionWasSubmitted = array_key_exists($option->longCode, $this->submittedOptions); + $shortOptionWasSubmitted = array_key_exists($option->shortCode, $this->submittedOptions); + + if ($option->valueIsRequired()) { + if ($longOptionWasSubmitted) { + return $this->submittedOptions[$option->longCode]; + } + + if ($shortOptionWasSubmitted) { + return $this->submittedOptions[$option->shortCode]; + } + + return false; + } + + return $longOptionWasSubmitted || $shortOptionWasSubmitted; + } +} diff --git a/src/InputOption.php b/src/InputOption.php new file mode 100644 index 0000000..1f822a6 --- /dev/null +++ b/src/InputOption.php @@ -0,0 +1,39 @@ +shortCode . ($this->valueMode === self::VALUE_REQUIRED && $this->shortCode !== '' ? ':' : ''); + } + + public function fullLongCode(): string { + return $this->longCode . ($this->valueMode === self::VALUE_REQUIRED && $this->longCode !== '' ? ':' : ''); + } + + public function valueIsRequired(): bool + { + return $this->valueMode === self::VALUE_REQUIRED; + } +} diff --git a/src/JsonWriter.php b/src/JsonWriter.php index cbe0f1d..b8332e2 100644 --- a/src/JsonWriter.php +++ b/src/JsonWriter.php @@ -2,9 +2,9 @@ namespace Brunty\Cigar; -class JsonWriter implements WriterInterface +class JsonWriter implements Writer { - public function writeErrorLine(string $message) + public function writeErrorLine(string $message): void { echo json_encode([ 'type' => 'error', @@ -12,7 +12,7 @@ public function writeErrorLine(string $message) ]), PHP_EOL; } - public function writeResults(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff, Result ...$results) + public function writeResults(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff, Result ...$results): void { echo json_encode([ 'type' => 'results', diff --git a/src/Output.php b/src/Output.php new file mode 100644 index 0000000..e2fb05e --- /dev/null +++ b/src/Output.php @@ -0,0 +1,119 @@ +writer = $writer instanceof Writer ? $writer : new EchoWriter(); + } + + public function writeErrorLine(string $message): void + { + $this->writer->writeErrorLine($message); + } + + public function outputResults(array $passedResults, array $results, float $startTime): void + { + $numberOfResults = count($results); + $numberOfPassedResults = count($passedResults); + $endTime = microtime(true); + $timeDiff = round($endTime - $startTime, 3); + $passed = $numberOfPassedResults === $numberOfResults; + + $this->writer->writeResults($numberOfPassedResults, $numberOfResults, $passed, $timeDiff, ...$results); + } + + public function helpOutputForInputOptions(Input $input): string + { + $optionStartSequence = "\033[32m"; + $optionEndSequence = "\033[0m"; + + $output = ''; + + if ($input->options() !== []) { + $output = "\033[33mOptions:\033[0m" . PHP_EOL; + } + + $longestShortCodeLength = 0; + $longestLongCodeLength = 0; + $valuePlaceholderLength = mb_strlen(self::VALUE_PLACEHOLDER); + + foreach ($input->options() as $option) { + $shortCodeLength = 3; // at it's shortest it is "-c " + $longCodeLength = mb_strlen($option->longCode) + 3; // the +3 is for the double dash before and the space or equals after + + if ($option->valueIsRequired()) { + $shortCodeLength += $valuePlaceholderLength; + $longCodeLength += $valuePlaceholderLength; + } + + if ($shortCodeLength > $longestShortCodeLength) { + $longestShortCodeLength = $shortCodeLength; + } + + if ($longCodeLength > $longestLongCodeLength) { + $longestLongCodeLength = $longCodeLength; + } + } + + foreach ($input->options() as $option) { + $shortCode = $this->getShortCodeOutput($option); + $longCode = $this->getLongCodeOutput($option); + + $shortCode = str_pad($shortCode, $longestShortCodeLength, ' '); + $longCode = str_pad($longCode, $longestLongCodeLength, ' '); + + $output .= " {$optionStartSequence}{$shortCode} {$longCode}{$optionEndSequence} {$option->description}" . + PHP_EOL; + } + + return $output; + /* +\033[33mOptions:\033[0m + \033[32m-c file.json, --config=file.json\033[0m Use the specified config file instead of the default cigar.json file + \033[32m-u URL, --url=URL\033[0m Base URL for checks, e.g. https://example.org/ + \033[32m-i, --insecure\033[0m Allow invalid SSL certificates + \033[32m-a, --auth\033[0m Authorization header "\074type\076 \074credentials\076" + \033[32m-h, --header\033[0m Custom header "\074name\076: \074value\076" + \033[32m-s --connect-timeout=TIMEOUT\033[0m Connect Timeout + \033[32m-t, --timeout=TIMEOUT\033[0m Timeout + \033[32m-j, --json\033[0m Output JSON + \033[32m --quiet\033[0m Do not output any message + \033[32m --version\033[0m Print the version of Cigar + */ + } + + private function getShortCodeOutput(InputOption $option): string + { + $shortCode = ''; + + if ($option->shortCode !== '') { + $shortCode = "-{$option->shortCode}"; + } + + if ($option->valueIsRequired()) { + $shortCode = "-{$option->shortCode} VALUE"; + } + + return $shortCode; + } + + private function getLongCodeOutput(InputOption $option): string + { + $longCode = "--{$option->longCode}"; + + if ($option->valueIsRequired()) { + $longCode = "--{$option->longCode}=VALUE"; + } + + return $longCode; + } +} diff --git a/src/Outputter.php b/src/Outputter.php deleted file mode 100644 index dfbd006..0000000 --- a/src/Outputter.php +++ /dev/null @@ -1,46 +0,0 @@ -isQuiet = $isQuiet; - $this->writer = $writer instanceof WriterInterface ? $writer : new EchoWriter(); - } - - public function writeErrorLine(string $message) - { - if ($this->isQuiet) { - return; - } - - $this->writer->writeErrorLine($message); - } - - public function outputResults(array $passedResults, array $results, float $startTime) - { - if ($this->isQuiet) { - return; - } - - $numberOfResults = count($results); - $numberOfPassedResults = count($passedResults); - $end = microtime(true); - $timeDiff = round($end - $startTime, 3); - $passed = $numberOfPassedResults === $numberOfResults; - - $this->writer->writeResults($numberOfPassedResults, $numberOfResults, $passed, $timeDiff, ...$results); - } -} diff --git a/src/QuietWriter.php b/src/QuietWriter.php new file mode 100644 index 0000000..f7c8761 --- /dev/null +++ b/src/QuietWriter.php @@ -0,0 +1,14 @@ + Date: Thu, 21 Sep 2023 20:13:28 +0100 Subject: [PATCH 12/38] Code standards and static analysis updates --- .gitignore | 1 + bin/cigar | 9 +- composer.json | 10 +- phpcs.xml | 202 ++++++++++++++++++++ psalm.xml | 22 +++ spec/AsyncCheckerSpec.php | 32 ++-- spec/CigarCliSpec.php | 71 ++++--- spec/JsonWriterSpec.php | 38 ++-- spec/OutputterSpec.php | 21 +- spec/ParserSpec.php | 24 +-- spec/ResultsSpec.php | 20 +- spec/output/output-results-all-passed.json | 1 + spec/output/output-results-some-failed.json | 1 + src/AsyncChecker.php | 50 ++--- src/EchoWriter.php | 56 ++++-- src/Input.php | 19 +- src/InputOption.php | 16 +- src/JsonWriter.php | 23 ++- src/Output.php | 42 ++-- src/Parser.php | 48 ++--- src/QuietWriter.php | 11 +- src/Result.php | 62 ++---- src/Url.php | 80 +------- src/Writer.php | 10 +- 24 files changed, 524 insertions(+), 345 deletions(-) create mode 100644 phpcs.xml create mode 100644 psalm.xml create mode 100644 spec/output/output-results-all-passed.json create mode 100644 spec/output/output-results-some-failed.json diff --git a/.gitignore b/.gitignore index 3cf5c84..e6dc20f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ cigar.json .idea coverage.xml clover.xml +cache/ diff --git a/bin/cigar b/bin/cigar index 50f7524..ea55710 100755 --- a/bin/cigar +++ b/bin/cigar @@ -6,6 +6,7 @@ $start = microtime(true); foreach (['/../../..', '/../..', '/../vendor', '/vendor'] as $autoloadFileDirectory) { if (file_exists(__DIR__ . $autoloadFileDirectory . '/autoload.php')) { + /** @psalm-suppress UnresolvableInclude */ require __DIR__ . $autoloadFileDirectory . '/autoload.php'; break; } @@ -23,15 +24,15 @@ use Brunty\Cigar\Result; $input = new Input(); $input->configureOptions([ - 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message', false), - 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar', false), + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), + 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), 'insecure' => InputOption::create('insecure', 'i', InputOption::VALUE_NONE, 'Allow invalid SSL certificates'), 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, 'Authorization header " "'), 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, 'Base URL for checks, e.g. https://example.org/'), 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, 'Custom header ": ", can be passed multiple times to send multiple headers'), - 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_NONE, 'Timeout in seconds'), + 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_REQUIRED, 'Timeout in seconds'), 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, 'Connect Timeout in seconds'), 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), ]); @@ -74,7 +75,7 @@ if ($input->getOption('version')) { \033[0;90;49mThe simple smoke testing tool.\033[0m -Version \033[36m{$version}\033[0m +Version \033[36m$version\033[0m For additional help use \033[36m--help\033[0m diff --git a/composer.json b/composer.json index ff07871..485f2a1 100755 --- a/composer.json +++ b/composer.json @@ -26,12 +26,20 @@ "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.6", "kahlan/kahlan": "^5.2", - "symfony/process": "^6.3" + "symfony/process": "^6.3", + "vimeo/psalm": "^5.15", + "squizlabs/php_codesniffer": "^3.7", + "slevomat/coding-standard": "^8.13" }, "bin": [ "bin/cigar" ], "scripts": { "test": "vendor/bin/kahlan --reporter=verbose" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1d1c07a --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,202 @@ + + + + + + src/App/Command/TwigLintCommand.php + + + + + + + + src/Migrations + tests/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/App/Validator/Rule/*Exception.php + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app/bootstrap.php + cron/index.php + + + + + + + + + + + + + + + + + tests/integration/* + tests/functional/* + tests/integration/* + tests/unit/* + + + + + + + bin/cigar + src + spec + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..586de52 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/spec/AsyncCheckerSpec.php b/spec/AsyncCheckerSpec.php index a911d03..176a1b0 100644 --- a/spec/AsyncCheckerSpec.php +++ b/spec/AsyncCheckerSpec.php @@ -1,15 +1,17 @@ check($domains); + $results = (new AsyncChecker())->check($domains); $expected = [ new Result($domain, 200, '', 'text/html; charset=utf-8'), @@ -18,7 +20,7 @@ expect($results)->toEqual($expected); }); - it('checks more than one domain', function () { + it('checks more than one domain', function (): void { $domains = [ new Url('http://httpbin.org/status/200', 200), @@ -26,29 +28,29 @@ new Url('http://httpbin.org/status/404', 404), ]; - $results = (new AsyncChecker)->check($domains); + $results = (new AsyncChecker())->check($domains); $expected = array_map(function (Url $domain) { - return new Result($domain, $domain->getStatus(), '', 'text/html; charset=utf-8'); + return new Result($domain, $domain->status, '', 'text/html; charset=utf-8'); }, $domains); expect($results)->toEqual($expected); }); - it('checks authorization header', function () { + it('checks authorization header', function (): void { $domain = new Url('http://httpbin.org/get', 200); $expectedAuthHeader = 'Basic dXNyOnBzd2Q='; $results = (new AsyncChecker(false, $expectedAuthHeader))->check([$domain]); - $decodedContent = json_decode($results[0]->getContents(), true); + $decodedContent = json_decode($results[0]->contents, true); $actualAuthHeader = $decodedContent['headers']['Authorization'] ?? null; expect($actualAuthHeader)->toEqual($expectedAuthHeader); }); - context('when SSL verification is disabled', function () { - it('checks a domain that has an invalid certificate', function () { + context('when SSL verification is disabled', function (): void { + it('checks a domain that has an invalid certificate', function (): void { $statusCode = 403; // Need to change for a better setup URL that doesn't default to a potentially unknown site $domain = new Url('https://cigar-do-not-work.apps.brunty.me', $statusCode); @@ -58,12 +60,12 @@ $expected = [new Result($domain, $statusCode)]; - expect($results[0]->getStatusCode())->toEqual($expected[0]->getStatusCode()); + expect($results[0]->statusCode)->toEqual($expected[0]->statusCode); }); }); - context('when timeouts are set', function () { - it('checks a URL that will timeout', function () { + context('when timeouts are set', function (): void { + it('checks a URL that will timeout', function (): void { // Need to change for a better setup URL that doesn't default to a potentially unknown site $domain = new Url('https://httpbin.org/delay/3', 200, null, null, 1, 1); $domains = [$domain]; @@ -74,7 +76,7 @@ new Result($domain, 0), ]; - expect($results[0]->getStatusCode())->toEqual($expected[0]->getStatusCode()); + expect($results[0]->statusCode)->toEqual($expected[0]->statusCode); }); }); }); diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php index 35028d7..2e9f3f0 100755 --- a/spec/CigarCliSpec.php +++ b/spec/CigarCliSpec.php @@ -1,95 +1,116 @@ run(); }); - it('passes if given good URLs to check', function () { + it('passes if given good URLs to check', function (): void { $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('is quiet if given the option', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar --quiet'); + it('is quiet if given the option', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar --quiet' + ); $process->run(); expect($process->getOutput())->toBe(''); expect($process->getExitCode())->toBe(0); }); - it('fails if given bad URLs to check', function () { + it('fails if given bad URLs to check', function (): void { $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.fail.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); - it('can be given an alternative configuration file to load with a short command line flag', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -f .config.json'); + it('can be given an alternative configuration file to load with a short command line flag', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -f .config.json' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be given an alternative configuration file to load with a long command line flag', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar --config .config.json'); + it('can be given an alternative configuration file to load with a long command line flag', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar --config .config.json' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('fails with insecure certs', function () { + it('fails with insecure certs', function (): void { $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar'); $process->run(); expect($process->getExitCode())->toBe(1); }); - - it('passes with insecure certs if the insecure flag is specified', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar --insecure'); + + it('passes with insecure certs if the insecure flag is specified', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar --insecure' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed a base URL as a short command argument', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar -u http://httpbin.org'); + it('can be passed a base URL as a short command argument', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar -u http://httpbin.org' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed a base URL as a full command argument', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar --url=http://httpbin.org'); + it('can be passed a base URL as a full command argument', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar --url=http://httpbin.org' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed headers as full arguments', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); + it('can be passed headers as full arguments', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar ' . + '--header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed headers as short arguments', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); + it('can be passed headers as short arguments', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar ' . + '-h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"' + ); $process->run(); expect($process->getExitCode())->toBe(0); }); - it('can be passed timeout arguments and it overwrites the configured value in cigar.json', function () { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.timeouts.json cigar.json && ../bin/cigar -t 1'); + it('can be passed timeout arguments and it overwrites the configured value in cigar.json', function (): void { + $process = Process::fromShellCommandline( + 'cd spec && cp stubs/cigar.timeouts.json cigar.json && ../bin/cigar -t 1' + ); $process->run(); expect($process->getExitCode())->toBe(1); diff --git a/spec/JsonWriterSpec.php b/spec/JsonWriterSpec.php index 8d1ef60..6a21057 100644 --- a/spec/JsonWriterSpec.php +++ b/spec/JsonWriterSpec.php @@ -1,52 +1,50 @@ domain = new \Brunty\Cigar\Url('url', 418, 'teapot', 'teapot'); +describe('JsonWriter', function (): void { + beforeEach(function (): void { + $this->domain = new Url('url', 418, 'teapot', 'teapot'); $this->results = [ - new \Brunty\Cigar\Result($this->domain, 418, 'teapot', 'teapot'), - new \Brunty\Cigar\Result($this->domain, 419), + new Result($this->domain, 418, 'teapot', 'teapot'), + new Result($this->domain, 419), ]; }); - it('outputs an error line', function () { - $fn = function () { - (new JsonWriter)->writeErrorLine('Error message'); + it('outputs an error line', function (): void { + $fn = function (): void { + (new JsonWriter())->writeErrorLine('Error message'); }; expect($fn)->toEcho('{"type":"error","message":"Error message"}' . PHP_EOL); }); - it('outputs results if all results have passed', function () { + it('outputs results if all results have passed', function (): void { $results = $this->results; - $fn = function () use ($results) { + $fn = function () use ($results): void { $writer = new JsonWriter(); $writer->writeResults(2, 2, true, 1.5, ...$results); }; - $output = <<< OUTPUT -{"type":"results","time_taken":1.5,"passed":true,"results_count":2,"results_passed_count":2,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} - -OUTPUT; + $output = file_get_contents(__DIR__ . '/output/output-results-all-passed.json'); expect($fn)->toEcho($output); }); - it('outputs results if some URLs have failed', function () { + it('outputs results if some URLs have failed', function (): void { $results = $this->results; - $fn = function () use ($results) { + $fn = function () use ($results): void { $writer = new JsonWriter(); $writer->writeResults(1, 2, false, 1.5, ...$results); }; - $output = <<< OUTPUT -{"type":"results","time_taken":1.5,"passed":false,"results_count":2,"results_passed_count":1,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} - -OUTPUT; + $output = file_get_contents(__DIR__ . '/output/output-results-some-failed.json'); expect($fn)->toEcho($output); }); diff --git a/spec/OutputterSpec.php b/spec/OutputterSpec.php index e83ca8e..1339be6 100644 --- a/spec/OutputterSpec.php +++ b/spec/OutputterSpec.php @@ -1,35 +1,38 @@ domain = new \Brunty\Cigar\Url('url', 418, 'teapot', 'teapot'); +describe('Output', function (): void { + beforeEach(function (): void { + $this->domain = new Url('url', 418, 'teapot', 'teapot'); $this->results = [ - new \Brunty\Cigar\Result($this->domain, 418, 'teapot', 'teapot'), - new \Brunty\Cigar\Result($this->domain, 419), + new Result($this->domain, 418, 'teapot', 'teapot'), + new Result($this->domain, 419), ]; $this->passedResults = array_filter($this->results, function (Result $result) { return $result->hasPassed(); }); }); - it('outputs an error line', function () { - $fn = function () { + it('outputs an error line', function (): void { + $fn = function (): void { (new Output())->writeErrorLine('Error message'); }; expect($fn)->toEcho("\033[31mError message\033[0m\n"); }); - it('outputs results if some results have passed', function () { + it('outputs results if some results have passed', function (): void { $results = $this->results; $passedResults = $this->passedResults; allow('microtime')->toBeCalled()->andReturn(3); - $fn = function () use ($passedResults, $results) { + $fn = function () use ($passedResults, $results): void { (new Output())->outputResults($passedResults, $results, 1.5); }; diff --git a/spec/ParserSpec.php b/spec/ParserSpec.php index 6e1bcf9..ad425e6 100644 --- a/spec/ParserSpec.php +++ b/spec/ParserSpec.php @@ -1,11 +1,13 @@ '[ { @@ -29,7 +31,7 @@ ]; vfsStream::setup('root', null, $structure); - $results = (new Parser)->parse('vfs://root/cigar.json'); + $results = (new Parser())->parse('vfs://root/cigar.json'); $expected = [ new Url('http://httpbin.org/status/418', 418), @@ -40,20 +42,20 @@ expect($results)->toEqual($expected); }); - it('lets errors be thrown on parsing a file', function () { + it('lets errors be thrown on parsing a file', function (): void { $structure = [ 'cigar.json' => 'http://httpbin.org/status/418', ]; vfsStream::setup('root', null, $structure); - $fn = function () { - (new Parser)->parse('vfs://root/cigar.json'); + $fn = function (): void { + (new Parser())->parse('vfs://root/cigar.json'); }; expect($fn)->toThrow(new ParseError('Could not parse vfs://root/cigar.json')); }); - it('parses a file that contains both relative and absolute URLs', function () { + it('parses a file that contains both relative and absolute URLs', function (): void { $structure = [ 'cigar.json' => '[ { @@ -85,7 +87,7 @@ expect($results)->toEqual($expected); }); - it('throws an exception if a URL cannot be parsed', function () { + it('throws an exception if a URL cannot be parsed', function (): void { $structure = [ 'cigar.json' => '[ { @@ -99,8 +101,8 @@ vfsStream::setup('root', null, $structure); - $fn = function () { - (new Parser)->parse('vfs://root/cigar.json'); + $fn = function (): void { + (new Parser())->parse('vfs://root/cigar.json'); }; expect($fn)->toThrow(new ParseError('Could not parse URL: http://:80')); diff --git a/spec/ResultsSpec.php b/spec/ResultsSpec.php index 94ca34f..42dca4a 100644 --- a/spec/ResultsSpec.php +++ b/spec/ResultsSpec.php @@ -1,40 +1,42 @@ hasPassed())->toBe(true); }); - it('fails if status codes do not match', function () { + it('fails if status codes do not match', function (): void { $domain = new Url('http://httpbin.org/status/200', 200); $result = new Result($domain, 201); expect($result->hasPassed())->toBe(false); }); - it('passes if the response contains matching content', function () { + it('passes if the response contains matching content', function (): void { $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); $result = new Result($domain, 200, '

foobar

'); expect($result->hasPassed())->toBe(true); }); - it('fails if the response does not contain matching content', function () { + it('fails if the response does not contain matching content', function (): void { $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); $result = new Result($domain, 200, '

hi there

'); expect($result->hasPassed())->toBe(false); }); - it('returns the domain and status code', function () { + it('returns the domain and status code', function (): void { $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); $result = new Result($domain, 200, '

hi there

'); - expect($result->getUrl())->toBe($domain); - expect($result->getStatusCode())->toBe(200); + expect($result->url)->toBe($domain); + expect($result->statusCode)->toBe(200); }); }); diff --git a/spec/output/output-results-all-passed.json b/spec/output/output-results-all-passed.json new file mode 100644 index 0000000..9012260 --- /dev/null +++ b/spec/output/output-results-all-passed.json @@ -0,0 +1 @@ +{"type":"results","time_taken":1.5,"passed":true,"results_count":2,"results_passed_count":2,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} diff --git a/spec/output/output-results-some-failed.json b/spec/output/output-results-some-failed.json new file mode 100644 index 0000000..7449dc9 --- /dev/null +++ b/spec/output/output-results-some-failed.json @@ -0,0 +1 @@ +{"type":"results","time_taken":1.5,"passed":false,"results_count":2,"results_passed_count":1,"results":[{"passed":true,"url":"url","status_code_expected":418,"status_code_actual":418,"content_type_expected":"teapot","content_type_actual":"teapot","content_expected":"teapot"},{"passed":false,"url":"url","status_code_expected":418,"status_code_actual":419,"content_type_expected":"teapot","content_type_actual":null,"content_expected":"teapot"}]} diff --git a/src/AsyncChecker.php b/src/AsyncChecker.php index a95ac8c..1b62f85 100644 --- a/src/AsyncChecker.php +++ b/src/AsyncChecker.php @@ -1,30 +1,16 @@ checkSsl = $checkSsl; - $this->authorizationHeader = $authorizationHeader; - $this->headers = $headers; + public function __construct( + private readonly bool $checkSsl = true, + private readonly ?string $authorizationHeader = null, + private array $headers = [] + ) { } /** @@ -39,29 +25,30 @@ public function check(array $urlsToCheck): array foreach ($urlsToCheck as $urlToCheck) { $channel = curl_init(); - $url = $urlToCheck->getUrl(); + $url = $urlToCheck->url; curl_setopt($channel, CURLOPT_URL, $url); curl_setopt($channel, CURLOPT_HEADER, 0); curl_setopt($channel, CURLOPT_RETURNTRANSFER, 1); - if ( ! $this->checkSsl) { + if (!$this->checkSsl) { curl_setopt($channel, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($channel, CURLOPT_SSL_VERIFYHOST, 0); } if ($this->authorizationHeader) { - $this->headers[] = "Authorization: {$this->authorizationHeader}"; + $this->headers[] = "Authorization: $this->authorizationHeader"; } if (!empty($this->headers)) { curl_setopt($channel, CURLOPT_HTTPHEADER, $this->headers); } - if ($urlToCheck->getConnectTimeout() !== null && $urlToCheck->getConnectTimeout() > 0) { - curl_setopt($channel, CURLOPT_CONNECTTIMEOUT, $urlToCheck->getConnectTimeout()); + if ($urlToCheck->connectTimeout !== null && $urlToCheck->connectTimeout > 0) { + curl_setopt($channel, CURLOPT_CONNECTTIMEOUT, $urlToCheck->connectTimeout); } - if ($urlToCheck->getTimeout() !== null && $urlToCheck->getTimeout() > 0) { - curl_setopt($channel, CURLOPT_TIMEOUT, $urlToCheck->getTimeout()); + + if ($urlToCheck->timeout !== null && $urlToCheck->timeout > 0) { + curl_setopt($channel, CURLOPT_TIMEOUT, $urlToCheck->timeout); } curl_multi_add_handle($mh, $channel); @@ -69,22 +56,19 @@ public function check(array $urlsToCheck): array $channels[$url] = $channel; } - $active = null; - - $running = null; do { curl_multi_exec($mh, $running); } while ($running); $return = []; foreach ($urlsToCheck as $urlToCheck) { - $key = $urlToCheck->getUrl(); + $key = $urlToCheck->url; $channel = $channels[$key]; $code = (int) curl_getinfo($channel, CURLINFO_HTTP_CODE); $content = curl_multi_getcontent($channel); $contentType = curl_getinfo($channel, CURLINFO_CONTENT_TYPE) ?? null; - $return[] = new Result($urlToCheck, $code, $content, $contentType); + $return[] = new Result($urlToCheck, $code, $content, (string) $contentType); curl_multi_remove_handle($mh, $channel); } diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 8cab877..a8a687e 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -1,23 +1,29 @@ writeStats($numberOfPassedResults, $numberOfResults, $passed, $timeDiff); } - private function writeLine(Result $result) + private function writeLine(Result $result): void { $contentType = ''; - list ($colour, $status) = $this->getColourAndStatus($result); - if ($result->getUrl()->getContentType() !== null) { - $contentType = " [{$result->getUrl()->getContentType()}:{$result->getContentType()}]"; + [$colour, $status] = $this->getColourAndStatus($result); + if ($result->url->contentType !== null) { + $contentType = sprintf(' [%s:%s]', $result->url->contentType, (string) $result->contentType); } - echo "{$colour}{$status} {$result->getUrl()->getUrl()} [{$result->getUrl()->getStatus()}:{$result->getStatusCode()}]{$contentType} {$result->getUrl()->getContent()}" . self::CONSOLE_RESET . PHP_EOL; + echo sprintf( + '%s%s %s [%s:%s]%s %s' . self::CONSOLE_RESET . PHP_EOL, + $colour, + $status, + $result->url->url, + $result->url->status, + $result->statusCode, + $contentType, + (string) $result->url->content + ); } - private function writeStats(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff) + private function writeStats(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff): void { $color = self::CONSOLE_GREEN; $reset = self::CONSOLE_RESET; - if ( ! $passed) { + if (!$passed) { $color = self::CONSOLE_RED; } - echo PHP_EOL . "[{$color}{$numberOfPassedResults}/{$numberOfResults}{$reset}] passed in {$timeDiff}s" . PHP_EOL . PHP_EOL; + echo sprintf( + PHP_EOL . '[%s%s/%s%s] passed in %ss' . PHP_EOL . PHP_EOL, + $color, + $numberOfPassedResults, + $numberOfResults, + $reset, + $timeDiff + ); } private function getColourAndStatus(Result $result): array @@ -59,7 +81,7 @@ private function getColourAndStatus(Result $result): array $colour = self::CONSOLE_GREEN; $status = self::SYMBOL_PASSED; - if ( ! $passed) { + if (!$passed) { $colour = self::CONSOLE_RED; $status = self::SYMBOL_FAILED; } diff --git a/src/Input.php b/src/Input.php index a0fa002..07afd9a 100644 --- a/src/Input.php +++ b/src/Input.php @@ -1,5 +1,7 @@ */ private array $options = []; + private array $submittedOptions = []; /** @@ -35,16 +40,14 @@ public function options(): array /** * If the option was configured with {@see InputOption::VALUE_REQUIRED} then it will be the string value of what was - * submitted via the option on the command line, if it's set to {@see InputOption::VALUE_NONE} then it'll be just - * true / false based on if the option was set on the command line or not - * - * @param string $optionName - * @return array|string|bool + * submitted via the option on the command line if supplied, if not it'll be what's configured as the default for + * that option, if it's set to {@see InputOption::VALUE_NONE} then it'll be just true / false based on if the option + * was set on the command line or not */ - public function getOption(string $optionName): array|string|bool + public function getOption(string $optionName): mixed { if (array_key_exists($optionName, $this->options) === false) { - throw new InvalidArgumentException("Could not find option with {$optionName}"); + throw new InvalidArgumentException("Could not find option with $optionName"); } if ($this->submittedOptions === []) { @@ -65,7 +68,7 @@ public function getOption(string $optionName): array|string|bool return $this->submittedOptions[$option->shortCode]; } - return false; + return $option->default; } return $longOptionWasSubmitted || $shortOptionWasSubmitted; diff --git a/src/InputOption.php b/src/InputOption.php index 1f822a6..42bacc9 100644 --- a/src/InputOption.php +++ b/src/InputOption.php @@ -1,5 +1,7 @@ shortCode . ($this->valueMode === self::VALUE_REQUIRED && $this->shortCode !== '' ? ':' : ''); } - public function fullLongCode(): string { + public function fullLongCode(): string + { return $this->longCode . ($this->valueMode === self::VALUE_REQUIRED && $this->longCode !== '' ? ':' : ''); } diff --git a/src/JsonWriter.php b/src/JsonWriter.php index b8332e2..22a1d10 100644 --- a/src/JsonWriter.php +++ b/src/JsonWriter.php @@ -1,5 +1,7 @@ 'results', 'time_taken' => $timeDiff, @@ -28,12 +35,12 @@ private function line(Result $result): array { return [ 'passed' => $result->hasPassed(), - 'url' => $result->getUrl()->getUrl(), - 'status_code_expected' => $result->getUrl()->getStatus(), - 'status_code_actual' => $result->getStatusCode(), - 'content_type_expected' => $result->getUrl()->getContentType(), - 'content_type_actual' => $result->getContentType(), - 'content_expected' => $result->getUrl()->getContent(), + 'url' => $result->url->url, + 'status_code_expected' => $result->url->status, + 'status_code_actual' => $result->statusCode, + 'content_type_expected' => $result->url->contentType, + 'content_type_actual' => $result->contentType, + 'content_expected' => $result->url->content, ]; } } diff --git a/src/Output.php b/src/Output.php index e2fb05e..ece54a4 100644 --- a/src/Output.php +++ b/src/Output.php @@ -1,8 +1,8 @@ options() as $option) { $shortCodeLength = 3; // at it's shortest it is "-c " - $longCodeLength = mb_strlen($option->longCode) + 3; // the +3 is for the double dash before and the space or equals after + $longCodeLength = mb_strlen($option->longCode) + 3; // +3 is for double dash before & the space or = after if ($option->valueIsRequired()) { $shortCodeLength += $valuePlaceholderLength; @@ -68,27 +68,21 @@ public function helpOutputForInputOptions(Input $input): string $shortCode = $this->getShortCodeOutput($option); $longCode = $this->getLongCodeOutput($option); - $shortCode = str_pad($shortCode, $longestShortCodeLength, ' '); - $longCode = str_pad($longCode, $longestLongCodeLength, ' '); + $shortCode = str_pad($shortCode, $longestShortCodeLength); + $longCode = str_pad($longCode, $longestLongCodeLength); + - $output .= " {$optionStartSequence}{$shortCode} {$longCode}{$optionEndSequence} {$option->description}" . - PHP_EOL; + $output = sprintf( + ' %s%s %s%s %s' . PHP_EOL, + $optionStartSequence, + $shortCode, + $longCode, + $optionEndSequence, + $option->description + ); } return $output; - /* -\033[33mOptions:\033[0m - \033[32m-c file.json, --config=file.json\033[0m Use the specified config file instead of the default cigar.json file - \033[32m-u URL, --url=URL\033[0m Base URL for checks, e.g. https://example.org/ - \033[32m-i, --insecure\033[0m Allow invalid SSL certificates - \033[32m-a, --auth\033[0m Authorization header "\074type\076 \074credentials\076" - \033[32m-h, --header\033[0m Custom header "\074name\076: \074value\076" - \033[32m-s --connect-timeout=TIMEOUT\033[0m Connect Timeout - \033[32m-t, --timeout=TIMEOUT\033[0m Timeout - \033[32m-j, --json\033[0m Output JSON - \033[32m --quiet\033[0m Do not output any message - \033[32m --version\033[0m Print the version of Cigar - */ } private function getShortCodeOutput(InputOption $option): string @@ -96,11 +90,11 @@ private function getShortCodeOutput(InputOption $option): string $shortCode = ''; if ($option->shortCode !== '') { - $shortCode = "-{$option->shortCode}"; + $shortCode = "-$option->shortCode"; } if ($option->valueIsRequired()) { - $shortCode = "-{$option->shortCode} VALUE"; + $shortCode = "-$option->shortCode VALUE"; } return $shortCode; @@ -108,10 +102,10 @@ private function getShortCodeOutput(InputOption $option): string private function getLongCodeOutput(InputOption $option): string { - $longCode = "--{$option->longCode}"; + $longCode = "--$option->longCode"; if ($option->valueIsRequired()) { - $longCode = "--{$option->longCode}=VALUE"; + $longCode = "--$option->longCode=VALUE"; } return $longCode; diff --git a/src/Parser.php b/src/Parser.php index 4eb369d..9284772 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1,46 +1,37 @@ baseUrl = rtrim((string) $baseUrl, '/'); - $this->connectTimeout = $connectTimeout; - $this->timeout = $timeout; } /** - * @param string $filename - * * @return Url[] - * @throws \ParseError + * + * @throws ParseError */ public function parse(string $filename): array { $urls = json_decode(file_get_contents($filename), true); - if($urls === null) { - throw new \ParseError('Could not parse ' . $filename); + if ($urls === null) { + throw new ParseError('Could not parse ' . $filename); } - return array_map(function($value) { + return array_map(function (array $value) { $url = $this->getUrl($value['url']); return new Url( @@ -55,20 +46,17 @@ public function parse(string $filename): array } /** - * @param string $url - * - * @return string - * @throws \ParseError + * @throws ParseError */ private function getUrl(string $url): string { $urlParts = parse_url($url); if ($urlParts === false) { - throw new \ParseError("Could not parse URL: $url"); + throw new ParseError("Could not parse URL: $url"); } - if ($this->baseUrl !== null && ! isset($urlParts['host'])) { + if ($this->baseUrl !== '' && !isset($urlParts['host'])) { $url = $this->baseUrl . '/' . ltrim($url, '/'); } diff --git a/src/QuietWriter.php b/src/QuietWriter.php index f7c8761..0eb08c9 100644 --- a/src/QuietWriter.php +++ b/src/QuietWriter.php @@ -1,5 +1,7 @@ url = $url; - $this->statusCode = $statusCode; - $this->contents = $contents; - $this->contentType = $contentType; + public function __construct( + public readonly Url $url, + public readonly int $statusCode, + public readonly ?string $contents = null, + public readonly ?string $contentType = null + ) { } public function hasPassed(): bool @@ -37,34 +19,14 @@ public function hasPassed(): bool return $this->statusMatches() && $this->responseMatchesContentType() && $this->responseHasContent(); } - public function getContentType() - { - return $this->contentType; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - - public function getUrl(): Url - { - return $this->url; - } - - public function getContents() - { - return $this->contents; - } - private function statusMatches(): bool { - return $this->statusCode === $this->url->getStatus(); + return $this->statusCode === $this->url->status; } private function responseMatchesContentType(): bool { - $expectedContentType = $this->url->getContentType(); + $expectedContentType = $this->url->contentType; if ($expectedContentType === null || $this->contentType === null) { return true; // nothing to check @@ -75,12 +37,12 @@ private function responseMatchesContentType(): bool private function responseHasContent(): bool { - $expectedContent = $this->url->getContent(); + $expectedContent = $this->url->content; if ($expectedContent === null) { return true; // nothing to check } - return (bool) strstr($this->contents, $expectedContent); + return (bool) strstr((string) $this->contents, $expectedContent); } } diff --git a/src/Url.php b/src/Url.php index d53ca37..3d48184 100644 --- a/src/Url.php +++ b/src/Url.php @@ -1,84 +1,18 @@ url = $url; - $this->status = $status; - $this->content = $content; - $this->contentType = $contentType; - $this->connectTimeout = $connectTimeout; - $this->timeout = $timeout; - } - - public function getUrl(): string - { - return $this->url; - } - - public function getStatus(): int - { - return $this->status; - } - - public function getContent() - { - return $this->content; - } - - public function getContentType() - { - return $this->contentType; - } - - public function getConnectTimeout() - { - return $this->connectTimeout; - } - - public function getTimeout() - { - return $this->timeout; } } diff --git a/src/Writer.php b/src/Writer.php index e5d85ec..40d2844 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -1,10 +1,18 @@ Date: Thu, 21 Sep 2023 20:14:46 +0100 Subject: [PATCH 13/38] Add github workflow for analysis --- .github/workflows/analysis.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/analysis.yml diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 0000000..ac6e7ef --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,32 @@ +name: Tests +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ['8.1', '8.2'] + name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: curl #optional + coverage: xdebug + - name: Check PHP Version + run: php -v + - name: Install dependencies + run: composer install + - name: Run PHP_CS + run: vendor/bin/phpcs -p --colors --standard=phpcs.xml + - name: Run Psalm + run: vendor/bin/psalm --config=psalm.xml --show-info=true From 138f908b383090215cfe8c1f938cec1a976b7fe6 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 20:16:17 +0100 Subject: [PATCH 14/38] Update master -> main for GH actions --- .github/workflows/analysis.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index ac6e7ef..b0586cb 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -1,9 +1,9 @@ name: Tests on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: run: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5560687..1c19c66 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,9 @@ name: Tests on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: run: diff --git a/README.md b/README.md index 1f694c5..2dc5166 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cigar -[![Build Status](https://travis-ci.org/Brunty/cigar.svg?branch=master)](https://travis-ci.org/Brunty/cigar) [![Coverage Status](https://coveralls.io/repos/github/Brunty/cigar/badge.svg?branch=master)](https://coveralls.io/github/Brunty/cigar?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f/mini.png)](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f) +[![Build Status](https://travis-ci.org/Brunty/cigar.svg?branch=main)](https://travis-ci.org/Brunty/cigar) [![Coverage Status](https://coveralls.io/repos/github/Brunty/cigar/badge.svg?branch=master)](https://coveralls.io/github/Brunty/cigar?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f/mini.png)](https://insight.sensiolabs.com/projects/d89a0b55-8ce6-4f85-a09c-7852d986225f) A smoke testing tool inspired by [symm/vape](https://github.com/symm/vape) From 66aa1dca5eecaccca978e61ba66cd871e3fd04ce Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 20:17:43 +0100 Subject: [PATCH 15/38] Remove phpcs cache --- phpcs.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/phpcs.xml b/phpcs.xml index 1d1c07a..0e29f19 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -192,7 +192,6 @@ tests/unit/* - From fe1e9d29b434fcb0ba06c4c0b189fd152d557ae7 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 20:19:01 +0100 Subject: [PATCH 16/38] Rename code analysis workflow --- .github/workflows/analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index b0586cb..dadb594 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -1,4 +1,4 @@ -name: Tests +name: Code Analysis on: push: branches: [ main ] @@ -11,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['8.1', '8.2'] + php-versions: ['8.2'] name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} steps: - name: Checkout From e0e394791d46f730b3447e132ed2ed787188a93d Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 20:32:19 +0100 Subject: [PATCH 17/38] Add BC notes to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2dc5166..08ba70f 100755 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ Pull via docker: - `.cigar.json` default config file name is now just `cigar.json` - No longer testing against PHP 7.X and PHP 8.0 +- Command line option changes: + - `-c` is now `-f` for config file + - `-s` is now `-c` for connect-timeout ## To use From 78d25934f9ad35828d7633d0ef0efd43e39b48d9 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Thu, 21 Sep 2023 20:38:28 +0100 Subject: [PATCH 18/38] Proper || on PHP requirement --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 485f2a1..6ff4408 100755 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "psr-4": {} }, "require": { - "php": "8.1.*|8.2.*", + "php": "8.1.*||8.2.*", "ext-curl": "*", "ext-json": "*" }, From bb5d16b096f40ae589db023aa439b8fdcc883f0e Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 12:55:33 +0100 Subject: [PATCH 19/38] Start porting tests to PHPUnit with infection --- .gitignore | 1 + composer.json | 22 +++++++-- infection.json5 | 24 ++++++++++ phpunit.xml | 14 ++++++ src/EchoWriter.php | 5 +- tests/Unit/EchoWriterTest.php | 68 ++++++++++++++++++++++++++++ tests/Unit/InputOptionTest.php | 64 ++++++++++++++++++++++++++ tests/Unit/JsonWriterTest.php | 63 ++++++++++++++++++++++++++ tests/Unit/ResultTest.php | 83 ++++++++++++++++++++++++++++++++++ 9 files changed, 336 insertions(+), 8 deletions(-) create mode 100644 infection.json5 create mode 100644 phpunit.xml create mode 100644 tests/Unit/EchoWriterTest.php create mode 100644 tests/Unit/InputOptionTest.php create mode 100644 tests/Unit/JsonWriterTest.php create mode 100644 tests/Unit/ResultTest.php diff --git a/.gitignore b/.gitignore index e6dc20f..3cfe82e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cigar.json coverage.xml clover.xml cache/ +build/ diff --git a/composer.json b/composer.json index 6ff4408..72e4cb9 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "authors": [ { "name": "Matt Brunt", - "email": "matt@mfyu.co.uk" + "email": "matt@brunty.me" } ], "autoload": { @@ -15,7 +15,9 @@ } }, "autoload-dev": { - "psr-4": {} + "psr-4": { + "Brunty\\Cigar\\Tests\\": "tests/" + } }, "require": { "php": "8.1.*||8.2.*", @@ -29,17 +31,27 @@ "symfony/process": "^6.3", "vimeo/psalm": "^5.15", "squizlabs/php_codesniffer": "^3.7", - "slevomat/coding-standard": "^8.13" + "slevomat/coding-standard": "^8.13", + "phpunit/phpunit": "^10.3", + "infection/infection": "^0.27.2" }, "bin": [ "bin/cigar" ], "scripts": { - "test": "vendor/bin/kahlan --reporter=verbose" + "test": "XDEBUG_MODE=coverage vendor/bin/phpunit --color=always --coverage-html=./build/coverage", + "test:mutation": "vendor/bin/infection", + "psalm": "vendor/bin/psalm --config=psalm.xml --show-info=true", + "phpcs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", + "check": [ + "@psalm", + "@phpcs" + ] }, "config": { "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "infection/extension-installer": true } } } diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..cef2cd9 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,24 @@ +{ + "source": { + "directories": [ + "src" + ], + "excludes": [ + "tests", + "spec", + ] + }, + "timeout": 10, + "logs": { + "text": "build/infection/infection.log", + "html": "build/infection/infection.html", + "summary": "build/infection/summary.log", + "json": "build/infection/infection-log.json", + "perMutator": "build/infection/per-mutator.md", + "github": true, + }, + "testFramework":"phpunit", +// "bootstrap":"./infection-bootstrap.php", +// "initialTestsPhpOptions": "-d zend_extension=xdebug.so", +// "testFrameworkOptions": "--filter=Unit" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6b1effd --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + tests + + + + + + src + + + diff --git a/src/EchoWriter.php b/src/EchoWriter.php index a8a687e..0cf2846 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -28,7 +28,6 @@ public function writeResults( foreach ($results as $result) { $this->writeLine($result); - ob_flush(); } ob_end_flush(); @@ -41,7 +40,7 @@ private function writeLine(Result $result): void $contentType = ''; [$colour, $status] = $this->getColourAndStatus($result); if ($result->url->contentType !== null) { - $contentType = sprintf(' [%s:%s]', $result->url->contentType, (string) $result->contentType); + $contentType = sprintf(' [%s:%s]', $result->url->contentType, $result->contentType); } echo sprintf( @@ -52,7 +51,7 @@ private function writeLine(Result $result): void $result->url->status, $result->statusCode, $contentType, - (string) $result->url->content + $result->url->content ); } diff --git a/tests/Unit/EchoWriterTest.php b/tests/Unit/EchoWriterTest.php new file mode 100644 index 0000000..e735964 --- /dev/null +++ b/tests/Unit/EchoWriterTest.php @@ -0,0 +1,68 @@ +writer = new EchoWriter(); + } + + #[Test] + public function it_writes_an_error_line(): void + { + $expected = <<writer->writeErrorLine('error'); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } + + #[Test] + public function it_writes_results(): void + { + $results = [ + new Result(new Url('url1', 201, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url2', 200, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url3', 200, null), 200, null, null), + ]; + + $expected = <<writer->writeResults(2, 3, false, 0.5, ...$results); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } +} diff --git a/tests/Unit/InputOptionTest.php b/tests/Unit/InputOptionTest.php new file mode 100644 index 0000000..4a38dde --- /dev/null +++ b/tests/Unit/InputOptionTest.php @@ -0,0 +1,64 @@ +assertSame('l:', $inputOption->fullShortCode()); + } + + #[Test] + public function it_returns_the_full_long_code_when_a_value_is_required(): void + { + $inputOption = InputOption::create('long-code', 'l', InputOption::VALUE_REQUIRED); + + $this->assertSame('long-code:', $inputOption->fullLongCode()); + } + + #[Test] + public function it_returns_the_full_short_code_when_a_value_is_not_required(): void + { + $inputOption = InputOption::create('long-code', 'l'); + + $this->assertSame('l', $inputOption->fullShortCode()); + } + + #[Test] + public function it_returns_the_full_long_code_when_a_value_is_not_required(): void + { + $inputOption = InputOption::create('long-code', 'l'); + + $this->assertSame('long-code', $inputOption->fullLongCode()); + } + + #[Test] + #[DataProvider('value_required_or_not')] + public function it_returns_if_value_is_required(int $valueMode, bool $expectedReturn): void + { + $inputOption = InputOption::create('long-code', 'l', $valueMode); + $this->assertSame($expectedReturn, $inputOption->valueIsRequired()); + } + + public static function value_required_or_not(): array + { + return [ + [InputOption::VALUE_NONE, false], + [InputOption::VALUE_REQUIRED, true], + ]; + } +} diff --git a/tests/Unit/JsonWriterTest.php b/tests/Unit/JsonWriterTest.php new file mode 100644 index 0000000..8ddc2a4 --- /dev/null +++ b/tests/Unit/JsonWriterTest.php @@ -0,0 +1,63 @@ +writer = new JsonWriter(); + } + + #[Test] + public function it_writes_an_error_line(): void + { + $expected = <<writer->writeErrorLine('problem'); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } + + #[Test] + public function it_writes_results(): void + { + $results = [ + new Result(new Url('url', 201, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), + new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), + ]; + + $expected = <<writer->writeResults(2, 3, false, 0.5, ...$results); + $output = ob_get_contents(); + ob_end_clean(); + + $this->assertSame($expected, $output); + } +} diff --git a/tests/Unit/ResultTest.php b/tests/Unit/ResultTest.php new file mode 100644 index 0000000..0e51009 --- /dev/null +++ b/tests/Unit/ResultTest.php @@ -0,0 +1,83 @@ + [ + new Url('url', 200, 'content', 'text'), + 200, + 'content', + 'text', + true, + ], + 'Status mismatch, Content match, Content Type match' => [ + new Url('url', 201, 'content', 'text'), + 200, + 'content', + 'text', + false, + ], + 'Status match, Content match, Content Type mismatch' => [ + new Url('url', 200, 'content', 'text'), + 200, + 'content', + 'html', + false, + ], + 'Status match, Content mismatch, Content Type match' => [ + new Url('url', 200, 'content', 'text'), + 200, + 'some text', + 'text', + false, + ], + 'Status match, Content null, Content Type match' => [ + new Url('url', 200, null, 'TEXT'), + 200, + 'some text', + 'TExt', + true, + ], + 'Status match, Content null, Content Type null' => [ + new Url('url', 200, null, null), + 200, + 'some text', + 'text', + true, + ], + 'Status match, Content null mismatch, Content Type null' => [ + new Url('url', 200, 'content', null), + 200, + null, + 'text', + false, + ], + ]; + } + + #[Test] + #[DataProvider('results')] + public function it_returns_if_it_has_passed( + Url $url, + int $statusCode, + ?string $contents, + ?string $contentType, + bool $hasPassed + ): void { + $result = new Result($url, $statusCode, $contents, $contentType); + + $this->assertSame($hasPassed, $result->hasPassed()); + } +} From 02f585a2eaa2c3272c75e84a7d648c021b9d4e46 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 12:57:36 +0100 Subject: [PATCH 20/38] Refactor Parser -> ConfigParser --- bin/cigar | 4 ++-- infection.json5 | 3 --- spec/ParserSpec.php | 12 ++++++------ src/{Parser.php => ConfigParser.php} | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) rename src/{Parser.php => ConfigParser.php} (98%) diff --git a/bin/cigar b/bin/cigar index ea55710..1a397ab 100755 --- a/bin/cigar +++ b/bin/cigar @@ -18,7 +18,7 @@ use Brunty\Cigar\Input; use Brunty\Cigar\InputOption; use Brunty\Cigar\JsonWriter; use Brunty\Cigar\Output; -use Brunty\Cigar\Parser; +use Brunty\Cigar\ConfigParser; use Brunty\Cigar\QuietWriter; use Brunty\Cigar\Result; @@ -100,7 +100,7 @@ $connectTimeout = $input->getOption('connect-timeout') ?: null; $timeout = $input->getOption('timeout') ?: null; try { - $domains = (new Parser($baseUrl, $connectTimeout, $timeout))->parse($configFile); + $domains = (new ConfigParser($baseUrl, $connectTimeout, $timeout))->parse($configFile); } catch (Throwable $e) { $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); exit(1); diff --git a/infection.json5 b/infection.json5 index cef2cd9..1c2fc72 100644 --- a/infection.json5 +++ b/infection.json5 @@ -18,7 +18,4 @@ "github": true, }, "testFramework":"phpunit", -// "bootstrap":"./infection-bootstrap.php", -// "initialTestsPhpOptions": "-d zend_extension=xdebug.so", -// "testFrameworkOptions": "--filter=Unit" } diff --git a/spec/ParserSpec.php b/spec/ParserSpec.php index ad425e6..03af976 100644 --- a/spec/ParserSpec.php +++ b/spec/ParserSpec.php @@ -2,11 +2,11 @@ declare(strict_types=1); -use Brunty\Cigar\Parser; +use Brunty\Cigar\ConfigParser; use Brunty\Cigar\Url; use org\bovigo\vfs\vfsStream; -describe('Parser', function (): void { +describe('ConfigParser', function (): void { it('parses a file that is correctly formatted', function (): void { $structure = [ 'cigar.json' => '[ @@ -31,7 +31,7 @@ ]; vfsStream::setup('root', null, $structure); - $results = (new Parser())->parse('vfs://root/cigar.json'); + $results = (new ConfigParser())->parse('vfs://root/cigar.json'); $expected = [ new Url('http://httpbin.org/status/418', 418), @@ -49,7 +49,7 @@ vfsStream::setup('root', null, $structure); $fn = function (): void { - (new Parser())->parse('vfs://root/cigar.json'); + (new ConfigParser())->parse('vfs://root/cigar.json'); }; expect($fn)->toThrow(new ParseError('Could not parse vfs://root/cigar.json')); @@ -76,7 +76,7 @@ ]; vfsStream::setup('root', null, $structure); - $results = (new Parser('http://httpbin.org'))->parse('vfs://root/cigar.json'); + $results = (new ConfigParser('http://httpbin.org'))->parse('vfs://root/cigar.json'); $expected = [ new Url('http://httpbin.org/status/418', 418), @@ -102,7 +102,7 @@ $fn = function (): void { - (new Parser())->parse('vfs://root/cigar.json'); + (new ConfigParser())->parse('vfs://root/cigar.json'); }; expect($fn)->toThrow(new ParseError('Could not parse URL: http://:80')); diff --git a/src/Parser.php b/src/ConfigParser.php similarity index 98% rename from src/Parser.php rename to src/ConfigParser.php index 9284772..aa5fbfa 100644 --- a/src/Parser.php +++ b/src/ConfigParser.php @@ -6,7 +6,7 @@ use ParseError; -class Parser +class ConfigParser { private string $baseUrl; From 9c94a246e8b1b83a8dbabf68e56ed6937f1d6da1 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 13:16:20 +0100 Subject: [PATCH 21/38] Config parser test --- tests/Unit/ConfigParserTest.php | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/Unit/ConfigParserTest.php diff --git a/tests/Unit/ConfigParserTest.php b/tests/Unit/ConfigParserTest.php new file mode 100644 index 0000000..7705268 --- /dev/null +++ b/tests/Unit/ConfigParserTest.php @@ -0,0 +1,123 @@ + '[ + { + "url": "http://httpbin.org/status/418", + "status": 418 + }, + { + "url": "http://httpbin.org/status/200", + "status": 200 + }, + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot", + "content-type": "kitchen/teapot", + "connect-timeout": 1, + "timeout": 2 + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + $results = (new ConfigParser())->parse('vfs://root/cigar.json'); + + $expected = [ + new Url('http://httpbin.org/status/418', 418), + new Url('http://httpbin.org/status/200', 200), + new Url('http://httpbin.org/status/418', 418, 'teapot', 'kitchen/teapot', 1, 2), + ]; + + $this->assertEquals($expected, $results); + } + + #[Test] + public function it_parses_a_file_that_contains_both_absolute_and_relative_urls(): void + { + $structure = [ + 'cigar.json' => '[ + { + "url": "/status/418", + "status": 418 + }, + { + "url": "status/200", + "status": 200 + }, + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot" + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + $results = (new ConfigParser('http://httpbin.org/'))->parse('vfs://root/cigar.json'); + + $expected = [ + new Url('http://httpbin.org/status/418', 418), + new Url('http://httpbin.org/status/200', 200), + new Url('http://httpbin.org/status/418', 418, 'teapot'), + ]; + + $this->assertEquals($expected, $results); + } + + #[Test] + public function it_throws_an_exception_if_a_url_cannot_be_parsed(): void + { + $this->expectException(ParseError::class); + $this->expectExceptionMessage('Could not parse URL: http://:80'); + $structure = [ + 'cigar.json' => '[ + { + "url": "http://:80", + "status": 418, + "content": "teapot" + } +] +', + ]; + vfsStream::setup('root', null, $structure); + + (new ConfigParser())->parse('vfs://root/cigar.json'); + } + + #[Test] + public function it_lets_errors_be_thrown_on_parsing_a_file(): void + { + $this->expectException(ParseError::class); + $this->expectExceptionMessage('Could not parse vfs://root/cigar.json'); + $structure = [ + 'cigar.json' => 'http://httpbin.org/status/418', + ]; + vfsStream::setup('root', null, $structure); + + (new ConfigParser())->parse('vfs://root/cigar.json'); + } +} From e30c781a3ebc127b406e82684cf13d368adfc8c1 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 15:21:15 +0100 Subject: [PATCH 22/38] More test updates lol messy commits --- bin/cigar | 10 +- composer.json | 4 +- spec/AsyncCheckerSpec.php | 82 ---------- spec/CigarCliSpec.php | 118 --------------- spec/JsonWriterSpec.php | 51 ------- spec/OutputterSpec.php | 50 ------- spec/ParserSpec.php | 110 -------------- spec/ResultsSpec.php | 42 ------ spec/stubs/cigar.fail.json | 32 ---- spec/stubs/cigar.headers.json | 12 -- spec/stubs/cigar.insecure.json | 6 - spec/stubs/cigar.pass.json | 32 ---- spec/stubs/cigar.timeouts.json | 14 -- spec/stubs/cigar.without-base.json | 16 -- src/ConfigParser.php | 4 +- src/EchoWriter.php | 2 +- src/Input.php | 40 +---- src/InputOptions.php | 30 ++++ src/Output.php | 8 +- src/Result.php | 10 +- src/Url.php | 4 +- .../Output}/output-results-all-passed.json | 0 .../Output}/output-results-some-failed.json | 0 tests/Unit/EchoWriterTest.php | 2 +- tests/Unit/InputOptionsTest.php | 29 ++++ tests/Unit/InputTest.php | 140 ++++++++++++++++++ tests/Unit/ResultTest.php | 19 ++- 27 files changed, 241 insertions(+), 626 deletions(-) delete mode 100644 spec/AsyncCheckerSpec.php delete mode 100755 spec/CigarCliSpec.php delete mode 100644 spec/JsonWriterSpec.php delete mode 100644 spec/OutputterSpec.php delete mode 100644 spec/ParserSpec.php delete mode 100644 spec/ResultsSpec.php delete mode 100644 spec/stubs/cigar.fail.json delete mode 100755 spec/stubs/cigar.headers.json delete mode 100644 spec/stubs/cigar.insecure.json delete mode 100644 spec/stubs/cigar.pass.json delete mode 100644 spec/stubs/cigar.timeouts.json delete mode 100644 spec/stubs/cigar.without-base.json create mode 100644 src/InputOptions.php rename {spec/output => tests/Output}/output-results-all-passed.json (100%) rename {spec/output => tests/Output}/output-results-some-failed.json (100%) create mode 100644 tests/Unit/InputOptionsTest.php create mode 100644 tests/Unit/InputTest.php diff --git a/bin/cigar b/bin/cigar index 1a397ab..87ebc8a 100755 --- a/bin/cigar +++ b/bin/cigar @@ -16,14 +16,14 @@ use Brunty\Cigar\AsyncChecker; use Brunty\Cigar\EchoWriter; use Brunty\Cigar\Input; use Brunty\Cigar\InputOption; +use Brunty\Cigar\InputOptions; use Brunty\Cigar\JsonWriter; use Brunty\Cigar\Output; use Brunty\Cigar\ConfigParser; use Brunty\Cigar\QuietWriter; use Brunty\Cigar\Result; -$input = new Input(); -$input->configureOptions([ +$inputOptions = new InputOptions([ 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), @@ -37,13 +37,17 @@ $input->configureOptions([ 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), ]); +$submittedInput = getopt($inputOptions->shortCodes, $inputOptions->longCodes); + +$input = new Input($inputOptions, $submittedInput); + $output = new Output( $input->getOption('quiet') ? new QuietWriter() : ($input->getOption('json') ? new JsonWriter() : new EchoWriter()) ); -$inputOptionsHelpOutput = $output->helpOutputForInputOptions($input); +$inputOptionsHelpOutput = $output->helpOutputForInputOptions($inputOptions); if ($input->getOption('help')) { $content = <<check($domains); - - $expected = [ - new Result($domain, 200, '', 'text/html; charset=utf-8'), - ]; - - expect($results)->toEqual($expected); - }); - - it('checks more than one domain', function (): void { - - $domains = [ - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/500', 500), - new Url('http://httpbin.org/status/404', 404), - ]; - - $results = (new AsyncChecker())->check($domains); - - $expected = array_map(function (Url $domain) { - return new Result($domain, $domain->status, '', 'text/html; charset=utf-8'); - }, $domains); - - expect($results)->toEqual($expected); - }); - - it('checks authorization header', function (): void { - $domain = new Url('http://httpbin.org/get', 200); - $expectedAuthHeader = 'Basic dXNyOnBzd2Q='; - - $results = (new AsyncChecker(false, $expectedAuthHeader))->check([$domain]); - - $decodedContent = json_decode($results[0]->contents, true); - $actualAuthHeader = $decodedContent['headers']['Authorization'] ?? null; - - expect($actualAuthHeader)->toEqual($expectedAuthHeader); - }); - - context('when SSL verification is disabled', function (): void { - it('checks a domain that has an invalid certificate', function (): void { - $statusCode = 403; - // Need to change for a better setup URL that doesn't default to a potentially unknown site - $domain = new Url('https://cigar-do-not-work.apps.brunty.me', $statusCode); - $domains = [$domain]; - - $results = (new AsyncChecker(false))->check($domains); - - $expected = [new Result($domain, $statusCode)]; - - expect($results[0]->statusCode)->toEqual($expected[0]->statusCode); - }); - }); - - context('when timeouts are set', function (): void { - it('checks a URL that will timeout', function (): void { - // Need to change for a better setup URL that doesn't default to a potentially unknown site - $domain = new Url('https://httpbin.org/delay/3', 200, null, null, 1, 1); - $domains = [$domain]; - - $results = (new AsyncChecker())->check($domains); - - $expected = [ - new Result($domain, 0), - ]; - - expect($results[0]->statusCode)->toEqual($expected[0]->statusCode); - }); - }); -}); diff --git a/spec/CigarCliSpec.php b/spec/CigarCliSpec.php deleted file mode 100755 index 2e9f3f0..0000000 --- a/spec/CigarCliSpec.php +++ /dev/null @@ -1,118 +0,0 @@ -run(); - }); - - it('passes if given good URLs to check', function (): void { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('is quiet if given the option', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.pass.json cigar.json && ../bin/cigar --quiet' - ); - $process->run(); - - expect($process->getOutput())->toBe(''); - expect($process->getExitCode())->toBe(0); - }); - - it('fails if given bad URLs to check', function (): void { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.fail.json cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); - - it('can be given an alternative configuration file to load with a short command line flag', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar -f .config.json' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be given an alternative configuration file to load with a long command line flag', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.pass.json .config.json && ../bin/cigar --config .config.json' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('fails with insecure certs', function (): void { - $process = Process::fromShellCommandline('cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar'); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); - - it('passes with insecure certs if the insecure flag is specified', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.insecure.json cigar.json && ../bin/cigar --insecure' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed a base URL as a short command argument', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar -u http://httpbin.org' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed a base URL as a full command argument', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.without-base.json cigar.json && ../bin/cigar --url=http://httpbin.org' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed headers as full arguments', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar ' . - '--header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed headers as short arguments', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.headers.json cigar.json && ../bin/cigar ' . - '-h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"' - ); - $process->run(); - - expect($process->getExitCode())->toBe(0); - }); - - it('can be passed timeout arguments and it overwrites the configured value in cigar.json', function (): void { - $process = Process::fromShellCommandline( - 'cd spec && cp stubs/cigar.timeouts.json cigar.json && ../bin/cigar -t 1' - ); - $process->run(); - - expect($process->getExitCode())->toBe(1); - }); -}); diff --git a/spec/JsonWriterSpec.php b/spec/JsonWriterSpec.php deleted file mode 100644 index 6a21057..0000000 --- a/spec/JsonWriterSpec.php +++ /dev/null @@ -1,51 +0,0 @@ -domain = new Url('url', 418, 'teapot', 'teapot'); - $this->results = [ - new Result($this->domain, 418, 'teapot', 'teapot'), - new Result($this->domain, 419), - ]; - }); - - it('outputs an error line', function (): void { - $fn = function (): void { - (new JsonWriter())->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho('{"type":"error","message":"Error message"}' . PHP_EOL); - }); - - it('outputs results if all results have passed', function (): void { - $results = $this->results; - - $fn = function () use ($results): void { - $writer = new JsonWriter(); - $writer->writeResults(2, 2, true, 1.5, ...$results); - }; - - $output = file_get_contents(__DIR__ . '/output/output-results-all-passed.json'); - - expect($fn)->toEcho($output); - }); - - it('outputs results if some URLs have failed', function (): void { - $results = $this->results; - - $fn = function () use ($results): void { - $writer = new JsonWriter(); - $writer->writeResults(1, 2, false, 1.5, ...$results); - }; - - $output = file_get_contents(__DIR__ . '/output/output-results-some-failed.json'); - - expect($fn)->toEcho($output); - }); -}); diff --git a/spec/OutputterSpec.php b/spec/OutputterSpec.php deleted file mode 100644 index 1339be6..0000000 --- a/spec/OutputterSpec.php +++ /dev/null @@ -1,50 +0,0 @@ -domain = new Url('url', 418, 'teapot', 'teapot'); - $this->results = [ - new Result($this->domain, 418, 'teapot', 'teapot'), - new Result($this->domain, 419), - ]; - $this->passedResults = array_filter($this->results, function (Result $result) { - return $result->hasPassed(); - }); - }); - - it('outputs an error line', function (): void { - $fn = function (): void { - (new Output())->writeErrorLine('Error message'); - }; - - expect($fn)->toEcho("\033[31mError message\033[0m\n"); - }); - - it('outputs results if some results have passed', function (): void { - $results = $this->results; - $passedResults = $this->passedResults; - - allow('microtime')->toBeCalled()->andReturn(3); - - $fn = function () use ($passedResults, $results): void { - (new Output())->outputResults($passedResults, $results, 1.5); - }; - - $output = <<< OUTPUT -\e[32m✓ url [418:418] [teapot:teapot] teapot\e[0m -\e[31m✘ url [418:419] [teapot:] teapot\e[0m - -[\e[31m1/2\e[0m] passed in 1.5s - - -OUTPUT; - - expect($fn)->toEcho($output); - }); -}); diff --git a/spec/ParserSpec.php b/spec/ParserSpec.php deleted file mode 100644 index 03af976..0000000 --- a/spec/ParserSpec.php +++ /dev/null @@ -1,110 +0,0 @@ - '[ - { - "url": "http://httpbin.org/status/418", - "status": 418 - }, - { - "url": "http://httpbin.org/status/200", - "status": 200 - }, - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot", - "content-type": "kitchen/teapot", - "connect-timeout": 1, - "timeout": 2 - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - $results = (new ConfigParser())->parse('vfs://root/cigar.json'); - - $expected = [ - new Url('http://httpbin.org/status/418', 418), - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/418', 418, 'teapot', 'kitchen/teapot', 1, 2), - ]; - - expect($results)->toEqual($expected); - }); - - it('lets errors be thrown on parsing a file', function (): void { - $structure = [ - 'cigar.json' => 'http://httpbin.org/status/418', - ]; - vfsStream::setup('root', null, $structure); - - $fn = function (): void { - (new ConfigParser())->parse('vfs://root/cigar.json'); - }; - - expect($fn)->toThrow(new ParseError('Could not parse vfs://root/cigar.json')); - }); - - it('parses a file that contains both relative and absolute URLs', function (): void { - $structure = [ - 'cigar.json' => '[ - { - "url": "/status/418", - "status": 418 - }, - { - "url": "status/200", - "status": 200 - }, - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - $results = (new ConfigParser('http://httpbin.org'))->parse('vfs://root/cigar.json'); - - $expected = [ - new Url('http://httpbin.org/status/418', 418), - new Url('http://httpbin.org/status/200', 200), - new Url('http://httpbin.org/status/418', 418, 'teapot'), - ]; - - expect($results)->toEqual($expected); - }); - - it('throws an exception if a URL cannot be parsed', function (): void { - $structure = [ - 'cigar.json' => '[ - { - "url": "http://:80", - "status": 418, - "content": "teapot" - } -] -', - ]; - vfsStream::setup('root', null, $structure); - - - $fn = function (): void { - (new ConfigParser())->parse('vfs://root/cigar.json'); - }; - - expect($fn)->toThrow(new ParseError('Could not parse URL: http://:80')); - }); -}); diff --git a/spec/ResultsSpec.php b/spec/ResultsSpec.php deleted file mode 100644 index 42dca4a..0000000 --- a/spec/ResultsSpec.php +++ /dev/null @@ -1,42 +0,0 @@ -hasPassed())->toBe(true); - }); - - it('fails if status codes do not match', function (): void { - $domain = new Url('http://httpbin.org/status/200', 200); - $result = new Result($domain, 201); - - expect($result->hasPassed())->toBe(false); - }); - - it('passes if the response contains matching content', function (): void { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

foobar

'); - expect($result->hasPassed())->toBe(true); - }); - - it('fails if the response does not contain matching content', function (): void { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

hi there

'); - expect($result->hasPassed())->toBe(false); - }); - - it('returns the domain and status code', function (): void { - $domain = new Url('http://httpbin.org/status/200', 200, 'foobar'); - $result = new Result($domain, 200, '

hi there

'); - - expect($result->url)->toBe($domain); - expect($result->statusCode)->toBe(200); - }); -}); diff --git a/spec/stubs/cigar.fail.json b/spec/stubs/cigar.fail.json deleted file mode 100644 index 0f3d661..0000000 --- a/spec/stubs/cigar.fail.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "http://httpbin.org/status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - }, - { - "url": "http://httpbin.org/status/500", - "status": 501 - }, - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 1 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 1 - } -] diff --git a/spec/stubs/cigar.headers.json b/spec/stubs/cigar.headers.json deleted file mode 100755 index 856017f..0000000 --- a/spec/stubs/cigar.headers.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "url": "http://httpbin.org/headers", - "status": 200, - "content": "some-header-content-here" - }, - { - "url": "http://httpbin.org/headers", - "status": 200, - "content": "more-header-content-here" - } -] diff --git a/spec/stubs/cigar.insecure.json b/spec/stubs/cigar.insecure.json deleted file mode 100644 index 15a0268..0000000 --- a/spec/stubs/cigar.insecure.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "url": "https://cigar-do-not-work.apps.brunty.me", - "status": 403 - } -] diff --git a/spec/stubs/cigar.pass.json b/spec/stubs/cigar.pass.json deleted file mode 100644 index a422997..0000000 --- a/spec/stubs/cigar.pass.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "url": "http://httpbin.org/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "http://httpbin.org/status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - }, - { - "url": "http://httpbin.org/status/500", - "status": 500 - }, - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - } -] diff --git a/spec/stubs/cigar.timeouts.json b/spec/stubs/cigar.timeouts.json deleted file mode 100644 index f3b12cc..0000000 --- a/spec/stubs/cigar.timeouts.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "url": "http://httpbin.org/delay/3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - }, - { - "url": "http://httpbin.org/drip?duration=3", - "status": 200, - "request-timeout": 1, - "response-timeout": 5 - } -] diff --git a/spec/stubs/cigar.without-base.json b/spec/stubs/cigar.without-base.json deleted file mode 100644 index 57fb68e..0000000 --- a/spec/stubs/cigar.without-base.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "url": "/status/418", - "status": 418, - "content": "teapot" - }, - { - "url": "status/200", - "status": 200, - "content-type": "text/html" - }, - { - "url": "http://httpbin.org/status/304", - "status": 304 - } -] diff --git a/src/ConfigParser.php b/src/ConfigParser.php index aa5fbfa..a8146c1 100644 --- a/src/ConfigParser.php +++ b/src/ConfigParser.php @@ -37,8 +37,8 @@ public function parse(string $filename): array return new Url( $url, $value['status'], - $value['content'] ?? null, - $value['content-type'] ?? null, + $value['content'] ?? '', + $value['content-type'] ?? '', $value['connect-timeout'] ?? $this->connectTimeout, $value['timeout'] ?? $this->timeout ); diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 0cf2846..8145680 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -39,7 +39,7 @@ private function writeLine(Result $result): void { $contentType = ''; [$colour, $status] = $this->getColourAndStatus($result); - if ($result->url->contentType !== null) { + if ($result->url->contentType !== '') { $contentType = sprintf(' [%s:%s]', $result->url->contentType, $result->contentType); } diff --git a/src/Input.php b/src/Input.php index 07afd9a..cd9c9f4 100644 --- a/src/Input.php +++ b/src/Input.php @@ -8,34 +8,8 @@ class Input { - private string $optionShortCodes = ''; - - private array $optionLongCodes = []; - - /** @var array */ - private array $options = []; - - private array $submittedOptions = []; - - /** - * @param array $options - */ - public function configureOptions(array $options): void - { - ksort($options); - $this->options = $options; - foreach ($options as $inputOption) { - $this->optionShortCodes .= $inputOption->fullShortCode(); - $this->optionLongCodes[] = $inputOption->fullLongCode(); - } - } - - /** - * @return array - */ - public function options(): array + public function __construct(private readonly InputOptions $inputOptions, private readonly array $submittedOptions) { - return $this->options; } /** @@ -46,26 +20,22 @@ public function options(): array */ public function getOption(string $optionName): mixed { - if (array_key_exists($optionName, $this->options) === false) { + if (array_key_exists($optionName, $this->inputOptions->options) === false) { throw new InvalidArgumentException("Could not find option with $optionName"); } - if ($this->submittedOptions === []) { - $this->submittedOptions = getopt($this->optionShortCodes, $this->optionLongCodes); - } - - $option = $this->options[$optionName]; + $option = $this->inputOptions->options[$optionName]; $longOptionWasSubmitted = array_key_exists($option->longCode, $this->submittedOptions); $shortOptionWasSubmitted = array_key_exists($option->shortCode, $this->submittedOptions); if ($option->valueIsRequired()) { if ($longOptionWasSubmitted) { - return $this->submittedOptions[$option->longCode]; + return $this->submittedOptions[$option->longCode] ?: $option->default; } if ($shortOptionWasSubmitted) { - return $this->submittedOptions[$option->shortCode]; + return $this->submittedOptions[$option->shortCode] ?: $option->default; } return $option->default; diff --git a/src/InputOptions.php b/src/InputOptions.php new file mode 100644 index 0000000..6977d1f --- /dev/null +++ b/src/InputOptions.php @@ -0,0 +1,30 @@ + $options */ + public function __construct(public readonly array $options) + { + $shortCodes = ''; + $longCodes = []; + + ksort($options); + + foreach ($options as $inputOption) { + $shortCodes .= $inputOption->fullShortCode(); + $longCodes[] = $inputOption->fullLongCode(); + } + + $this->shortCodes = $shortCodes; + $this->longCodes = $longCodes; + } +} diff --git a/src/Output.php b/src/Output.php index ece54a4..b759339 100644 --- a/src/Output.php +++ b/src/Output.php @@ -31,14 +31,14 @@ public function outputResults(array $passedResults, array $results, float $start $this->writer->writeResults($numberOfPassedResults, $numberOfResults, $passed, $timeDiff, ...$results); } - public function helpOutputForInputOptions(Input $input): string + public function helpOutputForInputOptions(InputOptions $inputOptions): string { $optionStartSequence = "\033[32m"; $optionEndSequence = "\033[0m"; $output = ''; - if ($input->options() !== []) { + if ($inputOptions->options !== []) { $output = "\033[33mOptions:\033[0m" . PHP_EOL; } @@ -46,7 +46,7 @@ public function helpOutputForInputOptions(Input $input): string $longestLongCodeLength = 0; $valuePlaceholderLength = mb_strlen(self::VALUE_PLACEHOLDER); - foreach ($input->options() as $option) { + foreach ($inputOptions->options as $option) { $shortCodeLength = 3; // at it's shortest it is "-c " $longCodeLength = mb_strlen($option->longCode) + 3; // +3 is for double dash before & the space or = after @@ -64,7 +64,7 @@ public function helpOutputForInputOptions(Input $input): string } } - foreach ($input->options() as $option) { + foreach ($inputOptions->options as $option) { $shortCode = $this->getShortCodeOutput($option); $longCode = $this->getLongCodeOutput($option); diff --git a/src/Result.php b/src/Result.php index c969427..da5d871 100644 --- a/src/Result.php +++ b/src/Result.php @@ -9,8 +9,8 @@ class Result public function __construct( public readonly Url $url, public readonly int $statusCode, - public readonly ?string $contents = null, - public readonly ?string $contentType = null + public readonly string $contents = '', + public readonly string $contentType = '' ) { } @@ -28,7 +28,7 @@ private function responseMatchesContentType(): bool { $expectedContentType = $this->url->contentType; - if ($expectedContentType === null || $this->contentType === null) { + if ($expectedContentType === '') { return true; // nothing to check } @@ -39,10 +39,10 @@ private function responseHasContent(): bool { $expectedContent = $this->url->content; - if ($expectedContent === null) { + if ($expectedContent === '') { return true; // nothing to check } - return (bool) strstr((string) $this->contents, $expectedContent); + return (bool) strstr($this->contents, $expectedContent); } } diff --git a/src/Url.php b/src/Url.php index 3d48184..91b0cea 100644 --- a/src/Url.php +++ b/src/Url.php @@ -9,8 +9,8 @@ class Url public function __construct( public readonly string $url, public readonly int $status, - public readonly ?string $content = null, - public readonly ?string $contentType = null, + public readonly string $content = '', + public readonly string $contentType = '', public readonly ?int $connectTimeout = null, public readonly ?int $timeout = null ) { diff --git a/spec/output/output-results-all-passed.json b/tests/Output/output-results-all-passed.json similarity index 100% rename from spec/output/output-results-all-passed.json rename to tests/Output/output-results-all-passed.json diff --git a/spec/output/output-results-some-failed.json b/tests/Output/output-results-some-failed.json similarity index 100% rename from spec/output/output-results-some-failed.json rename to tests/Output/output-results-some-failed.json diff --git a/tests/Unit/EchoWriterTest.php b/tests/Unit/EchoWriterTest.php index e735964..9612a9f 100644 --- a/tests/Unit/EchoWriterTest.php +++ b/tests/Unit/EchoWriterTest.php @@ -45,7 +45,7 @@ public function it_writes_results(): void $results = [ new Result(new Url('url1', 201, 'c', 't'), 200, 'c', 't'), new Result(new Url('url2', 200, 'c', 't'), 200, 'c', 't'), - new Result(new Url('url3', 200, null), 200, null, null), + new Result(new Url('url3', 200, ''), 200, '', ''), ]; $expected = << InputOption::create('help', 'h'), + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED), + ]); + + $this->assertSame('f:h', $inputOptions->shortCodes); + $this->assertSame(['file:', 'help'], $inputOptions->longCodes); + } +} diff --git a/tests/Unit/InputTest.php b/tests/Unit/InputTest.php new file mode 100644 index 0000000..2f5c599 --- /dev/null +++ b/tests/Unit/InputTest.php @@ -0,0 +1,140 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Could not find option with my-option'); + + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE), + ]), + [], + ); + + $input->getOption('my-option'); + } + + #[Test] + public function it_returns_false_if_a_no_value_option_was_not_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + [], + ); + + $this->assertFalse($input->getOption('help')); + } + + #[Test] + public function it_returns_true_if_a_no_value_option_was_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + ['h' => false], + ); + + $this->assertTrue($input->getOption('help')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_not_submitted_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + [], + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_submitted_with_no_value_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['f' => false], // getopt() wouldn't even put this in the array, but this is just to be safe + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_value_if_a_required_value_option_was_submitted_with_a_value_with_a_short_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['f' => 'bar'], + ); + + $this->assertSame('bar', $input->getOption('file')); + } + + #[Test] + public function it_returns_true_if_a_no_value_option_was_submitted_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'help' => InputOption::create('help', 'h'), + ]), + ['help' => false], + ); + + $this->assertTrue($input->getOption('help')); + } + + #[Test] + public function it_returns_the_default_value_if_a_required_value_option_was_submitted_with_no_value_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['file' => false], // getopt() wouldn't even put this in the array, but this is just to be safe + ); + + $this->assertSame('default-value', $input->getOption('file')); + } + + #[Test] + public function it_returns_the_value_if_a_required_value_option_was_submitted_with_a_value_with_a_long_code(): void + { + $input = new Input( + new InputOptions([ + 'file' => InputOption::create('file', 'f', InputOption::VALUE_REQUIRED, 'description', 'default-value'), + ]), + ['file' => 'bar'], + ); + + $this->assertSame('bar', $input->getOption('file')); + } +} diff --git a/tests/Unit/ResultTest.php b/tests/Unit/ResultTest.php index 0e51009..b396547 100644 --- a/tests/Unit/ResultTest.php +++ b/tests/Unit/ResultTest.php @@ -44,26 +44,33 @@ public static function results(): array false, ], 'Status match, Content null, Content Type match' => [ - new Url('url', 200, null, 'TEXT'), + new Url('url', 200, '', 'TEXT'), 200, 'some text', 'TExt', true, ], - 'Status match, Content null, Content Type null' => [ - new Url('url', 200, null, null), + 'Status match, Content empty, Content Type empty' => [ + new Url('url', 200, '', ''), 200, 'some text', 'text', true, ], - 'Status match, Content null mismatch, Content Type null' => [ - new Url('url', 200, 'content', null), + 'Status match, Content mismatch, Content Type empty' => [ + new Url('url', 200, 'content', ''), 200, - null, + '', 'text', false, ], + 'Status match, Content both empty, Content Type empty' => [ + new Url('url', 200, '', 't'), + 200, + '', + 'y', + false, + ], ]; } From 531b96ed07dea8e11d9ecfb1977ccc83348a5f60 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 16:59:19 +0100 Subject: [PATCH 23/38] A bunch of refactoring innit --- bin/cigar | 68 +++++++++++++++++------------------ composer.json | 1 + phpcs.xml | 2 +- src/AsyncChecker.php | 10 +++--- src/EchoWriter.php | 23 +++++------- src/JsonWriter.php | 13 +++---- src/Output.php | 21 ++++------- src/QuietWriter.php | 9 ++--- src/Results.php | 45 +++++++++++++++++++++++ src/SystemTimer.php | 39 ++++++++++++++++++++ src/Timer.php | 12 +++++++ src/Writer.php | 8 +---- tests/Unit/EchoWriterTest.php | 8 +++-- tests/Unit/JsonWriterTest.php | 8 +++-- tests/Unit/OutputTest.php | 59 ++++++++++++++++++++++++++++++ 15 files changed, 229 insertions(+), 97 deletions(-) create mode 100644 src/Results.php create mode 100644 src/SystemTimer.php create mode 100644 src/Timer.php create mode 100644 tests/Unit/OutputTest.php diff --git a/bin/cigar b/bin/cigar index 87ebc8a..83a8ee7 100755 --- a/bin/cigar +++ b/bin/cigar @@ -2,7 +2,6 @@ start(); $inputOptions = new InputOptions([ 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), - 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), + 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, + 'Use the specified config file instead of the default cigar.json file'), 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), 'insecure' => InputOption::create('insecure', 'i', InputOption::VALUE_NONE, 'Allow invalid SSL certificates'), - 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, 'Authorization header " "'), - 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, 'Base URL for checks, e.g. https://example.org/'), - 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, 'Custom header ": ", can be passed multiple times to send multiple headers'), + 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, + 'Authorization header " "'), + 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, + 'Base URL for checks, e.g. https://example.org/'), + 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, + 'Custom header ": ", can be passed multiple times to send multiple headers'), 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_REQUIRED, 'Timeout in seconds'), - 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, 'Connect Timeout in seconds'), + 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, + 'Connect Timeout in seconds'), 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), ]); @@ -44,33 +51,31 @@ $input = new Input($inputOptions, $submittedInput); $output = new Output( $input->getOption('quiet') ? new QuietWriter() - : ($input->getOption('json') ? new JsonWriter() : new EchoWriter()) + : ($input->getOption('json') ? new JsonWriter() : new EchoWriter()), + $timer, ); -$inputOptionsHelpOutput = $output->helpOutputForInputOptions($inputOptions); - if ($input->getOption('help')) { - $content = <<helpOutputForInputOptions($inputOptions)} Created by Matt Brunt E: matt@brunty.me -T: twitter.com/Brunty M: brunty.social/@brunty +T: twitter.com/Brunty G: github.com/brunty/cigar HELP; - echo $content; exit(0); } if ($input->getOption('version')) { $version = CIGAR_VERSION; - $content = <<getOption('insecure'); -$authorization = $input->getOption('auth') ?: null; -$headers = $input->getOption('header') ?: []; -$connectTimeout = $input->getOption('connect-timeout') ?: null; -$timeout = $input->getOption('timeout') ?: null; - try { - $domains = (new ConfigParser($baseUrl, $connectTimeout, $timeout))->parse($configFile); + $configParser = new ConfigParser( + $baseUrl, + $input->getOption('connect-timeout') ?: null, + $input->getOption('timeout') ?: null, + ); + $domains = $configParser->parse($configFile); } catch (Throwable $e) { $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); exit(1); } +$checker = new AsyncChecker( + ! $input->getOption('insecure'), + $input->getOption('auth') ?: null, + $input->getOption('header') ?: [], +); +$results = $checker->check($domains); -$results = (new AsyncChecker($secure, $authorization, $headers))->check($domains); -$passedResults = array_filter($results, function (Result $result) { - return $result->hasPassed(); -}); - -$output->outputResults($passedResults, $results, $start); - -if (count($passedResults) !== count($results)) { - exit(1); -} +$output->outputResults($results, $start); -exit(0); +exit($results->hasPassed() ? 0 : 1); diff --git a/composer.json b/composer.json index 477a8a5..67233c3 100755 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "test:mutation": "vendor/bin/infection", "psalm": "vendor/bin/psalm --config=psalm.xml --show-info=true --no-cache", "phpcs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", + "phpcs:fix": "vendor/bin/phpcbf -p --colors --standard=phpcs.xml", "check": [ "@psalm", "@phpcs" diff --git a/phpcs.xml b/phpcs.xml index 0e29f19..3939cbd 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -197,5 +197,5 @@ bin/cigar src - spec + tests diff --git a/src/AsyncChecker.php b/src/AsyncChecker.php index 1b62f85..08cf1f7 100644 --- a/src/AsyncChecker.php +++ b/src/AsyncChecker.php @@ -15,10 +15,8 @@ public function __construct( /** * @param Url[] $urlsToCheck - * - * @return Result[] */ - public function check(array $urlsToCheck): array + public function check(array $urlsToCheck): Results { $mh = curl_multi_init(); $channels = []; @@ -60,7 +58,7 @@ public function check(array $urlsToCheck): array curl_multi_exec($mh, $running); } while ($running); - $return = []; + $results = []; foreach ($urlsToCheck as $urlToCheck) { $key = $urlToCheck->url; $channel = $channels[$key]; @@ -68,12 +66,12 @@ public function check(array $urlsToCheck): array $content = curl_multi_getcontent($channel); $contentType = curl_getinfo($channel, CURLINFO_CONTENT_TYPE) ?? null; - $return[] = new Result($urlToCheck, $code, $content, (string) $contentType); + $results[] = new Result($urlToCheck, $code, $content, (string) $contentType); curl_multi_remove_handle($mh, $channel); } curl_multi_close($mh); - return $return; + return new Results(...$results); } } diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 8145680..5534f9d 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -17,22 +17,17 @@ public function writeErrorLine(string $message): void echo self::CONSOLE_RED . $message . self::CONSOLE_RESET . PHP_EOL; } - public function writeResults( - int $numberOfPassedResults, - int $numberOfResults, - bool $passed, - float $timeDiff, - Result ...$results - ): void { + public function writeResults(Results $results, float $timeDiff): void + { ob_start(); - foreach ($results as $result) { + foreach ($results->results as $result) { $this->writeLine($result); } ob_end_flush(); - $this->writeStats($numberOfPassedResults, $numberOfResults, $passed, $timeDiff); + $this->writeStats($results, $timeDiff); } private function writeLine(Result $result): void @@ -55,20 +50,20 @@ private function writeLine(Result $result): void ); } - private function writeStats(int $numberOfPassedResults, int $numberOfResults, bool $passed, float $timeDiff): void + private function writeStats(Results $results, float $timeDiff): void { $color = self::CONSOLE_GREEN; $reset = self::CONSOLE_RESET; - if (!$passed) { + if ($results->hasPassed() === false) { $color = self::CONSOLE_RED; } echo sprintf( PHP_EOL . '[%s%s/%s%s] passed in %ss' . PHP_EOL . PHP_EOL, $color, - $numberOfPassedResults, - $numberOfResults, + $results->numberOfPassedResults(), + $results->numberOfTotalResults(), $reset, $timeDiff ); @@ -80,7 +75,7 @@ private function getColourAndStatus(Result $result): array $colour = self::CONSOLE_GREEN; $status = self::SYMBOL_PASSED; - if (!$passed) { + if ($passed === false) { $colour = self::CONSOLE_RED; $status = self::SYMBOL_FAILED; } diff --git a/src/JsonWriter.php b/src/JsonWriter.php index 22a1d10..3e79dcb 100644 --- a/src/JsonWriter.php +++ b/src/JsonWriter.php @@ -15,19 +15,16 @@ public function writeErrorLine(string $message): void } public function writeResults( - int $numberOfPassedResults, - int $numberOfResults, - bool $passed, + Results $results, float $timeDiff, - Result ...$results ): void { echo json_encode([ 'type' => 'results', 'time_taken' => $timeDiff, - 'passed' => $passed, - 'results_count' => $numberOfResults, - 'results_passed_count' => $numberOfPassedResults, - 'results' => array_map([$this, 'line'], $results), + 'passed' => $results->hasPassed(), + 'results_count' => $results->numberOfTotalResults(), + 'results_passed_count' => $results->numberOfPassedResults(), + 'results' => array_map([$this, 'line'], $results->results), ]), PHP_EOL; } diff --git a/src/Output.php b/src/Output.php index b759339..8a3f450 100644 --- a/src/Output.php +++ b/src/Output.php @@ -6,13 +6,10 @@ class Output { - private Writer $writer; - private const VALUE_PLACEHOLDER = 'VALUE'; - public function __construct(Writer $writer = null) + public function __construct(private readonly Writer $writer, private readonly Timer $timer) { - $this->writer = $writer instanceof Writer ? $writer : new EchoWriter(); } public function writeErrorLine(string $message): void @@ -20,15 +17,11 @@ public function writeErrorLine(string $message): void $this->writer->writeErrorLine($message); } - public function outputResults(array $passedResults, array $results, float $startTime): void + public function outputResults(Results $results, float $startedAt): void { - $numberOfResults = count($results); - $numberOfPassedResults = count($passedResults); - $endTime = microtime(true); - $timeDiff = round($endTime - $startTime, 3); - $passed = $numberOfPassedResults === $numberOfResults; + $timeDiff = round($this->timer->stop() - $startedAt, 3); - $this->writer->writeResults($numberOfPassedResults, $numberOfResults, $passed, $timeDiff, ...$results); + $this->writer->writeResults($results, $timeDiff); } public function helpOutputForInputOptions(InputOptions $inputOptions): string @@ -72,7 +65,7 @@ public function helpOutputForInputOptions(InputOptions $inputOptions): string $longCode = str_pad($longCode, $longestLongCodeLength); - $output = sprintf( + $output .= sprintf( ' %s%s %s%s %s' . PHP_EOL, $optionStartSequence, $shortCode, @@ -94,7 +87,7 @@ private function getShortCodeOutput(InputOption $option): string } if ($option->valueIsRequired()) { - $shortCode = "-$option->shortCode VALUE"; + $shortCode = "-$option->shortCode " . self::VALUE_PLACEHOLDER; } return $shortCode; @@ -105,7 +98,7 @@ private function getLongCodeOutput(InputOption $option): string $longCode = "--$option->longCode"; if ($option->valueIsRequired()) { - $longCode = "--$option->longCode=VALUE"; + $longCode = "--$option->longCode=" . self::VALUE_PLACEHOLDER; } return $longCode; diff --git a/src/QuietWriter.php b/src/QuietWriter.php index 0eb08c9..10b8113 100644 --- a/src/QuietWriter.php +++ b/src/QuietWriter.php @@ -10,12 +10,7 @@ public function writeErrorLine(string $message): void { } - public function writeResults( - int $numberOfPassedResults, - int $numberOfResults, - bool $passed, - float $timeDiff, - Result ...$results - ): void { + public function writeResults(Results $results, float $timeDiff): void + { } } diff --git a/src/Results.php b/src/Results.php new file mode 100644 index 0000000..396ec79 --- /dev/null +++ b/src/Results.php @@ -0,0 +1,45 @@ +results = $results; + } + + public function numberOfPassedResults(): int + { + if ($this->passedResults !== null) { + return $this->passedResults; + } + + $this->passedResults = 0; + + foreach ($this->results as $result) { + if ($result->hasPassed()) { + $this->passedResults++; + } + } + + return $this->passedResults; + } + + public function numberOfTotalResults(): int + { + return count($this->results); + } + + public function hasPassed(): bool + { + return $this->numberOfTotalResults() === $this->numberOfPassedResults(); + } +} diff --git a/src/SystemTimer.php b/src/SystemTimer.php new file mode 100644 index 0000000..3f1a8d2 --- /dev/null +++ b/src/SystemTimer.php @@ -0,0 +1,39 @@ +startedAt !== null) { + throw new LogicException('Cannot started a timer that has already been started'); + } + $this->startedAt = microtime(true); + + return $this->startedAt; + } + + public function stop(): float + { + if ($this->startedAt === null) { + throw new LogicException('Cannot end a timer that has not been started'); + } + + if ($this->endedAt !== null) { + throw new LogicException('Cannot end a timer that has already been ended'); + } + + $this->endedAt = microtime(true); + + return $this->endedAt; + } +} diff --git a/src/Timer.php b/src/Timer.php new file mode 100644 index 0000000..e6bd99c --- /dev/null +++ b/src/Timer.php @@ -0,0 +1,12 @@ +writer->writeResults(2, 3, false, 0.5, ...$results); + $this->writer->writeResults($results, 0.5); $output = ob_get_contents(); ob_end_clean(); diff --git a/tests/Unit/JsonWriterTest.php b/tests/Unit/JsonWriterTest.php index 8ddc2a4..72b7b03 100644 --- a/tests/Unit/JsonWriterTest.php +++ b/tests/Unit/JsonWriterTest.php @@ -6,12 +6,14 @@ use Brunty\Cigar\JsonWriter; use Brunty\Cigar\Result; +use Brunty\Cigar\Results; use Brunty\Cigar\Url; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; /** * @covers \Brunty\Cigar\JsonWriter + * @uses \Brunty\Cigar\Results * @uses \Brunty\Cigar\Result * @uses \Brunty\Cigar\Url */ @@ -42,11 +44,11 @@ public function it_writes_an_error_line(): void #[Test] public function it_writes_results(): void { - $results = [ + $results = new Results( new Result(new Url('url', 201, 'c', 't'), 200, 'c', 't'), new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), new Result(new Url('url', 200, 'c', 't'), 200, 'c', 't'), - ]; + ); $expected = <<writer->writeResults(2, 3, false, 0.5, ...$results); + $this->writer->writeResults($results, 0.5); $output = ob_get_contents(); ob_end_clean(); diff --git a/tests/Unit/OutputTest.php b/tests/Unit/OutputTest.php new file mode 100644 index 0000000..67d2057 --- /dev/null +++ b/tests/Unit/OutputTest.php @@ -0,0 +1,59 @@ +errorMessage .= $message; + } + + public function writeResults(Results $results, float $timeDiff): void + { + } + }; + + $output = new Output($writer, new SystemTimer()); + $output->writeErrorLine('message-here'); + + $this->assertSame('message-here', $writer->errorMessage); + } + + #[Test] + public function it_outputs_results(): void + { + $writer = new class implements Writer { + public string $errorMessage = ''; + + public function writeErrorLine(string $message): void + { + $this->errorMessage .= $message; + } + + public function writeResults(Results $results, float $timeDiff): void + { + } + }; + + $output = new Output($writer, new SystemTimer()); + $output->writeErrorLine('message-here'); + + $this->assertSame('message-here', $writer->errorMessage); + } +} From f4f626273d31991f03943fbddede142b0c9f3a0d Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 17:30:25 +0100 Subject: [PATCH 24/38] More testing for output! --- bin/cigar | 2 +- infection.json5 | 15 +++++++++ src/Output.php | 15 ++++----- tests/Unit/OutputTest.php | 69 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/bin/cigar b/bin/cigar index 83a8ee7..b20d376 100755 --- a/bin/cigar +++ b/bin/cigar @@ -61,7 +61,7 @@ if ($input->getOption('help')) { \033[33mUsage:\033[0m cigar [options] -{$output->helpOutputForInputOptions($inputOptions)} +{$output->generateHelpOutputForOptions($inputOptions)} Created by Matt Brunt E: matt@brunty.me diff --git a/infection.json5 b/infection.json5 index 1c2fc72..db51a93 100644 --- a/infection.json5 +++ b/infection.json5 @@ -18,4 +18,19 @@ "github": true, }, "testFramework":"phpunit", + "mutators": { + "@default": true, + "DecrementInteger": { + "ignore": [ + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::38", + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::39", + ] + }, + "GreaterThan": { + "ignore": [ + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::51", + "Brunty\\Cigar\\Output::generateHelpOutputForOptions::55", + ] + } + } } diff --git a/src/Output.php b/src/Output.php index 8a3f450..16483ef 100644 --- a/src/Output.php +++ b/src/Output.php @@ -24,7 +24,7 @@ public function outputResults(Results $results, float $startedAt): void $this->writer->writeResults($results, $timeDiff); } - public function helpOutputForInputOptions(InputOptions $inputOptions): string + public function generateHelpOutputForOptions(InputOptions $inputOptions): string { $optionStartSequence = "\033[32m"; $optionEndSequence = "\033[0m"; @@ -37,11 +37,11 @@ public function helpOutputForInputOptions(InputOptions $inputOptions): string $longestShortCodeLength = 0; $longestLongCodeLength = 0; - $valuePlaceholderLength = mb_strlen(self::VALUE_PLACEHOLDER); + $valuePlaceholderLength = strlen(self::VALUE_PLACEHOLDER); foreach ($inputOptions->options as $option) { $shortCodeLength = 3; // at it's shortest it is "-c " - $longCodeLength = mb_strlen($option->longCode) + 3; // +3 is for double dash before & the space or = after + $longCodeLength = strlen($option->longCode) + 3; // +3 is for double dash before & the space or = after if ($option->valueIsRequired()) { $shortCodeLength += $valuePlaceholderLength; @@ -64,7 +64,6 @@ public function helpOutputForInputOptions(InputOptions $inputOptions): string $shortCode = str_pad($shortCode, $longestShortCodeLength); $longCode = str_pad($longCode, $longestLongCodeLength); - $output .= sprintf( ' %s%s %s%s %s' . PHP_EOL, $optionStartSequence, @@ -80,12 +79,12 @@ public function helpOutputForInputOptions(InputOptions $inputOptions): string private function getShortCodeOutput(InputOption $option): string { - $shortCode = ''; - - if ($option->shortCode !== '') { - $shortCode = "-$option->shortCode"; + if ($option->shortCode === '') { + return ''; } + $shortCode = "-$option->shortCode"; + if ($option->valueIsRequired()) { $shortCode = "-$option->shortCode " . self::VALUE_PLACEHOLDER; } diff --git a/tests/Unit/OutputTest.php b/tests/Unit/OutputTest.php index 67d2057..de13cda 100644 --- a/tests/Unit/OutputTest.php +++ b/tests/Unit/OutputTest.php @@ -4,13 +4,23 @@ namespace Brunty\Cigar\Tests\Unit; +use Brunty\Cigar\InputOption; +use Brunty\Cigar\InputOptions; use Brunty\Cigar\Output; use Brunty\Cigar\Results; use Brunty\Cigar\SystemTimer; +use Brunty\Cigar\Timer; use Brunty\Cigar\Writer; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +/** + * @covers \Brunty\Cigar\Output + * @uses \Brunty\Cigar\SystemTimer + * @uses \Brunty\Cigar\InputOptions + * @uses \Brunty\Cigar\InputOption + * @uses \Brunty\Cigar\Results + */ class OutputTest extends TestCase { #[Test] @@ -39,11 +49,46 @@ public function writeResults(Results $results, float $timeDiff): void public function it_outputs_results(): void { $writer = new class implements Writer { - public string $errorMessage = ''; + public Results $results; + + public float $timeDiff = 0; public function writeErrorLine(string $message): void { - $this->errorMessage .= $message; + } + + public function writeResults(Results $results, float $timeDiff): void + { + $this->results = $results; + $this->timeDiff = $timeDiff; + } + }; + + $timer = new class implements Timer { + public function start(): float + { + return 0.5; + } + + public function stop(): float + { + return 2.63456; + } + }; + + $output = new Output($writer, $timer); + $results = new Results(); + $output->outputResults($results, 0.5); + + $this->assertSame(2.135, $writer->timeDiff); + } + + #[Test] + public function it_generates_help_output(): void + { + $writer = new class implements Writer { + public function writeErrorLine(string $message): void + { } public function writeResults(Results $results, float $timeDiff): void @@ -52,8 +97,24 @@ public function writeResults(Results $results, float $timeDiff): void }; $output = new Output($writer, new SystemTimer()); - $output->writeErrorLine('message-here'); - $this->assertSame('message-here', $writer->errorMessage); + $inputOptions = new InputOptions([ + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), + 'version' => InputOption::create('version', 'v', InputOption::VALUE_NONE, 'Print the version of Cigar'), + 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config'), + 'url' => InputOption::create('url', '', InputOption::VALUE_REQUIRED, 'Something with a URL'), + ]); + $helpText = $output->generateHelpOutputForOptions($inputOptions); + file_put_contents('test', $helpText); + $expectedHelpText = <<assertSame($expectedHelpText, $helpText); } } From 1c8f50ac1d5c3893757b6bc14ee121058188a030 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 18:43:14 +0100 Subject: [PATCH 25/38] More tests --- bin/cigar | 2 +- src/EchoWriter.php | 2 +- src/JsonWriter.php | 2 +- src/Output.php | 2 +- src/Results.php | 4 +- src/SystemTimer.php | 30 +-------------- src/Timer.php | 4 +- tests/Unit/OutputTest.php | 9 +---- tests/Unit/ResultTest.php | 3 ++ tests/Unit/ResultsTest.php | 68 ++++++++++++++++++++++++++++++++++ tests/Unit/SystemTimerTest.php | 23 ++++++++++++ 11 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 tests/Unit/ResultsTest.php create mode 100644 tests/Unit/SystemTimerTest.php diff --git a/bin/cigar b/bin/cigar index b20d376..80d4fdc 100755 --- a/bin/cigar +++ b/bin/cigar @@ -23,7 +23,7 @@ use Brunty\Cigar\QuietWriter; use Brunty\Cigar\SystemTimer; $timer = new SystemTimer(); -$start = $timer->start(); +$start = $timer->now(); $inputOptions = new InputOptions([ 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 5534f9d..2b1251a 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -63,7 +63,7 @@ private function writeStats(Results $results, float $timeDiff): void PHP_EOL . '[%s%s/%s%s] passed in %ss' . PHP_EOL . PHP_EOL, $color, $results->numberOfPassedResults(), - $results->numberOfTotalResults(), + $results->totalNumberOfResults(), $reset, $timeDiff ); diff --git a/src/JsonWriter.php b/src/JsonWriter.php index 3e79dcb..f0e90b5 100644 --- a/src/JsonWriter.php +++ b/src/JsonWriter.php @@ -22,7 +22,7 @@ public function writeResults( 'type' => 'results', 'time_taken' => $timeDiff, 'passed' => $results->hasPassed(), - 'results_count' => $results->numberOfTotalResults(), + 'results_count' => $results->totalNumberOfResults(), 'results_passed_count' => $results->numberOfPassedResults(), 'results' => array_map([$this, 'line'], $results->results), ]), PHP_EOL; diff --git a/src/Output.php b/src/Output.php index 16483ef..fa298f5 100644 --- a/src/Output.php +++ b/src/Output.php @@ -19,7 +19,7 @@ public function writeErrorLine(string $message): void public function outputResults(Results $results, float $startedAt): void { - $timeDiff = round($this->timer->stop() - $startedAt, 3); + $timeDiff = round($this->timer->now() - $startedAt, 3); $this->writer->writeResults($results, $timeDiff); } diff --git a/src/Results.php b/src/Results.php index 396ec79..275d153 100644 --- a/src/Results.php +++ b/src/Results.php @@ -33,13 +33,13 @@ public function numberOfPassedResults(): int return $this->passedResults; } - public function numberOfTotalResults(): int + public function totalNumberOfResults(): int { return count($this->results); } public function hasPassed(): bool { - return $this->numberOfTotalResults() === $this->numberOfPassedResults(); + return $this->totalNumberOfResults() === $this->numberOfPassedResults(); } } diff --git a/src/SystemTimer.php b/src/SystemTimer.php index 3f1a8d2..34881eb 100644 --- a/src/SystemTimer.php +++ b/src/SystemTimer.php @@ -4,36 +4,10 @@ namespace Brunty\Cigar; -use LogicException; - class SystemTimer implements Timer { - private ?float $startedAt = null; - - private ?float $endedAt = null; - - public function start(): float - { - if ($this->startedAt !== null) { - throw new LogicException('Cannot started a timer that has already been started'); - } - $this->startedAt = microtime(true); - - return $this->startedAt; - } - - public function stop(): float + public function now(): float { - if ($this->startedAt === null) { - throw new LogicException('Cannot end a timer that has not been started'); - } - - if ($this->endedAt !== null) { - throw new LogicException('Cannot end a timer that has already been ended'); - } - - $this->endedAt = microtime(true); - - return $this->endedAt; + return microtime(true); } } diff --git a/src/Timer.php b/src/Timer.php index e6bd99c..ca04b01 100644 --- a/src/Timer.php +++ b/src/Timer.php @@ -6,7 +6,5 @@ interface Timer { - public function start(): float; - - public function stop(): float; + public function now(): float; } diff --git a/tests/Unit/OutputTest.php b/tests/Unit/OutputTest.php index de13cda..2dcaa40 100644 --- a/tests/Unit/OutputTest.php +++ b/tests/Unit/OutputTest.php @@ -65,12 +65,7 @@ public function writeResults(Results $results, float $timeDiff): void }; $timer = new class implements Timer { - public function start(): float - { - return 0.5; - } - - public function stop(): float + public function now(): float { return 2.63456; } @@ -105,7 +100,7 @@ public function writeResults(Results $results, float $timeDiff): void 'url' => InputOption::create('url', '', InputOption::VALUE_REQUIRED, 'Something with a URL'), ]); $helpText = $output->generateHelpOutputForOptions($inputOptions); - file_put_contents('test', $helpText); + $expectedHelpText = <<assertSame(1, $results->numberOfPassedResults()); + $this->assertSame(1, $results->numberOfPassedResults()); + } + + #[Test] + public function it_returns_the_total_number_of_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 200), + ); + + $this->assertSame(3, $results->totalNumberOfResults()); + } + + #[Test] + public function it_returns_true_if_the_total_results_equals_passed_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + ); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_returns_false_if_the_total_results_does_not_equal_passed_results(): void + { + $results = new Results( + new Result(new Url('url', 200), 201), + new Result(new Url('url', 200), 200), + new Result(new Url('url', 200), 200), + ); + + $this->assertFalse($results->hasPassed()); + } +} diff --git a/tests/Unit/SystemTimerTest.php b/tests/Unit/SystemTimerTest.php new file mode 100644 index 0000000..5e3d211 --- /dev/null +++ b/tests/Unit/SystemTimerTest.php @@ -0,0 +1,23 @@ +now(); + + $this->assertNotSame($now, $timer->now()); + } +} From 60ff1adda8f362e16f7358b671f032ffa743a39d Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 18:44:05 +0100 Subject: [PATCH 26/38] Fix phpcs --- tests/Unit/SystemTimerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/SystemTimerTest.php b/tests/Unit/SystemTimerTest.php index 5e3d211..59132b3 100644 --- a/tests/Unit/SystemTimerTest.php +++ b/tests/Unit/SystemTimerTest.php @@ -1,5 +1,7 @@ Date: Sun, 24 Sep 2023 18:45:20 +0100 Subject: [PATCH 27/38] Move from kahlan to phpunit in CI tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1c19c66..392bfb7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: run: php -v - name: Install dependencies run: composer install - - name: Run Kahlan - run: XDEBUG_MODE=coverage vendor/bin/kahlan --reporter=verbose --clover=coverage.xml + - name: Run Tests + run: composer test #- name: Coveralls # run: php vendor/bin/php-coveralls From 7537ec80f7e303c2738e9ec25cd2a8ac2f4eeb73 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 18:45:43 +0100 Subject: [PATCH 28/38] Remove kahlan dependency --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 67233c3..c463cd7 100755 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ "require-dev": { "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.6", - "kahlan/kahlan": "^5.2", "symfony/process": "^6.3", "vimeo/psalm": "^5.15", "squizlabs/php_codesniffer": "^3.7", From 41bc17e57ba62ad9d94fceed5d69ea54996698a5 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 18:46:34 +0100 Subject: [PATCH 29/38] Remove kahlan dependency in docs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df6a295..e44119b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, av 1. Fork the repository 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Install dependencies (`composer install`) -4. Cigar uses [Kahlan](https://kahlan.github.io/docs/) as test runner. The integration tests reside within the `spec` directory. So, please make sure all test suite pass (`vendor/bin/kahlan --reporter=verbose`) +4. Cigar uses [PHPUnit](https://phpunit.de/) as test runner. The tests reside within the `tests` directory. So, please make sure all tests pass with `composer test` 5. Commit your changes (`git commit -am 'Add some feature'`) 6. Push to the branch (`git push origin my-new-feature`) 7. Create new Pull Request From 52a968434bb7af8302dea5a7d25df031b65dbfbd Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 19:22:15 +0100 Subject: [PATCH 30/38] Infection config to ignore AsyncChecker --- infection.json5 | 3 +- phpunit.xml | 35 ++++++---- tests/Functional/AsyncCheckerTest.php | 93 +++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 tests/Functional/AsyncCheckerTest.php diff --git a/infection.json5 b/infection.json5 index db51a93..a519264 100644 --- a/infection.json5 +++ b/infection.json5 @@ -4,8 +4,8 @@ "src" ], "excludes": [ + "AsyncChecker.php", "tests", - "spec", ] }, "timeout": 10, @@ -18,6 +18,7 @@ "github": true, }, "testFramework":"phpunit", + "testFrameworkOptions": "--testsuite=unit", "mutators": { "@default": true, "DecrementInteger": { diff --git a/phpunit.xml b/phpunit.xml index 6b1effd..4797bb1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,25 @@ - - - - tests - - - - - - src - - + + + + tests/Functional + + + tests/Unit + + + + + + src + + diff --git a/tests/Functional/AsyncCheckerTest.php b/tests/Functional/AsyncCheckerTest.php new file mode 100644 index 0000000..3e176c0 --- /dev/null +++ b/tests/Functional/AsyncCheckerTest.php @@ -0,0 +1,93 @@ +check($urls); + + $this->assertFalse($results->hasPassed()); + $this->assertEquals(5, $results->totalNumberOfResults()); + $this->assertEquals(3, $results->numberOfPassedResults()); + } + + #[Test] + public function it_checks_urls_with_authorization(): void + { + $urls = [ + new Url('http://httpbin.org/headers', 200, 'some-random-string'), + ]; + + $checker = new AsyncChecker(true, 'some-random-string'); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_headers_set(): void + { + $urls = [ + new Url('http://httpbin.org/headers', 200, 'Header-Foo'), + ]; + + $checker = new AsyncChecker(true, null, ['Header-Foo: Content-Bar']); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_insecure_ssl(): void + { + $urls = [ + new Url('https://cigar-do-not-work.apps.brunty.me', 403, ''), + ]; + + $checker = new AsyncChecker(false); + + $results = $checker->check($urls); + + $this->assertTrue($results->hasPassed()); + } + + #[Test] + public function it_checks_urls_with_insecure_ssl_fail(): void + { + $urls = [ + new Url('https://cigar-do-not-work.apps.brunty.me', 403, ''), + ]; + + $checker = new AsyncChecker(); + + $results = $checker->check($urls); + + $this->assertFalse($results->hasPassed()); + } +} From 6baa12a59161506db9f2ed470e0797655b2c90ff Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sun, 24 Sep 2023 19:23:03 +0100 Subject: [PATCH 31/38] CS fix --- composer.json | 6 +++--- tests/Functional/AsyncCheckerTest.php | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c463cd7..e7b2247 100755 --- a/composer.json +++ b/composer.json @@ -41,11 +41,11 @@ "test": "XDEBUG_MODE=coverage php vendor/bin/phpunit --color=always --coverage-html=./build/coverage", "test:mutation": "vendor/bin/infection", "psalm": "vendor/bin/psalm --config=psalm.xml --show-info=true --no-cache", - "phpcs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", - "phpcs:fix": "vendor/bin/phpcbf -p --colors --standard=phpcs.xml", + "cs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", + "cs:fix": "vendor/bin/phpcbf -p --colors --standard=phpcs.xml", "check": [ "@psalm", - "@phpcs" + "@cs" ] }, "config": { diff --git a/tests/Functional/AsyncCheckerTest.php b/tests/Functional/AsyncCheckerTest.php index 3e176c0..a76cfcb 100644 --- a/tests/Functional/AsyncCheckerTest.php +++ b/tests/Functional/AsyncCheckerTest.php @@ -1,5 +1,7 @@ Date: Sun, 24 Sep 2023 21:49:37 +0100 Subject: [PATCH 32/38] Start adding command line tests --- phpunit.xml | 3 + tests/ConfigExamples/cigar.fail.json | 32 +++++++++ tests/ConfigExamples/cigar.pass.json | 32 +++++++++ tests/Functional/CommandLineTest.php | 67 +++++++++++++++++++ .../AsyncCheckerTest.php | 2 +- 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/ConfigExamples/cigar.fail.json create mode 100644 tests/ConfigExamples/cigar.pass.json create mode 100644 tests/Functional/CommandLineTest.php rename tests/{Functional => Integration}/AsyncCheckerTest.php (98%) diff --git a/phpunit.xml b/phpunit.xml index 4797bb1..cb79cae 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,6 +12,9 @@ tests/Functional + + tests/Integration + tests/Unit diff --git a/tests/ConfigExamples/cigar.fail.json b/tests/ConfigExamples/cigar.fail.json new file mode 100644 index 0000000..52bec9a --- /dev/null +++ b/tests/ConfigExamples/cigar.fail.json @@ -0,0 +1,32 @@ +[ + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot" + }, + { + "url": "http://httpbin.org/status/200", + "status": 200, + "content-type": "text/html" + }, + { + "url": "http://httpbin.org/status/304", + "status": 304 + }, + { + "url": "http://httpbin.org/status/500", + "status": 501 + }, + { + "url": "http://httpbin.org/delay/3", + "status": 200, + "request-timeout": 1, + "response-timeout": 1 + }, + { + "url": "http://httpbin.org/drip?duration=3", + "status": 200, + "request-timeout": 1, + "response-timeout": 1 + } +] diff --git a/tests/ConfigExamples/cigar.pass.json b/tests/ConfigExamples/cigar.pass.json new file mode 100644 index 0000000..cd68ffa --- /dev/null +++ b/tests/ConfigExamples/cigar.pass.json @@ -0,0 +1,32 @@ +[ + { + "url": "http://httpbin.org/status/418", + "status": 418, + "content": "teapot" + }, + { + "url": "http://httpbin.org/status/200", + "status": 200, + "content-type": "text/html" + }, + { + "url": "http://httpbin.org/status/304", + "status": 304 + }, + { + "url": "http://httpbin.org/status/500", + "status": 500 + }, + { + "url": "http://httpbin.org/delay/3", + "status": 200, + "request-timeout": 1, + "response-timeout": 5 + }, + { + "url": "http://httpbin.org/drip?duration=3", + "status": 200, + "request-timeout": 1, + "response-timeout": 5 + } +] diff --git a/tests/Functional/CommandLineTest.php b/tests/Functional/CommandLineTest.php new file mode 100644 index 0000000..e47091d --- /dev/null +++ b/tests/Functional/CommandLineTest.php @@ -0,0 +1,67 @@ +run(); + } + + #[Test] + public function it_displays_help_output(): void + { + $process = Process::fromShellCommandline('./bin/cigar --help'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringContainsString('--connect-timeout=VALUE', $process->getOutput()); + } + + #[Test] + public function it_displays_version_output(): void + { + $process = Process::fromShellCommandline('./bin/cigar --version'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringContainsString('Version', $process->getOutput()); + } + + #[Test] + public function it_passes_if_given_good_urls_to_check(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.pass.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_is_quiet_if_the_option_is_passed(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.pass.json cigar.json && ../../bin/cigar --quiet'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + $this->assertSame('', $process->getOutput()); + } + + #[Test] + public function it_fails_if_given_bad_urls_to_check(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.fail.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } +} diff --git a/tests/Functional/AsyncCheckerTest.php b/tests/Integration/AsyncCheckerTest.php similarity index 98% rename from tests/Functional/AsyncCheckerTest.php rename to tests/Integration/AsyncCheckerTest.php index a76cfcb..3b13945 100644 --- a/tests/Functional/AsyncCheckerTest.php +++ b/tests/Integration/AsyncCheckerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Brunty\Cigar\Tests\Functional; +namespace Brunty\Cigar\Tests\Integration; use Brunty\Cigar\AsyncChecker; use Brunty\Cigar\Url; From 9207a7f181b0143d783e41f46069cbafd987e9fd Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Wed, 27 Sep 2023 18:39:40 +0100 Subject: [PATCH 33/38] Command line tool tests --- .gitignore | 2 + bin/cigar | 1 + .../{cigar.fail.json => fail.json} | 0 tests/ConfigExamples/headers.json | 12 +++ tests/ConfigExamples/insecure.json | 6 ++ .../{cigar.pass.json => pass.json} | 0 tests/ConfigExamples/timeouts.json | 14 +++ tests/ConfigExamples/without-base.json | 16 +++ tests/Functional/CommandLineTest.php | 99 ++++++++++++++++++- 9 files changed, 146 insertions(+), 4 deletions(-) rename tests/ConfigExamples/{cigar.fail.json => fail.json} (100%) create mode 100644 tests/ConfigExamples/headers.json create mode 100644 tests/ConfigExamples/insecure.json rename tests/ConfigExamples/{cigar.pass.json => pass.json} (100%) create mode 100644 tests/ConfigExamples/timeouts.json create mode 100644 tests/ConfigExamples/without-base.json diff --git a/.gitignore b/.gitignore index 3cfe82e..8c0f8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ coverage.xml clover.xml cache/ build/ +tests/ConfigExamples/cigar.json +tests/ConfigExamples/alt.json diff --git a/bin/cigar b/bin/cigar index 80d4fdc..7d5b41a 100755 --- a/bin/cigar +++ b/bin/cigar @@ -112,6 +112,7 @@ try { $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); exit(1); } + $checker = new AsyncChecker( ! $input->getOption('insecure'), $input->getOption('auth') ?: null, diff --git a/tests/ConfigExamples/cigar.fail.json b/tests/ConfigExamples/fail.json similarity index 100% rename from tests/ConfigExamples/cigar.fail.json rename to tests/ConfigExamples/fail.json diff --git a/tests/ConfigExamples/headers.json b/tests/ConfigExamples/headers.json new file mode 100644 index 0000000..1b80588 --- /dev/null +++ b/tests/ConfigExamples/headers.json @@ -0,0 +1,12 @@ +[ + { + "url": "http://httpbin.org/headers", + "status": 200, + "content": "some-header-content-here" + }, + { + "url": "http://httpbin.org/headers", + "status": 200, + "content": "more-header-content-here" + } +] diff --git a/tests/ConfigExamples/insecure.json b/tests/ConfigExamples/insecure.json new file mode 100644 index 0000000..eff0bea --- /dev/null +++ b/tests/ConfigExamples/insecure.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://cigar-do-not-work.apps.brunty.me", + "status": 403 + } +] diff --git a/tests/ConfigExamples/cigar.pass.json b/tests/ConfigExamples/pass.json similarity index 100% rename from tests/ConfigExamples/cigar.pass.json rename to tests/ConfigExamples/pass.json diff --git a/tests/ConfigExamples/timeouts.json b/tests/ConfigExamples/timeouts.json new file mode 100644 index 0000000..c6a54a5 --- /dev/null +++ b/tests/ConfigExamples/timeouts.json @@ -0,0 +1,14 @@ +[ + { + "url": "http://httpbin.org/delay/3", + "status": 200, + "request-timeout": 1, + "response-timeout": 5 + }, + { + "url": "http://httpbin.org/drip?duration=3", + "status": 200, + "request-timeout": 1, + "response-timeout": 5 + } +] diff --git a/tests/ConfigExamples/without-base.json b/tests/ConfigExamples/without-base.json new file mode 100644 index 0000000..6a0aad9 --- /dev/null +++ b/tests/ConfigExamples/without-base.json @@ -0,0 +1,16 @@ +[ + { + "url": "/status/418", + "status": 418, + "content": "teapot" + }, + { + "url": "status/200", + "status": 200, + "content-type": "text/html" + }, + { + "url": "http://httpbin.org/status/304", + "status": 304 + } +] diff --git a/tests/Functional/CommandLineTest.php b/tests/Functional/CommandLineTest.php index e47091d..67dbb21 100644 --- a/tests/Functional/CommandLineTest.php +++ b/tests/Functional/CommandLineTest.php @@ -13,7 +13,7 @@ class CommandLineTest extends TestCase public function tearDown(): void { // clear out cigar config after each test - $process = Process::fromShellCommandline('cd tests/ConfigExamples && rm cigar.json'); + $process = Process::fromShellCommandline('cd tests/ConfigExamples && rm cigar.json alt.json'); $process->run(); } @@ -40,7 +40,7 @@ public function it_displays_version_output(): void #[Test] public function it_passes_if_given_good_urls_to_check(): void { - $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.pass.json cigar.json && ../../bin/cigar'); + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json cigar.json && ../../bin/cigar'); $process->run(); $this->assertSame(0, $process->getExitCode()); @@ -49,7 +49,7 @@ public function it_passes_if_given_good_urls_to_check(): void #[Test] public function it_is_quiet_if_the_option_is_passed(): void { - $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.pass.json cigar.json && ../../bin/cigar --quiet'); + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json cigar.json && ../../bin/cigar --quiet'); $process->run(); $this->assertSame(0, $process->getExitCode()); @@ -59,7 +59,98 @@ public function it_is_quiet_if_the_option_is_passed(): void #[Test] public function it_fails_if_given_bad_urls_to_check(): void { - $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp cigar.fail.json cigar.json && ../../bin/cigar'); + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp fail.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } + + #[Test] + public function it_can_be_given_an_alternative_config_file_by_a_short_command_line_flag(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json alt.json && ../../bin/cigar -f alt.json'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_given_an_alternative_config_file_by_a_long_command_line_flag(): void + { + + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp pass.json alt.json && ../../bin/cigar --config alt.json'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_fails_with_insecure_certs(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar'); + $process->run(); + + $this->assertSame(1, $process->getExitCode()); + } + + #[Test] + public function it_passes_with_insecure_certs_if_the_long_flag_is_given(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar --insecure'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_passes_with_insecure_certs_if_the_short_flag_is_given(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp insecure.json cigar.json && ../../bin/cigar -i'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_a_base_url_as_a_short_command_argument(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp without-base.json cigar.json && ../../bin/cigar -u http://httpbin.org'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_a_base_url_as_a_long_command_argument(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp without-base.json cigar.json && ../../bin/cigar --url http://httpbin.org'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_headers_as_long_options(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp headers.json cigar.json && ../../bin/cigar --header="X-Cigar-Header: some-header-content-here" --header="X-Cigar-Header-2: more-header-content-here"'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_be_passed_headers_as_short_options(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp headers.json cigar.json && ../../bin/cigar -h "X-Cigar-Header: some-header-content-here" -h "X-Cigar-Header-2: more-header-content-here"'); + $process->run(); + + $this->assertSame(0, $process->getExitCode()); + } + + #[Test] + public function it_can_bt_passed_timeout_arguments_and_overwrites_the_configured_value_in_the_config_file(): void + { + $process = Process::fromShellCommandline('cd tests/ConfigExamples && cp timeouts.json cigar.json && ../../bin/cigar -t 1'); $process->run(); $this->assertSame(1, $process->getExitCode()); From 45b5e02ffb37fefd34ce4d6843e900e82d9b78c8 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Wed, 27 Sep 2023 18:48:00 +0100 Subject: [PATCH 34/38] Exit code refactoring to enum to stop magic numbers --- bin/cigar | 20 +++++++++----------- src/ExitCode.php | 11 +++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 src/ExitCode.php diff --git a/bin/cigar b/bin/cigar index 7d5b41a..c4716a1 100755 --- a/bin/cigar +++ b/bin/cigar @@ -13,6 +13,7 @@ foreach (['/../../..', '/../..', '/../vendor', '/vendor'] as $autoloadFileDirect use Brunty\Cigar\AsyncChecker; use Brunty\Cigar\EchoWriter; +use Brunty\Cigar\ExitCode; use Brunty\Cigar\Input; use Brunty\Cigar\InputOption; use Brunty\Cigar\InputOptions; @@ -44,9 +45,7 @@ $inputOptions = new InputOptions([ 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), ]); -$submittedInput = getopt($inputOptions->shortCodes, $inputOptions->longCodes); - -$input = new Input($inputOptions, $submittedInput); +$input = new Input($inputOptions, getopt($inputOptions->shortCodes, $inputOptions->longCodes)); $output = new Output( $input->getOption('quiet') @@ -70,7 +69,7 @@ T: twitter.com/Brunty G: github.com/brunty/cigar HELP; - exit(0); + exit(ExitCode::SUCCESS->value); } if ($input->getOption('version')) { @@ -90,7 +89,7 @@ For additional help use \033[36m--help\033[0m VERSION; - exit(0); + exit(ExitCode::SUCCESS->value); } $configFile = $input->getOption('config') ?: 'cigar.json'; @@ -98,7 +97,7 @@ $baseUrl = $input->getOption('url') ?: null; if ( ! file_exists($configFile)) { $output->writeErrorLine('Could not find configuration file: ' . $configFile); - exit(1); + exit(ExitCode::FAILURE->value); } try { @@ -110,16 +109,15 @@ try { $domains = $configParser->parse($configFile); } catch (Throwable $e) { $output->writeErrorLine(sprintf('Unable to parse Cigar JSON file: %s', $e->getMessage())); - exit(1); + exit(ExitCode::FAILURE->value); } -$checker = new AsyncChecker( +$results = (new AsyncChecker( ! $input->getOption('insecure'), $input->getOption('auth') ?: null, $input->getOption('header') ?: [], -); -$results = $checker->check($domains); +))->check($domains); $output->outputResults($results, $start); -exit($results->hasPassed() ? 0 : 1); +exit($results->hasPassed() ? ExitCode::SUCCESS->value : ExitCode::FAILURE->value); diff --git a/src/ExitCode.php b/src/ExitCode.php new file mode 100644 index 0000000..4f9064d --- /dev/null +++ b/src/ExitCode.php @@ -0,0 +1,11 @@ + Date: Sat, 30 Sep 2023 12:33:11 +0100 Subject: [PATCH 35/38] Refactor console escape codes to new enum --- bin/cigar | 39 +++++++++++++----------- src/ConsoleColours.php | 57 +++++++++++++++++++++++++++++++++++ src/EchoWriter.php | 17 +++++------ src/Output.php | 10 +++--- tests/Unit/EchoWriterTest.php | 17 ++++++----- tests/Unit/OutputTest.php | 11 ++++--- 6 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 src/ConsoleColours.php diff --git a/bin/cigar b/bin/cigar index c4716a1..df725a9 100755 --- a/bin/cigar +++ b/bin/cigar @@ -12,6 +12,7 @@ foreach (['/../../..', '/../..', '/../vendor', '/vendor'] as $autoloadFileDirect } use Brunty\Cigar\AsyncChecker; +use Brunty\Cigar\ConsoleColours; use Brunty\Cigar\EchoWriter; use Brunty\Cigar\ExitCode; use Brunty\Cigar\Input; @@ -27,22 +28,21 @@ $timer = new SystemTimer(); $start = $timer->now(); $inputOptions = new InputOptions([ - 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), - 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), - 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, - 'Use the specified config file instead of the default cigar.json file'), - 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), + 'config' => InputOption::create('config', 'f', InputOption::VALUE_REQUIRED, 'Use the specified config file instead of the default cigar.json file'), + + 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, 'Authorization header " "'), + 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, 'Custom header ": ", can be passed multiple times to send multiple headers'), 'insecure' => InputOption::create('insecure', 'i', InputOption::VALUE_NONE, 'Allow invalid SSL certificates'), - 'auth' => InputOption::create('auth', 'a', InputOption::VALUE_REQUIRED, - 'Authorization header " "'), - 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, - 'Base URL for checks, e.g. https://example.org/'), - 'header' => InputOption::create('header', 'h', InputOption::VALUE_REQUIRED, - 'Custom header ": ", can be passed multiple times to send multiple headers'), + 'url' => InputOption::create('url', 'u', InputOption::VALUE_REQUIRED, 'Base URL for checks, e.g. https://example.org/'), + + 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, 'Connect Timeout in seconds'), 'timeout' => InputOption::create('timeout', 't', InputOption::VALUE_REQUIRED, 'Timeout in seconds'), - 'connect-timeout' => InputOption::create('connect-timeout', 'c', InputOption::VALUE_REQUIRED, - 'Connect Timeout in seconds'), + + 'quiet' => InputOption::create('quiet', 'q', InputOption::VALUE_NONE, 'Do not output any message'), 'json' => InputOption::create('json', 'j', InputOption::VALUE_NONE, 'Output JSON'), + + 'help' => InputOption::create('help', '', InputOption::VALUE_NONE, 'Show the help message'), + 'version' => InputOption::create('version', '', InputOption::VALUE_NONE, 'Print the version of Cigar'), ]); $input = new Input($inputOptions, getopt($inputOptions->shortCodes, $inputOptions->longCodes)); @@ -54,10 +54,15 @@ $output = new Output( $timer, ); +$consoleCyan = ConsoleColours::cyan(); +$consoleYellow = ConsoleColours::yellow(); +$consoleGrey = ConsoleColours::grey(); +$consoleReset = ConsoleColours::reset(); + if ($input->getOption('help')) { echo <<generateHelpOutputForOptions($inputOptions)} @@ -81,11 +86,11 @@ if ($input->getOption('version')) { | |___ | | |_| |/ ___ \| _ < \____|___\____/_/ \_\_| \_\ -\033[0;90;49mThe simple smoke testing tool.\033[0m +{$consoleGrey}The simple smoke testing tool.{$consoleReset} -Version \033[36m$version\033[0m +Version {$consoleCyan}$version{$consoleReset} -For additional help use \033[36m--help\033[0m +For additional help use {$consoleCyan}--help{$consoleReset} VERSION; diff --git a/src/ConsoleColours.php b/src/ConsoleColours.php new file mode 100644 index 0000000..b9941ee --- /dev/null +++ b/src/ConsoleColours.php @@ -0,0 +1,57 @@ +value . 'm'; + } +} diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 2b1251a..574f335 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -6,15 +6,12 @@ class EchoWriter implements Writer { - private const CONSOLE_GREEN = "\033[32m"; - private const CONSOLE_RED = "\033[31m"; - private const CONSOLE_RESET = "\033[0m"; private const SYMBOL_PASSED = '✓'; private const SYMBOL_FAILED = '✘'; public function writeErrorLine(string $message): void { - echo self::CONSOLE_RED . $message . self::CONSOLE_RESET . PHP_EOL; + echo ConsoleColours::red() . $message . ConsoleColours::reset() . PHP_EOL; } public function writeResults(Results $results, float $timeDiff): void @@ -39,7 +36,7 @@ private function writeLine(Result $result): void } echo sprintf( - '%s%s %s [%s:%s]%s %s' . self::CONSOLE_RESET . PHP_EOL, + '%s%s %s [%s:%s]%s %s' . ConsoleColours::reset() . PHP_EOL, $colour, $status, $result->url->url, @@ -52,11 +49,11 @@ private function writeLine(Result $result): void private function writeStats(Results $results, float $timeDiff): void { - $color = self::CONSOLE_GREEN; - $reset = self::CONSOLE_RESET; + $color = ConsoleColours::green(); + $reset = ConsoleColours::reset(); if ($results->hasPassed() === false) { - $color = self::CONSOLE_RED; + $color = ConsoleColours::red(); } echo sprintf( @@ -72,11 +69,11 @@ private function writeStats(Results $results, float $timeDiff): void private function getColourAndStatus(Result $result): array { $passed = $result->hasPassed(); - $colour = self::CONSOLE_GREEN; + $colour = ConsoleColours::green(); $status = self::SYMBOL_PASSED; if ($passed === false) { - $colour = self::CONSOLE_RED; + $colour = ConsoleColours::red(); $status = self::SYMBOL_FAILED; } diff --git a/src/Output.php b/src/Output.php index fa298f5..b07ba58 100644 --- a/src/Output.php +++ b/src/Output.php @@ -26,13 +26,13 @@ public function outputResults(Results $results, float $startedAt): void public function generateHelpOutputForOptions(InputOptions $inputOptions): string { - $optionStartSequence = "\033[32m"; - $optionEndSequence = "\033[0m"; + $consoleGreen = ConsoleColours::green(); + $consoleReset = ConsoleColours::reset(); $output = ''; if ($inputOptions->options !== []) { - $output = "\033[33mOptions:\033[0m" . PHP_EOL; + $output = ConsoleColours::yellow() . 'Options:' . $consoleReset . PHP_EOL; } $longestShortCodeLength = 0; @@ -66,10 +66,10 @@ public function generateHelpOutputForOptions(InputOptions $inputOptions): string $output .= sprintf( ' %s%s %s%s %s' . PHP_EOL, - $optionStartSequence, + $consoleGreen, $shortCode, $longCode, - $optionEndSequence, + $consoleReset, $option->description ); } diff --git a/tests/Unit/EchoWriterTest.php b/tests/Unit/EchoWriterTest.php index b2d60c5..cb3e200 100644 --- a/tests/Unit/EchoWriterTest.php +++ b/tests/Unit/EchoWriterTest.php @@ -13,9 +13,10 @@ /** * @covers \Brunty\Cigar\EchoWriter - * @uses \Brunty\Cigar\Results - * @uses \Brunty\Cigar\Result - * @uses \Brunty\Cigar\Url + * @uses \Brunty\Cigar\ConsoleColours + * @uses \Brunty\Cigar\Results + * @uses \Brunty\Cigar\Result + * @uses \Brunty\Cigar\Url */ class EchoWriterTest extends TestCase { @@ -30,7 +31,7 @@ protected function setUp(): void public function it_writes_an_error_line(): void { $expected = <<generateHelpOutputForOptions($inputOptions); $expectedHelpText = << Date: Sat, 30 Sep 2023 12:36:01 +0100 Subject: [PATCH 36/38] Add some colour to socials output --- bin/cigar | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/cigar b/bin/cigar index df725a9..0af2b5f 100755 --- a/bin/cigar +++ b/bin/cigar @@ -68,10 +68,10 @@ if ($input->getOption('help')) { {$output->generateHelpOutputForOptions($inputOptions)} Created by Matt Brunt -E: matt@brunty.me -M: brunty.social/@brunty -T: twitter.com/Brunty -G: github.com/brunty/cigar +{$consoleCyan}E:{$consoleReset} matt@brunty.me +{$consoleCyan}M:{$consoleReset} brunty.social/@brunty +{$consoleCyan}T:{$consoleReset} twitter.com/Brunty +{$consoleCyan}G:{$consoleReset} github.com/brunty/cigar HELP; exit(ExitCode::SUCCESS->value); From 77bfcfd42d730754d6e1d977f2d544164a15ad83 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Sat, 30 Sep 2023 20:23:20 +0100 Subject: [PATCH 37/38] Update docker build docs for 2.0.0 --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 2729845..3d6b80e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,7 +2,7 @@ Replace the versions as appropriate ``` -docker build . -t brunty/cigar:1.12.4 -f ./docker/1.12/Dockerfile +docker build . -t brunty/cigar:2.0.0 -f ./docker/2.0/Dockerfile docker push brunty/cigar --all-tags ``` From 79841a0a6a72828d672df802cd02216e435cefa5 Mon Sep 17 00:00:00 2001 From: Matt Brunt Date: Tue, 3 Oct 2023 15:25:23 +0100 Subject: [PATCH 38/38] Improve psalm strictness and mutation coverage --- bin/cigar | 12 +++++------ composer.json | 1 + infection.json5 | 15 +++++++++++++ psalm.xml | 3 +-- src/AsyncChecker.php | 4 ++-- src/ConfigParser.php | 20 ++++++++++-------- src/EchoWriter.php | 3 +++ tests/Unit/ConfigParserTest.php | 18 +++++++++------- tests/Unit/ConsoleColoursTest.php | 35 +++++++++++++++++++++++++++++++ 9 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 tests/Unit/ConsoleColoursTest.php diff --git a/bin/cigar b/bin/cigar index 0af2b5f..3d977a0 100755 --- a/bin/cigar +++ b/bin/cigar @@ -97,8 +97,8 @@ VERSION; exit(ExitCode::SUCCESS->value); } -$configFile = $input->getOption('config') ?: 'cigar.json'; -$baseUrl = $input->getOption('url') ?: null; +$configFile = (string) $input->getOption('config') ?: 'cigar.json'; +$baseUrl = (string) $input->getOption('url') ?: null; if ( ! file_exists($configFile)) { $output->writeErrorLine('Could not find configuration file: ' . $configFile); @@ -108,8 +108,8 @@ if ( ! file_exists($configFile)) { try { $configParser = new ConfigParser( $baseUrl, - $input->getOption('connect-timeout') ?: null, - $input->getOption('timeout') ?: null, + (int) $input->getOption('connect-timeout') ?: null, + (int) $input->getOption('timeout') ?: null, ); $domains = $configParser->parse($configFile); } catch (Throwable $e) { @@ -119,8 +119,8 @@ try { $results = (new AsyncChecker( ! $input->getOption('insecure'), - $input->getOption('auth') ?: null, - $input->getOption('header') ?: [], + (string) $input->getOption('auth') ?: null, + (array) $input->getOption('header') ?: [], ))->check($domains); $output->outputResults($results, $start); diff --git a/composer.json b/composer.json index e7b2247..d6f77e6 100755 --- a/composer.json +++ b/composer.json @@ -39,6 +39,7 @@ ], "scripts": { "test": "XDEBUG_MODE=coverage php vendor/bin/phpunit --color=always --coverage-html=./build/coverage", + "test:unit": "@test --testsuite=unit", "test:mutation": "vendor/bin/infection", "psalm": "vendor/bin/psalm --config=psalm.xml --show-info=true --no-cache", "cs": "vendor/bin/phpcs -p --colors --standard=phpcs.xml", diff --git a/infection.json5 b/infection.json5 index a519264..ab6d189 100644 --- a/infection.json5 +++ b/infection.json5 @@ -21,12 +21,27 @@ "testFrameworkOptions": "--testsuite=unit", "mutators": { "@default": true, + "CastArray": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::31", + ], + }, + "CastString": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::37", + ], + }, "DecrementInteger": { "ignore": [ "Brunty\\Cigar\\Output::generateHelpOutputForOptions::38", "Brunty\\Cigar\\Output::generateHelpOutputForOptions::39", ] }, + "IncrementInteger": { + "ignore": [ + "Brunty\\Cigar\\ConfigParser::parse::31" + ] + }, "GreaterThan": { "ignore": [ "Brunty\\Cigar\\Output::generateHelpOutputForOptions::51", diff --git a/psalm.xml b/psalm.xml index 586de52..59ecbc6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,7 +1,6 @@ getUrl($value['url']); + $url = $this->getUrl((string) $value['url']); return new Url( $url, - $value['status'], - $value['content'] ?? '', - $value['content-type'] ?? '', - $value['connect-timeout'] ?? $this->connectTimeout, - $value['timeout'] ?? $this->timeout + (int) $value['status'], + array_key_exists('content', $value) ? (string) $value['content'] : '', + array_key_exists('content-type', $value) ? (string) $value['content-type'] : '', + array_key_exists('connect-timeout', $value) ? (int) $value['connect-timeout'] : $this->connectTimeout, + array_key_exists('timeout', $value) ? (int) $value['timeout'] : $this->timeout, ); }, $urls); } diff --git a/src/EchoWriter.php b/src/EchoWriter.php index 574f335..73cb8ee 100644 --- a/src/EchoWriter.php +++ b/src/EchoWriter.php @@ -66,6 +66,9 @@ private function writeStats(Results $results, float $timeDiff): void ); } + /** + * @return string[] + */ private function getColourAndStatus(Result $result): array { $passed = $result->hasPassed(); diff --git a/tests/Unit/ConfigParserTest.php b/tests/Unit/ConfigParserTest.php index 7705268..e39de4a 100644 --- a/tests/Unit/ConfigParserTest.php +++ b/tests/Unit/ConfigParserTest.php @@ -18,14 +18,10 @@ class ConfigParserTest extends TestCase { #[Test] - public function it_parses_a_correctly_formatted_config_file(): void + public function it_parses_a_correctly_formatted_config_file_and_casts_values(): void { $structure = [ 'cigar.json' => '[ - { - "url": "http://httpbin.org/status/418", - "status": 418 - }, { "url": "http://httpbin.org/status/200", "status": 200 @@ -37,6 +33,14 @@ public function it_parses_a_correctly_formatted_config_file(): void "content-type": "kitchen/teapot", "connect-timeout": 1, "timeout": 2 + }, + { + "url": "http://httpbin.org/status/200", + "status": 200, + "content": 1, + "content-type": 2, + "connect-timeout": "3", + "timeout": "4" } ] ', @@ -46,9 +50,9 @@ public function it_parses_a_correctly_formatted_config_file(): void $results = (new ConfigParser())->parse('vfs://root/cigar.json'); $expected = [ - new Url('http://httpbin.org/status/418', 418), new Url('http://httpbin.org/status/200', 200), new Url('http://httpbin.org/status/418', 418, 'teapot', 'kitchen/teapot', 1, 2), + new Url('http://httpbin.org/status/200', 200, '1', '2', 3, 4), ]; $this->assertEquals($expected, $results); @@ -65,7 +69,7 @@ public function it_parses_a_file_that_contains_both_absolute_and_relative_urls() }, { "url": "status/200", - "status": 200 + "status": "200" }, { "url": "http://httpbin.org/status/418", diff --git a/tests/Unit/ConsoleColoursTest.php b/tests/Unit/ConsoleColoursTest.php new file mode 100644 index 0000000..9396c42 --- /dev/null +++ b/tests/Unit/ConsoleColoursTest.php @@ -0,0 +1,35 @@ +assertSame("\e[{$code}m", ConsoleColours::$method()); + } +}