Skip to content

Commit

Permalink
CP-51694: Add date deserialization unit tests for C#/Java/Go (#6027)
Browse files Browse the repository at this point in the history
I didn't update C because it was out of scope for the CP. Also, we're
likely to change it soon enough when moving to JSON-RPC so I saw little
use in investing that time.

Note that the Java tests don't need an explicit step in the GitHub
action since `surefire` plugin runs it at compile time.

I added the Go tests under `go/src` and not `component-test/` because
they're unit tests, and don't _really_ belong in that series of test.
  • Loading branch information
danilo-delbusso authored Nov 26, 2024
2 parents 6f64a78 + b81d11e commit fce648f
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 71 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/generate-and-build-sdks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ jobs:
shell: bash
run: opam exec -- make sdk

# sdk-ci runs some Go unit tests.
# This setting ensures that SDK date time
# tests are run on a machine that
# isn't using UTC
- name: Set Timezone to Tokyo for datetime tests
run: |
sudo timedatectl set-timezone Asia/Tokyo
- name: Run CI for SDKs
uses: ./.github/workflows/sdk-ci

Expand Down Expand Up @@ -54,6 +62,7 @@ jobs:
path: |
_build/install/default/share/go/*
!_build/install/default/share/go/dune
!_build/install/default/share/go/**/*_test.go
- name: Store Java SDK source
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -110,6 +119,14 @@ jobs:
java-version: '17'
distribution: 'temurin'

# Java Tests are run at compile time.
# This setting ensures that SDK date time
# tests are run on a machine that
# isn't using UTC
- name: Set Timezone to Tokyo for datetime tests
run: |
sudo timedatectl set-timezone Asia/Tokyo
- name: Build Java SDK
shell: bash
run: |
Expand Down Expand Up @@ -138,6 +155,21 @@ jobs:
name: SDK_Source_CSharp
path: source/

# All tests builds and pipelines should
# work on other timezones. This setting ensures that
# SDK date time tests are run on a machine that
# isn't using UTC
- name: Set Timezone to Tokyo for datetime tests
shell: pwsh
run: Set-TimeZone -Id "Tokyo Standard Time"

- name: Test C# SDK
shell: pwsh
run: |
dotnet test source/XenServerTest `
--disable-build-servers `
--verbosity=normal
- name: Build C# SDK
shell: pwsh
run: |
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/go-ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ runs:
working-directory: ${{ github.workspace }}/_build/install/default/share/go/src
args: --config=${{ github.workspace }}/.golangci.yml

- name: Run Go Tests
shell: bash
working-directory: ${{ github.workspace }}/_build/install/default/share/go/src
run: go test -v

- name: Run CI for Go SDK
shell: bash
run: |
Expand Down
148 changes: 148 additions & 0 deletions ocaml/sdk-gen/csharp/autogen/XenServerTest/DateTimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright (c) Cloud Software Group, Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1) Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2) Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System.Reflection;
using Newtonsoft.Json;
using XenAPI;
using Console = System.Console;

namespace XenServerTest;

internal class DateTimeObject
{
[JsonConverter(typeof(XenDateTimeConverter))]
public DateTime Date { get; set; }
}

[TestClass]
public class DateTimeTests
{
private readonly JsonSerializerSettings _settings = new()
{
Converters = new List<JsonConverter> { new XenDateTimeConverter() }
};

[TestMethod]
[DynamicData(nameof(GetTestData), DynamicDataSourceType.Method,
DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))]
public void TestXenDateTimeConverter(string dateString, DateTime expectedDateTime, DateTimeKind expectedDateTimeKind)
{
try
{
var jsonDateString = "{ \"Date\" : \"" + dateString + "\" }";
var actualDateTimeObject = JsonConvert.DeserializeObject<DateTimeObject>(jsonDateString, _settings);


Assert.IsNotNull(actualDateTimeObject?.Date, $"Failed to convert '{dateString}'");
var actualDateTime = actualDateTimeObject.Date;
Assert.IsTrue(expectedDateTimeKind.Equals(actualDateTime.Kind));

// expected times are in UTC to ensure these tests do
// not fail when running in other timezones
if (expectedDateTimeKind == DateTimeKind.Local)
actualDateTime = actualDateTime.ToUniversalTime();

Assert.IsTrue(expectedDateTime.Equals(actualDateTime),
$"Conversion of '{dateString}' resulted in an incorrect DateTime value. Expected '{expectedDateTime} but instead received '{actualDateTime}'");
}
catch (Exception ex)
{
// Log the error or mark this specific data entry as failed
Console.WriteLine($@"Error processing dateString '{dateString}': {ex.Message}");
Assert.Fail($"An error occurred while processing '{dateString}'");
}
}

public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data)
{
return $"{methodInfo.Name}: '{data[0] as string}'";
}

public static IEnumerable<object[]> GetTestData()
{
// no dashes, no colons
yield return new object[] { "20220101T123045", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[] { "20220101T123045Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[] { "20220101T123045+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[] { "20220101T123045+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[] { "20220101T123045+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };

yield return new object[]
{ "20220101T123045.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[]
{ "20220101T123045.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[]
{ "20220101T123045.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "20220101T123045.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "20220101T123045.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };

// no dashes, with colons
yield return new object[]
{ "20220101T12:30:45", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[] { "20220101T12:30:45Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[] { "20220101T12:30:45+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[] { "20220101T12:30:45+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "20220101T12:30:45+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };

yield return new object[]
{ "20220101T12:30:45.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[]
{ "20220101T12:30:45.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[]
{ "20220101T12:30:45.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "20220101T12:30:45.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "20220101T12:30:45.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };

// dashes and colons
yield return new object[]
{ "2022-01-01T12:30:45", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[] { "2022-01-01T12:30:45Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[] { "2022-01-01T12:30:45+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "2022-01-01T12:30:45+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "2022-01-01T12:30:45+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };

yield return new object[]
{ "2022-01-01T12:30:45.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
yield return new object[]
{ "2022-01-01T12:30:45.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
yield return new object[]
{ "2022-01-01T12:30:45.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "2022-01-01T12:30:45.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
yield return new object[]
{ "2022-01-01T12:30:45.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
}
}
27 changes: 27 additions & 0 deletions ocaml/sdk-gen/csharp/autogen/XenServerTest/XenServerTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\XenServer.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

</Project>
15 changes: 10 additions & 5 deletions ocaml/sdk-gen/csharp/autogen/src/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;

[assembly: InternalsVisibleTo("XenServerTest")]

namespace XenAPI
{
Expand Down Expand Up @@ -437,12 +439,16 @@ internal class XenDateTimeConverter : IsoDateTimeConverter

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string str = JToken.Load(reader).ToString();
// JsonReader may have already parsed the date for us
if (reader.ValueType != null && reader.ValueType == typeof(DateTime))
{
return reader.Value;
}

DateTime result;
var str = JToken.Load(reader).ToString();

if (DateTime.TryParseExact(str, DateFormatsUtc, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result))
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result))
return result;

if (DateTime.TryParseExact(str, DateFormatsLocal, CultureInfo.InvariantCulture,
Expand All @@ -454,9 +460,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateTime)
if (value is DateTime dateTime)
{
var dateTime = (DateTime)value;
dateTime = dateTime.ToUniversalTime();
var text = dateTime.ToString(DateFormatsUtc[0], CultureInfo.InvariantCulture);
writer.WriteValue(text);
Expand Down
91 changes: 91 additions & 0 deletions ocaml/sdk-gen/go/autogen/src/convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) Cloud Software Group, Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1) Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2) Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package xenapi_test

import (
"testing"
"time"

"go/xenapi"
)

func TestDateDeseralization(t *testing.T) {
dates := map[string]time.Time{
// no dashes, no colons
"20220101T123045": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"20220101T123045Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"20220101T123045+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)), // +03 timezone
"20220101T123045+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
"20220101T123045+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),

"20220101T123045.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"20220101T123045.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"20220101T123045.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
"20220101T123045.123+0300": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
"20220101T123045.123+03:00": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),

// no dashes, with colons
"20220101T12:30:45": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"20220101T12:30:45Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"20220101T12:30:45+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
"20220101T12:30:45+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
"20220101T12:30:45+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),

"20220101T12:30:45.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"20220101T12:30:45.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"20220101T12:30:45.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
"20220101T12:30:45.123+0300": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
"20220101T12:30:45.123+03:00": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),

// dashes and colons
"2022-01-01T12:30:45": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"2022-01-01T12:30:45Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
"2022-01-01T12:30:45+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
"2022-01-01T12:30:45+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
"2022-01-01T12:30:45+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),

"2022-01-01T12:30:45.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"2022-01-01T12:30:45.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
"2022-01-01T12:30:45.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
}
for input, expected := range dates {
t.Run("Input:"+input, func(t *testing.T) {
result, err := xenapi.DeserializeTime("", input)
if err == nil {
matching := expected.Equal(result)
if !matching {
t.Fatalf(`Failed to find match for '%s'`, input)
}
} else {
t.Fatalf(`Failed to find match for '%s'`, input)
}
})
}
}
Loading

0 comments on commit fce648f

Please sign in to comment.