Skip to content

Commit 1186a4c

Browse files
Merge pull request #6631 from federico-stacks/chore/aac-parse-error-test
test: aac add test coverage for `ParseError` variants
2 parents ebc8fb0 + de77d02 commit 1186a4c

File tree

35 files changed

+3786
-66
lines changed

35 files changed

+3786
-66
lines changed

clarity/src/vm/ast/definition_sorter/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ impl DefinitionSorter {
9090
let sorted_indexes = walker.get_sorted_dependencies(&self.graph);
9191

9292
if let Some(deps) = walker.get_cycling_dependencies(&self.graph, &sorted_indexes) {
93-
let functions_names = deps
93+
let mut function_names = deps
9494
.into_iter()
9595
.filter_map(|i| {
9696
let exp = &contract_ast.pre_expressions[i];
@@ -99,7 +99,10 @@ impl DefinitionSorter {
9999
.map(|i| i.0.to_string())
100100
.collect::<Vec<_>>();
101101

102-
let error = ParseError::new(ParseErrorKind::CircularReference(functions_names));
102+
// Sorting function names to make the error contents deterministic
103+
function_names.sort();
104+
105+
let error = ParseError::new(ParseErrorKind::CircularReference(function_names));
103106
return Err(error);
104107
}
105108

clarity/src/vm/ast/mod.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub fn build_ast<T: CostTracker>(
238238
mod test {
239239
use std::collections::HashMap;
240240

241+
use clarity_types::types::MAX_VALUE_SIZE;
241242
use stacks_common::types::StacksEpochId;
242243

243244
use crate::vm::ast::build_ast;
@@ -447,4 +448,141 @@ mod test {
447448
}
448449
}
449450
}
451+
452+
#[test]
453+
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_parse() {
454+
let limit = ExecutionCost {
455+
read_count: u64::MAX,
456+
write_count: u64::MAX,
457+
read_length: u64::MAX,
458+
write_length: u64::MAX,
459+
runtime: 1,
460+
};
461+
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);
462+
463+
let err = build_ast(
464+
&QualifiedContractIdentifier::transient(),
465+
"(define-constant my-const u1)",
466+
&mut tracker,
467+
ClarityVersion::Clarity4,
468+
StacksEpochId::Epoch33,
469+
)
470+
.unwrap_err();
471+
472+
assert!(
473+
matches!(*err.err, ParseErrorKind::CostBalanceExceeded(_, _)),
474+
"Instead found: {err}"
475+
);
476+
}
477+
478+
#[test]
479+
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_cycle_detection_with_0_edges() {
480+
let expected_ast_parse_cost = 1215;
481+
let expected_cycle_det_cost = 72;
482+
let expected_total = expected_ast_parse_cost + expected_cycle_det_cost;
483+
484+
let limit = ExecutionCost {
485+
read_count: u64::MAX,
486+
write_count: u64::MAX,
487+
read_length: u64::MAX,
488+
write_length: u64::MAX,
489+
runtime: expected_ast_parse_cost,
490+
};
491+
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);
492+
493+
let err = build_ast(
494+
&QualifiedContractIdentifier::transient(),
495+
"(define-constant a 0)(define-constant b 1)", // no dependency = 0 graph edge
496+
&mut tracker,
497+
ClarityVersion::Clarity4,
498+
StacksEpochId::Epoch33,
499+
)
500+
.expect_err("Expected parse error, but found success!");
501+
502+
let total = match *err.err {
503+
ParseErrorKind::CostBalanceExceeded(total, _) => total,
504+
_ => panic!("Expected CostBalanceExceeded, but found: {err}"),
505+
};
506+
507+
assert_eq!(expected_total, total.runtime);
508+
}
509+
510+
#[test]
511+
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_cycle_detection_with_1_edge() {
512+
let expected_ast_parse_cost = 1215;
513+
let expected_cycle_det_cost = 213;
514+
let expected_total = expected_ast_parse_cost + expected_cycle_det_cost;
515+
516+
let limit = ExecutionCost {
517+
read_count: u64::MAX,
518+
write_count: u64::MAX,
519+
read_length: u64::MAX,
520+
write_length: u64::MAX,
521+
runtime: expected_ast_parse_cost,
522+
};
523+
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);
524+
525+
let err = build_ast(
526+
&QualifiedContractIdentifier::transient(),
527+
"(define-constant a 0)(define-constant b a)", // 1 dependency = 1 graph edge
528+
&mut tracker,
529+
ClarityVersion::Clarity4,
530+
StacksEpochId::Epoch33,
531+
)
532+
.expect_err("Expected parse error, but found success!");
533+
534+
let total = match *err.err {
535+
ParseErrorKind::CostBalanceExceeded(total, _) => total,
536+
_ => panic!("Expected CostBalanceExceeded, but found: {err}"),
537+
};
538+
539+
assert_eq!(expected_total, total.runtime);
540+
}
541+
542+
#[test]
543+
fn test_build_ast_error_vary_stack_too_deep() {
544+
// This contract pass the parse v2 MAX_NESTING_DEPTH but fails the [`VaryStackDepthChecker`]
545+
let contract = {
546+
let count = AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) - 1;
547+
let body_start = "(list ".repeat(count as usize);
548+
let body_end = ")".repeat(count as usize);
549+
format!("{{ a: {body_start}u1 {body_end} }}")
550+
};
551+
552+
let err = build_ast(
553+
&QualifiedContractIdentifier::transient(),
554+
&contract,
555+
&mut (),
556+
ClarityVersion::Clarity4,
557+
StacksEpochId::Epoch33,
558+
)
559+
.expect_err("Expected parse error, but found success!");
560+
561+
assert!(
562+
matches!(*err.err, ParseErrorKind::VaryExpressionStackDepthTooDeep),
563+
"Instead found: {err}"
564+
);
565+
}
566+
567+
#[test]
568+
fn test_build_ast_error_illegal_ascii_string_due_to_size() {
569+
let contract = {
570+
let string = "a".repeat(MAX_VALUE_SIZE as usize + 1);
571+
format!("(define-constant my-str \"{string}\")")
572+
};
573+
574+
let err = build_ast(
575+
&QualifiedContractIdentifier::transient(),
576+
&contract,
577+
&mut (),
578+
ClarityVersion::Clarity4,
579+
StacksEpochId::Epoch33,
580+
)
581+
.expect_err("Expected parse error, but found success!");
582+
583+
assert!(
584+
matches!(*err.err, ParseErrorKind::IllegalASCIIString(_)),
585+
"Instead found: {err}"
586+
);
587+
}
450588
}

clarity/src/vm/ast/parser/v2/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,12 @@ impl<'a> Parser<'a> {
896896
match Value::string_ascii_from_bytes(val.clone().into_bytes()) {
897897
Ok(s) => PreSymbolicExpression::atom_value(s),
898898
Err(_) => {
899+
// Protect against console flooding and process hanging while running tests,
900+
// using a purely arbitrary max chars limit.
901+
// NOTE: A better place for this would be the enum itself, but then we need to write a custom Debug implementation
902+
#[cfg(any(test, feature = "testing"))]
903+
let val = ellipse_string_for_test(val, 128);
904+
899905
self.add_diagnostic(
900906
ParseErrorKind::IllegalASCIIString(val.clone()),
901907
token.span.clone(),
@@ -1131,6 +1137,25 @@ pub fn parse_collect_diagnostics(
11311137
(stmts, diagnostics, parser.success)
11321138
}
11331139

1140+
/// Test helper function to shorten big strings while running tests
1141+
///
1142+
/// This prevents both:
1143+
/// - Console flooding with multi-megabyte output during test runs.
1144+
/// - Potential test process blocking or hanging due to stdout buffering limits.
1145+
///
1146+
/// In case a the input `string` need to be shortned based on `max_chars`,
1147+
/// the resulting string will be ellipsed showing the original character count.
1148+
#[cfg(any(test, feature = "testing"))]
1149+
fn ellipse_string_for_test(string: &str, max_chars: usize) -> String {
1150+
let char_count = string.chars().count();
1151+
if char_count <= max_chars {
1152+
string.into()
1153+
} else {
1154+
let shortened: String = string.chars().take(max_chars).collect();
1155+
format!("{shortened}...[{char_count}]")
1156+
}
1157+
}
1158+
11341159
#[cfg(test)]
11351160
#[cfg(feature = "developer-mode")]
11361161
mod tests {

clarity/src/vm/costs/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,49 @@ impl LimitedCostTracker {
854854
};
855855
Ok(result)
856856
}
857+
858+
/// Create a [`LimitedCostTracker`] given an epoch id and an execution cost limit for testing purpose
859+
///
860+
/// Autoconfigure itself loading all clarity const functions without the need of passing a clarity database
861+
#[cfg(any(test, feature = "testing"))]
862+
pub fn new_with_limit(epoch_id: StacksEpochId, limit: ExecutionCost) -> LimitedCostTracker {
863+
use stacks_common::consts::CHAIN_ID_TESTNET;
864+
865+
let contract_name = LimitedCostTracker::default_cost_contract_for_epoch(epoch_id)
866+
.expect("Failed retrieving cost contract!");
867+
let boot_costs_id = boot_code_id(&contract_name, false);
868+
869+
let version = DefaultVersion::try_from(false, &boot_costs_id)
870+
.expect("Failed defining default version!");
871+
872+
let mut cost_functions = HashMap::new();
873+
for each in ClarityCostFunction::ALL {
874+
let evaluator = ClarityCostFunctionEvaluator::Default(
875+
ClarityCostFunctionReference {
876+
contract_id: boot_costs_id.clone(),
877+
function_name: each.get_name(),
878+
},
879+
each.clone(),
880+
version,
881+
);
882+
cost_functions.insert(each, evaluator);
883+
}
884+
885+
let cost_tracker = TrackerData {
886+
cost_function_references: cost_functions,
887+
cost_contracts: HashMap::new(),
888+
contract_call_circuits: HashMap::new(),
889+
limit,
890+
memory_limit: CLARITY_MEMORY_LIMIT,
891+
total: ExecutionCost::ZERO,
892+
memory: 0,
893+
epoch: epoch_id,
894+
mainnet: false,
895+
chain_id: CHAIN_ID_TESTNET,
896+
};
897+
898+
LimitedCostTracker::Limited(cost_tracker)
899+
}
857900
}
858901

859902
impl TrackerData {

0 commit comments

Comments
 (0)