#![allow(clippy::unwrap_used)]
use probar_js_gen::prelude::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_valid_identifier_accepted(
name in "[a-zA-Z_$][a-zA-Z0-9_$]{0,20}"
) {
if !Identifier::RESERVED_WORDS.contains(&name.as_str()) {
let result = Identifier::new(&name);
prop_assert!(result.is_ok(), "Valid identifier rejected: {}", name);
}
}
#[test]
fn prop_empty_identifier_rejected(
_prefix in "[a-zA-Z]{0,5}"
) {
let result = Identifier::new("");
prop_assert!(result.is_err(), "Empty identifier should be rejected");
}
#[test]
fn prop_digit_start_rejected(
digit in "[0-9]",
suffix in "[a-zA-Z0-9_$]{0,10}"
) {
let name = format!("{}{}", digit, suffix);
let result = Identifier::new(&name);
prop_assert!(result.is_err(), "Digit-start should be rejected: {}", name);
}
#[test]
fn prop_reserved_word_rejected(
idx in 0..Identifier::RESERVED_WORDS.len()
) {
let word = Identifier::RESERVED_WORDS[idx];
let result = Identifier::new(word);
prop_assert!(result.is_err(), "Reserved word should be rejected: {}", word);
}
#[test]
fn prop_invalid_chars_rejected(
valid_prefix in "[a-zA-Z_$]{1,5}",
invalid_char in "[-!@#%^&*()+=\\[\\]{};':\"<>,./? ]",
valid_suffix in "[a-zA-Z0-9_$]{0,5}"
) {
let name = format!("{}{}{}", valid_prefix, invalid_char, valid_suffix);
let result = Identifier::new(&name);
prop_assert!(result.is_err(), "Invalid char should be rejected: {}", name);
}
}
proptest! {
#[test]
fn prop_number_roundtrip(n in any::<i32>()) {
let expr = Expr::num(f64::from(n));
let module = JsModuleBuilder::new()
.let_decl("x", expr).unwrap()
.build();
let js = generate(&module);
prop_assert!(js.contains(&n.to_string()), "Number not in output: {}", n);
}
#[test]
fn prop_string_quoted(
s in "[a-zA-Z0-9 ]{1,50}" ) {
let expr = Expr::str(&s);
let module = JsModuleBuilder::new()
.const_decl("s", expr).unwrap()
.build();
let js = generate(&module);
prop_assert!(
js.contains(&format!(r#""{}""#, s)),
"String not properly quoted in output"
);
}
#[test]
fn prop_bool_literal(b in any::<bool>()) {
let expr = Expr::bool(b);
let module = JsModuleBuilder::new()
.let_decl("b", expr).unwrap()
.build();
let js = generate(&module);
let expected = if b { "true" } else { "false" };
prop_assert!(js.contains(expected), "Bool not found: {}", b);
}
}
proptest! {
#[test]
fn prop_no_forbidden_patterns(
var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
value in any::<i32>()
) {
if Identifier::RESERVED_WORDS.contains(&var_name.as_str()) {
return Ok(());
}
let module = JsModuleBuilder::new()
.let_decl(&var_name, Expr::num(f64::from(value))).unwrap()
.build();
let js = generate(&module);
for pattern in probar_js_gen::validator::FORBIDDEN_PATTERNS {
prop_assert!(
!js.contains(pattern),
"Forbidden pattern '{}' found in output",
pattern
);
}
}
#[test]
fn prop_deterministic_generation(
var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
value in any::<i32>()
) {
if Identifier::RESERVED_WORDS.contains(&var_name.as_str()) {
return Ok(());
}
let build = || {
JsModuleBuilder::new()
.let_decl(&var_name, Expr::num(f64::from(value))).unwrap()
.build()
};
let js1 = generate(&build());
let js2 = generate(&build());
prop_assert_eq!(js1, js2, "Generation not deterministic");
}
}
proptest! {
#[test]
fn prop_hash_changes_with_content(
content1 in "[a-zA-Z0-9 ]{1,100}",
content2 in "[a-zA-Z0-9 ]{1,100}"
) {
if content1 != content2 {
let hash1 = probar_js_gen::manifest::hash_file_contents(&content1);
let hash2 = probar_js_gen::manifest::hash_file_contents(&content2);
prop_assert_ne!(hash1, hash2, "Different content should have different hash");
}
}
#[test]
fn prop_hash_deterministic(
content in "[a-zA-Z0-9 ]{1,100}"
) {
let hash1 = probar_js_gen::manifest::hash_file_contents(&content);
let hash2 = probar_js_gen::manifest::hash_file_contents(&content);
prop_assert_eq!(hash1, hash2, "Hash not deterministic");
}
}
proptest! {
#[test]
fn prop_binop_parenthesized(
a in -1000i32..1000i32,
b in -1000i32..1000i32
) {
let expr = Expr::num(f64::from(a)).add(Expr::num(f64::from(b)));
let module = JsModuleBuilder::new()
.let_decl("result", expr).unwrap()
.build();
let js = generate(&module);
prop_assert!(js.contains('('), "Missing opening paren");
prop_assert!(js.contains(')'), "Missing closing paren");
prop_assert!(js.contains('+'), "Missing operator");
}
}
proptest! {
#[test]
fn prop_extends_has_super(
class_name in "[A-Z][a-zA-Z0-9]{0,10}",
parent_name in "[A-Z][a-zA-Z0-9]{0,10}"
) {
if Identifier::RESERVED_WORDS.contains(&class_name.as_str())
|| Identifier::RESERVED_WORDS.contains(&parent_name.as_str())
{
return Ok(());
}
let class = JsClassBuilder::new(&class_name).unwrap()
.extends(&parent_name).unwrap()
.constructor(vec![])
.build();
let module = JsModuleBuilder::new().class(class).build();
let js = generate(&module);
prop_assert!(
js.contains("super()"),
"Class with extends must have super() in constructor"
);
}
#[test]
fn prop_no_extends_no_super(
class_name in "[A-Z][a-zA-Z0-9]{0,10}"
) {
if Identifier::RESERVED_WORDS.contains(&class_name.as_str()) {
return Ok(());
}
let class = JsClassBuilder::new(&class_name).unwrap()
.constructor(vec![])
.build();
let module = JsModuleBuilder::new().class(class).build();
let js = generate(&module);
prop_assert!(
!js.contains("super()"),
"Class without extends must NOT have super()"
);
}
}
proptest! {
#[test]
fn prop_special_chars_escaped(
prefix in "[a-zA-Z]{0,10}",
suffix in "[a-zA-Z]{0,10}"
) {
for (char, escaped) in &[
('"', "\\\""),
('\\', "\\\\"),
('\n', "\\n"),
('\r', "\\r"),
('\t', "\\t"),
] {
let s = format!("{}{}{}", prefix, char, suffix);
let expr = Expr::str(&s);
let module = JsModuleBuilder::new()
.const_decl("s", expr).unwrap()
.build();
let js = generate(&module);
prop_assert!(
js.contains(escaped),
"Character {:?} should be escaped to '{}'",
char,
escaped
);
}
}
}