#![allow(clippy::result_large_err)]
use qlexpress::check_options::CheckOptions;
use qlexpress::exception::error_codes;
use qlexpress::init_options::InitOptions;
use qlexpress::operator::operator_check_strategy::OperatorCheckStrategy;
use qlexpress::ql_options::QLOptions;
use qlexpress::Express4Runner;
fn runner() -> Express4Runner {
Express4Runner::with_init_options(InitOptions::default())
}
fn ok(runner: &Express4Runner, script: &str, opts: CheckOptions) {
runner.check(script, &opts).expect("check should pass");
}
fn err(
runner: &Express4Runner,
script: &str,
opts: CheckOptions,
) -> qlexpress::exception::QLSyntaxException {
runner.check(script, &opts).expect_err("check should fail")
}
#[test]
fn whitelist_allows_listed_operators() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["+", "*"].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "a + b * c", opts);
}
#[test]
fn whitelist_blocks_unlisted_assignment() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["+", "*"].into_iter().map(String::from).collect(),
))
.build();
let e = err(&runner(), "a = b + c", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.pos(), 2);
assert_eq!(e.line_no(), 1);
assert_eq!(e.col_no(), 3);
assert_eq!(e.err_lexeme(), "=");
assert!(e.reason().contains("Script uses disallowed operator"));
assert!(e.reason().contains('='));
let message = e.to_string();
assert!(message.contains("OPERATOR_NOT_ALLOWED"));
assert!(message.contains("Line: 1"));
assert!(message.contains("Column: 3"));
}
#[test]
fn blacklist_allows_unblocked() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(
["="].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "a + b * c - d / e", opts);
}
#[test]
fn blacklist_blocks_listed() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(
["="].into_iter().map(String::from).collect(),
))
.build();
let e = err(&runner(), "a = b + c", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.pos(), 2);
assert_eq!(e.line_no(), 1);
assert_eq!(e.col_no(), 3);
assert_eq!(e.err_lexeme(), "=");
assert!(e.reason().contains("Script uses disallowed operator"));
assert!(e.reason().contains('='));
let message = e.to_string();
assert!(message.contains("OPERATOR_NOT_ALLOWED"));
assert!(message.contains("Line: 1"));
assert!(message.contains("Column: 3"));
}
#[test]
fn whitelist_with_prefix_operator() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["++", "+"].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "++a + b", opts.clone());
let e = err(&runner(), "--a + b", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.err_lexeme(), "--");
assert_eq!(e.line_no(), 1);
assert_eq!(e.col_no(), 1);
}
#[test]
fn whitelist_with_suffix_operator() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["++", "+"].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "a++ + b", opts.clone());
let e = err(&runner(), "a-- + b", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.err_lexeme(), "--");
assert_eq!(e.line_no(), 1);
assert_eq!(e.col_no(), 2);
}
#[test]
fn blacklist_with_multiple_operators() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(
["=", "*"].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "a + b - c", opts.clone());
let e1 = err(&runner(), "a = b", opts.clone());
assert_eq!(e1.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e1.err_lexeme(), "=");
let e2 = err(&runner(), "a * b", opts);
assert_eq!(e2.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e2.err_lexeme(), "*");
}
#[test]
fn empty_whitelist_blocks_everything() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(Default::default()))
.build();
let e = err(&runner(), "a + b", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.err_lexeme(), "+");
}
#[test]
fn empty_blacklist_allows_everything() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(Default::default()))
.build();
ok(&runner(), "a = b + c * d / e % f", opts.clone());
ok(&runner(), "++a--", opts);
}
#[test]
fn null_whitelist_normalizes_to_empty_set() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(Default::default()))
.build();
let e = err(&runner(), "a + b", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.err_lexeme(), "+");
}
#[test]
fn null_blacklist_normalizes_to_empty_set() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(Default::default()))
.build();
ok(&runner(), "a = b + c * d / e % f", opts.clone());
ok(&runner(), "++a--", opts);
}
#[test]
fn multi_line_error_points_at_correct_line() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["+"].into_iter().map(String::from).collect(),
))
.build();
let e = err(&runner(), "a + b\nc = d\ne + f", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(e.err_lexeme(), "=");
assert_eq!(e.line_no(), 2);
assert_eq!(e.col_no(), 3);
}
#[test]
fn complex_expression_blocks_inner_assignment() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["+", "*"].into_iter().map(String::from).collect(),
))
.build();
ok(&runner(), "(a + b) * (c + d)", opts.clone());
let division = err(&runner(), "(a + b) * (c + d) / e", opts.clone());
assert_eq!(division.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(division.err_lexeme(), "/");
let assignment = err(&runner(), "(a + b) * (c = d)", opts);
assert_eq!(assignment.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert_eq!(assignment.err_lexeme(), "=");
assert!(assignment.pos() > 10);
}
#[test]
fn error_message_includes_operator() {
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Whitelist(
["+"].into_iter().map(String::from).collect(),
))
.build();
let e = err(&runner(), "a * b", opts);
assert_eq!(e.error_code(), error_codes::OPERATOR_NOT_ALLOWED);
assert!(e.reason().contains('*'));
assert!(e.reason().contains('+') || e.reason().contains("allowed"));
}
#[test]
fn default_check_options_allows_everything() {
let opts = CheckOptions::default();
ok(&runner(), "a + b; c * d; e = f; g - h; i / j", opts);
}
#[test]
fn runner_execute_unaffected_by_check_strategy() {
let _opts = QLOptions::builder().build();
let opts = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::Blacklist(
["="].into_iter().map(String::from).collect(),
))
.build();
assert!(runner().check("a = 1", &opts).is_err());
}
#[test]
fn custom_operator_strategy_observes_dynamic_host_policy() {
use std::cell::Cell;
use std::collections::HashSet;
use std::rc::Rc;
let allow_plus = Rc::new(Cell::new(false));
let captured = Rc::clone(&allow_plus);
let options = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::custom(
move |operator| operator != "+" || captured.get(),
HashSet::from(["+".to_string()]),
))
.build();
let runner = runner();
assert_eq!(
runner
.check("1 + 2", &options)
.expect_err("custom policy initially denies plus")
.error_code(),
error_codes::OPERATOR_NOT_ALLOWED
);
allow_plus.set(true);
runner
.check("1 + 2", &options)
.expect("existing options must observe the updated custom policy");
}
#[test]
fn shared_operator_whitelist_observes_backing_set_mutation() {
use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;
let allowed = Rc::new(RefCell::new(HashSet::from(["*".to_string()])));
let options = CheckOptions::builder()
.operator_check_strategy(OperatorCheckStrategy::shared_whitelist(Rc::clone(&allowed)))
.build();
let runner = runner();
assert_eq!(
runner
.check("1 + 2", &options)
.expect_err("plus is absent from the initial backing set")
.error_code(),
error_codes::OPERATOR_NOT_ALLOWED
);
allowed.borrow_mut().insert("+".to_string());
runner
.check("1 + 2", &options)
.expect("existing options must observe the added operator");
allowed.borrow_mut().remove("+");
assert_eq!(
runner
.check("1 + 2", &options)
.expect_err("removing the operator must revoke later checks")
.error_code(),
error_codes::OPERATOR_NOT_ALLOWED
);
}