Skip to content

Commit

Permalink
Attempt to implement better error messages for Windows CI failures
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Jan 9, 2025
1 parent b289d0b commit 1e339e7
Showing 1 changed file with 116 additions and 58 deletions.
174 changes: 116 additions & 58 deletions core/windows/test/TimeZoneRulesCompleteTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.datetime.*
import kotlinx.datetime.internal.*
import platform.windows.*
import kotlin.test.*
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds

class TimeZoneRulesCompleteTest {
Expand All @@ -25,6 +26,32 @@ class TimeZoneRulesCompleteTest {
val inputSystemtime = alloc<SYSTEMTIME>()
val outputSystemtime = alloc<SYSTEMTIME>()
val dtzi = alloc<DYNAMIC_TIME_ZONE_INFORMATION>()
fun offsetAtAccordingToWindows(instant: Instant): Int {
val ldtAccordingToWindows =
instant.toLocalDateTime(dtzi, inputSystemtime.ptr, outputSystemtime.ptr)
return (ldtAccordingToWindows.toInstant(UtcOffset.ZERO) - instant).inWholeSeconds.toInt()
}
fun transitionsAccordingToWindows(year: Int): List<OffsetInfo> = buildList {
var lastInstant = LocalDate(year, Month.JANUARY, 1)
.atTime(0, 0).toInstant(UtcOffset.ZERO)
var lastOffsetAccordingToWindows = offsetAtAccordingToWindows(lastInstant)
repeat(LocalDate(year, Month.DECEMBER, 31).dayOfYear - 1) {
val instant = lastInstant + 24.hours
val offset = offsetAtAccordingToWindows(instant)
if (lastOffsetAccordingToWindows != offset) {
add(OffsetInfo(
binarySearchInstant(lastInstant, instant) {
offset == offsetAtAccordingToWindows(it)
},
UtcOffset(seconds = lastOffsetAccordingToWindows),
UtcOffset(seconds = offset)
))
lastOffsetAccordingToWindows = offset
}
lastInstant = instant
}
}
val issues = mutableListOf<IncompatibilityWithWindowsRegistry>()
var i: DWORD = 0u
while (true) {
when (val dwResult: Int = EnumDynamicTimeZoneInformation(i++, dtzi.ptr).toInt()) {
Expand All @@ -40,79 +67,83 @@ class TimeZoneRulesCompleteTest {
continue
}
val rules = tzdb.rulesForId(id)
fun checkAtInstant(instant: Instant) {
fun MutableList<Mismatch>.checkAtInstant(instant: Instant) {
val ldt = instant.toLocalDateTime(dtzi, inputSystemtime.ptr, outputSystemtime.ptr)
val offset = rules.infoAtInstant(instant)
val ourLdt = instant.toLocalDateTime(offset)
if (ldt != ourLdt) {
val offsetsAccordingToWindows = buildList {
var date = LocalDate(ldt.year, Month.JANUARY, 1)
while (date.year == ldt.year) {
val instant = date.atTime(0, 0).toInstant(UtcOffset.ZERO)
val ldtAccordingToWindows =
instant.toLocalDateTime(dtzi, inputSystemtime.ptr, outputSystemtime.ptr)
val offsetAccordingToWindows =
(ldtAccordingToWindows.toInstant(UtcOffset.ZERO) - instant).inWholeSeconds
add(date to offsetAccordingToWindows)
date = date.plus(1, DateTimeUnit.DAY)
}
}
val rawData = memScoped {
val hKey = alloc<HKEYVar>()
RegOpenKeyExW(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\$windowsName", 0u, KEY_READ.toUInt(), hKey.ptr)
try {
val cbDataBuffer = alloc<DWORDVar>()
val SIZE_BYTES = 44
val zoneInfoBuffer = allocArray<BYTEVar>(SIZE_BYTES)
cbDataBuffer.value = SIZE_BYTES.convert()
RegQueryValueExW(hKey.value, "TZI", null, null, zoneInfoBuffer, cbDataBuffer.ptr)
zoneInfoBuffer.readBytes(SIZE_BYTES).toHexString()
} finally {
RegCloseKey(hKey.value)
}
}
throw AssertionError(
"Expected $ldt, got $ourLdt in zone $windowsName at $instant (our guess at the offset is $offset)." +
"The rules are $rules, and the offsets throughout the year according to Windows are: $offsetsAccordingToWindows; the raw data for the recurring rules is $rawData"
)
}
if (ldt != ourLdt) add(Mismatch(ourLdt, ldt, instant))
}
fun checkTransition(instant: Instant) {
fun MutableList<Mismatch>.checkTransition(instant: Instant) {
checkAtInstant(instant - 2.milliseconds)
checkAtInstant(instant)
}
// check historical data
for (transition in rules.transitionEpochSeconds) {
checkTransition(Instant.fromEpochSeconds(transition))
}
// check recurring rules
if (windowsName !in strangeTimeZones) {
// we skip checking these time zones because Windows does something arbitrary with them
// after 2030. For example, Morocco DST transitions are linked to the month of Ramadan,
// and after 2030, Windows doesn't seem to calculate Ramadan properly, but also, it doesn't
// follow the rules stored in the registry. Odd, but it doesn't seem worth it trying to
// reverse engineer results that aren't even correct.
val lastTransition = Instant.fromEpochSeconds(
rules.transitionEpochSeconds.lastOrNull() ?: 1715000000 // arbitrary time
)
val lastTransitionYear = lastTransition.toLocalDateTime(TimeZone.UTC).year
for (year in lastTransitionYear + 1..lastTransitionYear + 15) {
val rulesForYear = rules.recurringZoneRules!!.rulesForYear(year)
if (rulesForYear.isEmpty()) {
checkAtInstant(
LocalDate(year, 6, 1).atStartOfDayIn(TimeZone.UTC)
)
} else {
for (rule in rulesForYear) {
checkTransition(rule.transitionDateTime)
val mismatches = buildList {
// check historical data
for (transition in rules.transitionEpochSeconds) {
checkTransition(Instant.fromEpochSeconds(transition))
}
// check recurring rules
if (windowsName !in strangeTimeZones) {
// we skip checking these time zones because Windows does something arbitrary with them
// after 2030. For example, Morocco DST transitions are linked to the month of Ramadan,
// and after 2030, Windows doesn't seem to calculate Ramadan properly, but also, it doesn't
// follow the rules stored in the registry. Odd, but it doesn't seem worth it trying to
// reverse engineer results that aren't even correct.
val lastTransitionYear = Instant.fromEpochSeconds(
rules.transitionEpochSeconds.lastOrNull() ?: 1715000000 // arbitrary time
).toLocalDateTime(TimeZone.UTC).year
val firstTransitionYear = Instant.fromEpochSeconds(
rules.transitionEpochSeconds.firstOrNull() ?: 0 // arbitrary time
).toLocalDateTime(TimeZone.UTC).year
val yearsToCheck = ((firstTransitionYear - 15)..<firstTransitionYear) +
((lastTransitionYear + 1)..(lastTransitionYear + 15))
for (year in yearsToCheck) {
val rulesForYear = rules.recurringZoneRules!!.rulesForYear(year)
if (rulesForYear.isEmpty()) {
checkAtInstant(
LocalDate(year, 6, 1).atStartOfDayIn(TimeZone.UTC)
)
} else {
for (rule in rulesForYear) {
checkTransition(rule.transitionDateTime)
}
}
}
}
}
if (mismatches.isNotEmpty()) {
val mismatchYears =
mismatches.map { it.instant.toLocalDateTime(TimeZone.UTC).year }.distinct()
val rawData = memScoped {
val hKey = alloc<HKEYVar>()
RegOpenKeyExW(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\$windowsName", 0u, KEY_READ.toUInt(), hKey.ptr)
try {
val cbDataBuffer = alloc<DWORDVar>()
val SIZE_BYTES = 44
val zoneInfoBuffer = allocArray<BYTEVar>(SIZE_BYTES)
cbDataBuffer.value = SIZE_BYTES.convert()
RegQueryValueExW(hKey.value, "TZI", null, null, zoneInfoBuffer, cbDataBuffer.ptr)
zoneInfoBuffer.readBytes(SIZE_BYTES).toHexString()
} finally {
RegCloseKey(hKey.value)
}
}
issues.add(
IncompatibilityWithWindowsRegistry(
timeZoneName = windowsName,
dataOnAffectedYears = mismatchYears.flatMap {
transitionsAccordingToWindows(it)
},
registryData = rawData,
mismatches = mismatches,
)
)
}
}
else -> error("Unexpected error code $dwResult")
}
}
if (issues.isNotEmpty()) throw AssertionError(issues.toString())
}
}
}
Expand Down Expand Up @@ -156,3 +187,30 @@ private val strangeTimeZones = listOf(
"Morocco Standard Time", "West Bank Standard Time", "Iran Standard Time", "Syria Standard Time",
"Paraguay Standard Time"
)

private fun binarySearchInstant(instant1: Instant, instant2: Instant, predicate: (Instant) -> Boolean): Instant {
var low = instant1
var high = instant2
while (low < high) {
val mid = low + (high - low) / 2
if (predicate(mid)) {
high = mid
} else {
low = mid + 1.milliseconds
}
}
return low
}

private data class IncompatibilityWithWindowsRegistry(
val timeZoneName: String,
val dataOnAffectedYears: List<OffsetInfo>,
val registryData: String,
val mismatches: List<Mismatch>,
)

private data class Mismatch(
val ourGuess: LocalDateTime,
val windowsGuess: LocalDateTime,
val instant: Instant,
)

0 comments on commit 1e339e7

Please sign in to comment.