From 1988f1402e55db782708c31f7c16a96faaea9da4 Mon Sep 17 00:00:00 2001
From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
Date: Fri, 10 Jan 2025 00:43:24 +0100
Subject: [PATCH] Repeating pattern values for BitAutoData attribute (#5167)
* Repeating pattern values for BitAutoData attribute
* nullable enabled, added documentation
* execute test method even if no repeating pattern data provided (empty array).
* RepeatingPatternBitAutoDataAttribute unit tests
---
.../RepeatingPatternBitAutoDataAttribute.cs | 112 +++++++
...peatingPatternBitAutoDataAttributeTests.cs | 290 ++++++++++++++++++
2 files changed, 402 insertions(+)
create mode 100644 test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs
create mode 100644 test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttributeTests.cs
diff --git a/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs
new file mode 100644
index 0000000000..48b8c1e92c
--- /dev/null
+++ b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs
@@ -0,0 +1,112 @@
+#nullable enable
+using System.Reflection;
+
+namespace Bit.Test.Common.AutoFixture.Attributes;
+
+///
+/// This attribute helps to generate all possible combinations of the provided pattern values for a given number of parameters.
+///
+///
+/// The repeating pattern values should be provided as an array for each parameter. Currently supports up to 3 parameters.
+///
+///
+/// The attribute is a variation of the attribute and can be used in the same way, except that all fixed value parameters needs to be provided as an array.
+///
+///
+/// Note: Use it with caution. While this attribute is useful for handling repeating parameters, having too many parameters should be avoided as it is considered a code smell in most of the cases.
+/// If your test requires more than 2 repeating parameters, or the test have too many conditions that change the behavior of the test, consider refactoring the test by splitting it into multiple smaller ones.
+///
+///
+///
+/// 1st example:
+///
+/// [RepeatingPatternBitAutoData([false], [1,2,3])]
+/// public void TestMethod(bool first, int second, SomeOtherData third, ...)
+///
+/// Would generate the following test cases:
+///
+/// - false, 1
+/// - false, 2
+/// - false, 3
+///
+/// 2nd example:
+///
+/// [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]
+/// public void TestMethod(bool first, bool second, bool third)
+///
+/// Would generate the following test cases:
+///
+/// - false, false, false
+/// - false, false, true
+/// - false, true, false
+/// - false, true, true
+/// - true, false, false
+/// - true, false, true
+/// - true, true, false
+/// - true, true, true
+///
+///
+///
+public class RepeatingPatternBitAutoDataAttribute : BitAutoDataAttribute
+{
+ private readonly List _repeatingDataList;
+
+ public RepeatingPatternBitAutoDataAttribute(object?[] first)
+ {
+ _repeatingDataList = AllValues([first]);
+ }
+
+ public RepeatingPatternBitAutoDataAttribute(object?[] first, object?[] second)
+ {
+ _repeatingDataList = AllValues([first, second]);
+ }
+
+ public RepeatingPatternBitAutoDataAttribute(object?[] first, object?[] second, object?[] third)
+ {
+ _repeatingDataList = AllValues([first, second, third]);
+ }
+
+ public override IEnumerable GetData(MethodInfo testMethod)
+ {
+ if (_repeatingDataList.Count == 0)
+ {
+ yield return base.GetData(testMethod).First();
+ }
+
+ foreach (var repeatingData in _repeatingDataList)
+ {
+ var bitData = base.GetData(testMethod).First();
+ for (var i = 0; i < repeatingData.Length; i++)
+ {
+ bitData[i] = repeatingData[i];
+ }
+
+ yield return bitData;
+ }
+ }
+
+ private static List AllValues(object?[][] parameterToPatternValues)
+ {
+ var result = new List();
+ GenerateCombinations(parameterToPatternValues, new object[parameterToPatternValues.Length], 0, result);
+ return result;
+ }
+
+ private static void GenerateCombinations(object?[][] parameterToPatternValues, object?[] current, int index,
+ List result)
+ {
+ if (index == current.Length)
+ {
+ result.Add((object[])current.Clone());
+ return;
+ }
+
+ var patternValues = parameterToPatternValues[index];
+
+ foreach (var value in patternValues)
+ {
+ current[index] = value;
+ GenerateCombinations(parameterToPatternValues, current, index + 1, result);
+ }
+ }
+}
diff --git a/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttributeTests.cs b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttributeTests.cs
new file mode 100644
index 0000000000..b23fda8657
--- /dev/null
+++ b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttributeTests.cs
@@ -0,0 +1,290 @@
+#nullable enable
+using Xunit;
+
+namespace Bit.Test.Common.AutoFixture.Attributes;
+
+public class RepeatingPatternBitAutoDataAttributeTests
+{
+ public class OneParam1 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public OneParam1(TestDataContext context)
+ {
+ context.SetData(1, [], [], []);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([])]
+ public void NoPattern_NoTestExecution(string autoDataFilled)
+ {
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class OneParam2 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public OneParam2(TestDataContext context)
+ {
+ context.SetData(2, [false, true], [], []);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([false, true])]
+ public void TrueFalsePattern_2Executions(bool first, string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedBooleans1.Remove(first));
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class OneParam3 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public OneParam3(TestDataContext context)
+ {
+ context.SetData(4, [], [], [null, "", " ", "\t"]);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([null, "", " ", "\t"])]
+ public void NullableEmptyStringPattern_4Executions(string? first, string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedStrings.Remove(first));
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class OneParam4 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public OneParam4(TestDataContext context)
+ {
+ context.SetData(6, [], [], [null, "", " ", "\t", "\n", " \t\n"]);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([null, "", " ", "\t"])] // 4 executions
+ [BitAutoData("\n")] // 1 execution
+ [BitAutoData(" \t\n", "test data")] // 1 execution
+ public void MixedPatternsWithBitAutoData_6Executions(string? first, string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedStrings.Remove(first));
+ Assert.NotEmpty(autoDataFilled);
+ if (first == " \t\n")
+ {
+ Assert.Equal("test data", autoDataFilled);
+ }
+
+ _context.TestExecuted();
+ }
+ }
+
+ public class TwoParams1 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public TwoParams1(TestDataContext context)
+ {
+ context.SetData(8, TestDataContext.GenerateData([false, true], 4), [],
+ TestDataContext.GenerateData([null, "", " ", "\t"], 2));
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([false, true], [null, "", " ", "\t"])]
+ public void TrueFalsePatternFirstNullableEmptyStringPatternSecond_8Executions(
+ bool first, string? second,
+ string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedBooleans1.Remove(first));
+ Assert.True(_context.ExpectedStrings.Remove(second));
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class TwoParams2 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public TwoParams2(TestDataContext context)
+ {
+ context.SetData(8, TestDataContext.GenerateData([false, true], 4), [],
+ TestDataContext.GenerateData([null, "", " ", "\t"], 2));
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([null, "", " ", "\t"], [false, true])]
+ public void NullableEmptyStringPatternFirstTrueFalsePatternSecond_8Executions(
+ string? first, bool second,
+ string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedStrings.Remove(first));
+ Assert.True(_context.ExpectedBooleans1.Remove(second));
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class TwoParams3 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public TwoParams3(TestDataContext context)
+ {
+ var expectedBooleans1 = TestDataContext.GenerateData([false], 4);
+ expectedBooleans1.AddRange(TestDataContext.GenerateData([true], 5));
+ var expectedStrings = TestDataContext.GenerateData([null, "", " "], 2);
+ expectedStrings.AddRange(["\t", "\n", " \t\n"]);
+ context.SetData(9, expectedBooleans1, [], expectedStrings);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([null, "", " "], [false, true])] // 6 executions
+ [RepeatingPatternBitAutoData(["\t"], [false])] // 1 execution
+ [BitAutoData("\n", true)] // 1 execution
+ [BitAutoData(" \t\n", true, "test data")] // 1 execution
+ public void MixedPatternsWithBitAutoData_9Executions(
+ string? first, bool second,
+ string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedStrings.Remove(first));
+ Assert.True(_context.ExpectedBooleans1.Remove(second));
+ Assert.NotEmpty(autoDataFilled);
+ if (first == " \t\n")
+ {
+ Assert.Equal("test data", autoDataFilled);
+ }
+
+ _context.TestExecuted();
+ }
+ }
+
+ public class ThreeParams1 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public ThreeParams1(TestDataContext context)
+ {
+ context.SetData(16, TestDataContext.GenerateData([false, true], 8),
+ TestDataContext.GenerateData([false, true], 8),
+ TestDataContext.GenerateData([null, "", " ", "\t"], 4));
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([false, true], [null, "", " ", "\t"], [false, true])]
+ public void TrueFalsePatternFirstNullableEmptyStringPatternSecondFalsePatternThird_16Executions(
+ bool first, string? second, bool third,
+ string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedBooleans1.Remove(first));
+ Assert.True(_context.ExpectedStrings.Remove(second));
+ Assert.True(_context.ExpectedBooleans2.Remove(third));
+ Assert.NotEmpty(autoDataFilled);
+ _context.TestExecuted();
+ }
+ }
+
+ public class ThreeParams2 : IClassFixture
+ {
+ private readonly TestDataContext _context;
+
+ public ThreeParams2(TestDataContext context)
+ {
+ var expectedBooleans1 = TestDataContext.GenerateData([false, true], 6);
+ expectedBooleans1.AddRange(TestDataContext.GenerateData([true], 3));
+ var expectedBooleans2 = TestDataContext.GenerateData([false, true], 7);
+ expectedBooleans2.Add(true);
+ var expectedStrings = TestDataContext.GenerateData([null, "", " "], 4);
+ expectedStrings.AddRange(["\t", "\t", " \t\n"]);
+ context.SetData(15, expectedBooleans1, expectedBooleans2, expectedStrings);
+ _context = context;
+ }
+
+ [Theory]
+ [RepeatingPatternBitAutoData([false, true], [null, "", " "], [false, true])] // 12 executions
+ [RepeatingPatternBitAutoData([true], ["\t"], [false, true])] // 2 executions
+ [BitAutoData(true, " \t\n", true, "test data")] // 1 execution
+ public void MixedPatternsWithBitAutoData_15Executions(
+ bool first, string? second, bool third,
+ string autoDataFilled)
+ {
+ Assert.True(_context.ExpectedBooleans1.Remove(first));
+ Assert.True(_context.ExpectedStrings.Remove(second));
+ Assert.True(_context.ExpectedBooleans2.Remove(third));
+ Assert.NotEmpty(autoDataFilled);
+ if (second == " \t\n")
+ {
+ Assert.Equal("test data", autoDataFilled);
+ }
+
+ _context.TestExecuted();
+ }
+ }
+}
+
+public class TestDataContext : IDisposable
+{
+ internal List ExpectedBooleans1 = [];
+ internal List ExpectedBooleans2 = [];
+
+ internal List ExpectedStrings = [];
+
+ private int _expectedExecutionCount;
+ private bool _dataSet;
+
+ public void TestExecuted()
+ {
+ _expectedExecutionCount--;
+ }
+
+ public void SetData(int expectedExecutionCount, List expectedBooleans1, List expectedBooleans2,
+ List expectedStrings)
+ {
+ if (_dataSet)
+ {
+ return;
+ }
+
+ _expectedExecutionCount = expectedExecutionCount;
+ ExpectedBooleans1 = expectedBooleans1;
+ ExpectedBooleans2 = expectedBooleans2;
+ ExpectedStrings = expectedStrings;
+
+ _dataSet = true;
+ }
+
+ public static List GenerateData(List list, int count)
+ {
+ var repeatedList = new List();
+ for (var i = 0; i < count; i++)
+ {
+ repeatedList.AddRange(list);
+ }
+
+ return repeatedList;
+ }
+
+ public void Dispose()
+ {
+ Assert.Equal(0, _expectedExecutionCount);
+ Assert.Empty(ExpectedBooleans1);
+ Assert.Empty(ExpectedBooleans2);
+ Assert.Empty(ExpectedStrings);
+ }
+}