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