diff --git a/.github/workflows/generate-and-build-sdks.yml b/.github/workflows/generate-and-build-sdks.yml index a439c969b50..39645cf68bf 100644 --- a/.github/workflows/generate-and-build-sdks.yml +++ b/.github/workflows/generate-and-build-sdks.yml @@ -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 @@ -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 @@ -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: | @@ -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: | diff --git a/.github/workflows/go-ci/action.yml b/.github/workflows/go-ci/action.yml index c1b2df7f1e1..30bcbfee923 100644 --- a/.github/workflows/go-ci/action.yml +++ b/.github/workflows/go-ci/action.yml @@ -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: | diff --git a/ocaml/sdk-gen/csharp/autogen/XenServerTest/DateTimeTests.cs b/ocaml/sdk-gen/csharp/autogen/XenServerTest/DateTimeTests.cs new file mode 100644 index 00000000000..981204df714 --- /dev/null +++ b/ocaml/sdk-gen/csharp/autogen/XenServerTest/DateTimeTests.cs @@ -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 { 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(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 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 }; + } +} diff --git a/ocaml/sdk-gen/csharp/autogen/XenServerTest/XenServerTest.csproj b/ocaml/sdk-gen/csharp/autogen/XenServerTest/XenServerTest.csproj new file mode 100644 index 00000000000..8300b4b7edb --- /dev/null +++ b/ocaml/sdk-gen/csharp/autogen/XenServerTest/XenServerTest.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/ocaml/sdk-gen/csharp/autogen/src/Converters.cs b/ocaml/sdk-gen/csharp/autogen/src/Converters.cs index 32b02d987a6..6f828fdc0a6 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/Converters.cs +++ b/ocaml/sdk-gen/csharp/autogen/src/Converters.cs @@ -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 { @@ -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, @@ -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); diff --git a/ocaml/sdk-gen/go/autogen/src/convert_test.go b/ocaml/sdk-gen/go/autogen/src/convert_test.go new file mode 100644 index 00000000000..48dabc82898 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/src/convert_test.go @@ -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) + } + }) + } +} diff --git a/ocaml/sdk-gen/go/autogen/src/export_test.go b/ocaml/sdk-gen/go/autogen/src/export_test.go new file mode 100644 index 00000000000..5dbdbeb47e3 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/src/export_test.go @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// This file contains exports of private functions specifically for testing purposes. +// It allows test code to access and verify the behavior of internal functions within the `xenapi` package. + +package xenapi + +// DeserializeTime is a private function that deserializes a time value. +// It is exported for testing to allow verification of its functionality. +var DeserializeTime = deserializeTime diff --git a/ocaml/sdk-gen/java/autogen/xen-api/pom.xml b/ocaml/sdk-gen/java/autogen/xen-api/pom.xml index 66e1b633db2..c3a6cabdfda 100644 --- a/ocaml/sdk-gen/java/autogen/xen-api/pom.xml +++ b/ocaml/sdk-gen/java/autogen/xen-api/pom.xml @@ -62,6 +62,13 @@ httpclient5 5.3 + + + org.junit.jupiter + junit-jupiter + 5.11.1 + test + @@ -119,6 +126,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.0 + diff --git a/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/CustomDateDeserializer.java b/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/CustomDateDeserializer.java index 3ba135e0a40..63be5c1c458 100644 --- a/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/CustomDateDeserializer.java +++ b/ocaml/sdk-gen/java/autogen/xen-api/src/main/java/com/xensource/xenapi/CustomDateDeserializer.java @@ -49,7 +49,7 @@ public class CustomDateDeserializer extends StdDeserializer { /** * Array of {@link SimpleDateFormat} objects representing the date formats * used in xen-api responses. - * + *
* RFC-3339 date formats can be returned in either Zulu or time zone agnostic. * This list is not an exhaustive list of formats supported by RFC-3339, rather * a set of formats that will enable the deserialization of xen-api dates. @@ -57,17 +57,24 @@ public class CustomDateDeserializer extends StdDeserializer { * to this list, please ensure the order is kept. */ private static final SimpleDateFormat[] dateFormatsUtc = { - // Most commonly returned formats - new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"), - new SimpleDateFormat("ss.SSS"), - - // Other - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss'Z'"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSS'Z'"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS'Z'"), - + // Most commonly returned formats + new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"), + new SimpleDateFormat("ss.SSS"), + + // Other + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss'Z'"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSS'Z'"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS'Z'"), + + // Formats without timezone info default to UTC in xapi + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSS"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"), }; /** @@ -78,61 +85,55 @@ public class CustomDateDeserializer extends StdDeserializer { * to this list, please ensure the order is kept. */ private static final SimpleDateFormat[] dateFormatsLocal = { - // no dashes, no colons - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZZZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSXXX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSXX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS"), - - new SimpleDateFormat("yyyyMMdd'T'HHmmssZZZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmssZZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmssZ"), - new SimpleDateFormat("yyyyMMdd'T'HHmmssXXX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmssXX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmssX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss"), - - // no dashes, with colons - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZZZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSXXX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSXX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSS"), - - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZZZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZ"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssXXX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssXX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"), - - // dashes and colons - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"), - - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"), + // no dashes, no colons + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZZZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSXXX"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSXX"), + new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSX"), + + new SimpleDateFormat("yyyyMMdd'T'HHmmssZZZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmssZZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmssZ"), + new SimpleDateFormat("yyyyMMdd'T'HHmmssXXX"), + new SimpleDateFormat("yyyyMMdd'T'HHmmssXX"), + new SimpleDateFormat("yyyyMMdd'T'HHmmssX"), + + // no dashes, with colons + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZZZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSXXX"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSXX"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss.SSSX"), + + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZZZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssZ"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssXXX"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssXX"), + new SimpleDateFormat("yyyyMMdd'T'HH:mm:ssX"), + + // dashes and colons + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXX"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"), + + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXX"), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), }; /** * Constructs a {@link CustomDateDeserializer} instance. - */ + */ public CustomDateDeserializer() { this(null); } @@ -163,9 +164,13 @@ public CustomDateDeserializer(Class t) { @Override public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { var text = jsonParser.getText(); + Date localDate = null; + Date utcDate = null; + for (SimpleDateFormat formatter : dateFormatsUtc) { try { - return formatter.parse(text); + utcDate = formatter.parse(text); + break; } catch (ParseException e) { // ignore } @@ -173,12 +178,26 @@ public Date deserialize(JsonParser jsonParser, DeserializationContext deserializ for (SimpleDateFormat formatter : dateFormatsLocal) { try { - return formatter.parse(text); + localDate = formatter.parse(text); + break; } catch (ParseException e) { // ignore } } - throw new IOException("Failed to deserialize a Date value."); + // Some dates such as 20220101T12:30:45.123+03:00 will match both with a UTC + // and local date format. In that case, we pick the date returned by the + // local formatter, as it's more precise. + // This allows us to match strings with no timezone information (such as 20220101T12:30:45.123) + // as UTC, while correctly parsing more precise date representations + if (localDate != null && utcDate != null) { + return localDate; // Prioritize local format if both match + } else if (localDate != null) { + return localDate; + } else if (utcDate != null) { + return utcDate; + } else { + throw new IOException("Failed to deserialize a Date value."); + } } } diff --git a/ocaml/sdk-gen/java/autogen/xen-api/src/test/java/CustomDateDeserializerTest.java b/ocaml/sdk-gen/java/autogen/xen-api/src/test/java/CustomDateDeserializerTest.java new file mode 100644 index 00000000000..f125e1d1174 --- /dev/null +++ b/ocaml/sdk-gen/java/autogen/xen-api/src/test/java/CustomDateDeserializerTest.java @@ -0,0 +1,123 @@ +/* + * 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. + */ + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.xensource.xenapi.CustomDateDeserializer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomDateDeserializerTest { + + private static Stream provideDateStringsAndExpectedDates() { + Hashtable dates = new Hashtable<>(); + + // no dashes, no colons + dates.put("20220101T123045", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T123045Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T123045+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T123045+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T123045+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + + dates.put("20220101T123045.123", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T123045.123Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T123045.123+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T123045.123+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T123045.123+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + + // no dashes, with colons + dates.put("20220101T12:30:45", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T12:30:45Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T12:30:45+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T12:30:45+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T12:30:45+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + + dates.put("20220101T12:30:45.123", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T12:30:45.123Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("20220101T12:30:45.123+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T12:30:45.123+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("20220101T12:30:45.123+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + + // dashes and colons + dates.put("2022-01-01T12:30:45", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("2022-01-01T12:30:45Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("UTC"))); + dates.put("2022-01-01T12:30:45+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("2022-01-01T12:30:45+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + dates.put("2022-01-01T12:30:45+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 0, TimeZone.getTimeZone("GMT+03"))); + + dates.put("2022-01-01T12:30:45.123", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("2022-01-01T12:30:45.123Z", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("UTC"))); + dates.put("2022-01-01T12:30:45.123+03", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("2022-01-01T12:30:45.123+0300", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + dates.put("2022-01-01T12:30:45.123+03:00", createDate(2022, Calendar.JANUARY, 1, 12, 30, 45, 123, TimeZone.getTimeZone("GMT+03"))); + + + return dates.entrySet().stream() + .map(entry -> Arguments.of(entry.getKey(), entry.getValue())); + } + + private static Date createDate(int year, int month, int day, int hour, int minute, int seconds, int milliseconds, TimeZone timeZone) { + Calendar calendar = new GregorianCalendar(timeZone); + calendar.set(year, month, day, hour, minute, seconds); + calendar.set(Calendar.MILLISECOND, milliseconds); + return calendar.getTime(); + } + + private static ObjectMapper createObjectMapperWithCustomDeserializer() { + ObjectMapper mapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addDeserializer(Date.class, new CustomDateDeserializer()); + mapper.registerModule(module); + return mapper; + } + + @ParameterizedTest + @MethodSource("provideDateStringsAndExpectedDates") + public void shouldParseDateStringsCorrectlyWithCustomDeserializer(String dateString, Date expectedDate) throws Exception { + ObjectMapper mapper = createObjectMapperWithCustomDeserializer(); + + Date parsedDate = mapper.readValue("\"" + dateString + "\"", Date.class); + + SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z"); + String parsedDateString = outputFormat.format(parsedDate); + String expectedDateString = outputFormat.format(expectedDate); + + assertEquals(expectedDate, parsedDate, + () -> "Failed to parse datetime value: " + dateString + + ". Parsed date: " + parsedDateString + + ", expected: " + expectedDateString); + } +}