Skip to content

Commit

Permalink
Ensure that bionic handles time zones in the same way as java.time does.
Browse files Browse the repository at this point in the history
Bug: 196028120

Test: atest luni/src/test/java/libcore/java/time/BionicTzdbConsistencyTest.java
Change-Id: I6c4beacfec0f9d77dd2ef8ee16f8d3a457f0f785
  • Loading branch information
Yqwed committed Sep 13, 2021
1 parent cc33251 commit 0a4a1ed
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions NativeCode.bp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ cc_library_shared {
"luni/src/test/native/libcore_java_io_FileTest.cpp",
"luni/src/test/native/libcore_java_lang_ThreadTest.cpp",
"luni/src/test/native/libcore_java_nio_BufferTest.cpp",
"luni/src/test/native/libcore_java_time_BionicTzdbConsistencyTest.cpp",
"luni/src/test/native/libcore_libcore_util_NativeAllocationRegistryTest.cpp",
],
shared_libs: [
Expand Down
146 changes: 146 additions & 0 deletions luni/src/test/java/libcore/java/time/BionicTzdbConsistencyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package libcore.java.time;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import dalvik.system.VMRuntime;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;

/**
* Tests that bionic's interpretation of the TZDB rules on a device matches Android's java.time
* behavior. On Android, java.time is implemented using ICU4J, so these confirm that bionic's
* understanding of TZDB transitions and UTC offsets matches those of ICU4J.
*/
@RunWith(Parameterized.class)
public class BionicTzdbConsistencyTest {

// The lower bound for testing.
private static final LocalDateTime START_DATE;

// The upper bound for testing.
private static final LocalDateTime END_DATE;

static {
// time_t is platform dependent on Linux at the moment of writing. So we can't test dates
// outside of the range allowed by 32-bit integer.
if (VMRuntime.getRuntime().is64Bit()) {
// Chosen because TZDB has no rules defined prior to this date.
START_DATE = LocalDateTime.of(1800, 1, 1, 0, 0);
// Chosen because it's considered to be sufficiently in the future that Android devices
// won't be running. Any TZDB entries affecting this date are recurring so going beyond
// shouldn't do anything unexpected.
END_DATE = LocalDateTime.of(2100, 1, 1, 0, 0);
} else {
// Date close to minimal value allowed by 32-bit integer.
START_DATE = LocalDateTime.of(1902, 1, 1, 0, 0);
// Date close to maximal value allowed by 32-bit integer.
END_DATE = LocalDateTime.of(2038, 1, 1, 0, 0);
}
}

// Android's DateTimeFormatter implementation formats time zone as offset only for historic
// dates on certain time zones, while bionic takes abbreviation from TZif file.
// So time zone is not included here because we know it can differ. Equality of numeric local
// date/time components implies that used offsets were the same in libcore and bionic.
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("dd MM yyyy HH:mm:ss").withLocale(Locale.US);

private static final Set<Duration> INTERESTING_OFFSETS =
Set.of(Duration.ZERO, Duration.ofMinutes(30), Duration.ofMinutes(-30),
Duration.ofHours(1), Duration.ofHours(-1),
Duration.ofHours(2), Duration.ofHours(-2),
Duration.ofDays(1), Duration.ofDays(-1));

@Parameters(name = "{0}")
public static String[] getZoneIds() {
// We use java.util.TimeZone.getAvailableIDs() since that uses the IDs from TZDB, not the
// expanded set recognized by ICU.
String[] zones = TimeZone.getAvailableIDs();
assertNotEquals("no zones returned", 0, zones.length);
return zones;
}

static {
System.loadLibrary("javacoretests");
}

private final String timeZoneId;

public BionicTzdbConsistencyTest(String timeZoneId) {
this.timeZoneId = timeZoneId;
}

/**
* Compares bionic's instant formatting output (localtime()) with java.time's. If it fails,
* bionic's data and ICU's data have not been updated at the same time, or ICU and bionic's
* understanding of the same data differs.
*/
@Test
public void compareBionicFormattingWithJavaTime() {
ZoneRules zoneRules = ZoneId.of(timeZoneId).getRules();

Instant start = START_DATE.atOffset(ZoneOffset.UTC).toInstant();
Instant stop = END_DATE.atOffset(ZoneOffset.UTC).toInstant();

ZoneOffsetTransition zoneOffsetTransition = zoneRules.nextTransition(start);

while (start.isBefore(stop)) {
for (Duration interestingOffset : INTERESTING_OFFSETS) {
Instant instantToCheck = start.plus(interestingOffset);
String bionicResult = formatWithBionic(instantToCheck, timeZoneId);
String javaResult = instantToCheck.atZone(ZoneId.of(timeZoneId)).format(FORMATTER);

String errorMessage = "Failed to format " + start + " at " + timeZoneId
+ " with offset=" + interestingOffset;
assertEquals(errorMessage, javaResult, bionicResult);
}

if (zoneOffsetTransition == null) {
break;
}

start = zoneOffsetTransition.getInstant();
zoneOffsetTransition = zoneRules.nextTransition(start);
}
}

private static String formatWithBionic(Instant instant, String timeZoneId) {
return formatWithBionic(instant.getEpochSecond(), timeZoneId);
}

private static native String formatWithBionic(long epochSeconds, String timeZoneId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include <locale.h>
#include <time.h>
#include <stdlib.h>
#include <jni.h>

extern "C"
JNIEXPORT jstring JNICALL Java_libcore_java_time_BionicTzdbConsistencyTest_formatWithBionic(
JNIEnv* env, jclass, jlong epochSeconds, jstring timeZoneId) {

const char* oldTimeZone = getenv("TZ");
const char* oldLocale = setlocale(LC_ALL, NULL);

const char* nativeTimeZone = env->GetStringUTFChars(timeZoneId, 0);
setenv("TZ", nativeTimeZone, /* overwrite */ 1);
env->ReleaseStringUTFChars(timeZoneId, nativeTimeZone);

setlocale(LC_ALL, "en_US");

// Formatted string should fit into this buffer
char buf[32];
time_t t = epochSeconds;
strftime(buf, 32, "%d %m %Y %H:%M:%S", localtime(&t));

if (oldTimeZone) {
setenv("TZ", oldTimeZone, /* overwrite */ 1);
} else {
unsetenv("TZ");
}

setlocale(LC_ALL, oldLocale);

return env->NewStringUTF(buf);
}

0 comments on commit 0a4a1ed

Please sign in to comment.