Skip to content

Commit fce648f

Browse files
CP-51694: Add date deserialization unit tests for C#/Java/Go (#6027)
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.
2 parents 6f64a78 + b81d11e commit fce648f

File tree

10 files changed

+570
-71
lines changed

10 files changed

+570
-71
lines changed

.github/workflows/generate-and-build-sdks.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ jobs:
2424
shell: bash
2525
run: opam exec -- make sdk
2626

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

@@ -54,6 +62,7 @@ jobs:
5462
path: |
5563
_build/install/default/share/go/*
5664
!_build/install/default/share/go/dune
65+
!_build/install/default/share/go/**/*_test.go
5766
5867
- name: Store Java SDK source
5968
uses: actions/upload-artifact@v4
@@ -110,6 +119,14 @@ jobs:
110119
java-version: '17'
111120
distribution: 'temurin'
112121

122+
# Java Tests are run at compile time.
123+
# This setting ensures that SDK date time
124+
# tests are run on a machine that
125+
# isn't using UTC
126+
- name: Set Timezone to Tokyo for datetime tests
127+
run: |
128+
sudo timedatectl set-timezone Asia/Tokyo
129+
113130
- name: Build Java SDK
114131
shell: bash
115132
run: |
@@ -138,6 +155,21 @@ jobs:
138155
name: SDK_Source_CSharp
139156
path: source/
140157

158+
# All tests builds and pipelines should
159+
# work on other timezones. This setting ensures that
160+
# SDK date time tests are run on a machine that
161+
# isn't using UTC
162+
- name: Set Timezone to Tokyo for datetime tests
163+
shell: pwsh
164+
run: Set-TimeZone -Id "Tokyo Standard Time"
165+
166+
- name: Test C# SDK
167+
shell: pwsh
168+
run: |
169+
dotnet test source/XenServerTest `
170+
--disable-build-servers `
171+
--verbosity=normal
172+
141173
- name: Build C# SDK
142174
shell: pwsh
143175
run: |

.github/workflows/go-ci/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ runs:
1414
working-directory: ${{ github.workspace }}/_build/install/default/share/go/src
1515
args: --config=${{ github.workspace }}/.golangci.yml
1616

17+
- name: Run Go Tests
18+
shell: bash
19+
working-directory: ${{ github.workspace }}/_build/install/default/share/go/src
20+
run: go test -v
21+
1722
- name: Run CI for Go SDK
1823
shell: bash
1924
run: |
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (c) Cloud Software Group, Inc.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions
6+
* are met:
7+
*
8+
* 1) Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
*
11+
* 2) Redistributions in binary form must reproduce the above
12+
* copyright notice, this list of conditions and the following
13+
* disclaimer in the documentation and/or other materials
14+
* provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
20+
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
21+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
25+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
27+
* OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*/
29+
30+
using System.Reflection;
31+
using Newtonsoft.Json;
32+
using XenAPI;
33+
using Console = System.Console;
34+
35+
namespace XenServerTest;
36+
37+
internal class DateTimeObject
38+
{
39+
[JsonConverter(typeof(XenDateTimeConverter))]
40+
public DateTime Date { get; set; }
41+
}
42+
43+
[TestClass]
44+
public class DateTimeTests
45+
{
46+
private readonly JsonSerializerSettings _settings = new()
47+
{
48+
Converters = new List<JsonConverter> { new XenDateTimeConverter() }
49+
};
50+
51+
[TestMethod]
52+
[DynamicData(nameof(GetTestData), DynamicDataSourceType.Method,
53+
DynamicDataDisplayName = nameof(GetCustomDynamicDataDisplayName))]
54+
public void TestXenDateTimeConverter(string dateString, DateTime expectedDateTime, DateTimeKind expectedDateTimeKind)
55+
{
56+
try
57+
{
58+
var jsonDateString = "{ \"Date\" : \"" + dateString + "\" }";
59+
var actualDateTimeObject = JsonConvert.DeserializeObject<DateTimeObject>(jsonDateString, _settings);
60+
61+
62+
Assert.IsNotNull(actualDateTimeObject?.Date, $"Failed to convert '{dateString}'");
63+
var actualDateTime = actualDateTimeObject.Date;
64+
Assert.IsTrue(expectedDateTimeKind.Equals(actualDateTime.Kind));
65+
66+
// expected times are in UTC to ensure these tests do
67+
// not fail when running in other timezones
68+
if (expectedDateTimeKind == DateTimeKind.Local)
69+
actualDateTime = actualDateTime.ToUniversalTime();
70+
71+
Assert.IsTrue(expectedDateTime.Equals(actualDateTime),
72+
$"Conversion of '{dateString}' resulted in an incorrect DateTime value. Expected '{expectedDateTime} but instead received '{actualDateTime}'");
73+
}
74+
catch (Exception ex)
75+
{
76+
// Log the error or mark this specific data entry as failed
77+
Console.WriteLine($@"Error processing dateString '{dateString}': {ex.Message}");
78+
Assert.Fail($"An error occurred while processing '{dateString}'");
79+
}
80+
}
81+
82+
public static string GetCustomDynamicDataDisplayName(MethodInfo methodInfo, object[] data)
83+
{
84+
return $"{methodInfo.Name}: '{data[0] as string}'";
85+
}
86+
87+
public static IEnumerable<object[]> GetTestData()
88+
{
89+
// no dashes, no colons
90+
yield return new object[] { "20220101T123045", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
91+
yield return new object[] { "20220101T123045Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
92+
yield return new object[] { "20220101T123045+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
93+
yield return new object[] { "20220101T123045+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
94+
yield return new object[] { "20220101T123045+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
95+
96+
yield return new object[]
97+
{ "20220101T123045.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
98+
yield return new object[]
99+
{ "20220101T123045.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
100+
yield return new object[]
101+
{ "20220101T123045.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
102+
yield return new object[]
103+
{ "20220101T123045.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
104+
yield return new object[]
105+
{ "20220101T123045.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
106+
107+
// no dashes, with colons
108+
yield return new object[]
109+
{ "20220101T12:30:45", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
110+
yield return new object[] { "20220101T12:30:45Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
111+
yield return new object[] { "20220101T12:30:45+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
112+
yield return new object[] { "20220101T12:30:45+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
113+
yield return new object[]
114+
{ "20220101T12:30:45+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
115+
116+
yield return new object[]
117+
{ "20220101T12:30:45.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
118+
yield return new object[]
119+
{ "20220101T12:30:45.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
120+
yield return new object[]
121+
{ "20220101T12:30:45.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
122+
yield return new object[]
123+
{ "20220101T12:30:45.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
124+
yield return new object[]
125+
{ "20220101T12:30:45.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
126+
127+
// dashes and colons
128+
yield return new object[]
129+
{ "2022-01-01T12:30:45", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Unspecified };
130+
yield return new object[] { "2022-01-01T12:30:45Z", new DateTime(2022, 1, 1, 12, 30, 45, DateTimeKind.Utc), DateTimeKind.Utc };
131+
yield return new object[] { "2022-01-01T12:30:45+03", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
132+
yield return new object[]
133+
{ "2022-01-01T12:30:45+0300", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
134+
yield return new object[]
135+
{ "2022-01-01T12:30:45+03:00", new DateTime(2022, 1, 1, 9, 30, 45, DateTimeKind.Utc), DateTimeKind.Local };
136+
137+
yield return new object[]
138+
{ "2022-01-01T12:30:45.123", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Unspecified };
139+
yield return new object[]
140+
{ "2022-01-01T12:30:45.123Z", new DateTime(2022, 1, 1, 12, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Utc };
141+
yield return new object[]
142+
{ "2022-01-01T12:30:45.123+03", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
143+
yield return new object[]
144+
{ "2022-01-01T12:30:45.123+0300", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
145+
yield return new object[]
146+
{ "2022-01-01T12:30:45.123+03:00", new DateTime(2022, 1, 1, 9, 30, 45, 123, DateTimeKind.Utc), DateTimeKind.Local };
147+
}
148+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
<IsTestProject>true</IsTestProject>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="coverlet.collector" Version="3.2.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
15+
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
16+
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\src\XenServer.csproj" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
25+
</ItemGroup>
26+
27+
</Project>

ocaml/sdk-gen/csharp/autogen/src/Converters.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
using System.Collections.Generic;
3232
using System.Globalization;
3333
using System.Linq;
34+
using System.Runtime.CompilerServices;
3435
using Newtonsoft.Json;
3536
using Newtonsoft.Json.Converters;
3637
using Newtonsoft.Json.Linq;
3738

39+
[assembly: InternalsVisibleTo("XenServerTest")]
3840

3941
namespace XenAPI
4042
{
@@ -437,12 +439,16 @@ internal class XenDateTimeConverter : IsoDateTimeConverter
437439

438440
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
439441
{
440-
string str = JToken.Load(reader).ToString();
442+
// JsonReader may have already parsed the date for us
443+
if (reader.ValueType != null && reader.ValueType == typeof(DateTime))
444+
{
445+
return reader.Value;
446+
}
441447

442-
DateTime result;
448+
var str = JToken.Load(reader).ToString();
443449

444450
if (DateTime.TryParseExact(str, DateFormatsUtc, CultureInfo.InvariantCulture,
445-
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result))
451+
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var result))
446452
return result;
447453

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

455461
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
456462
{
457-
if (value is DateTime)
463+
if (value is DateTime dateTime)
458464
{
459-
var dateTime = (DateTime)value;
460465
dateTime = dateTime.ToUniversalTime();
461466
var text = dateTime.ToString(DateFormatsUtc[0], CultureInfo.InvariantCulture);
462467
writer.WriteValue(text);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) Cloud Software Group, Inc.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions
6+
* are met:
7+
*
8+
* 1) Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
*
11+
* 2) Redistributions in binary form must reproduce the above
12+
* copyright notice, this list of conditions and the following
13+
* disclaimer in the documentation and/or other materials
14+
* provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
19+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
20+
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
21+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
25+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
27+
* OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*/
29+
30+
package xenapi_test
31+
32+
import (
33+
"testing"
34+
"time"
35+
36+
"go/xenapi"
37+
)
38+
39+
func TestDateDeseralization(t *testing.T) {
40+
dates := map[string]time.Time{
41+
// no dashes, no colons
42+
"20220101T123045": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
43+
"20220101T123045Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
44+
"20220101T123045+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)), // +03 timezone
45+
"20220101T123045+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
46+
"20220101T123045+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
47+
48+
"20220101T123045.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
49+
"20220101T123045.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
50+
"20220101T123045.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
51+
"20220101T123045.123+0300": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
52+
"20220101T123045.123+03:00": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
53+
54+
// no dashes, with colons
55+
"20220101T12:30:45": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
56+
"20220101T12:30:45Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
57+
"20220101T12:30:45+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
58+
"20220101T12:30:45+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
59+
"20220101T12:30:45+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
60+
61+
"20220101T12:30:45.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
62+
"20220101T12:30:45.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
63+
"20220101T12:30:45.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
64+
"20220101T12:30:45.123+0300": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
65+
"20220101T12:30:45.123+03:00": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
66+
67+
// dashes and colons
68+
"2022-01-01T12:30:45": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
69+
"2022-01-01T12:30:45Z": time.Date(2022, 1, 1, 12, 30, 45, 0, time.UTC),
70+
"2022-01-01T12:30:45+03": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
71+
"2022-01-01T12:30:45+0300": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
72+
"2022-01-01T12:30:45+03:00": time.Date(2022, 1, 1, 12, 30, 45, 0, time.FixedZone("", 3*60*60)),
73+
74+
"2022-01-01T12:30:45.123": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
75+
"2022-01-01T12:30:45.123Z": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.UTC),
76+
"2022-01-01T12:30:45.123+03": time.Date(2022, 1, 1, 12, 30, 45, 123000000, time.FixedZone("", 3*60*60)),
77+
}
78+
for input, expected := range dates {
79+
t.Run("Input:"+input, func(t *testing.T) {
80+
result, err := xenapi.DeserializeTime("", input)
81+
if err == nil {
82+
matching := expected.Equal(result)
83+
if !matching {
84+
t.Fatalf(`Failed to find match for '%s'`, input)
85+
}
86+
} else {
87+
t.Fatalf(`Failed to find match for '%s'`, input)
88+
}
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)