Skip to content

Commit

Permalink
Implement timeout for recurrence handling
Browse files Browse the repository at this point in the history
Modified Constants.cs:
- Deprecated `RecurrenceRestrictionType` and `RecurrenceEvaluationModeType` enums.

Updated RecurrencePattern.cs:
- Deprecated `RestrictionType` and `EvaluationMode` properties.
- Set default for `RestrictionType`.

Enhanced RecurrenceUtil:
- Added new using directives.
- Introduced `DefaultTimeout`.
- Updated `GetOccurrences` with timeout logic.

Refined RecurrencePatternEvaluator:
- Removed `_maxIncrementCount` with connected logic in favor of `RecurrenceUtil.DefaultTimeout`
- Improved XML documentation comments.

Updated Calendar.cs:
- Simplified `Load` method.
- Deprecated certain properties and methods associated with `RestrictionType` and `EvaluationMode`
- Removed obsolete `Evaluate` method.

Enhanced RecurrenceTests.cs with more robust test cases:
- Replaced `Assert.That` with `Assert.Multiple`.
- Changed assertion for occurrences count.
- Renamed test methods for clarity.
- Added timeout settings and updated expected exceptions.
- Adjusted test data and added comments.

Corrected typos and improved formatting for better readability.

Fixes ical-org#622
  • Loading branch information
axunonb committed Oct 27, 2024
1 parent 52b34e2 commit 02f08af
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 248 deletions.
147 changes: 73 additions & 74 deletions Ical.Net.Tests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,30 @@ int eventIndex
.OrderBy(o => o.Period.StartTime)
.ToList();

Assert.That(
occurrences,
Has.Count.EqualTo(dateTimes.Length),
"There should be exactly " + dateTimes.Length + " occurrences; there were " + occurrences.Count);

if (evt.RecurrenceRules.Count > 0)
Assert.Multiple(() =>
{
Assert.That(evt.RecurrenceRules, Has.Count.EqualTo(1));
}
Assert.That(
occurrences,
Has.Count.GreaterThanOrEqualTo(dateTimes.Length),
"There should have " + dateTimes.Length + " or more occurrences; there were " + occurrences.Count);

for (var i = 0; i < dateTimes.Length; i++)
{
// Associate each incoming date/time with the calendar.
dateTimes[i].AssociatedObject = cal;
if (evt.RecurrenceRules.Count > 0)
{
Assert.That(evt.RecurrenceRules, Has.Count.EqualTo(1));
}

for (var i = 0; i < dateTimes.Length; i++)
{
// Associate each incoming date/time with the calendar.
dateTimes[i].AssociatedObject = cal;

var dt = dateTimes[i];
Assert.That(occurrences[i].Period.StartTime, Is.EqualTo(dt), "Event should occur on " + dt);
if (timeZones != null)
Assert.That(dt.TimeZoneName, Is.EqualTo(timeZones[i]), "Event " + dt + " should occur in the " + timeZones[i] + " timezone");
}
var dt = dateTimes[i];
Assert.That(occurrences[i].Period.StartTime, Is.EqualTo(dt), "Event should occur on " + dt);
if (timeZones != null)
Assert.That(dt.TimeZoneName, Is.EqualTo(timeZones[i]),
"Event " + dt + " should occur in the " + timeZones[i] + " timezone");
}
});
}

private void EventOccurrenceTest(
Expand Down Expand Up @@ -1868,27 +1872,26 @@ public void Bug1741093()
}

/// <summary>
/// Ensures that, by default, SECONDLY recurrence rules are not allowed.
/// Too many SECONDLY recurrences should time out.
/// </summary>
[Test, Category("Recurrence")]
public void Secondly1()
public void Secondly_HighNumberOfOccurrences_ShouldTimeout()
{
RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout
Assert.That(() =>
{
var iCal = Calendar.Load(IcsFiles.Secondly1);
_ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid));
}, Throws.Exception.TypeOf<ArgumentException>(), "Evaluation engine should have failed.");
}, Throws.Exception.TypeOf<TimeoutException>(), "Too many occurrences should timeout.");
}

/// <summary>
/// Ensures that the proper behavior occurs when the evaluation
/// mode is set to adjust automatically for SECONDLY evaluation
/// At least a few SECONDLY occurrences are generated without TimeoutException.
/// </summary>
[Test, Category("Recurrence")]
public void Secondly1_1()
public void Secondly_LowNumberOfOccurrences_ShouldSucceed()
{
var iCal = Calendar.Load(IcsFiles.Secondly1);
iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically;

EventOccurrenceTest(
iCal,
Expand All @@ -1897,47 +1900,45 @@ public void Secondly1_1()
new[]
{
new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 1, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 2, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 3, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 4, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 5, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 6, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 7, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 8, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 9, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 10, 0, _tzid)
new CalDateTime(2007, 6, 21, 8, 0, 1, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 2, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 3, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 4, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 5, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 6, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 7, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 8, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 9, _tzid),
new CalDateTime(2007, 6, 21, 8, 0, 10, _tzid)
},
null
);
}

/// <summary>
/// Ensures that if configured, MINUTELY recurrence rules are not allowed.
/// Too many MINUTELY occurrences time out.
/// </summary>
[Test, Category("Recurrence")]
public void Minutely1()
public void Minutely_HighNumberOfOccurrences_ShouldTimeout()
{
RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout
Assert.That(() =>
{
var iCal = Calendar.Load(IcsFiles.Minutely1);
iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictMinutely;
var occurrences = iCal.GetOccurrences(
var evt = iCal.Events.First();
var occurrences = evt.GetOccurrences(
new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid),
new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid));
}, Throws.Exception.TypeOf<ArgumentException>());
}, Throws.Exception.TypeOf<TimeoutException>(), "Too many occurrences should timeout.");
}

/// <summary>
/// Ensures that the proper behavior occurs when the evaluation
/// mode is set to adjust automatically for MINUTELY evaluation
/// At least a few MINUTELY occurrences are generated without TimeoutException.
/// </summary>
[Test, Category("Recurrence")]
public void Minutely1_1()
public void Minutely_LowNumberOfOccurrences_ShouldSucceed()
{
var iCal = Calendar.Load(IcsFiles.Minutely1);
iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictMinutely;
iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically;

EventOccurrenceTest(
iCal,
Expand All @@ -1946,40 +1947,38 @@ public void Minutely1_1()
new[]
{
new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 9, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 10, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 11, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 12, 0, 0, _tzid)
new CalDateTime(2007, 6, 21, 8, 1, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 2, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 3, 0, _tzid),
new CalDateTime(2007, 6, 21, 8, 4, 0, _tzid)
},
null
);
}

/// <summary>
/// Ensures that if configured, HOURLY recurrence rules are not allowed.
/// Too many HOURLY occurrences time out.
/// </summary>
[Test, Category("Recurrence")/*, ExpectedException(typeof(EvaluationEngineException))*/]
public void Hourly1()
[Test, Category("Recurrence")]
public void Hourly_HighNumberOfOccurrences_ShouldTimeout()
{
RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout
Assert.That(() =>
{
{
var iCal = Calendar.Load(IcsFiles.Hourly1);
iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictHourly;
_ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid));

}, Throws.Exception.TypeOf<ArgumentException>());
_ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid),
new CalDateTime(2013, 7, 21, 8, 0, 0, _tzid));
},
Throws.Exception.TypeOf<TimeoutException>(), "Too many occurrences should timeout.");
}

/// <summary>
/// Ensures that the proper behavior occurs when the evaluation
/// mode is set to adjust automatically for HOURLY evaluation
/// At least a few HOURLY occurrences are generated without TimeoutException.
/// </summary>
[Test, Category("Recurrence")]
public void Hourly1_1()
public void Hourly_LowNumberOfOccurrences_ShouldSucceed()
{
var iCal = Calendar.Load(IcsFiles.Hourly1);
iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictHourly;
iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically;

EventOccurrenceTest(
iCal,
Expand All @@ -1988,10 +1987,10 @@ public void Hourly1_1()
new[]
{
new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 22, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 23, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 24, 8, 0, 0, _tzid),
new CalDateTime(2007, 6, 25, 8, 0, 0, _tzid)
new CalDateTime(2007, 6, 21, 9, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 10, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 11, 0, 0, _tzid),
new CalDateTime(2007, 6, 21, 12, 0, 0, _tzid)
},
null
);
Expand Down Expand Up @@ -2038,7 +2037,7 @@ public void YearlyInterval1()
}

/// <summary>
/// Ensures that "off-day" calcuation works correctly
/// Ensures that "off-day" calculation works correctly
/// </summary>
[Test, Category("Recurrence")]
public void DailyInterval1()
Expand Down Expand Up @@ -2841,10 +2840,7 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime)

// This case (DTSTART of type DATE and FREQ=MINUTELY) is undefined in RFC 5545.
// ical.net handles the case by pretending DTSTART has the time set to midnight.
evt.RecurrenceRules.Add(new RecurrencePattern($"FREQ={freq};INTERVAL=10;COUNT=5")
{
RestrictionType = RecurrenceRestrictionType.NoRestriction,
});
evt.RecurrenceRules.Add(new RecurrencePattern($"FREQ={freq};INTERVAL=10;COUNT=5"));

var occurrences = evt.GetOccurrences(CalDateTime.Today.AddDays(-1), CalDateTime.Today.AddDays(100))
.OrderBy(x => x)
Expand All @@ -2868,8 +2864,7 @@ public void RecurrencePattern1()
{
// NOTE: evaluators are not generally meant to be used directly like this.
// However, this does make a good test to ensure they behave as they should.
RecurrencePattern pattern = new RecurrencePattern("FREQ=SECONDLY;INTERVAL=10");
pattern.RestrictionType = RecurrenceRestrictionType.NoRestriction;
var pattern = new RecurrencePattern("FREQ=SECONDLY;INTERVAL=10");

var us = new CultureInfo("en-US");

Expand Down Expand Up @@ -3177,10 +3172,14 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange()
Assert.That(occurrences.Last().StartTime.Equals(lastExpected), Is.True);
}

[Test, Ignore("Turn on in v3")]
/// <summary>
/// Evaluate relevancy and validity of the request.
/// Find a solution for issue #120 or close forever
/// </summary>
[Test, Ignore("Turn on in v3", Until = "2024-12-31")]
public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet()
{
//https://github.com/rianjs/ical.net/issues/120
//https://github.com/rianjs/ical.net/issues/120 dated Sep 5, 2016
const string ical =
@"BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
Expand Down
72 changes: 23 additions & 49 deletions Ical.Net/Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static Calendar Load(Stream s)
=> CalendarCollection.Load(new StreamReader(s, Encoding.UTF8)).SingleOrDefault();

public static Calendar Load(TextReader tr)
=> CalendarCollection.Load(tr).OfType<Calendar>().SingleOrDefault();
=> CalendarCollection.Load(tr)?.SingleOrDefault();

public static IList<T> Load<T>(Stream s, Encoding e)
=> Load<T>(new StreamReader(s, e));
Expand Down Expand Up @@ -118,7 +118,7 @@ public override int GetHashCode()
public virtual IEnumerable<IRecurrable> RecurringItems => Children.OfType<IRecurrable>();

/// <summary>
/// A collection of <see cref="Components.Event"/> components in the iCalendar.
/// A collection of <see cref="CalendarEvent"/> components in the iCalendar.
/// </summary>
public virtual IUniqueComponentList<CalendarEvent> Events => _mEvents;

Expand Down Expand Up @@ -166,12 +166,14 @@ public virtual string Method
set => Properties.Set("METHOD", value);
}

[Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)]
public virtual RecurrenceRestrictionType RecurrenceRestriction
{
get => Properties.Get<RecurrenceRestrictionType>("X-DDAY-ICAL-RECURRENCE-RESTRICTION");
set => Properties.Set("X-DDAY-ICAL-RECURRENCE-RESTRICTION", value);
}

[Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)]
public virtual RecurrenceEvaluationModeType RecurrenceEvaluationMode
{
get => Properties.Get<RecurrenceEvaluationModeType>("X-DDAY-ICAL-RECURRENCE-EVALUATION-MODE");
Expand All @@ -190,35 +192,7 @@ public VTimeZone AddTimeZone(VTimeZone tz)
return tz;
}

/// <summary>
/// Evaluates component recurrences for the given range of time.
/// <example>
/// For example, if you are displaying a month-view for January 2007,
/// you would want to evaluate recurrences for Jan. 1, 2007 to Jan. 31, 2007
/// to display relevant information for those dates.
/// </example>
/// </summary>
/// <param name="fromDate">The beginning date/time of the range to test.</param>
/// <param name="toDate">The end date/time of the range to test.</param>
[Obsolete("This method is no longer supported. Use GetOccurrences() instead.")]
public void Evaluate(IDateTime fromDate, IDateTime toDate)
{
throw new NotSupportedException("Evaluate() is no longer supported as a public method. Use GetOccurrences() instead.");
}

/// <summary>
/// Evaluates component recurrences for the given range of time, for
/// the type of recurring component specified.
/// </summary>
/// <typeparam name="T">The type of component to be evaluated for recurrences.</typeparam>
/// <param name="fromDate">The beginning date/time of the range to test.</param>
/// <param name="toDate">The end date/time of the range to test.</param>
[Obsolete("This method is no longer supported. Use GetOccurrences() instead.")]
public void Evaluate<T>(IDateTime fromDate, IDateTime toDate)
{
throw new NotSupportedException("Evaluate() is no longer supported as a public method. Use GetOccurrences() instead.");
}


/// <summary>
/// Clears recurrence evaluations for recurring components.
/// </summary>
Expand Down Expand Up @@ -273,6 +247,9 @@ public virtual HashSet<Occurrence> GetOccurrences<T>(IDateTime dt) where T : IRe
public virtual HashSet<Occurrence> GetOccurrences<T>(DateTime dt) where T : IRecurringComponent
=> GetOccurrences<T>(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddTicks(-1)));

public virtual HashSet<Occurrence> GetOccurrences<T>(DateTime startTime, DateTime endTime) where T : IRecurringComponent
=> GetOccurrences<T>(new CalDateTime(startTime), new CalDateTime(endTime));

/// <summary>
/// Returns all occurrences of components of type T that start within the date range provided.
/// All components occurring between <paramref name="startTime"/> and <paramref name="endTime"/>
Expand All @@ -282,24 +259,21 @@ public virtual HashSet<Occurrence> GetOccurrences<T>(DateTime dt) where T : IRec
/// <param name="endTime">The ending date range</param>
public virtual HashSet<Occurrence> GetOccurrences<T>(IDateTime startTime, IDateTime endTime) where T : IRecurringComponent
{
var occurrences = new HashSet<Occurrence>(RecurringItems
.OfType<T>()
.SelectMany(recurrable => recurrable.GetOccurrences(startTime, endTime)));

var removeOccurrencesQuery = occurrences
.Where(o => o.Source is UniqueComponent)
.GroupBy(o => ((UniqueComponent)o.Source).Uid)
.SelectMany(group => group
.Where(o => o.Source.RecurrenceId != null)
.SelectMany(occurrence => group.
Where(o => o.Source.RecurrenceId == null && occurrence.Source.RecurrenceId.Date.Equals(o.Period.StartTime.Date))));

occurrences.ExceptWith(removeOccurrencesQuery);
return occurrences;
}

public virtual HashSet<Occurrence> GetOccurrences<T>(DateTime startTime, DateTime endTime) where T : IRecurringComponent
=> GetOccurrences<T>(new CalDateTime(startTime), new CalDateTime(endTime));
var occurrences = new HashSet<Occurrence>(RecurringItems
.OfType<T>()
.SelectMany(recurrable => recurrable.GetOccurrences(startTime, endTime)));

var removeOccurrencesQuery = occurrences
.Where(o => o.Source is UniqueComponent)
.GroupBy(o => ((UniqueComponent)o.Source).Uid)
.SelectMany(group => group
.Where(o => o.Source.RecurrenceId != null)
.SelectMany(occurrence => group.
Where(o => o.Source.RecurrenceId == null && occurrence.Source.RecurrenceId.Date.Equals(o.Period.StartTime.Date))));

occurrences.ExceptWith(removeOccurrencesQuery);
return occurrences;
}

/// <summary>
/// Creates a typed object that is a direct child of the iCalendar itself. Generally,
Expand Down
Loading

0 comments on commit 02f08af

Please sign in to comment.