Skip to content

Commit 448476a

Browse files
committed
Improve test coverage. Reduce unneeded work
1 parent 96b8a9b commit 448476a

131 files changed

Lines changed: 33291 additions & 32109 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ using (var connection = new SqlConnection(connectionString))
6464
// Delete
6565
connection.Delete(customer);
6666

67-
// Bulk Operations
67+
// Batch Operations
6868
var customers = GetLargeDataset();
69-
connection.BulkInsert(customers); // Sets identity values automatically
69+
connection.InsertAll(customers); // Sets identity values automatically
7070
}
7171
```
7272

@@ -103,7 +103,7 @@ The fluent LINQ-style API is available for:
103103
- [PostgreSQL](https://www.nuget.org/packages/AmpScm.RepoDb.PostgreSql)
104104
- [Oracle](https://www.nuget.org/packages/AmpScm.RepoDb.Oracle)
105105

106-
Operations: `Query`, `Insert`, `Update`, `Delete`, `Merge`, `BulkInsert`, `BulkUpdate`, `BulkDelete`, `BulkMerge`
106+
Operations: `Query`, `Insert`, `Update`, `Merge`, `Delete`, `Truncate`, `Exists`, `Average`, `Sum`, etc. etc.
107107

108108
## Advanced Features
109109

@@ -113,7 +113,7 @@ RepoDb provides advanced type conversion between databases:
113113

114114
- **Automatic Conversion** – Smart type mapping with AOT-compiled expressions
115115
- **Property Handlers** – Custom Get/Set transformation logic for specialized types
116-
- **Modern .NET Support** – Built-in support for `DateOnly`, `TimeOnly`, enums with NULL handling
116+
- **Modern .NET Support** – Built-in support for `DateOnly`, `TimeOnly`, enums and `Nullable` NULL handling on all value types.
117117
- **IParsable/IFormattable** – Automatic handling of types implementing these interfaces
118118
- **Cross-Database Testing** – SQLite works as a drop-in replacement for other databases during testing
119119

@@ -269,7 +269,7 @@ And also, thanks to these awesome OSS projects.
269269
* [SharpLab](https://sharplab.io/) - for helping us on our IL coding.
270270
* [Shields](https://shields.io/) - for the awesome badges.
271271
* [StackEdit](https://stackedit.io) - for being the markdown file editor.
272-
* [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite/), [System.Data.SQLite.Core](https://www.nuget.org/packages/System.Data.SQLite.Core/), [MySql.Data](https://www.nuget.org/packages/MySql.Data/), [MySqlConnector](https://www.nuget.org/packages/MySqlConnector/), [Npgsql](https://www.nuget.org/packages/Npgsql/) - for being the extended DB provider drivers.
272+
* [Microsoft.Data.SqlClient](https://www.nuget.org/packages/Microsoft.Data.SqlClient/), [Microsoft.Data.Sqlite](https://www.nuget.org/packages/Microsoft.Data.Sqlite/), [System.Data.SQLite.Core](https://www.nuget.org/packages/System.Data.SQLite.Core/), [MySql.Data](https://www.nuget.org/packages/MySql.Data/), [MySqlConnector](https://www.nuget.org/packages/MySqlConnector/), [Npgsql](https://www.nuget.org/packages/Npgsql/), [Oracle.ManagedDataAccess](https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core) - for being the extended DB provider drivers.
273273

274274
## License
275275

RepoDb.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<Project Path="src/RepoDb/RepoDb.csproj" />
1515
</Folder>
1616
<Folder Name="/Tests/">
17+
<Project Path="src/RepoDb.Core.DefinitionTests/RepoDb.Core.DefinitionTests.csproj" />
1718
<Project Path="src/RepoDb.Core.IntegrationTests/RepoDb.Core.IntegrationTests.csproj" />
1819
<Project Path="src/RepoDb.Core.UnitTests/RepoDb.Core.UnitTests.csproj" />
1920
<Project Path="src/RepoDb.MySql.IntegrationTests/RepoDb.MySql.IntegrationTests.csproj" />

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ services:
66
- 127.0.0.1:43306:3306
77
environment:
88
MYSQL_ROOT_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
9+
restart: no
910

1011
oracle:
1112
image: gvenzl/oracle-free:slim-faststart
@@ -16,13 +17,15 @@ services:
1617
ORACLE_PASSWORD: "ddd53e85-b15e-4da8"
1718
ORACLE_ALLOW_REMOTE: "true"
1819
shm_size: '1gb'
20+
restart: no
1921

2022
postgresql:
2123
image: pgvector/pgvector:pg18
2224
ports:
2325
- 127.0.0.1:45432:5432
2426
environment:
2527
POSTGRES_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
28+
restart: no
2629

2730
sqlserver:
2831
image: mcr.microsoft.com/mssql/server:2025-latest
@@ -31,3 +34,4 @@ services:
3134
environment:
3235
ACCEPT_EULA: true
3336
MSSQL_SA_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
37+
restart: no

older/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ services:
66
- 127.0.0.1:43306:3306
77
environment:
88
MYSQL_ROOT_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
9+
restart: no
910

1011
oracle:
1112
image: gvenzl/oracle-xe:18
@@ -16,13 +17,15 @@ services:
1617
ORACLE_PASSWORD: ddd53e85-b15e-4da8
1718
ORACLE_ALLOW_REMOTE: true
1819
shm_size: '1gb'
20+
restart: no
1921

2022
postgresql:
2123
image: postgres:14-alpine
2224
ports:
2325
- 127.0.0.1:45432:5432
2426
environment:
2527
POSTGRES_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
28+
restart: no
2629

2730
sqlserver:
2831
image: mcr.microsoft.com/mssql/server:2019-latest
@@ -31,3 +34,4 @@ services:
3134
environment:
3235
ACCEPT_EULA: true
3336
MSSQL_SA_PASSWORD: ddd53e85-b15e-4da8-91e5-a7d3b00a0ab2
37+
restart: no
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data.Common;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using System.Runtime.CompilerServices;
7+
using System.Text;
8+
using Microsoft.Data.SqlClient;
9+
using RepoDb.Enumerations;
10+
using RepoDb.Extensions;
11+
using RepoDb.IntegrationTests;
12+
using RepoDb.IntegrationTests.Models;
13+
using RepoDb.IntegrationTests.Setup;
14+
using RepoDb.Trace;
15+
16+
namespace RepoDb.IntegrationTests.Operations;
17+
18+
[TestClass]
19+
public class CallThemAllTests : TestBase
20+
{
21+
protected override void InitializeCore()
22+
{
23+
base.InitializeCore();
24+
Database.Cleanup();
25+
26+
using var db = CreateOpenConnection();
27+
db.Insert(new IdentityTable());
28+
}
29+
public static IEnumerable<MethodInfo> AllDbOperations = typeof(DbConnectionExtension).GetMethods()
30+
.Where(x => x.IsPublic && x.IsStatic)
31+
.Where(x => x.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(DbConnection)));
32+
33+
34+
public static IEnumerable<object[]> AllDbOperationsNonAsync =>
35+
AllDbOperations.Where(x => x.Name.EndsWith("Async") == false)
36+
.Select(x => new[] { x });
37+
38+
public static IEnumerable<object[]> AllDbOperationsAsync =>
39+
AllDbOperations.Where(x => x.Name.EndsWith("Async") == true)
40+
.Select(x => new[] { x });
41+
42+
public static string GetDisplayName(MethodInfo call, object[] info)
43+
{
44+
var mi = (MethodInfo)info[0];
45+
return $"{call.Name} {mi.Name}({string.Join(", ", mi.GetParameters().Select(x => x.ParameterType))})";
46+
}
47+
48+
[TestMethod, DynamicData(nameof(AllDbOperationsNonAsync), DynamicDataDisplayName = nameof(GetDisplayName))]
49+
public void DbConnectionOperation(MethodInfo mi)
50+
{
51+
using var db = mi.Name.StartsWith("Truncate") ? CreateOpenConnection() : CreateOpenLimitedConnection();
52+
var parameters = mi.GetParameters();
53+
var args = new object?[parameters.Length];
54+
mi = SetupCall(mi, db, ref parameters, args);
55+
56+
var r = mi.Invoke(null, args);
57+
58+
if (r is System.Collections.IEnumerable enumerable)
59+
{
60+
foreach (var i in enumerable)
61+
{
62+
GC.KeepAlive(i);
63+
}
64+
}
65+
}
66+
67+
68+
[TestMethod, DynamicData(nameof(AllDbOperationsAsync), DynamicDataDisplayName = nameof(GetDisplayName))]
69+
public async Task DbConnectionOperationAsync(MethodInfo mi)
70+
{
71+
using var db = mi.Name.StartsWith("Truncate") ? await CreateOpenConnectionAsync() : await CreateOpenLimitedConnectionAsync();
72+
73+
var parameters = mi.GetParameters();
74+
var args = new object?[parameters.Length];
75+
mi = SetupCall(mi, db, ref parameters, args);
76+
77+
var r = mi.Invoke(null, args);
78+
79+
if (r is not Task tsk)
80+
{
81+
var type = r.GetType();
82+
if (type.GetInterfaces().FirstOrDefault(x=>x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) is { } enumType)
83+
{
84+
tsk = (Task)typeof(CallThemAllTests).GetMethod(nameof(Walk)).MakeGenericMethod(enumType.GetGenericArguments()).Invoke(this, [r]);
85+
}
86+
else
87+
tsk = (Task)type.GetMethod("AsTask").Invoke(r, []);
88+
}
89+
await tsk;
90+
}
91+
92+
public async Task Walk<T>(IAsyncEnumerable<T> v)
93+
{
94+
await foreach(var i in v)
95+
{
96+
GC.KeepAlive(i);
97+
}
98+
}
99+
100+
101+
public static IEnumerable<MethodInfo> AllRepoOperations = typeof(DbRepository<SqlConnection>).GetMethods()
102+
.Where(x=>x.IsPublic && !x.IsStatic);
103+
104+
public static IEnumerable<object[]> AllRepoOperationsNonAync =>
105+
AllRepoOperations.Where(x => x.Name.EndsWith("Async") == false)
106+
.Select(x => new[] { x });
107+
108+
public static IEnumerable<object[]> AllRepoOperationsAsync =>
109+
AllRepoOperations.Where(x => x.Name.EndsWith("Async") == true)
110+
.Select(x => new[] { x });
111+
112+
113+
[TestMethod, DynamicData(nameof(AllRepoOperationsNonAync), DynamicDataDisplayName = nameof(GetDisplayName))]
114+
public void RepoConnectionOperation(MethodInfo mi)
115+
{
116+
using var repo = new DbRepository<SqlConnection>(mi.Name.StartsWith("Truncate") ? DbInstance.ConnectionString : DbInstance.LimitedConnectionString);
117+
var parameters = mi.GetParameters();
118+
var args = new object?[parameters.Length];
119+
mi = SetupCall(mi, null, ref parameters, args);
120+
121+
mi.Invoke(repo, args);
122+
}
123+
124+
125+
[TestMethod, DynamicData(nameof(AllRepoOperationsAsync), DynamicDataDisplayName = nameof(GetDisplayName))]
126+
public async Task RepoConnectionOperationAsync(MethodInfo mi)
127+
{
128+
using var repo = new DbRepository<SqlConnection>(mi.Name.StartsWith("Truncate") ? DbInstance.ConnectionString : DbInstance.LimitedConnectionString);
129+
var parameters = mi.GetParameters();
130+
var args = new object?[parameters.Length];
131+
mi = SetupCall(mi, null, ref parameters, args);
132+
133+
134+
var r = mi.Invoke(repo, args);
135+
136+
if (r is not Task tsk)
137+
{
138+
var type = r.GetType();
139+
if (type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) is { } enumType)
140+
{
141+
tsk = (Task)typeof(CallThemAllTests).GetMethod(nameof(Walk)).MakeGenericMethod(enumType.GetGenericArguments()).Invoke(this, [r]);
142+
}
143+
else
144+
tsk = (Task)type.GetMethod("AsTask").Invoke(r, []);
145+
}
146+
await tsk;
147+
}
148+
149+
public static IEnumerable<MethodInfo> AllBaseRepoOperations = typeof(BaseRepository<IdentityTable, SqlConnection>).GetMethods()
150+
.Where(x => x.IsPublic && !x.IsStatic);
151+
152+
public static IEnumerable<object[]> AllBaseRepoOperationsNonAync =>
153+
AllBaseRepoOperations.Where(x => x.Name.EndsWith("Async") == false)
154+
.Select(x => new[] { x });
155+
156+
public static IEnumerable<object[]> AllBaseRepoOperationsAsync =>
157+
AllBaseRepoOperations.Where(x => x.Name.EndsWith("Async") == true)
158+
.Select(x => new[] { x });
159+
160+
161+
class MyRepository<TEntity, TConnection> : BaseRepository<IdentityTable, SqlConnection>
162+
where TEntity : class
163+
where TConnection : DbConnection, new()
164+
{
165+
public MyRepository(string connectionString, ConnectionPersistency connectionPersistency)
166+
: base(connectionString, null, connectionPersistency, null, Constant.DefaultCacheItemExpirationInMinutes, null, null)
167+
{
168+
}
169+
}
170+
171+
[TestMethod, DynamicData(nameof(AllBaseRepoOperationsNonAync), DynamicDataDisplayName = nameof(GetDisplayName))]
172+
public void BaseRepoConnectionOperation(MethodInfo mi)
173+
{
174+
var BaseRepo = new MyRepository<IdentityTable, SqlConnection>(mi.Name.StartsWith("Truncate") ? DbInstance.ConnectionString : DbInstance.LimitedConnectionString, ConnectionPersistency.PerCall);
175+
var parameters = mi.GetParameters();
176+
var args = new object?[parameters.Length];
177+
mi = SetupCall(mi, null, ref parameters, args);
178+
179+
mi.Invoke(BaseRepo, args);
180+
}
181+
182+
183+
[TestMethod, DynamicData(nameof(AllBaseRepoOperationsAsync), DynamicDataDisplayName = nameof(GetDisplayName))]
184+
public async Task BaseRepoConnectionOperationAsync(MethodInfo mi)
185+
{
186+
var BaseRepo = new MyRepository<IdentityTable, SqlConnection>(mi.Name.StartsWith("Truncate") ? DbInstance.ConnectionString : DbInstance.LimitedConnectionString, ConnectionPersistency.PerCall);
187+
var parameters = mi.GetParameters();
188+
var args = new object?[parameters.Length];
189+
mi = SetupCall(mi, null, ref parameters, args);
190+
191+
var r = mi.Invoke(BaseRepo, args);
192+
193+
if (r is not Task tsk)
194+
{
195+
var type = r.GetType();
196+
if (type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) is { } enumType)
197+
{
198+
tsk = (Task)typeof(CallThemAllTests).GetMethod(nameof(Walk)).MakeGenericMethod(enumType.GetGenericArguments()).Invoke(this, [r]);
199+
}
200+
else
201+
tsk = (Task)type.GetMethod("AsTask").Invoke(r, []);
202+
}
203+
await tsk;
204+
}
205+
206+
private static MethodInfo SetupCall(MethodInfo mi, DbConnection? db, ref ParameterInfo[] parameters, object?[] args)
207+
{
208+
if (mi.IsGenericMethod)
209+
{
210+
var ga = mi.GetGenericArguments();
211+
212+
if (ga.Length == 1 && ga[0].Name is "TEntity" or "TWhat")
213+
mi = mi.MakeGenericMethod(typeof(IdentityTable));
214+
else if (ga.Length == 1 && ga[0].Name is "TResult" or "TKey")
215+
mi = mi.MakeGenericMethod(typeof(int?));
216+
else if (ga.Length == 2 && ga[0].Name == "TEntity" && ga[1].Name is "TResult" or "TKey")
217+
mi = mi.MakeGenericMethod(typeof(IdentityTable), typeof(int?));
218+
else if (ga.Length == 2 && ga[0].Name == "TEntity" && ga[1].Name == "TWhat")
219+
mi = mi.MakeGenericMethod(typeof(IdentityTable), typeof(IdentityTable));
220+
else if (ga.All(x => x.Name.StartsWith("T") && x.Name.Substring(1).All(char.IsDigit)))
221+
{
222+
mi = mi.MakeGenericMethod(ga.Select(x => typeof(IdentityTable)).ToArray());
223+
}
224+
parameters = mi.GetParameters();
225+
}
226+
if (db is { })
227+
{
228+
args[0] = db;
229+
}
230+
231+
for (int i = 0; i < parameters.Length; i++)
232+
{
233+
var qp = parameters[i];
234+
235+
object? value = Convert.IsDBNull(qp.DefaultValue) ? null : qp.DefaultValue;
236+
237+
var an = qp.Name.ToLowerInvariant();
238+
239+
while (an.Length > 0 && char.IsDigit(an[an.Length - 1]))
240+
an = an.Substring(0, an.Length - 1);
241+
242+
switch (an)
243+
{
244+
case "tablename":
245+
value = nameof(IdentityTable);
246+
break;
247+
case "commandtext":
248+
value = "SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;SELECT 1 AS Id;";
249+
break;
250+
case "connection":
251+
value = db;
252+
break;
253+
case "entities":
254+
value = new[]
255+
{
256+
new IdentityTable(),
257+
new IdentityTable()
258+
};
259+
break;
260+
case "keys" when qp.ParameterType.IsAssignableFrom(typeof(int?[])):
261+
value = new int?[] { 1, 2, 3 };
262+
break;
263+
case "keys" when qp.ParameterType.IsAssignableFrom(typeof(object?[])):
264+
value = new object?[] { 1, 2, 3 };
265+
break;
266+
case "entity":
267+
value = new IdentityTable();
268+
break;
269+
case "qualifier" when !qp.HasDefaultValue:
270+
value = new Field(nameof(IdentityTable.Id));
271+
break;
272+
case "qualifiers" when !qp.HasDefaultValue && qp.ParameterType.IsAssignableFrom(typeof(FieldSet)):
273+
value = new Field(nameof(IdentityTable.Id)).AsEnumerable();
274+
break;
275+
case "qualifiers" when !qp.HasDefaultValue && qp.ParameterType.IsAssignableFrom(typeof(Expression<Func<IdentityTable, object?>>)):
276+
value = (Expression<Func<IdentityTable, object?>>)(x => x.Id);
277+
break;
278+
case "field" when !qp.HasDefaultValue && qp.ParameterType.IsAssignableFrom(typeof(Field)):
279+
value = new Field(nameof(IdentityTable.ColumnInt));
280+
break;
281+
case "field" when !qp.HasDefaultValue && qp.ParameterType.IsAssignableFrom(typeof(Expression<Func<IdentityTable, object?>>)):
282+
value = (Expression<Func<IdentityTable, object?>>)(x => x.ColumnInt);
283+
break;
284+
case "field" when !qp.HasDefaultValue && qp.ParameterType.IsAssignableFrom(typeof(Expression<Func<IdentityTable, int?>>)):
285+
value = (Expression<Func<IdentityTable, int?>>)(x => x.ColumnInt);
286+
break;
287+
}
288+
289+
args[i] = value;
290+
}
291+
292+
return mi;
293+
}
294+
}

0 commit comments

Comments
 (0)