329 $name =
'lgv_tz_demo_'.bin2hex(random_bytes(12));
334 $blocked = function_exists(
'pcntl_sigprocmask') && pcntl_sigprocmask(SIG_BLOCK, [SIGHUP, SIGINT, SIGTERM], $previousMask);
338 if (
false === file_put_contents(
$directory.
'/database.json', json_encode([
'database' =>
$name,
'driver' =>
$driver], JSON_THROW_ON_ERROR))) {
340 throw new RuntimeException(
'Could not record the temporary database for cleanup.');
343 if ($blocked) { pcntl_sigprocmask(SIG_SETMASK, $previousMask); }
366 if (PHP_VERSION_ID < 80000) {
367 throw new RuntimeException(
'PHP 8.0 or later is required.');
370 foreach ([
'pdo_'.
$driver,
'curl',
'zip',
'mbstring',
'ctype'] as $extension) {
371 if (!extension_loaded($extension)) {
372 throw new RuntimeException(
'The PHP '.$extension.
' extension is required.');
375 if (!function_exists(
'proc_open')) {
376 throw new RuntimeException(
'PHP proc_open must be enabled to run Composer.');
380 printf(
"Database: %s %s.\n",
$driver,
$admin->getAttribute(PDO::ATTR_SERVER_VERSION));
386 echo
"Finding the latest timezone boundary release...\n";
389 $release = json_decode(file_get_contents(
$directory.
'/release.json'),
true, 512, JSON_THROW_ON_ERROR);
391 foreach ($release[
'assets'] ?? [] as $candidate) {
392 if ($candidate[
'name'] ===
'timezones-with-oceans.geojson.zip') {
397 if (
null === $asset) {
398 throw new RuntimeException(
'The latest release does not contain timezones-with-oceans.geojson.zip.');
400 printf(
"Downloading boundary release %s (%.1f MiB compressed)...\n", $release[
'tag_name'], $asset[
'size'] / 1048576);
404 if (isset($asset[
'digest']) && str_starts_with($asset[
'digest'],
'sha256:') &&
405 !hash_equals(substr($asset[
'digest'], 7), hash_file(
'sha256', $archive))) {
406 throw new RuntimeException(
'Boundary archive checksum did not match the release metadata.');
414 printf(
"Extracted %.1f MiB of GeoJSON.\n", filesize($boundary) / 1048576);
416 echo
'Loading boundaries into temporary database '.$name.
"...\n";
417 require_once __DIR__.
'/SystemTests.php';
422 $stream = fopen($boundary,
'rb');
423 if (
false === $stream) {
424 throw new RuntimeException(
'Could not open the boundary file.');
432 $parser =
new JsonStreamingParser\Parser($stream, $inspector);
434 $demoMetrics[
'load_peak_bytes'] = memory_get_peak_usage();
437 unset($parser, $listener, $inspector);
444 $validation->exec(
'ANALYZE timezones');
446 $polygons = $validation->query(
'SELECT COUNT(*) FROM timezones')->fetchColumn();
447 printf(
"Loaded %s polygons in %.2f seconds. Download and extracted file deleted.\n", $polygons, (hrtime(
true) -
$start) / 1e9);
448 if ((
int)$polygons === 0) {
449 throw new RuntimeException(
'No polygons were loaded.');
454 echo
"Checking stored polygons against the decoded source GeoJSON...\n";
455 $storageFailures =
$source->validateStorage($validation);
456 foreach ($storageFailures as $failure) { echo
'FAIL '.$failure.
"\n"; }
457 printf(
"Source/storage validation: %d polygons checked, %d failures.\n", count(
$source->shapes), count($storageFailures));
459 $largestCount = filter_var(
demoEnvironment(
'LARGEST_SHAPES',
'10'), FILTER_VALIDATE_INT,
460 [
'options' => [
'min_range' => 1,
'max_range' => 100]]);
461 if (
false === $largestCount) {
throw new RuntimeException(
'LGV_TZ_DEMO_LARGEST_SHAPES must be between 1 and 100.'); }
462 $demoMetrics[
'largest'] = array_map(
static fn($shape) => array_intersect_key($shape, array_flip([
'id',
'tzname',
'vertices',
'bytes'])),
$source->largest($largestCount));
463 echo
'Generating independent interior and two-sided boundary probes for the '.$largestCount.
" largest polygons...\n";
464 $generated =
$source->casesForLargest($largestCount);
466 foreach ($generated as $case) {
467 if (isset($case[
'pair'])) { $pairs[$case[
'pair']][$case[
'side']] = $case[
'expected']; }
472 foreach ($pairs as $sides) {
473 if (isset($sides[-1], $sides[1]) && $sides[-1] !== $sides[1]) {
475 $zones = array_merge($sides[-1], $sides[1]);
476 if (!empty($sides[-1]) && !empty($sides[1]) &&
477 count(array_filter($zones,
static fn($z) => str_starts_with($z,
'Etc/'))) === 0) {
482 $demoMetrics[
'reference_peak_bytes'] = memory_get_peak_usage();
485 if (function_exists(
'gc_mem_caches')) { gc_mem_caches(); }
488 $package = Composer\InstalledVersions::getInstallPath(
DEMO_PACKAGE);
489 require $package.
'/src/TestLocations.php';
491 throw new RuntimeException(
'The installed package contains no location tests.');
496 $test[
'expected'] = [$test[
'result']];
497 $test[
'kind'] =
'known location';
504 echo
'Running '.count(
$test_locations_param_array).
' known locations and '.count($generated).
" generated system tests through the Composer lookup API...\n";
505 foreach (
$cases as $index => $test) {
506 $lng = $test[
'params'][
'lng'];
507 $lat = $test[
'params'][
'lat'];
508 while ($lng < -180) { $lng += 360; }
509 while ($lng > 180) { $lng -= 360; }
510 while ($lat < -90) { $lat += 180; }
511 while ($lat > 90) { $lat -= 180; }
513 $baselineMemory = memory_get_usage();
514 $database->generatedCall = $test[
'kind'] !==
'known location';
515 $callStart = hrtime(
true);
516 $actual =
$lookup->get_tz($lng, $lat);
517 $elapsed = (hrtime(
true) - $callStart) / 1e6;
519 if ($test[
'kind'] !==
'known location') {
$demoMetrics[
'generated_lookup_ms'][] = $elapsed; }
521 $demoMetrics[
'lookup_extra_bytes'] = max(
$demoMetrics[
'lookup_extra_bytes'], memory_get_peak_usage() - $baselineMemory);
523 $pass = in_array($actual, empty($test[
'expected']) ? [
''] : $test[
'expected'],
true);
524 $groups[$test[
'kind']][
'total'] = ($groups[$test[
'kind']][
'total'] ?? 0) + 1;
525 $groups[$test[
'kind']][
'passed'] = ($groups[$test[
'kind']][
'passed'] ?? 0) + (
int)$pass;
528 printf(
"FAIL %s [%.6f, %.6f]: expected %s, received %s\n", $test[
'title'], $lng, $lat,
529 empty($test[
'expected']) ?
'(no match)' : implode(
' or ', $test[
'expected']), $actual ===
'' ?
'(no match)' : $actual);
530 } elseif ($index < 5) {
531 printf(
"PASS %s: %s\n", $test[
'title'], $actual);
542 echo
'FAIL coverage: generated probes did not decode largest polygon '.$shape[
'id'].
' ('.$shape[
'tzname'].
").\n";
545 foreach ($groups as $kind => $group) { printf(
"%s: %d/%d passed.\n", ucfirst($kind), $group[
'passed'], $group[
'total']); }
548 fwrite(STDERR,
"System tests found mismatches. Generated expectations come from GeoJSON, not package output.\n");
560if (realpath($_SERVER[
'SCRIPT_FILENAME'] ??
'') !== __FILE__) {
return; }
562if (PHP_SAPI !==
'cli' || $argc !== 3 || !in_array($argv[1], [
'run',
'cleanup'],
true) || !is_dir($argv[2])) {
563 fwrite(STDERR,
"Run this demo with: ./demo/run.sh\n");
567if (function_exists(
'pcntl_async_signals')) {
568 pcntl_async_signals(
true);
569 foreach ([SIGHUP, SIGINT, SIGTERM] as $signal) {
570 pcntl_signal($signal,
static function(
int $signal):
void {
571 throw new RuntimeException(
'Demo interrupted by signal '.$signal.
'.');
577}
catch (Throwable
$error) {
578 fwrite(STDERR,
'Demo error: '.
$error->getMessage().PHP_EOL);
demoRun(string $directory)
This performs the complete Composer installation, data load, system test, and metrics run.