Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions components/clarinet-cli/tests/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,105 @@ fn can_init_console_with_mxs() {
assert_eq!(output[1], "false");
assert_eq!(output[2], "true");
}

#[test]
fn test_get_constant_command() {
let output = run_console_command(
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
&[
"::get_constant counter MISSING_CONSTANT",
],
);

assert!(!output.is_empty());
let output_text = output.join(" ");

// Should contain error message for missing constant
assert!(output_text.contains("not found") || output_text.contains("MISSING_CONSTANT"));
assert!(output_text.contains("Constant:") && output_text.contains("MISSING_CONSTANT"));
assert!(output_text.contains("contract:") && output_text.contains("counter"));
}

#[test]
fn test_get_constant_command_found() {
let output = run_console_command(
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
&[
"::get_constant counter MAX_COUNT",
],
);

assert!(!output.is_empty());
let output_text = output.join(" ");

// Should contain the constant value
assert!(output_text.contains("Contract:"));
assert!(output_text.contains("counter"));
assert!(output_text.contains("Constant:"));
assert!(output_text.contains("MAX_COUNT"));
assert!(output_text.contains("Value:"));
assert!(output_text.contains("u100"));
}

#[test]
fn test_get_data_var_command() {
let output = run_console_command(
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
&[
"::get_data_var counter count",
],
);

assert!(!output.is_empty());
let output_text = output.join(" ");

// Should contain the data variable value
assert!(output_text.contains("Contract:"));
assert!(output_text.contains("counter"));
assert!(output_text.contains("Data var:"));
assert!(output_text.contains("count"));
assert!(output_text.contains("Value:"));
assert!(output_text.contains("u0")); // Initial value should be u0
}

#[test]
fn test_get_map_val_command_not_found() {
let output = run_console_command(
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
&[
"::get_map_val counter test-map u1",
],
);

assert!(!output.is_empty());
let output_text = output.join(" ");

// Should contain not found message for empty map
assert!(output_text.contains("Map entry not found") || output_text.contains("not found"));
assert!(output_text.contains("test-map"));
assert!(output_text.contains("u1"));
}

#[test]
fn test_get_map_val_command_found() {
let output = run_console_command(
&["-m", &format!("{}/tests/fixtures/mxs/Clarinet.toml", env!("CARGO_MANIFEST_DIR"))],
&[
"(contract-call? .counter set-map-entry u1 u42)",
"::get_map_val counter test-map u1",
],
);

assert!(!output.is_empty());
let output_text = output.join(" ");

// Should contain the map value
assert!(output_text.contains("Contract:"));
assert!(output_text.contains("counter"));
assert!(output_text.contains("Map:"));
assert!(output_text.contains("test-map"));
assert!(output_text.contains("Key:"));
assert!(output_text.contains("u1"));
assert!(output_text.contains("Value:"));
assert!(output_text.contains("some u42"));
}
8 changes: 8 additions & 0 deletions components/clarinet-cli/tests/fixtures/mxs/counter.clar
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
(define-data-var count uint u0)

(define-constant MAX_COUNT u100)

(define-map test-map uint uint)

(define-public (increment)
(ok (var-set count (+ (var-get count) u1)))
)

(define-public (set-map-entry (key uint) (value uint))
(ok (map-set test-map key value))
)
235 changes: 235 additions & 0 deletions components/clarity-repl/src/repl/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ impl Session {
cmd if cmd.starts_with("::set_epoch") => self.set_epoch(cmd),
cmd if cmd.starts_with("::encode") => self.encode(cmd),
cmd if cmd.starts_with("::decode") => self.decode(cmd),
cmd if cmd.starts_with("::get_constant") => self.get_constant(cmd),
cmd if cmd.starts_with("::get_data_var") => self.get_data_var(cmd),
cmd if cmd.starts_with("::get_map_val") => self.get_map_val(cmd),

_ => "Invalid command. Try `::help`".yellow().to_string(),
}
Expand Down Expand Up @@ -884,6 +887,18 @@ impl Session {
"{}",
"::decode <bytes>\t\t\tDecode a Clarity Value bytes representation".yellow()
));
output.push(format!(
"{}",
"::get_constant <contract> <constant>\tGet constant value from a contract".yellow()
));
output.push(format!(
"{}",
"::get_data_var <contract> <var>\t\tGet data variable value from a contract".yellow()
));
output.push(format!(
"{}",
"::get_map_val <contract> <map> <key>\tGet map value from a contract".yellow()
));

output.join("\n")
}
Expand Down Expand Up @@ -1107,6 +1122,226 @@ impl Session {
format!("{}", value_to_string(&value).green())
}

pub fn get_constant(&mut self, cmd: &str) -> String {
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();

if args.len() != 2 {
return format!("{}", "Usage: ::get_constant <contract> <constant>".red());
}

let contract_name = args[0];
let constant_name = args[1];

let default_deployer = match self.settings.initial_deployer.as_ref() {
Some(account) => account.address.clone(),
None => self.get_tx_sender(),
};

let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
Ok(id) => id,
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
};

let contract = match self.contracts.get(&contract_id) {
Some(contract) => contract,
None => return format!("{} {}", "Contract not found:".red(), contract_id),
};

// Search for constant in the contract's AST
for function_definition in &contract.ast.expressions {
if let Some(expr) = function_definition.match_list() {
if expr.len() >= 3 {
if let Some(name_expr) = expr.get(1) {
if let Some(name) = name_expr.match_atom() {
if name.as_str() == constant_name && expr.len() >= 3 {
let expr_str = format!("{}", expr[2]);
return format!("{} {}\n{} {}\n{} {}",
"Contract:".yellow(),
contract_id.to_string().green(),
"Constant:".yellow(),
name.green(),
"Value:".yellow(),
expr_str.green()
);
}
}
}
}
}
}

format!("{} {} {} {}",
"Constant:".red(),
constant_name.red(),
"not found in contract:".red(),
contract_id.to_string().red()
)
}

pub fn get_data_var(&mut self, cmd: &str) -> String {
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();

if args.len() != 2 {
return format!("{}", "Usage: ::get_data_var <contract> <var>".red());
}

let contract_name = args[0];
let var_name = args[1];

let default_deployer = match self.settings.initial_deployer.as_ref() {
Some(account) => account.address.clone(),
None => self.get_tx_sender(),
};

let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
Ok(id) => id,
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
};

match self.interpreter.get_data_var(&contract_id, var_name) {
Some(value_hex) => {
// Convert hex string back to Clarity Value for display
let value_bytes = match decode_hex(&value_hex) {
Ok(bytes) => bytes,
Err(e) => return format!("{} {} {} {}",
"Failed to decode value:".red(),
e.to_string().red(),
"from contract:".red(),
contract_id.to_string().red()
)
};

let value = match Value::consensus_deserialize(&mut &value_bytes[..]) {
Ok(value) => value,
Err(e) => return format!("{} {} {} {}",
"Failed to deserialize value:".red(),
e.to_string().red(),
"from contract:".red(),
contract_id.to_string().red()
)
};

format!("{} {}\n{} {}\n{} {}",
"Contract:".yellow(),
contract_id.to_string().green(),
"Data var:".yellow(),
var_name.green(),
"Value:".yellow(),
value_to_string(&value).green()
)
}
None => {
format!("{} {} {} {}",
"Data var:".red(),
var_name.red(),
"not found in contract:".red(),
contract_id.to_string().red()
)
}
}
}

pub fn get_map_val(&mut self, cmd: &str) -> String {
let args: Vec<_> = cmd.split_whitespace().skip(1).collect();

if args.len() < 3 {
return format!("{}", "Usage: ::get_map_val <contract> <map> <key>".red());
}

let contract_name = args[0];
let map_name = args[1];
let key_expr = args[2..].join(" ");

let default_deployer = match self.settings.initial_deployer.as_ref() {
Some(account) => account.address.clone(),
None => self.get_tx_sender(),
};

let contract_id = match Self::desugar_contract_id(&default_deployer, contract_name) {
Ok(id) => id,
Err(e) => return format!("{} {}", "Invalid contract identifier:".red(), e),
};

// Parse the key expression to get a Clarity Value
let key_value = match self.eval_clarity_string(&key_expr) {
value => value,
};

// Extract the actual Value from the SymbolicExpression
let key_value = match key_value.match_atom_value() {
Some(value) => value.clone(),
None => {
// For complex expressions, we need to evaluate them differently
let result = match self.eval_with_hooks(format!("({})", key_expr), None, false) {
Ok(result) => {
match &result.result {
EvaluationResult::Snippet(snippet_result) => snippet_result.result.clone(),
_ => return format!("{} {} {} {}",
"Unable to evaluate key expression:".red(),
key_expr.red(),
"for contract:".red(),
contract_id.to_string().red()
)
}
}
Err(_) => return format!("{} {} {} {}",
"Unable to evaluate key expression:".red(),
key_expr.red(),
"for contract:".red(),
contract_id.to_string().red()
)
};
result
}
};

match self.interpreter.get_map_entry(&contract_id, map_name, &key_value) {
Some(value_hex) => {
// Convert hex string back to Clarity Value for display
let value_bytes = match decode_hex(&value_hex) {
Ok(bytes) => bytes,
Err(e) => return format!("{} {} {} {}",
"Failed to decode value:".red(),
e.to_string().red(),
"from contract:".red(),
contract_id.to_string().red()
)
};

let value = match Value::consensus_deserialize(&mut &value_bytes[..]) {
Ok(value) => value,
Err(e) => return format!("{} {} {} {}",
"Failed to deserialize value:".red(),
e.to_string().red(),
"from contract:".red(),
contract_id.to_string().red()
)
};

format!("{} {}\n{} {}\n{} {}\n{} {}",
"Contract:".yellow(),
contract_id.to_string().green(),
"Map:".yellow(),
map_name.green(),
"Key:".yellow(),
key_expr.green(),
"Value:".yellow(),
value_to_string(&value).green()
)
}
None => {
format!("{} {} {} {} {} {}",
"Map entry not found for key:".red(),
key_expr.red(),
"in map:".red(),
map_name.red(),
"of contract:".red(),
contract_id.to_string().red()
)
}
}
}

#[cfg(not(target_arch = "wasm32"))]
pub fn get_costs(&mut self, output: &mut Vec<String>, cmd: &str) {
let Some((_, expr)) = cmd.split_once(' ') else {
Expand Down
Loading