<!--
GENERATED FILE - DO NOT EDIT
This file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).
Source File: /docs/mdsource/combinations.source.md
To change this file edit the source file and then run MarkdownSnippets.
-->

# Combinations

Combinations allows all combinations of the given input lists to be executed, and the results all written to a single file.


## Example


### Method being tested

<!-- snippet: CombinationTargetMethod -->
<a id='snippet-CombinationTargetMethod'></a>
```cs
public static string BuildAddress(int number, string street, string city)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(street);
    ArgumentException.ThrowIfNullOrWhiteSpace(city);
    ArgumentOutOfRangeException.ThrowIfLessThan(number, 1);

    return $"{number} {street}, {city}";
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.cs#L5-L16' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationTargetMethod' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Test

<!-- snippet: CombinationSample -->
<a id='snippet-CombinationSample'></a>
```cs
[Fact]
public Task BuildAddressTest()
{
    int[] number = [1, 10];
    string[] street = ["Smith St", "Wallace St"];
    string[] city = ["Sydney", "Chicago"];
    return Combination()
        .Verify(
            BuildAddress,
            number,
            street,
            city);
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.cs#L18-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Result

<!-- snippet: CombinationSample.BuildAddressTest.verified.txt -->
<a id='snippet-CombinationSample.BuildAddressTest.verified.txt'></a>
```txt
{
   1, Smith St  , Sydney : 1 Smith St, Sydney,
   1, Smith St  , Chicago: 1 Smith St, Chicago,
   1, Wallace St, Sydney : 1 Wallace St, Sydney,
   1, Wallace St, Chicago: 1 Wallace St, Chicago,
  10, Smith St  , Sydney : 10 Smith St, Sydney,
  10, Smith St  , Chicago: 10 Smith St, Chicago,
  10, Wallace St, Sydney : 10 Wallace St, Sydney,
  10, Wallace St, Chicago: 10 Wallace St, Chicago
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.BuildAddressTest.verified.txt#L1-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample.BuildAddressTest.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


## Column Alignment

Key value are aligned based on type.

 * Numbers (int, double, float etc) are aligned right
 * All other types are aligned left


## CaptureExceptions

By default exceptions are not captured. So if an exception is thrown by the method being tested, it will bubble up.

Exceptions can be optionally "captured". This approach uses the `Exception.Message` as the result of the method being tested.

To enable exception capture use `captureExceptions = true`:

<!-- snippet: CombinationSample_CaptureExceptions -->
<a id='snippet-CombinationSample_CaptureExceptions'></a>
```cs
[Fact]
public Task BuildAddressExceptionsTest()
{
    int[] number = [-1, 0, 10];
    string[] street = ["", " ", "Valid St"];
    string[] city = [null!, "Valid City"];
    return Combination(captureExceptions: true)
        .Verify(
            BuildAddress,
            number,
            street,
            city
        );
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.cs#L92-L109' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample_CaptureExceptions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Result

<!-- snippet: CombinationSample.BuildAddressExceptionsTest.verified.txt -->
<a id='snippet-CombinationSample.BuildAddressExceptionsTest.verified.txt'></a>
```txt
{
  -1,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
  -1, Valid St, Valid City: ArgumentOutOfRangeException: number ('-1') must be greater than or equal to '1'. (Parameter 'number'). Actual value was -1.,
   0,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
   0, Valid St, Valid City: ArgumentOutOfRangeException: number ('0') must be greater than or equal to '1'. (Parameter 'number'). Actual value was 0.,
  10,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
  10, Valid St, Valid City: 10 Valid St, Valid City
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.BuildAddressExceptionsTest.verified.txt#L1-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample.BuildAddressExceptionsTest.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Global CaptureExceptions

Exception capture can be enabled globally:

<!-- snippet: GlobalCaptureExceptions -->
<a id='snippet-GlobalCaptureExceptions'></a>
```cs
[ModuleInitializer]
public static void EnableCaptureExceptions() =>
    CombinationSettings.CaptureExceptions();
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.cs#L3-L9' title='Snippet source file'>snippet source</a> | <a href='#snippet-GlobalCaptureExceptions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

If exception capture has been enabled globally, it can be disable at the method test level using `captureExceptions: false`.

<!-- snippet: CombinationSample_CaptureExceptionsFalse -->
<a id='snippet-CombinationSample_CaptureExceptionsFalse'></a>
```cs
[Fact]
public Task BuildAddressExceptionsDisabledTest()
{
    int[] number = [1, 10];
    string[] street = ["Smith St", "Wallace St"];
    string[] city = ["Sydney", "Chicago"];
    return Combination(captureExceptions: false)
        .Verify(
            BuildAddress,
            number,
            street,
            city);
}
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.cs#L177-L193' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample_CaptureExceptionsFalse' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


## Result serialization

Serialization of results is done using `CombinationResultsConverter`

<!-- snippet: CombinationResultsConverter.cs -->
<a id='snippet-CombinationResultsConverter.cs'></a>
```cs
namespace VerifyTests;

public class CombinationResultsConverter :
    WriteOnlyJsonConverter<CombinationResults>
{
    public override void Write(VerifyJsonWriter writer, CombinationResults results)
    {
        writer.WriteStartObject();

        var items = results.Items;
        if (items.Count == 0)
        {
            return;
        }

        var keysLength = items[0].Keys.Count;

        int[] maxKeyLengths;
        if (results.Columns == null)
        {
            maxKeyLengths = new int[keysLength];
        }
        else
        {
            maxKeyLengths = results.Columns.Select(_=>_.Length).ToArray();
        }

        var keyValues = new string[items.Count, keysLength];

        for (var itemIndex = 0; itemIndex < items.Count; itemIndex++)
        {
            var item = items[itemIndex];
            for (var keyIndex = 0; keyIndex < keysLength; keyIndex++)
            {
                var key = item.Keys[keyIndex];
                var name = VerifierSettings.GetNameForParameter(key, writer.Counter, pathFriendly: false);
                keyValues[itemIndex, keyIndex] = name;
                var currentKeyLength = maxKeyLengths[keyIndex];
                if (name.Length > currentKeyLength)
                {
                    maxKeyLengths[keyIndex] = name.Length;
                }
            }
        }

        WriteColumns(writer, results, maxKeyLengths);

        // keys is reused
        var keys = new CombinationKey[keysLength];
        for (var itemIndex = 0; itemIndex < items.Count; itemIndex++)
        {
            for (var keyIndex = 0; keyIndex < keysLength; keyIndex++)
            {
                keys[keyIndex] = new(
                    Value: keyValues[itemIndex, keyIndex],
                    MaxLength: maxKeyLengths[keyIndex],
                    Type: results.KeyTypes?[keyIndex]);
            }

            var item = items[itemIndex];
            var name = BuildPropertyName(keys);
            writer.WritePropertyName(name);
            WriteValue(writer, item);
        }

        writer.WriteEndObject();
    }

    static void WriteColumns(VerifyJsonWriter writer, CombinationResults results, int[] maxKeyLengths)
    {
        if (results.Columns == null)
        {
            return;
        }

        var builder = new StringBuilder();
        for (var index = 0; index < results.Columns.Count; index++)
        {
            var column = results.Columns[index];
            var maxLength = maxKeyLengths[index];
            var padding = maxLength - column.Length;
            builder.Append(column);
            builder.Append(' ', padding);
            builder.Append(", ");
        }
        builder.Length -= 2;

        writer.WritePropertyName(builder.ToString());
        writer.WriteValue("Result");
    }

    protected virtual string BuildPropertyName(IReadOnlyList<CombinationKey> keys)
    {
        var builder = new StringBuilder();
        foreach (var (value, maxLength, type) in keys)
        {
            var padding = maxLength - value.Length;
            if (type != null &&
                type.IsNumeric())
            {
                builder.Append(' ', padding);
                builder.Append(value);
            }
            else
            {
                builder.Append(value);
                builder.Append(' ', padding);
            }

            builder.Append(", ");
        }

        builder.Length -= 2;
        return builder.ToString();
    }

    protected virtual void WriteValue(VerifyJsonWriter writer, CombinationResult result)
    {
        switch (result.Type)
        {
            case CombinationResultType.Void:
                writer.WriteValue("void");
                break;
            case CombinationResultType.Value:
                if (result.Value == null)
                {
                    writer.WriteNull();
                }
                else
                {
                    writer.Serialize(result.Value);
                }
                break;
            case CombinationResultType.Exception:
                var exception = result.Exception;
                var message = exception.Message;
                if (exception is ArgumentException)
                {
                    message = FlattenMessage(message);
                }

                writer.WriteValue($"{exception.GetType().Name}: {message}");
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    static string FlattenMessage(string message)
    {
        var builder = new StringBuilder();

        foreach (var line in message.AsSpan().EnumerateLines())
        {
            var trimmed = line.TrimEnd();
            builder.Append(trimmed);
            if (!trimmed.EndsWith('.'))
            {
                builder.Append(". ");
            }
        }

        builder.TrimEnd();
        return builder.ToString();
    }
}
```
<sup><a href='/src/Verify/Combinations/CombinationResultsConverter.cs#L1-L166' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationResultsConverter.cs' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Custom

Combination serialization can be customized using a Converter.


#### Converter

Inherit from `CombinationResultsConverter` and override the desired members.

The below sample override `BuildPropertyName` to customize the property name. It bypasses the default implementation and hence does not pad columns or use `VerifierSettings.GetNameForParameter` for key conversion.

<!-- snippet: CombinationSample_CustomSerializationConverter -->
<a id='snippet-CombinationSample_CustomSerializationConverter'></a>
```cs
class CustomCombinationConverter :
    CombinationResultsConverter
{
    protected override string BuildPropertyName(IReadOnlyList<CombinationKey> keys) =>
        string.Join(", ", keys.Select(_ => _.Value));
}
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.cs#L223-L232' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample_CustomSerializationConverter' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Full control of serialization can be achieved by inheriting from `WriteOnlyJsonConverter<CombinationResults>`.


#### Insert Converter

Insert the new converter at the top of the converter stack.

<!-- snippet: CombinationSample_CustomSerializationModuleInitializer -->
<a id='snippet-CombinationSample_CustomSerializationModuleInitializer'></a>
```cs
static CustomCombinationConverter customConverter = new();

[ModuleInitializer]
public static void Init() =>
    VerifierSettings.AddExtraSettings(_ => _.Converters.Insert(0, customConverter));
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.cs#L195-L203' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample_CustomSerializationModuleInitializer' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


#### Result

<!-- snippet: CombinationTests.Combination_CustomSerialization.verified.txt -->
<a id='snippet-CombinationTests.Combination_CustomSerialization.verified.txt'></a>
```txt
{
  streetNumbers, streets   , cities : Result,
  1, Smith St, Sydney: 1 Smith St, Sydney,
  1, Smith St, Chicago: 1 Smith St, Chicago,
  1, Wallace St, Sydney: 1 Wallace St, Sydney,
  1, Wallace St, Chicago: 1 Wallace St, Chicago,
  10, Smith St, Sydney: 10 Smith St, Sydney,
  10, Smith St, Chicago: 10 Smith St, Chicago,
  10, Wallace St, Sydney: 10 Wallace St, Sydney,
  10, Wallace St, Chicago: 10 Wallace St, Chicago
}
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.Combination_CustomSerialization.verified.txt#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationTests.Combination_CustomSerialization.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


## Header

By default no column headers are included. To include a header pass through `header: true`

<!-- snippet: CombinationSampleWithHeader -->
<a id='snippet-CombinationSampleWithHeader'></a>
```cs
[Fact]
public Task BuildAddressWithHeaderTest()
{
    int[] number = [1, 10];
    string[] street = ["Smith St", "Wallace St"];
    string[] city = ["Sydney", "Chicago"];
    return Combination(header: true)
        .Verify(
            BuildAddress,
            number,
            street,
            city);
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.cs#L36-L52' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSampleWithHeader' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The variable names of the inputted collections will be used.

Result:

<!-- snippet: CombinationSample.BuildAddressWithHeaderTest.verified.txt -->
<a id='snippet-CombinationSample.BuildAddressWithHeaderTest.verified.txt'></a>
```txt
{
  number, street    , city   : Result,
       1, Smith St  , Sydney : 1 Smith St, Sydney,
       1, Smith St  , Chicago: 1 Smith St, Chicago,
       1, Wallace St, Sydney : 1 Wallace St, Sydney,
       1, Wallace St, Chicago: 1 Wallace St, Chicago,
      10, Smith St  , Sydney : 10 Smith St, Sydney,
      10, Smith St  , Chicago: 10 Smith St, Chicago,
      10, Wallace St, Sydney : 10 Wallace St, Sydney,
      10, Wallace St, Chicago: 10 Wallace St, Chicago
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.BuildAddressWithHeaderTest.verified.txt#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample.BuildAddressWithHeaderTest.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Override

Header names can be overridden:

<!-- snippet: CombinationSampleWithHeaderOverrides -->
<a id='snippet-CombinationSampleWithHeaderOverrides'></a>
```cs
[Fact]
public Task BuildAddressWithHeaderOverridesTest()
{
    int[] number = [1, 10];
    string[] street = ["Smith St", "Wallace St"];
    string[] city = ["Sydney", "Chicago"];
    return Combination(header: true)
        .Verify(
            BuildAddress,
            number,
            street,
            city,
            "Number",
            "Street",
            "City");
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.cs#L53-L72' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSampleWithHeaderOverrides' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Result:

<!-- snippet: CombinationSample.BuildAddressWithHeaderOverridesTest.verified.txt -->
<a id='snippet-CombinationSample.BuildAddressWithHeaderOverridesTest.verified.txt'></a>
```txt
{
  Number, Street    , City   : Result,
       1, Smith St  , Sydney : 1 Smith St, Sydney,
       1, Smith St  , Chicago: 1 Smith St, Chicago,
       1, Wallace St, Sydney : 1 Wallace St, Sydney,
       1, Wallace St, Chicago: 1 Wallace St, Chicago,
      10, Smith St  , Sydney : 10 Smith St, Sydney,
      10, Smith St  , Chicago: 10 Smith St, Chicago,
      10, Wallace St, Sydney : 10 Wallace St, Sydney,
      10, Wallace St, Chicago: 10 Wallace St, Chicago
}
```
<sup><a href='/src/Verify.Tests/CombinationSample.BuildAddressWithHeaderOverridesTest.verified.txt#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationSample.BuildAddressWithHeaderOverridesTest.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


### Global

Headers can be enabled globally:

<!-- snippet: GlobalCombinationHeader -->
<a id='snippet-GlobalCombinationHeader'></a>
```cs
[ModuleInitializer]
public static void EnableIncludeHeaders() =>
    CombinationSettings.IncludeHeaders();
```
<sup><a href='/src/StaticSettingsTests/CombinationTests.cs#L234-L240' title='Snippet source file'>snippet source</a> | <a href='#snippet-GlobalCombinationHeader' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->