Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0db522b
chore: make aac macros usable from other modules, #6627
federico-stacks Oct 28, 2025
1eead86
chore: aac add clarity_types as test dependency for doc purpose, #6627
federico-stacks Oct 28, 2025
c01f81e
test: aac add consensus coverage for ParseError variants subset (11),…
federico-stacks Oct 28, 2025
fe6a8d5
refactor: aac consensus macro just produce a test body, #6627
federico-stacks Oct 30, 2025
298959a
merge: address conflict with develop, #6627
federico-stacks Oct 30, 2025
2ac068b
crc: remove clarity_types dependency and use clarity crate aliases, #…
federico-stacks Oct 30, 2025
730fc6d
crc: clean contract code string literal, #6627
federico-stacks Oct 30, 2025
a81db5d
chore: aac fix ParseError::CircularReference indeterminism, #6627
federico-stacks Oct 31, 2025
cac6d65
Merge branch 'develop' into chore/aac-parse-error-test
federico-stacks Nov 3, 2025
d28f8bc
test: add unit tests for build_ast, #6627
federico-stacks Nov 5, 2025
83f08ad
test: improve parse consensus test and add VaryExpressionStackDepthTo…
federico-stacks Nov 5, 2025
1ea97d3
test: add ExpectedWhitespace aac test, #6627
federico-stacks Nov 5, 2025
dc157e4
test: add UnexpectedToken aac test, #6627
federico-stacks Nov 6, 2025
d9c6948
test: add NameTooLong aac test, #6627
federico-stacks Nov 6, 2025
a0f34a8
chore: document variant_coverate_report, #6627
federico-stacks Nov 6, 2025
a04618b
test: add InvalidPrincipalLiteral aac test, #6627
federico-stacks Nov 6, 2025
42f94d6
test: add InvalidBuffer as unreachable, #6627
federico-stacks Nov 6, 2025
9e74b7c
test: add ExpectedContractIdentifier aac test, #6627
federico-stacks Nov 6, 2025
190f702
test: add ExpectedTraitIdentifier aac test, #6627
federico-stacks Nov 6, 2025
03368c7
test: add TupleColonExpectedv2 aac test, #6627
federico-stacks Nov 6, 2025
6145f1d
test: add TupleCommaExpectedv2 aac test, #6627
federico-stacks Nov 6, 2025
49935ab
test: add TupleValueExpected aac test, #6627
federico-stacks Nov 6, 2025
4b70a35
test: add ContractNameTooLong aac test, #6627
federico-stacks Nov 6, 2025
5a1b8c3
test: add IllegalASCIIString aac test, #6627
federico-stacks Nov 7, 2025
2924f6e
test: add IllegalContractName aac as unreachable, #6627
federico-stacks Nov 7, 2025
8c0a7fc
chore: fix failing unit tests, #6627
federico-stacks Nov 7, 2025
7848a34
test: update insta for test_illegal_ascii_string, #6627
federico-stacks Nov 7, 2025
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
7 changes: 5 additions & 2 deletions clarity/src/vm/ast/definition_sorter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl DefinitionSorter {
let sorted_indexes = walker.get_sorted_dependencies(&self.graph);

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

let error = ParseError::new(ParseErrors::CircularReference(functions_names));
// Sorting function names to make the error contents deterministic
function_names.sort();

let error = ParseError::new(ParseErrors::CircularReference(function_names));
return Err(error);
}

Expand Down
138 changes: 138 additions & 0 deletions clarity/src/vm/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ pub fn build_ast<T: CostTracker>(
mod test {
use std::collections::HashMap;

use clarity_types::types::MAX_VALUE_SIZE;
use stacks_common::types::StacksEpochId;

use crate::vm::ast::build_ast;
Expand Down Expand Up @@ -446,4 +447,141 @@ mod test {
}
}
}

#[test]
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_parse() {
let limit = ExecutionCost {
read_count: u64::MAX,
write_count: u64::MAX,
read_length: u64::MAX,
write_length: u64::MAX,
runtime: 1,
};
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);

let err = build_ast(
&QualifiedContractIdentifier::transient(),
"(define-constant my-const u1)",
&mut tracker,
ClarityVersion::Clarity4,
StacksEpochId::Epoch33,
)
.unwrap_err();

assert!(
matches!(*err.err, ParseErrors::CostBalanceExceeded(_, _)),
"Instead found: {err}"
);
}

#[test]
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_cycle_detection_with_0_edges() {
let expected_ast_parse_cost = 1215;
let expected_cycle_det_cost = 72;
let expected_total = expected_ast_parse_cost + expected_cycle_det_cost;

let limit = ExecutionCost {
read_count: u64::MAX,
write_count: u64::MAX,
read_length: u64::MAX,
write_length: u64::MAX,
runtime: expected_ast_parse_cost,
};
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);

let err = build_ast(
&QualifiedContractIdentifier::transient(),
"(define-constant a 0)(define-constant b 1)", // no dependency = 0 graph edge
&mut tracker,
ClarityVersion::Clarity4,
StacksEpochId::Epoch33,
)
.expect_err("Expected parse error, but found success!");

let total = match *err.err {
ParseErrors::CostBalanceExceeded(total, _) => total,
_ => panic!("Expected CostBalanceExceeded, but found: {err}"),
};

assert_eq!(expected_total, total.runtime);
}

#[test]
fn test_build_ast_error_exceeding_cost_balance_due_to_ast_cycle_detection_with_1_edge() {
let expected_ast_parse_cost = 1215;
let expected_cycle_det_cost = 213;
let expected_total = expected_ast_parse_cost + expected_cycle_det_cost;

let limit = ExecutionCost {
read_count: u64::MAX,
write_count: u64::MAX,
read_length: u64::MAX,
write_length: u64::MAX,
runtime: expected_ast_parse_cost,
};
let mut tracker = LimitedCostTracker::new_with_limit(StacksEpochId::Epoch33, limit);

let err = build_ast(
&QualifiedContractIdentifier::transient(),
"(define-constant a 0)(define-constant b a)", // 1 dependency = 1 graph edge
&mut tracker,
ClarityVersion::Clarity4,
StacksEpochId::Epoch33,
)
.expect_err("Expected parse error, but found success!");

let total = match *err.err {
ParseErrors::CostBalanceExceeded(total, _) => total,
_ => panic!("Expected CostBalanceExceeded, but found: {err}"),
};

assert_eq!(expected_total, total.runtime);
}

#[test]
fn test_build_ast_error_vary_stack_too_deep() {
// This contract pass the parse v2 MAX_NESTING_DEPTH but fails the [`VaryStackDepthChecker`]
let contract = {
let count = AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) - 1;
let body_start = "(list ".repeat(count as usize);
let body_end = ")".repeat(count as usize);
format!("{{ a: {body_start}u1 {body_end} }}")
};

let err = build_ast(
&QualifiedContractIdentifier::transient(),
&contract,
&mut (),
ClarityVersion::Clarity4,
StacksEpochId::Epoch33,
)
.expect_err("Expected parse error, but found success!");

assert!(
matches!(*err.err, ParseErrors::VaryExpressionStackDepthTooDeep),
"Instead found: {err}"
);
}

#[test]
fn test_build_ast_error_illegal_ascii_string_due_to_size() {
let contract = {
let string = "a".repeat(MAX_VALUE_SIZE as usize + 1);
format!("(define-constant my-str \"{string}\")")
};

let err = build_ast(
&QualifiedContractIdentifier::transient(),
&contract,
&mut (),
ClarityVersion::Clarity4,
StacksEpochId::Epoch33,
)
.expect_err("Expected parse error, but found success!");

assert!(
matches!(*err.err, ParseErrors::IllegalASCIIString(_)),
"Instead found: {err}"
);
}
}
25 changes: 25 additions & 0 deletions clarity/src/vm/ast/parser/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,12 @@ impl<'a> Parser<'a> {
match Value::string_ascii_from_bytes(val.clone().into_bytes()) {
Ok(s) => PreSymbolicExpression::atom_value(s),
Err(_) => {
// Protect against console flooding and process hanging while running tests,
// using a purely arbitrary max chars limit.
// NOTE: A better place for this would be the enum itself, but then we need to write a custom Debug implementation
#[cfg(any(test, feature = "testing"))]
let val = ellipse_string_for_test(val, 128);

self.add_diagnostic(
ParseErrors::IllegalASCIIString(val.clone()),
token.span.clone(),
Expand Down Expand Up @@ -1110,6 +1116,25 @@ pub fn parse_collect_diagnostics(
(stmts, diagnostics, parser.success)
}

/// Test helper function to shorten big strings while running tests
///
/// This prevents both:
/// - Console flooding with multi-megabyte output during test runs.
/// - Potential test process blocking or hanging due to stdout buffering limits.
///
/// In case a the input `string` need to be shortned based on `max_chars`,
/// the resulting string will be ellipsed showing the original character count.
#[cfg(any(test, feature = "testing"))]
fn ellipse_string_for_test(string: &str, max_chars: usize) -> String {
let char_count = string.chars().count();
if char_count <= max_chars {
string.into()
} else {
let shortened: String = string.chars().take(max_chars).collect();
format!("{shortened}...[{char_count}]")
}
}

#[cfg(test)]
#[cfg(feature = "developer-mode")]
mod tests {
Expand Down
43 changes: 43 additions & 0 deletions clarity/src/vm/costs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,49 @@ impl LimitedCostTracker {
};
Ok(result)
}

/// Create a [`LimitedCostTracker`] given an epoch id and an execution cost limit for testing purpose
///
/// Autoconfigure itself loading all clarity const functions without the need of passing a clarity database
#[cfg(any(test, feature = "testing"))]
pub fn new_with_limit(epoch_id: StacksEpochId, limit: ExecutionCost) -> LimitedCostTracker {
use stacks_common::consts::CHAIN_ID_TESTNET;

let contract_name = LimitedCostTracker::default_cost_contract_for_epoch(epoch_id)
.expect("Failed retrieving cost contract!");
let boot_costs_id = boot_code_id(&contract_name, false);

let version = DefaultVersion::try_from(false, &boot_costs_id)
.expect("Failed defining default version!");

let mut cost_functions = HashMap::new();
for each in ClarityCostFunction::ALL {
let evaluator = ClarityCostFunctionEvaluator::Default(
ClarityCostFunctionReference {
contract_id: boot_costs_id.clone(),
function_name: each.get_name(),
},
each.clone(),
version,
);
cost_functions.insert(each, evaluator);
}

let cost_tracker = TrackerData {
cost_function_references: cost_functions,
cost_contracts: HashMap::new(),
contract_call_circuits: HashMap::new(),
limit,
memory_limit: CLARITY_MEMORY_LIMIT,
total: ExecutionCost::ZERO,
memory: 0,
epoch: epoch_id,
mainnet: false,
chain_id: CHAIN_ID_TESTNET,
};

LimitedCostTracker::Limited(cost_tracker)
}
}

impl TrackerData {
Expand Down
Loading
Loading