Skip to content

Commit 63c29c6

Browse files
committed
test: add unit tests for ddless_eval_in_context() functionality
1 parent cd5797b commit 63c29c6

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

tests/EvalInContextTest.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
}

tests/run-all.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'NormalizeValueTest.php',
1919
'InstrumentTraceTest.php',
2020
'HttpRequestTest.php',
21+
'EvalInContextTest.php',
2122
];
2223

2324
foreach ($testFiles as $file) {

0 commit comments

Comments
 (0)