sdk-csharp/test/CloudNative.CloudEvents.Uni.../TimestampsTest.cs

184 lines
8.8 KiB
C#

// Copyright 2021 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using Xunit;
using static CloudNative.CloudEvents.UnitTests.TestHelpers;
namespace CloudNative.CloudEvents.UnitTests
{
public class TimestampsTest
{
// TryParse and Parse are tested together, as they're effectively alternatives for the same thing.
/// <summary>
/// Just a selection of simple tests; this is not trying to be exhaustive.
/// (The other parse tests check specific aspects more thoroughly.)
/// </summary>
[Theory]
[InlineData("2021-01-18T14:52:01Z", 2021, 1, 18, 14, 52, 1, 0, 0)]
[InlineData("2000-02-29T01:23:45.678+01:30", 2000, 2, 29, 1, 23, 45, 6_780_000, 90)]
[InlineData("2000-02-29T01:23:45-01:30", 2000, 2, 29, 1, 23, 45, 0, -90)]
public void Parse_Success_Simple(string text, int year, int month, int day, int hour, int minute, int second, int ticks, int offsetMinutes)
{
var expected = new DateTimeOffset(year, month, day, hour, minute, second, 0, TimeSpan.FromMinutes(offsetMinutes))
.AddTicks(ticks);
AssertParseSuccess(expected, text);
}
[Theory]
[InlineData("", 0)]
[InlineData(".0", 0)]
[InlineData(".1", 1_000_000)]
[InlineData(".12", 1_200_000)]
[InlineData(".123", 1_230_000)]
[InlineData(".1234", 1_234_000)]
[InlineData(".12345", 1_234_500)]
[InlineData(".123456", 1_234_560)]
[InlineData(".1234567", 1_234_567)]
// We truncate nanoseconds to the tick
[InlineData(".12345678", 1_234_567)]
[InlineData(".123456789", 1_234_567)]
// (Realistically we're unlikely to get any values with greater precision than nanoseconds, but
// we might as well test it.)
[InlineData(".12345678912345", 1_234_567)]
public void Parse_Success_VaryingFractionalSeconds(string fractionalPart, int expectedTicks)
{
string text = $"2021-01-18T14:52:01{fractionalPart}+05:00";
DateTimeOffset expected = new DateTimeOffset(2021, 1, 18, 14, 52, 1, 0, TimeSpan.FromHours(5))
.AddTicks(expectedTicks);
AssertParseSuccess(expected, text);
}
[Theory]
[InlineData("Z", 0)]
// Alternative way of representing UTC (this is perfectly valid).
[InlineData("+00:00", 0)]
// This is the "unknown local offset". We treat this as UTC, as there is no reasonable
// way to express it as a DateTimeOffset, and that's better than failing. It's unlikely
// we'll ever see this, and arguably it's not really a "timestamp" at that point anyway.
[InlineData("-00:00", 0)]
[InlineData("+01:00", 60)]
[InlineData("-01:00", -60)]
[InlineData("+01:30", 90)]
[InlineData("-01:30", -90)]
// Extreme values
[InlineData("+14:00", 14 * 60)]
[InlineData("-14:00", -14 * 60)]
public void Parse_Success_VaryingUtcOffset(string offsetPart, int expectedOffsetMinutes)
{
// No fractional seconds
string text = $"2021-01-18T14:52:01{offsetPart}";
DateTimeOffset expected = new DateTimeOffset(2021, 1, 18, 14, 52, 1, 0, TimeSpan.FromMinutes(expectedOffsetMinutes));
AssertParseSuccess(expected, text);
// Single check for fractional seconds
text = $"2021-01-18T14:52:01.500{offsetPart}";
expected = expected.AddMilliseconds(500);
AssertParseSuccess(expected, text);
}
private static void AssertParseSuccess(DateTimeOffset expected, string text)
{
var parsed = Timestamps.Parse(text);
AssertTimestampsEqual(expected, parsed);
Assert.True(Timestamps.TryParse(text, out parsed));
AssertTimestampsEqual(expected, parsed);
}
[Theory]
[InlineData("")]
[InlineData("garbage")]
[InlineData("garbage that is long enough")]
[InlineData("2021-01-18T14:52:01")] // No UTC offset indicator
[InlineData("2021-01-18T14:52:01.1234567XYZ")] // Garbage after 7 significant digits of sub-second
[InlineData("2021-01-18T14:52:01Z01")] // Text after UTC offset indicator
[InlineData("2021-01-18T14:52:01X")] // Garbage UTC offset indicator
[InlineData("2021-01-18T14:52:01+XX:XX")] // Garbage UTC offset indicator (but right length)
[InlineData("2021-01-18T14:52:01+01")] // Hour-only UTC offset indicator
[InlineData("2021-01-18T14:52:01+01:30:30")] // Sub-minute UTC offset indicator
[InlineData("2021-01-18T14:52:01+14:01")] // UTC offset indicator out of range
[InlineData("2021-01-18T14:52:01-14:01")] // UTC offset indicator out of range
[InlineData("2021-01-18T14:52:01-00:60")] // UTC offset indicator with invalid minutes
[InlineData("2021-01-18 14:52:01Z")] // Space instead of 'T'
[InlineData("2100-02-29T14:52:01Z")] // Feb 29th in non-leap-year
[InlineData("10000-01-01T00:00:00Z")] // Year out of range
[InlineData("2021-13-01T00:00:00Z")] // Month out of range
[InlineData("2021-01-50T00:00:00Z")] // Day out of range
[InlineData("2021-01-18T24:00:00Z")] // Hour out of range
[InlineData("2021-01-18T14:60:00Z")] // Minute out of range
[InlineData("2021-01-18T14:00:60Z")] // Second out of range
[InlineData("100-01-01T00:00:00Z")] // Non-padded year
[InlineData("2021-1-01T00:00:00Z")] // Non-padded month
[InlineData("2021-01-1T00:00:00Z")] // Non-padded day
[InlineData("2021-01-01T1:00:00Z")] // Non-padded hour
[InlineData("2021-01-01T00:1:00Z")] // Non-padded minute
[InlineData("2021-01-01T00:01:1Z")] // Non-padded second
[InlineData("2021-01-01T00:01Z")] // No second part
public void Parse_Failure(string text)
{
Assert.False(Timestamps.TryParse(text, out _));
Assert.Throws<FormatException>(() => Timestamps.Parse(text));
}
[Fact]
public void Parse_Null()
{
Assert.Throws<ArgumentNullException>(() => Timestamps.TryParse(null!, out _));
Assert.Throws<ArgumentNullException>(() => Timestamps.Parse(null!));
}
/// <summary>
/// As we're already testing parsing thoroughly, the simplest way of providing
/// a value to format is to parse a string. Many examples will round-trip, in which
/// case it's simple just to provide the information once.
/// <see cref="Format_NonRoundtrip(string, string)"/> tests situations which don't round-trip.
/// </summary>
/// <param name="input"></param>
[Theory]
[InlineData("2021-01-18T14:52:01Z")]
[InlineData("2000-02-29T01:23:45.678+01:30")]
[InlineData("2000-02-29T01:23:45-01:30")]
[InlineData("2000-02-29T01:23:45.678+10:00")]
[InlineData("2000-02-29T01:23:45-10:00")]
[InlineData("2021-01-18T14:52:01.100Z")]
[InlineData("2021-01-18T14:52:01.120Z")]
[InlineData("2021-01-18T14:52:01.123Z")]
[InlineData("2021-01-18T14:52:01.123400Z")]
[InlineData("2021-01-18T14:52:01.123450Z")]
[InlineData("2021-01-18T14:52:01.123456Z")]
[InlineData("2021-01-18T14:52:01.1234567Z")]
public void Format_Roundtrip(string input)
{
var parsed = Timestamps.Parse(input);
var formatted = Timestamps.Format(parsed);
Assert.Equal(input, formatted);
}
[Theory]
// Zero offset normalized to Z
[InlineData("2021-01-18T14:52:01+00:00", "2021-01-18T14:52:01Z")]
[InlineData("2021-01-18T14:52:01-00:00", "2021-01-18T14:52:01Z")]
// Second precision
[InlineData("2000-02-29T01:23:45.000-01:30", "2000-02-29T01:23:45-01:30")]
// Millisecond precision
[InlineData("2000-02-29T01:23:45.67+01:30", "2000-02-29T01:23:45.670+01:30")]
[InlineData("2000-02-29T01:23:45.678000+01:30", "2000-02-29T01:23:45.678+01:30")]
[InlineData("2000-02-29T01:23:45.6780000000+01:30", "2000-02-29T01:23:45.678+01:30")]
// Microssecond precision
[InlineData("2000-02-29T01:23:45.6781+01:30", "2000-02-29T01:23:45.678100+01:30")]
[InlineData("2000-02-29T01:23:45.6781000+01:30", "2000-02-29T01:23:45.678100+01:30")]
[InlineData("2000-02-29T01:23:45.6781000000+01:30", "2000-02-29T01:23:45.678100+01:30")]
// Tick precision
[InlineData("2021-01-18T14:52:01.123456789Z", "2021-01-18T14:52:01.1234567Z")]
public void Format_NonRoundtrip(string input, string expectedFormatted)
{
var parsed = Timestamps.Parse(input);
var formatted = Timestamps.Format(parsed);
Assert.Equal(expectedFormatted, formatted);
}
}
}