Skip to content

Commit

Permalink
feat: Add support to ignore column via index when CSV Headers are mis…
Browse files Browse the repository at this point in the history
…sing (#161)

* Added an option to ignore column by its index and validation to avoid using this option alongside with ColumnOrder = Ignore

* Overload IgnoreColumn method

* Move validation of IgnoredColumnIndexes used with ColumnOrder Ignore to existing method

* Fixed exception message

* Fixed FailsWithDescription test message content

* Created a IgnoredIndex prop in TestCsv that returns any indexes of an existing column of that CSV

* Added test case to make sure assertion still succeeds when same column is ignored via its index or header name at the same time

* Added guard against negative index values

* Updated docs to include IgnoreColumns by index

* Typo

* Typo

* Update src/Arcus.Testing.Tests.Unit/Assert_/AssertCsvTests.cs

Co-authored-by: Stijn Moreels <[email protected]>

---------

Co-authored-by: Stijn Moreels <[email protected]>
  • Loading branch information
ClementVaillantCodit and stijnmoreels authored Jul 15, 2024
1 parent 6ed8f40 commit ec39775
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 32 deletions.
4 changes: 4 additions & 0 deletions docs/preview/02-Features/02-assertion.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ AssertCsv.Equal(..., options =>
{
// Adds one ore more column names that should be excluded from the CSV comparison.
options.IgnoreColumn("ignore-this-column");

// Adds one ore more zero-based column index that should be excluded from the CSV comparison.
options.IgnoreColumn(0);

// The type of header handling the loaded CSV document should have.
// Default: Present.
Expand Down Expand Up @@ -236,6 +239,7 @@ AssertCsv.Equal(..., options =>
> ⚠️ **️IMPORTANT:** beware of combining ordering options:
> * If you want to ignore the order of columns, but do not use headers in your CSV contents (`options.Header = Missing`), the order cannot be determined. Make sure to include headers in your CSV contents, or do not use `Ignore` for columns.
> * If you want to ignore the order of columns, but do not use headers or use duplicate headers, the comparison cannot determine whether all the cells are there. Make sure to include headers and use `IgnoreColumn` to remove any duplicates, or do not use `Ignore` for columns.
> * If you want to ignore a column via its index, but also want to ignore the order of columns, the comparison cannot determine the column index to ignore. Either remove all calls to `options.IgnoreColumn(0);` or set `options.ColumnOrder` to `AssertCsvOrder.Include;`.
### Loading CSV tables yourself
The CSV assertion equalization can be called directly with with raw contents - internally it parses the contents to a valid tabular structure: `CsvTable`. If it so happens that you want to compare two CSV tables each with different header, separators or other serialization settings, you can load the two tables separately and do the equalization on the loaded CSV tables.
Expand Down
73 changes: 53 additions & 20 deletions src/Arcus.Testing.Assert/AssertCsv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Arcus.Testing
public enum AssertCsvHeader
{
/// <summary>
/// Indicate that the CSV table has an header present.
/// Indicate that the CSV table has a header present.
/// </summary>
Present = 0,

Expand All @@ -31,12 +31,12 @@ public enum AssertCsvHeader
public enum AssertCsvOrder
{
/// <summary>
/// Take the order of rows into account when comparing tables (default).
/// Take the order of rows or columns into account when comparing tables (default).
/// </summary>
Include = 0,

/// <summary>
/// Ignore the order of rows when comparing tables.
/// Ignore the order of rows or columns when comparing tables.
/// </summary>
Ignore
}
Expand All @@ -47,6 +47,7 @@ public enum AssertCsvOrder
public class AssertCsvOptions
{
private readonly Collection<string> _ignoredColumns = new();
private readonly Collection<int> _ignoredColumnIndexes = new();
private int _maxInputCharacters = ReportBuilder.DefaultMaxInputCharacters;
private string _newRow = Environment.NewLine;
private AssertCsvHeader _header = AssertCsvHeader.Present;
Expand All @@ -69,11 +70,31 @@ public AssertCsvOptions IgnoreColumn(string headerName)
return this;
}

/// <summary>
/// Adds a column via a zero-based index which will get ignored when comparing CSV tables.
/// </summary>
/// <param name="index">The zero-based index of the column that should be ignored.</param>
public AssertCsvOptions IgnoreColumn(int index)
{
if (index < 0)
{
throw new ArgumentOutOfRangeException(nameof(index), $"Requires a positive '{nameof(index)}' value when adding an ignored column of a CSV table");
}

_ignoredColumnIndexes.Add(index);
return this;
}

/// <summary>
/// Gets the header names of the columns that should be ignored when comparing CSV tables.
/// </summary>
internal IReadOnlyCollection<string> IgnoredColumns => _ignoredColumns;

/// <summary>
/// Gets the indexes of the columns that should be ignored when comparing CSV tables.
/// </summary>
internal IReadOnlyCollection<int> IgnoredColumnIndexes => _ignoredColumnIndexes;

/// <summary>
/// Gets or sets the separator character to be used when determining CSV columns in the loaded table, default: ; (semicolon).
/// </summary>
Expand Down Expand Up @@ -251,7 +272,7 @@ public static void Equal(string expectedCsv, string actualCsv, Action<AssertCsvO

var expected = CsvTable.Load(expectedCsv, options);
var actual = CsvTable.Load(actualCsv, options);

Equal(expected, actual, configureOptions);
}

Expand All @@ -266,8 +287,8 @@ public static void Equal(string expectedCsv, string actualCsv, Action<AssertCsvO
/// </exception>
public static void Equal(CsvTable expected, CsvTable actual)
{
Equal(expected ?? throw new ArgumentNullException(nameof(expected)),
actual ?? throw new ArgumentNullException(nameof(actual)),
Equal(expected ?? throw new ArgumentNullException(nameof(expected)),
actual ?? throw new ArgumentNullException(nameof(actual)),
configureOptions: null);
}

Expand All @@ -293,9 +314,10 @@ public static void Equal(CsvTable expected, CsvTable actual, Action<AssertCsvOpt

if (diff != null)
{
string optionsDescription =
string optionsDescription =
$"Options: {Environment.NewLine}" +
$"\t- ignored columns: [{string.Join($"{options.Separator} ", options.IgnoredColumns)}]{Environment.NewLine}" +
$"\t- ignored column indexes: [{string.Join($"{options.Separator} ", options.IgnoredColumnIndexes)}]{Environment.NewLine}" +
$"\t- column order: {options.ColumnOrder}{Environment.NewLine}" +
$"\t- row order: {options.RowOrder}";

Expand Down Expand Up @@ -375,6 +397,15 @@ private static void EnsureOnlyIgnoreColumnsOnPresentHeaders(CsvTable expected, C
$"please provide such headers in the contents, or remove the 'options.{nameof(AssertCsvOptions.IgnoreColumn)}' call(s)")
.ToString());
}

if (options.IgnoredColumnIndexes.Count > 0 && options.ColumnOrder == AssertCsvOrder.Ignore)
{
throw new EqualAssertionException(
ReportBuilder.ForMethod(EqualMethodName, "cannot compare expected and actual CSV contents")
.AppendLine($"columns can only be ignored by their indexes when column order is included in the expected and actual CSV tables, " +
$"please remove the 'options.{nameof(AssertCsvOptions.IgnoreColumn)}', or remove the 'options.{nameof(AssertCsvOptions.ColumnOrder)}={AssertCsvOrder.Ignore}'")
.ToString());
}
}

private static void EnsureOnlyIgnoreColumnsOnUniqueHeaders(CsvTable expected, AssertCsvOptions options)
Expand Down Expand Up @@ -404,10 +435,10 @@ private static CsvDifference CompareHeaders(CsvTable expected, CsvTable actual,
return new(DifferentHeaderConfig, expected.Header.ToString(), actual.Header.ToString(), rowNumber: 0);
}

IReadOnlyCollection<string>
IReadOnlyCollection<string>
expectedHeaders = expected.HeaderNames,
actualHeaders = actual.HeaderNames;

if (options.ColumnOrder is AssertCsvOrder.Ignore)
{
expectedHeaders = expectedHeaders.OrderBy(h => h).ToArray();
Expand All @@ -418,7 +449,7 @@ private static CsvDifference CompareHeaders(CsvTable expected, CsvTable actual,
{
string expectedHeader = expectedHeaders.ElementAt(i),
actualHeader = actualHeaders.ElementAt(i);

if (expectedHeader != actualHeader)
{
return new(ActualMissingColumn, expectedHeader, actualHeader, rowNumber: 0);
Expand All @@ -430,10 +461,10 @@ private static CsvDifference CompareHeaders(CsvTable expected, CsvTable actual,

private static CsvDifference CompareRows(CsvTable expectedCsv, CsvTable actualCsv, AssertCsvOptions options)
{
IReadOnlyCollection<CsvRow>
IReadOnlyCollection<CsvRow>
expectedRows = expectedCsv.Rows,
actualRows = actualCsv.Rows;

if (options.ColumnOrder is AssertCsvOrder.Ignore)
{
expectedRows = CsvRow.WithOrderedCells(expectedRows);
Expand All @@ -451,8 +482,8 @@ private static CsvDifference CompareRows(CsvTable expectedCsv, CsvTable actualCs
{
CsvRow expectedRow = expectedRows.ElementAt(row),
actualRow = actualRows.ElementAt(row);
IReadOnlyCollection<CsvCell>

IReadOnlyCollection<CsvCell>
expectedCells = expectedRow.Cells,
actualCells = actualRow.Cells;

Expand All @@ -461,7 +492,7 @@ private static CsvDifference CompareRows(CsvTable expectedCsv, CsvTable actualCs
CsvCell expectedCell = expectedCells.ElementAt(col),
actualCell = actualCells.ElementAt(col);

if (options.IgnoredColumns.Contains(expectedCell.HeaderName))
if (options.IgnoredColumnIndexes.Contains(col) || options.IgnoredColumns.Contains(expectedCell.HeaderName))
{
continue;
}
Expand All @@ -470,7 +501,7 @@ private static CsvDifference CompareRows(CsvTable expectedCsv, CsvTable actualCs
{
return shouldIgnoreOrder
? new(ActualMissingRow, expectedRow, actualRow)
: new(ActualOtherValue, expectedCell, actualCell);
: new(ActualOtherValue, expectedCell, actualCell);
}
}
}
Expand Down Expand Up @@ -514,7 +545,7 @@ internal CsvDifference(CsvDifferenceKind kind, string expected, string actual, i

private static string QuoteValueUponSpaces(string value)
{
return value.Contains(' ')
return value.Contains(' ')
&& !value.StartsWith('"')
&& !value.EndsWith('"') ? $"\"{value}\"" : value;
}
Expand Down Expand Up @@ -704,7 +735,7 @@ private static void EnsureAllRowsSameLength(string csv, string[][] rawRows, Asse
.Select(row => $"\t - {row.Count()} row(s) with {row.Key} columns")
.Aggregate((x, y) => x + Environment.NewLine + y);

string optionsDescription =
string optionsDescription =
$"Options: {Environment.NewLine}" +
$"\t- separator: {options.Separator}{Environment.NewLine}" +
$"\t- escape: {options.Escape}{Environment.NewLine}" +
Expand Down Expand Up @@ -781,7 +812,9 @@ internal static CsvRow[] WithOrderedRows(IReadOnlyCollection<CsvRow> rows, Asser

return rows.OrderBy(r =>
{
string[] line = r.Cells.Where(c => !options.IgnoredColumns.Contains(c.HeaderName)).Select(c => c.Value).ToArray();
string[] line = r.Cells.Where(c => !options.IgnoredColumns.Contains(c.HeaderName) && !options.IgnoredColumnIndexes.Contains(c.ColumnNumber))
.Select(c => c.Value)
.ToArray();
return string.Join(options.Separator, line);
}).ToArray();
}
Expand Down Expand Up @@ -855,7 +888,7 @@ public bool Equals(CsvCell other)
const char blankSpace = ' ';
bool containsSpaces = Value.Contains(blankSpace) || other.Value.Contains(blankSpace);

if (!containsSpaces
if (!containsSpaces
&& float.TryParse(Value, style, _culture, out float expectedValue)
&& float.TryParse(other.Value, style, _culture, out float actualValue))
{
Expand Down
82 changes: 74 additions & 8 deletions src/Arcus.Testing.Tests.Unit/Assert_/AssertCsvTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public void CompareWithIgnoredColumnOrder_WithShuffledActual_StillSucceeds()
TestCsv actual = expected.Copy();

actual.ShuffleColumns();

// Act / Assert
EqualCsv(expected, actual, options => options.ColumnOrder = AssertCsvOrder.Ignore);
}
Expand Down Expand Up @@ -240,6 +240,72 @@ public void CompareWithIgnoredRowAndColumnOrder_WithShuffledCsv_Succeeds()
});
}

[Property]
public void CompareWithIgnoredColumnOrderAndColumnIndexes_WithShuffledCsv_FailsWithDescription()
{
// Arrange
TestCsv expected = TestCsv.Generate();
TestCsv actual = expected.Copy();

actual.ShuffleColumns();

CompareShouldFailWithDescription(
expected,
actual,
options =>
{
options.ColumnOrder = AssertCsvOrder.Ignore;
options.IgnoreColumn(expected.IgnoredIndex);
},
"cannot compare", "indexes", "column order", "included",
nameof(AssertCsvOptions.IgnoreColumn), AssertCsvOrder.Ignore.ToString()
);
}

[Property]
public void CompareWithIgnoredColumnIndex_WithSameCsv_Succeeds()
{
// Arrange
TestCsv expected = TestCsv.Generate();
TestCsv actual = expected.Copy();

// Act / Assert
EqualCsv(expected, actual, options => options.IgnoreColumn(expected.IgnoredIndex));
}

[Property]
public void CompareWithIgnoredColumnIndexAndMissingHeaders_WithSameCsv_StillSucceeds()
{
// Arrange
TestCsv expected = TestCsv.Generate(opt => opt.Header = AssertCsvHeader.Missing);
TestCsv actual = expected.Copy();

// Act / Assert
EqualCsv(expected, actual, options =>
{
options.IgnoreColumn(expected.IgnoredIndex);
options.Header = AssertCsvHeader.Missing;
});
}

[Property]
public void CompareWithIgnoredColumnIndexAndIgnoredColumnHeader_WithSameCsvAndIndexIsSameAsHeader_StillSucceeds()
{
// Arrange
TestCsv expected = TestCsv.Generate();
TestCsv actual = expected.Copy();

var ignoredIndex = expected.IgnoredIndex;
var headerName = expected.HeaderNames[ignoredIndex];

// Act / Assert
EqualCsv(expected, actual, options =>
{
options.IgnoreColumn(ignoredIndex);
options.IgnoreColumn(headerName);
});
}

[Property]
public void CompareWithIgnoredColumn_WithDuplicateColumn_StillSucceeds()
{
Expand Down Expand Up @@ -269,7 +335,7 @@ public void CompareWithIgnoredColumnOrder_WithIgnoredColumn_FailsWithDescription
actual.AddColumn(extraColumn);
expected.AddColumn(extraColumn);

CompareShouldFailWithDescription(expected, actual, options => options.ColumnOrder = AssertCsvOrder.Ignore,
CompareShouldFailWithDescription(expected, actual, options => options.ColumnOrder = AssertCsvOrder.Ignore,
"cannot compare", AssertCsvOrder.Ignore.ToString(), "duplicate", "columns", extraColumn);
}

Expand Down Expand Up @@ -507,8 +573,8 @@ public static IEnumerable<object[]> FailingBeEquivalentCases
{
yield return new object[]
{
"id",
"ids",
"id",
"ids",
"missing", "column", "id"
};
yield return new object[]
Expand Down Expand Up @@ -629,7 +695,7 @@ public void Load_WithRandomCsvWithHeader_Succeeds()
{
// Arrange
TestCsv expected = TestCsv.Generate();

// Act
CsvTable actual = LoadCsv(expected, opt =>
{
Expand Down Expand Up @@ -756,12 +822,12 @@ private CsvTable LoadCsv(TestCsv csv, Action<AssertCsvOptions> configureOptions
});
}

private CsvTable LoadCsv(string csv, Action<AssertCsvOptions> configureOptions = null, string tag = "Input")
private CsvTable LoadCsv(string csv, Action<AssertCsvOptions> configureOptions = null, string tag = "Input")
{
_outputWriter.WriteLine("{0}: {1}", NewLine + tag, csv + NewLine);

return configureOptions is null
? AssertCsv.Load(csv)
return configureOptions is null
? AssertCsv.Load(csv)
: AssertCsv.Load(csv, configureOptions);
}
}
Expand Down
Loading

0 comments on commit ec39775

Please sign in to comment.