Skip to content

Commit 4736a98

Browse files
committed
wip
1 parent 0cdab18 commit 4736a98

8 files changed

Lines changed: 957 additions & 1 deletion

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"Bash(git diff:*)",
1313
"Bash(dotnet test:*)",
1414
"Bash(docker compose:*)",
15-
"Bash(dotnet run:*)"
15+
"Bash(dotnet run:*)",
16+
"Bash(pg_isready -h localhost -p 5432)"
1617
],
1718
"deny": [],
1819
"ask": []
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Cleipnir.ResilientFunctions.Storage;
6+
using Shouldly;
7+
8+
namespace Cleipnir.ResilientFunctions.Tests.TestTemplates;
9+
10+
public abstract class StateStoreTests
11+
{
12+
public abstract Task GetAndReadReturnsEmptyDictionaryWhenNoStateExists();
13+
protected async Task GetAndReadReturnsEmptyDictionaryWhenNoStateExists<TStateStore, TStoredState>(
14+
Task<TStateStore> storeTask,
15+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead
16+
)
17+
{
18+
var stateStore = await storeTask;
19+
var storedId = new StoredId(Guid.NewGuid());
20+
21+
var result = await getAndRead(stateStore, new[] { storedId });
22+
23+
result.ShouldNotBeNull();
24+
result.Count.ShouldBe(0);
25+
}
26+
27+
public abstract Task SetAndExecuteAndGetAndReadReturnsSingleStateSuccessfully();
28+
protected async Task SetAndExecuteAndGetAndReadReturnsSingleStateSuccessfully<TStateStore, TStoredState>(
29+
Task<TStateStore> storeTask,
30+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
31+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead,
32+
Func<StoredId, long, byte[], TStoredState> createState,
33+
Func<TStoredState, StoredId> getId,
34+
Func<TStoredState, long> getPosition,
35+
Func<TStoredState, byte[]> getContent
36+
)
37+
{
38+
var stateStore = await storeTask;
39+
var storedId = new StoredId(Guid.NewGuid());
40+
var content = new byte[] { 1, 2, 3, 4, 5 };
41+
var position = 0L;
42+
43+
// Insert state using SetAndExecute
44+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
45+
{
46+
[storedId] = new() { [position] = createState(storedId, position, content) }
47+
});
48+
49+
// Read state back using GetAndRead
50+
var result = await getAndRead(stateStore, new[] { storedId });
51+
52+
result.ShouldNotBeNull();
53+
result.Count.ShouldBe(1);
54+
result.ContainsKey(storedId).ShouldBeTrue();
55+
result[storedId].Count.ShouldBe(1);
56+
result[storedId].ContainsKey(position).ShouldBeTrue();
57+
58+
var storedState = result[storedId][position];
59+
getId(storedState).ShouldBe(storedId);
60+
getPosition(storedState).ShouldBe(position);
61+
getContent(storedState).ShouldBe(content);
62+
}
63+
64+
public abstract Task SetAndExecuteAndGetAndReadReturnsMultipleStatesForSingleId();
65+
protected async Task SetAndExecuteAndGetAndReadReturnsMultipleStatesForSingleId<TStateStore, TStoredState>(
66+
Task<TStateStore> storeTask,
67+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
68+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead,
69+
Func<StoredId, long, byte[], TStoredState> createState,
70+
Func<TStoredState, byte[]> getContent
71+
)
72+
{
73+
var stateStore = await storeTask;
74+
var storedId = new StoredId(Guid.NewGuid());
75+
var content1 = new byte[] { 1, 2, 3 };
76+
var content2 = new byte[] { 4, 5, 6 };
77+
var position1 = 0L;
78+
var position2 = 1L;
79+
80+
// Insert multiple states for the same ID using SetAndExecute
81+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
82+
{
83+
[storedId] = new()
84+
{
85+
[position1] = createState(storedId, position1, content1),
86+
[position2] = createState(storedId, position2, content2)
87+
}
88+
});
89+
90+
// Read states back using GetAndRead
91+
var result = await getAndRead(stateStore, new[] { storedId });
92+
93+
result.ShouldNotBeNull();
94+
result.Count.ShouldBe(1);
95+
result.ContainsKey(storedId).ShouldBeTrue();
96+
result[storedId].Count.ShouldBe(2);
97+
98+
getContent(result[storedId][position1]).ShouldBe(content1);
99+
getContent(result[storedId][position2]).ShouldBe(content2);
100+
}
101+
102+
public abstract Task SetAndExecuteAndGetAndReadReturnsMultipleIdsWithMultiplePositions();
103+
protected async Task SetAndExecuteAndGetAndReadReturnsMultipleIdsWithMultiplePositions<TStateStore, TStoredState>(
104+
Task<TStateStore> storeTask,
105+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
106+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead,
107+
Func<StoredId, long, byte[], TStoredState> createState,
108+
Func<TStoredState, byte[]> getContent
109+
)
110+
{
111+
var stateStore = await storeTask;
112+
var storedId1 = new StoredId(Guid.NewGuid());
113+
var storedId2 = new StoredId(Guid.NewGuid());
114+
var content1 = new byte[] { 1, 2, 3 };
115+
var content2 = new byte[] { 4, 5, 6 };
116+
var content3 = new byte[] { 7, 8, 9 };
117+
118+
// Insert states for multiple IDs using SetAndExecute
119+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
120+
{
121+
[storedId1] = new()
122+
{
123+
[0L] = createState(storedId1, 0L, content1),
124+
[1L] = createState(storedId1, 1L, content2)
125+
},
126+
[storedId2] = new()
127+
{
128+
[0L] = createState(storedId2, 0L, content3)
129+
}
130+
});
131+
132+
// Read states back using GetAndRead
133+
var result = await getAndRead(stateStore, new[] { storedId1, storedId2 });
134+
135+
result.ShouldNotBeNull();
136+
result.Count.ShouldBe(2);
137+
result.ContainsKey(storedId1).ShouldBeTrue();
138+
result.ContainsKey(storedId2).ShouldBeTrue();
139+
result[storedId1].Count.ShouldBe(2);
140+
result[storedId2].Count.ShouldBe(1);
141+
142+
getContent(result[storedId1][0L]).ShouldBe(content1);
143+
getContent(result[storedId1][1L]).ShouldBe(content2);
144+
getContent(result[storedId2][0L]).ShouldBe(content3);
145+
}
146+
147+
public abstract Task SetAndExecuteAndGetAndReadHandlesNullContent();
148+
protected async Task SetAndExecuteAndGetAndReadHandlesNullContent<TStateStore, TStoredState>(
149+
Task<TStateStore> storeTask,
150+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
151+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead,
152+
Func<StoredId, long, byte[], TStoredState> createState,
153+
Func<TStoredState, StoredId> getId,
154+
Func<TStoredState, long> getPosition,
155+
Func<TStoredState, byte[]> getContent
156+
)
157+
{
158+
var stateStore = await storeTask;
159+
var storedId = new StoredId(Guid.NewGuid());
160+
var position = 0L;
161+
162+
// Insert state with null content using SetAndExecute
163+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
164+
{
165+
[storedId] = new()
166+
{
167+
[position] = createState(storedId, position, null)
168+
}
169+
});
170+
171+
// Read state back using GetAndRead
172+
var result = await getAndRead(stateStore, new[] { storedId });
173+
174+
result.ShouldNotBeNull();
175+
result.Count.ShouldBe(1);
176+
result.ContainsKey(storedId).ShouldBeTrue();
177+
result[storedId].Count.ShouldBe(1);
178+
179+
var storedState = result[storedId][position];
180+
getId(storedState).ShouldBe(storedId);
181+
getPosition(storedState).ShouldBe(position);
182+
getContent(storedState).ShouldBeNull();
183+
}
184+
185+
public abstract Task SetAndExecuteUpdatesExistingState();
186+
protected async Task SetAndExecuteUpdatesExistingState<TStateStore, TStoredState>(
187+
Task<TStateStore> storeTask,
188+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
189+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead,
190+
Func<StoredId, long, byte[], TStoredState> createState,
191+
Func<TStoredState, byte[]> getContent
192+
)
193+
{
194+
var stateStore = await storeTask;
195+
var storedId = new StoredId(Guid.NewGuid());
196+
var position = 0L;
197+
var initialContent = new byte[] { 1, 2, 3 };
198+
var updatedContent = new byte[] { 4, 5, 6, 7 };
199+
200+
// Insert initial state
201+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
202+
{
203+
[storedId] = new()
204+
{
205+
[position] = createState(storedId, position, initialContent)
206+
}
207+
});
208+
209+
// Update the same state
210+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>
211+
{
212+
[storedId] = new()
213+
{
214+
[position] = createState(storedId, position, updatedContent)
215+
}
216+
});
217+
218+
// Read state back - should have updated content
219+
var result = await getAndRead(stateStore, new[] { storedId });
220+
221+
result.ShouldNotBeNull();
222+
result.Count.ShouldBe(1);
223+
result[storedId].Count.ShouldBe(1);
224+
getContent(result[storedId][position]).ShouldBe(updatedContent);
225+
}
226+
227+
public abstract Task SetAndExecuteHandlesEmptyDictionary();
228+
protected async Task SetAndExecuteHandlesEmptyDictionary<TStateStore, TStoredState>(
229+
Task<TStateStore> storeTask,
230+
Func<TStateStore, Dictionary<StoredId, Dictionary<long, TStoredState>>, Task> setAndExecute,
231+
Func<TStateStore, IReadOnlyList<StoredId>, Task<Dictionary<StoredId, Dictionary<long, TStoredState>>>> getAndRead
232+
)
233+
{
234+
var stateStore = await storeTask;
235+
236+
// Should not throw when called with empty dictionary
237+
await setAndExecute(stateStore, new Dictionary<StoredId, Dictionary<long, TStoredState>>());
238+
239+
// Verify no data was added
240+
var storedId = new StoredId(Guid.NewGuid());
241+
var result = await getAndRead(stateStore, new[] { storedId });
242+
243+
result.ShouldNotBeNull();
244+
result.Count.ShouldBe(0);
245+
}
246+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Threading.Tasks;
6+
using Cleipnir.ResilientFunctions.Storage;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using MySqlConnector;
9+
10+
namespace Cleipnir.ResilientFunctions.MariaDb.Tests;
11+
12+
[TestClass]
13+
public class StateStoreTests : Cleipnir.ResilientFunctions.Tests.TestTemplates.StateStoreTests
14+
{
15+
private static async Task<MariaDbStateStore> CreateAndInitializeStateStore(
16+
[CallerFilePath] string sourceFilePath = "",
17+
[CallerMemberName] string callMemberName = ""
18+
)
19+
{
20+
var sourceFileName = sourceFilePath
21+
.Split(new[] { "\\", "/" }, StringSplitOptions.None)
22+
.Last()
23+
.Replace(".cs", "")
24+
.Replace(".", "_");
25+
26+
var tablePrefix = "T" + Guid.NewGuid().ToString("N")[..25];
27+
var stateStore = new MariaDbStateStore(Sql.ConnectionString, tablePrefix);
28+
await stateStore.Initialize();
29+
30+
// Clean up any existing data
31+
await using var conn = new MySqlConnection(Sql.ConnectionString);
32+
await conn.OpenAsync();
33+
await using var cmd = new MySqlCommand($"TRUNCATE TABLE {tablePrefix}_state", conn);
34+
await cmd.ExecuteNonQueryAsync();
35+
36+
return stateStore;
37+
}
38+
39+
[TestMethod]
40+
public override Task GetAndReadReturnsEmptyDictionaryWhenNoStateExists()
41+
=> GetAndReadReturnsEmptyDictionaryWhenNoStateExists<MariaDbStateStore, MariaDbStateStore.StoredState>(
42+
CreateAndInitializeStateStore(),
43+
(store, ids) => store.GetAndRead(ids)
44+
);
45+
46+
[TestMethod]
47+
public override Task SetAndExecuteAndGetAndReadReturnsSingleStateSuccessfully()
48+
=> SetAndExecuteAndGetAndReadReturnsSingleStateSuccessfully<MariaDbStateStore, MariaDbStateStore.StoredState>(
49+
CreateAndInitializeStateStore(),
50+
(store, states) => store.SetAndExecute(states),
51+
(store, ids) => store.GetAndRead(ids),
52+
(id, pos, content) => new MariaDbStateStore.StoredState(id, pos, content),
53+
state => state.Id,
54+
state => state.Position,
55+
state => state.Content
56+
);
57+
58+
[TestMethod]
59+
public override Task SetAndExecuteAndGetAndReadReturnsMultipleStatesForSingleId()
60+
=> SetAndExecuteAndGetAndReadReturnsMultipleStatesForSingleId<MariaDbStateStore, MariaDbStateStore.StoredState>(
61+
CreateAndInitializeStateStore(),
62+
(store, states) => store.SetAndExecute(states),
63+
(store, ids) => store.GetAndRead(ids),
64+
(id, pos, content) => new MariaDbStateStore.StoredState(id, pos, content),
65+
state => state.Content
66+
);
67+
68+
[TestMethod]
69+
public override Task SetAndExecuteAndGetAndReadReturnsMultipleIdsWithMultiplePositions()
70+
=> SetAndExecuteAndGetAndReadReturnsMultipleIdsWithMultiplePositions<MariaDbStateStore, MariaDbStateStore.StoredState>(
71+
CreateAndInitializeStateStore(),
72+
(store, states) => store.SetAndExecute(states),
73+
(store, ids) => store.GetAndRead(ids),
74+
(id, pos, content) => new MariaDbStateStore.StoredState(id, pos, content),
75+
state => state.Content
76+
);
77+
78+
[TestMethod]
79+
public override Task SetAndExecuteAndGetAndReadHandlesNullContent()
80+
=> SetAndExecuteAndGetAndReadHandlesNullContent<MariaDbStateStore, MariaDbStateStore.StoredState>(
81+
CreateAndInitializeStateStore(),
82+
(store, states) => store.SetAndExecute(states),
83+
(store, ids) => store.GetAndRead(ids),
84+
(id, pos, content) => new MariaDbStateStore.StoredState(id, pos, content),
85+
state => state.Id,
86+
state => state.Position,
87+
state => state.Content
88+
);
89+
90+
[TestMethod]
91+
public override Task SetAndExecuteUpdatesExistingState()
92+
=> SetAndExecuteUpdatesExistingState<MariaDbStateStore, MariaDbStateStore.StoredState>(
93+
CreateAndInitializeStateStore(),
94+
(store, states) => store.SetAndExecute(states),
95+
(store, ids) => store.GetAndRead(ids),
96+
(id, pos, content) => new MariaDbStateStore.StoredState(id, pos, content),
97+
state => state.Content
98+
);
99+
100+
[TestMethod]
101+
public override Task SetAndExecuteHandlesEmptyDictionary()
102+
=> SetAndExecuteHandlesEmptyDictionary<MariaDbStateStore, MariaDbStateStore.StoredState>(
103+
CreateAndInitializeStateStore(),
104+
(store, states) => store.SetAndExecute(states),
105+
(store, ids) => store.GetAndRead(ids)
106+
);
107+
}

0 commit comments

Comments
 (0)