Lines
99.7 %
Functions
98.67 %
Branches
100 %
use chrono::NaiveDate;
use finance::error::ReportError;
use num_rational::Rational64;
use sqlx::types::Uuid;
use super::super::{BreakdownSort, FilterEntity, PeriodGrouping, ReportFilter};
use super::filter::SqlParam;
use super::period::{
boundaries, check_range, generate_month_boundaries, generate_quarter_boundaries,
generate_year_boundaries,
};
use super::tree::{
AccountAmounts, AccountRow, ConversionTarget, accumulate_split_converted, build_tree,
#[test]
fn test_to_sql_account_eq() {
let id = Uuid::new_v4();
let filter = ReportFilter::AccountEq(id);
let mut offset = 0;
let (sql, params) = filter.to_sql(&mut offset);
assert_eq!(sql, "s.account_id = $1");
assert_eq!(offset, 1);
assert!(matches!(¶ms[0], SqlParam::Uuid(v) if *v == id));
}
fn test_to_sql_and_combinator() {
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let filter = ReportFilter::And(vec![
ReportFilter::AccountEq(id1),
ReportFilter::CommodityEq(id2),
]);
let mut offset = 2;
assert_eq!(sql, "(s.account_id = $3 AND s.commodity_id = $4)");
assert_eq!(params.len(), 2);
assert_eq!(offset, 4);
fn test_to_sql_not() {
let filter = ReportFilter::Not(Box::new(ReportFilter::AccountEq(id)));
let (sql, _params) = filter.to_sql(&mut offset);
assert_eq!(sql, "NOT (s.account_id = $1)");
fn test_to_sql_amount_gt() {
let r = Rational64::new(100, 1);
let filter = ReportFilter::AmountGt(r);
assert_eq!(sql, "s.value_num * $2 > $1 * s.value_denom");
assert!(matches!(¶ms[0], SqlParam::I64(100)));
assert!(matches!(¶ms[1], SqlParam::I64(1)));
fn test_to_sql_counterparty_eq() {
let filter = ReportFilter::CounterpartyEq(id);
let (sql, _) = filter.to_sql(&mut offset);
assert!(sql.contains("EXISTS"));
assert!(sql.contains("o.account_id = $1"));
fn test_to_sql_tag_filter() {
let filter = ReportFilter::Tag {
entity: FilterEntity::Account,
name: "category".to_string(),
value: "food".to_string(),
assert!(sql.contains("account_tags"));
assert!(sql.contains("$1"));
assert!(sql.contains("$2"));
fn test_to_sql_or_combinator() {
let filter = ReportFilter::Or(vec![
ReportFilter::AccountEq(Uuid::new_v4()),
assert!(sql.contains(" OR "));
fn test_to_sql_account_subtree() {
let filter = ReportFilter::AccountSubtree(id);
assert!(sql.contains("WITH RECURSIVE descendants"));
fn test_to_sql_account_in() {
let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
let filter = ReportFilter::AccountIn(ids.clone());
assert_eq!(sql, "s.account_id = ANY($1)");
assert!(matches!(¶ms[0], SqlParam::UuidVec(v) if v.len() == 2));
fn test_build_tree_empty() {
let accounts: Vec<AccountRow> = vec![];
let amounts = AccountAmounts::new();
let roots = build_tree(&accounts, &amounts);
assert!(roots.is_empty());
fn test_build_tree_single_account() {
let commodity_id = Uuid::new_v4();
let accounts = vec![AccountRow {
account_id: id,
parent_id: None,
account_name: "Assets".to_string(),
account_type: None,
}];
let mut amounts = AccountAmounts::new();
amounts.insert(
id,
[(commodity_id, (Rational64::new(100, 1), "USD".to_string()))]
.into_iter()
.collect(),
);
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].account_name, "Assets");
assert_eq!(roots[0].account_path, "Assets");
assert_eq!(roots[0].depth, 0);
assert_eq!(roots[0].amounts.len(), 1);
assert_eq!(roots[0].amounts[0].amount, Rational64::new(100, 1));
fn test_build_tree_hierarchy() {
let parent_id = Uuid::new_v4();
let child_id = Uuid::new_v4();
let accounts = vec![
AccountRow {
account_id: parent_id,
},
account_id: child_id,
parent_id: Some(parent_id),
account_name: "Bank".to_string(),
];
child_id,
[(commodity_id, (Rational64::new(50, 1), "USD".to_string()))]
assert_eq!(roots[0].children.len(), 1);
assert_eq!(roots[0].children[0].account_path, "Assets:Bank");
assert_eq!(roots[0].children[0].depth, 1);
assert_eq!(roots[0].amounts[0].amount, Rational64::new(50, 1));
assert_eq!(
roots[0].children[0].amounts[0].amount,
Rational64::new(50, 1)
fn test_build_tree_prunes_empty_accounts() {
let child_with_data = Uuid::new_v4();
let child_empty = Uuid::new_v4();
let empty_root = Uuid::new_v4();
account_id: child_with_data,
account_id: child_empty,
account_name: "Empty".to_string(),
account_id: empty_root,
account_name: "Liabilities".to_string(),
child_with_data,
assert_eq!(roots.len(), 1, "empty root Liabilities should be pruned");
roots[0].children.len(),
1,
"empty child Empty should be pruned"
assert_eq!(roots[0].children[0].account_name, "Bank");
fn test_generate_month_boundaries() {
let from = NaiveDate::from_ymd_opt(2025, 1, 15)
.unwrap()
.and_hms_opt(0, 0, 0)
.and_utc();
let to = NaiveDate::from_ymd_opt(2025, 4, 1)
let boundaries = generate_month_boundaries(from, to);
assert_eq!(boundaries.len(), 3);
assert_eq!(boundaries[0].0, "2025-01");
assert_eq!(boundaries[1].0, "2025-02");
assert_eq!(boundaries[2].0, "2025-03");
fn test_generate_quarter_boundaries() {
let from = NaiveDate::from_ymd_opt(2025, 1, 1)
let to = NaiveDate::from_ymd_opt(2025, 7, 1)
let boundaries = generate_quarter_boundaries(from, to);
assert_eq!(boundaries.len(), 2);
assert_eq!(boundaries[0].0, "2025-Q1");
assert_eq!(boundaries[1].0, "2025-Q2");
fn test_generate_year_boundaries() {
let from = NaiveDate::from_ymd_opt(2024, 6, 1)
let to = NaiveDate::from_ymd_opt(2026, 3, 1)
let boundaries = generate_year_boundaries(from, to);
assert_eq!(boundaries[0].0, "2024");
assert_eq!(boundaries[1].0, "2025");
assert_eq!(boundaries[2].0, "2026");
fn test_accumulate_split_converted_same_commodity() {
let account_id = Uuid::new_v4();
let target = ConversionTarget {
commodity_id,
symbol: "USD",
let result = accumulate_split_converted(
&mut amounts,
account_id,
Rational64::new(100, 1),
"USD",
&target,
(None, None),
assert!(result.is_ok());
amounts[&account_id][&commodity_id].0,
Rational64::new(100, 1)
fn test_accumulate_split_converted_with_price() {
let from_commodity = Uuid::new_v4();
let target_commodity = Uuid::new_v4();
commodity_id: target_commodity,
from_commodity,
"EUR",
(Some(1176), Some(1000)),
amounts[&account_id][&target_commodity].0,
Rational64::new(100, 1) * Rational64::new(1176, 1000)
fn test_accumulate_split_converted_missing_price() {
commodity_id: Uuid::new_v4(),
Uuid::new_v4(),
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ReportError::MissingConversion { .. }));
mod sexpr_tests {
use super::*;
fn test_from_sexpr_account_eq() {
let input = format!("(account= \"{id}\")");
let filter = ReportFilter::from_sexpr(&input).unwrap();
assert!(matches!(filter, ReportFilter::AccountEq(v) if v == id));
fn test_from_sexpr_commodity_eq() {
let input = format!("(commodity= \"{id}\")");
assert!(matches!(filter, ReportFilter::CommodityEq(v) if v == id));
fn test_from_sexpr_amount_gt() {
let input = "(amount> 100)";
let filter = ReportFilter::from_sexpr(input).unwrap();
assert!(matches!(filter, ReportFilter::AmountGt(r) if r == Rational64::new(100, 1)));
fn test_from_sexpr_and() {
let input = format!("(and (account= \"{id1}\") (commodity= \"{id2}\"))");
assert!(matches!(filter, ReportFilter::And(v) if v.len() == 2));
fn test_from_sexpr_not() {
let input = format!("(not (account= \"{id}\"))");
assert!(matches!(filter, ReportFilter::Not(_)));
fn test_from_sexpr_tag() {
let input = "(account-tag= \"category\" \"food\")";
match filter {
ReportFilter::Tag {
entity,
name,
value,
} => {
assert!(matches!(entity, FilterEntity::Account));
assert_eq!(name, "category");
assert_eq!(value, "food");
_ => panic!("Expected Tag filter"),
fn test_from_sexpr_account_subtree() {
let input = format!("(account-subtree \"{id}\")");
assert!(matches!(filter, ReportFilter::AccountSubtree(v) if v == id));
fn test_from_sexpr_invalid() {
let result = ReportFilter::from_sexpr("invalid");
fn test_from_sexpr_counterparty() {
let input = format!("(counterparty= \"{id}\")");
assert!(matches!(filter, ReportFilter::CounterpartyEq(v) if v == id));
fn test_from_sexpr_or() {
let input = format!("(or (account= \"{id1}\") (account= \"{id2}\"))");
assert!(matches!(filter, ReportFilter::Or(v) if v.len() == 2));
fn test_period_grouping_deserializes_from_web_forms() {
let cases = [
("\"month\"", PeriodGrouping::Month),
("\"quarter\"", PeriodGrouping::Quarter),
("\"year\"", PeriodGrouping::Year),
for (input, expected) in cases {
let parsed: PeriodGrouping = serde_json::from_str(input).unwrap();
assert_eq!(parsed, expected);
fn test_period_grouping_rejects_invalid() {
assert!(serde_json::from_str::<PeriodGrouping>("\"weekly\"").is_err());
fn test_breakdown_sort_deserializes_from_web_forms() {
("\"amount_desc\"", BreakdownSort::AmountDesc),
("\"amount_asc\"", BreakdownSort::AmountAsc),
("\"name_asc\"", BreakdownSort::NameAsc),
("\"name_desc\"", BreakdownSort::NameDesc),
let parsed: BreakdownSort = serde_json::from_str(input).unwrap();
fn test_breakdown_sort_default_is_amount_desc() {
assert_eq!(BreakdownSort::default(), BreakdownSort::AmountDesc);
fn test_activity_group_serde_round_trip() {
use super::super::ActivityGroup;
let g = ActivityGroup {
label: "Savings".into(),
filter: ReportFilter::Tag {
name: "type".into(),
value: "savings".into(),
flip_sign: true,
let json = serde_json::to_string(&g).unwrap();
let parsed: ActivityGroup = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.label, g.label);
assert_eq!(parsed.flip_sign, g.flip_sign);
// filter equality: easier to compare via re-serialization
let re_json = serde_json::to_string(&parsed).unwrap();
assert_eq!(json, re_json);
fn test_activity_group_flip_sign_defaults_false() {
let json = r#"{"label":"Expense","filter":{"op":"tag","args":{"entity":"account","name":"type","value":"expense"}}}"#;
let parsed: ActivityGroup = serde_json::from_str(json).unwrap();
assert!(!parsed.flip_sign);
/// A range ending at chrono's last representable year must terminate.
///
/// `%Y-%m-%d` parses `+262142-12-01` from the report's own query parameters, and
/// the `next_*` helpers step past `NaiveDate::MAX`. If that step yields anything
/// below `to`, the generators walk backwards and loop forever, allocating as
/// they go — an authenticated request that never returns.
fn boundaries_terminate_at_the_end_of_representable_time() {
let from = NaiveDate::from_ymd_opt(262_142, 12, 1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.expect("last representable month")
let to = NaiveDate::from_ymd_opt(262_142, 12, 31)
.and_then(|d| d.and_hms_opt(23, 59, 59))
.expect("last representable day")
for periods in [
generate_month_boundaries(from, to),
generate_quarter_boundaries(from, to),
generate_year_boundaries(from, to),
] {
assert_eq!(periods.len(), 1, "one period, not an unbounded walk");
assert_eq!(periods[0].2, to, "the final period is clamped to `to`");
/// A range wider than a report can usefully be must be refused, not generated.
/// The callers issue one query per period per group, so `+262142-12-31` — which
/// `%Y-%m-%d` accepts from a query parameter — would otherwise mean millions of
/// sequential queries against a cluster shared with other tenants.
fn an_absurd_report_range_is_refused() {
let from = NaiveDate::from_ymd_opt(2026, 1, 1)
.expect("valid")
for grouping in [
PeriodGrouping::Month,
PeriodGrouping::Quarter,
PeriodGrouping::Year,
assert!(
boundaries(grouping, from, to).is_err(),
"an unbounded span must be refused, not walked"
boundaries(PeriodGrouping::Month, to, from).is_err(),
"an inverted range must be refused"
boundaries(PeriodGrouping::Month, from, from).is_err(),
"an empty range must be refused"
boundaries(
from,
NaiveDate::from_ymd_opt(2027, 1, 1)
.and_utc()
)
.is_ok(),
"an ordinary range still works"
/// The first bucket is aligned to its calendar unit, so without clamping the
/// report would sum transactions from before the requested start and contradict
/// the `date_from` it echoes back.
fn the_first_period_never_starts_before_the_requested_range() {
let at = |y, m, d| {
NaiveDate::from_ymd_opt(y, m, d)
.and_then(|x| x.and_hms_opt(0, 0, 0))
let from = at(2026, 5, 15);
let to = at(2026, 8, 20);
let periods = boundaries(grouping, from, to).expect("an ordinary range");
periods.first().expect("at least one period").1,
"{grouping:?} must not reach back before `from`"
assert!(periods.iter().all(|p| p.1 >= from && p.2 <= to));
/// The report runs one query per period PER GROUP. Bounding only the periods
/// leaves the product unbounded — `groups` is JSON on the query string with no
/// length of its own.
fn the_query_budget_bounds_periods_times_groups() {
use super::period::query_budget;
query_budget(600, 6, 4096).is_ok(),
"an ordinary report runs"
query_budget(600, 7, 4096).is_err(),
"the same span with one more group exceeds the budget"
// The ungrouped path still issues one query per group.
query_budget(0, 5000, 4096).is_err(),
"groups alone are bounded"
assert!(query_budget(0, 0, 4096).is_ok(), "no groups, no queries");
/// The cap is counted in the grouping actually requested: a 51-year YEARLY
/// report is 51 rows and must not be refused for spanning 612 months.
fn the_span_cap_counts_the_requested_grouping() {
let at = |y| {
NaiveDate::from_ymd_opt(y, 1, 1)
boundaries(PeriodGrouping::Year, at(1975), at(2026)).is_ok(),
"51 years of yearly rows is a legitimate report"
boundaries(PeriodGrouping::Month, at(1975), at(2026)).is_err(),
"the same span monthly is 612 rows and is refused"
/// Bounding how many queries run says nothing about the cost of each one: a
/// caller-shaped filter becomes bind parameters and correlated subqueries.
fn filter_leaves_are_counted_for_the_size_bound() {
let leaf = ReportFilter::AccountEq(Uuid::new_v4());
assert_eq!(leaf.leaves(), 1);
let wide = ReportFilter::Or(
(0..50)
.map(|_| ReportFilter::AccountEq(Uuid::new_v4()))
assert_eq!(wide.leaves(), 50);
let ids: Vec<Uuid> = (0..30).map(|_| Uuid::new_v4()).collect();
ReportFilter::AccountIn(ids).leaves(),
30,
"an In list is its length"
let nested = ReportFilter::Not(Box::new(ReportFilter::And(vec![
ReportFilter::Or(
(0..40)
),
])));
assert_eq!(nested.leaves(), 41, "nesting does not hide leaves");
assert!(nested.leaves() < super::filter::MAX_FILTER_LEAVES);
/// Every command that accepts a filter must enforce the size cap. Guarding one
/// of the three is how the bound ends up protecting a single caller — which is
/// exactly how this defect was introduced.
fn every_filter_accepting_command_enforces_the_size_cap() {
let sources = [
include_str!("activity.rs"),
include_str!("balance.rs"),
include_str!("category_breakdown.rs"),
for src in sources {
src.contains("check_filter_size"),
"a command accepts `report_filter` without checking its size"
/// The cap itself: an over-large filter is refused, an ordinary one is not.
fn the_filter_size_cap_refuses_an_oversized_filter() {
use super::filter::{MAX_FILTER_LEAVES, check_filter_size};
assert!(check_filter_size(MAX_FILTER_LEAVES).is_ok());
assert!(check_filter_size(MAX_FILTER_LEAVES + 1).is_err());
assert!(check_filter_size(0).is_ok(), "no filter is not an error");
/// Each refusal must name the dimension the caller actually blew, with the real
/// numbers. A bound that fires with the wrong message sends the user to change
/// the wrong thing.
fn every_report_refusal_reports_its_own_dimension() {
use super::activity::MAX_QUERIES;
use crate::command::CmdError;
let msg = |err: CmdError| match err {
CmdError::Args(m) => m,
other => panic!("every report refusal must be `Args`, got {other:?}"),
// Too many queries: names the count and the budget, not the filter.
let m = msg(query_budget(600, 100, MAX_QUERIES).expect_err("60000 queries"));
m.contains("60000") && m.contains(&MAX_QUERIES.to_string()),
"{m}"
m.contains("queries"),
"must name queries, not conditions: {m}"
// Too big a filter: names the conditions and the cap, not the query count.
let m = msg(check_filter_size(MAX_FILTER_LEAVES + 1).expect_err("65 leaves"));
m.contains("65") && m.contains(&MAX_FILTER_LEAVES.to_string()),
m.contains("conditions"),
"must name conditions, not queries: {m}"
// Inverted range, and its equal-bounds twin.
for (from, to) in [
(at(2026, 5, 2), at(2026, 5, 1)),
(at(2026, 5, 1), at(2026, 5, 1)),
let m = msg(boundaries(PeriodGrouping::Month, from, to).expect_err("bad range"));
assert!(m.contains("ends before it starts"), "{m}");
// Too wide a span: names periods in the requested grouping.
let m =
msg(boundaries(PeriodGrouping::Month, at(1900, 1, 1), at(2026, 1, 1)).expect_err("wide"));
assert!(m.contains("periods") && m.contains("600"), "{m}");
/// The per-query cost is the report filter plus the LARGEST group, because each
/// query is `And(group.filter, report_filter)` for one group. Summing refuses
/// requests that cost nothing near the bound.
fn the_widest_query_is_the_largest_group_not_their_sum() {
use super::activity::widest_query_leaves;
use crate::command::ActivityGroup;
let one = |n: usize| ActivityGroup {
label: String::new(),
filter: ReportFilter::Or(
(0..n)
flip_sign: false,
let many_small: Vec<ActivityGroup> = (0..100).map(|_| one(1)).collect();
widest_query_leaves(None, &many_small),
"100 one-condition groups are 100 queries of ONE leaf, not a 100-leaf query"
super::filter::check_filter_size(widest_query_leaves(None, &many_small)).is_ok(),
"a legitimate many-group report must not be refused for filter size"
// The report filter still adds to whichever group is widest.
let report = ReportFilter::Or(
(0..10)
assert_eq!(widest_query_leaves(Some(&report), &[one(5), one(20)]), 30);
widest_query_leaves(None, &[]),
0,
"no groups, no filter, no cost"
/// The cap must count what the generators actually emit. The old estimate
/// rounded the elapsed month count, which refused a 600-month report and let
/// 601 quarters and 601 years straight through.
fn the_period_cap_counts_the_buckets_that_will_be_generated() {
let at = |y: i32, m: u32, d: u32| {
.expect("valid date")
.expect("valid time")
// Exactly 600 monthly buckets: allowed, and 600 of them.
let months = boundaries(PeriodGrouping::Month, at(2000, 1, 1), at(2050, 1, 1))
.expect("600 months is within the cap");
assert_eq!(months.len(), 600);
// 601 buckets, one over: refused for every grouping, including the two
// that used to slip past because the estimate undercounted.
for (grouping, from, to) in [
(PeriodGrouping::Month, at(2000, 1, 1), at(2050, 2, 1)),
(PeriodGrouping::Quarter, at(2000, 3, 1), at(2150, 1, 2)),
(PeriodGrouping::Year, at(2000, 12, 1), at(2600, 1, 2)),
let err = boundaries(grouping, from, to).expect_err("601 buckets must be refused");
format!("{err:?}").contains("601"),
"must report the real bucket count: {err:?}"
/// An empty `And`/`Or` used to render as `()`, a Postgres syntax error the
/// user could trigger with `(and)` — a 500 where a filter should just be
/// vacuous. Reachable from the activity `groups` JSON too, which never passes
/// through the s-expression parser.
fn an_empty_conjunction_renders_as_its_identity() {
assert_eq!(ReportFilter::And(vec![]).to_sql(&mut 1).0, "TRUE");
assert_eq!(ReportFilter::Or(vec![]).to_sql(&mut 1).0, "FALSE");
let nested = ReportFilter::And(vec![
ReportFilter::AccountEq(Uuid::nil()),
ReportFilter::Or(vec![]),
!nested.to_sql(&mut 1).0.contains("()"),
"an empty child must not leave an empty parenthesis in the SQL"
/// `from_sexpr` parsed one expression and threw away the rest, so a second
/// condition silently did not apply.
fn a_filter_must_be_exactly_one_expression() {
let one = format!("(account= \"{}\")", Uuid::nil());
ReportFilter::from_sexpr(&one).expect("one expression parses");
let two = format!("{one} {one}");
ReportFilter::from_sexpr(&two).is_err(),
"the second condition must not be dropped"
assert!(ReportFilter::from_sexpr("").is_err());
/// A leap second is a representable `DateTime<Utc>` STRICTLY GREATER than
/// `MAX_UTC`, which every `next_*` saturates to — so the generator stepped to
/// `MAX_UTC`, stayed `< to`, and appended periods until memory ran out.
fn a_leap_second_end_does_not_run_the_generator_forever() {
let leap = chrono::NaiveDate::MAX
.and_hms_nano_opt(23, 59, 59, 1_000_000_000)
.expect("chrono represents a leap second")
assert!(leap > chrono::DateTime::<chrono::Utc>::MAX_UTC);
let periods = boundaries(PeriodGrouping::Month, from, leap).expect("one month");
assert_eq!(periods.len(), 1);
/// Extra arguments were dropped in silence, so a second condition the user
/// wrote simply did not apply.
fn a_filter_operator_refuses_extra_arguments() {
assert!(ReportFilter::from_sexpr("(amount> 0 (amount< 100))").is_err());
assert!(ReportFilter::from_sexpr(&format!("(account= \"{}\" 1)", Uuid::nil())).is_err());
assert!(ReportFilter::from_sexpr("(transaction-tag= \"a\" \"b\" \"c\")").is_err());
// The variadic and single-argument forms still parse.
ReportFilter::from_sexpr("(and (amount> 0) (amount< 100))").expect("and is variadic");
ReportFilter::from_sexpr("(amount> 0)").expect("one argument is right");
/// `parse_uuid_list_arg` consumes every remaining argument, so the `*-in`
/// forms are variadic; an arity table that called them one-argument silently
/// broke every multi-account filter.
fn the_in_operators_take_more_than_one_id() {
let two = format!("(account-in \"{}\" \"{}\")", Uuid::nil(), Uuid::max());
let filter = ReportFilter::from_sexpr(&two).expect("account-in is variadic");
assert!(matches!(filter, ReportFilter::AccountIn(ids) if ids.len() == 2));
for op in ["counterparty-in", "commodity-in"] {
ReportFilter::from_sexpr(&format!("({op} \"{}\" \"{}\")", Uuid::nil(), Uuid::max()))
.unwrap_or_else(|e| panic!("{op} is variadic: {e:?}"));
// Still bounded below: an empty list is not a filter.
assert!(ReportFilter::from_sexpr("(account-in)").is_err());
/// A reversed range is refused whether or not the report is grouped: without
/// the shared check an ungrouped one matched nothing and returned an empty 200.
fn a_reversed_range_is_refused_without_grouping() {
let at = |d: u32| {
NaiveDate::from_ymd_opt(2026, 9, d)
assert!(check_range(at(10), at(1)).is_err());
check_range(at(1), at(1)).is_err(),
"an empty range ends as it starts"
assert!(check_range(at(1), at(10)).is_ok());