-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
feat(cheatcodes) vm.prompt: Prompt user for interactive input #7012
Conversation
7bd5a3e
to
afe4598
Compare
this looks great! thanks for implementing this
Your code from console-rs/dialoguer#266 (comment) with some small modifications seems to be working for me fn prompt_secret(prompt_text: String) -> Result<String> {
let (send, recv) = mpsc::channel();
thread::spawn(move || {
send.send(Password::new().with_prompt(prompt_text).interact()).unwrap();
});
match recv.recv_timeout(Duration::from_secs(5)) {
Ok(res) => res.map_err(|_| "i/o error occured".into()),
Err(_) => Err("timeout".into())
}
} However, there were several times when it didn't react on any input and failed with timeout, but this looked like a shell bug and simple restart of the shell fixed it. I can't reliably reproduce this behavior.
I can imagine some kind of Let's also hide values received from foundry/crates/evm/traces/src/decoder/mod.rs Lines 460 to 470 in 7922fd5
|
Thank you @klkvr for the quick response! 🙏
|
Let's figure out how to deal with timeouts first, and after that we can add tests for timeouts causing reverts which seems to be more important and won't require any mocking |
One thing we also should figure out is how can scripts be used in a non-interactive/test environment with this cheat. It's best practice to test scripts, and many use scripts to setup their test state by running the script in One approach might to add two extra args: the default value, and a bool of whether or not to immediately use that value (i.e. no timeout). This is also nice because the default arg gives us stronger return typing for free. This would fit really nicely with #2900 to remove the need for an env var. Examples // new cheat signature
prompt(string calldata promptText, uint256 default, bool useDefault) external returns (uint256 value);
// usage with env var config
uint256 myUint = vm.prompt("enter uint", 42, vm.envOr("TEST_MODE", false));
// with forge environment detection
uint256 myUint = vm.prompt("enter uint", 42, vm.isTestContext()); |
@mds1 Good point. IMO, having a single default value which is used in test environment will not always be enough. I believe that for testing scripts with contract Script {
function run() public {
uint256 myUint = vm.parseUint(vm.prompt("enter uint"));
run(myUint);
}
function run(uint256 myUint) public {
// actual logic
}
} That way, we are keeping UX gain (don't have to provide |
0bcee87
to
e7cbaa0
Compare
@mds1 would you like me to implement #2399 as part of this PR? Should be simple enough I think. Dialoguer supports a confirm-type prompt: Regarding a "default value" ― script authors could catch the timeout and provide a default for themselves: function promptWithDefault (
string memory prompt,
string memory defaultValue
) internal returns (string memory input) {
try vm.prompt(prompt) returns (string memory res) {
if (bytes(res).length == 0) input = defaultValue;
else input = res;
}
catch (bytes memory) {
input = defaultValue;
}
} Not sure if that truly answers your concerns, but something to consider. |
I'm still working on the timeout, I added it in the config but it seems I broke some tests. Looking into it. |
@klkvr @mds1 @DaniPopes I see I have tests that are failing. I am very certain there is an actual issue because the original commit for the PR fully passed the tests. $ cargo nextest run -E 'kind(test) & !test(/issue|forge_std|ext_integration/)' --partition count:2/3 I get different tests failing 🤔 (specifically one One noticeable difference is that it seems these tests are running on Windows while my machine is Linux. What would be the best way to try and reproduce these failures locally? |
I would do this as a separate PR, just to manage scope, and ensure one feature doesn't block the other
This is a good idea, I'd be ok with this as the solution, and we should make sure to document this pattern in the book. Let's go with your workflow here to keep things simpler, as my suggestion can always be implemented in the future in a backwards compatible way |
@Tudmotu Some tests are very flaky unfortunately. I've re-ran the failing jobs and they pass now. |
Ah, thank you @DaniPopes 🙏 |
Looking at my code I am pretty sure I did not implement the config option correctly. It works, but it feels wrong to modify |
e7cbaa0
to
a107e34
Compare
I pushed another change that removes the unnecessary changes I made to I would like to push this PR forward. Regarding tests: |
I think it's fine to just have a couple tests like
we have default timeout set to 0 for testing, so it shouldn't cause any issues with our CI @mattsse wdyt? |
After looking into it, it seems to be caused by |
Hi all, |
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.
sorry for the late review...
I have only a few nits.
I'm fine with not having tests for this, because not really possible without doing a lot of work,
but you're right this will fail with "not a terminal" if there's no terminal
I like the test case for prompttimeout that @klkvr suggested, this one we can add easily
we could add another cheatcode for is_terminal
, but can do this seprately
crates/cheatcodes/src/fs.rs
Outdated
@@ -370,6 +387,39 @@ fn ffi(state: &Cheatcodes, input: &[String]) -> Result<FfiResult> { | |||
}) | |||
} | |||
|
|||
fn prompt_input(prompt_text: &String) -> Result<String, dialoguer::Error> { |
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.
fn prompt_input(prompt_text: &String) -> Result<String, dialoguer::Error> { | |
fn prompt_input(prompt_text: &str) -> Result<String, dialoguer::Error> { |
crates/cheatcodes/src/fs.rs
Outdated
Input::new().allow_empty(true).with_prompt(prompt_text).interact_text() | ||
} | ||
|
||
fn prompt_password(prompt_text: &String) -> Result<String, dialoguer::Error> { |
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.
fn prompt_password(prompt_text: &String) -> Result<String, dialoguer::Error> { | |
fn prompt_password(prompt_text: &str) -> Result<String, dialoguer::Error> { |
crates/cheatcodes/src/fs.rs
Outdated
println!(); | ||
"I/O error occured".into() |
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 think we should return the error as the return value and also log in in an error! trace
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 not sure I understand - do you mean the prompt()
function should return Result<String, dialoguer::Error>
?
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.
we want to return err.to_string()
here as the response value so the users has a chance to look at the error in the revert message if the call failed
crates/cheatcodes/src/fs.rs
Outdated
fn prompt( | ||
state: &Cheatcodes, | ||
prompt_text: &str, | ||
input: fn(&String) -> Result<String, dialoguer::Error>, |
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.
&str
crates/cheatcodes/src/fs.rs
Outdated
fn prompt( | ||
state: &Cheatcodes, | ||
prompt_text: &str, | ||
input: fn(&String) -> Result<String, dialoguer::Error>, |
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.
&str
ba54c2d
to
d3dae42
Compare
Pushed a change which propagates the error message. Will prepare PRs for Foundry Book & Forge Std. Let me know if there is anything else you'd like me to fix here. |
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 after addressing outstanding comments, feel free to open the PRs
d3dae42
to
8da53cf
Compare
Whoops, I was certain I already made these changes but I guess I messed something up with my rebases 😅 I believe everything should be addressed now 🙂 |
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,
I set prompt timeout to 0 for testing and use a regular revert, so this is still usable when running tests locally
we can now add it to forge-std |
Thank you @mattsse 🙏 I will open the Book & Std PRs soon. |
Thank you all for the review, effort and patience 🙂 🙏 |
Implementing #5509. I am reviving this, took me a while to get to this, apologies.
This PR will add two cheatcodes:
vm.prompt()
andvm.promptSecret()
. These cheatcodes will let users enter dynamic values that can be consumed by Forge scripts.The description below is long. It mostly explains the issues I encountered.
TLDR:
Motivation
It would be useful to pass arguments to scripts in a dynamic & user-friendly way. Today the options for passing arguments are either environment variables or using the
--sig
argument. Both of these are cumbersome and in addition, if the parameter is some secret (password, private key, api key, etc), it exposes it in the shell history.Adding the
vm.prompt()
andvm.promptSecret()
cheatcodes will allow script authors to write better scripts and more easily generalize their arguments. For example, scripts could prompt for a fork alias to pass tovm.createSelectFork()
, or the address of a contract they interact with, etc.Implementation
We decided to keep the interface simple and only allow
string memory
responses, letting the script author handle any necessary parsing using the usual string parsing methods. The implementation is based on the Dialoguer crate which is already used elsewhere and it is quite straightforward.Timeout
Adding a timeout was suggested in order to prevent CI environments from hanging unexpectedly. These timeouts would be handled by script authors using
try / catch
in Solidity if desired.Unfortunately, Dialoguer does not support timeouts for prompts. Looking at other prompt libraries it does not seem they support a timeout either. I was able to hack together a partial solution by running Dialoguer in a thread and using
mpsc::channel
to wait for its response up to a certain timeout. This works forInput
prompts but not forPassword
prompts. See this comment I left on the timeout feature request on Dialoguer repo.Therefore I decided to leave the timeout feature out of the initial PR so we can discuss other approaches (see "Open questions").
Testing
Since this cheat requires interactivity it cannot be tested in Solidity ― only in Rust. But unfortunately I couldn't figure out exactly how to test it. The main problem is that the
One option I thought we could try is mocking one of the underlying components Dialoguer uses ―
Term
― in order to mock user input. But after looking at some Rust libraries it seems you cannot mock a struct from external libraries.Another idea I had was to try and interact with the
Term
object via threads: run prompt in main thread, write intoTerm
in a different thread. While visually it works (the terminal display the prompt & answer) it seems that Dialoguer does not register the input from the other thread.Open questions
mpsc::channel
solutionatty
(not sure if this would actually work)--non-interactive
flagforge script
already has. This would require implementing a similar flag forforge test
catch (bytes memory)
and not withcatch Error(string memory)
. Is there a way to fix this?Dependent PRs