-
-
Notifications
You must be signed in to change notification settings - Fork 13
Refactor Result and Exception classes. #192
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
Refactor Result and Exception classes. #192
Conversation
dfc40cf to
6cc6c0c
Compare
| * If the comparison succeeds with [[Result.Success]] then no report is printed. | ||
| * If the comparison fails with [[Result.Failure]], then the report is printed | ||
| * with the test failure. | ||
| * If the comparison succeeds with [[Comparison.Result.Success]] then no report |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This scaladoc was incorrect.
| extends WeaverTestException(message, None) { | ||
| private[weaver] def withLocation( | ||
| location: SourceLocation): AssertionException = | ||
| new AssertionException(message, locations.append(location)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that AssertionException isn't meant to be part of the public API.
If users want to create their own AssertionException, they can do so using the failure expectation and fail using failFast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think we need to put more thought into it : userland should have the ability to recover from throwables resulting from failed assertions to create some combinators for things like "I want this test to eventually pass".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does need more thought. The AssertionException type is also part of the public API in Expectations:
case class Expectations(run: ValidatedNel[AssertionException, Unit])What do you think about making the type public, but the message and locations fields private?
Here's an example of a userland retry function using a public AssertionException type:
def retryThreeTimes(expectationIO: IO[Unit]): IO[Expectations] = {
fs2.Stream
.repeatEval(expectationIO.attemptNarrow[AssertionException])
.takeWhile({
case Left(_) => true
case Right(_) => false
},
takeFailure = true
) // Repeat until the assertion is successful
.take(3) // Repeat at most three times
.scan(success)({
case (_, Right(_)) =>
success
// If an expectation succeeds, the retry operation is successful
case (failedExpectations, Left(assertionException)) =>
failedExpectations.and(Expectations(Validated.invalidNel(assertionException)))
// Accumulate the failures into a single expectation
})
.compile
.lastOrError
}The gist of it is the ability to pattern match on AssertionException as a type, but not using unapply, and reconstruct the failed expectations with Expectations(Validated.invalidNel(assertionException)).
We could expose message and locations too, but I'm not convinced they would be helpful for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about making the type public, but the message and locations fields private?
Agreed. While we're at it making breaking changes, we may as well rename it, I was never really happy with AssertionException.
We could expose message and locations too, but I'm not convinced they would be helpful for users.
Neither am I.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have any suggestions for the new name?
We're using the term assertion and expectations interchangably in the docs and internals, however the public API now only refers to Expectations and expect. We could prefer the term expectation instead:
ExpectationFailedExceptionExpectationFailedExpectationException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExpectationFailed sounds good to me
| .fold(className)(m => s"$className: $m") | ||
| } | ||
|
|
||
| val maxStackFrames = sys.props.get("WEAVER_MAX_STACKFRAMES").flatMap(s => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we removing this altogether ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WEAVER_MAX_STACKFRAMES is alse used in the Formatter, although I haven't checked if the code path using it is actually hit.
We're removing maxStackFrames because the Exception class never has a defined location, and so the stackTraceLimit would always be None.
If you're reviewing on GitHub, this is easier to see in the split diff view.
| } | ||
| } | ||
|
|
||
| final case class Failure( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| } | ||
|
|
||
| private def indent( | ||
| message: String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indent function has been removed, and its usages have been replaced with formatDescription.
The functions were practically identical. The only difference was the treatment of the prefix.
indentwould assume the prefix was whitespace, and so not add it for empty lines.formatDescriptionwould always add the prefix
This change rewrites formatDescription to test if the prefix is whitespace and line is empty, and if so not add the prefix.
| extends Result { | ||
|
|
||
| def formatted: Option[String] = | ||
| Some(formatError(msg, source, location, Some(0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I walked through the code for formatError for this case and found it was exactly the same as formatDescription.
I think we can reserve formatError for the Exceptioncase, where were have a stack trace we want to render.
| } | ||
|
|
||
| final case class Cancelled(reason: Option[String], location: SourceLocation) | ||
| final case class Cancelled(reason: String, location: SourceLocation) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm itching to rename this to Canceled to align with cats-effect, but will leave that to a follow-up PR. There are several other datatypes which have Cancelled vs Canceled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In all frankness this is just mirroring the SBT test interface, but I don't think there's a meaningful difference beween ignored and canceled, so you could just remove it and dodge the question of which spelling should be applied 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading the test interface docs it seems that each test framework has their own interpretation. I think we could remove the cancel function, and the idea of cancelled tests, but would rather leave that to a separate PR.
EDIT: On second thought, it may be best to do it in this PR. That way, we'd ensure it was in the same minor version bump.
957e22e to
65a1a2d
Compare
| /** | ||
| * Raises an error that leads to the running test being tagged as "ignored" | ||
| */ | ||
| def ignore(reason: String)(implicit pos: SourceLocation): F[Nothing] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about moving this to Expectations.Helpers, or some other object?
Currently, users can only call ignore from within a suite, but they may want to do so in a helper function. For example:
object EnvironmentHelpers {
import Expectations.Helpers.* // or some other object
def ignoreInDev: IO[Unit] = ignore("Skipping test in DEV environment").whenA(isDev)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup I'm fine with it
Baccata
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job 👍
Feel free to move the ignore after your suggestion and consider this approval still valid
b944cc8 to
204a577
Compare
This PR refactors the
Resultdatatype to make it easier to correlate it to different test scenarios.The
Resultdatatype is now one of:Success, meaning the test passed.Failures, corresponding to invalid expect statements.Exception, meaning an exception was thrown by user code.Ignored, meaning the test was ignored.OnlyTagNotAllowedInCI, meaning the.onlytag was used in CI.FailuresvsFailureInvalid expectations are equivalent to
Failures. Each individualFailurecorresponds to anexpect,expect.same, orexpect.eqlstatement. For example,expect(1 == 2) and expect.same(3, 4)would result in a non-empty list of twoFailurevalues.The
Failureitself isn't a subclass ofResult, since it only ever appears as part of a collection ofFailures.OnlyTagNotAllowedInCIThere is a specific class for
OnlyTagNotAllowedInCI. It was previously the onlyFailurecreated from something other than expectations, so deserves its own class. Unlike failures originating from expectations, it doesn't have an underlying exception.Removal of
WeaverExceptionI assume that noone is using the abstract
WeaverExceptionclass.This has been removed, along with the
OurExceptionextractors and pattern match for creatingResult.Exception.Result.Exceptionnow only corresponds to an exception thrown by the user.IgnoredExceptionmade privateThe
IgnoredExceptionis raised byignore. It shouldn't be raised by users. By making it private, we can also make thereasonfield non-optional and remove thecausefield.cancelfunction removedThe
cancelfunction has been removed in favour ofignore. They both achieve the same effect of skipping the test without marking it as a success or failure.Binary compatibility
This PR will break binary compatibility. Before merging, we should make the
Resulttype private such that this won't happen again.