diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 4f7b8d5b..570f316a 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -48,23 +48,23 @@ jobs: continue-on-error: true - name: Report - Text - run: OUTPUT=text make demo-github --no-print-directory + run: REPORT=text make demo-github --no-print-directory continue-on-error: true - name: Report - Github Actions - run: OUTPUT=github make demo-github --no-print-directory + run: REPORT=github make demo-github --no-print-directory continue-on-error: true - name: Report - GitLab - run: OUTPUT=gitlab make demo-github --no-print-directory + run: REPORT=gitlab make demo-github --no-print-directory continue-on-error: true - name: Report - TeamCity CI - run: OUTPUT=teamcity make demo-github --no-print-directory + run: REPORT=teamcity make demo-github --no-print-directory continue-on-error: true - name: Report - JUnit - run: OUTPUT=junit make demo-github --no-print-directory + run: REPORT=junit make demo-github --no-print-directory continue-on-error: true @@ -95,7 +95,7 @@ jobs: with: csv: tests/fixtures/demo.csv schema: tests/schemas/demo_invalid.yml - output: table + report: table continue-on-error: true diff --git a/.phan.php b/.phan.php index 8780395e..4c22cc0a 100644 --- a/.phan.php +++ b/.phan.php @@ -30,5 +30,6 @@ 'vendor/league/csv/src', 'vendor/fakerphp/faker/src', 'vendor/symfony/console', + 'vendor/symfony/finder', ], ]); diff --git a/Makefile b/Makefile index 880e4b8a..b41f5a70 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ ifneq (, $(wildcard ./vendor/jbzoo/codestyle/src/init.Makefile)) include ./vendor/jbzoo/codestyle/src/init.Makefile endif -OUTPUT ?= table +REPORT ?= table build: ##@Project Install all 3rd party dependencies $(call title,"Install/Update all 3rd party dependencies") @@ -77,14 +77,14 @@ demo-invalid: ##@Project Run demo invalid CSV @${PHP_BIN} ./csv-blueprint validate:csv \ --csv=./tests/fixtures/demo.csv \ --schema=./tests/schemas/demo_invalid.yml \ - --output=$(OUTPUT) + --report=$(REPORT) demo-github: ##@Project Run demo invalid CSV @${PHP_BIN} ./csv-blueprint validate:csv \ - --csv=./tests/fixtures/demo.csv \ + --csv=./tests/fixtures/batch/*.csv \ --schema=./tests/schemas/demo_invalid.yml \ - --output=$(OUTPUT) \ + --report=$(REPORT) \ --ansi diff --git a/README.md b/README.md index e9cea785..664fe6b5 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ * [As Docker container](#as-docker-container) * [As PHP binary](#as-php-binary) * [As PHP project](#as-php-project) + * [CLI Help Message](#cli-help-message) + * [Report examples](#report-examples) * [Schema Definition](#schema-definition) * [Schema file examples](#schema-file-examples) * [Coming soon](#coming-soon) * [Disadvantages?](#disadvantages) -* [Interesting fact](#interesting-fact) -* [Unit tests and check code style](#unit-tests-and-check-code-style) +* [Contributing](#contributing) * [License](#license) * [See Also](#see-also) @@ -80,9 +81,9 @@ Also see demo in the [GitHub Actions](https://github.com/JBZoo/Csv-Blueprint/act with: csv: tests/fixtures/demo.csv schema: tests/schemas/demo_invalid.yml - output: table # Optional. Default is "github" + report: table # Optional. Default is "github" ``` -**Note**. Output format for GitHub Actions is `github` by default. [GitHub Actions friendly](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message). +**Note**. Report format for GitHub Actions is `github` by default. [GitHub Actions friendly](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message). This allows you to see bugs in the GitHub interface at the PR level. That is, the error will be shown in a specific place in the CSV file right in diff of your Pull Requests! @@ -145,8 +146,7 @@ make build ### CLI Help Message -Here you can see all available options and commands. -Tool uses [JBZoo/Cli](https://github.com/JBZoo/Cli) package for the CLI interface. +Here you can see all available options and commands. Tool uses [JBZoo/Cli](https://github.com/JBZoo/Cli) package for the CLI interface. So there are options here for all occasions. @@ -155,15 +155,20 @@ So there are options here for all occasions. Description: - Validate CSV file by schema. + Validate CSV file(s) by schema. Usage: validate:csv [options] Options: - -c, --csv=CSV CSV filepath to validate. - -s, --schema=SCHEMA Schema rule filepath. It can be a .yml/.json/.php file. - -o, --output=OUTPUT Report output format. Available options: text, table, github, gitlab, teamcity, junit [default: "table"] + -c, --csv=CSV Path(s) to validate. + You can specify path in which CSV files will be searched (max depth is 10). + Feel free to use glob pattrens. Usage examples: + /full/path/file.csv, p/file.csv, p/*.csv, p/**/*.csv, p/**/name-*.csv, **/*.csv, etc. (multiple values allowed) + -s, --schema=SCHEMA Schema filepath. + It can be a YAML, JSON or PHP. See examples on GitHub. + -r, --report=REPORT Report output format. Available options: + text, table, github, gitlab, teamcity, junit [default: "table"] --no-progress Disable progress bar animation for logs. It will be used only for text output format. --mute-errors Mute any sort of errors. So exit code will be always "0" (if it's possible). It has major priority then --non-zero-on-error. It's on your own risk! @@ -188,52 +193,58 @@ Options: ``` -### Output Example +### Report examples As a result of the validation process, you will receive a human-readable table with a list of errors found in the CSV file. By defualt, the output format is a table, but you can choose from a variety of formats, such as text, GitHub, GitLab, TeamCity, JUnit, and more. For example, the following output is generated using the "table" format. **Notes** -* Output format for GitHub Actions is `github` by default. +* Report format for GitHub Actions is `github` by default. * Tools uses [JBZoo/CI-Report-Converter](https://github.com/JBZoo/CI-Report-Converter) as SDK to convert reports to different formats. So you can easily integrate it with any CI system. -Default output format is `table`: +Default report format is `table`: ``` -./csv-blueprint validate:csv --output=table - - -CSV : ./tests/fixtures/demo.csv -Schema : ./tests/schemas/demo_invalid.yml - -+------+------------------+--------------+-- demo.csv --------------------------------------------+ -| Line | id:Column | Rule | Message | -+------+------------------+--------------+--------------------------------------------------------+ -| 1 | 1: | csv.header | Property "name" is not defined in schema: | -| | | | "./tests/schemas/demo_invalid.yml" | -| 5 | 2:Float | max | Value "74605.944" is greater than "74605" | -| 5 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | -| | | | "green", "Blue"] | -| 6 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | -| 6 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | -| | | | "1955-05-15T00:00:00.000+00:00" | -| 8 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | -| | | | "1955-05-15T00:00:00.000+00:00" | -| 9 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | -| | | | "2009-01-01T00:00:00.000+00:00" | -| 11 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | -+------+------------------+--------------+-- demo.csv --------------------------------------------+ - -CSV file is not valid! Found 8 errors. +./csv-blueprint validate:csv --csv='./tests/fixtures/batch/*.csv' --schema='./tests/schemas/demo_invalid.yml' + + +Schema: ./tests/schemas/demo_invalid.yml + +Invalid file: ./tests/fixtures/batch/demo-1.csv ++------+------------------+--------------+ demo-1.csv ------------------------------------------+ +| Line | id:Column | Rule | Message | ++------+------------------+--------------+------------------------------------------------------+ +| 3 | 2:Float | max | Value "74605.944" is greater than "74605" | +| 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | +| | | | "green", "Blue"] | ++------+------------------+--------------+ demo-1.csv ------------------------------------------+ + +Invalid file: ./tests/fixtures/batch/demo-2.csv ++------+------------+------------+----- demo-2.csv ---------------------------------------+ +| Line | id:Column | Rule | Message | ++------+------------+------------+--------------------------------------------------------+ +| 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | +| 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | +| | | | "1955-05-15T00:00:00.000+00:00" | +| 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | +| | | | "1955-05-15T00:00:00.000+00:00" | +| 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | +| | | | "2009-01-01T00:00:00.000+00:00" | +| 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | ++------+------------+------------+----- demo-2.csv ---------------------------------------+ + +OK: ./tests/fixtures/batch/sub/demo-3.csv +Found 7 issues in 2 out of 3 CSV files. ``` -Optional output format `text`: + +Optional format `text` with highlited keywords: ```sh -./csv-blueprint validate:csv --output=text +./csv-blueprint validate:csv --report=text ``` -![Output - Text](.github/assets/output-text.png) +![Report - Text](.github/assets/output-text.png) ### Schema Definition @@ -452,24 +463,27 @@ return [ It's random ideas and plans. No orderings and deadlines. But batch processing is the priority #1. +* [x] CSV/Schema file discovery in the folder with regex filename pattern (like `glob(./**/dir/*.csv)`). +* [x] If option `--csv` is a folder, then validate all files in the folder. +* [x] Checking multiple CSV files in one schema. Batch processing. * [ ] Filename pattern validation with regex (like "all files in the folder should be in the format `/^[\d]{4}-[\d]{2}-[\d]{2}\.csv$/`"). -* [ ] CSV/Schema file discovery in the folder with regex filename pattern (like `glob(./**/dir/*.csv)`). +* [ ] Quick stop mode. If the first error is found, then stop the validation process to save time. +* [ ] S3 Storage support. Validate files in the S3 bucket? * [ ] Build phar file and release via GitHub Actions. -* [ ] If option `--csv` is a folder, then validate all files in the folder. * [ ] If option `--csv` is not specified, then the STDIN is used. To build a pipeline in Unix-like systems. * [ ] If option `--schema` is not specified, then validate only super base level things (like "is it a CSV file?"). * [ ] Agregate rules (like "at least one of the fields should be not empty" or "all values must be unique"). * [ ] Create CSV files based on the schema (like "create 1000 rows with random data based on schema and rules"). -* [ ] Checking multiple CSV files in one schema. Batch processing. * [ ] Using multiple schemas for one csv file. Batch processing. * [ ] Parallel validation of really-really large files (1GB+ ?). I know you have them and not so much memory. * [ ] Parallel validation of multiple files at once. * [ ] Benchmarks as part of the CI process and Readme. It's important to know how much time the validation process takes. * [ ] Inheritance of schemas, rules and columns. Define parent schema and override some rules in the child schemas. Make it DRY and easy to maintain. -* [ ] More output formats (like JSON, XML, etc). Any ideas? +* [ ] More report formats (like JSON, XML, etc). Any ideas? * [ ] Complex rules (like "if field `A` is not empty, then field `B` should be not empty too"). * [ ] Input encoding detection + `BOM` (right now it's experimental). It works but not so accurate... UTF-8/16/32 is the best choice for now. -* [ ] Extending with custom rules and custom output formats. Plugins? +* [ ] Gitlab and JUnit reports mus be as one structure. It's not so easy to implement. But it's a good idea. +* [ ] Extending with custom rules and custom report formats. Plugins? * [ ] Optimazation on `php.ini` level to start it faster. JIT. * [ ] More examples and documentation. diff --git a/action.yml b/action.yml index 1bd76e5c..ca2f392b 100644 --- a/action.yml +++ b/action.yml @@ -20,12 +20,16 @@ branding: inputs: csv: - description: 'CSV filepath to validate.' + description: > + Path(s) to validate. You can specify path in which CSV files will be searched + (max depth is 10). + Feel free to use glob pattrens. Usage examples: + /full/path/file.csv, p/file.csv, p/*.csv, p/**/*.csv, p/**/name-*.csv, **/*.csv, etc. required: true schema: - description: 'Schema rule filepath. File can be a Yml or JSON. See examples in the repository.' + description: 'Schema filepath. It can be a YAML, JSON or PHP. See examples on GitHub.' required: true - output: + report: description: 'Report output format. Available options: text, table, github, gitlab, teamcity, junit' default: github required: true @@ -39,7 +43,7 @@ runs: - ${{ inputs.csv }} - '--schema' - ${{ inputs.schema }} - - '--output' - - ${{ inputs.output }} + - '--report' + - ${{ inputs.report }} - '--ansi' - '-vvv' diff --git a/composer.json b/composer.json index abb98fdc..a5820752 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,9 @@ "jbzoo/utils" : "^7.1", "jbzoo/ci-report-converter" : "^7.2", "league/csv" : "^9.15", - "symfony/yaml" : "^6.4.3" + "symfony/yaml" : "^6.4.3", + "symfony/filesystem" : "^6.4", + "symfony/finder" : "^6.4" }, "require-dev" : { diff --git a/composer.lock b/composer.lock index 8ccb8d2d..ff94acda 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2041fe2d7b451ac595ae0abf49f0eeac", + "content-hash": "8f84bb595c9bcc066f1d026f986eab7f", "packages": [ { "name": "bluepsyduck/symfony-process-manager", @@ -977,6 +977,133 @@ ], "time": "2023-05-23T14:45:45+00:00" }, + { + "name": "symfony/filesystem", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T17:30:12+00:00" + }, { "name": "symfony/lock", "version": "v6.4.3", @@ -5228,7 +5355,7 @@ "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", "ezsystems/ezplatform-user": ">=1,<1.0.1", "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", - "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.06,<=2019.03.5.1", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", "ezyang/htmlpurifier": "<4.1.1", @@ -7290,133 +7417,6 @@ ], "time": "2023-05-23T14:45:45+00:00" }, - { - "name": "symfony/filesystem", - "version": "v6.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T14:51:35+00:00" - }, - { - "name": "symfony/finder", - "version": "v6.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "symfony/filesystem": "^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-10-31T17:30:12+00:00" - }, { "name": "symfony/options-resolver", "version": "v6.4.0", diff --git a/src/Commands/ValidateCsv.php b/src/Commands/ValidateCsv.php index 32291220..3a359db0 100644 --- a/src/Commands/ValidateCsv.php +++ b/src/Commands/ValidateCsv.php @@ -20,8 +20,10 @@ use JBZoo\Cli\OutLvl; use JBZoo\CsvBlueprint\Csv\CsvFile; use JBZoo\CsvBlueprint\Exception; +use JBZoo\CsvBlueprint\Utils; use JBZoo\CsvBlueprint\Validators\ErrorSuite; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Finder\SplFileInfo; /** * @psalm-suppress PropertyNotSetInConstructor @@ -32,25 +34,38 @@ protected function configure(): void { $this ->setName('validate:csv') - ->setDescription('Validate CSV file by schema.') + ->setDescription('Validate CSV file(s) by schema.') ->addOption( 'csv', 'c', - InputOption::VALUE_REQUIRED, - 'CSV filepath to validate.', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + \implode('', [ + "Path(s) to validate.\n" . + 'You can specify path in which CSV files will be searched ', + '(max depth is ' . Utils::MAX_DIRECTORY_DEPTH . ").\n", + "Feel free to use glob pattrens. Usage examples: \n", + '/full/path/file.csv, ', + 'p/file.csv, ', + 'p/*.csv, ', + 'p/**/*.csv, ', + 'p/**/name-*.csv, ', + '**/*.csv, ', + 'etc.', + ]), ) ->addOption( 'schema', 's', InputOption::VALUE_REQUIRED, - 'Schema rule filepath. It can be a .yml/.json/.php file.', + "Schema filepath.\n" . + 'It can be a YAML, JSON or PHP. See examples on GitHub.', ) ->addOption( - 'output', - 'o', + 'report', + 'r', InputOption::VALUE_REQUIRED, - 'Report output format. Available options: ' . - \implode(', ', ErrorSuite::getAvaiableRenderFormats()) . '', + "Report output format. Available options:\n" . + '' . \implode(', ', ErrorSuite::getAvaiableRenderFormats()) . '', ErrorSuite::RENDER_TABLE, ); @@ -59,25 +74,43 @@ protected function configure(): void protected function executeAction(): int { - $csvFilename = $this->getCsvFilepath(); + $csvFilenames = $this->getCsvFilepaths(); $schemaFilename = $this->getSchemaFilepath(); + $this->_(''); + + $errorCounter = 0; + $invalidFiles = 0; + $totalFiles = \count($csvFilenames); + + foreach ($csvFilenames as $csvFilename) { + $csvFile = new CsvFile($csvFilename->getPathname(), $schemaFilename); + $errorSuite = $csvFile->validate(); + + if ($errorSuite->count() > 0) { + $invalidFiles++; + $errorCounter += $errorSuite->count(); + + if ($this->isTextMode()) { + $this->_('Invalid file: ' . Utils::cutPath($csvFilename->getPathname()), OutLvl::E); + } + $output = $errorSuite->render($this->getOptString('report')); + if ($output !== null) { + $this->_($output, $this->isTextMode() ? OutLvl::E : OutLvl::DEFAULT); + } + } elseif ($this->isTextMode()) { + $this->_('OK: ' . Utils::cutPath($csvFilename->getPathname())); + } + } - $csvFile = new CsvFile($csvFilename, $schemaFilename); - $errorSuite = $csvFile->validate(); - if ($errorSuite->count() > 0) { - $this->_( - $errorSuite->render($this->getOptString('output')), - $this->isTextMode() ? OutLvl::E : OutLvl::DEFAULT, - ); - - if ($this->isTextMode()) { - $this->_( - 'CSV file is not valid! ' . - 'Found ' . $errorSuite->count() . ' errors.', - OutLvl::E, - ); + if ($errorCounter > 0 && $this->isTextMode()) { + if ($totalFiles === 1) { + $errMessage = "Found {$errorCounter} issues in CSV file."; + } else { + $errMessage = "Found {$errorCounter} issues in {$invalidFiles} out of {$totalFiles} CSV files."; } + $this->_($errMessage, OutLvl::E); + return self::FAILURE; } @@ -88,19 +121,19 @@ protected function executeAction(): int return self::SUCCESS; } - private function getCsvFilepath(): string + /** + * @return SplFileInfo[] + */ + private function getCsvFilepaths(): array { - $csvFilename = $this->getOptString('csv'); - - if (\file_exists($csvFilename) === false) { - throw new Exception("CSV file not found: {$csvFilename}"); - } + $rawInput = $this->getOptArray('csv'); + $scvFilenames = Utils::findFiles($rawInput); - if ($this->isTextMode()) { - $this->_('CSV : ' . \realpath($csvFilename)); + if (\count($scvFilenames) === 0) { + throw new Exception('CSV file(s) not found in path(s): ' . \implode("\n, ", $rawInput)); } - return $csvFilename; + return $scvFilenames; } private function getSchemaFilepath(): string @@ -112,8 +145,7 @@ private function getSchemaFilepath(): string } if ($this->isTextMode()) { - $this->_('Schema : ' . \realpath($schemaFilename)); - $this->_(''); + $this->_('Schema: ' . Utils::cutPath($schemaFilename)); } return $schemaFilename; @@ -121,14 +153,14 @@ private function getSchemaFilepath(): string private function isTextMode(): bool { - return $this->getOutputMode() === ErrorSuite::RENDER_TEXT - || $this->getOutputMode() === ErrorSuite::RENDER_GITHUB - || $this->getOutputMode() === ErrorSuite::RENDER_TEAMCITY - || $this->getOutputMode() === ErrorSuite::RENDER_TABLE; + return $this->getReportType() === ErrorSuite::REPORT_TEXT + || $this->getReportType() === ErrorSuite::REPORT_GITHUB + || $this->getReportType() === ErrorSuite::REPORT_TEAMCITY + || $this->getReportType() === ErrorSuite::RENDER_TABLE; } - private function getOutputMode(): string + private function getReportType(): string { - return $this->getOptString('output', ErrorSuite::RENDER_TABLE, ErrorSuite::getAvaiableRenderFormats()); + return $this->getOptString('report', ErrorSuite::RENDER_TABLE, ErrorSuite::getAvaiableRenderFormats()); } } diff --git a/src/Utils.php b/src/Utils.php index dd26fed0..b18b8a70 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -16,8 +16,13 @@ namespace JBZoo\CsvBlueprint; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; + final class Utils { + public const MAX_DIRECTORY_DEPTH = 10; + public static function kebabToCamelCase(string $input): string { return \str_replace(' ', '', \ucwords(\str_replace(['-', '_'], ' ', $input))); @@ -44,4 +49,52 @@ public static function prepareRegex(?string $pattern, string $addDelimiter = '/' return $addDelimiter . $pattern . $addDelimiter . 'u'; } + + /** + * Find files from given paths. + * @param string[] $paths + * @return SplFileInfo[] + */ + public static function findFiles(array $paths): array + { + $fileList = []; + + foreach ($paths as $path) { + $path = \trim($path); + if ($path === '') { + continue; + } + + if (\strpos($path, '*') !== false) { + $finder = (new Finder()) + ->in(\dirname($path)) + ->depth('< ' . self::MAX_DIRECTORY_DEPTH) + ->ignoreVCSIgnored(true) + ->ignoreDotFiles(true) + ->followLinks() + ->name(\basename($path)); + + foreach ($finder as $file) { + if (!$file->isReadable()) { + throw new \RuntimeException("File is not readable: {$file->getPathname()}"); + } + + $fileList[$file->getPathname()] = $file; + } + } elseif (\file_exists($path)) { + $fileList[$path] = new SplFileInfo($path, '', $path); + } else { + throw new \RuntimeException("File not found: {$path}"); + } + } + + \ksort($fileList, \SORT_NATURAL); + + return $fileList; + } + + public static function cutPath(string $fullpath): string + { + return \str_replace((string)\getcwd(), '.', $fullpath); + } } diff --git a/src/Validators/ErrorSuite.php b/src/Validators/ErrorSuite.php index f9d128b7..b04184d6 100644 --- a/src/Validators/ErrorSuite.php +++ b/src/Validators/ErrorSuite.php @@ -26,39 +26,45 @@ final class ErrorSuite { - public const RENDER_TEXT = 'text'; + public const REPORT_TEXT = 'text'; public const RENDER_TABLE = 'table'; - public const RENDER_TEAMCITY = 'teamcity'; - public const RENDER_GITLAB = 'gitlab'; - public const RENDER_GITHUB = 'github'; - public const RENDER_JUNIT = 'junit'; + public const REPORT_TEAMCITY = 'teamcity'; + public const REPORT_GITLAB = 'gitlab'; + public const REPORT_GITHUB = 'github'; + public const REPORT_JUNIT = 'junit'; /** @var Error[] */ private array $errors = []; - public function __construct(private ?string $csvFilename = null) + private ?string $csvFilename; + + public function __construct(?string $csvFilename = null) { + $this->csvFilename = $csvFilename; } public function __toString(): string { - return $this->render(self::RENDER_TEXT); + return (string)$this->render(self::REPORT_TEXT); } - public function render(string $mode = self::RENDER_TEXT): string + public function render(string $mode = self::REPORT_TEXT): ?string { if ($this->count() === 0) { - return ''; + return null; } - $sourceSuite = $this->prepareSourceSuite(); - $map = [ - self::RENDER_TEXT => fn (): string => $this->renderPlainText(), + $suite = $this->prepareSourceSuite(); + $map = [ + self::REPORT_TEXT => fn (): string => $this->renderPlainText(), self::RENDER_TABLE => fn (): string => $this->renderTable(), - self::RENDER_GITHUB => static fn (): string => (new GithubCliConverter())->fromInternal($sourceSuite), - self::RENDER_GITLAB => static fn (): string => (new GitLabJsonConverter())->fromInternal($sourceSuite), - self::RENDER_TEAMCITY => static fn (): string => (new TeamCityTestsConverter())->fromInternal($sourceSuite), - self::RENDER_JUNIT => static fn (): string => (new JUnitConverter())->fromInternal($sourceSuite), + self::REPORT_GITHUB => static fn (): string => (new GithubCliConverter())->fromInternal($suite), + self::REPORT_GITLAB => static fn (): string => (new GitLabJsonConverter())->fromInternal($suite), + self::REPORT_JUNIT => static fn (): string => (new JUnitConverter())->fromInternal($suite), + self::REPORT_TEAMCITY => static fn (): string => (new TeamCityTestsConverter( + ['show-datetime' => false], + 42, + ))->fromInternal($suite), ]; if (isset($map[$mode])) { @@ -111,12 +117,12 @@ public function get(int $index): ?Error public static function getAvaiableRenderFormats(): array { return [ - self::RENDER_TEXT, + self::REPORT_TEXT, self::RENDER_TABLE, - self::RENDER_GITHUB, - self::RENDER_GITLAB, - self::RENDER_TEAMCITY, - self::RENDER_JUNIT, + self::REPORT_GITHUB, + self::REPORT_GITLAB, + self::REPORT_TEAMCITY, + self::REPORT_JUNIT, ]; } diff --git a/tests/Blueprint/CommandsTest.php b/tests/Blueprint/CommandsTest.php index 15020dd7..848c5f93 100644 --- a/tests/Blueprint/CommandsTest.php +++ b/tests/Blueprint/CommandsTest.php @@ -25,66 +25,19 @@ use Symfony\Component\Console\Output\BufferedOutput; use function JBZoo\PHPUnit\isFileContains; +use function JBZoo\PHPUnit\isNotEmpty; use function JBZoo\PHPUnit\isSame; final class CommandsTest extends PHPUnit { public function testCreateCsvHelp(): void { - $rootPath = PROJECT_ROOT; - - $actual = $this->realExecution('validate:csv', ['help' => null]); - - $expected = \implode("\n", [ - 'Description:', - ' Validate CSV file by schema.', - '', - 'Usage:', - ' validate:csv [options]', - '', - 'Options:', - ' -c, --csv=CSV CSV filepath to validate.', - ' -s, --schema=SCHEMA Schema rule filepath. It can be a .yml/.json/.php file.', - ' -o, --output=OUTPUT Report output format. Available options: text, table, github, ' . - 'gitlab, teamcity, junit [default: "table"]', - ' --no-progress Disable progress bar animation for logs. It will be used only ' . - 'for text output format.', - ' --mute-errors Mute any sort of errors. So exit code will be always "0" ' . - '(if it\'s possible).', - ' It has major priority then --non-zero-on-error. It\'s on your own risk!', - ' --stdout-only For any errors messages application will use StdOut instead of StdErr. ' . - 'It\'s on your own risk!', - ' --non-zero-on-error None-zero exit code on any StdErr message.', - ' --timestamp Show timestamp at the beginning of each message.It will be used only ' . - 'for text output format.', - ' --profile Display timing and memory usage information.', - ' --output-mode=OUTPUT-MODE Output format. Available options:', - ' text - Default text output format, userfriendly and easy to read.', - ' cron - Shortcut for crontab. It\'s basically focused on human-readable ' . - 'logs output.', - ' It\'s combination of --timestamp --profile --stdout-only --no-progress ' . - '-vv.', - ' logstash - Logstash output format, for integration with ELK stack.', - ' [default: "text"]', - ' --cron Alias for --output-mode=cron. Deprecated!', - ' -h, --help Display help for the given command. When no command is given display ' . - 'help for the list command', - ' -q, --quiet Do not output any message', - ' -V, --version Display this application version', - ' --ansi|--no-ansi Force (or disable --no-ansi) ANSI output', - ' -n, --no-interaction Do not ask any interactive question', - ' -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more ' . - 'verbose output and 3 for debug', - '', - ]); - - isSame($expected, $actual); isFileContains(\implode("\n", [ '```', './csv-blueprint validate:csv --help', '', '', - $expected, + $this->realExecution('validate:csv', ['help' => null]), '```', ]), PROJECT_ROOT . '/README.md'); } @@ -98,13 +51,13 @@ public function testCreateValidatePositive(): void 'schema' => "{$rootPath}/tests/schemas/demo_valid.yml", ]); - $expected = \implode("\n", [ - "CSV : {$rootPath}/tests/fixtures/demo.csv", - "Schema : {$rootPath}/tests/schemas/demo_valid.yml", - '', - 'Looks good!', - '', - ]); + $expected = $expected = <<<'TXT' + Schema: ./tests/schemas/demo_valid.yml + + OK: ./tests/fixtures/demo.csv + Looks good! + + TXT; isSame(0, $exitCode); isSame($expected, $actual); @@ -115,45 +68,91 @@ public function testCreateValidateNegative(): void $rootPath = PROJECT_ROOT; [$actual, $exitCode] = $this->virtualExecution('validate:csv', [ - 'csv' => "{$rootPath}/tests/fixtures/demo.csv", - 'schema' => './tests/schemas/demo_invalid.yml', + 'csv' => "{$rootPath}/tests/fixtures/demo.csv", // Full path + 'schema' => './tests/schemas/demo_invalid.yml', // Relative path ]); - $expected = \implode("\n", [ - "CSV : {$rootPath}/tests/fixtures/demo.csv", - "Schema : {$rootPath}/tests/schemas/demo_invalid.yml", - '', - '+------+------------------+--------------+-- demo.csv --------------------------------------------+', - '| Line | id:Column | Rule | Message |', - '+------+------------------+--------------+--------------------------------------------------------+', - '| 1 | 1: | csv.header | Property "name" is not defined in schema: |', - '| | | | "./tests/schemas/demo_invalid.yml" |', - '| 5 | 2:Float | max | Value "74605.944" is greater than "74605" |', - '| 5 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", |', - '| | | | "green", "Blue"] |', - '| 6 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 |', - '| 6 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date |', - '| | | | "1955-05-15T00:00:00.000+00:00" |', - '| 8 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date |', - '| | | | "1955-05-15T00:00:00.000+00:00" |', - '| 9 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date |', - '| | | | "2009-01-01T00:00:00.000+00:00" |', - '| 11 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 |', - '+------+------------------+--------------+-- demo.csv --------------------------------------------+', - '', - 'CSV file is not valid! Found 8 errors.', - '', - ]); + $this->dumpText($actual); + + $expected = <<<'TXT' + Schema: ./tests/schemas/demo_invalid.yml + + Invalid file: ./tests/fixtures/demo.csv + +------+------------------+--------------+-- demo.csv --------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------------+--------------+--------------------------------------------------------+ + | 5 | 2:Float | max | Value "74605.944" is greater than "74605" | + | 5 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | + | | | | "green", "Blue"] | + | 6 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | + | 6 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | + | | | | "1955-05-15T00:00:00.000+00:00" | + | 8 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | + | | | | "1955-05-15T00:00:00.000+00:00" | + | 9 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | + | | | | "2009-01-01T00:00:00.000+00:00" | + | 11 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | + +------+------------------+--------------+-- demo.csv --------------------------------------------+ + + Found 7 issues in CSV file. + + TXT; + + isSame(1, $exitCode, $actual); + isSame($expected, $actual); + } - isSame(1, $exitCode); + public function testCreateValidateNegativeMultiple(): void + { + $options = [ + 'csv' => './tests/fixtures/batch/*.csv', + 'schema' => './tests/schemas/demo_invalid.yml', + ]; + $optionsAsString = new StringInput(Cli::build('', $options)); + [$actual, $exitCode] = $this->virtualExecution('validate:csv', $options); + + $this->dumpText($actual); + + $expected = <<<'TXT' + Schema: ./tests/schemas/demo_invalid.yml + + Invalid file: ./tests/fixtures/batch/demo-1.csv + +------+------------------+--------------+ demo-1.csv ------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------------+--------------+------------------------------------------------------+ + | 3 | 2:Float | max | Value "74605.944" is greater than "74605" | + | 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | + | | | | "green", "Blue"] | + +------+------------------+--------------+ demo-1.csv ------------------------------------------+ + + Invalid file: ./tests/fixtures/batch/demo-2.csv + +------+------------+------------+----- demo-2.csv ---------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------+------------+--------------------------------------------------------+ + | 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | + | 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | + | | | | "1955-05-15T00:00:00.000+00:00" | + | 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | + | | | | "1955-05-15T00:00:00.000+00:00" | + | 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | + | | | | "2009-01-01T00:00:00.000+00:00" | + | 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | + +------+------------+------------+----- demo-2.csv ---------------------------------------+ + + OK: ./tests/fixtures/batch/sub/demo-3.csv + Found 7 issues in 2 out of 3 CSV files. + + TXT; + + isSame(1, $exitCode, $actual); isSame($expected, $actual); isFileContains(\implode("\n", [ '```', - './csv-blueprint validate:csv --output=table', + "./csv-blueprint validate:csv {$optionsAsString}", '', '', - \str_replace($rootPath, '.', $expected), + $expected, '```', ]), PROJECT_ROOT . '/README.md'); } @@ -163,44 +162,126 @@ public function testCreateValidateNegativeText(): void $rootPath = PROJECT_ROOT; [$actual, $exitCode] = $this->virtualExecution('validate:csv', [ - 'csv' => "{$rootPath}/tests/fixtures/demo.csv", - 'schema' => "{$rootPath}/tests/schemas/demo_invalid.yml", - 'output' => 'text', + 'csv' => './tests/fixtures/demo.csv', + 'schema' => './tests/schemas/demo_invalid.yml', + 'report' => 'text', ]); - $expected = \implode("\n", [ - "CSV : {$rootPath}/tests/fixtures/demo.csv", - "Schema : {$rootPath}/tests/schemas/demo_invalid.yml", - '', - '"csv.header" at line 1, column "1:". Property "name" is not defined in schema: ' . - "\"{$rootPath}/tests/schemas/demo_invalid.yml\".", - - '"max" at line 5, column "2:Float". Value "74605.944" is greater than "74605".', - - '"allow_values" at line 5, column "4:Favorite color". Value "blue" is not allowed. ' . - 'Allowed values: ["red", "green", "Blue"].', - - '"min_length" at line 6, column "0:Name". Value "Carl" (length: 4) is too short. ' . - 'Min length is 5.', + $this->dumpText($actual); + + $expected = <<<'TXT' + Schema: ./tests/schemas/demo_invalid.yml + + Invalid file: ./tests/fixtures/demo.csv + "max" at line 5, column "2:Float". Value "74605.944" is greater than "74605". + "allow_values" at line 5, column "4:Favorite color". Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"]. + "min_length" at line 6, column "0:Name". Value "Carl" (length: 4) is too short. Min length is 5. + "min_date" at line 6, column "3:Birthday". Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00". + "min_date" at line 8, column "3:Birthday". Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00". + "max_date" at line 9, column "3:Birthday". Value "2010-07-20" is more than the maximum date "2009-01-01T00:00:00.000+00:00". + "min_length" at line 11, column "0:Name". Value "Lois" (length: 4) is too short. Min length is 5. + + Found 7 issues in CSV file. + + TXT; + + isSame(1, $exitCode, $actual); + isSame($expected, $actual); + } - '"min_date" at line 6, column "3:Birthday". Value "1955-05-14" is less than the ' . - 'minimum date "1955-05-15T00:00:00.000+00:00".', + public function testCreateValidateNegativeTeamcity(): void + { + $rootPath = PROJECT_ROOT; - '"min_date" at line 8, column "3:Birthday". Value "1955-05-14" is less than the ' . - 'minimum date "1955-05-15T00:00:00.000+00:00".', + [$actual, $exitCode] = $this->virtualExecution('validate:csv', [ + 'csv' => './tests/fixtures/batch/*.csv', + 'schema' => './tests/schemas/demo_invalid.yml', + 'report' => 'teamcity', + ]); - '"max_date" at line 9, column "3:Birthday". Value "2010-07-20" is more than the ' . - 'maximum date "2009-01-01T00:00:00.000+00:00".', + $this->dumpText($actual); + + $expected = <<<'TXT' + Schema: ./tests/schemas/demo_invalid.yml + + Invalid file: ./tests/fixtures/batch/demo-1.csv + + ##teamcity[testCount count='2' flowId='42'] + + ##teamcity[testSuiteStarted name='demo-1.csv' flowId='42'] + + ##teamcity[testStarted name='max at column 2:Float' locationHint='php_qn://./tests/fixtures/batch/demo-1.csv' flowId='42'] + "max" at line 3, column "2:Float". Value "74605.944" is greater than "74605". + ##teamcity[testFinished name='max at column 2:Float' flowId='42'] + + ##teamcity[testStarted name='allow_values at column 4:Favorite color' locationHint='php_qn://./tests/fixtures/batch/demo-1.csv' flowId='42'] + "allow_values" at line 3, column "4:Favorite color". Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"]. + ##teamcity[testFinished name='allow_values at column 4:Favorite color' flowId='42'] + + ##teamcity[testSuiteFinished name='demo-1.csv' flowId='42'] + + Invalid file: ./tests/fixtures/batch/demo-2.csv + + ##teamcity[testCount count='5' flowId='42'] + + ##teamcity[testSuiteStarted name='demo-2.csv' flowId='42'] + + ##teamcity[testStarted name='min_length at column 0:Name' locationHint='php_qn://./tests/fixtures/batch/demo-2.csv' flowId='42'] + "min_length" at line 2, column "0:Name". Value "Carl" (length: 4) is too short. Min length is 5. + ##teamcity[testFinished name='min_length at column 0:Name' flowId='42'] + + ##teamcity[testStarted name='min_date at column 3:Birthday' locationHint='php_qn://./tests/fixtures/batch/demo-2.csv' flowId='42'] + "min_date" at line 2, column "3:Birthday". Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00". + ##teamcity[testFinished name='min_date at column 3:Birthday' flowId='42'] + + ##teamcity[testStarted name='min_date at column 3:Birthday' locationHint='php_qn://./tests/fixtures/batch/demo-2.csv' flowId='42'] + "min_date" at line 4, column "3:Birthday". Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00". + ##teamcity[testFinished name='min_date at column 3:Birthday' flowId='42'] + + ##teamcity[testStarted name='max_date at column 3:Birthday' locationHint='php_qn://./tests/fixtures/batch/demo-2.csv' flowId='42'] + "max_date" at line 5, column "3:Birthday". Value "2010-07-20" is more than the maximum date "2009-01-01T00:00:00.000+00:00". + ##teamcity[testFinished name='max_date at column 3:Birthday' flowId='42'] + + ##teamcity[testStarted name='min_length at column 0:Name' locationHint='php_qn://./tests/fixtures/batch/demo-2.csv' flowId='42'] + "min_length" at line 7, column "0:Name". Value "Lois" (length: 4) is too short. Min length is 5. + ##teamcity[testFinished name='min_length at column 0:Name' flowId='42'] + + ##teamcity[testSuiteFinished name='demo-2.csv' flowId='42'] + + OK: ./tests/fixtures/batch/sub/demo-3.csv + Found 7 issues in 2 out of 3 CSV files. + + TXT; + + isSame(1, $exitCode, $actual); + isSame($expected, $actual); + } - '"min_length" at line 11, column "0:Name". Value "Lois" (length: 4) is too short. ' . - 'Min length is 5.', + public function testMultipleCsvOptions(): void + { + [$expected, $expectedCode] = $this->virtualExecution('validate:csv', [ + 'csv' => './tests/fixtures/batch/*.csv', + 'schema' => './tests/schemas/demo_invalid.yml', + ]); + $actual = $this->realExecution( + 'validate:csv ' . \implode(' ', [ + '--csv="./tests/fixtures/batch/sub/demo-3.csv"', + '--csv="./tests/fixtures/batch/demo-1.csv"', + '--csv="./tests/fixtures/batch/demo-2.csv"', + '--csv="./tests/fixtures/batch/*.csv"', + '--schema="./tests/schemas/demo_invalid.yml"', + '--mute-errors', + '--stdout-only', + '--no-ansi', + ]), + [], '', - 'CSV file is not valid! Found 8 errors.', - '', - ]); + ); - isSame(1, $exitCode); + isNotEmpty($expected); + isNotEmpty($actual); + isSame($expectedCode, 1); isSame($expected, $actual); } @@ -219,14 +300,14 @@ private function virtualExecution(string $action, array $params = []): array return [$buffer->fetch(), $exitCode]; } - private function realExecution(string $action, array $params = []): string + private function realExecution(string $action, array $params = [], string $extra = '--no-ansi'): string { $rootDir = PROJECT_ROOT; return Cli::exec( \implode(' ', [ Sys::getBinary(), - "{$rootDir}/csv-blueprint.php --no-ansi", + "{$rootDir}/csv-blueprint.php {$extra}", $action, '2>&1', ]), @@ -235,4 +316,9 @@ private function realExecution(string $action, array $params = []): string false, ); } + + private function dumpText($text): void + { + \file_put_contents(PROJECT_ROOT . '/build/dump.txt', $text); + } } diff --git a/tests/Blueprint/MiscTest.php b/tests/Blueprint/MiscTest.php index 1003aed7..8c114b5d 100644 --- a/tests/Blueprint/MiscTest.php +++ b/tests/Blueprint/MiscTest.php @@ -20,6 +20,7 @@ use JBZoo\CsvBlueprint\Utils; use JBZoo\PHPUnit\PHPUnit; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; use function JBZoo\Data\json; use function JBZoo\Data\phpArray; @@ -128,6 +129,72 @@ public function testCompareExamplesWithOrig(): void isSame($origYml, json("{$basepath}.json")->getArrayCopy(), 'JSON config is invalid'); } + public function testFindFiles(): void + { + isSame(['demo.csv'], $this->getFileName(Utils::findFiles([ + PROJECT_ROOT . '/tests/fixtures/demo.csv', + ]))); + + isSame([], $this->getFileName(Utils::findFiles([]))); + + $this->getFileName(Utils::findFiles(['*.qwerty'])); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv'], $this->getFileName(Utils::findFiles([ + PROJECT_ROOT . '/tests/fixtures/batch/*.csv', + ]))); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv'], $this->getFileName(Utils::findFiles([ + 'tests/fixtures/batch/*.csv', + ]))); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv'], $this->getFileName(Utils::findFiles([ + './tests/fixtures/batch/*.csv', + ]))); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv'], $this->getFileName(Utils::findFiles(['**/demo-*.csv']))); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv', 'demo.csv'], $this->getFileName(Utils::findFiles([ + PROJECT_ROOT . '/tests/fixtures/batch/*.csv', + PROJECT_ROOT . '/tests/fixtures/demo.csv', + ]))); + + isSame(['demo-1.csv', 'demo-2.csv', 'demo-3.csv', 'demo.csv'], $this->getFileName(Utils::findFiles([ + PROJECT_ROOT . '/tests/fixtures/demo.csv', + PROJECT_ROOT . '/tests/fixtures/batch/*.csv', + ]))); + + isSame( + [ + 'demo-1.csv', + 'demo-2.csv', + 'demo-3.csv', + 'complex_header.csv', + 'complex_no_header.csv', + 'demo.csv', + 'empty_header.csv', + 'empty_no_header.csv', + 'simple_header.csv', + 'simple_no_header.csv', + ], + $this->getFileName(Utils::findFiles(['tests/**/*.csv'])), + ); + } + + public function testFindFilesNotFound(): void + { + $this->expectExceptionMessage('File not found: demo.csv'); + $this->getFileName(Utils::findFiles(['demo.csv'])); + } + + /** + * @param SplFileInfo[] $files + * @return string[] + */ + private function getFileName(array $files): array + { + return \array_values(\array_map(static fn (SplFileInfo $file) => $file->getFilename(), $files)); + } + private function testCheckExampleInReadme( string $filepath, string $type, diff --git a/tests/Blueprint/ValidatorTest.php b/tests/Blueprint/ValidatorTest.php index a74202cf..259b0b0a 100644 --- a/tests/Blueprint/ValidatorTest.php +++ b/tests/Blueprint/ValidatorTest.php @@ -21,20 +21,19 @@ use JBZoo\PHPUnit\PHPUnit; use function JBZoo\Data\json; -use function JBZoo\PHPUnit\isContain; use function JBZoo\PHPUnit\isSame; final class ValidatorTest extends PHPUnit { - private const CSV_SIMPLE_HEADER = PROJECT_TESTS . '/fixtures/simple_header.csv'; - private const CSV_SIMPLE_NO_HEADER = PROJECT_TESTS . '/fixtures/simple_no_header.csv'; - private const CSV_COMPLEX = PROJECT_TESTS . '/fixtures/complex_header.csv'; + private const CSV_SIMPLE_HEADER = './tests/fixtures/simple_header.csv'; + private const CSV_SIMPLE_NO_HEADER = './tests/fixtures/simple_no_header.csv'; + private const CSV_COMPLEX = './tests/fixtures/complex_header.csv'; - private const SCHEMA_SIMPLE_HEADER = PROJECT_TESTS . '/schemas/simple_header.yml'; - private const SCHEMA_SIMPLE_NO_HEADER = PROJECT_TESTS . '/schemas/simple_no_header.yml'; + private const SCHEMA_SIMPLE_HEADER = './tests/schemas/simple_header.yml'; + private const SCHEMA_SIMPLE_NO_HEADER = './tests/schemas/simple_no_header.yml'; - private const SCHEMA_SIMPLE_HEADER_PHP = PROJECT_TESTS . '/schemas/simple_header.php'; - private const SCHEMA_SIMPLE_HEADER_JSON = PROJECT_TESTS . '/schemas/simple_header.json'; + private const SCHEMA_SIMPLE_HEADER_PHP = './tests/schemas/simple_header.php'; + private const SCHEMA_SIMPLE_HEADER_JSON = './tests/schemas/simple_header.json'; protected function setUp(): void { @@ -418,7 +417,7 @@ public function testRenderText(): void $csv = new CsvFile(self::CSV_SIMPLE_HEADER, $this->getRule('seq', 'min', 3)); isSame( '"min" at line 2, column "0:seq". Value "1" is less than "3".' . "\n", - \strip_tags($csv->validate(true)->render(ErrorSuite::RENDER_TEXT)), + \strip_tags($csv->validate(true)->render(ErrorSuite::REPORT_TEXT)), ); isSame( @@ -426,7 +425,7 @@ public function testRenderText(): void '"min" at line 2, column "0:seq". Value "1" is less than "3".', '"min" at line 3, column "0:seq". Value "2" is less than "3".' . "\n", ]), - \strip_tags($csv->validate()->render(ErrorSuite::RENDER_TEXT)), + \strip_tags($csv->validate()->render(ErrorSuite::REPORT_TEXT)), ); } @@ -462,16 +461,28 @@ public function testRenderTable(): void public function testRenderTeamCity(): void { $csv = new CsvFile(self::CSV_SIMPLE_HEADER, $this->getRule('seq', 'min', 3)); - $out = $csv->validate()->render(ErrorSuite::RENDER_TEAMCITY); + $out = $csv->validate()->render(ErrorSuite::REPORT_TEAMCITY); $path = self::CSV_SIMPLE_HEADER; - isContain("##teamcity[testCount count='2' ", $out); - isContain("##teamcity[testSuiteStarted name='simple_header.csv' ", $out); - isContain("##teamcity[testStarted name='min at column 0:seq' locationHint='php_qn://{$path}'", $out); - isContain("##teamcity[testFinished name='min at column 0:seq' timestamp", $out); - isContain('Value "1" is less than "3"', $out); - isContain('Value "2" is less than "3"', $out); - isContain("##teamcity[testSuiteFinished name='simple_header.csv'", $out); + $expected = <<<'TEAMCITY' + + ##teamcity[testCount count='2' flowId='42'] + + ##teamcity[testSuiteStarted name='simple_header.csv' flowId='42'] + + ##teamcity[testStarted name='min at column 0:seq' locationHint='php_qn://./tests/fixtures/simple_header.csv' flowId='42'] + "min" at line 2, column "0:seq". Value "1" is less than "3". + ##teamcity[testFinished name='min at column 0:seq' flowId='42'] + + ##teamcity[testStarted name='min at column 0:seq' locationHint='php_qn://./tests/fixtures/simple_header.csv' flowId='42'] + "min" at line 3, column "0:seq". Value "2" is less than "3". + ##teamcity[testFinished name='min at column 0:seq' flowId='42'] + + ##teamcity[testSuiteFinished name='simple_header.csv' flowId='42'] + + TEAMCITY; + + isSame($expected, $out); } public function testRenderGithub(): void @@ -487,7 +498,7 @@ public function testRenderGithub(): void 'column "0:seq". Value "2" is less than "3".', '', ]), - $csv->validate()->render(ErrorSuite::RENDER_GITHUB), + $csv->validate()->render(ErrorSuite::REPORT_GITHUB), ); } @@ -496,7 +507,7 @@ public function testRenderGitlab(): void $csv = new CsvFile(self::CSV_SIMPLE_HEADER, $this->getRule('seq', 'min', 3)); $path = self::CSV_SIMPLE_HEADER; - $cleanJson = json($csv->validate()->render(ErrorSuite::RENDER_GITLAB))->getArrayCopy(); + $cleanJson = json($csv->validate()->render(ErrorSuite::REPORT_GITLAB))->getArrayCopy(); unset($cleanJson[0]['fingerprint'], $cleanJson[1]['fingerprint']); isSame( @@ -504,22 +515,14 @@ public function testRenderGitlab(): void [ 'description' => "min at column 0:seq\n\"min\" at line 2, " . 'column "0:seq". Value "1" is less than "3".', - // 'fingerprint' => '...', 'severity' => 'major', - 'location' => [ - 'path' => $path, - 'lines' => ['begin' => 2], - ], + 'location' => ['path' => $path, 'lines' => ['begin' => 2]], ], [ 'description' => "min at column 0:seq\n\"min\" at line 3, " . 'column "0:seq". Value "2" is less than "3".', - // 'fingerprint' => '..', 'severity' => 'major', - 'location' => [ - 'path' => $path, - 'lines' => ['begin' => 3], - ], + 'location' => ['path' => $path, 'lines' => ['begin' => 3]], ], ], $cleanJson, @@ -545,7 +548,7 @@ public function testRenderJUnit(): void '', '', ]), - $csv->validate()->render(ErrorSuite::RENDER_JUNIT), + $csv->validate()->render(ErrorSuite::REPORT_JUNIT), ); } diff --git a/tests/fixtures/batch/demo-1.csv b/tests/fixtures/batch/demo-1.csv new file mode 100644 index 00000000..daad3d23 --- /dev/null +++ b/tests/fixtures/batch/demo-1.csv @@ -0,0 +1,3 @@ +Name,City,Float,Birthday,Favorite color +Derek,Sarefunaw,-177.9088,2000-01-31,green +Dylan,Wufolu,74605.944,1998-02-28,blue diff --git a/tests/fixtures/batch/demo-2.csv b/tests/fixtures/batch/demo-2.csv new file mode 100644 index 00000000..564b3598 --- /dev/null +++ b/tests/fixtures/batch/demo-2.csv @@ -0,0 +1,7 @@ +Name,City,Float,Birthday,Favorite color +Carl,Gorriju,0.8431,1955-05-14,red +Landon,Mojebol,123.64,1989-05-15,red +Olive,Pebiogu,0,1955-05-14,green +Willie,Sowaah,0.001,2010-07-20,red +Derrick,Rakufag,42,1990-09-10,green +Lois,Mofninle,-19366059127.6032,1988-08-24,green diff --git a/tests/fixtures/batch/sub/demo-3.csv b/tests/fixtures/batch/sub/demo-3.csv new file mode 100644 index 00000000..e417a0e6 --- /dev/null +++ b/tests/fixtures/batch/sub/demo-3.csv @@ -0,0 +1 @@ +Name,City,Float,Birthday,Favorite color diff --git a/tests/schemas/demo_invalid.yml b/tests/schemas/demo_invalid.yml index fe8e396e..e6034d35 100644 --- a/tests/schemas/demo_invalid.yml +++ b/tests/schemas/demo_invalid.yml @@ -19,7 +19,7 @@ columns: min_length: 5 max_length: 7 - - name: + - name: City rules: not_empty: true only_capitalize: true