How to check the parameters without enumerating infinite source in c# extension method

89 Views Asked by At

I'm trying to write an NUnit test to check that my extension method throws an exception when called with a negative parameter, what i want is to execute the first two if condition to check the parameters without enumerating the soruce "infiniteSeq", not even partially.

I want this test to have success without using a toList or something to force the enumeration.

Assert.That(()=> infiniteSeq.Smooth(-1), Throws.TypeOf<ArgumentOutOfRangeException>());

This is my extension method, it must never end, if it does then the source is finite and it throws a FiniteSourceException.

public static IEnumerable<double> Smooth(this IEnumerable<double> s, int N)
    {
        if (N < 0) throw new ArgumentOutOfRangeException(nameof(N), "Is negative");
        if (s is null) throw new ArgumentNullException(nameof(s), "is null");
        
        while (true)
        {
            List<double> buffer = new List<double>();
            using (var seqEnumerator = s.GetEnumerator())
            {
                int index = 0;
                while (seqEnumerator.MoveNext())
                {
                    buffer.Add(seqEnumerator.Current);
                        
                    //Enough items to start Smoothing
                    if (buffer.Count >= N*2)
                    {
                        List<double> elementForAvg;
                        try
                        {
                            int startIndex = (index-N<0) ? 0 : index-N;
                            int endIndex = index+N;
                            
                            elementForAvg = buffer.GetRange(startIndex,endIndex);
                        }
                        catch (Exception e)
                        {
                            if (e is ArgumentException)
                                throw new FiniteSourceException();
                            
                            throw;
                        }
                        
                        yield return AvgCalculator(elementForAvg);
                        index++;
                    }
                }
                throw new FiniteSourceException();
            }

Is this possible?

2

There are 2 best solutions below

0
Johnathan Barclay On

If you're concerned about invoking an infinite loop, you could always perform a single iteration which will execute the if statements:

Assert.That(
    () => infiniteSeq.Smooth(-1).GetEnumerator().MoveNext(),
    Throws.TypeOf<ArgumentOutOfRangeException>());

An alternative approach would be to alter the behaviour of Smooth to throw immediately rather than after the first iteration.

You could do this by returning an iterator instead of using yield within the body of Smooth:

public static IEnumerable<double> Smooth(this IEnumerable<double> s, int N)
{
    if (N < 0) throw new ArgumentOutOfRangeException(nameof(N), "Is negative");
    if (s is null) throw new ArgumentNullException(nameof(s), "is null");
 
    return Iterator();
    
    IEnumerable<double> Iterator()
    {
        while (true)
        {
            //...
                    
            yield return AvgCalculator(elementForAvg);
         
            //...

            throw new FiniteSourceException();
        }
    }
}

In the above example, the yield keyword is used within the Iterator local method rather than Smooth, meaning Smooth is no longer an iterator and it's body will execute at the point of call rather than being deferred.

0
Steve Czetty On

In a case like this, where you want to "fail fast", before any enumeration has been executed, you can break the method up into two, using an inner function or lambda. Something like this:

public static IEnumerable<double> Smooth(this IEnumerable<double> s, int N)
{
    if (N < 0) throw new ArgumentOutOfRangeException(nameof(N), "Is negative");
    if (s is null) throw new ArgumentNullException(nameof(s), "is null");
    return Smooth();

    IEnumerable<double> Smooth() 
    {
       while (true)
       {
            // ...
            yield return s.MoveNext();
       }
    }
}

The net effect will be that the arguments will be checked before the enumeration begins, throwing an exception at the time the call is made. The original way will cause the validation exceptions to be thrown from where the return value is first enumerated, potentially causing strange errors in unexpected locations.