Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExecutionTimeAssertions works on cases where the code deadlocks, test… #842

Merged
merged 9 commits into from
May 24, 2018

Conversation

peterekepeter
Copy link
Contributor

@peterekepeter peterekepeter commented May 2, 2018

ExecutionTimeAssertions works on cases where the code deadlocks, tests finish as soon as conditions are met or failed instead of running indefinitely.

I implemented the feature where you can have test cases can run indefinitely or deadlock. The assertions will only wait until necessary for them to determine if test passes or fails, and they continue on. The only side effect is that if the thread runs indefinitely or deadlocks then the thread will stay hanging, but then again in these cases your test should fail.

IMPORTANT

…s finish as soon as conditions are met or failed instead of running indefinitely.
@peterekepeter
Copy link
Contributor Author

Looks like I have more work to do...

@jnyrup
Copy link
Member

jnyrup commented May 2, 2018

I like the idea to handle deadlocks and return early, when the criteria has been meet.
Maybe I'm missing out on something, but the code looks quite complicated and involving low-level stuff like Thread.

How about this approach:
In ExecutionTime wrap the Action inside a Task and expose it in an internal property.

internal Task Task { get; }

protected ExecutionTime(Action action, string actionDescription)
{
	ActionDescription = actionDescription;
	Task = Task.Run(action);
}

Add the following method to ExecutionTime;

internal TimeSpan Execute(TimeSpan timeout)
{
    var stopwatch = Stopwatch.StartNew();
    Task.Wait(timeout);
    stopwatch.Stop();

    return stopwatch.Elapsed;
}

Change ExecutionTimeAssertions to save the ExecutionTime object as a field instead of unwrapping it;

public ExecutionTimeAssertions(ExecutionTime executionTime)
{
    this.executionTime = executionTime;
}

In the assertion method like e.g. BeLessOrEqualTo we know for how long to run the task
run and wait the Task with a maximum timeout

public void BeLessOrEqualTo(TimeSpan maxDuration, string because = "", params object[] becauseArgs)
{
    TimeSpan duration = executionTime.Execute(maxDuration);

    Execute.Assertion
        .ForCondition(duration.CompareTo(maxDuration) <= 0)
    ...
}

@peterekepeter
Copy link
Contributor Author

peterekepeter commented May 2, 2018

It seems to me you are assuming that there is only one condition can be applied to the ExectionTime. While that might be the case I can see that API is meant for multiple possible assertions on the same exection time, and I don't want to rerun the task for each of them, especially if it would end up blocking a thread or something.
But I will have to look for a Task based solution, the build failed because Thread cannot be used in one of the target platforms. I'll see if I can works something out based on your suggestion.

@peterekepeter
Copy link
Contributor Author

I think I can make this work with Task I like most of your suggestion but assertion and execution are separate concerns, I don't want to specialize the execution for each type of assertion, it would unnecessarily complicate the code. And we don't just know for how long we need to run for BeLessOrEqualTo, we actually know it for all conditions.

@peterekepeter
Copy link
Contributor Author

Ended up spending more time on this than expected, but build succeeded, verify the PR again and let me know if I should make other changes.

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you at least verify part of the message. Because if your code doesn't work, you'll still get an XUnit exception if I'm not mistaken. Same for the other tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, if my code doesnt work, the test never finishes to run, but fair enough, better to be safe

{
break;
}
Task.Delay(rate).Wait();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is safe in all environments. I've seen lockups on AppVeyor while waiting for Wait call. I would just use a Thread.Sleep instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, wanted to use that, originally and yes, .Result blocks a thread here, ill see if i can improve

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread.Sleep() is available in net45+ and netstandard2.
For netstandard1.3 and netstandard1.6 there is System.Threading.Thread.

@peterekepeter
Copy link
Contributor Author

So I think there is an even more elegant solution, the execution is already wrapped inside a Task.Run(), I just wait on it to finish with the polling rate as the timeout. If execution finishes earlier than that then the test finishes earlier as well, if not then the assertion passes or fail without needing for the execution to actually finish.


internal Exception Exception { get; private set; }

internal readonly Stopwatch Stopwatch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this should be private readonly Stopwatch stopwatch; so private and lowercase s.

{
break;
}
Task.Delay(rate).Wait();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread.Sleep() is available in net45+ and netstandard2.
For netstandard1.3 and netstandard1.6 there is System.Threading.Thread.

/// <param name="condition">Condition to check un the current elapsed time.</param>
/// <param name="result">The expected result when polling is stopped.</param>
/// <param name="stopExecution">If polling is stopped then task execution is also stopped.</param>
/// <param name="rate">The rate at which the conition is re-checked.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conition = condition

/// Checks the executing action if it satisfies a condition.
/// If the execution runs into an exeption, then this will rethrow it.
/// </summary>
/// <param name="condition">Condition to check un the current elapsed time.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

un = on

/// </summary>
/// <param name="condition">Condition to check un the current elapsed time.</param>
/// <param name="result">The expected result when polling is stopped.</param>
/// <param name="stopExecution">If polling is stopped then task execution is also stopped.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopExecution doesn't exists anymore?

/// <param name="rate">The rate at which the conition is re-checked.</param>
private void PollUntil(Func<TimeSpan, bool> condition, bool result, TimeSpan rate)
{
var forcedStop = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is forcedStop ever set to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remnants from a feature that's not possible, I originally planned to use thread and kill it if not needed anymore :)

{
break;
}
execution.Task.Wait(rate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the return value of Wait is intentionally ignored, yo could use _ = execution.Task.Wait(rate); to explicitly state the intention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hah, this is a strange thing to do :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to be explicit :)
I haven't tested it out, but this should help silence static analysers reporting unused return values, when I intentionally didn't use it.

@peterekepeter
Copy link
Contributor Author

So it looks like everything is fixed! Is there anything more I should add?

@dennisdoomen
Copy link
Member

As soon as @jnyrup approves the PR, it'll get merged.

Copy link
Member

@jnyrup jnyrup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, only one change needed.

The minor and suggestions are up to you.

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action act = () => someAction.ExecutionTime().Should().BeLessThan(100.Milliseconds());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BeLessOrEqualTo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, looks like github didn't get my change, check the source peterekepeter@8ab0e1b#diff-dd0f96bbe45e01b1feafc897a60a299dL114

@@ -133,12 +164,14 @@ public void BeCloseTo(TimeSpan expectedDuration, TimeSpan precision, string beca
{
var minimumValue = expectedDuration - precision;
var maximumValue = expectedDuration + precision;

PollUntil(condition: elapsed => elapsed.CompareTo(maximumValue) < 0, result: false, rate: maximumValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: extract to local variable as in the other Assertions.

//-----------------------------------------------------------------------------------------------------------
act
.Should().Throw<XunitException>()
.And.Message.Should().StartWith(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: use WithMessage.
E.g. WithMessage("*action should be less than 0.100s, but it required*").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, did not know about this one

/// If the execution runs into an exeption, then this will rethrow it.
/// </summary>
/// <param name="condition">Condition to check on the current elapsed time.</param>
/// <param name="result">The expected result when polling is stopped.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor:
Maybe it's just me, but result indicates a computed value to me, whereas this is a stop condition.
So maybe rename to something like stopCondition, expectedResult or?

@peterekepeter
Copy link
Contributor Author

updated, also changed all assertions from that file to use WithMessage(), it looks readable that way, and it's consistent

@jnyrup
Copy link
Member

jnyrup commented May 14, 2018

@peterekepeter I tried to poke around with the PR and noticed two things:

  • If we break out of while before the Task has completed, the stopWatch is not stopped, is that intentional?
    As executionTime.ElapsedTime is called several times afterwards I guess we could hit a case, where the failure condition and failure message are inconsistent.

  • Task already has a IsCompleted property, it seems we can use that instead of IsRunning?

No tests failed when changing these two things, so I conclude that they are either compatible or we are missing a test.
Any thoughts?

@peterekepeter
Copy link
Contributor Author

  • Yes it's intentional, I've added a test. Nothing stops the client code from calling multiple time assertions, so it should give consistent results.
  • It might seem the same, but I get tests failing. The way Tasks are handled might not always be how one might expect, so I think it's better to take the safe side with try finally and a boolean variable.

@dennisdoomen
Copy link
Member

@jnyrup I assume you're taking care of this PR, right?

@jnyrup
Copy link
Member

jnyrup commented May 18, 2018

@dennisdoomen I will. I've just been short on spare time, but hope to review the last part during the weekend.

@dennisdoomen
Copy link
Member

Don't worry. Was just checking you were not waiting for me.

@jnyrup
Copy link
Member

jnyrup commented May 20, 2018

As we can now break early from a long running action, the failure message is now a bit misleading if we broke out early.

E.g.

Action act = () => Thread.Sleep(10.Seconds());
act.ExecutionTime().Should().BeLessThan(50.Milliseconds());

Execution of the action should be less than 0.050s, but it required 0.052s and 30.7µs.

But we don't know how long it will take, we just know that it required more than 50 ms.

Maybe adjust parts of the failure message based on execution.IsRunning.
If task completed, keep the current failure message,
otherwise: adjust it to e.g. "Execution of the action should be less than 0.050s, but it did not finish after 0.052s and 30.7µs.".
I'm not sure whether to prefer "did not finish after" or "had not finished after", hopefully someone with better English skills than me can answer that.

…still running.

Refactored PollUntil to return elapsed time and wether the action is still running or not.
@peterekepeter
Copy link
Contributor Author

Okay, I've added a "more than" in case there is an early exit, and the message will contain "exactly" if it's not an early exit. This way it does not break existing tests. So you get messages like:
Execution of the action should be less than 0.100s, but it required more than 0.200s.
and in case it's not an early exit:
Execution of the action should be less than 0.100s, but it required exactly 0.200s.

@peterekepeter
Copy link
Contributor Author

I don't understand AppVeyor's error message. I did not touch the test which failed.

@jnyrup jnyrup merged commit 59bc1cd into fluentassertions:master May 24, 2018
@jnyrup
Copy link
Member

jnyrup commented May 24, 2018

@peterekepeter, thanks for the contribution to Fluent Assertions!
We appreciate the time you've put into reworking this PR to its merged shape.

@peterekepeter
Copy link
Contributor Author

@jnyrup It was a long process, but finally!

@dennisdoomen
Copy link
Member

So? Time for another release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants