-
Notifications
You must be signed in to change notification settings - Fork 16
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: Solidity test runner progress callback completion #563
refactor: Solidity test runner progress callback completion #563
Conversation
|
Thanks for the explanation, I'm ok with this change. And I guess the function will become something like this soon, right? function runSolidityTests(artifacts: Array<Artifact>, test_suites: Array<ArtifactId>, gas_report: boolean, progress_callback: (result: SuiteResult) => void): void |
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.
LGTM on the interface side, but I didn't check the rust changes.
Ok cheers and yeah that's what the interface would change to and |
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.
Nice fix for this problem.
I had some nice to have comments and a recommendation regarding N-API.
crates/edr_napi/index.d.ts
Outdated
* It is up to the caller to track how many times the callback is called to | ||
* know when all tests are done. | ||
*/ | ||
export function runSolidityTests(test_suites: Array<TestSuite>, gas_report: boolean, progress_callback: (result: SuiteResult) => void): void |
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.
Consideration: As this is spawning a background process, it might be good to indicate this in the function name. The only thing I can come up with is spawnSolidityTestRunner
.
Up to you whether you think this makes sense. The docs also outline this, in case the name is not self-explanatory.
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 ok with having this low-level interface being callback based, as we can then wrap it in something more idiomatic for js. But it should have a simple callback indicating the completion of the process.
I know we discussed leaving that responsibility to the user, but I think that would be less flexible/evolvable. E.g. if we implement a feature in edr that skips parts of the tests, every consumer would have to adapt the "counting the results until finish" code.
Note that the completion callback doesn't need to accumulate all the results. It could probably be () => void
.
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.
But it should have a simple callback indicating the completion of the process.
I like the idea and I can make that change, but tbh I'm a bit wary of doing it for the following reasons:
- For now, the only users of this interface are in the EDR repo and there would be no usage of the completion callback.
- We ran into the problem fixed by this PR, because we tried to anticipate usage patterns and provide a more ergonomic interface. This tells me that it's too early to polish the interface.
So I'd just add the completion callback to the interface design doc if that's ok?
@@ -61,10 +61,18 @@ async function runForgeStdTests(forgeStdRepoPath) { | |||
.map(loadContract.bind(null, hardhatConfig)) | |||
.filter((ts) => !EXCLUDED_TEST_SUITES.has(ts.id.name)); | |||
|
|||
const runner = new SolidityTestRunner(gasReport, (...args) => { | |||
console.error(`${args[1].name} took ${elapsedSec(start)} seconds`); | |||
const results = await new Promise((resolve) => { |
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.
Nice to have: I see some duplication of this code with other call sites. We could provide a helper function in TS that accumulates the results as callbacks are called.
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.
Yeah we will definitely want to have a JS wrapper at some point. It's not so clear how to handle it now especially assuming it's TS, so I'd defer this. E.g. is the wrapper in edr_napi
(if yes how does the build tie together) or is it a separate package (how do we release that?).
Problem
The Solidity test runner interface that is exposed from Rust looks like this currently (config options are WIP):
The
SolidityTestRunner
makes no guarantees about when the progress callbacks are called. The only guarantee is that the callback will be scheduled to be called for each test suite as soon as the test suite finished executing. The scheduling is done by the NodeJS event loop and it can happen after the promise returned byrunTests
is resolved.Normally NodeJS won’t exit as long as there are callbacks scheduled to be called by the event loop, but this can be overridden by calling
process.exit()
. So given code like this:It’s possible that the progress callback is never invoked.
As it turns out, the Hardhat CLI calls
process.exit
after all plugins have finished executing, which leads to progress reports not being printed sometimes in the Solidity tests plugin.Solution
The
process.exit
call in Hardhat is probably there for a good reason, so I can see two solutions to this problem:runTests
return nothing, and have the callee accumulate results through the progress callback that receive eachSuiteResult
as it’s ready.runTests
only resolve after the callbacks have finished executing (as opposed to the current behavior which is to resolve after all test suites have finished executing).I prefer the first solution, because it’s simpler on the Rust side and it gives full flexibility on the JS side where it’s easy to keep track of when all test suites have finished. E.g. one could promisify it as follows:
And this could be modified to support event subscriptions or async progress callbacks without modifications on the Rust side.
Interface
The previous object-oriented interface had to be abandoned, because of lifetime issues with the JS progress callback once the
runTests
method was changed to return immediately after test execution started.For background, we first wanted to have a single function to call to execute Solidity tests, but we also wanted to have this function return all the results. This meant the function had to be async. But the
JsFunction
callback passed into Rust is notSend
which means it cannot be passed as argument into a napi-rs async function. As a workaround we added aSolidityTestRunner
class, passed theJsFunction
into its sync constructor and then added an async method to the class.But because we held on the
JsFunction
in theSolidityTestRunner
, we needed another workaround to let the event loop exit before the object is GC-ed by callingunref
on the thread safe function wrapper for theJsFunction
. When I changed therunTest
method in this PR to return immediately after test execution started, thisunref
workaround was causing problems as the lifetime of theSolidityTestRunner
object and the callback no longer aligned when called like this:With the
unref
workaround, the code above would panic with "thread safe function is closed" message and without theunref
workaround there would be a noticeable delay between finishing test execution and the interpreter exiting. So I went back to the original design to just have a single free-standing function to run Solidity tests as the lifetimes are naturally aligned this way:See full diff in
crates/edr_napi/index.d.ts
.An alternative to the free standing function could be keeping the
SolidityTestRunner
and haverunTests
take the callback as argument, but I don't think having aSolidityTestRunner
object is warranted (sans async workaround) as it'd have no other methods and it'd achieve the same operation with two calls instead of one. So it'd be just ceremony.Considerations
runTests
was originally introduced to make the JS interface more ergonomic, but it looks like it adds significantly more complexity on the Rust side than it saves on the JS side.