Hi! Would it be possible to enable power-assert's...
# power-assert
c
Hi! Would it be possible to enable power-assert's transformations, but without the last string conversion? For example, I'd like:
Copy code
fun foo(a: Boolean)
fun foo(a: Boolean, () -> PowerAssertNode)
which would enable the exact same transformation as when it returns a
String
, but instead returning a
PowerAssertNode
, which would be a new interface like:
Copy code
interface PowerAssertNode {
    val value: Any?         // the value at this particular node
    fun toString(): String  // the existing PowerAssert string representation
    val children: Sequence<PowerAssertNode>
}
This would be very useful to integrate Power Assert into other projects, for example assertion libraries. Currently, assertion libraries can't embed the information they have into Power Assert diagrams, so they can't be used together easily. With this feature, it would become possible.
b
https://github.com/Kotlin/KEEP/blob/bnorm/call-explanation/proposals/call-explanation.md https://github.com/JetBrains/kotlin/tree/rr/bnorm/power-assert-runtime/plugins/pow[…]ssert/power-assert-runtime/src/commonMain/kotlin/kotlin/explain (This is all pretty old, as I had to put it on hold due to other, higher priority projects. But the core concept is there and I'm hoping to have a public KEEP soon ™️ and really pushing to have something in 2.4 . No promises. So... 🤫) TL;DR, this is the design we're thinking:
Copy code
@PowerAssert
fun myAssert(condition: Boolean) {
  if (!condition) {
    val explanation: CallExplanation? = PowerAssert.explanation // intrinsic
    throw AssertionError(explanation?.toDefaultMessage() ?: "Assertion failed") // extension function
  }
}
c
I see. To clarify my needs then, so you can check if it would be compatible; When a user writes
Copy code
assertThat(foo) {
    isGreaterThan(emptyList())
    get { this[0] } isEqualTo 5
}
I'd like to use Power Assert on each of these definitions, and within the assertion library be able to combine them with the intrinsic knowledge I have about the structure, so I can generate a bigger Power Assert output string that contains all that information
b
If
assertThat
,
isGreaterThan
, and
isEqualTo
are all annotated, you'll get information about
foo
,
emptyList()
,
get { this[0] }
, and
5
in 3 separate call explanations you can combine how see you see fit. Source information will be available in each explanation like file offset of source code and display offset of each expression. I'll mention that lambda are extremely difficult to handle in any meaningful way, so the
assertThat
and
get
lambdas will not be analyzed, similar to how it works today with something like
assert(list.filter { it <= 0 }.isEmpty))
.
c
The
assertThat
lambda is not an issue, because it contains function calls that are themselves annotated, so the information is recoverable that way. The
get { this[0] }
would be useful, I'll think of how that can be adapted
b
?
Copy code
assertThat(foo) {
    isGreaterThan(emptyList())
    subject[0] isEqualTo 5
}
c
Now that we have context parameters, there may be a way to get this to work yeah
Small note on your KEEP draft: the ability to configure functions at the module-level is important for us. For example, we write all our assertions using
check
from the stdlib
b
And it's not being removed. Will continue to be supported. Things will just be more automatic for functions annotated with
@PowerAssert
. In fact, we're also considering changing the current transformation to create a
CallExplanation
and then immediately call
toDefaultMessage()
on it for the message parameter, so we can improve things with runtime behavior (proper array rendering, flattening of the diagram, etc). Will likely happen, but we need to carefully review the API for any backwards compatibility concerns.
👍 1
c
Do you want a few comments on the KEEP draft, or it's too early for that to be useful?
b
Happy to take comments. The design of the annotation and transformation is pretty settled, but the actual API of
Explanation
and
Expression
is still under review and might change. But if you have use case requests or other comments/concerns, happy to hear them.
👍 1
c
One of them would be to make
Expression
an
interface
, to be able to use interface delegation.
The
explanation
property is a compiler-plugin intrinsic, and access will always result in a runtime error unless the compiler-plugin is applied.
I expect that libraries adopting this will want to work even if users haven't enabled Power Assert.
try…catch
the explanation each time doesn't seem very convenient (but also,
explanationOrNull
would be confusing since it's already nullable). Since it will generate two different functions with a different branch, how will that behave with code coverage tools?
For example, a
StringTemplateExpression
could be introduced to help describe String concatenation and each of the arguments provided.
I think this may be useful in many more situations than Power Assert itself. For example, string sanitization, similar to what #C07CVTQ6WBG is doing. I find the syntax
PowerAssert.explanation
to be a bit strange, it feels a bit alien. Though this is a very specific thing used in very specific situations, so maybe it's ok. Overall, I like this, and I hope we see an actual KEEP of it one day 🙂
b
One of them would be to make
Expression
an
interface
, to be able to use interface delegation.
What's the use case for creating your own
Expression
implementation?
[...] even if users haven't enabled Power Assert.
So with this new annotation, library authors need to apply the power-assert compiler plugin to their library as well. This makes sure that the synthetic copy of the function is generated. And this also makes sure that calls to
PowerAssert.explanation
are replaced with a hard-coded
null
in the original function and parameter access in the synthetic copy. This means that downstream users of the annotated function don't need to have power-assert applied to be able to use the original function, as the explanation will just always be null.
Since it will generate two different functions with a different branch, how will that behave with code coverage tools?
I'm honestly not sure. But if the tool respects line numbers - which are preserved in the synthetic copy of the function - the coverage should be combined?
I think this may be useful in many more situations than Power Assert itself.
Yes, there are some ideas along this line similar to python's f-string support for self-documenting expressions: KT-71211. It's sort of possible to do it now, as you can get the last expression of the argument and extract the template from the source code. Not pretty, so we're thinking about how we can expand on it in the future.
I find the syntax
PowerAssert.explanation
to be a bit strange, [...]
It's sort of similar to
coroutineContext
just not at the top-level. Or
currentComposer
in Compose. I don't think it should be at the top-level because I don't really want to pollute that scope, but I could certainly reconsider it during the KEEP review if there are good suggestions.
👍 1
c
What's the use case for creating your own
Expression
implementation?
Creating an
Expression
that combines others, while still being able to use all the built-in features (e.g.
toDefaultMessage()
)
b
Do you mean
Explanation
rather than
Expression
? So that you can combine `Explanation`s from multiple calls into a single diagram?
c
Ah, yes, I think so.
b
Your example is definitely possible: https://gist.github.com/bnorm/92f21b9ad99edd85d622f972b3736080
Copy code
Assertion failed:
 * String `Kodee` does not have length `1`.
assertThat(name) {
           |
           Kodee

    hasLength(expected)
              |
              1

}
c
Perfect!
Since Power Assert was released, we migrated all our projects to it, but there are definitely things we miss from traditional assertion libraries. However, so far, they don't integrate very well… Happy to see a Power Assert-enhanced assertion library is possible in the future 👀
Though I'll probably keep it way more minimal, since Power Assert itself is able to handle all the matchers Maybe something like
Copy code
assertThat(someComputationHere) {
    check(subject != null)
    check(subject.length > 3)
}
b
Yep, something like that is totally possible as well (updated the gist):
Copy code
Assertion failed:
 * String `Kodee` does not have length `1`.
 * Condition failed: (subject?.length ?: 0) > 7
assertThat(name) {
           |
           Kodee

    hasLength(1)
    check(subject != null)
    check((subject?.length ?: 0) > 7)
                    |            |
                    5            false

}
Smart-casting is tough if you don't have eager failing, since you cannot apply contracts to the
check
function. But maybe there could be a difference between a
check
(soft assertion) and a
require
that immediately fails.
👍 1