ChaiScript/unittests/exception_comprehensive.chai
leftibot d4c5bdb3e4
Fix #61: Comprehensive exception test suite and fix for silently swallowed exceptions (#681)
When all typed catch blocks failed to match a thrown exception's type,
handle_exception() would silently discard the exception and return a
default-constructed Boxed_Value instead of propagating it. This meant
code like `try { throw(42) } catch(string e) { }` would swallow the
int exception rather than letting it propagate to an outer handler.

The fix adds an explicit re-throw when no catch block matches, and
restructures eval_internal() with a nested try/catch to ensure the
finally block still executes before the unhandled exception propagates.

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:05:55 -06:00

863 lines
17 KiB
ChaiScript

// Comprehensive exception throwing and catching tests
// Tests throw/catch from various contexts: operators, functions, lambdas,
// methods, [] operator, nested scopes, and with various value types.
// ============================================================
// Section 1: Throwing and catching different value types
// ============================================================
// Throw int
try {
throw(42)
}
catch(e) {
assert_equal(42, e)
}
// Throw string
try {
throw("error message")
}
catch(e) {
assert_equal("error message", e)
}
// Throw double
try {
throw(3.14)
}
catch(e) {
assert_equal(3.14, e)
}
// Throw bool
try {
throw(true)
}
catch(e) {
assert_equal(true, e)
}
// Throw a Vector (via named variable to preserve mutability)
auto thrown_vec = [1, 2, 3]
try {
throw(thrown_vec)
}
catch(e) {
assert_equal(3, e.size())
assert_equal(1, e[0])
assert_equal(2, e[1])
assert_equal(3, e[2])
}
// Throw a Map (via named variable to preserve mutability)
auto thrown_map = ["key": 42]
try {
throw(thrown_map)
}
catch(e) {
assert_equal(42, e["key"])
}
// ============================================================
// Section 2: Typed catch blocks
// ============================================================
// Typed catch matching int
auto typed_result = 0
try {
throw(10)
}
catch(int e) {
typed_result = e + 1
}
assert_equal(11, typed_result)
// Typed catch matching string
typed_result = 0
try {
throw("hello")
}
catch(string e) {
typed_result = 1
}
assert_equal(1, typed_result)
// Typed catch mismatch falls through to untyped
typed_result = 0
try {
throw(42)
}
catch(string e) {
typed_result = 1
}
catch(e) {
typed_result = 2
}
assert_equal(2, typed_result)
// Multiple typed catches - first match wins
typed_result = 0
try {
throw("test")
}
catch(int e) {
typed_result = 1
}
catch(string e) {
typed_result = 2
}
catch(e) {
typed_result = 3
}
assert_equal(2, typed_result)
// Typed catch with int, multiple typed blocks, none match except untyped
typed_result = 0
try {
throw(3.14)
}
catch(int e) {
typed_result = 1
}
catch(string e) {
typed_result = 2
}
catch(e) {
typed_result = 3
}
assert_equal(3, typed_result)
// ============================================================
// Section 3: Catch-all (no variable) catch block
// ============================================================
auto catch_all_reached = false
try {
throw(99)
}
catch {
catch_all_reached = true
}
assert_true(catch_all_reached)
// ============================================================
// Section 4: Finally block semantics
// ============================================================
// Finally runs after exception
auto finally_ran = false
try {
throw(1)
}
catch(e) {
// caught
}
finally {
finally_ran = true
}
assert_true(finally_ran)
// Finally runs without exception
finally_ran = false
try {
auto x = 1
}
catch(e) {
// not reached
}
finally {
finally_ran = true
}
assert_true(finally_ran)
// Finally runs even with typed catch that matches
finally_ran = false
try {
throw(42)
}
catch(int e) {
assert_equal(42, e)
}
finally {
finally_ran = true
}
assert_true(finally_ran)
// Finally runs when typed catch does NOT match (exception not caught)
finally_ran = false
auto outer_caught = false
try {
try {
throw(42)
}
catch(string e) {
// wrong type, won't match
}
finally {
finally_ran = true
}
}
catch(e) {
outer_caught = true
}
assert_true(finally_ran)
assert_true(outer_caught)
// ============================================================
// Section 5: Throwing from functions
// ============================================================
def throwing_function() {
throw("from function")
}
try {
throwing_function()
}
catch(e) {
assert_equal("from function", e)
}
// Throwing from nested function calls
def inner_throw() {
throw("inner")
}
def outer_call() {
inner_throw()
}
try {
outer_call()
}
catch(e) {
assert_equal("inner", e)
}
// Function that throws conditionally
def conditional_throw(should_throw) {
if (should_throw) {
throw("conditional")
}
return "no throw"
}
assert_equal("no throw", conditional_throw(false))
try {
conditional_throw(true)
}
catch(e) {
assert_equal("conditional", e)
}
// ============================================================
// Section 6: Throwing from lambdas
// ============================================================
auto throwing_lambda = fun() { throw("from lambda") }
try {
throwing_lambda()
}
catch(e) {
assert_equal("from lambda", e)
}
// Lambda that captures and throws
auto captured_val = "captured"
auto capture_lambda = fun[captured_val]() { throw(captured_val) }
try {
capture_lambda()
}
catch(e) {
assert_equal("captured", e)
}
// ============================================================
// Section 7: Throwing from binary operators
// ============================================================
// Define a type and an operator that throws
attr ThrowOnAdd::val
def ThrowOnAdd::ThrowOnAdd(v) { this.val = v }
def `+`(ThrowOnAdd x, ThrowOnAdd y) {
throw("add not supported")
}
try {
auto a = ThrowOnAdd(1)
auto b = ThrowOnAdd(2)
auto c = a + b
assert_true(false) // should not reach here
}
catch(e) {
assert_equal("add not supported", e)
}
// Operator that throws for specific values
def `-`(ThrowOnAdd x, ThrowOnAdd y) {
if (x.val == y.val) {
throw("cannot subtract equal values")
}
return ThrowOnAdd(x.val - y.val)
}
try {
auto a = ThrowOnAdd(5)
auto b = ThrowOnAdd(5)
auto c = a - b
assert_true(false)
}
catch(e) {
assert_equal("cannot subtract equal values", e)
}
// Multiplication operator that throws (not pre-defined for Dynamic_Object)
def `*`(ThrowOnAdd x, ThrowOnAdd y) {
throw("multiply not supported")
}
try {
auto a = ThrowOnAdd(1)
auto b = ThrowOnAdd(2)
auto c = a * b
assert_true(false)
}
catch(e) {
assert_equal("multiply not supported", e)
}
// Subtraction works for non-equal values
auto sub_result = ThrowOnAdd(10) - ThrowOnAdd(3)
assert_equal(7, sub_result.val)
// ============================================================
// Section 8: Throwing from unary/prefix operators
// ============================================================
def `++`(ThrowOnAdd x) {
throw("increment not supported")
}
try {
auto a = ThrowOnAdd(1)
++a
assert_true(false)
}
catch(e) {
assert_equal("increment not supported", e)
}
// ============================================================
// Section 9: Throwing from [] operator
// ============================================================
attr ThrowOnIndex::data
def ThrowOnIndex::ThrowOnIndex() { this.data = [1, 2, 3] }
def `[]`(ThrowOnIndex obj, int idx) {
if (idx < 0) {
throw("negative index not allowed")
}
return obj.data[idx]
}
auto toi = ThrowOnIndex()
assert_equal(1, toi[0])
assert_equal(2, toi[1])
try {
auto val = toi[-1]
assert_true(false)
}
catch(e) {
assert_equal("negative index not allowed", e)
}
// ============================================================
// Section 10: Throwing from member functions
// ============================================================
attr Validatable::value
def Validatable::Validatable(v) { this.value = v }
def Validatable::validate() {
if (this.value < 0) {
throw("validation failed: negative value")
}
return true
}
auto valid_obj = Validatable(10)
assert_true(valid_obj.validate())
auto invalid_obj = Validatable(-1)
try {
invalid_obj.validate()
assert_true(false)
}
catch(e) {
assert_equal("validation failed: negative value", e)
}
// ============================================================
// Section 11: Nested try/catch
// ============================================================
auto inner_caught = false
auto outer_caught_val = 0
try {
try {
throw(1)
}
catch(e) {
inner_caught = true
throw(e + 10)
}
}
catch(e) {
outer_caught_val = e
}
assert_true(inner_caught)
assert_equal(11, outer_caught_val)
// Deeply nested try/catch
auto depth = 0
try {
try {
try {
throw("deep")
}
catch(e) {
depth = 1
throw(e + "er")
}
}
catch(e) {
depth = 2
throw(e + "est")
}
}
catch(e) {
depth = 3
assert_equal("deeperest", e)
}
assert_equal(3, depth)
// ============================================================
// Section 12: Rethrow from catch block
// ============================================================
auto rethrow_caught = false
try {
try {
throw("rethrown")
}
catch(e) {
throw(e)
}
}
catch(e) {
rethrow_caught = true
assert_equal("rethrown", e)
}
assert_true(rethrow_caught)
// ============================================================
// Section 13: Exception in for loop
// ============================================================
auto loop_exception_val = 0
try {
for (auto i = 0; i < 10; ++i) {
if (i == 5) {
throw(i)
}
}
}
catch(e) {
loop_exception_val = e
}
assert_equal(5, loop_exception_val)
// ============================================================
// Section 14: Exception in while loop
// ============================================================
auto while_exc_val = 0
auto counter = 0
try {
while (true) {
++counter
if (counter == 3) {
throw(counter)
}
}
}
catch(e) {
while_exc_val = e
}
assert_equal(3, while_exc_val)
// ============================================================
// Section 15: Exception preserves value through nested calls
// ============================================================
def deep_throw(val) {
throw(val)
}
def middle_call(val) {
deep_throw(val)
}
def top_call(val) {
middle_call(val)
}
auto nested_map = ["key": "value"]
try {
top_call(nested_map)
}
catch(e) {
assert_equal("value", e["key"])
}
auto nested_vec = [10, 20, 30]
try {
top_call(nested_vec)
}
catch(e) {
assert_equal(3, e.size())
assert_equal(20, e[1])
}
// ============================================================
// Section 16: Code after throw is not executed
// ============================================================
auto after_throw = false
try {
throw(1)
after_throw = true
}
catch(e) {
// caught
}
assert_false(after_throw)
// ============================================================
// Section 17: Exception value is usable in catch block arithmetic
// ============================================================
auto catch_computed = 0
try {
throw(1)
}
catch(e) {
catch_computed = e + 100
}
assert_equal(101, catch_computed)
// ============================================================
// Section 18: No exception means catch is skipped
// ============================================================
auto catch_skipped = true
try {
auto x = 42
}
catch(e) {
catch_skipped = false
}
assert_true(catch_skipped)
// ============================================================
// Section 19: Exception from dynamic object method chaining
// ============================================================
attr Chain::val
def Chain::Chain(v) { this.val = v }
def Chain::add(n) {
if (this.val + n > 100) {
throw("overflow: " + to_string(this.val + n))
}
this.val = this.val + n
return this
}
auto chain = Chain(50)
try {
chain.add(30).add(30)
assert_true(false)
}
catch(e) {
assert_equal("overflow: 110", e)
}
assert_equal(80, chain.val)
// ============================================================
// Section 20: Exception thrown during map construction
// ============================================================
def exploding_value() {
throw("boom during construction")
}
try {
auto m = ["ok": 1, "bad": exploding_value()]
assert_true(false)
}
catch(e) {
assert_equal("boom during construction", e)
}
// ============================================================
// Section 21: Exception thrown during vector construction
// ============================================================
try {
auto v = [1, 2, exploding_value(), 4]
assert_true(false)
}
catch(e) {
assert_equal("boom during construction", e)
}
// ============================================================
// Section 22: Exception in if-condition
// ============================================================
def exploding_condition() {
throw("condition exploded")
}
try {
if (exploding_condition()) {
assert_true(false)
}
}
catch(e) {
assert_equal("condition exploded", e)
}
// ============================================================
// Section 23: Multiple catch blocks - only first matching runs
// ============================================================
auto catch_count = 0
try {
throw(42)
}
catch(int e) {
++catch_count
}
catch(e) {
++catch_count
}
assert_equal(1, catch_count)
// ============================================================
// Section 24: Throwing from within catch, with finally
// ============================================================
auto s24_finally = false
auto s24_outer = false
try {
try {
throw("original")
}
catch(e) {
throw("replaced: " + e)
}
finally {
s24_finally = true
}
}
catch(e) {
s24_outer = true
assert_equal("replaced: original", e)
}
assert_true(s24_finally)
assert_true(s24_outer)
// ============================================================
// Section 25: Unhandled typed catch propagates exception
// ============================================================
auto s25_caught = false
try {
try {
throw(3.14)
}
catch(int e) {
assert_true(false) // should not match double
}
catch(string e) {
assert_true(false) // should not match double
}
}
catch(e) {
s25_caught = true
assert_equal(3.14, e)
}
assert_true(s25_caught)
// ============================================================
// Section 26: Throw from range-based for
// ============================================================
auto s26_val = 0
try {
for (x : [10, 20, 30, 40]) {
if (x == 30) {
throw(x)
}
}
}
catch(e) {
s26_val = e
}
assert_equal(30, s26_val)
// ============================================================
// Section 27: Throw from eval
// ============================================================
try {
eval("throw(\"from eval\")")
}
catch(e) {
assert_equal("from eval", e)
}
// ============================================================
// Section 28: Exception from built-in operations (out of range)
// ============================================================
auto s28_caught = false
try {
auto v = [1, 2, 3]
auto x = v[10]
}
catch(e) {
s28_caught = true
}
assert_true(s28_caught)
// ============================================================
// Section 29: Throw zero and empty string (falsy values)
// ============================================================
try {
throw(0)
}
catch(e) {
assert_equal(0, e)
}
try {
throw("")
}
catch(e) {
assert_equal("", e)
}
// ============================================================
// Section 30: Throw from ternary-style inline_if
// ============================================================
def maybe_throw(do_it) {
if (do_it) { throw("inline threw") } else { "ok" }
}
try {
maybe_throw(true)
}
catch(e) {
assert_equal("inline threw", e)
}
assert_equal("ok", maybe_throw(false))
// ============================================================
// Section 31: Verify catch variable scope isolation
// ============================================================
auto outer_e = "untouched"
try {
throw("caught_value")
}
catch(e) {
assert_equal("caught_value", e)
}
assert_equal("untouched", outer_e)
// ============================================================
// Section 32: Exception from recursive function
// ============================================================
def recursive_throw(n) {
if (n == 0) {
throw("bottom")
}
recursive_throw(n - 1)
}
try {
recursive_throw(5)
}
catch(e) {
assert_equal("bottom", e)
}
// ============================================================
// Section 33: Try/catch in a function body
// ============================================================
def safe_divide(a, b) {
try {
if (b == 0) {
throw("division by zero")
}
return a / b
}
catch(e) {
return e
}
}
assert_equal(5, safe_divide(10, 2))
assert_equal("division by zero", safe_divide(10, 0))
// ============================================================
// Section 34: Throw from [] on a Map with missing key
// ============================================================
auto s34_caught = false
try {
auto m = ["a": 1]
auto x = m["nonexistent"]
}
catch(e) {
s34_caught = true
}
assert_true(s34_caught)
// ============================================================
// Section 35: Arithmetic exception (divide by zero)
// ============================================================
auto s35_caught = false
try {
auto x = 1 / 0
}
catch(e) {
s35_caught = true
}
assert_true(s35_caught)