duckling 0.4.7-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ # The tz-database axis
2
+
3
+ This document is the central reference for time zone (tz) data in this gem.
4
+ It replaces long inline comments. Keep inline comments short. Point here
5
+ instead.
6
+
7
+ ## Why the tz database is an axis
8
+
9
+ `reference_zone:` resolves zones with the `tzinfo` gem. tzinfo uses the
10
+ `tzinfo-data` gem when that gem is installed. Without that gem, tzinfo uses
11
+ the zoneinfo files of the host. This gem does not depend on `tzinfo-data`.
12
+ Both configurations are valid production configurations.
13
+
14
+ The two databases do not give the same answers. They differ on three
15
+ independent axes:
16
+
17
+ - **Modelling.** Some distributions compile tzdata in rearguard format.
18
+ Rearguard data strips negative DST. Example: `Europe/Dublin` is a
19
+ negative-DST zone in vanguard data. It is an ordinary positive-DST zone in
20
+ rearguard data. Ubuntu 24.04 and macOS ship rearguard data. Debian trixie,
21
+ Alpine, and FreeBSD ship vanguard data.
22
+ - **Backward-compatibility links.** Debian and Ubuntu move the links to a
23
+ separate `tzdata-legacy` package. That package is not installed by
24
+ default. `US/Eastern` and approximately 100 other names then stop
25
+ resolving. The identifier count drops from approximately 600 to
26
+ approximately 500.
27
+ - **Vintage.** A pinned `tzinfo-data` gem or an unpatched host can predate a
28
+ rule change. Example: `America/Nuuk` got new rules in tzdata 2023a. A
29
+ 2021–2022 vintage resolves the zone and answers with the old rules. Before
30
+ 2020a the name `America/Nuuk` does not exist at all.
31
+
32
+ A host can also have no tz database at all. Scratch and distroless
33
+ containers are examples. There `reference_zone:` raises
34
+ `Duckling::TZDataUnavailable`.
35
+
36
+ A suite run cannot observe which database it ran against. It passes
37
+ identically on both. Four mechanisms keep the coverage honest: environments,
38
+ probes, capability-gated tests, and environment contracts.
39
+
40
+ ## Mechanism 1: Environments
41
+
42
+ Two environment variables select the database under test:
43
+
44
+ - `DUCKLING_TZINFO_DATA` (read in the Gemfile) controls the `tzinfo-data`
45
+ gem in the bundle:
46
+ - unset: the current release of the gem. This is the default environment.
47
+ - `none`: no gem. tzinfo falls back to the zoneinfo files of the host.
48
+ - an exact version, for example `1.2022.7`: a stale database.
49
+ - `DUCKLING_ZONEINFO_DIR` (read in `test/test_helper.rb`) points tzinfo at a
50
+ specific compiled zoneinfo directory. It is set before `duckling` is
51
+ required. Nothing then resolves a zone against the default source first.
52
+
53
+ Always set `BUNDLE_LOCKFILE` for any environment except the default.
54
+ Without it, `bundle install` overwrites the committed `Gemfile.lock`. The
55
+ per-environment lockfiles are gitignored (`/Gemfile.*.lock`). A dirty tree
56
+ also blocks `rake release` and `rake benchmark:record_pr`. Both are guarded
57
+ by `release:guard_clean`. The error looks unrelated.
58
+
59
+ An unrecognized `DUCKLING_TZINFO_DATA` value raises in the Gemfile. A typo
60
+ must fail there. It must not fail deep in the resolver as an unsatisfiable
61
+ constraint.
62
+
63
+ Seven environments run in CI:
64
+
65
+ | Environment | CI job | Database under test |
66
+ |---|---|---|
67
+ | default | `baseline` | current `tzinfo-data` gem |
68
+ | system-zoneinfo | `baseline` step | zoneinfo files of the runner |
69
+ | linkless-zoneinfo | `baseline` step | built by `bin/build-linkless-zoneinfo` |
70
+ | tzinfo-data 1.2022.7 | `timezones` matrix | pinned gem |
71
+ | stale system zoneinfo | `timezones` matrix | built by `bin/build-stale-zoneinfo` |
72
+ | Debian + tzdata-legacy | `tz-containers` matrix | system zoneinfo with the links |
73
+ | Alpine | `tz-containers` matrix | vanguard zoneinfo, musl source build |
74
+
75
+ Gating:
76
+
77
+ - Only `baseline` blocks a merge. It is the only required check. So the
78
+ three environments a consumer is actually on run there as steps.
79
+ - `timezones` and `tz-containers` do not block a merge. They do block a
80
+ release. `release.yml` waits on the full workflow (`needs: ci`). A red
81
+ result there means "this vintage answers differently". It does not mean
82
+ "the gem is broken for anyone today".
83
+
84
+ The linkless environment must be built. No runner is in that state.
85
+ `ubuntu-latest` resolves `US/Eastern` from its own tzdata. macOS does too.
86
+ Note: the runner is not a stock Ubuntu for tz data. A plain `ubuntu:24.04`
87
+ container of an earlier tzdata point release does not resolve the links.
88
+ The runner does. So no runner fact settles the links. The suite probes them.
89
+
90
+ One configuration deliberately has no environment: a host with no tz
91
+ database at all. A runner without tz data would break more than this gem.
92
+ Every probe answers `false` there. So the suite loads and runs. The
93
+ `DataSourceNotFound` arms assert this. `test/gem/installed_gem_test.rb`
94
+ skips its `reference_zone:` case there for the same reason.
95
+
96
+ ## Mechanism 2: Behavioral probes
97
+
98
+ Neither datasource exposes a version. `RubyDataSource` keeps `version_info`
99
+ private. `ZoneinfoDataSource` exposes only `zoneinfo_dir`. So a probe asks
100
+ the database a question. It does not read a release string.
101
+
102
+ Probes are split by consumer:
103
+
104
+ - `Duckling::TZInfoCapabilities` (`lib/duckling/tzinfo_capabilities.rb`)
105
+ ships in the gem. It holds only what the unknown-identifier error message
106
+ needs.
107
+ - `TZCapabilities` (`test/support/tz_capabilities.rb`) holds the probes that
108
+ only the suite calls. `lib/` reaches every consumer. `test/` reaches
109
+ none. A probe with no production caller does not belong in `lib/`.
110
+
111
+ `backward_compat_links?` is needed on both sides. Production owns it. The
112
+ test module delegates to it.
113
+
114
+ Probe rules:
115
+
116
+ - A probe is a total boolean. It answers `false` on a host with no database.
117
+ It must not raise. `TZInfo::DataSourceNotFound` is a sibling of
118
+ `InvalidTimezoneIdentifier`. It is not a subclass. Rescue it explicitly.
119
+ - Nothing is memoized. `TZInfo::DataSource.set` can swap the database
120
+ mid-process. The fixture-zone tests do this. A cached answer would
121
+ describe a database that is no longer in use.
122
+ - `TZCapabilities.supports?` raises `ArgumentError` on an unknown capability
123
+ name. A typo must be a hard error. It must not be a silent `false`.
124
+
125
+ The three probes:
126
+
127
+ - `models_negative_dst?` asks about `Europe/Dublin`. IANA models it as
128
+ +01:00 standard all year, with a negative one-hour saving in winter. So
129
+ tzinfo reports January as the `dst?` period. Rearguard data re-expresses
130
+ the same offsets as ordinary positive DST.
131
+ - `backward_compat_links?` asks for `US/Eastern`. It is a link to
132
+ `America/New_York` in IANA's `backward` file.
133
+ - `greenland_2023_rules?` asks whether `America/Nuuk` skips
134
+ 2026-03-28 23:30. Older vintages fail in two ways. Before 2020a the name
135
+ does not exist. The 2021–2022 vintages know the name but answer with the
136
+ old rules. The zone resolves and gives a different answer. That is the
137
+ harder failure to attribute.
138
+
139
+ ## Mechanism 3: Capability-gated tests
140
+
141
+ A test whose premise is a capability lives in
142
+ `test/capabilities/<capability>_test.rb`. The loader at the bottom of
143
+ `test/test_helper.rb` loads the file only where the probe passes. On a
144
+ database that cannot answer, the test is not in the run. It does not fail
145
+ for want of the capability. It does not pass vacuously.
146
+
147
+ The filename is the declaration. The loader calls `supports?` with the
148
+ filename. An unknown name raises at load time.
149
+
150
+ Rules:
151
+
152
+ - A test whose weak mode is a vacuous pass must assert its own premise.
153
+ Example: `negative_dst_test.rb` asserts `models_negative_dst?` in the
154
+ test body. The probe gates the load. The assertion catches the day the
155
+ probe or the IANA data drifts. The test uses the same predicate as the
156
+ loader. Two definitions could drift apart.
157
+ - A file run directly (`ruby -Itest test/capabilities/negative_dst_test.rb`)
158
+ runs regardless of the probe. This is how you exercise one test against a
159
+ database that lacks the capability.
160
+ - Do not use `expect_failure` for environment-dependent tests. It cannot
161
+ tell an absent capability from a genuine regression. It would convert
162
+ either into the same skip.
163
+
164
+ ## Mechanism 4: Environment contracts
165
+
166
+ Each synthesized or pinned environment has a contract in
167
+ `test/environments/<name>_test.rb`. The CI step that creates the
168
+ environment invokes the contract directly:
169
+
170
+ ```bash
171
+ bundle exec ruby -Ilib -Itest test/environments/<name>_test.rb
172
+ ```
173
+
174
+ The suite never loads the contracts. A contract asserts its state
175
+ positively:
176
+
177
+ - `tzinfo_data_test.rb`: the datasource is the gem, and all three probes
178
+ answer true. The default environment is the one place all three
179
+ capabilities are guaranteed. So this contract is the tripwire for the
180
+ loader. A probe that rotted to false would unload its capability file
181
+ silently everywhere else. Here it turns red.
182
+ - `linkless_zoneinfo_test.rb`: `US/Eastern` raises, and the message names
183
+ both remedies. It also asserts the identifier count stays in the range of
184
+ a real links-less host. The build script's own check catches only
185
+ under-stripping. Over-stripping is the likelier drift. A future tzdata or
186
+ a different base image can add a top-level entry a real host keeps.
187
+ - `stale_vintage_test.rb`: `America/Nuuk` answers with the pre-2023a rules.
188
+ The wrong answer is asserted on purpose. A stale database's dangerous
189
+ failure is a wrong answer. A missing zone is easier to attribute. If the
190
+ pin or the rollback
191
+ stops taking effect, the capability-gated test simply starts loading and
192
+ passing. The suite goes green against a different database than the
193
+ environment exists for. This contract turns red instead.
194
+ - `system_zoneinfo_links_test.rb` and `alpine_vanguard_test.rb`: the
195
+ defining probe answers true. An environment that lost its defining
196
+ capability would silently run less. The contract is the loud half.
197
+
198
+ Without a contract, a broken setup presents as a smaller green suite. The
199
+ capability-gated files simply load less.
200
+
201
+ ## The error messages
202
+
203
+ ### Unknown identifier
204
+
205
+ `timezone_for` raises `ArgumentError` for an unknown identifier. The
206
+ message includes `unknown_identifier_diagnosis`. It names the database that
207
+ answered and how many identifiers it has. The count separates the two
208
+ databases legibly: approximately 600 against approximately 500.
209
+
210
+ Rules for the remedy clause:
211
+
212
+ - It is a condition the reader evaluates ("if that is what this is"). It is
213
+ not a claim about the identifier. Only the database is checked here.
214
+ Whether the name is one of the approximately 100 in IANA's `backward`
215
+ file is not knowable without shipping that list. A claim would tell every
216
+ typo on a links-less host that `tzdata-legacy` supplies it. A shipped
217
+ list would be worse: a name it missed would get no remedy at all.
218
+ - The identifier the caller passed must not appear in the remedy clause.
219
+ Naming it there turns the condition back into a claim.
220
+ - The clause appears only where the database has no links.
221
+ - The remedies assume the datasource is the host's default. A caller who
222
+ pointed `TZInfo::DataSource` at their own directory must fix that
223
+ directory instead. The message names the directory. That makes the case
224
+ recognizable.
225
+
226
+ Rules for `datasource_description`:
227
+
228
+ - The zoneinfo case is detected by capability (`respond_to?(:zoneinfo_dir)`).
229
+ A caller can install a custom subclass. The directory is the useful part
230
+ of the answer.
231
+ - The gem case is detected by class. Whether `tzinfo-data` is loaded says
232
+ nothing about whether it answered. A custom datasource in a bundle that
233
+ also carries the gem must not be described as tzinfo-data.
234
+ - An unrecognized datasource is named by its own class. Do not describe it
235
+ as another database.
236
+ - A host with no database gets its own string. This method builds failure
237
+ messages. It must not raise. Raising would replace the explanation with a
238
+ raw tzinfo error at the moment the explanation was wanted.
239
+
240
+ ### No database at all
241
+
242
+ `timezone_for` raises `Duckling::TZDataUnavailable` when tzinfo raises
243
+ `DataSourceNotFound`. tzinfo raises it before any identifier lookup. The
244
+ zone name is beside the point. The message names the `reference_zone:`
245
+ keyword and both fixes: the `tzinfo-data` gem, or the system `tzdata`
246
+ package.
247
+
248
+ `TZDataUnavailable` is deliberately not an `ArgumentError`. It reports the
249
+ state of the deployment. A caller that validates user
250
+ input by rescuing `ArgumentError` must not swallow it. It is a named class
251
+ for the same reason as `ShapeError`: greppable, and not satisfiable by an
252
+ unrelated `RuntimeError`.
253
+
254
+ ## Fixture zones
255
+
256
+ `test/fixtures/tz/*.zi` files are compiled by `zic` into a private zoneinfo
257
+ directory at test time. `TZFixtures::Datasource` swaps `TZInfo::DataSource`
258
+ in `setup` and restores it in `teardown`.
259
+
260
+ Why fixture zones:
261
+
262
+ - A fixture zone is identical on every host and every vintage. Real zones
263
+ are not. Which real zones exist depends on the datasource. What they do
264
+ depends on the vintage.
265
+ - The fixture directory exposes only its own three identifiers. Nothing
266
+ about the host's database can leak into a test.
267
+ - The swap is process-global because `timezone_for` reaches the datasource
268
+ through `TZInfo::Timezone.get` inside `Duckling.parse`. No injection
269
+ point exists. This is also why Ruby doubles cannot replace the fixture
270
+ zones. A double can only reach `local_time_in_zone` directly. That stops
271
+ short of the outside-in path through `Duckling.parse`.
272
+ - The restore in `teardown` is mandatory. A leaked fixture
273
+ datasource leaves every later test with three zones and nothing else.
274
+ - `TZInfo::DataSource.get` creates the default source when none is set. It
275
+ raises when it cannot. So setup tolerates the absence. Teardown restores
276
+ conditionally: `set(nil)` raises `ArgumentError`. On a failed setup that
277
+ would replace the real error with a worse one.
278
+
279
+ The tests reach the fixtures through `Duckling.parse`. The coverage stays
280
+ outside-in. One exception: the half-hour gap test calls
281
+ `local_time_in_zone` directly. No English expression lands reliably inside
282
+ a 30-minute window.
283
+
284
+ The three fixture zones:
285
+
286
+ - `Fixture/NegativeDst`: shaped like `Europe/Dublin`. Negative DST. It
287
+ distinguishes first-occurrence-by-position from a `dst?`-flag lookup.
288
+ Picking by flag gives the second occurrence, an hour off as an instant.
289
+ `ActiveSupport::TimeZone#local` picks by flag (`period_for_local`'s
290
+ `dst=true` default).
291
+ - `Fixture/HalfHourGap`: shaped like `Australia/Lord_Howe`. A 30-minute
292
+ gap. It distinguishes the transition's real width from a hardcoded
293
+ one-hour shift. ActiveSupport's `@time += 1.hour` retry overshoots.
294
+ Lord Howe is the only zone in current use with a sub-hour gap. The
295
+ coverage rested on one zone's continued existence.
296
+ - `Fixture/LateGap`: shaped like `America/Nuuk`. A gap late in the local
297
+ day in a negative-offset zone. The transition instant falls past the next
298
+ UTC midnight. `gap_delta`'s scan window must center on the skipped wall
299
+ clock read as UTC. A midnight-anchored window misses the transition.
300
+ `gap_delta` then crashes with `NoMethodError` on a nil `find`.
301
+
302
+ `zic` needs no provisioning except on Alpine:
303
+
304
+ - Debian/Ubuntu: `zic` is in `libc-bin` (Priority: required, a dependency
305
+ of libc6). It is not in `tzdata`. A slim image without
306
+ `/usr/share/zoneinfo` still has it.
307
+ - macOS: `/usr/sbin/zic` is a stock utility.
308
+ - Alpine: `zic` is in `tzdata-utils`. The `tz-containers` job installs it.
309
+ - `zic` lives in `sbin`. That is off a non-root `PATH`. `TZFixtures` and
310
+ `bin/build-stale-zoneinfo` search there explicitly.
311
+ - A missing `zic` is a hard error. These fixtures exist because this
312
+ coverage kept degrading silently on hosts nobody watched. A skip would
313
+ reintroduce exactly that.
314
+
315
+ ## The build scripts
316
+
317
+ ### `bin/build-linkless-zoneinfo <output-dir>`
318
+
319
+ Copies the host's zoneinfo directory and removes the top-level
320
+ backward-compatibility entries. `ZONEINFO_DIR` overrides the source
321
+ directory (default `/usr/share/zoneinfo`).
322
+
323
+ - The keep-list is transcribed from a real links-less host: a Debian-family
324
+ container with `tzdata` and no `tzdata-legacy`, 497 identifiers. No name
325
+ is added defensively. `Factory` and `posixrules` stay because Ubuntu
326
+ 24.04's tzdata 2025b still has them.
327
+ - The script hard-fails if `US/Eastern` survives the strip. That check
328
+ catches under-stripping only. The environment contract's identifier-count
329
+ range catches over-stripping.
330
+ - `cp -RL` dereferences the alias symlinks. Removing an entry cannot leave
331
+ a dangling link. It cannot follow one back into the host's directory.
332
+ - The approximately 60 in-region aliases that `tzdata-legacy` also owns are
333
+ not removed (`America/Godthab`, `Europe/Kiev`, `Asia/Calcutta`). Inside a
334
+ region directory they are indistinguishable from aliases a stock host
335
+ keeps (`Asia/Istanbul`, `Pacific/Samoa`). So the tree exposes more
336
+ identifiers than a stock host. That costs nothing. The guarantee the
337
+ environment needs is that `US/Eastern` is genuinely gone. It is the probe
338
+ target and the name the CHANGELOG and the error message use.
339
+ - The copy keeps the host's modelling. Only the links absence gets an
340
+ environment contract. Modelling follows the host. The capability-gated
341
+ Dublin test loads or does not load.
342
+
343
+ ### `bin/build-stale-zoneinfo <output-dir>`
344
+
345
+ Copies the host's zoneinfo directory and compiles
346
+ `test/fixtures/zoneinfo-overrides/*.zi` over it. The overrides roll named
347
+ zones back to earlier rules. `America/Nuuk` goes back to the pre-2023a
348
+ rules.
349
+
350
+ - The copy keeps the host's modelling and links state. The capability-gated
351
+ tests absorb both.
352
+ - Rolling back only the zones under assertion says plainly which staleness
353
+ is tested. Compiling a full old tzdata release would need a download.
354
+ - The override replaces the zone's entire history, including the pre-2023a
355
+ rules. Harmless for the contract: it only looks at 2026. Every other zone
356
+ in the copied directory stays as the host has it.
357
+ - This script needs `tzdata` installed. It copies `/usr/share/zoneinfo`. It
358
+ fails with a clear message if the directory is missing.
359
+ - The override shadows a real identifier on purpose. The
360
+ `test/fixtures/tz/*.zi` zones are `Fixture/`-prefixed so they cannot be
361
+ mistaken for real ones. Here shadowing is the point. The environment must
362
+ be a plausible stale host. A synthetic zone would give nothing to assert
363
+ against.
364
+
365
+ ## `expect_failure`
366
+
367
+ `expect_failure(reason)` in `test/test_helper.rb` is for known limitations
368
+ that fail on every host. The upstream grammar and ranking gaps are the
369
+ current cases. It runs the block for real:
370
+
371
+ - A failure reports as a skip that names the reason.
372
+ - A pass flunks. The limitation stopped reproducing. Drop the wrapper and
373
+ keep the assertions.
374
+ - Only `Minitest::Assertion` is rescued. It inherits from `Exception`, not
375
+ from `StandardError`. A wider rescue would launder any crash before the
376
+ assertions into "known limitation". A genuine regression must surface as
377
+ a crash.
378
+ - A `skip` inside the block is re-raised first. `Minitest::Skip` is a
379
+ subclass of `Minitest::Assertion`. Otherwise the real explanation would
380
+ be replaced by the reason.
381
+
382
+ Do not use it for anything environment-dependent. Use
383
+ `test/capabilities/` instead.
384
+
385
+ ## Running an environment locally
386
+
387
+ Always pass `BUNDLE_LOCKFILE` to both the `bundle install` and the run.
388
+ The suite adapts itself to whatever database it gets. There is no
389
+ environment name to set. Run the matching contract afterward to prove you
390
+ got the state you meant to.
391
+
392
+ ```bash
393
+ # no tzinfo-data: the host's zoneinfo files
394
+ export DUCKLING_TZINFO_DATA=none BUNDLE_LOCKFILE=Gemfile.system-zoneinfo.lock
395
+ bundle install && bundle exec rake test
396
+
397
+ # the same, with the backward-compat links stripped (US/Eastern stops resolving)
398
+ bin/build-linkless-zoneinfo /tmp/linkless-zoneinfo
399
+ DUCKLING_ZONEINFO_DIR=/tmp/linkless-zoneinfo bundle exec rake test
400
+ DUCKLING_ZONEINFO_DIR=/tmp/linkless-zoneinfo \
401
+ bundle exec ruby -Ilib -Itest test/environments/linkless_zoneinfo_test.rb
402
+
403
+ # a pinned stale vintage
404
+ export DUCKLING_TZINFO_DATA=1.2022.7 BUNDLE_LOCKFILE=Gemfile.tzinfo-data-1.2022.7.lock
405
+ bundle install && bundle exec rake test
406
+ bundle exec ruby -Ilib -Itest test/environments/stale_vintage_test.rb
407
+
408
+ # both axes at once
409
+ bin/build-stale-zoneinfo /tmp/stale-zoneinfo
410
+ export DUCKLING_TZINFO_DATA=none BUNDLE_LOCKFILE=Gemfile.stale-system-zoneinfo.lock
411
+ bundle install && DUCKLING_ZONEINFO_DIR=/tmp/stale-zoneinfo bundle exec rake test
412
+ DUCKLING_ZONEINFO_DIR=/tmp/stale-zoneinfo \
413
+ bundle exec ruby -Ilib -Itest test/environments/stale_vintage_test.rb
414
+ ```
415
+
416
+ `DUCKLING_ZONEINFO_DIR` alone (pointing at `/usr/share/zoneinfo`, with
417
+ `tzinfo-data` still bundled) reaches the same datasource as the system leg
418
+ without re-resolving anything. It is the quick way to reproduce a
419
+ system-zoneinfo failure. It is not the same configuration. The gem is still
420
+ installed. So it does not exercise tzinfo's own fallback.
421
+
422
+ ## CI notes
423
+
424
+ - `bundler-cache` is off in `timezones` and `tz-containers`. These
425
+ environments resolve a different bundle than the committed lockfile. The
426
+ cache is keyed on that lockfile.
427
+ - The system-zoneinfo step in `baseline` runs
428
+ `bundle config unset --local deployment` (and `frozen`).
429
+ `bundler-cache: true` writes `deployment: true` into `.bundle/config`.
430
+ Deployment requires a committed lockfile. The environment lockfiles are
431
+ gitignored by design. It must be unset in the local config.
432
+ `BUNDLE_DEPLOYMENT` cannot override it. Bundler resolves local config first and environment
433
+ variables second (`Bundler::Settings#configs`). An env var cannot
434
+ override anything `bundle config --local` has written.
435
+ - The Alpine image is pinned by digest. The floating `ruby:3.4-alpine` tag
436
+ silently rebases across Alpine releases. Alpine drops older versioned
437
+ clang packages as it rolls. A rebase also moves the host's tz data under
438
+ a leg that asserts against it. Bump by resolving the tag's current digest
439
+ (`docker buildx imagetools inspect ruby:3.4-alpine`). Then re-verify the
440
+ clang package names and the contract against the new release.
441
+ - Alpine splits clang's resource headers (`stdckdint.h`, which ruby-3.4's
442
+ headers include) away from the library's default search path.
443
+ `BINDGEN_EXTRA_CLANG_ARGS=-I<resource-dir>` points bindgen at them. Any
444
+ musl consumer building the source gem needs the same. No precompiled gem
445
+ targets musl (`cross_targets.rb`). So the source build is the path musl
446
+ consumers actually take.
447
+ - The banner line at suite start (`tz datasource: ...; negative_dst=true
448
+ ...`) records which database answered and which probes passed. Two images
449
+ with the same name can disagree about the links. The banner is what
450
+ reconciles a missing capability test with a CI log.
451
+ - The container jobs run the image's own Ruby. `ruby/setup-ruby` does not
452
+ apply (it has no musl support). The toolchain comes from the image's
453
+ package manager, git included. That is why the install step precedes
454
+ checkout.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tzinfo"
4
+
5
+ module Duckling
6
+ # Which tz database `reference_zone:` resolves against, for the
7
+ # unknown-identifier error message. Behavioral because neither datasource
8
+ # exposes a version. Internal. See docs/tz-database-axis.md.
9
+ module TZInfoCapabilities
10
+ module_function
11
+
12
+ # DataSourceNotFound does not inherit from InvalidTimezoneIdentifier;
13
+ # name it explicitly. A host with no database answers false.
14
+ def backward_compat_links?
15
+ TZInfo::Timezone.get("US/Eastern")
16
+ true
17
+ rescue TZInfo::InvalidTimezoneIdentifier, TZInfo::DataSourceNotFound
18
+ false
19
+ end
20
+
21
+ def identifier_count
22
+ TZInfo::Timezone.all_identifiers.size
23
+ rescue TZInfo::DataSourceNotFound
24
+ 0
25
+ end
26
+
27
+ # Zoneinfo is detected by capability (a caller may install a subclass),
28
+ # the gem by class (loaded is not answered). Must not raise: it builds
29
+ # failure messages.
30
+ def datasource_description
31
+ source = begin
32
+ TZInfo::DataSource.get
33
+ rescue TZInfo::DataSourceNotFound
34
+ return "no tz datasource (no zoneinfo files, no tzinfo-data gem)"
35
+ end
36
+
37
+ return "system zoneinfo at #{source.zoneinfo_dir}" if source.respond_to?(:zoneinfo_dir)
38
+
39
+ if defined?(TZInfo::DataSources::RubyDataSource) && source.is_a?(TZInfo::DataSources::RubyDataSource)
40
+ version = " (tzdata #{TZInfo::Data::Version::TZDATA})" if defined?(TZInfo::Data::Version::TZDATA)
41
+ return "the tzinfo-data gem#{version}"
42
+ end
43
+
44
+ "the #{source.class} tz datasource"
45
+ end
46
+
47
+ # The remedy is phrased as a condition: only the database is checked.
48
+ # See docs/tz-database-axis.md.
49
+ def unknown_identifier_diagnosis
50
+ diagnosis = "resolved against #{datasource_description}, " \
51
+ "which provides #{identifier_count} identifiers"
52
+ return diagnosis if backward_compat_links?
53
+
54
+ "#{diagnosis}; this database has no backward-compat names (US/Eastern and ~100 others), " \
55
+ "so if that is what this is, it needs either the tzinfo-data gem or the " \
56
+ "tzdata-legacy system package"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Duckling
4
+ VERSION = "0.4.7"
5
+ end