From 9fd89b78158517d28941d135f70bf9a351c1b94a Mon Sep 17 00:00:00 2001 From: Denis Smetannikov Date: Tue, 12 Mar 2024 00:53:13 +0400 Subject: [PATCH] Improvements tests and minor fixes (#9) - Update command description to reflect schema validation. - Update schema option description to include supported file types. - Add a new task to the TODO list. - Remove unnecessary comment from the code. --- .github/workflows/main.yml | 25 +--- README.md | 5 +- src/Commands/CreateCsv.php | 60 -------- src/Commands/CreateSchema.php | 60 -------- src/Commands/ValidateCsv.php | 10 +- src/Commands/ValidateDir.php | 51 ------- src/Validators/ErrorSuite.php | 4 +- tests/Blueprint/CommandsTest.php | 227 ++++++++++++++++++++++++++++++ tests/Blueprint/ValidatorTest.php | 32 +++-- 9 files changed, 256 insertions(+), 218 deletions(-) delete mode 100644 src/Commands/CreateCsv.php delete mode 100644 src/Commands/CreateSchema.php delete mode 100644 src/Commands/ValidateDir.php create mode 100644 tests/Blueprint/CommandsTest.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31f5357c..89eb356c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -175,14 +175,13 @@ jobs: name: Reports - ${{ matrix.php-version }} path: build/ + docker: name: Docker runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - with: - fetch-depth: 0 - name: 🐳 Building Docker Image run: make build-docker @@ -193,25 +192,3 @@ jobs: - name: Reporting example via Docker run: make demo-docker --no-print-directory continue-on-error: true - - github-action: - name: GitHub Action - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: 👍 Valid CSV file - uses: ./ - with: - csv: tests/fixtures/demo.csv - schema: tests/schemas/demo_valid.yml - - - name: 👎 Invalid CSV file - uses: ./ - with: - csv: tests/fixtures/demo.csv - schema: tests/schemas/demo_invalid.yml - continue-on-error: true diff --git a/README.md b/README.md index 767dd5d0..ca63f6b6 100644 --- a/README.md +++ b/README.md @@ -155,14 +155,14 @@ So there are options here for all occasions. Description: - Validate CSV file by rule + Validate CSV file by schema. Usage: validate:csv [options] Options: -c, --csv=CSV CSV filepath to validate. - -s, --schema=SCHEMA Schema rule filepath. + -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). @@ -445,6 +445,7 @@ It's random ideas and plans. No orderings and deadlines. But 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)`). +* [ ] 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?"). diff --git a/src/Commands/CreateCsv.php b/src/Commands/CreateCsv.php deleted file mode 100644 index 16ca548d..00000000 --- a/src/Commands/CreateCsv.php +++ /dev/null @@ -1,60 +0,0 @@ -setName('create:csv') - ->setDescription('Generate random CSV file by rules from yml file. Data based on fakerphp/faker library.') - ->addArgument( - 'rule-file', - InputArgument::REQUIRED, - 'Path to rule file (yml)', - ) - ->addOption( - 'seed', - null, - InputOption::VALUE_OPTIONAL, - 'Seed for random data generation (fakerphp/faker library)', - ) - ->addOption( - 'to-file', - null, - InputOption::VALUE_OPTIONAL, - 'If set, the generated CSV will be saved to the specified file. ' - . 'Otherwise, the result will be output to the console as STDOUT.', - ); - - parent::configure(); - } - - protected function executeAction(): int - { - throw new \RuntimeException('Not implemented yet'); - } -} diff --git a/src/Commands/CreateSchema.php b/src/Commands/CreateSchema.php deleted file mode 100644 index 0221ff90..00000000 --- a/src/Commands/CreateSchema.php +++ /dev/null @@ -1,60 +0,0 @@ -setName('create:schema') - ->setDescription('Generate random CSV file by rules from yml file. Data based on fakerphp/faker library.') - ->addArgument( - 'rule-file', - InputArgument::REQUIRED, - 'Path to rule file (yml)', - ) - ->addOption( - 'seed', - null, - InputOption::VALUE_OPTIONAL, - 'Seed for random data generation (fakerphp/faker library)', - ) - ->addOption( - 'to-file', - null, - InputOption::VALUE_OPTIONAL, - 'If set, the generated CSV will be saved to the specified file. ' - . 'Otherwise, the result will be output to the console as STDOUT.', - ); - - parent::configure(); - } - - protected function executeAction(): int - { - throw new \RuntimeException('Not implemented yet'); - } -} diff --git a/src/Commands/ValidateCsv.php b/src/Commands/ValidateCsv.php index 3e5da270..637c74b4 100644 --- a/src/Commands/ValidateCsv.php +++ b/src/Commands/ValidateCsv.php @@ -25,7 +25,6 @@ /** * @psalm-suppress PropertyNotSetInConstructor - * @codeCoverageIgnore */ final class ValidateCsv extends CliCommand { @@ -33,18 +32,18 @@ protected function configure(): void { $this ->setName('validate:csv') - ->setDescription('Validate CSV file by rule') + ->setDescription('Validate CSV file by schema.') ->addOption( 'csv', 'c', InputOption::VALUE_REQUIRED, - 'CSV filepath to validate. If not set or empty, then the STDIN is used.', + 'CSV filepath to validate.', ) ->addOption( 'schema', 's', InputOption::VALUE_REQUIRED, - 'Schema rule filepath', + 'Schema rule filepath. It can be a .yml/.json/.php file.', ) ->addOption( 'output', @@ -73,7 +72,8 @@ protected function executeAction(): int if ($this->isTextMode()) { $this->_( - 'CSV file is not valid! Found ' . $errorSuite->count() . ' errors.', + 'CSV file is not valid! ' . + 'Found ' . $errorSuite->count() . ' errors.', OutLvl::E, ); } diff --git a/src/Commands/ValidateDir.php b/src/Commands/ValidateDir.php deleted file mode 100644 index 15b11df1..00000000 --- a/src/Commands/ValidateDir.php +++ /dev/null @@ -1,51 +0,0 @@ -setName('validate:dir') - ->setDescription('Validate CSV file(s) by rules from yml file(s)') - ->addArgument( - 'csv-file-or-dir', - InputArgument::REQUIRED, - 'Path to CSV file or directory with CSV files', - ) - ->addArgument( - 'rule-file-or-dir', - InputArgument::REQUIRED, - 'Path to rule file (yml) or directory with rule files', - ); - - parent::configure(); - } - - protected function executeAction(): int - { - throw new \RuntimeException('Not implemented yet'); - } -} diff --git a/src/Validators/ErrorSuite.php b/src/Validators/ErrorSuite.php index 3e2f5838..0c95d7a4 100644 --- a/src/Validators/ErrorSuite.php +++ b/src/Validators/ErrorSuite.php @@ -128,7 +128,7 @@ private function renderPlainText(): string $result[] = (string)$error; } - return \implode("\n", $result); + return \implode("\n", $result) . "\n"; } private function renderTable(): string @@ -161,7 +161,7 @@ private function prepareSourceSuite(): SourceSuite $case = $suite->addTestCase($caseName); $case->line = $error->getLine(); $case->file = $this->csvFilename; - $case->errOut = $error->getMessage(); + $case->errOut = (string)$error; } return $suite; diff --git a/tests/Blueprint/CommandsTest.php b/tests/Blueprint/CommandsTest.php new file mode 100644 index 00000000..519cf54e --- /dev/null +++ b/tests/Blueprint/CommandsTest.php @@ -0,0 +1,227 @@ +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, + '```', + ]), PROJECT_ROOT . '/README.md'); + } + + public function testCreateValidatePositive(): void + { + $rootPath = PROJECT_ROOT; + + [$actual, $exitCode] = $this->virtualExecution('validate:csv', [ + 'csv' => "{$rootPath}/tests/fixtures/demo.csv", + '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!', + '', + ]); + + isSame(0, $exitCode); + isSame($expected, $actual); + } + + 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', + ]); + + $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" (legth: 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" (legth: 4) is too short. Min length is 5 |', + '+------+------------------+--------------+-- demo.csv -------------------------------------------+', + '', + 'CSV file is not valid! Found 8 errors.', + '', + ]); + + isSame(1, $exitCode); + isSame($expected, $actual); + } + + 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', + ]); + + $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" (legth: 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" (legth: 4) is too short. ' . + 'Min length is 5.', + + '', + 'CSV file is not valid! Found 8 errors.', + '', + ]); + + isSame(1, $exitCode); + isSame($expected, $actual); + } + + private function virtualExecution(string $action, array $params = []): array + { + $params['no-ansi'] = null; + + $application = new CliApplication(); + $application->add(new ValidateCsv()); + $command = $application->find($action); + + $buffer = new BufferedOutput(); + $args = new StringInput(Cli::build('', $params)); + $exitCode = $command->run($args, $buffer); + + return [$buffer->fetch(), $exitCode]; + } + + private function realExecution(string $action, array $params = []): string + { + $rootDir = PROJECT_ROOT; + + return Cli::exec( + \implode(' ', [ + Sys::getBinary(), + "{$rootDir}/csv-blueprint.php --no-ansi", + $action, + '2>&1', + ]), + $params, + $rootDir, + false, + ); + } +} diff --git a/tests/Blueprint/ValidatorTest.php b/tests/Blueprint/ValidatorTest.php index bbb617fa..a9cd4771 100644 --- a/tests/Blueprint/ValidatorTest.php +++ b/tests/Blueprint/ValidatorTest.php @@ -72,7 +72,7 @@ public function testSchemaAsPhpFile(): void { $csv = new CsvFile(self::CSV_SIMPLE_HEADER, self::SCHEMA_SIMPLE_HEADER_PHP); isSame( - '"min" at line 2, column "0:seq". Value "1" is less than "2".', + '"min" at line 2, column "0:seq". Value "1" is less than "2".' . "\n", (string)$csv->validate(), ); } @@ -81,7 +81,7 @@ public function testSchemaAsJsonFile(): void { $csv = new CsvFile(self::CSV_SIMPLE_HEADER, self::SCHEMA_SIMPLE_HEADER_JSON); isSame( - '"min" at line 2, column "0:seq". Value "1" is less than "2".', + '"min" at line 2, column "0:seq". Value "1" is less than "2".' . "\n", (string)$csv->validate(), ); } @@ -93,7 +93,7 @@ public function testNotEmptyMessage(): void $csv = new CsvFile(self::CSV_COMPLEX, $this->getRule('integer', 'not_empty', true)); isSame( - '"not_empty" at line 19, column "0:integer". Value is empty.', + '"not_empty" at line 19, column "0:integer". Value is empty.' . "\n", (string)$csv->validate(), ); } @@ -103,7 +103,7 @@ public function testNoName(): void $csv = new CsvFile(self::CSV_COMPLEX, $this->getRule(null, 'not_empty', true)); isSame( '"csv.header" at line 1, column "0:". ' . - 'Property "name" is not defined in schema: "_custom_array_".', + 'Property "name" is not defined in schema: "_custom_array_".' . "\n", (string)$csv->validate(), ); } @@ -417,14 +417,14 @@ 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".', + '"min" at line 2, column "0:seq". Value "1" is less than "3".' . "\n", $csv->validate(true)->render(ErrorSuite::RENDER_TEXT), ); isSame( \implode("\n", [ '"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".', + '"min" at line 3, column "0:seq". Value "2" is less than "3".' . "\n", ]), $csv->validate()->render(ErrorSuite::RENDER_TEXT), ); @@ -480,9 +480,11 @@ public function testRenderGithub(): void $path = self::CSV_SIMPLE_HEADER; isSame( \implode("\n", [ - "::error file={$path},line=2::min at column 0:seq%0AValue \"1\" is less than \"3\"", + "::error file={$path},line=2::min at column 0:seq%0A\"min\" at line 2, " . + 'column "0:seq". Value "1" is less than "3".', '', - "::error file={$path},line=3::min at column 0:seq%0AValue \"2\" is less than \"3\"", + "::error file={$path},line=3::min at column 0:seq%0A\"min\" at line 3, " . + 'column "0:seq". Value "2" is less than "3".', '', ]), $csv->validate()->render(ErrorSuite::RENDER_GITHUB), @@ -500,8 +502,9 @@ public function testRenderGitlab(): void isSame( [ [ - 'description' => "min at column 0:seq\nValue \"1\" is less than \"3\"", - // 'fingerprint' => '2c2639beb20e2e9ea13a414ce91865522f6e1885abcf1f99ada44de007cdb01f', + '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, @@ -509,8 +512,9 @@ public function testRenderGitlab(): void ], ], [ - 'description' => "min at column 0:seq\nValue \"2\" is less than \"3\"", - // 'fingerprint' => '0cda6e2df28be9033542ab504e315d070951a206446eb7005d2060d44cfa0e45', + '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, @@ -532,10 +536,10 @@ public function testRenderJUnit(): void '', ' ', " ", - ' Value "1" is less than "3"', + ' "min" at line 2, column "0:seq". Value "1" is less than "3".', ' ', " ", - ' Value "2" is less than "3"', + ' "min" at line 3, column "0:seq". Value "2" is less than "3".', ' ', ' ', '',