blob: 67e229d9057b54c2fb7716a487a5480206c16819 [file] [log] [blame]
/*
* Copyright (C) 2016 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.util;
import junit.framework.TestCase;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Date;
import libcore.io.BufferIterator;
/**
* Tests for {@link ZoneInfo}
*/
public class ZoneInfoTest extends TestCase {
/**
* Checks that a {@link ZoneInfo} cannot be created without any offsets.
*/
public void testMakeTimeZone_NoOffsets() throws Exception {
int[][] times = {};
int[][] offsets = {};
try {
createZoneInfo(times, offsets);
fail("Did not detect no transitions");
} catch (IllegalStateException expected) {
// Expected this to happen
}
}
/**
* Checks that a {@link ZoneInfo} can be created with one offset and no transitions.
*/
public void testMakeTimeZone_OneOffset_NoTransitions() throws Exception {
int[][] times = {};
int[][] offsets = {
{ 4800, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets);
// If there are no transitions then the offset should be constant irrespective of the time.
assertEquals(secondsInMillis(4800), zoneInfo.getOffset(Long.MIN_VALUE));
assertEquals(secondsInMillis(4800), zoneInfo.getOffset(0));
assertEquals(secondsInMillis(4800), zoneInfo.getOffset(Long.MAX_VALUE));
// No transitions means no DST.
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
// The raw offset should be the first offset.
assertEquals(secondsInMillis(4800), zoneInfo.getRawOffset());
}
/**
* Checks that a {@link ZoneInfo} can be created with one non-DST transition.
*/
public void testMakeTimeZone_OneNonDstTransition() throws Exception {
int[][] times = {
{ 0, 0 }
};
int[][] offsets = {
{ 3600, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets);
// Any time before the first transition is assumed to use the first standard transition.
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(-2)));
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(0));
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(2)));
// No transitions means no DST.
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
// The raw offset should be the first offset.
assertEquals(secondsInMillis(3600), zoneInfo.getRawOffset());
}
/**
* Checks that a {@link ZoneInfo} cannot be created with one DST but no non-DSTs transitions.
*/
public void testMakeTimeZone_OneDstTransition() throws Exception {
int[][] times = {
{ 0, 0 }
};
int[][] offsets = {
{ 3600, 1 }
};
try {
createZoneInfo(times, offsets);
fail("Did not detect no non-DST transitions");
} catch (IllegalStateException expected) {
// Expected this to happen
}
}
/**
* Checks to make sure that rounding the time from milliseconds to seconds does not cause issues
* around the boundary of negative transitions.
*/
public void testMakeTimeZone_NegativeTransition() throws Exception {
int[][] times = {
{ -2000, 0 },
{ -5, 1 },
{ 0, 2 },
};
int[][] offsets = {
{ 1800, 0 },
{ 3600, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets);
// Even a millisecond before a transition means that the transition is not active.
assertEquals(1800000, zoneInfo.getOffset(secondsInMillis(-5) - 1));
assertFalse(zoneInfo.inDaylightTime(new Date(secondsInMillis(-5) - 1)));
// A time equal to the transition point activates the transition.
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(-5)));
assertTrue(zoneInfo.inDaylightTime(new Date(secondsInMillis(-5))));
// A time after the transition point but before the next activates the transition.
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(-5) + 1));
assertTrue(zoneInfo.inDaylightTime(new Date(secondsInMillis(-5) + 1)));
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
assertEquals(secondsInMillis(5400), zoneInfo.getRawOffset());
}
/**
* Checks to make sure that rounding the time from milliseconds to seconds does not cause issues
* around the boundary of positive transitions.
*/
public void testMakeTimeZone_PositiveTransition() throws Exception {
int[][] times = {
{ 0, 0 },
{ 5, 1 },
{ 2000, 2 },
};
int[][] offsets = {
{ 1800, 0 },
{ 3600, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets);
// Even a millisecond before a transition means that the transition is not active.
assertEquals(secondsInMillis(1800), zoneInfo.getOffset(secondsInMillis(5) - 1));
assertFalse(zoneInfo.inDaylightTime(new Date(secondsInMillis(5) - 1)));
// A time equal to the transition point activates the transition.
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(5)));
assertTrue(zoneInfo.inDaylightTime(new Date(secondsInMillis(5))));
// A time after the transition point but before the next activates the transition.
assertEquals(secondsInMillis(3600), zoneInfo.getOffset(secondsInMillis(5) + 1));
assertTrue(zoneInfo.inDaylightTime(new Date(secondsInMillis(5) + 1)));
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
assertEquals(secondsInMillis(5400), zoneInfo.getRawOffset());
}
/**
* Checks that creating a {@link ZoneInfo} with future DST transitions but no past DST
* transitions where the transition times are negative is not affected by rounding issues.
*/
public void testMakeTimeZone_HasFutureDST_NoPastDST_NegativeTransitions() throws Exception {
int[][] times = {
{ -2000, 0 },
{ -500, 1 },
{ -100, 2 },
};
int[][] offsets = {
{ 1800, 0 },
{ 3600, 0 },
{ 5400, 1 }
};
// The expected DST savings is the difference between the DST offset (which includes the
// raw offset) and the preceding non-DST offset (which should just be the raw offset).
// Or in other words (5400 - 3600) * 1000
int expectedDSTSavings = secondsInMillis(5400 - 3600);
ZoneInfo zoneInfo = createZoneInfo(times, offsets, secondsInMillis(-700));
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertEquals(expectedDSTSavings, zoneInfo.getDSTSavings());
// Now create one a few milliseconds before the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(times, offsets, secondsInMillis(-100) - 5);
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertEquals(expectedDSTSavings, zoneInfo.getDSTSavings());
}
/**
* Checks that creating a {@link ZoneInfo} with future DST transitions but no past DST
* transitions where the transition times are positive is not affected by rounding issues.
*/
public void testMakeTimeZone_HasFutureDST_NoPastDST_PositiveTransitions() throws Exception {
int[][] times = {
{ 4000, 0 },
{ 5500, 1 },
{ 6000, 2 },
};
int[][] offsets = {
{ 1800, 0 },
{ 3600, 0 },
{ 7200, 1 }
};
// The expected DST savings is the difference between the DST offset (which includes the
// raw offset) and the preceding non-DST offset (which should just be the raw offset).
// Or in other words (7200 - 3600) * 1000
int expectedDSTSavings = secondsInMillis(7200 - 3600);
ZoneInfo zoneInfo = createZoneInfo(times, offsets, secondsInMillis(4500));
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertEquals(expectedDSTSavings, zoneInfo.getDSTSavings());
// Now create one a few milliseconds before the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(times, offsets, secondsInMillis(6000) - 5);
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertEquals(expectedDSTSavings, zoneInfo.getDSTSavings());
}
/**
* Checks that creating a {@link ZoneInfo} with past DST transitions but no future DST
* transitions where the transition times are negative is not affected by rounding issues.
*/
public void testMakeTimeZone_HasPastDST_NoFutureDST_NegativeTransitions() throws Exception {
int[][] times = {
{ -5000, 0 },
{ -2000, 1 },
{ -500, 0 },
{ 0, 2 },
};
int[][] offsets = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets, secondsInMillis(-1));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
// Now create one a few milliseconds after the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(times, offsets, secondsInMillis(-2000) + 5);
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
}
/**
* Checks that creating a {@link ZoneInfo} with past DST transitions but no future DST
* transitions where the transition times are positive is not affected by rounding issues.
*/
public void testMakeTimeZone_HasPastDST_NoFutureDST_PositiveTransitions() throws Exception {
int[][] times = {
{ 1000, 0 },
{ 4000, 1 },
{ 5500, 0 },
{ 6000, 2 },
};
int[][] offsets = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(times, offsets, secondsInMillis(4700));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
// Now create one a few milliseconds after the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(times, offsets, secondsInMillis(4000) + 5);
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
}
/**
* Checks to make sure that it can handle up to 256 offsets.
*/
public void testMakeTimeZone_LotsOfOffsets() throws Exception {
int[][] times = {
{ -2000, 255 },
};
int[][] offsets = new int[256][];
Arrays.fill(offsets, new int[2]);
offsets[255] = new int[] { 3600, 0 };
ZoneInfo zoneInfo = createZoneInfo(times, offsets, Integer.MIN_VALUE);
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertEquals(0, zoneInfo.getDSTSavings());
// Make sure that WallTime works properly with a ZoneInfo with 256 offsets.
ZoneInfo.WallTime wallTime = new ZoneInfo.WallTime();
wallTime.localtime(0, zoneInfo);
wallTime.mktime(zoneInfo);
}
/**
* Checks to make sure that it rejects more than 256 offsets.
*/
public void testMakeTimeZone_TooManyOffsets() throws Exception {
int[][] times = {
{ -2000, 255 },
};
int[][] offsets = new int[257][];
Arrays.fill(offsets, new int[2]);
offsets[255] = new int[] { 3600, 0 };
try {
createZoneInfo(times, offsets);
fail("Did not detect too many offsets");
} catch (IllegalStateException expected) {
// Expected this to happen
}
}
/**
* Create an instance for every available time zone for which we have data to ensure that they
* can all be handled correctly.
*
* <p>This is to ensure that ZoneInfo can read all time zone data without failing, it doesn't
* check that it reads it correctly or that the data itself is correct. This is a sanity test
* to ensure that any additional checks added to the code that reads the data source and
* creates the {@link ZoneInfo} instances does not prevent any of the time zones being loaded.
*/
public void testMakeTimeZone_All() throws Exception {
ZoneInfoDB.TzData instance = ZoneInfoDB.getInstance();
String[] availableIDs = instance.getAvailableIDs();
Arrays.sort(availableIDs);
for (String id : availableIDs) {
BufferIterator bufferIterator = instance.getBufferIterator(id);
// Create a ZoneInfo at the earliest possible time to allow us to use the
// useDaylightTime() method to check whether it ever has or ever will support daylight
// savings time.
ZoneInfo zoneInfo = ZoneInfo.makeTimeZone(id, bufferIterator, Long.MIN_VALUE);
assertNotNull("TimeZone " + id + " was not created", zoneInfo);
assertEquals(id, zoneInfo.getID());
}
}
/**
* Checks that we can read the serialized form of a {@link ZoneInfo} created in pre-OpenJDK
* AOSP.
*
* <p>One minor difference is that in pre-OpenJDK {@link ZoneInfo#mDstSavings} can be non-zero
* even if {@link ZoneInfo#mUseDst} was false. That was not visible externally (except through
* the {@link ZoneInfo#toString()} method) as the {@link ZoneInfo#getDSTSavings()} would check
* {@link ZoneInfo#mUseDst} and if it was false then would return 0. This checks to make sure
* that is handled properly. See {@link ZoneInfo#readObject(ObjectInputStream)}.
*/
public void testReadSerialized() throws Exception {
ZoneInfo zoneInfoRead;
try (InputStream is = getClass().getResourceAsStream("ZoneInfoTest_ZoneInfo.golden.ser");
ObjectInputStream ois = new ObjectInputStream(is)) {
Object object = ois.readObject();
assertTrue("Not a ZoneInfo instance", object instanceof ZoneInfo);
zoneInfoRead = (ZoneInfo) object;
}
int[][] times = {
{ -5000, 0 },
{ -2000, 1 },
{ -500, 0 },
{ 0, 2 },
};
int[][] offsets = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfoCreated = createZoneInfo("test", times, offsets, secondsInMillis(-1));
assertEquals("Read ZoneInfo does not match created one", zoneInfoCreated, zoneInfoRead);
assertEquals("useDaylightTime() mismatch",
zoneInfoCreated.useDaylightTime(), zoneInfoRead.useDaylightTime());
assertEquals("getDSTSavings() mismatch",
zoneInfoCreated.getDSTSavings(), zoneInfoRead.getDSTSavings());
}
private static int secondsInMillis(int seconds) {
return seconds * 1000;
}
private ZoneInfo createZoneInfo(int[][] transitionTimes, int[][] transitionTypes)
throws Exception {
return createZoneInfo(getName(), transitionTimes, transitionTypes, System.currentTimeMillis());
}
private ZoneInfo createZoneInfo(int[][] transitionTimes, int[][] transitionTypes,
long currentTimeMillis) throws Exception {
return createZoneInfo(getName(), transitionTimes, transitionTypes, currentTimeMillis);
}
private ZoneInfo createZoneInfo(String name, int[][] transitionTimes, int[][] transitionTypes,
long currentTimeMillis) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Magic number.
writeInt(baos, 0x545a6966);
// Some useless stuff in the header.
for (int i = 0; i < 28; ++i) {
baos.write(i);
}
// Transition time count
writeInt(baos, transitionTimes.length);
// Transition type count.
writeInt(baos, transitionTypes.length);
// Useless stuff.
writeInt(baos, 0xdeadbeef);
// Transition time array, as ints.
for (int[] transitionTime : transitionTimes) {
int transition = transitionTime[0];
writeInt(baos, transition);
}
// Transition type array.
for (int[] transitionTime : transitionTimes) {
byte type = (byte) transitionTime[1];
baos.write(type);
}
for (int i = 0; i < transitionTypes.length; i++) {
int[] transitionType = transitionTypes[i];
int offset = transitionType[0];
byte dst = (byte) transitionType[1];
writeInt(baos, offset);
baos.write(dst);
// Useless stuff.
baos.write(i);
}
return ZoneInfo.makeTimeZone("TimeZone for '" + name + "'",
new ByteBufferIterator(ByteBuffer.wrap(baos.toByteArray())), currentTimeMillis);
}
private static void writeInt(OutputStream os, int value) throws Exception {
byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
os.write(bytes);
}
/**
* A {@link BufferIterator} that wraps a {@link ByteBuffer}.
*/
private static class ByteBufferIterator extends BufferIterator {
private final ByteBuffer buffer;
public ByteBufferIterator(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void seek(int offset) {
buffer.position(offset);
}
@Override
public void skip(int byteCount) {
buffer.position(buffer.position() + byteCount);
}
@Override
public void readByteArray(byte[] dst, int dstOffset, int byteCount) {
buffer.get(dst, dstOffset, byteCount);
}
@Override
public byte readByte() {
return buffer.get();
}
@Override
public int readInt() {
int value = buffer.asIntBuffer().get();
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(4);
return value;
}
@Override
public void readIntArray(int[] dst, int dstOffset, int intCount) {
buffer.asIntBuffer().get(dst, dstOffset, intCount);
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(4 * intCount);
}
@Override
public short readShort() {
short value = buffer.asShortBuffer().get();
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(2);
return value;
}
}
}