- 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