|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Tests for ddless_eval_in_context() |
| 4 | + * Run: php tests/php/EvalInContextTest.php |
| 5 | + */ |
| 6 | +require_once __DIR__ . '/bootstrap.php'; |
| 7 | + |
| 8 | +// ── Helper class for $this binding tests ──────────────────────────────────── |
| 9 | + |
| 10 | +class EvalTestDummy { |
| 11 | + public string $name = 'Alice'; |
| 12 | + private int $secret = 42; |
| 13 | + |
| 14 | + public function getSecret(): int { |
| 15 | + return $this->secret; |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +section('ddless_eval_in_context() — returnValue=true (default)'); |
| 20 | + |
| 21 | +test('evaluates simple expression', function () { |
| 22 | + $result = ddless_eval_in_context('1 + 2', [], []); |
| 23 | + assert_eq(3, $result); |
| 24 | +}); |
| 25 | + |
| 26 | +test('evaluates string expression', function () { |
| 27 | + $result = ddless_eval_in_context('"hello " . "world"', [], []); |
| 28 | + assert_eq('hello world', $result); |
| 29 | +}); |
| 30 | + |
| 31 | +test('accesses scope variables', function () { |
| 32 | + $vars = ['x' => 10, 'y' => 20]; |
| 33 | + $result = ddless_eval_in_context('$x + $y', $vars, []); |
| 34 | + assert_eq(30, $result); |
| 35 | +}); |
| 36 | + |
| 37 | +test('accesses array scope variable', function () { |
| 38 | + $vars = ['items' => ['a', 'b', 'c']]; |
| 39 | + $result = ddless_eval_in_context('count($items)', $vars, []); |
| 40 | + assert_eq(3, $result); |
| 41 | +}); |
| 42 | + |
| 43 | +test('scope variables do not leak between calls', function () { |
| 44 | + ddless_eval_in_context('$leak = 999', ['leak' => 999], []); |
| 45 | + // $leak should not exist in the next call |
| 46 | + $result = ddless_eval_in_context('isset($leak) ? $leak : null', [], []); |
| 47 | + assert_null($result); |
| 48 | +}); |
| 49 | + |
| 50 | +section('ddless_eval_in_context() — returnValue=false (playground mode)'); |
| 51 | + |
| 52 | +test('executes raw code and returns result', function () { |
| 53 | + $result = ddless_eval_in_context('$a = 5; $b = 10; return $a * $b;', [], [], false); |
| 54 | + assert_eq(50, $result); |
| 55 | +}); |
| 56 | + |
| 57 | +test('multi-line code with return', function () { |
| 58 | + $code = <<<'PHP' |
| 59 | +$items = [1, 2, 3, 4, 5]; |
| 60 | +$sum = 0; |
| 61 | +foreach ($items as $i) { |
| 62 | + $sum += $i; |
| 63 | +} |
| 64 | +return $sum; |
| 65 | +PHP; |
| 66 | + $result = ddless_eval_in_context($code, [], [], false); |
| 67 | + assert_eq(15, $result); |
| 68 | +}); |
| 69 | + |
| 70 | +test('returns null when no return statement', function () { |
| 71 | + $result = ddless_eval_in_context('$x = 42;', [], [], false); |
| 72 | + assert_null($result); |
| 73 | +}); |
| 74 | + |
| 75 | +test('accesses scope variables in raw mode', function () { |
| 76 | + $vars = ['user' => ['name' => 'Bob', 'age' => 30]]; |
| 77 | + $result = ddless_eval_in_context('return $user["name"] . " is " . $user["age"];', $vars, [], false); |
| 78 | + assert_eq('Bob is 30', $result); |
| 79 | +}); |
| 80 | + |
| 81 | +test('can use built-in functions', function () { |
| 82 | + $result = ddless_eval_in_context('return array_map(fn($v) => $v * 2, [1,2,3]);', [], [], false); |
| 83 | + assert_eq([2, 4, 6], $result); |
| 84 | +}); |
| 85 | + |
| 86 | +test('can use anonymous functions', function () { |
| 87 | + $code = <<<'PHP' |
| 88 | +$fn = function($a, $b) { return $a + $b; }; |
| 89 | +return $fn(3, 7); |
| 90 | +PHP; |
| 91 | + $result = ddless_eval_in_context($code, [], [], false); |
| 92 | + assert_eq(10, $result); |
| 93 | +}); |
| 94 | + |
| 95 | +section('ddless_eval_in_context() — $this binding'); |
| 96 | + |
| 97 | +test('binds $this when backtrace has object', function () { |
| 98 | + $obj = new EvalTestDummy(); |
| 99 | + $backtrace = [ |
| 100 | + ['function' => 'someMethod', 'object' => $obj], |
| 101 | + ]; |
| 102 | + $result = ddless_eval_in_context('$this->name', [], $backtrace); |
| 103 | + assert_eq('Alice', $result); |
| 104 | +}); |
| 105 | + |
| 106 | +test('$this can access private members via closure binding', function () { |
| 107 | + $obj = new EvalTestDummy(); |
| 108 | + $backtrace = [ |
| 109 | + ['function' => 'someMethod', 'object' => $obj], |
| 110 | + ]; |
| 111 | + $result = ddless_eval_in_context('$this->getSecret()', [], $backtrace); |
| 112 | + assert_eq(42, $result); |
| 113 | +}); |
| 114 | + |
| 115 | +test('skips ddless_ frames in backtrace', function () { |
| 116 | + $obj = new EvalTestDummy(); |
| 117 | + $backtrace = [ |
| 118 | + ['function' => 'ddless_step_check'], |
| 119 | + ['function' => 'ddless_handle_breakpoint'], |
| 120 | + ['function' => 'someMethod', 'object' => $obj], |
| 121 | + ]; |
| 122 | + $result = ddless_eval_in_context('$this->name', [], $backtrace); |
| 123 | + assert_eq('Alice', $result); |
| 124 | +}); |
| 125 | + |
| 126 | +test('no $this when backtrace has no object', function () { |
| 127 | + $backtrace = [ |
| 128 | + ['function' => 'plainFunction'], |
| 129 | + ]; |
| 130 | + // Without object binding, $this is not available — eval returns null via @ |
| 131 | + $result = ddless_eval_in_context('isset($this) ? "has this" : "no this"', [], $backtrace); |
| 132 | + assert_eq('no this', $result); |
| 133 | +}); |
| 134 | + |
| 135 | +test('combines scope variables with $this binding', function () { |
| 136 | + $obj = new EvalTestDummy(); |
| 137 | + $backtrace = [ |
| 138 | + ['function' => 'someMethod', 'object' => $obj], |
| 139 | + ]; |
| 140 | + $vars = ['suffix' => '!']; |
| 141 | + $result = ddless_eval_in_context('$this->name . $suffix', $vars, $backtrace); |
| 142 | + assert_eq('Alice!', $result); |
| 143 | +}); |
| 144 | + |
| 145 | +section('ddless_eval_in_context() — error handling'); |
| 146 | + |
| 147 | +test('syntax error throws ParseError', function () { |
| 148 | + $threw = false; |
| 149 | + try { |
| 150 | + ddless_eval_in_context('invalid syntax !!!', [], []); |
| 151 | + } catch (\ParseError $e) { |
| 152 | + $threw = true; |
| 153 | + } |
| 154 | + assert_true($threw, 'eval with syntax error should throw ParseError'); |
| 155 | +}); |
| 156 | + |
| 157 | +test('undefined variable returns null (suppressed by @)', function () { |
| 158 | + $result = ddless_eval_in_context('$nonExistentVar', [], []); |
| 159 | + assert_null($result); |
| 160 | +}); |
| 161 | + |
| 162 | +// Print results if run standalone |
| 163 | +if (basename($argv[0] ?? '') === basename(__FILE__)) { |
| 164 | + exit(print_test_results()); |
| 165 | +} |
0 commit comments