use vb6interpret::run_source;
use vb6interpret::Interpreter;
use vb6parse::files::ModuleFile;
use vb6parse::io::SourceFile;
use vb6runtime::state::settings as settings_state;
use std::sync::Mutex;
static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
fn run(body: &str) -> Vec<String> {
let source = format!("Attribute VB_Name = \"M\"\nSub Main()\n{}\nEnd Sub\n", body);
run_source(&source).expect("interpretation failed")
}
fn run_final_line(source: &str) -> usize {
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
let _ = interpreter.run_module(&module);
interpreter.current_line()
}
#[test]
fn arithmetic_and_concat() {
let out = run(" Debug.Print 2 + 3 * 4\n\
Debug.Print 10 \\ 3\n\
Debug.Print 10 Mod 3\n\
Debug.Print 2 ^ 10\n\
Debug.Print \"a\" & \"b\" & 1\n");
assert_eq!(out, vec!["14", "3", "1", "1024", "ab1"]);
}
#[test]
fn integer_types() {
let out = run(" Dim i As Integer\n\
Dim l As Long\n\
i = 32767\n\
l = i + 1\n\
Debug.Print i\n\
Debug.Print l\n");
assert_eq!(out, vec!["32767", "32768"]);
}
#[test]
fn string_functions() {
let out = run(" Dim s As String\n\
s = \" Hello World \"\n\
Debug.Print Trim(s)\n\
Debug.Print UCase(s)\n\
Debug.Print Left(s, 5)\n\
Debug.Print Right(s, 5)\n\
Debug.Print Mid(s, 8, 5)\n\
Debug.Print Len(s)\n\
Debug.Print InStr(\"hello\", \"ll\")\n\
Debug.Print Chr(65) & Chr(66)\n\
Debug.Print Space(3) & \"!\"\n");
assert_eq!(
out,
vec![
"Hello World",
" HELLO WORLD ",
" Hel",
"rld ",
" Worl",
"15",
"3",
"AB",
" !",
]
);
}
#[test]
fn string_function_suffix_variants() {
let out = run(" Debug.Print Chr$(65) & Chr$(66)\n\
Debug.Print ChrB$(65) & ChrB$(66)\n\
Debug.Print ChrW$(65) & ChrW$(66)\n\
Debug.Print Trim$(\" hi \")\n");
assert_eq!(out, vec!["AB", "AB", "AB", "hi"]);
}
#[test]
fn lset_statement_pads_on_the_right() {
let out = run(" Dim s As String\n\
s = String(10, \"X\")\n\
LSet s = \"Left\"\n\
Debug.Print \"[\" & s & \"]\"\n");
assert_eq!(out, vec!["[Left ]"]);
}
#[test]
fn lset_statement_truncates_long_sources() {
let out = run(" Dim s As String\n\
s = \"ABCDEFGH\"\n\
LSet s = \"1234567890\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["12345678"]);
}
#[test]
fn lset_statement_accepts_a_variable_source() {
let out = run(" Dim s As String\n\
Dim t As String\n\
s = String(6, \"*\")\n\
t = \"abc\"\n\
LSet s = t\n\
Debug.Print \"[\" & s & \"]\"\n");
assert_eq!(out, vec!["[abc ]"]);
}
#[test]
fn lset_statement_keeps_exact_fit_unchanged() {
let out = run(" Dim s As String\n\
s = \"abc\"\n\
LSet s = \"xyz\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["xyz"]);
}
#[test]
fn rset_statement_pads_on_the_left() {
let out = run(" Dim s As String\n\
s = String(10, \"X\")\n\
RSet s = \"Right\"\n\
Debug.Print \"[\" & s & \"]\"\n");
assert_eq!(out, vec!["[ Right]"]);
}
#[test]
fn rset_statement_truncates_long_sources() {
let out = run(" Dim s As String\n\
s = \"ABCDEFGH\"\n\
RSet s = \"1234567890\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["34567890"]);
}
#[test]
fn rset_statement_accepts_a_variable_source() {
let out = run(" Dim s As String\n\
Dim t As String\n\
s = String(6, \"*\")\n\
t = \"abc\"\n\
RSet s = t\n\
Debug.Print \"[\" & s & \"]\"\n");
assert_eq!(out, vec!["[ abc]"]);
}
#[test]
fn rset_statement_keeps_exact_fit_unchanged() {
let out = run(" Dim s As String\n\
s = \"abc\"\n\
RSet s = \"xyz\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["xyz"]);
}
#[test]
fn mid_statement_replaces_with_explicit_length() {
let out = run(" Dim s As String\n\
s = \"Hello World\"\n\
Mid(s, 7, 5) = \"VB6!!\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["Hello VB6!!"]);
}
#[test]
fn mid_statement_without_length_replaces_to_the_end() {
let out = run(" Dim s As String\n\
s = \"ABCDEFGH\"\n\
Mid(s, 3) = \"123\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["AB123FGH"]);
}
#[test]
fn mid_statement_shorter_replacement_keeps_the_tail() {
let out = run(" Dim s As String\n\
s = \"Test\"\n\
Mid(s, 2, 2) = \"XX\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["TXXt"]);
}
#[test]
fn midb_statement_replaces_bytes() {
let out = run(" Dim s As String\n\
s = \"ABCDEFGH\"\n\
MidB(s, 3, 4) = \"12\"\n\
Debug.Print s\n");
assert_eq!(out, vec!["A12DEFGH"]);
}
#[test]
fn math_functions() {
let out = run(" Debug.Print Abs(-5)\n\
Debug.Print Sqr(16)\n\
Debug.Print Int(3.7)\n\
Debug.Print Fix(-3.7)\n\
Debug.Print Sgn(-7)\n\
Debug.Print Round(123.456, 2)\n\
Debug.Print Exp(0)\n\
Debug.Print Log(1)\n");
assert_eq!(out, vec!["5", "4", "3", "-3", "-1", "123.46", "1", "0"]);
}
#[test]
fn if_elseif_else_block() {
let out = run(" Dim n As Integer\n\
n = 7\n\
If n < 5 Then\n\
Debug.Print \"low\"\n\
ElseIf n < 10 Then\n\
Debug.Print \"mid\"\n\
Else\n\
Debug.Print \"high\"\n\
End If\n\
If n < 5 Then Debug.Print \"one\" Else Debug.Print \"two\"\n");
assert_eq!(out, vec!["mid", "two"]);
}
#[test]
fn select_case() {
let out = run(" Dim a As Long\n\
a = 4\n\
Select Case a\n\
Case 1, 2\n\
Debug.Print \"low\"\n\
Case 3 To 5\n\
Debug.Print \"three-to-five\"\n\
Case Is > 100\n\
Debug.Print \"big\"\n\
Case Else\n\
Debug.Print \"other\"\n\
End Select\n");
assert_eq!(out, vec!["three-to-five"]);
}
#[test]
fn for_loop_with_step() {
let out = run(" Dim total As Long\n\
Dim i As Integer\n\
For i = 1 To 10 Step 2\n\
total = total + i\n\
Next i\n\
Debug.Print total\n\
Dim j As Integer\n\
For j = 5 To 1 Step -1\n\
Debug.Print j\n\
Next j\n");
assert_eq!(out, vec!["25", "5", "4", "3", "2", "1"]);
}
#[test]
fn do_and_while_loops() {
let out = run(" Dim x As Long\n\
x = 1\n\
Do While x < 1000\n\
x = x * 2\n\
Loop\n\
Debug.Print x\n\
x = 1\n\
Do\n\
x = x + 1\n\
Loop Until x >= 5\n\
Debug.Print x\n\
Dim n As Integer\n\
n = 0\n\
While n < 4\n\
n = n + 1\n\
Wend\n\
Debug.Print n\n");
assert_eq!(out, vec!["1024", "5", "4"]);
}
#[test]
fn recursion_factorial() {
let source = "Attribute VB_Name = \"M\"\n\
Function Factorial(n As Integer) As Long\n\
If n <= 1 Then\n\
Factorial = 1\n\
Else\n\
Factorial = n * Factorial(n - 1)\n\
End If\n\
End Function\n\
Sub Main()\n\
Debug.Print Factorial(6)\n\
Debug.Print Factorial(0)\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["720", "1"]);
}
#[test]
fn arrays_and_dim_const() {
let source = "Attribute VB_Name = \"M\"\n\
Const MAX As Integer = 3\n\
Sub Main()\n\
Dim a(1 To MAX) As Integer\n\
Dim total As Long\n\
a(1) = 10\n\
a(2) = 20\n\
a(3) = 30\n\
For i = 1 To MAX\n\
total = total + a(i)\n\
Next i\n\
Debug.Print total\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["60"]);
}
#[test]
fn function_returns_default_when_unset() {
let source = "Attribute VB_Name = \"M\"\n\
Function Unset() As Integer\n\
End Function\n\
Sub Main()\n\
Debug.Print Unset()\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["0"]);
}
#[test]
fn global_const_and_module_level_init() {
let source = "Attribute VB_Name = \"M\"\n\
Dim gCount As Integer\n\
Const BASE As Long = 100\n\
Sub Main()\n\
Debug.Print gCount\n\
Debug.Print BASE\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["0", "100"]);
}
#[test]
fn division_by_zero_reports_line() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim x As Double\n\
x = 1 / 0\n\
End Sub\n";
let error = run_source(source).expect_err("expected division by zero");
assert_eq!(error.error.number, 11);
assert!(error.to_string().contains("line 3"));
}
#[test]
fn expression_builtin_conversion_failure_reports_line() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Debug.Print Chr$(CVErr(31337))\n\
End Sub\n";
let error = run_source(source).expect_err("expected CVErr propagation");
assert_eq!(error.error.number, 31337);
assert!(error.to_string().contains("line 2"));
}
#[test]
fn statement_conversion_failure_reports_line() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim s As String\n\
Dim v As Variant\n\
v = CVErr(4242)\n\
Mid(s, v) = \"x\"\n\
End Sub\n";
let error = run_source(source).expect_err("expected CVErr propagation");
assert_eq!(error.error.number, 4242);
assert!(error.to_string().contains("line 5"));
}
#[test]
fn flat_statement_builtin_conversion_failure_reports_line() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim v As Variant\n\
Set v = Chr$(CVErr(777))\n\
End Sub\n";
let error = run_source(source).expect_err("expected CVErr propagation");
assert_eq!(error.error.number, 777);
assert!(error.to_string().contains("line 3"));
}
#[test]
fn print_separators() {
let out = run(" Debug.Print \"a\"; \"b\"\n Debug.Print \"c\"\n Debug.Print \"x\";\n Debug.Print \"y\"\n");
assert_eq!(out, vec!["ab", "c", "xy"]);
}
#[test]
fn like_operator() {
let out = run(" Debug.Print \"Hello\" Like \"H*\"\n\
Debug.Print \"hello\" Like \"H*\"\n\
Debug.Print \"abc123\" Like \"???###\"\n\
Debug.Print \"cat\" Like \"[a-d]at\"\n\
Debug.Print \"eat\" Like \"[!a-d]at\"\n\
Debug.Print \"x?y\" Like \"x[?]y\"\n\
Debug.Print \"[a\" Like \"[[]a\"\n\
Debug.Print \"hello\" Like \"h[eo]l?o\"\n");
assert_eq!(
out,
vec!["True", "True", "True", "True", "True", "True", "True", "True"]
);
}
#[test]
fn is_operator() {
let out = run(" Dim v As Variant\n\
Debug.Print v Is Nothing\n\
v = Nothing\n\
Debug.Print v Is Nothing\n\
Dim w As Variant\n\
w = \"x\"\n\
Debug.Print w Is Nothing\n");
assert_eq!(out, vec!["False", "True", "False"]);
}
#[test]
fn bitwise_logical_operators() {
let out = run(" Debug.Print 5 And 3\n\
Debug.Print 5 Or 2\n\
Debug.Print 5 Xor 1\n\
Debug.Print 5 Eqv 3\n\
Debug.Print True And False\n\
Debug.Print True Imp False\n");
assert_eq!(out, vec!["1", "7", "4", "-7", "False", "False"]);
}
#[test]
fn run_final_line_lands_on_end_sub_after_loop() {
let source = "Attribute VB_Name = \"M\"\n\n\
Sub Main()\n\
For i = 1 To 2\n\
Debug.Print i\n\
Next i\n\
End Sub\n";
assert_eq!(run_final_line(source), 7);
}
#[test]
fn run_final_line_lands_on_end_sub_after_while() {
let source = "Attribute VB_Name = \"M\"\n\n\
Sub Main()\n\
i = 0\n\
While i < 2\n\
i = i + 1\n\
Wend\n\
End Sub\n";
assert_eq!(run_final_line(source), 8);
}
#[test]
fn run_final_line_lands_on_end_sub_after_do() {
let source = "Attribute VB_Name = \"M\"\n\n\
Sub Main()\n\
i = 0\n\
Do While i < 2\n\
i = i + 1\n\
Loop\n\
End Sub\n";
assert_eq!(run_final_line(source), 8);
}
#[test]
fn run_final_line_lands_on_end_sub_after_post_test_loop() {
let source = "Attribute VB_Name = \"M\"\n\n\
Sub Main()\n\
i = 0\n\
Do\n\
i = i + 1\n\
Loop While i < 2\n\
End Sub\n";
assert_eq!(run_final_line(source), 8);
}
#[test]
fn run_final_line_lands_on_end_sub_after_function_entry() {
let source = "Attribute VB_Name = \"M\"\n\n\
Function Answer() As Integer\n\
Answer = 42\n\
End Function\n\
Sub Main()\n\
Debug.Print Answer\n\
End Sub\n";
assert_eq!(run_final_line(source), 8);
}
#[test]
fn irr_passes_whole_array_with_empty_parens() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim Guess, RetRate\n\
Dim Values(5) As Double\n\
Guess = .1\n\
Values(0) = -70000\n\
Values(1) = 22000\n\
Values(2) = 25000\n\
Values(3) = 28000\n\
Values(4) = 31000\n\
RetRate = IRR(Values(), Guess) * 100\n\
Debug.Print Format(RetRate, \"0.0\")\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["17.7"]);
}
#[test]
fn irr_default_guess_with_empty_parens() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim cfs(0 To 3) As Double\n\
cfs(0) = -1000\n\
cfs(1) = 400\n\
cfs(2) = 400\n\
cfs(3) = 400\n\
Debug.Print Format(IRR(cfs()), \"0.00\")\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["0.10"]);
}
#[test]
fn array_element_indexing_still_works() {
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Dim a(3) As Integer\n\
a(0) = 10\n\
a(1) = 20\n\
a(2) = 30\n\
Debug.Print a(1)\n\
Debug.Print a(0)\n\
End Sub\n";
let out = run_source(source).expect("interpretation failed");
assert_eq!(out, vec!["20", "10"]);
}
fn run_with_env(body: &str, name: &str, value: &str) -> Vec<String> {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = format!("Attribute VB_Name = \"M\"\nSub Main()\n{body}\nEnd Sub\n");
let mut interpreter = Interpreter::new();
interpreter.set_environment(name, value);
interpreter
.run_source(&source)
.expect("interpretation failed");
interpreter.output().to_vec()
}
#[test]
fn environ_reads_variable_assigned_before_running() {
let out = run_with_env(
" Debug.Print Environ(\"VB6_TEST_VAR\")\n",
"VB6_TEST_VAR",
"hello",
);
assert_eq!(out, vec!["hello"]);
}
#[test]
fn environ_numeric_argument_enumerates_the_table() {
let out = run_with_env(
" Dim i As Integer\n\
i = 1\n\
Do While Environ(i) <> \"\"\n\
Debug.Print Environ(i)\n\
i = i + 1\n\
Loop\n",
"VB6_TEST_VAR",
"hello",
);
assert!(!out.is_empty());
for entry in &out {
assert!(entry.contains('='));
}
assert_eq!(out.last().map(String::as_str), Some("VB6_TEST_VAR=hello"));
}
#[test]
fn environ_error_for_bad_index() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Debug.Print Environ(0)\n\
End Sub\n";
let mut interpreter = Interpreter::new();
interpreter.set_environment("VB6_TEST_VAR", "hello");
let error = interpreter
.run_source(source)
.expect_err("expected error 5");
assert_eq!(error.error.number, 5);
}
#[test]
fn environ_dollar_reads_variable_assigned_before_running() {
let out = run_with_env(
" Debug.Print Environ$(\"VB6_TEST_VAR\")\n",
"VB6_TEST_VAR",
"hello",
);
assert_eq!(out, vec!["hello"]);
}
#[test]
fn environ_dollar_lookup_is_case_insensitive() {
let out = run_with_env(
" Debug.Print Environ$(\"vb6_test_var\")\n",
"VB6_TEST_VAR",
"hello",
);
assert_eq!(out, vec!["hello"]);
}
#[test]
fn environ_dollar_returns_empty_for_unset_variable() {
let out = run_with_env(
" Debug.Print \"[\" & Environ$(\"VB6_TEST_MISSING\") & \"]\"\n",
"VB6_TEST_VAR",
"hello",
);
assert_eq!(out, vec!["[]"]);
}
#[test]
fn environ_dollar_numeric_argument_enumerates_the_table() {
let out = run_with_env(
" Dim i As Integer\n\
i = 1\n\
Do While Environ$(i) <> \"\"\n\
Debug.Print Environ$(i)\n\
i = i + 1\n\
Loop\n",
"VB6_TEST_VAR",
"hello",
);
assert!(!out.is_empty());
for entry in &out {
assert!(entry.contains('='));
}
assert_eq!(out.last().map(String::as_str), Some("VB6_TEST_VAR=hello"));
}
#[test]
fn environ_dollar_assignment_survives_repeated_runs() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let mut interpreter = Interpreter::new();
interpreter.set_environment("VB6_TEST_VAR", "one");
interpreter
.run_source(
"Attribute VB_Name = \"M\"\nSub Main()\n Debug.Print Environ$(\"VB6_TEST_VAR\")\nEnd Sub\n",
)
.expect("interpretation failed");
assert_eq!(interpreter.output().to_vec(), vec!["one"]);
interpreter
.run_source(
"Attribute VB_Name = \"M\"\nSub Main()\n Debug.Print Environ$(\"VB6_TEST_VAR\")\nEnd Sub\n",
)
.expect("interpretation failed");
assert_eq!(interpreter.output().to_vec(), vec!["one"]);
}
#[test]
fn environ_dollar_error_for_bad_index() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = "Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Debug.Print Environ$(0)\n\
End Sub\n";
let mut interpreter = Interpreter::new();
interpreter.set_environment("VB6_TEST_VAR", "hello");
let error = interpreter
.run_source(source)
.expect_err("expected error 5");
assert_eq!(error.error.number, 5);
}
#[test]
fn get_setting_reads_from_the_settings_store() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let dir = tempfile::tempdir().expect("failed to create temp dir");
settings_state::set_store_root(dir.path());
settings_state::set("MyApp", "Startup", "Left", "150").unwrap();
let out = run(
" Debug.Print GetSetting(\"MyApp\", \"Startup\", \"Left\", \"0\")\n\
Debug.Print GetSetting(\"MyApp\", \"Startup\", \"Missing\", \"42\")\n\
Debug.Print GetSetting(\"myapp\", \"startup\", \"left\")\n",
);
assert_eq!(out, vec!["150", "42", "150"]);
settings_state::reset_store_root();
}
struct TempSettingsStore {
_dir: tempfile::TempDir,
}
impl TempSettingsStore {
fn new() -> Self {
let dir = tempfile::tempdir().expect("failed to create temp dir");
settings_state::set_store_root(dir.path());
Self { _dir: dir }
}
}
impl Drop for TempSettingsStore {
fn drop(&mut self) {
settings_state::reset_store_root();
}
}
fn run_with_settings(body: &str, setup: impl FnOnce(&mut Interpreter)) -> Vec<String> {
let source = format!("Attribute VB_Name = \"M\"\nSub Main()\n{body}\nEnd Sub\n");
let mut interpreter = Interpreter::new();
setup(&mut interpreter);
interpreter
.run_source(&source)
.expect("interpretation failed");
interpreter.output().to_vec()
}
#[test]
fn staged_settings_are_visible_to_getsetting_during_a_run() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let out = run_with_settings(
" Debug.Print GetSetting(\"MyApp\", \"Startup\", \"Left\", \"0\")\n",
|i| i.set_setting("MyApp", "Startup", "Left", "150"),
);
assert_eq!(out, vec!["150"]);
}
#[test]
fn get_setting_returns_staged_values_before_and_after_a_run() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let mut interpreter = Interpreter::new();
interpreter.set_setting("MyApp", "Startup", "Left", "150");
assert_eq!(
interpreter.get_setting("MyApp", "Startup", "Left"),
Some("150".to_string())
);
assert_eq!(interpreter.get_setting("MyApp", "Startup", "Missing"), None);
interpreter
.run_source("Attribute VB_Name = \"M\"\nSub Main()\nEnd Sub\n")
.expect("interpretation failed");
assert_eq!(
interpreter.get_setting("myapp", "startup", "left"),
Some("150".to_string())
);
}
#[test]
fn staged_settings_override_values_already_in_the_store() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
settings_state::set("MyApp", "Startup", "Left", "150").unwrap();
let out = run_with_settings(
" Debug.Print GetSetting(\"MyApp\", \"Startup\", \"Left\", \"0\")\n",
|i| i.set_setting("MyApp", "Startup", "Left", "200"),
);
assert_eq!(out, vec!["200"]);
}
#[test]
fn remove_setting_removes_staged_and_store_values() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let mut interpreter = Interpreter::new();
interpreter.set_setting("MyApp", "Startup", "Left", "150");
interpreter.set_setting("MyApp", "Startup", "Right", "300");
interpreter.remove_setting("MyApp", "Startup", "Left");
assert_eq!(interpreter.get_setting("MyApp", "Startup", "Left"), None);
assert_eq!(
interpreter.get_setting("MyApp", "Startup", "Right"),
Some("300".to_string())
);
interpreter
.run_source("Attribute VB_Name = \"M\"\nSub Main()\nEnd Sub\n")
.expect("interpretation failed");
interpreter.remove_setting("MyApp", "Startup", "Right");
assert_eq!(interpreter.get_setting("MyApp", "Startup", "Right"), None);
}
#[test]
fn clear_settings_removes_staged_and_store_values() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let mut interpreter = Interpreter::new();
interpreter.set_setting("MyApp", "Startup", "Left", "150");
interpreter.set_setting("MyApp", "Startup", "Right", "300");
interpreter.clear_settings();
assert_eq!(interpreter.get_setting("MyApp", "Startup", "Left"), None);
assert_eq!(interpreter.get_setting("MyApp", "Startup", "Right"), None);
}
#[test]
fn staged_settings_survive_clear() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let mut interpreter = Interpreter::new();
interpreter.set_setting("MyApp", "Startup", "Left", "150");
interpreter.clear();
assert_eq!(
interpreter.get_setting("MyApp", "Startup", "Left"),
Some("150".to_string())
);
interpreter
.run_source(
"Attribute VB_Name = \"M\"\n\
Sub Main()\n\
Debug.Print GetSetting(\"MyApp\", \"Startup\", \"Left\", \"0\")\n\
End Sub\n",
)
.expect("interpretation failed");
assert_eq!(interpreter.output(), &["150"]);
}
#[test]
fn set_settings_backend_switches_to_new_backend() {
use vb6runtime::state::settings::memory::MemoryBackend;
let _guard = ENV_TEST_LOCK.lock().unwrap();
let interpreter = Interpreter::new();
interpreter.set_settings_backend(Box::new(MemoryBackend::new()));
settings_state::set("MyApp", "TestSection", "TestKey", "MemValue").unwrap();
let out = run_with_settings(
" Debug.Print GetSetting(\"MyApp\", \"TestSection\", \"TestKey\", \"default\")\n",
|_| {},
);
assert_eq!(out, vec!["MemValue"]);
interpreter.reset_settings_backend();
}
#[test]
fn reset_settings_backend_restores_default() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let _store = TempSettingsStore::new();
let interpreter = Interpreter::new();
settings_state::set("MyApp", "TestSection", "TestKey", "FileValue").unwrap();
let out = run_with_settings(
" Debug.Print GetSetting(\"MyApp\", \"TestSection\", \"TestKey\", \"default\")\n",
|_| {},
);
assert_eq!(out, vec!["FileValue"]);
interpreter.reset_settings_backend();
}
const STOP_SOURCE: &str = "Attribute VB_Name = \"M\"\n\
Dim gX As Integer\n\
Sub Main()\n \
gX = 42\n \
Debug.Print \"before\"\n \
Stop\n \
Debug.Print \"after\"\n \
End Sub\n";
#[test]
fn stop_terminates_like_end_outside_a_debugger() {
let source_file = SourceFile::from_string("scratch.bas", STOP_SOURCE);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.run_module(&module).unwrap();
assert!(interpreter.is_terminated());
assert_eq!(interpreter.output(), vec!["before".to_string()]);
}
#[test]
fn stop_enters_break_mode_with_a_debugger_attached() {
let source_file = SourceFile::from_string("scratch.bas", STOP_SOURCE);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_record_debug_snapshots(true);
let error = interpreter.run_module(&module).unwrap_err();
assert!(error.is_debug_pause());
assert_eq!(error.line, Some(5));
assert_eq!(error.procedure.as_deref(), Some("Main"));
assert_eq!(
interpreter.global("gX").and_then(|v| v.as_i32().ok()),
Some(42)
);
assert_eq!(interpreter.output(), vec!["before".to_string()]);
}
use vb6runtime::state::interaction::{
self, memory::MemoryBackend as InteractionMemory, MsgBoxButton,
};
fn run_with_msgbox_responses(
body: &str,
responses: Vec<MsgBoxButton>,
) -> (
Vec<String>,
Vec<vb6runtime::state::interaction::MsgBoxRecord>,
) {
let source = format!("Attribute VB_Name = \"M\"\nSub Main()\n{}\nEnd Sub\n", body);
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_interaction_backend(Box::new(InteractionMemory::with_msgbox_responses(
responses,
)));
interpreter
.run_module(&module)
.expect("interpretation failed");
let out = interpreter.output().to_vec();
let requests = interaction::with_memory_backend(|m| m.take_msgbox_requests())
.expect("memory interaction backend installed");
interpreter.reset_interaction_backend();
(out, requests)
}
#[test]
fn msgbox_function_returns_scripted_buttons_in_order() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let (out, requests) = run_with_msgbox_responses(
" Dim r As Integer\n\
\x20 r = MsgBox(\"Save changes?\", vbYesNo + vbQuestion, \"Confirm\")\n\
\x20 Debug.Print \"r=\" & r\n\
\x20 r = MsgBox(\"Really quit?\", vbYesNo)\n\
\x20 Debug.Print \"r=\" & r\n",
vec![MsgBoxButton::Yes, MsgBoxButton::No],
);
assert_eq!(out, vec!["r=6", "r=7"]);
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].prompt, "Save changes?");
assert_eq!(requests[0].title.as_deref(), Some("Confirm"));
assert_eq!(
requests[0].offered_buttons,
vec![MsgBoxButton::Yes, MsgBoxButton::No]
);
assert_eq!(requests[1].prompt, "Really quit?");
}
#[test]
fn msgbox_statement_defaults_without_scripting() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let (out, requests) = run_with_msgbox_responses(
" MsgBox \"Done.\", vbInformation\n\
\x20 Debug.Print \"after\"\n",
vec![],
);
assert_eq!(out, vec!["after"]);
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].default_button, MsgBoxButton::Ok);
}
#[test]
fn msgbox_mismatched_scripted_response_is_a_runtime_error() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = "Attribute VB_Name = \"M\"\nSub Main()\n\
\x20 Dim r As Integer\n\
\x20 r = MsgBox(\"Overwrite file?\", vbYesNo)\n\
\x20 Debug.Print \"r=\" & r\n\
End Sub\n";
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_interaction_backend(Box::new(InteractionMemory::with_msgbox_responses([
MsgBoxButton::Cancel,
])));
let error = interpreter.run_module(&module).unwrap_err();
interpreter.reset_interaction_backend();
assert_eq!(error.error.number, 5);
assert!(error.error.description.contains("Cancel"));
assert_eq!(interpreter.output(), Vec::<String>::new());
}
use vb6runtime::state::interaction::SendKeysRecord;
fn run_with_sendkeys(body: &str, responses: Vec<bool>) -> (Vec<String>, Vec<SendKeysRecord>) {
let source = format!("Attribute VB_Name = \"M\"\nSub Main()\n{}\nEnd Sub\n", body);
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_interaction_backend(Box::new(InteractionMemory::with_sendkeys_responses(
responses,
)));
interpreter
.run_module(&module)
.expect("interpretation failed");
let out = interpreter.output().to_vec();
let requests = interaction::with_memory_backend(|m| m.take_sendkeys_requests())
.expect("memory interaction backend installed");
interpreter.reset_interaction_backend();
(out, requests)
}
#[test]
fn sendkeys_statement_reaches_the_memory_backend_verbatim() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let (out, requests) = run_with_sendkeys(
" AppActivate \"Notepad\"\n\
\x20 SendKeys \"John Doe{TAB}555-1234{ENTER}\", True\n\
\x20 SendKeys \"^s\"\n\
\x20 Debug.Print \"sent\"\n",
vec![],
);
assert_eq!(out, vec!["sent"]);
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].keys, "John Doe{TAB}555-1234{ENTER}");
assert!(requests[0].wait);
assert_eq!(requests[1].keys, "^s");
assert!(!requests[1].wait);
}
#[test]
fn sendkeys_malformed_key_string_is_a_runtime_error() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = "Attribute VB_Name = \"M\"\nSub Main()\n\
\x20 SendKeys \"{BOGUS}\"\n\
\x20 Debug.Print \"after\"\n\
End Sub\n";
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_interaction_backend(Box::new(InteractionMemory::new()));
let error = interpreter.run_module(&module).unwrap_err();
interpreter.reset_interaction_backend();
assert_eq!(error.error.number, 5);
assert!(error.error.description.contains("{BOGUS}"));
assert_eq!(interpreter.output(), Vec::<String>::new());
}
#[test]
fn appactivate_accepts_a_bare_true_wait_argument() {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let source = "Attribute VB_Name = \"M\"\nSub Main()\n\
\x20 AppActivate \"Calculator\", True\n\
End Sub\n";
let source_file = SourceFile::from_string("scratch.bas", source);
let module = ModuleFile::parse(&source_file).unwrap_or_fail();
let mut interpreter = Interpreter::new();
interpreter.set_interaction_backend(Box::new(InteractionMemory::new()));
interpreter
.run_module(&module)
.expect("interpretation failed");
let requests = interaction::with_memory_backend(|m| m.take_appactivate_requests())
.expect("memory interaction backend installed");
interpreter.reset_interaction_backend();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].title, "Calculator");
assert!(requests[0].wait);
}
use vb6runtime::state::file as file_state;
fn one_pixel_bmp() -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"BM");
bytes.extend_from_slice(&58u32.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&54u32.to_le_bytes()); bytes.extend_from_slice(&40u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&1u32.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&24u16.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); bytes.extend_from_slice(&4u32.to_le_bytes()); bytes.extend_from_slice(&2835u32.to_le_bytes());
bytes.extend_from_slice(&2835u32.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]); bytes
}
fn with_temp_file_root<T>(f: impl FnOnce(&std::path::Path) -> T) -> T {
let _guard = ENV_TEST_LOCK.lock().unwrap();
let dir = tempfile::tempdir().expect("failed to create temp dir");
file_state::reset_with_root(dir.path());
let result = f(dir.path());
file_state::reset();
result
}
#[test]
fn savepicture_statement_writes_a_bitmap_file() {
with_temp_file_root(|dir| {
std::fs::write(dir.join("in.bmp"), one_pixel_bmp()).unwrap();
let out = run(" Dim p As Object\n\
\x20 Set p = LoadPicture(\"in.bmp\")\n\
\x20 SavePicture p, \"out.bmp\"\n\
\x20 Debug.Print Dir(\"out.bmp\") <> \"\"\n");
assert_eq!(out, vec!["True"]);
let bytes = std::fs::read(dir.join("out.bmp")).unwrap();
assert_eq!(&bytes[0..2], b"BM");
assert_eq!(bytes.len(), 54 + 4); });
}
#[test]
fn savepicture_statement_overwrites_an_existing_file() {
with_temp_file_root(|dir| {
std::fs::write(dir.join("in.bmp"), one_pixel_bmp()).unwrap();
std::fs::write(dir.join("out.bmp"), b"stale").unwrap();
let out = run(" Dim p As Object\n\
\x20 Set p = LoadPicture(\"in.bmp\")\n\
\x20 SavePicture p, \"out.bmp\"\n\
\x20 Debug.Print FileLen(\"out.bmp\") > 4\n");
assert_eq!(out, vec!["True"]);
});
}
#[test]
fn savepicture_statement_with_a_non_picture_value_is_a_type_mismatch() {
let source = "Attribute VB_Name = \"M\"\nSub Main()\n\
\x20 SavePicture 42, \"out.bmp\"\n\
\x20 Debug.Print \"after\"\n\
End Sub\n";
let error = vb6interpret::run_source(source).unwrap_err();
assert_eq!(error.error.number, 13);
}
#[test]
fn savepicture_statement_with_nothing_raises_object_variable_not_set() {
with_temp_file_root(|_| {
let source = "Attribute VB_Name = \"M\"\nSub Main()\n\
\x20 Dim p As Object\n\
\x20 SavePicture p, \"out.bmp\"\n\
\x20 Debug.Print \"after\"\n\
End Sub\n";
let error = vb6interpret::run_source(source).unwrap_err();
assert_eq!(error.error.number, 91);
});
}