Skip to content

Commit

Permalink
Added validation for example, refactoring and new error reporting i…
Browse files Browse the repository at this point in the history
…n tests (#62)

This update enhances the testing suite by adding schema validation and
reporting associated errors. Specifically, the 'ValidateCsvTest' now
validates the schema and shows detailed error messages if a schema is
found invalid. This makes debugging easier and improves the overall
reliability of the tests.
  • Loading branch information
SmetDenis committed Mar 19, 2024
1 parent 1735c8d commit 75d77c4
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 52 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,13 @@ Default report format is `table`:
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 1
Schema is invalid: ./tests/schemas/demo_invalid.yml
+-------+-----------+----- demo_invalid.yml --------------------------+
| Line | id:Column | Rule | Message |
+-------+-----------+----------+--------------------------------------+
| undef | 2:Float | is_float | Value "Qwerty" is not a float number |
+-------+-----------+----- demo_invalid.yml --------------------------+
(1/1) Invalid file: ./tests/fixtures/demo.csv
+------+------------------+------------------+----------------------- demo.csv ---------------------------------------------------------------------+
| Line | id:Column | Rule | Message |
Expand All @@ -536,6 +543,7 @@ Found CSV files: 1
Found 9 issues in CSV file.
Found 1 issues in schema.
```
<!-- /output-table -->
Expand Down
3 changes: 2 additions & 1 deletion src/Csv/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace JBZoo\CsvBlueprint\Csv;

use JBZoo\CsvBlueprint\Validators\ColumnValidator;
use JBZoo\CsvBlueprint\Validators\Error;
use JBZoo\CsvBlueprint\Validators\ErrorSuite;
use JBZoo\Data\Data;

Expand Down Expand Up @@ -96,7 +97,7 @@ public function getInherit(): string
return $this->column->getString('inherit', self::FALLBACK_VALUES['inherit']);
}

public function validateCell(string $cellValue, int $line): ErrorSuite
public function validateCell(string $cellValue, int $line = Error::UNDEFINED_LINE): ErrorSuite
{
return (new ColumnValidator($this))->validateCell($cellValue, $line);
}
Expand Down
59 changes: 9 additions & 50 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

use JBZoo\CsvBlueprint\Csv\Column;
use JBZoo\CsvBlueprint\Csv\ParseConfig;
use JBZoo\CsvBlueprint\Validators\Error;
use JBZoo\CsvBlueprint\Validators\ErrorSuite;
use JBZoo\CsvBlueprint\Validators\SchemaValidator;
use JBZoo\Data\AbstractData;
use JBZoo\Data\Data;

Expand Down Expand Up @@ -139,56 +139,15 @@ public function getIncludes(): array

public function validate(): ErrorSuite
{
$expected = phpArray(__DIR__ . '/../schema-examples/full.php');

$expectedColumn = $expected->find('columns.0');
$expectedMeta = $expected->remove('columns')->getArrayCopy();

$actual = clone $this->data; // We are going to modify the data. No external side effects.
$actualColumns = $actual->findSelf('columns');
$actualMeta = $actual->remove('columns');

$errors = new ErrorSuite($this->filename);

$metaErrors = Utils::compareArray($expectedMeta, $actualMeta->getArrayCopy(), 'meta', '.');

// Validate meta info
foreach ($metaErrors as $metaError) {
$errors->addError(new Error('schema', $metaError[1], $metaError[0]));
}

// Validate each columns
foreach ($actualColumns->getArrayCopy() as $columnKey => $actualColumn) {
$columnId = "{$columnKey}:" . ($actualColumn['name'] ?? '');

// Validate column names
if (
$this->getCsvStructure()->isHeader()
&& (!isset($actualColumn['name']) || $actualColumn['name'] === '')
) {
$errors->addError(
new Error(
'schema',
'The key "<c>name</c>" must be non-empty because the option "<green>csv.header</green>" = true',
$columnId,
),
);
}

// Validate column schema
$columnErrors = Utils::compareArray(
$expectedColumn,
$actualColumn,
$columnId,
"columns.{$columnKey}",
);

foreach ($columnErrors as $columnError) {
$errors->addError(new Error('schema', $columnError[1], $columnError[0]));
}
}
return (new SchemaValidator($this))->validate();
}

return $errors;
/**
* Clone data to avoid any external side effects.
*/
public function getData(): AbstractData
{
return clone $this->data;
}

/**
Expand Down
141 changes: 141 additions & 0 deletions src/Validators/SchemaValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

/**
* JBZoo Toolbox - Csv-Blueprint.
*
* This file is part of the JBZoo Toolbox project.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT
* @copyright Copyright (C) JBZoo.com, All rights reserved.
* @see https://github.com/JBZoo/Csv-Blueprint
*/

declare(strict_types=1);

namespace JBZoo\CsvBlueprint\Validators;

use JBZoo\CsvBlueprint\Csv\Column;
use JBZoo\CsvBlueprint\Schema;
use JBZoo\CsvBlueprint\Utils;
use JBZoo\Data\AbstractData;

use function JBZoo\Data\phpArray;

final class SchemaValidator
{
private ?string $filename;
private bool $isHeader;
private AbstractData $data;

public function __construct(Schema $schema)
{
$this->filename = $schema->getFilename();
$this->data = $schema->getData();
$this->isHeader = $schema->getCsvStructure()->isHeader();
}

public function validate(): ErrorSuite
{
[$expectedMeta, $expectedColumn] = self::getExpected();
[$actualMeta, $actualColumns] = $this->getActual();

return (new ErrorSuite($this->filename))
->addErrorSuit(self::validateMeta($expectedMeta, $actualMeta))
->addErrorSuit($this->validateColumns($expectedColumn, $actualColumns));
}

private function getActual(): array
{
$actualColumns = $this->data->findSelf('columns');
$actualMeta = $this->data->remove('columns');

return [$actualMeta, $actualColumns];
}

private function validateColumns(array $expectedColumn, AbstractData $actualColumns): ErrorSuite
{
$errors = new ErrorSuite();

foreach ($actualColumns->getArrayCopy() as $columnKey => $actualColumn) {
$columnId = "{$columnKey}:" . ($actualColumn['name'] ?? '');

// Validate column names
$errors->addErrorSuit($this->validateColumn($actualColumn, $columnId, (int)$columnKey));

// Validate column schema
$columnErrors = Utils::compareArray(
$expectedColumn,
$actualColumn,
$columnId,
"columns.{$columnKey}",
);

foreach ($columnErrors as $columnError) {
$errors->addError(new Error('schema', $columnError[1], $columnError[0]));
}
}

return $errors;
}

private function validateColumn(array $actualColumn, string $columnId, int $columnKey): ErrorSuite
{
return (new ErrorSuite())
->addError($this->validateColumnName($actualColumn, $columnId))
->addErrorSuit(self::validateColumnExample($actualColumn, $columnKey));
}

private function validateColumnName(array $actualColumn, string $columnId): ?Error
{
if ($this->isHeader && (!isset($actualColumn['name']) || $actualColumn['name'] === '')) {
return new Error(
'schema',
'The key "<c>name</c>" must be non-empty because the option "<green>csv.header</green>" = true',
$columnId,
);
}

return null;
}

private static function validateColumnExample(array $actualColumn, int $columnKey): ?ErrorSuite
{
$exclude = [
'Some example', // I.e. this value is taken from full.yml, then it will be invalid in advance.
];

if (isset($actualColumn['example']) && !\in_array($actualColumn['example'], $exclude, true)) {
return (new Column($columnKey, $actualColumn))->validateCell($actualColumn['example']);
}

return null;
}

private static function validateMeta(array $expectedMeta, AbstractData $actualMeta): ErrorSuite
{
$errors = new ErrorSuite();
$metaErrors = Utils::compareArray($expectedMeta, $actualMeta->getArrayCopy(), 'meta', '.');

foreach ($metaErrors as $metaError) {
$errors->addError(new Error('schema', $metaError[1], $metaError[0]));
}

return $errors;
}

private static function getExpected(): array
{
$referenceFile = __DIR__ . '/../../schema-examples/full.php';
if (!\file_exists($referenceFile)) {
throw new Exception("Reference schema not found: {$referenceFile}");
}

$expected = phpArray($referenceFile);
$expectedColumn = $expected->findArray('columns.0');
$expectedMeta = $expected->remove('columns')->getArrayCopy();

return [$expectedMeta, $expectedColumn];
}
}
41 changes: 41 additions & 0 deletions tests/Commands/ValidateCsvTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ public function testValidateOneFileNegativeTable(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 1
Schema is invalid: ./tests/schemas/demo_invalid.yml
+-------+-----------+----- demo_invalid.yml --------------------------+
| Line | id:Column | Rule | Message |
+-------+-----------+----------+--------------------------------------+
| undef | 2:Float | is_float | Value "Qwerty" is not a float number |
+-------+-----------+----- demo_invalid.yml --------------------------+
(1/1) Invalid file: ./tests/fixtures/demo.csv
+------+------------------+------------------+----------------------- demo.csv ---------------------------------------------------------------------+
| Line | id:Column | Rule | Message |
Expand All @@ -82,6 +89,7 @@ public function testValidateOneFileNegativeTable(): void
Found 9 issues in CSV file.
Found 1 issues in schema.
TXT;

Expand All @@ -102,6 +110,13 @@ public function testValidateManyFileNegativeTable(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 3
Schema is invalid: ./tests/schemas/demo_invalid.yml
+-------+-----------+----- demo_invalid.yml --------------------------+
| Line | id:Column | Rule | Message |
+-------+-----------+----------+--------------------------------------+
| undef | 2:Float | is_float | Value "Qwerty" is not a float number |
+-------+-----------+----- demo_invalid.yml --------------------------+
(1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv
+------+------------------+--------------+--------- demo-1.csv --------------------------------------------------+
| Line | id:Column | Rule | Message |
Expand Down Expand Up @@ -133,6 +148,7 @@ public function testValidateManyFileNegativeTable(): void
Found 8 issues in 3 out of 3 CSV files.
Found 1 issues in schema.
TXT;

Expand All @@ -152,6 +168,9 @@ public function testValidateOneFileNegativeText(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 1
Schema is invalid: ./tests/schemas/demo_invalid.yml
"is_float", column "2:Float". Value "Qwerty" is not a float number.
(1/1) Invalid file: ./tests/fixtures/demo.csv
"filename_pattern" at line 1, column "". Filename "./tests/fixtures/demo.csv" does not match pattern: "/demo-[12].csv$/i".
"length_min" at line 6, column "0:Name". The length of the value "Carl" is 4, which is less than the expected "5".
Expand All @@ -165,6 +184,7 @@ public function testValidateOneFileNegativeText(): void
Found 9 issues in CSV file.
Found 1 issues in schema.
TXT;

Expand All @@ -178,13 +198,17 @@ public function testValidateManyFilesNegativeTextQuick(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 3
Schema is invalid: ./tests/schemas/demo_invalid.yml
"is_float", column "2:Float". Value "Qwerty" is not a float number.
(1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv
"ag:is_unique" at line 1, column "1:City". Column has non-unique values. Unique: 1, total: 2.
(2/3) Skipped: ./tests/fixtures/batch/demo-2.csv
(3/3) Skipped: ./tests/fixtures/batch/sub/demo-3.csv
Found 1 issues in 1 out of 3 CSV files.
Found 1 issues in schema.
TXT;

Expand Down Expand Up @@ -241,6 +265,9 @@ public function testValidateManyFilesNegativeTextQuick(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 3
Schema is invalid: ./tests/schemas/demo_invalid.yml
"is_float", column "2:Float". Value "Qwerty" is not a float number.
(1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv
"ag:is_unique" at line 1, column "1:City". Column has non-unique values. Unique: 1, total: 2.
"allow_values" at line 3, column "4:Favorite color". Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"].
Expand All @@ -257,6 +284,7 @@ public function testValidateManyFilesNegativeTextQuick(): void
Found 8 issues in 3 out of 3 CSV files.
Found 1 issues in schema.
TXT;

Expand All @@ -278,6 +306,18 @@ public function testCreateValidateNegativeTeamcity(): void
Schema: ./tests/schemas/demo_invalid.yml
Found CSV files: 3
Schema is invalid: ./tests/schemas/demo_invalid.yml
##teamcity[testCount count='1' flowId='42']
##teamcity[testSuiteStarted name='demo_invalid.yml' flowId='42']
##teamcity[testStarted name='is_float at column 2:Float' locationHint='php_qn://./tests/schemas/demo_invalid.yml' flowId='42']
"is_float", column "2:Float". Value "Qwerty" is not a float number.
##teamcity[testFinished name='is_float at column 2:Float' flowId='42']
##teamcity[testSuiteFinished name='demo_invalid.yml' flowId='42']
(1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv
##teamcity[testCount count='2' flowId='42']
Expand Down Expand Up @@ -336,6 +376,7 @@ public function testCreateValidateNegativeTeamcity(): void
Found 8 issues in 3 out of 3 CSV files.
Found 1 issues in schema.
TXT;

Expand Down
3 changes: 2 additions & 1 deletion tests/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,11 @@ public function testValidateValidSchemaFixtures(): void
->notName([
'todo.yml',
'invalid_schema.yml',
'demo_invalid.yml',
])
->files();

isCount(7, $schemas);
isCount(6, $schemas);

foreach ($schemas as $schemaFile) {
$filepath = $schemaFile->getPathname();
Expand Down

0 comments on commit 75d77c4

Please sign in to comment.