DelayedConstraint fails polling properties on references which are initially null

Bug #893919 reported by GT
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
NUnit Framework
Fix Released
Medium
Charlie Poole
NUnit V2
Fix Released
Medium
Charlie Poole

Bug Description

Having seen the great progress made on bug #890129, I've been using the After() constraint even more!
(Still using v2.5.10.11092)

I noticed a quirk with how (properties of) objects which are initially null are polled.

An example:
    string statusString = null; // object starts off as null

    var worker = new BackgroundWorker();
    worker.RunWorkerCompleted += delegate { statusString = "finished"; /* object non-null after work */ };
    worker.DoWork += delegate { Thread.Sleep(TimeSpan.FromSeconds(1)); /* simulate work */ };
    worker.RunWorkerAsync();

    Assert.That(() => statusString, Has.Length.GreaterThan(0).After(3000, 100));

The assert will fail after its first poll, reporting:
         System.ArgumentNullException : Value cannot be null.
         Parameter name: actual

I assume this is because the 'statusString' object is null, so fetching the 'Length' property causes a ArgumentNullException.
However, I'd expect that the test would at least continue to try to poll even if the value is initially null, in case it gets initialised before the timeout.

A few simple workarounds exist:

Adding a prior null check to the constraint:
    Assert.That(() => statusString, Is.Not.Null.With.Length.GreaterThan(0).After(3000, 100));

Or adding another separate DelayedConstraint before the Assert which checks for non-null, like this:
    Assert.That(() => statusString, Is.Not.Null.After(3000, 100));
    Assert.That(() => statusString, Has.Length.GreaterThan(0).After(3000, 100));

Perhaps that is because each constraint must pass or timeout before the next is run. The documentation doesn't specify whether each 'After' assert is run on a separate thread or whether earlier ones block.

GT (gilesemail)
description: updated
Changed in nunitv2:
status: New → Triaged
importance: Undecided → Medium
Revision history for this message
Charlie Poole (charlie.poole) wrote :

Although the 2.6 beta 2 has a fix for use of polling with After, this particular issue hasn't come up before.

Although what you propose makes sense, it's not entirely clear how we can implement it because it's entirely possible to be testing for a null value...

   Assert.That(() => statusString, Is.Null.After(3000,100));

A silly example, but one we have to deal with.

One approach would be for DelayedConstraint to analyze all following constraints to determine if a null reference will cause a problem. This could be messy and violates the independence of composed constraints. Another might be to catch the null reference exception and ignore it during polling.

I think your first workaround is better, since it doesn't depend on knowledge of NUnit's internals. The After constraint is in fact run on a separate thread, but the main thread blocks until it is complete.

Charlie

Revision history for this message
Charlie Poole (charlie.poole) wrote :

BTW, it would be good if you could test out the behavior of your examples on the 2.6 beta, since there will be no further 2.5 releases.

Charlie

Revision history for this message
GT (gilesemail) wrote :

Thanks for the quick response.

I think your 'null test' example certainly makes good sense. I'm writing tests to check whether an event handler is called with the correct arguments from another thread within a specific time.
In some cases I also want to make sure it's *not* called, in which case the handler arguments should remain null - so I have plenty of Is.Null.After(...) tests too.

So the behaviour of that should not change. However, in the example I gave earlier, the problem isn't the object being null/not null, it's that polling properties of the object will always fail on the first poll in the case where the object is null.

Further similar problems can be found using the following examples:
    Assert.That(() => statusString.Length, Is.GreaterThan(0).After(3000, 100));
    Assert.That(() => statusString, Has.Property("Length").GreaterThan(0).After(3000, 100));

This problem has also lead to timing issues where the test passes if the event which initialises the object occurs early enough, otherwise it will fail (I only found the problem when I ran all the tests in the fixture, rather than each test individually).

Your suggestion of catching and ignoring the null reference exception seems like the right solution to me.

However, I thought of a case where this may lead to a problem where exceptions are expected after a delay, eg:
    Assert.That(() => statusString.Count, Throws.TypeOf<NullReferenceException>.After(3000, 100));

Perhaps the null reference exception should only be thrown when using a 'Throws' constraint?

(Apologies for syntax errors etc, I don't have access to Visual Studio presently)

Changed in nunitv2:
assignee: nobody → Charlie Poole (charlie.poole)
Changed in nunit-3.0:
status: New → Triaged
importance: Undecided → Medium
assignee: nobody → Charlie Poole (charlie.poole)
milestone: none → 2.9.6
Changed in nunit-3.0:
status: Triaged → In Progress
status: In Progress → Fix Committed
Changed in nunitv2:
milestone: none → 2.6.0
Changed in nunitv2:
status: Triaged → Fix Committed
Changed in nunitv2:
status: Fix Committed → Fix Released
Changed in nunit-3.0:
status: Fix Committed → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.