LGV_TZ_Lookup 1.4.2
Time zone lookup for PHP applications and HTTP clients
Loading...
Searching...
No Matches
regression.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
5require_once dirname(__DIR__).'/src/Sources/LGV_TZ_Lookup_Query.class.php';
6require_once __DIR__.'/support/SQLiteDatabase.php';
7
9function check($expected, $actual, string $label): void {
10 global $checks;
11 ++$checks;
12 if ($expected !== $actual) {
13 throw new RuntimeException($label.': expected '.var_export($expected, true).', got '.var_export($actual, true));
14 }
15}
16
17// The pre-optimization winding test is an independent reference for edge/vertex behavior.
18function referenceContains(array $point, array $polygon): bool {
19 $winding = 0;
20 $count = count($polygon);
21 for ($i = 0; $i < $count; ++$i) {
22 $a = $polygon[$i];
23 $b = $polygon[($i + 1) % $count];
24 $side = ($b[1] - $a[1]) * ($point[0] - $a[0]) - ($point[1] - $a[1]) * ($b[0] - $a[0]);
25 if ($a[0] <= $point[0]) {
26 if ($b[0] > $point[0] && $side > 0) { ++$winding; }
27 } elseif ($b[0] <= $point[0] && $side < 0) {
28 --$winding;
29 }
30 }
31 return $winding !== 0;
32}
33
34function packedRing(array $ring): string {
35 $binary = '';
36 foreach ($ring as $point) {
37 $binary .= pack('d2', $point[0], $point[1]);
38 }
39 return $binary;
40}
41
42$packedTest = new ReflectionMethod(LGV_TZ_Lookup_Query::class, '_wn_PackedPoly');
43if (PHP_VERSION_ID < 80100) { $packedTest->setAccessible(true); }
44$rings = [[], [[1.0, 2.0]], [[0.0, 0.0], [2.0, 2.0]],
45 [[0, 0], [4, 0], [4, 4], [0, 4]],
46 [[0, 0], [4, 0], [4, 4], [2, 2], [0, 4], [0, 0]],
47 [[0, 0], [4, 4], [0, 4], [4, 0], [0, 0]],
48 [[0, 0], [0, 0], [4, 0], [4, 4], [0, 4], [0, 0]],
49];
50// Cross decoding-block boundaries, with closed/open rings and either orientation.
51foreach ([1023, 1024, 1025, 2048, 2049, 8193] as $vertices) {
52 $ring = [];
53 for ($i = 0; $i < $vertices; ++$i) {
54 $angle = 2 * M_PI * $i / $vertices;
55 $radius = ($i % 2) ? 4 : 2;
56 $ring[] = [round(cos($angle) * $radius, 6), round(sin($angle) * $radius, 6)];
57 }
58 $rings[] = $ring;
59 $ring[] = $ring[0];
60 $rings[] = $ring;
61}
62mt_srand(20261007);
63foreach ($rings as $ring) {
64 foreach ([$ring, array_reverse($ring)] as $oriented) {
65 $packed = packedRing($oriented);
66 $points = [[0, 0], [1, 1], [4, 4], [-4, -4], [0, 4], [4, 0]];
67 foreach ([0, 1, 1022, 1023, 1024, count($oriented) - 1] as $index) {
68 if (isset($oriented[$index])) {
69 $points[] = $oriented[$index];
70 $points[] = [$oriented[$index][0] + 1e-7, $oriented[$index][1] - 1e-7];
71 if (isset($oriented[$index + 1])) {
72 $points[] = [($oriented[$index][0] + $oriented[$index + 1][0]) / 2,
73 ($oriented[$index][1] + $oriented[$index + 1][1]) / 2];
74 }
75 }
76 }
77 for ($i = 0; $i < 100; ++$i) {
78 $points[] = [mt_rand(-5000000, 5000000) / 1e6, mt_rand(-5000000, 5000000) / 1e6];
79 }
80 foreach ($points as $point) {
81 check(referenceContains($point, $oriented), $packedTest->invoke(null, $point[0], $point[1], $packed),
82 'Winding result with '.count($oriented).' vertices');
83 }
84 }
85}
86
88 public array $polygonRequests = [];
89 public function get_tz_polygons($ids) {
90 $this->polygonRequests[] = $ids;
91 yield from parent::get_tz_polygons($ids);
92 }
93}
94$database = new TrackingDatabase(':memory:');
95$database->reset_database();
97check('', $lookup->get_tz(0, 0), 'Empty database');
98check([], $database->polygonRequests, 'No empty polygon queries');
99$rect = ['east' => 4, 'west' => 0, 'north' => 4, 'south' => 0];
100$miss = [[0, 0], [4, 0], [4, 1], [0, 1], [0, 0]];
101$hit = [[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]];
102$database->store_entity(new LGV_TZ_Lookup_Entity('Region/First', $rect, $miss));
103check('Region/First', $lookup->get_tz(2, 2), 'Existing single-named-candidate shortcut');
104$database->store_entity(new LGV_TZ_Lookup_Entity('Region/Second', $rect, $miss));
105$database->store_entity(new LGV_TZ_Lookup_Entity('Etc/GMT', $rect, $hit));
106$database->polygonRequests = [];
107check('Etc/GMT', $lookup->get_tz(2, 2), 'Ocean fallback');
108check([[1, 2], [3]], $database->polygonRequests, 'Fetch each candidate only once');
109$database->store_entity(new LGV_TZ_Lookup_Entity('Region/Third', $rect, $hit));
110check('Region/Third', $lookup->get_tz(2, 2), 'Named zone takes precedence over oceans');
111$database->store_entity(new LGV_TZ_Lookup_Entity('Region/Fourth', $rect, $hit));
112check('Region/Third', $lookup->get_tz(2, 2), 'Primary-key first-match precedence');
113check('', $lookup->get_tz(10, 10), 'No bounding-box candidates');
114check([], iterator_to_array($database->get_tz_polygons([])), 'Empty polygon iterator');
115$decoded = $database->get_tz_entities([3]);
116check('Etc/GMT', $decoded[0]['tzname'], 'Legacy decoded-entity API');
117check($hit, array_map(static fn($p) => array_map('intval', $p), $decoded[0]['polygon']), 'Existing binary format');
118
119// Exercise the production PDO reader with an isolated PDO connection, including early cursor cleanup.
120$pdo = new PDO('sqlite::memory:');
121$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
122$wrapper = (new ReflectionClass(LGV_TZ_Lookup_PDO::class))->newInstanceWithoutConstructor();
123$connectionProperty = new ReflectionProperty(LGV_TZ_Lookup_PDO::class, '_pdo');
124if (PHP_VERSION_ID < 80100) { $connectionProperty->setAccessible(true); }
126$wrapper->driver_type = 'sqlite';
127$pdo->beginTransaction();
128check([['value' => 1]], $wrapper->preparedStatement('SELECT 1 AS value', [], true), 'Read API');
129check(true, $pdo->inTransaction(), 'Read leaves a caller transaction open');
130$rows = $wrapper->preparedRows('SELECT 1 AS value UNION ALL SELECT 2');
131foreach ($rows as $row) { break; }
132unset($rows);
133check([['value' => 3]], $wrapper->preparedStatement('SELECT 3 AS value', [], true), 'Query after early return');
134$pdo->rollBack();
135check(true, $wrapper->preparedStatement('CREATE TABLE write_test (value INTEGER)'), 'Write API return value');
136$wrapper->preparedStatement('INSERT INTO write_test VALUES (?)', [7]);
137check([['value' => 7]], $wrapper->preparedStatement('SELECT value FROM write_test', [], true), 'Write commits successfully');
138try {
139 iterator_to_array($wrapper->preparedRows('SELECT * FROM missing_table'));
140 throw new RuntimeException('Expected a database exception.');
141} catch (Exception $error) {
142 check(true, $error->getPrevious() instanceof PDOException, 'Preserve database exception context');
143}
144
145$options = getopt('', ['mysql:']);
146if (isset($options['mysql'])) {
147 if (!preg_match('/^lgv_tz_benchmark_[a-zA-Z0-9_]+$/D', $options['mysql'])) {
148 throw new InvalidArgumentException('Use an isolated lgv_tz_benchmark_NAME database.');
149 }
150 $mysqlWrapper = new LGV_TZ_Lookup_PDO($options['mysql'], getenv('LGV_TZ_BENCH_USER') ?: 'root',
151 getenv('LGV_TZ_BENCH_PASSWORD') ?: '', 'mysql', getenv('LGV_TZ_BENCH_HOST') ?: 'localhost',
152 (int)(getenv('LGV_TZ_BENCH_PORT') ?: 3306));
153 $mysqlConnection = $connectionProperty->getValue($mysqlWrapper);
154 $attribute = class_exists('Pdo\\Mysql') ? \Pdo\Mysql::ATTR_USE_BUFFERED_QUERY : PDO::MYSQL_ATTR_USE_BUFFERED_QUERY;
155 $originalBuffering = $mysqlConnection->getAttribute($attribute);
156 $rows = $mysqlWrapper->preparedRows('SELECT 1 AS value UNION ALL SELECT 2', [], false);
157 $rows->rewind();
158 check(false, $mysqlConnection->getAttribute($attribute), 'Unbuffered MySQL reader');
159 unset($rows);
160 check($originalBuffering, $mysqlConnection->getAttribute($attribute), 'Restore buffering after early return');
161 check(3, (int)$mysqlWrapper->preparedStatement('SELECT 3 AS value', [], true)[0]['value'], 'MySQL query after early return');
162 try {
163 iterator_to_array($mysqlWrapper->preparedRows('SELECT * FROM lgv_tz_nonexistent_table', [], false));
164 throw new RuntimeException('Expected a MySQL exception.');
165 } catch (Exception $error) {
166 check(true, $error->getPrevious() instanceof PDOException, 'MySQL exception context');
167 }
168 check($originalBuffering, $mysqlConnection->getAttribute($attribute), 'Restore buffering after database exception');
169 $mysqlConnection->beginTransaction();
170 $mysqlWrapper->preparedStatement('SELECT 1', [], true);
171 check(true, $mysqlConnection->inTransaction(), 'MySQL read leaves caller transaction open');
172 $mysqlConnection->rollBack();
173 check(true, $mysqlWrapper->preparedStatement('CREATE TEMPORARY TABLE lgv_tz_write_test (value INTEGER)'), 'MySQL write API');
174 $mysqlWrapper->preparedStatement('INSERT INTO lgv_tz_write_test VALUES (?)', [7]);
175 check(7, (int)$mysqlWrapper->preparedStatement('SELECT value FROM lgv_tz_write_test', [], true)[0]['value'], 'MySQL write commits successfully');
176}
177echo 'Passed '.$checks." regression checks.\n";
This class provides a genericized interface to the PHP PDO toolkit.
$options
Definition composer.php:6
foreach([[1, 'America/New_York', [0, 0, 4, 0, 4, 1, 0, 1, 0, 0]], [2, 'America/Chicago', [0, 0, 4, 0, 4, 4, 0, 4, 0, 0]], [3, 'Etc/GMT', [0, 0, 4, 0, 4, 4, 0, 4, 0, 0]],] as[$id, $zone, $points]) $wrapper
Definition composer.php:47
$database
Definition composer.php:52
$connectionProperty
Definition composer.php:48
if(!is_file($autoload)) $checks
Definition composer.php:11
$lookup
Definition composer.php:54
referenceContains(array $point, array $polygon)
$pdo
$miss
check($expected, $actual, string $label)
Definition regression.php:9
$decoded
packedRing(array $ring)
$database polygonRequests
if(PHP_VERSION_ID< 80100) $rings
$rect
$rows
$packedTest
$hit
$vertices
Definition demo.php:94
$error
Definition demo.php:116