diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index 0275f6124..becaec934 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Autofac; using Moq; @@ -18,7 +19,7 @@ namespace Orchard.Framework.Tests.Localization { public class DefaultDateFormatterTests { [Test] - [Description("Date and time parsing works correctly for all combinations of months, hours, format strings and cultures.")] + [Description("Date/time parsing works correctly for all combinations of months, format strings and cultures.")] public void ParseDateTimeTest01() { var allCases = new ConcurrentBag(); var failedCases = new ConcurrentDictionary(); @@ -34,27 +35,78 @@ namespace Orchard.Framework.Tests.Localization { var container = InitializeContainer(culture.Name, "GregorianCalendar"); var formats = container.Resolve(); var target = container.Resolve(); + foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date and time formats supported by the culture. - var caseKey = String.Format("{0}:{1}", culture.Name, dateTimeFormat); - allCases.Add(caseKey); - Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); - try { - for (var month = 1; month <= 12; month++) { // All months in the year. - for (var hour = 0; hour <= 23; hour++) { // All hours in the day. - DateTime dateTime = new DateTime(1998, month, 1, hour, 30, 30); - // Print string using Gregorian calendar to avoid calendar conversion. - var cultureGregorian = (CultureInfo)culture.Clone(); - cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); - var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian); - var result = target.ParseDateTime(dateTimeString, dateTimeFormat); - var reference = DateTime.ParseExact(dateTimeString, dateTimeFormat, culture); - var expected = new DateTimeParts(reference.Year, reference.Month, reference.Day, reference.Hour, reference.Minute, reference.Second, reference.Millisecond); - Assert.AreEqual(expected, result); - } + for (var month = 1; month <= 12; month++) { // All months in the year. + + DateTime dateTime = new DateTime(1998, month, 1, 10, 30, 30); + + // Print string using Gregorian calendar to avoid calendar conversion. + var cultureGregorian = (CultureInfo)culture.Clone(); + cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); + var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian); + + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateTimeFormat, dateTimeString); + allCases.Add(caseKey); + //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + + try { + var result = target.ParseDateTime(dateTimeString, dateTimeFormat); + var expected = GetExpectedDateTimeParts(dateTime, dateTimeFormat); + Assert.AreEqual(expected, result); + } + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); } } - catch (Exception ex) { - failedCases.TryAdd(caseKey, ex); + } + }); + + if (failedCases.Count > maxFailedCases) { + throw new AggregateException(String.Format("Parse tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values); + } + } + + [Test] + [Description("Date/time parsing works correctly for all combinations of hours, format strings and cultures.")] + public void ParseDateTimeTest02() { + var allCases = new ConcurrentBag(); + var failedCases = new ConcurrentDictionary(); + var maxFailedCases = 0; + + var options = new ParallelOptions(); + if (Debugger.IsAttached) { + options.MaxDegreeOfParallelism = 1; + } + + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + Parallel.ForEach(allCultures, options, culture => { // All cultures on the machine. + var container = InitializeContainer(culture.Name, "GregorianCalendar"); + var formats = container.Resolve(); + var target = container.Resolve(); + + foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date and time formats supported by the culture. + foreach (var hour in new [] {0, 6, 12, 18}) { // Enough hours to cover all code paths (AM/PM, 12<->00, etc). + + DateTime dateTime = new DateTime(1998, 1, 1, hour, 30, 30); + + // Print string using Gregorian calendar to avoid calendar conversion. + var cultureGregorian = (CultureInfo)culture.Clone(); + cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); + var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian); + + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateTimeFormat, dateTimeString); + allCases.Add(caseKey); + //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + + try { + var result = target.ParseDateTime(dateTimeString, dateTimeFormat); + var expected = GetExpectedDateTimeParts(dateTime, dateTimeFormat); + Assert.AreEqual(expected, result); + } + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } } } }); @@ -81,16 +133,21 @@ namespace Orchard.Framework.Tests.Localization { var container = InitializeContainer(culture.Name, "GregorianCalendar"); var formats = container.Resolve(); var target = container.Resolve(); + foreach (var dateFormat in formats.AllDateFormats) { // All date formats supported by the culture. for (var month = 1; month <= 12; month++) { // All months in the year. + DateTime date = new DateTime(1998, month, 1); + // Print string using Gregorian calendar to avoid calendar conversion. var cultureGregorian = (CultureInfo)culture.Clone(); cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); var dateString = date.ToString(dateFormat, cultureGregorian); + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateFormat, dateString); allCases.Add(caseKey); //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + try { var result = target.ParseDate(dateString, dateFormat); var expected = GetExpectedDateParts(date, dateFormat); @@ -125,13 +182,17 @@ namespace Orchard.Framework.Tests.Localization { var container = InitializeContainer(culture.Name, null); var formats = container.Resolve(); var target = container.Resolve(); + foreach (var timeFormat in formats.AllTimeFormats) { // All time formats supported by the culture. for (var hour = 0; hour <= 23; hour++) { // All hours in the day. + DateTime time = new DateTime(1998, 1, 1, hour, 30, 30); var timeString = time.ToString(timeFormat, culture); + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, timeFormat, timeString); allCases.Add(caseKey); //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + try { var result = target.ParseTime(timeString, timeFormat); var expected = GetExpectedTimeParts(time, timeFormat); @@ -149,20 +210,29 @@ namespace Orchard.Framework.Tests.Localization { } } + private DateTimeParts GetExpectedDateTimeParts(DateTime dateTime, string format) { + return new DateTimeParts( + GetExpectedDateParts(dateTime, format), + GetExpectedTimeParts(dateTime, format) + ); + } + private DateParts GetExpectedDateParts(DateTime date, string format) { + var formatWithoutLiterals = Regex.Replace(format, @"((?[0-9]{1,2})"}, {"ss", "(?[0-9]{2})"}, {"s", "(?[0-9]{1,2})"}, - {"f", "(?[0-9]{1})"}, - {"ff", "(?[0-9]{2})"}, - {"fff", "(?[0-9]{3})"}, - {"ffff", "(?[0-9]{4})"}, - {"fffff", "(?[0-9]{5})"}, + {"fffffff", "(?[0-9]{7})"}, {"ffffff", "(?[0-9]{6})"}, - {"tt", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, - {"t", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, - {" tt", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, - {" t", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))} + {"fffff", "(?[0-9]{5})"}, + {"ffff", "(?[0-9]{4})"}, + {"fff", "(?[0-9]{3})"}, + {"ff", "(?[0-9]{2})"}, + {"f", "(?[0-9]{1})"}, + {"tt", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {"t", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {" tt", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {" t", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {"K", @"(?Z|(\+|-)[0-9]{2}:[0-9]{2})*"}, }; } @@ -316,13 +318,13 @@ namespace Orchard.Framework.Localization.Services { // Transform the / and : characters into culture-specific date and time separators. result = Regex.Replace(result, @"\/|:", m => m.Value == "/" ? _dateTimeFormatProvider.DateSeparator : _dateTimeFormatProvider.TimeSeparator); - + // Escape all characters that are intrinsic Regex syntax. result = EscapeForRegex(result); - + // Transform all literals to corresponding wildcard matches. result = Regex.Replace(result, @"(? String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); - + // Transform all DateTime format specifiers into corresponding Regex captures. result = result.ReplaceAll(replacements);