LGV_TZ_Lookup 1.4.2
Time zone lookup for PHP applications and HTTP clients
Loading...
Searching...
No Matches
update.php
Go to the documentation of this file.
1<?php
2/**
3 © Copyright 2023-2026, Little Green Viper Software Development LLC.
4 MIT License; see LICENSE.
5 \file
6 \brief Check boundary updating and failure recovery against isolated MySQL or PostgreSQL databases.
7 Uses small local ZIP/release fixtures, with the same LGV_TZ_TEST_* settings as tests/databases.php.
8 Add --live to exercise the installed shell command with the latest full download and all known-location checks.
9*/
10declare(strict_types=1);
11require dirname(__DIR__).'/tools/update.php';
12require dirname(__DIR__).'/tools/deploy.php';
13require dirname(__DIR__).'/vendor/autoload.php';
14$options = getopt('', ['driver:', 'live']);
15$driver = $options['driver'] ?? 'mysql';
16if (!in_array($driver, ['mysql', 'pgsql'], true)) { throw new InvalidArgumentException('Choose mysql or pgsql.'); }
18
19/** \brief Check a result and count successful assertions. */
20function updaterCheck($expected, $actual, string $label): void {
21 global $checks;
22 ++$checks;
23 if ($expected !== $actual) { throw new RuntimeException($label.' failed: '.var_export($actual, true)); }
24}
25
26/** \brief Expect an operation to fail with a diagnostic message. */
27function updaterFailure(callable $operation, string $message): void {
28 try { $operation(); }
29 catch (Throwable $error) { updaterCheck(true, str_contains($error->getMessage(), $message), 'Expected '.$message); return; }
30 throw new RuntimeException('Expected failure: '.$message);
31}
32
33$name = 'lgv_tz_update_test_'.bin2hex(random_bytes(8));
34$settings = ['driver' => $driver, 'host' => getenv('LGV_TZ_TEST_HOST') ?: 'localhost',
35 'port' => (int)(getenv('LGV_TZ_TEST_PORT') ?: ($driver === 'pgsql' ? 5432 : 3306)),
36 'database' => $name, 'user' => getenv('LGV_TZ_TEST_USER') ?: ($driver === 'pgsql' ? (getenv('USER') ?: get_current_user()) : 'root'),
37 'password' => getenv('LGV_TZ_TEST_PASSWORD') ?: '', 'admin_database' => getenv('LGV_TZ_TEST_ADMIN_DATABASE') ?: 'postgres'];
39$admin->exec('CREATE DATABASE '.deployIdentifier($name, $driver));
40$work = sys_get_temp_dir().'/lgv-tz-update-test-'.bin2hex(random_bytes(8));
41mkdir($work, 0700);
43try {
44 $config = "<?php\n";
45 foreach (['g_dbName' => $name, 'g_dbType' => $driver, 'g_dbUserName' => $settings['user'],
46 'g_dbPassword' => $settings['password'], 'g_dbHost' => $settings['host'], 'g_dbPort' => $settings['port'],
47 'g_server_secret' => 'existing-secret'] as $key => $value) { $config .= '$'.$key.' = '.var_export($value, true).";\n"; }
48 file_put_contents($work.'/config.php', $config);
50 $connection->exec('CREATE TABLE unrelated (value INTEGER)');
51 $connection->exec('INSERT INTO unrelated VALUES (42)');
52 $database = new LGV_TZ_Lookup_Database($name, $settings['user'], $settings['password'], $driver, $settings['host'], $settings['port']);
53 $feature = ['type' => 'Feature', 'properties' => ['tzid' => 'Asia/Tokyo'],
54 'geometry' => ['type' => 'Polygon', 'coordinates' => [[[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]]]]];
55 $listener = new LGV_TZ_Lookup_Loader($database);
56 $listener::listener_action($feature);
57 $feature['properties']['tzid'] = 'America/New_York';
58 $json = json_encode(['type' => 'FeatureCollection', 'features' => [$feature]], JSON_THROW_ON_ERROR);
59 $zip = new ZipArchive();
60 $zip->open($work.'/fixture.zip', ZipArchive::CREATE);
61 $zip->addFromString('combined-with-oceans.json', $json);
62 $zip->close();
63 $metadata = ['tag_name' => 'test-release', 'assets' => [['name' => 'timezones-with-oceans.geojson.zip',
64 'browser_download_url' => 'https://example.com/boundaries.zip', 'size' => filesize($work.'/fixture.zip'),
65 'digest' => 'sha256:'.hash_file('sha256', $work.'/fixture.zip')]]];
66 $calls = [];
67 $download = static function(string $url, string $destination) use ($work, &$metadata, &$calls): void {
68 $calls[] = $url;
69 if (str_ends_with($url, '/releases/latest')) { file_put_contents($destination, json_encode($metadata, JSON_THROW_ON_ERROR)); }
70 else { copy($work.'/fixture.zip', $destination); }
71 };
72 ob_start();
73 updateRun($work, '/does/not/exist.php', true, $download);
74 $report = ob_get_clean();
75 updaterCheck(true, str_contains($report, 'Latest boundary release: test-release'), 'Version report');
76 updaterCheck(1, count($calls), 'Check downloads metadata only');
77 updaterCheck('Asia/Tokyo', (new LGV_TZ_Lookup_Query($database))->get_tz(1, 1), 'Check preserves live data');
78
80 updaterFailure(static fn() => updateConnection($settings), 'Another timezone');
81 $lock = null;
82 updateRun($work, $work.'/config.php', false, $download);
83 updaterCheck('America/New_York', (new LGV_TZ_Lookup_Query($database))->get_tz(1, 1), 'Publish new data');
84 updaterCheck(1, (int)$connection->query('SELECT COUNT(*) FROM timezones')->fetchColumn(), 'Replace rather than append');
85 updaterCheck(42, (int)$connection->query('SELECT value FROM unrelated')->fetchColumn(), 'Preserve unrelated tables');
86 updaterCheck($config, file_get_contents($work.'/config.php'), 'Preserve settings and secret');
88 foreach (['boundaries.zip', 'boundaries.json', 'release.json', 'update.json'] as $file) {
89 updaterCheck(false, is_file($work.'/'.$file), 'Delete '.$file);
90 }
91 // Repeated updates work even when the release tag is unchanged.
92 updateRun($work, $work.'/config.php', false, $download);
94
95 $original = $metadata;
96 $metadata['assets'][0]['digest'] = 'sha256:'.str_repeat('0', 64);
97 updaterFailure(static fn() => updateRun($work, $work.'/config.php', false, $download), 'checksum differs');
99 $metadata = $original;
100 $metadata['assets'] = [];
101 updaterFailure(static fn() => updateRun($work, $work.'/config.php', false, $download), 'no full oceans');
103 $metadata = $original;
104 $metadata['tag_name'] = '';
105 updaterFailure(static fn() => updateRun($work, $work.'/config.php', false, $download), 'no version tag');
107 $metadata = $original;
108
109 // A syntactically broken file writes one polygon into staging before the parser fails.
110 $zip->open($work.'/fixture.zip', ZipArchive::OVERWRITE);
111 $zip->addFromString('combined-with-oceans.json', substr($json, 0, -2).',');
112 $zip->close();
113 clearstatcache();
114 $metadata['assets'][0]['size'] = filesize($work.'/fixture.zip');
115 $metadata['assets'][0]['digest'] = 'sha256:'.hash_file('sha256', $work.'/fixture.zip');
116 updaterFailure(static fn() => updateRun($work, $work.'/config.php', false, $download), 'JSON');
117 updaterCheck('America/New_York', (new LGV_TZ_Lookup_Query($database))->get_tz(1, 1), 'Failed load preserves live data');
119 $zip->open($work.'/fixture.zip', ZipArchive::OVERWRITE);
120 $zip->addFromString('combined-with-oceans.json', '{"type":"FeatureCollection","features":[]}');
121 $zip->close();
122 clearstatcache();
123 $metadata['assets'][0]['size'] = filesize($work.'/fixture.zip');
124 $metadata['assets'][0]['digest'] = 'sha256:'.hash_file('sha256', $work.'/fixture.zip');
125 updaterFailure(static fn() => updateRun($work, $work.'/config.php', false, $download), 'No polygons');
127
128 $table = 'lgv_tz_update_'.bin2hex(random_bytes(8));
129 $connection->exec('CREATE TABLE '.$table.' (id INTEGER)');
130 $old = 'lgv_tz_old_'.substr($table, -16);
131 $connection->exec('CREATE TABLE '.$old.' (id INTEGER)');
132 file_put_contents($work.'/update.json', json_encode(['config' => $work.'/config.php', 'table' => $table], JSON_THROW_ON_ERROR));
133 file_put_contents($work.'/boundaries.json', 'partial download');
135 updaterCheck(false, deployTableExists($connection, $table), 'Remove interrupted staging table');
136 updaterCheck(false, deployTableExists($connection, $old), 'Remove interrupted old copy');
137 file_put_contents($work.'/update.json', json_encode(['config' => $work.'/config.php', 'table' => 'timezones'], JSON_THROW_ON_ERROR));
138 file_put_contents($work.'/boundaries.zip', 'partial download');
139 updaterFailure(static fn() => updateCleanup($work), 'Invalid staging table');
140 updaterCheck(false, is_file($work.'/boundaries.zip'), 'Delete downloads before recovery refusal');
141 updaterCheck(true, deployTableExists($connection, 'timezones'), 'Refuse cleanup of live table');
142 unlink($work.'/update.json');
143 $schema = $driver === 'pgsql' ? 'current_schema()' : 'DATABASE()';
144 $tables = $connection->query('SELECT table_name FROM information_schema.tables WHERE table_schema = '.$schema.' ORDER BY table_name')->fetchAll(PDO::FETCH_COLUMN);
145 updaterCheck(['timezones', 'unrelated'], $tables, 'No staging or backup tables remain');
146 if (isset($options['live'])) {
147 // Exercise the deployed layout and default config discovery, with the current package's Composer autoloader.
148 $installed = $work.'/installed';
149 mkdir($installed.'/tools', 0700, true);
150 mkdir($installed.'/app/vendor', 0700, true);
151 mkdir($work.'/downloads', 0700);
152 copy($work.'/config.php', $installed.'/config.php');
153 foreach (['update.php', 'Setup.php'] as $file) { copy(dirname(__DIR__).'/tools/'.$file, $installed.'/tools/'.$file); }
154 copy(dirname(__DIR__).'/update.sh', $installed.'/update.sh');
155 chmod($installed.'/update.sh', 0700);
156 file_put_contents($installed.'/app/vendor/autoload.php', '<?php require '.var_export(dirname(__DIR__).'/vendor/autoload.php', true).';');
157 $environment = getenv();
158 $environment['TMPDIR'] = $work.'/downloads';
159 $environment['LGV_TZ_UPDATE_PHP'] = PHP_BINARY;
160 $process = proc_open([$installed.'/update.sh'], [0 => ['file', '/dev/null', 'r'], 1 => STDOUT, 2 => STDERR], $pipes, null, $environment);
161 if ($process === false) { throw new RuntimeException('Could not start installed updater.'); }
162 updaterCheck(0, proc_close($process), 'Full installed update succeeds');
163 updaterCheck([], array_values(array_diff(scandir($work.'/downloads'), ['.', '..'])), 'Shell removes all downloaded files');
164 require dirname(__DIR__).'/src/TestLocations.php';
166 foreach ($test_locations_param_array as $case) {
167 $lng = $case['params']['lng']; $lat = $case['params']['lat'];
168 while ($lng < -180) { $lng += 360; } while ($lng > 180) { $lng -= 360; }
169 while ($lat < -90) { $lat += 180; } while ($lat > 90) { $lat -= 180; }
170 updaterCheck($case['result'], $lookup->get_tz($lng, $lat), $case['title']);
171 }
172 $lookup = null;
173 updaterCheck($config, file_get_contents($installed.'/config.php'), 'Full update preserves server configuration');
174 updaterCheck(42, (int)$connection->query('SELECT value FROM unrelated')->fetchColumn(), 'Full update preserves unrelated data');
175 }
176 echo 'Passed '.$checks.' '.$driver." updater checks.\n";
177} finally {
178 if (ob_get_level()) { ob_end_clean(); }
180 $database = null; $listener = null; $connection = null; $lock = null;
181 $admin->exec('DROP DATABASE '.deployIdentifier($name, $driver));
183}
$test_locations_param_array
This file has a list of long/lat pairs, along with the expected results, when presented to the server...
$options
Definition composer.php:6
$connection
Definition composer.php:31
$database
Definition composer.php:52
if(!is_file($autoload)) $checks
Definition composer.php:11
$lookup
Definition composer.php:54
$driver
Definition databases.php:38
$name
if($driver==='pgsql') $admin
if(realpath($_SERVER['SCRIPT_FILENAME'] ?? '') !==__FILE__) if(PHP_SAPI !=='cli'||$argc !==3||!in_array( $argv[1],[ 'run', 'cleanup'], true)||!is_dir( $argv[2])) if(function_exists('pcntl_async_signals')) try
This is the worker entrypoint.
Definition demo.php:575
$calls
Definition server.php:16
$config
Definition server.php:43
$process
Definition demo.php:113
$environment
Definition demo.php:128
$error
Definition demo.php:116
$work
Definition update.php:40
updaterFailure(callable $operation, string $message)
Expect an operation to fail with a diagnostic message.
Definition update.php:27
$settings
Definition update.php:34
updaterCheck($expected, $actual, string $label)
Check a result and count successful assertions.
Definition update.php:20
deployConnection(array $settings, bool $administrative=false)
This opens the configured application or administrative database.
Definition deploy.php:57
deployRemoveTree(string $directory)
This removes a directory tree without following symlinks.
Definition deploy.php:145
deployIdentifier(string $name, string $driver)
This quotes a database identifier for the selected backend.
Definition deploy.php:40
deployTableExists(PDO $connection, string $table)
This checks for an existing table without attempting to create or replace it.
Definition deploy.php:80
updateCleanup(string $work)
Remove downloaded data and a recorded staging table after an interrupted load.
Definition update.php:61
updateConnection(array $settings)
Connect and hold the same database lock used by the installer.
Definition update.php:48
updateRun(string $work, string $config, bool $check=false, ?callable $download=null)
Discover, report, download, and load boundaries; –check stops before reading config or touching the d...
Definition update.php:125