-1

I'm trying to test that my custom IProgress is doing what I expect it to do: "Report" the current status after every second.

So I have some code but it's always failing. But this is the kicker - it's only failing when I have hardly any code in the test case.

If I add in one more piece of code to the test (which is commented out in the repo) .. it works if I uncomment that.

It's like .. something needs to get fired off .. but it hasn't completed before the "assertions" are checked.

Full fiddle: https://dotnetfiddle.net/qXHzlx

Here's the code:

// Arrange.
var reportValue = 0;
var progress = new TimerProgress<int>((int value) => reportValue = value);

await Task.Delay(TimeSpan.FromSeconds(2)); // Need to wait more than 1 second for the report to work.

// Act.
progress.Report(5);

// Assert.
reportValue.ShouldBe(5);

and the custom TimerProgress :

public class TimerProgress<T> : IProgress<T>
{
    private readonly TimeSpan _intervalInMilliseconds;
    private readonly IProgress<T> _progress;
    private DateTime? _lastReportedOn;

    public TimerProgress(Action<T> handler, double intervalInMillieconds = 1000)
    {
        if (intervalInMillieconds <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(intervalInMillieconds));
        }

        _intervalInMilliseconds = TimeSpan.FromMilliseconds(intervalInMillieconds);

        _progress = new Progress<T>(handler);

        _lastReportedOn = DateTime.UtcNow;
    }

    public void Report(T value)
    {
        var now = DateTime.UtcNow;

        if (now - _lastReportedOn > _intervalInMilliseconds)
        {
            // We're due for a report!
            _progress.Report(value);
        }

        _lastReportedOn = now;
    }
}

With the test case, I expect:

  • new TimeProgress instance. Now is "remembered".
  • Wait 2 seconds (you'll see why in a tick)
  • "Report". This now checks to see if remembered-now is over 1 second ago. It should be because we've waited for 2 seconds! So now, we should "report"
  • Handler gets called .. which 'remembers' the number 5
  • we now assert that '5' was ended up getting reported.

So I keep getting a failure assertion saying the value is 0, not 5.

But when I add one more line of code to the test, then the value is now 5.

Is this about a timing or event firing issue?

Is this about how my handler is trying to update a variable outside of the handler, which is Bad™️ ?

Edit

Suggestions from people are saying that the Progress<T> actually uses a sync context under the hood and this is the issue?

halfer
  • 19,824
  • 17
  • 99
  • 186
Pure.Krome
  • 84,693
  • 113
  • 396
  • 647
  • 2
    It wasn't me but you've not included any code and while having the fiddle is good, we don't necessarily want to click another link. Just paste your code in the question. – Jeremy Thompson Mar 07 '23 at 04:32
  • It appears to be related to the use of `Progress`. If you call the delegate `handler` directly, it immediately sets it. – ProgrammingLlama Mar 07 '23 at 04:42
  • Your reporting is not done asynchronously. It is done after the 2 second wait. A real asynchronous application the report would be sent during the 2 second wait. – jdweng Mar 07 '23 at 04:45
  • Updated OP with more info/data/research – Pure.Krome Mar 07 '23 at 04:59
  • 1
    The handler is called and the variable is updated, but it's called after `reportValue.ShouldBe(5);` is evaluated. The docs for `Progress` say: "*If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.*" I'm guessing this means `.Report` schedules it for execution in another thread, and that it returns without waiting for it to happen. – ikegami Mar 07 '23 at 05:11
  • Yep - that's what i'm thinking, also @ikegami . it's it's not easy?? to define my own context? – Pure.Krome Mar 07 '23 at 05:11

1 Answers1

-1

This SO question has a great conversation about this issue, already.

It is because the progress runs on a SynchronizationContext. So they suggested not to use the Progress<T> inside my own class. Instead, just call the Action<T> directly which means it will happen in your current context.

e.g. (stollen code)

public class SynchronousProgress<T> : IProgress<T>
{
    private readonly Action<T> action;

    public SynchronousProgress(Action<T> action)
    {
        this.action = action;
    }

    public void Report(T value)
    {
        action(value);
    }
}
Pure.Krome
  • 84,693
  • 113
  • 396
  • 647