- Implemented parseNestedArrayUnset method to handle chained array dimension unsets - Added logic to process nested ArrayDimFetch expressions in unset statements - Created proper dimension parsing and code generation for nested array operations - Maintained PHP evaluation order for key materialization in nested unsets - Added comprehensive test coverage for nested unset chain behavior - Extended compiler tests to verify correct handling of complex unset scenariosmaster
parent
ab01b2c5c9
commit
9bc716a54c
4 changed files with 307 additions and 0 deletions
@ -0,0 +1,164 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
require dirname(__DIR__) . '/app/TunnelRule.php'; |
||||||
|
require dirname(__DIR__) . '/app/TunnelRepository.php'; |
||||||
|
require dirname(__DIR__) . '/app/SshOutputParser.php'; |
||||||
|
|
||||||
|
$scriptedEvents = []; |
||||||
|
$startedRuleIds = []; |
||||||
|
$startAttempts = []; |
||||||
|
$renderedRows = []; |
||||||
|
$shownErrors = []; |
||||||
|
|
||||||
|
function qt_tunnel_create(string $title): mixed |
||||||
|
{ |
||||||
|
return new stdClass(); |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_is_open(mixed $window): bool |
||||||
|
{ |
||||||
|
global $scriptedEvents; |
||||||
|
return $scriptedEvents !== []; |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_process_events(mixed $window): void {} |
||||||
|
|
||||||
|
function qt_tunnel_poll_event(mixed $window): array |
||||||
|
{ |
||||||
|
global $scriptedEvents; |
||||||
|
return array_shift($scriptedEvents) ?? []; |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_set_rules(mixed $window, array $rules): void |
||||||
|
{ |
||||||
|
global $renderedRows; |
||||||
|
$renderedRows = $rules; |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_start_process( |
||||||
|
mixed $window, |
||||||
|
string $id, |
||||||
|
string $program, |
||||||
|
array $arguments |
||||||
|
): bool { |
||||||
|
global $startedRuleIds, $startAttempts; |
||||||
|
$startAttempts[$id] = ($startAttempts[$id] ?? 0) + 1; |
||||||
|
// The "broken" rule starts once so the application comes up, then fails |
||||||
|
// on every later attempt to exercise start-all error isolation. |
||||||
|
if ($id === 'broken' && $startAttempts[$id] > 1) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$startedRuleIds[] = $id; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_stop_process(mixed $window, string $id): void {} |
||||||
|
function qt_tunnel_append_log(mixed $window, string $id, string $message): void {} |
||||||
|
|
||||||
|
function qt_tunnel_show_error(mixed $window, string $message): void |
||||||
|
{ |
||||||
|
global $shownErrors; |
||||||
|
$shownErrors[] = $message; |
||||||
|
} |
||||||
|
|
||||||
|
function qt_tunnel_destroy(mixed $window): void {} |
||||||
|
|
||||||
|
require dirname(__DIR__) . '/app/TunnelApplication.php'; |
||||||
|
|
||||||
|
function start_all_rule(string $id): TunnelRule |
||||||
|
{ |
||||||
|
return new TunnelRule([ |
||||||
|
'id' => $id, |
||||||
|
'name' => $id, |
||||||
|
'type' => TunnelRule::TYPE_LOCAL, |
||||||
|
'ssh_host' => 'gateway.example.com', |
||||||
|
'ssh_port' => 22, |
||||||
|
'ssh_user' => 'deploy', |
||||||
|
'identity_file' => '', |
||||||
|
'local_host' => '127.0.0.1', |
||||||
|
'local_port' => 8080, |
||||||
|
'remote_host' => '127.0.0.1', |
||||||
|
'remote_port' => 3000, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
function start_all_event(string $type, string $id): array |
||||||
|
{ |
||||||
|
return $type === 'start_all' ? ['type' => $type] : ['type' => $type, 'id' => $id]; |
||||||
|
} |
||||||
|
|
||||||
|
function statuses_by_id(array $rows): array |
||||||
|
{ |
||||||
|
$statuses = []; |
||||||
|
foreach ($rows as $row) { |
||||||
|
$statuses[$row['id']] = $row['status']; |
||||||
|
} |
||||||
|
return $statuses; |
||||||
|
} |
||||||
|
|
||||||
|
$file = sys_get_temp_dir() . '/typephp-ssh-start-all-' . uniqid('', true) . '.json'; |
||||||
|
$repository = new TunnelRepository($file); |
||||||
|
foreach (['a', 'b', 'c', 'broken'] as $id) { |
||||||
|
$repository->create(start_all_rule($id)); |
||||||
|
} |
||||||
|
|
||||||
|
$scriptedEvents = [ |
||||||
|
start_all_event('process_stopped', 'a'), |
||||||
|
start_all_event('process_stopped', 'b'), |
||||||
|
start_all_event('process_stopped', 'broken'), |
||||||
|
start_all_event('start_all', ''), |
||||||
|
]; |
||||||
|
|
||||||
|
$application = new TunnelApplication($repository); |
||||||
|
$result = $application->run(); |
||||||
|
|
||||||
|
if ($result !== 0) { |
||||||
|
throw new RuntimeException('application run must return 0'); |
||||||
|
} |
||||||
|
if ($startedRuleIds !== ['a', 'b', 'c', 'broken', 'a', 'b']) { |
||||||
|
throw new RuntimeException( |
||||||
|
'start all must restart every stopped rule once and skip transitioning ones, got ' |
||||||
|
. var_export($startedRuleIds, true) |
||||||
|
); |
||||||
|
} |
||||||
|
if (count($shownErrors) !== 1 || !str_contains($shownErrors[0], 'broken')) { |
||||||
|
throw new RuntimeException( |
||||||
|
'a failing rule must be reported without aborting the remaining starts, got ' |
||||||
|
. var_export($shownErrors, true) |
||||||
|
); |
||||||
|
} |
||||||
|
if (statuses_by_id($renderedRows) !== [ |
||||||
|
'a' => 'starting', |
||||||
|
'b' => 'starting', |
||||||
|
'c' => 'starting', |
||||||
|
'broken' => 'error', |
||||||
|
]) { |
||||||
|
throw new RuntimeException('unexpected statuses after start all: ' . var_export($renderedRows, true)); |
||||||
|
} |
||||||
|
|
||||||
|
// A second start-all with every rule running must be a no-op: the button is |
||||||
|
// disabled in the UI, and the controller must tolerate the event anyway. |
||||||
|
$startedRuleIds = []; |
||||||
|
$startAttempts = []; |
||||||
|
$shownErrors = []; |
||||||
|
$scriptedEvents = [ |
||||||
|
start_all_event('process_started', 'a'), |
||||||
|
start_all_event('process_started', 'b'), |
||||||
|
start_all_event('process_started', 'c'), |
||||||
|
start_all_event('process_started', 'broken'), |
||||||
|
start_all_event('start_all', ''), |
||||||
|
]; |
||||||
|
|
||||||
|
$application = new TunnelApplication($repository); |
||||||
|
$result = $application->run(); |
||||||
|
|
||||||
|
if ($result !== 0 || $startedRuleIds !== ['a', 'b', 'c', 'broken'] || $shownErrors !== []) { |
||||||
|
throw new RuntimeException( |
||||||
|
'start all must not touch running rules, got ' |
||||||
|
. var_export([$startedRuleIds, $shownErrors], true) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
unlink($file); |
||||||
|
unlink($file . '.bak'); |
||||||
|
echo "ssh-tunnel start-all tests passed\n"; |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
--TEST-- |
||||||
|
unset nested array offsets mutates original, skips missing parents, and evaluates keys in order |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class NestedUnsetHolder { |
||||||
|
public array $items = ['outer' => ['remove' => 1, 'keep' => 2]]; |
||||||
|
} |
||||||
|
|
||||||
|
function unset_chain_key(string $key): string { |
||||||
|
echo "key:$key\n"; |
||||||
|
return $key; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void { |
||||||
|
$items = ['outer' => ['deep' => ['remove' => 1, 'keep' => 2]]]; |
||||||
|
$copy = $items; |
||||||
|
unset($items['outer']['deep']['remove']); |
||||||
|
var_dump($items['outer']['deep']); |
||||||
|
var_dump($copy['outer']['deep']['remove']); |
||||||
|
|
||||||
|
unset($items[unset_chain_key('missing')][unset_chain_key('leaf')]); |
||||||
|
var_dump(isset($items['missing'])); |
||||||
|
|
||||||
|
$items['empty'] = null; |
||||||
|
unset($items['empty']['leaf']); |
||||||
|
var_dump($items['empty']); |
||||||
|
|
||||||
|
$items[''] = ['remove' => 9]; |
||||||
|
unset($items[null]['remove']); |
||||||
|
var_dump($items['']); |
||||||
|
|
||||||
|
$holder = new NestedUnsetHolder(); |
||||||
|
unset($holder->items['outer']['remove']); |
||||||
|
var_dump($holder->items['outer']); |
||||||
|
|
||||||
|
unset($items[unset_chain_key('outer')][unset_chain_key('deep')][unset_chain_key('keep')], |
||||||
|
$holder->items[unset_chain_key('outer')][unset_chain_key('keep')]); |
||||||
|
var_dump($items['outer']['deep'], $holder->items['outer']); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(1) { |
||||||
|
["keep"]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
|
int(1) |
||||||
|
key:missing |
||||||
|
key:leaf |
||||||
|
bool(false) |
||||||
|
NULL |
||||||
|
array(0) { |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
["keep"]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
|
key:outer |
||||||
|
key:deep |
||||||
|
key:keep |
||||||
|
key:outer |
||||||
|
key:keep |
||||||
|
array(0) { |
||||||
|
} |
||||||
|
array(0) { |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
--TEST-- |
||||||
|
unset fixed scalar and array properties of objects stored in arrays restores empty values |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ArrayElementWithTypedProperties { |
||||||
|
public int $number = 9; |
||||||
|
public float $fraction = 1.5; |
||||||
|
public bool $enabled = true; |
||||||
|
public string $label = 'initial'; |
||||||
|
public array $values = [1, 2]; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void { |
||||||
|
$objects = [new ArrayElementWithTypedProperties()]; |
||||||
|
$nested = ['group' => $objects]; |
||||||
|
|
||||||
|
unset($objects[0]->number); |
||||||
|
unset($objects[0]->fraction); |
||||||
|
unset($objects[0]->enabled); |
||||||
|
unset($nested['group'][0]->label); |
||||||
|
unset($nested['group'][0]->values); |
||||||
|
|
||||||
|
var_dump($objects[0]->number, $objects[0]->fraction, $objects[0]->enabled); |
||||||
|
var_dump($objects[0]->label, $objects[0]->values); |
||||||
|
var_dump(isset($objects[0]->number), isset($objects[0]->label)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(0) |
||||||
|
float(0) |
||||||
|
bool(false) |
||||||
|
string(0) "" |
||||||
|
array(0) { |
||||||
|
} |
||||||
|
bool(true) |
||||||
|
bool(true) |
||||||
Loading…
Reference in new issue