How can we run a test method with multiple parameters in MSTest?
Asked Answered
A

11

187

NUnit has a feature called Values, like below:

[Test]
public void MyTest(
    [Values(1,2,3)] int x,
    [Values("A","B")] string s)
{
    // ...
}

This means that the test method will run six times:

MyTest(1, "A")
MyTest(1, "B")
MyTest(2, "A")
MyTest(2, "B")
MyTest(3, "A")
MyTest(3, "B")

We're using MSTest now, but is there any equivalent for this so that I can run the same test with multiple parameters?

[TestMethod]
public void Mytest()
{
    // ...
}
Abstruse answered 26/1, 2012 at 16:50 Comment(6)
You can use MSTestHacks, as described in https://mcmap.net/q/137220/-how-to-rowtest-with-mstest answer.Overrule
Possible duplicate of How to RowTest with MSTest?Overrule
@MichaelFreidgeim This question has better answers than your suggested targetRedhot
@Rob: IMHO, the most appropriate answer -MSTestHacks -How to RowTest with MSTest? is missing in this question.Overrule
@MichaelFreidgeim Perhaps, though it appears that the functionality has existed for 3 1/2 years now (#9022381)Redhot
@Rob: only available within the unit testing project for WinRT/Metro. See visualstudio.uservoice.com/forums/330519-team-services/…Overrule
G
49

It is unfortunately not supported in older versions of MSTest. Apparently there is an extensibility model and you can implement it yourself. Another option would be to use data-driven tests.

My personal opinion would be to just stick with NUnit though...

As of Visual Studio 2012, update 1, MSTest has a similar feature. See McAden's answer.

Gaut answered 26/1, 2012 at 16:55 Comment(4)
We're using Selenium which generates NUnit code so we switched to use NUnit instead :)Abstruse
I've found that something similar is now possible in Visual Studio 2012 Update 1, just FYI for future consideration of anybody looking at this answer.Eurythmics
@Eurythmics do you have a link with an explanation?Gaut
I gave an answer below with an example and a link to my blog post. It mentions the attributes necessary and also the "DisplayName" property on the attribute that distinguishes the cases in the Test Explorer. It was also mentioned in the October announcment of the CTP (which now has the official release) blogs.msdn.com/b/visualstudioalm/archive/2012/10/26/… I've added the information to this SO question because I spent quite a bit of time looking for it. Hopefully this will save somebody some time.Eurythmics
E
212

EDIT 4: Looks like this is completed in MSTest V2 June 17, 2016: https://blogs.msdn.microsoft.com/visualstudioalm/2016/06/17/taking-the-mstest-framework-forward-with-mstest-v2/

Original Answer:

As of about a week ago in Visual Studio 2012 Update 1 something similar is now possible:

[DataTestMethod]
[DataRow(12,3,4)]
[DataRow(12,2,6)]
[DataRow(12,4,3)]
public void DivideTest(int n, int d, int q)
{
  Assert.AreEqual( q, n / d );
}

EDIT: It appears this is only available within the unit testing project for WinRT/Metro. Bummer

EDIT 2: The following is the metadata found using "Go To Definition" within Visual Studio:

#region Assembly Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll, v11.0.0.0
// C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll
#endregion

using System;

namespace Microsoft.VisualStudio.TestPlatform.UnitTestFramework
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DataTestMethodAttribute : TestMethodAttribute
    {
        public DataTestMethodAttribute();

        public override TestResult[] Execute(ITestMethod testMethod);
    }
}

EDIT 3: This issue was brought up in Visual Studio's UserVoice forums. Last Update states:

STARTED · Visual Studio Team ADMIN Visual Studio Team (Product Team, Microsoft Visual Studio) responded · April 25, 2016 Thank you for the feedback. We have started working on this.

Pratap Lakshman Visual Studio

https://visualstudio.uservoice.com/forums/330519-team-services/suggestions/3865310-allow-use-of-datatestmethod-datarow-in-all-unit

Eurythmics answered 4/12, 2012 at 19:51 Comment(11)
Windows Phone is now supported also, with Visual Studio 2012 Update 2 (currently, CTP 4)Inboard
I have update 1 but DataTestMethod and DataRow are not recognised, which library are these attributes in?Cardiology
What type of project are you testing?Eurythmics
Is there any official source about DataTestMethod? What namespace is it in, which assembly?Underwriter
I found that the UnitTestFramework.dll was installed on my computer and after manually referencing it I was able to write a method using the [DataTestMethod] attribute with data rows but I cannot get the Test Explorer in Visual Studio 2012.3 to find the method.Charry
In a non windows-store project?Eurythmics
I went to the file path "C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll" on my computer and the file was there. So I referenced it in my basic unit test project. Opening the dll in JustDecompile shows that the library only has references to mscorlib, System, and System.Core. It's not a Windows Store project.Charry
I've only been able to get it to work on Windows Store projects.Eurythmics
Can you specify expected result in DataRow?Fertilize
The only way I'm aware of is to make it one of the arguments just like the example I gave in the answer.Eurythmics
I am using VS2017 and I found DataTestMethod in a nuget package I unfortunately can't remember the name of. What I did was to write [DataTestMethod] and then press ctrl-. to let VS install the nuget package. I then had to remove the old testing lib as there is a name clash. FWIW.Site
R
73

This feature is in pre-release now and works with Visual Studio 2015.

For example:

[TestClass]
public class UnitTest1
{
    [TestMethod]
    [DataRow(1, 2, 2)]
    [DataRow(2, 3, 5)]
    [DataRow(3, 5, 8)]
    public void AdditionTest(int a, int b, int result)
    {
        Assert.AreEqual(result, a + b);
    }
}
Rollerskate answered 17/6, 2016 at 14:27 Comment(6)
This is the correct answer. Note that it is not necessary to say [DataTestMethod] to use [DataRow] (https://mcmap.net/q/137221/-datatestmethod-vs-testmethod)Shotwell
This is the correct answer which shall be accepted! Really works for me! Thanks!Conceptualism
[DataRow(1, 2, 3)] ??Arleanarlee
@Arleanarlee presumably the arguments for a, b, and result respectively.Chev
I'm saying the first set of test data is wrong. I think it should be: [DataRow(1, 2, 3)]Arleanarlee
After I add the dll in your reference point, the dll conflict with my project reference Microsoft.VisualStudio.QulityTools.UnitTestFrameWrok, I remove it. And DataRow is accepted, but my Tests Explorer can't find the Tests.Ithaca
G
49

It is unfortunately not supported in older versions of MSTest. Apparently there is an extensibility model and you can implement it yourself. Another option would be to use data-driven tests.

My personal opinion would be to just stick with NUnit though...

As of Visual Studio 2012, update 1, MSTest has a similar feature. See McAden's answer.

Gaut answered 26/1, 2012 at 16:55 Comment(4)
We're using Selenium which generates NUnit code so we switched to use NUnit instead :)Abstruse
I've found that something similar is now possible in Visual Studio 2012 Update 1, just FYI for future consideration of anybody looking at this answer.Eurythmics
@Eurythmics do you have a link with an explanation?Gaut
I gave an answer below with an example and a link to my blog post. It mentions the attributes necessary and also the "DisplayName" property on the attribute that distinguishes the cases in the Test Explorer. It was also mentioned in the October announcment of the CTP (which now has the official release) blogs.msdn.com/b/visualstudioalm/archive/2012/10/26/… I've added the information to this SO question because I spent quite a bit of time looking for it. Hopefully this will save somebody some time.Eurythmics
J
13

Not exactly the same as NUnit's Value (or TestCase) attributes, but MSTest has the DataSource attribute, which allows you to do a similar thing.

You can hook it up to database or XML file - it is not as straightforward as NUnit's feature, but it does the job.

Jeepers answered 26/1, 2012 at 16:58 Comment(0)
G
7

MSTest has a powerful attribute called DataSource. Using this you can perform data-driven tests as you asked. You can have your test data in XML, CSV, or in a database. Here are few links that will guide you

Gadget answered 27/1, 2012 at 5:15 Comment(0)
R
5

It's very simple to implement - you should use TestContext property and TestPropertyAttribute.

Example

public TestContext TestContext { get; set; }
private List<string> GetProperties()
{
    return TestContext.Properties
        .Cast<KeyValuePair<string, object>>()
        .Where(_ => _.Key.StartsWith("par"))
        .Select(_ => _.Value as string)
        .ToList();
}

//usage
[TestMethod]
[TestProperty("par1", "http://getbootstrap.com/components/")]
[TestProperty("par2", "http://www.wsj.com/europe")]
public void SomeTest()
{
    var pars = GetProperties();
    //...
}

EDIT:

I prepared few extension methods to simplify access to the TestContext property and act like we have several test cases. See example with processing simple test properties here:

[TestMethod]
[TestProperty("fileName1", @".\test_file1")]
[TestProperty("fileName2", @".\test_file2")]
[TestProperty("fileName3", @".\test_file3")]
public void TestMethod3()
{
    TestContext.GetMany<string>("fileName").ForEach(fileName =>
    {
        //Arrange
        var f = new FileInfo(fileName);

        //Act
        var isExists = f.Exists;

        //Asssert
        Assert.IsFalse(isExists);
    });
}

and example with creating complex test objects:

[TestMethod]
//Case 1
[TestProperty(nameof(FileDescriptor.FileVersionId), "673C9C2D-A29E-4ACC-90D4-67C52FBA84E4")]
//...
public void TestMethod2()
{
    //Arrange
    TestContext.For<FileDescriptor>().Fill(fi => fi.FileVersionId).Fill(fi => fi.Extension).Fill(fi => fi.Name).Fill(fi => fi.CreatedOn, new CultureInfo("en-US", false)).Fill(fi => fi.AccessPolicy)
        .ForEach(fileInfo =>
        {
            //Act
            var fileInfoString = fileInfo.ToString();

            //Assert
            Assert.AreEqual($"Id: {fileInfo.FileVersionId}; Ext: {fileInfo.Extension}; Name: {fileInfo.Name}; Created: {fileInfo.CreatedOn}; AccessPolicy: {fileInfo.AccessPolicy};", fileInfoString);
        });
}

Take a look to the extension methods and set of samples for more details.

Recessional answered 16/5, 2016 at 7:48 Comment(2)
This approach works but does not create individual test cases for each set of parameters.Judithjuditha
You can use something more complex as TestProperty value (ex. "0-100"), parse and handle it in the body of test.Recessional
B
5

I couldn't get The DataRowAttribute to work in Visual Studio 2015, and this is what I ended up with:

[TestClass]
public class Tests
{
    private Foo _toTest;

    [TestInitialize]
    public void Setup()
    {
        this._toTest = new Foo();
    }

    [TestMethod]
    public void ATest()
    {
        this.Perform_ATest(1, 1, 2);
        this.Setup();

        this.Perform_ATest(100, 200, 300);
        this.Setup();

        this.Perform_ATest(817001, 212, 817213);
        this.Setup();
    }

    private void Perform_ATest(int a, int b, int expected)
    {
        // Obviously this would be way more complex...

        Assert.IsTrue(this._toTest.Add(a,b) == expected);
    }
}

public class Foo
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

The real solution here is to just use NUnit (unless you're stuck in MSTest like I am in this particular instance).

Battiste answered 27/10, 2016 at 16:36 Comment(2)
you should split each test call to a separate test in order to save you time when one of them will break. (which we all know will happen)Camellia
Yes, of course. In practice that's how it would be done. In this case I was just illustrating it for simplicityBattiste
H
4

MSTest does not support that feature, but you can implement your own attribute to achieve that.

Have a look at Enabling parameterized tests in MSTest using PostSharp.

Hickerson answered 26/1, 2012 at 16:55 Comment(0)
D
4

There is, of course, another way to do this which has not been discussed in this thread, i.e. by way of inheritance of the class containing the TestMethod. In the following example, only one TestMethod has been defined but two test cases have been made.

In Visual Studio 2012, it creates two tests in the TestExplorer:

  1. DemoTest_B10_A5.test
  2. DemoTest_A12_B4.test

    public class Demo
    {
        int a, b;
    
        public Demo(int _a, int _b)
        {
            this.a = _a;
            this.b = _b;
        }
    
        public int Sum()
        {
            return this.a + this.b;
        }
    }
    
    public abstract class DemoTestBase
    {
        Demo objUnderTest;
        int expectedSum;
    
        public DemoTestBase(int _a, int _b, int _expectedSum)
        {
            objUnderTest = new Demo(_a, _b);
            this.expectedSum = _expectedSum;
        }
    
        [TestMethod]
        public void test()
        {
            Assert.AreEqual(this.expectedSum, this.objUnderTest.Sum());
        }
    }
    
    [TestClass]
    public class DemoTest_A12_B4 : DemoTestBase
    {
        public DemoTest_A12_B4() : base(12, 4, 16) { }
    }
    
    public abstract class DemoTest_B10_Base : DemoTestBase
    {
        public DemoTest_B10_Base(int _a) : base(_a, 10, _a + 10) { }
    }
    
    [TestClass]
    public class DemoTest_B10_A5 : DemoTest_B10_Base
    {
        public DemoTest_B10_A5() : base(5) { }
    }
    
Deepseated answered 25/3, 2014 at 17:13 Comment(0)
K
1

The OP example was about an NUnit feature that easily allows to have a cartesian product of provided values. As far as I was able to tell, no answer here covered that part. I saw this as a little challenge and ended up with the following implementation.

[Edit: Array based refactoring + Zip values]

I did some refactoring to the original Enumerator based version (see post history) to now use only Arrays and loop throuch indices instead. I also took the opporunity to add a new Zip type of values that will match a different value to every set created by the cartesian produce. This may be useful to add an ExpectedResult for instance.

It is still not really optimized so feel free to suggest improvements.

#nullable enable
public enum ValuesType
{
    Undefined = 0,
    Cartesian = 1,
    /// <summary>
    /// Values will be <see cref="Enumerable.Zip{TFirst, TSecond, TResult}(IEnumerable{TFirst}, IEnumerable{TSecond}, Func{TFirst, TSecond, TResult})">Zipped</see> with the cartesian produce of the other parameters.
    /// </summary>
    Zip = 2
}

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public ValuesType ValuesType { get; }
    public object[] Values { get; }

    public ValuesAttribute(params object[] values)
        : this(ValuesType.Cartesian, values)
    { }

    public ValuesAttribute(ValuesType valuesType, params object[] values)
    {
        ValuesType = valuesType;
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValuesDataSourceAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object[]> GetData(MethodInfo methodInfo)
    {
        var parameters = methodInfo.GetParameters();
        var values = new (ValuesType Type, object[] Values, int Index)[parameters.Length];
        for(var i=0; i < parameters.Length; i++)
        {
            var parameter = parameters[i];
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();
            if (attribute != null)
            {
                if (attribute.Values.Any(v => !parameter.ParameterType.IsAssignableFrom(v.GetType())))
                    throw new InvalidOperationException($"All values of {nameof(ValuesAttribute)} must be of type {parameter.ParameterType.Name}. ParameterName: {parameter.Name}.");

                switch (attribute.ValuesType)
                {
                    case ValuesType.Cartesian:
                        values[i] = (ValuesType.Cartesian, attribute.Values, 0);
                        break;
                    case ValuesType.Zip:
                        values[i] = (ValuesType.Zip, attribute.Values, 0);
                        break;
                }
            }
            else if (parameter.ParameterType == typeof(bool))
                values[i] = (ValuesType.Cartesian, new object[] { false, true }, 0);
            else if (parameter.ParameterType.IsEnum)
                values[i] = (ValuesType.Cartesian, Enum.GetValues(parameter.ParameterType).Cast<Object>().ToArray(), 0);
            else
                throw new InvalidOperationException($"All parameters must have either {nameof(ValuesAttribute)} attached or be a bool or an Enum . ParameterName: {parameter.Name}.");
        }

        //Since we are using ValueTuples, it is essential that once we created our collection, we stick to it. If we were to create a new one, we would end up with a copy of the ValueTuples that won't be synced anymore.
        var cartesianTotalCount = values.Where(v => v.Type == ValuesType.Cartesian).Aggregate(1, (actualTotal, currentValues) => actualTotal * currentValues.Values.Length);
        if (values.Any(v => v.Type == ValuesType.Zip && v.Values.Length != cartesianTotalCount))
            throw new InvalidOperationException($"{nameof(ValuesType.Zip)} typed attributes must have as many values as the produce of all the others ({cartesianTotalCount}).");

        bool doIncrement;
        for(var globalPosition = 0; globalPosition < cartesianTotalCount; globalPosition++)
        {
            yield return values.Select(v => v.Values[v.Index]).ToArray();
            doIncrement = true;
            for (var i = values.Length - 1; i >= 0 && doIncrement; i--)
            {
                switch (values[i].Type)
                {
                    case ValuesType.Zip:
                        values[i].Index++;
                        break;
                    case ValuesType.Cartesian:
                        if (doIncrement && ++values[i].Index >= values[i].Values.Length)
                            values[i].Index = 0;
                        else
                            doIncrement = false;
                        break;
                    default:
                        throw new InvalidOperationException($"{values[i].Type} is not supported.");
                }
            }
        }
    }

    public string GetDisplayName(MethodInfo methodInfo, object[] data)
    {
        return data.JoinStrings(" / ");
    }
}

Usage:

[TestMethod]
[ValuesDataSource]
public void Test([Values("a1", "a2")] string a, [Values(1, 2)] int b, bool c, System.ConsoleModifiers d, [Values(ValuesType.Zip, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)] int asserts)
{
    //Arrange / Act / Assert
    //Cases would be
    // a1, 1, false, System.ConsoleModifiers.Alt, 1
    // a1, 1, false, System.ConsoleModifiers.Shift, 2
    // a1, 1, false, System.ConsoleModifiers.Control, 3
    // a1, 1, true, System.ConsoleModifiers.Alt, 4
    // a1, 1, true, System.ConsoleModifiers.Shift, 5
    // a1, 1, true, System.ConsoleModifiers.Control, 6
    // a1, 2, false, System.ConsoleModifiers.Alt, 7
    // a1, 2, false, System.ConsoleModifiers.Shift, 8
    // a1, 2, false, System.ConsoleModifiers.Control, 9
    // a1, 2, true, System.ConsoleModifiers.Alt, 10
    // a1, 2, true, System.ConsoleModifiers.Shift, 11
    // a1, 2, true, System.ConsoleModifiers.Control, 12
    // a2, 1, false, System.ConsoleModifiers.Alt, 13
    // a2, 1, false, System.ConsoleModifiers.Shift, 14
    // a2, 1, false, System.ConsoleModifiers.Control, 15
    // a2, 1, true, System.ConsoleModifiers.Alt, 16
    // a2, 1, true, System.ConsoleModifiers.Shift, 17
    // a2, 1, true, System.ConsoleModifiers.Control, 18
    // a2, 2, false, System.ConsoleModifiers.Alt, 19
    // a2, 2, false, System.ConsoleModifiers.Shift, 20
    // a2, 2, false, System.ConsoleModifiers.Control, 21
    // a2, 2, true, System.ConsoleModifiers.Alt, 22
    // a2, 2, true, System.ConsoleModifiers.Shift, 23
    // a2, 2, true, System.ConsoleModifiers.Control, 24
}
Kosher answered 29/9, 2023 at 23:37 Comment(0)
N
0

Here is a reimplentation of NUnits [Combinatorial], [Sequential] and [Values].

Unlike NUnit, which assumes [Combinatorial] by default, in MSTest we must always specify which one we want, else the [Values] attribute will not have any effect.

Usage:

[TestClass]
public class TestMethods
{
    [TestMethod, Combinatorial]
    public void EnumIterationTestMethod(Season season) => Console.WriteLine(season);

    [TestMethod, Combinatorial]
    public void BoolIterationTestMethod(bool boolean) => Console.WriteLine(boolean);

    [TestMethod, Combinatorial]
    public void CombinatoralValuesIterationTestMethod(Season season, bool boolean) => Console.WriteLine($"{season} {boolean}");

    [TestMethod, Sequential]
    public void SequentialCombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");

    [TestMethod, Combinatorial]
    public void CombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");
}

Code:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public object?[] Values { get; }

    public ValuesAttribute(params object?[] values)
    {
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CombinatorialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.CreateCombinations(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SequentialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.ZipLongestFillWithNull(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

public static class Utils
{
    public static List<List<object?>> GetPossibleValuesForEachParameter(MethodInfo methodInfo)
    {
        List<List<object?>> values = new();

        foreach (var parameter in methodInfo.GetParameters())
        {
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();

            if (attribute == null || attribute.Values.Length == 0)
            {
                if (parameter.ParameterType.IsEnum)
                {
                    values.Add(Enum.GetValues(parameter.ParameterType).Cast<object?>().ToList());
                    continue;
                }

                if (parameter.ParameterType == typeof(bool))
                {
                    values.Add(new List<object?> { true, false });
                    continue;
                }

                if (attribute == null)
                {
                    throw new InvalidOperationException($"{parameter.Name} should have a [Values(...)] attribute set");
                }
                else
                {
                    throw new InvalidOperationException($"[Values] {parameter.ParameterType} {parameter.Name} is only valid for Enum or Boolean types. Consider using the attribute constructor [Values(...)].");
                }
            }

            values.Add(attribute.Values.ToList());
        }

        return values;
    }

    public static IEnumerable<object?[]> ZipLongestFillWithNull(List<List<object?>> values)
    {
        var longest = values.Max(e => e.Count);

        foreach (var list in values)
        {
            if (list.Count < longest)
            {
                var diff = longest - list.Count;
                list.AddRange(Enumerable.Repeat<object?>(null, diff));
            }
        }

        for (int i = 0; i < longest; i++)
        {
            yield return values.Select(e => e[i]).ToArray();
        }
    }

    public static IEnumerable<object?[]> CreateCombinations(List<List<object?>> values)
    {
        var indices = new int[values.Count];

        while (true)
        {
            // Create new arguments
            var arg = new object?[indices.Length];
            for (int i = 0; i < indices.Length; i++)
            {
                arg[i] = values[i][indices[i]];
            }

            yield return arg!;

            // Increment indices
            for (int i = indices.Length - 1; i >= 0; i--)
            {
                indices[i]++;
                if (indices[i] >= values[i].Count)
                {
                    indices[i] = 0;

                    if (i == 0)
                        yield break;
                }
                else
                    break;
            }
        }
    }
}
Norri answered 6/11, 2023 at 6:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.