Skip to content

Commit c7ff6c2

Browse files
author
Scott Williams
committed
move code + add immutable stack
1 parent c69ebee commit c7ff6c2

7 files changed

Lines changed: 217 additions & 21 deletions

File tree

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,15 @@ namespace Tocsoft.DateTimeAbstractions
66
{
77
public static class Clock
88
{
9-
private static AsyncLocal<Stack<DateTimeProvider>> clockStack = new AsyncLocal<Stack<DateTimeProvider>>();
9+
private static AsyncLocal<ImmutableStack<DateTimeProvider>> clockStack = new AsyncLocal<ImmutableStack<DateTimeProvider>>();
1010

1111
public static DateTimeProvider DefaultProvider { get; set; } = new CurrentDateTimeProvider();
1212

13-
static Clock()
14-
{
15-
clockStack.Value = new Stack<DateTimeProvider>();
16-
}
17-
18-
private static DateTimeProvider Current
13+
public static DateTimeProvider CurrentProvider
1914
{
2015
get
2116
{
22-
if (clockStack.Value.Count > 0)
17+
if (!clockStack.Value.IsEmpty)
2318
{
2419
return clockStack.Value.Peek();
2520
}
@@ -32,18 +27,23 @@ private static DateTimeProvider Current
3227

3328
public static IDisposable Pin()
3429
{
35-
return Pin(new StaticDateTimeProvider(Now));
30+
return Pin(Now);
31+
}
32+
public static IDisposable Pin(DateTime date)
33+
{
34+
return Pin(new StaticDateTimeProvider(date));
3635
}
3736

3837
internal static IDisposable Pin(DateTimeProvider provider)
3938
{
40-
clockStack.Value.Push(provider);
39+
var stack = clockStack.Value ?? ImmutableStack.Create<DateTimeProvider>();
40+
clockStack.Value = stack.Push(provider);
4141
return new PopWhenDisposed();
4242
}
4343

4444
private static void Pop()
4545
{
46-
clockStack.Value.Pop();
46+
clockStack.Value = clockStack.Value.Pop();
4747
}
4848

4949
private sealed class PopWhenDisposed : IDisposable
@@ -62,6 +62,6 @@ public void Dispose()
6262
}
6363
}
6464

65-
public static DateTime Now => Current.Now();
65+
public static DateTime Now => CurrentProvider.Now();
6666
}
6767
}
File renamed without changes.
File renamed without changes.

Tocsoft.Clock/ImmutableStack.cs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
6+
namespace Tocsoft.DateTimeAbstractions
7+
{
8+
internal static class ImmutableStack
9+
{
10+
/// <summary>
11+
/// Returns an empty collection.
12+
/// </summary>
13+
/// <typeparam name="T">The type of items stored by the collection.</typeparam>
14+
/// <returns>The immutable collection.</returns>
15+
public static ImmutableStack<T> Create<T>()
16+
{
17+
return ImmutableStack<T>.Empty;
18+
}
19+
20+
/// <summary>
21+
/// Creates a new immutable collection prefilled with the specified item.
22+
/// </summary>
23+
/// <typeparam name="T">The type of items stored by the collection.</typeparam>
24+
/// <param name="item">The item to prepopulate.</param>
25+
/// <returns>The new immutable collection.</returns>
26+
public static ImmutableStack<T> Create<T>(T item)
27+
{
28+
return ImmutableStack<T>.Empty.Push(item);
29+
}
30+
31+
}
32+
internal class ImmutableStack<T>
33+
{
34+
private static readonly ImmutableStack<T> emptyField = new ImmutableStack<T>();
35+
private T head;
36+
private ImmutableStack<T> tail;
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="ImmutableStack{T}"/> class
40+
/// that acts as the empty stack.
41+
/// </summary>
42+
private ImmutableStack()
43+
{
44+
}
45+
46+
/// <summary>
47+
/// Initializes a new instance of the <see cref="ImmutableStack{T}"/> class.
48+
/// </summary>
49+
/// <param name="head">The head element on the stack.</param>
50+
/// <param name="tail">The rest of the elements on the stack.</param>
51+
private ImmutableStack(T head, ImmutableStack<T> tail)
52+
{
53+
this.head = head;
54+
this.tail = tail;
55+
}
56+
57+
/// <summary>
58+
/// Gets the empty stack, upon which all stacks are built.
59+
/// </summary>
60+
public static ImmutableStack<T> Empty
61+
{
62+
get
63+
{
64+
return emptyField;
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Gets the empty stack, upon which all stacks are built.
70+
/// </summary>
71+
public ImmutableStack<T> Clear()
72+
{
73+
return Empty;
74+
}
75+
76+
/// <summary>
77+
/// Gets a value indicating whether this instance is empty.
78+
/// </summary>
79+
/// <value>
80+
/// <c>true</c> if this instance is empty; otherwise, <c>false</c>.
81+
/// </value>
82+
public bool IsEmpty
83+
{
84+
get { return tail == null; }
85+
}
86+
87+
/// <summary>
88+
/// Gets the element on the top of the stack.
89+
/// </summary>
90+
/// <returns>
91+
/// The element on the top of the stack.
92+
/// </returns>
93+
/// <exception cref="InvalidOperationException">Thrown when the stack is empty.</exception>
94+
public T Peek()
95+
{
96+
if (this.IsEmpty)
97+
{
98+
throw new InvalidOperationException("Collection empty");
99+
}
100+
101+
return head;
102+
}
103+
104+
/// <summary>
105+
/// Pushes an element onto a stack and returns the new stack.
106+
/// </summary>
107+
/// <param name="value">The element to push onto the stack.</param>
108+
/// <returns>The new stack.</returns>
109+
public ImmutableStack<T> Push(T value)
110+
{
111+
return new ImmutableStack<T>(value, this);
112+
}
113+
114+
/// <summary>
115+
/// Returns a stack that lacks the top element on this stack.
116+
/// </summary>
117+
/// <returns>A stack; never <c>null</c></returns>
118+
/// <exception cref="InvalidOperationException">Thrown when the stack is empty.</exception>
119+
public ImmutableStack<T> Pop()
120+
{
121+
if (this.IsEmpty)
122+
{
123+
throw new InvalidOperationException("Collection empty");
124+
}
125+
126+
return tail;
127+
}
128+
129+
/// <summary>
130+
/// Pops the top element off the stack.
131+
/// </summary>
132+
/// <param name="value">The value that was removed from the stack.</param>
133+
/// <returns>
134+
/// A stack; never <c>null</c>
135+
/// </returns>
136+
public ImmutableStack<T> Pop(out T value)
137+
{
138+
value = this.Peek();
139+
return this.Pop();
140+
}
141+
142+
}
143+
}
File renamed without changes.
File renamed without changes.

Tocsoft.DateTimeAbstractions.Tests/AsyncScoppedClock.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,81 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Threading.Tasks;
34
using Xunit;
45

56
namespace Tocsoft.DateTimeAbstractions.Tests
67
{
78
public class AsyncScoppedClock
89
{
9-
[Fact]
10-
public async Task ConcurrentAsyncThreadsReadingSamePinnedTimeAsync()
10+
[Theory]
11+
[InlineData(1)]
12+
[InlineData(100)]
13+
public async Task ConcurrentAsyncThreadsReadingSamePinnedTimeAsync(int concurrentCount)
14+
{
15+
List<Task> tasks = new List<Task>();
16+
for (var i = 0; i < concurrentCount; i++)
17+
{
18+
var task = RunTest(i);
19+
tasks.Add(task);
20+
}
21+
22+
await Task.WhenAll(tasks);
23+
}
24+
25+
[Theory]
26+
[InlineData(1)]
27+
[InlineData(100)]
28+
public async Task ConcurrentAsyncThreadsReadingSamePinnedTimeAsyncMultithread(int concurrentCount)
29+
{
30+
List<Task> tasks = new List<Task>();
31+
for (var i = 0; i < concurrentCount; i++)
32+
{
33+
var task = RunTestForceThread(i);
34+
tasks.Add(task);
35+
}
36+
37+
await Task.WhenAll(tasks);
38+
}
39+
40+
private Task RunTestForceThread(int count)
41+
{
42+
return Task.Run(async () =>
43+
{
44+
await RunTest(count);
45+
});
46+
}
47+
48+
private async Task RunTest(int count)
1149
{
1250
var date = new DateTime(2000, 01, 01);
51+
date = date.AddDays(count);
1352
using (Clock.Pin(new StaticDateTimeProvider(date)))
1453
{
15-
var task1 = DelayedNow();
16-
var task2 = DelayedNow();
17-
var dates = await Task.WhenAll(task1, task2);
54+
var task1 = DelayedNow(true);
55+
var task2 = DelayedNow(false);
56+
var task3 = DelayedNow(true);
57+
var dates = await Task.WhenAll(task1, task2, task3);
1858
Assert.All(dates, x =>
1959
{
2060
Assert.Equal(date, x);
2161
});
2262
}
2363
// we move away from the pinned after the using statement
2464
Assert.NotEqual(date, Clock.Now);
65+
66+
Clock.DefaultProvider = new UtcCurrentDateTimeProvider();
67+
68+
using (Clock.Pin(new StaticDateTimeProvider(date)))
69+
{
70+
var task1 = DelayedNow(true);
71+
var task2 = DelayedNow(false);
72+
var task3 = DelayedNow(true);
73+
var dates = await Task.WhenAll(task1, task2, task3);
74+
Assert.All(dates, x =>
75+
{
76+
Assert.Equal(date, x);
77+
});
78+
}
2579
}
2680

2781
[Fact]
@@ -44,11 +98,10 @@ public async Task PinSubContext()
4498
// we move away from the pinned after the using statement
4599
Assert.NotEqual(date, Clock.Now);
46100
}
47-
48-
49-
public async Task<DateTime> DelayedNow()
101+
102+
public async Task<DateTime> DelayedNow(bool continueOnCapturedContext)
50103
{
51-
await Task.Delay(1); // to force a propert delay
104+
await Task.Delay(1).ConfigureAwait(continueOnCapturedContext); // to force a propert delay
52105
return Clock.Now;
53106
}
54107

0 commit comments

Comments
 (0)