blob: 5a8958458834717143168f6b643f2b0b23a16bb4 [file] [log] [blame]
// Copyright (C) 2017 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 com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.sql.Timestamp;
import java.time.ZonedDateTime;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class CommentTimestampAdapterTest {
/** Arbitrary time outside of a DST transition, as an ISO instant. */
private static final String NON_DST_STR = "2017-02-07T10:20:30.123Z";
/** Arbitrary time outside of a DST transition, as a reasonable Java 11 representation. */
private static final ZonedDateTime NON_DST = ZonedDateTime.parse(NON_DST_STR);
/** {@link #NON_DST_STR} truncated to seconds. */
private static final String NON_DST_STR_TRUNC = "2017-02-07T10:20:30Z";
/** Arbitrary time outside of a DST transition, as an unreasonable Timestamp representation. */
private static final Timestamp NON_DST_TS = Timestamp.from(NON_DST.toInstant());
/** {@link #NON_DST_TS} truncated to seconds. */
private static final Timestamp NON_DST_TS_TRUNC =
Timestamp.from(ZonedDateTime.parse(NON_DST_STR_TRUNC).toInstant());
/**
* Real live ms since epoch timestamp of a comment that was posted during the PDT to PST
* transition in November 2013.
*/
private static final long MID_DST_MS = 1383466224175L;
/**
* Ambiguous string representation of {@link #MID_DST_MS} that was actually stored in NoteDb for
* this comment.
*/
private static final String MID_DST_STR = "Nov 3, 2013 1:10:24 AM";
private TimeZone systemTimeZone;
private Gson legacyGson;
private Gson gson;
@Before
public void setUp() {
systemTimeZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
// Match ChangeNoteUtil#gson as of 4e1f02db913d91f2988f559048e513e6093a1bce
legacyGson = new GsonBuilder().setPrettyPrinting().create();
gson = ChangeNoteJson.newGson();
}
@After
public void tearDown() {
TimeZone.setDefault(systemTimeZone);
}
@Test
public void legacyGsonBehavesAsExpectedDuringDstTransition() {
long oneHourMs = TimeUnit.HOURS.toMillis(1);
String beforeJson = "\"Nov 3, 2013 12:10:24 AM\"";
Timestamp beforeTs = new Timestamp(MID_DST_MS - oneHourMs);
assertThat(legacyGson.toJson(beforeTs)).isEqualTo(beforeJson);
String ambiguousJson = '"' + MID_DST_STR + '"';
Timestamp duringTs = new Timestamp(MID_DST_MS);
assertThat(legacyGson.toJson(duringTs)).isEqualTo(ambiguousJson);
Timestamp afterTs = new Timestamp(MID_DST_MS + oneHourMs);
assertThat(legacyGson.toJson(afterTs)).isEqualTo(ambiguousJson);
Timestamp beforeTsTruncated = new Timestamp(beforeTs.getTime() / 1000 * 1000);
assertThat(legacyGson.fromJson(beforeJson, Timestamp.class)).isEqualTo(beforeTsTruncated);
// Gson just picks one, and it happens to be the one after the PST transition.
Timestamp afterTsTruncated = new Timestamp(afterTs.getTime() / 1000 * 1000);
assertThat(legacyGson.fromJson(ambiguousJson, Timestamp.class)).isEqualTo(afterTsTruncated);
}
@Test
public void legacyAdapterViaZonedDateTime() {
assertThat(legacyGson.toJson(NON_DST_TS)).isEqualTo("\"Feb 7, 2017 2:20:30 AM\"");
}
@Test
public void legacyAdapterCanParseOutputOfNewAdapter() {
String instantJson = gson.toJson(NON_DST_TS);
assertThat(instantJson).isEqualTo('"' + NON_DST_STR_TRUNC + '"');
Timestamp result = legacyGson.fromJson(instantJson, Timestamp.class);
assertThat(result).isEqualTo(NON_DST_TS_TRUNC);
}
@Test
public void newAdapterCanParseOutputOfLegacyAdapter() {
String legacyJson = legacyGson.toJson(NON_DST_TS);
assertThat(legacyJson).isEqualTo("\"Feb 7, 2017 2:20:30 AM\"");
assertThat(gson.fromJson(legacyJson, Timestamp.class))
.isEqualTo(new Timestamp(NON_DST_TS.getTime() / 1000 * 1000));
}
@Test
public void fixedFallbackFormatCanParseOutputOfLegacyAdapter() {
assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 7, 2017 2:20:30 AM"))
.isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-07T10:20:30Z").toInstant()));
assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 10:20:30 AM"))
.isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T18:20:30Z").toInstant()));
assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 17, 2017 02:20:30 PM"))
.isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-17T22:20:30Z").toInstant()));
assertThat(CommentTimestampAdapter.parseDateTimeWithFixedFormat("Feb 07, 2017 10:20:30 PM"))
.isEqualTo(Timestamp.from(ZonedDateTime.parse("2017-02-08T06:20:30Z").toInstant()));
}
@Test
public void newAdapterDisagreesWithLegacyAdapterDuringDstTransition() {
String duringJson = legacyGson.toJson(new Timestamp(MID_DST_MS));
Timestamp duringTs = legacyGson.fromJson(duringJson, Timestamp.class);
// This is unfortunate, but it's just documenting the current behavior, there is no real good
// solution here. The goal is that all these changes will be rebuilt with proper UTC instant
// strings shortly after the new adapter is live.
Timestamp newDuringTs = gson.fromJson(duringJson, Timestamp.class);
assertThat(newDuringTs.toString()).isEqualTo(duringTs.toString());
assertThat(newDuringTs).isNotEqualTo(duringTs);
}
@Test
public void newAdapterRoundTrip() {
String json = gson.toJson(NON_DST_TS);
// Round-trip lossily truncates ms, but that's ok.
assertThat(json).isEqualTo('"' + NON_DST_STR_TRUNC + '"');
assertThat(gson.fromJson(json, Timestamp.class)).isEqualTo(NON_DST_TS_TRUNC);
}
@Test
public void nullSafety() {
assertThat(gson.toJson(null, Timestamp.class)).isEqualTo("null");
assertThat(gson.fromJson("null", Timestamp.class)).isNull();
}
@Test
public void newAdapterRoundTripOfWholeComment() {
Comment c =
new HumanComment(
new Comment.Key("uuid", "filename", 1),
Account.id(100),
NON_DST_TS.toInstant(),
(short) 0,
"message",
"serverId",
false);
c.lineNbr = 1;
c.setCommitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
String json = gson.toJson(c);
assertThat(json).contains("\"writtenOn\": \"" + NON_DST_STR_TRUNC + "\",");
Comment result = gson.fromJson(json, HumanComment.class);
// Round-trip lossily truncates ms, but that's ok.
assertThat(result.writtenOn).isEqualTo(NON_DST_TS_TRUNC);
result.writtenOn = NON_DST_TS;
assertThat(result).isEqualTo(c);
}
}