Skip to content

Commit 16fe863

Browse files
committed
Code coverage boost: fixed two latent bugs (Modulus precedence; NANOSECOND keyword bucket), implemented DbParameterCollection (was NotImplementedException stubs), refactored unreachable Encode/Decode/ConvertParameter overrides to virtual-throw tripwires on the SqlType base, and added ~64 tests filling real surface gaps and reducing noise in the coverage report.
1 parent 8b04dd0 commit 16fe863

19 files changed

Lines changed: 638 additions & 94 deletions

SqlServerSimulator.Tests/AggregateTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ public void Sum_IntOverflow_RaisesMsg8115()
9696
AreEqual("8115", ex.Data["HelpLink.EvtID"]);
9797
}
9898

99+
[TestMethod]
100+
public void Sum_Float_AccumulatesViaDouble()
101+
{
102+
using var connection = Seeded("a float", "(1.5), (2.25), (0.25)");
103+
AreEqual(4.0, connection.CreateCommand("select sum(a) from t").ExecuteScalar());
104+
}
105+
106+
[TestMethod]
107+
public void Sum_Real_AccumulatesViaDoubleAndNarrowsBack()
108+
{
109+
using var connection = Seeded("a real", "(1.5), (2.25), (0.25)");
110+
AreEqual(4.0f, connection.CreateCommand("select sum(a) from t").ExecuteScalar());
111+
}
112+
99113
[TestMethod]
100114
public void Avg_Int_TruncatesByIntegerDivision()
101115
{

SqlServerSimulator.Tests/CreateTableTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,14 @@ public void ContextualKeywordsAsColumnNames_RoundTrip(string columnName)
377377
Assert.IsTrue(reader.Read());
378378
Assert.AreEqual(42, reader.GetInt32(0));
379379
}
380+
381+
[TestMethod]
382+
public void CreateTable_DuplicateName_RaisesMsg2714()
383+
{
384+
var simulation = new Simulation();
385+
_ = simulation.ExecuteNonQuery("create table dup (a int)");
386+
var ex = Assert.Throws<DbException>(() => simulation.ExecuteNonQuery("create table dup (a int)"));
387+
Assert.AreEqual("There is already an object named 'dup' in the database.", ex.Message);
388+
Assert.AreEqual("2714", ex.Data["HelpLink.EvtID"]);
389+
}
380390
}

SqlServerSimulator.Tests/DataReaderTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,55 @@ public void GetDataTypeName_ReturnsBareSqlServerName(string expression, string e
203203
[DataRow("cast('12:00' as time(3))", typeof(TimeSpan))]
204204
[DataRow("cast('2024-01-01T12:00:00+00:00' as datetimeoffset(3))", typeof(DateTimeOffset))]
205205
[DataRow("cast('00000000-0000-0000-0000-000000000000' as uniqueidentifier)", typeof(Guid))]
206+
[DataRow("cast(1 as smallmoney)", typeof(decimal))]
207+
[DataRow("cast('2024-01-01' as smalldatetime)", typeof(DateTime))]
208+
[DataRow("cast('abc' as char(5))", typeof(string))]
209+
[DataRow("cast('abc' as nchar(5))", typeof(string))]
210+
[DataRow("cast(0x1234 as binary(4))", typeof(byte[]))]
206211
public void GetFieldType_ReturnsClrType(string expression, Type expected)
207212
{
208213
using var reader = OpenReader($"select {expression}");
209214
AreEqual(expected, reader.GetFieldType(0));
210215
}
211216

217+
[TestMethod]
218+
[DataRow("text", "text", typeof(string))]
219+
[DataRow("ntext", "ntext", typeof(string))]
220+
[DataRow("image", "image", typeof(byte[]))]
221+
public void LobTypes_FieldTypeAndDataTypeName_ViaColumnRoundTrip(string columnType, string expectedSqlName, Type expectedClr)
222+
{
223+
// text / ntext / image can't appear as a CAST target; reach their
224+
// type-metadata paths by declaring a column and reading it back.
225+
var sim = new Simulation();
226+
_ = sim.ExecuteNonQuery($"create table t (id int, c {columnType})");
227+
_ = sim.ExecuteNonQuery("insert t (id) values (1)");
228+
using var reader = sim.ExecuteReader("select c from t");
229+
AreEqual(expectedSqlName, reader.GetDataTypeName(0));
230+
AreEqual(expectedClr, reader.GetFieldType(0));
231+
}
232+
233+
[TestMethod]
234+
public void RowVersion_FieldTypeAndDataTypeName_ViaColumnRoundTrip()
235+
{
236+
// rowversion stores 8 bytes, surfaces under the legacy "timestamp" name.
237+
var sim = new Simulation();
238+
_ = sim.ExecuteNonQuery("create table t (id int, rv rowversion)");
239+
_ = sim.ExecuteNonQuery("insert t (id) values (1)");
240+
using var reader = sim.ExecuteReader("select rv from t");
241+
AreEqual("timestamp", reader.GetDataTypeName(0));
242+
AreEqual(typeof(byte[]), reader.GetFieldType(0));
243+
}
244+
245+
[TestMethod]
246+
public void SysName_FieldTypeAndDataTypeName_ViaSystemTable()
247+
{
248+
// sysname can't be declared in user CREATE TABLE; the systypes
249+
// catalog table exposes it via the "name" column.
250+
using var reader = new Simulation().ExecuteReader("select name from systypes");
251+
AreEqual("sysname", reader.GetDataTypeName(0));
252+
AreEqual(typeof(string), reader.GetFieldType(0));
253+
}
254+
212255
[TestMethod]
213256
public void GetOrdinal_CaseSensitiveThenCaseInsensitive()
214257
{
@@ -299,4 +342,20 @@ public void GetChar_AlwaysThrowsInvalidCast()
299342
IsTrue(reader.Read());
300343
_ = ThrowsExactly<InvalidCastException>(() => reader.GetChar(0));
301344
}
345+
346+
[TestMethod]
347+
public void GetFloat_ReadsRealColumn()
348+
{
349+
using var reader = OpenReader("select cast(3.5 as real)");
350+
IsTrue(reader.Read());
351+
AreEqual(3.5f, reader.GetFloat(0));
352+
}
353+
354+
[TestMethod]
355+
public void GetFloat_OnNull_ThrowsSqlNullValueException()
356+
{
357+
using var reader = OpenReader("select cast(null as real)");
358+
IsTrue(reader.Read());
359+
_ = Throws<SqlNullValueException>(() => reader.GetFloat(0));
360+
}
302361
}

SqlServerSimulator.Tests/DatePartTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ public void DatePart_OnTime_AcceptsTimeParts() =>
5151
public void DatePart_OnDateTimeOffset_AcceptsTzOffset() =>
5252
AreEqual(-420, ExecuteScalar("select datepart(tzoffset, cast('2024-06-15 13:45:30 -07:00' as datetimeoffset))"));
5353

54+
[TestMethod]
55+
[DataRow("2024-01-01", 1)] // Jan 1 is always week 1.
56+
[DataRow("2024-01-06", 1)] // Saturday before first Sunday roll → still week 1.
57+
[DataRow("2024-01-07", 2)] // Sunday → week 2 begins.
58+
[DataRow("2024-06-15", 24)] // Mid-year, default us_english Sunday-anchored.
59+
[DataRow("2024-12-31", 53)] // Last day of year — straddles into week 53.
60+
public void DatePart_Week_DefaultUsEnglishSundayAnchored(string dateStr, int expectedWeek) =>
61+
AreEqual(expectedWeek, ExecuteScalar($"select datepart(week, cast('{dateStr}' as date))"));
62+
5463
[TestMethod]
5564
public void DatePart_NullInput_ReturnsNullInt() =>
5665
IsInstanceOfType<DBNull>(ExecuteScalar("select datepart(year, cast(null as datetime2))"));
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Behavioral tests for the <see cref="DbParameterCollection"/> contract
8+
/// surfaced through <c>DbCommand.Parameters</c>. SqlClient-style consumers
9+
/// reach these methods directly (Add, Contains, IndexOf, RemoveAt by name,
10+
/// etc.) when manipulating parameter collections; the simulator's
11+
/// implementation is exercised by frameworks like Dapper / EF Core's
12+
/// command interception layer.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class DbParameterCollectionTests
16+
{
17+
private static (DbConnection conn, DbCommand cmd) Open()
18+
{
19+
var conn = new Simulation().CreateOpenConnection();
20+
return (conn, conn.CreateCommand());
21+
}
22+
23+
private static DbParameter MakeParam(DbCommand cmd, string name, object value)
24+
{
25+
var p = cmd.CreateParameter();
26+
p.ParameterName = name;
27+
p.Value = value;
28+
return p;
29+
}
30+
31+
[TestMethod]
32+
public void Add_ReturnsIndex_ParametersAccessibleByPosition()
33+
{
34+
var (conn, cmd) = Open();
35+
using (conn)
36+
using (cmd)
37+
{
38+
var p1 = MakeParam(cmd, "@a", 1);
39+
var p2 = MakeParam(cmd, "@b", 2);
40+
AreEqual(0, cmd.Parameters.Add(p1));
41+
AreEqual(1, cmd.Parameters.Add(p2));
42+
HasCount(2, cmd.Parameters);
43+
AreSame(p1, cmd.Parameters[0]);
44+
AreSame(p2, cmd.Parameters[1]);
45+
}
46+
}
47+
48+
[TestMethod]
49+
public void IndexOf_ByName_IsCaseInsensitive()
50+
{
51+
var (conn, cmd) = Open();
52+
using (conn)
53+
using (cmd)
54+
{
55+
_ = cmd.Parameters.Add(MakeParam(cmd, "@Foo", 1));
56+
AreEqual(0, cmd.Parameters.IndexOf("@foo"));
57+
AreEqual(0, cmd.Parameters.IndexOf("@FOO"));
58+
AreEqual(-1, cmd.Parameters.IndexOf("@bar"));
59+
}
60+
}
61+
62+
[TestMethod]
63+
public void Contains_ByName_AndByObject()
64+
{
65+
var (conn, cmd) = Open();
66+
using (conn)
67+
using (cmd)
68+
{
69+
var p = MakeParam(cmd, "@x", 1);
70+
_ = cmd.Parameters.Add(p);
71+
var hasX = cmd.Parameters.Contains("@x");
72+
var hasY = cmd.Parameters.Contains("@y");
73+
var hasP = cmd.Parameters.Contains(p);
74+
IsTrue(hasX);
75+
IsFalse(hasY);
76+
IsTrue(hasP);
77+
}
78+
}
79+
80+
[TestMethod]
81+
public void IndexerByName_GetAndSet()
82+
{
83+
var (conn, cmd) = Open();
84+
using (conn)
85+
using (cmd)
86+
{
87+
var p = MakeParam(cmd, "@x", 1);
88+
_ = cmd.Parameters.Add(p);
89+
AreSame(p, cmd.Parameters["@x"]);
90+
var replacement = MakeParam(cmd, "@x", 99);
91+
cmd.Parameters["@x"] = replacement;
92+
AreSame(replacement, cmd.Parameters["@x"]);
93+
}
94+
}
95+
96+
[TestMethod]
97+
public void IndexerByName_MissingNameThrows()
98+
{
99+
var (conn, cmd) = Open();
100+
using (conn)
101+
using (cmd)
102+
_ = Throws<ArgumentException>(() => _ = cmd.Parameters["@missing"]);
103+
}
104+
105+
[TestMethod]
106+
public void RemoveAt_ByIndex_AndByName()
107+
{
108+
var (conn, cmd) = Open();
109+
using (conn)
110+
using (cmd)
111+
{
112+
_ = cmd.Parameters.Add(MakeParam(cmd, "@a", 1));
113+
_ = cmd.Parameters.Add(MakeParam(cmd, "@b", 2));
114+
_ = cmd.Parameters.Add(MakeParam(cmd, "@c", 3));
115+
cmd.Parameters.RemoveAt(0);
116+
HasCount(2, cmd.Parameters);
117+
cmd.Parameters.RemoveAt("@c");
118+
HasCount(1, cmd.Parameters);
119+
AreEqual("@b", cmd.Parameters[0].ParameterName);
120+
}
121+
}
122+
123+
[TestMethod]
124+
public void Remove_ByObject()
125+
{
126+
var (conn, cmd) = Open();
127+
using (conn)
128+
using (cmd)
129+
{
130+
var p = MakeParam(cmd, "@x", 1);
131+
_ = cmd.Parameters.Add(p);
132+
cmd.Parameters.Remove(p);
133+
IsEmpty(cmd.Parameters);
134+
}
135+
}
136+
137+
[TestMethod]
138+
public void Insert_PutsParameterAtSpecificIndex()
139+
{
140+
var (conn, cmd) = Open();
141+
using (conn)
142+
using (cmd)
143+
{
144+
_ = cmd.Parameters.Add(MakeParam(cmd, "@a", 1));
145+
_ = cmd.Parameters.Add(MakeParam(cmd, "@c", 3));
146+
cmd.Parameters.Insert(1, MakeParam(cmd, "@b", 2));
147+
AreEqual("@a", cmd.Parameters[0].ParameterName);
148+
AreEqual("@b", cmd.Parameters[1].ParameterName);
149+
AreEqual("@c", cmd.Parameters[2].ParameterName);
150+
}
151+
}
152+
153+
[TestMethod]
154+
public void AddRange_AddsAllElements()
155+
{
156+
var (conn, cmd) = Open();
157+
using (conn)
158+
using (cmd)
159+
{
160+
var batch = new[] { MakeParam(cmd, "@a", 1), MakeParam(cmd, "@b", 2) };
161+
cmd.Parameters.AddRange(batch);
162+
HasCount(2, cmd.Parameters);
163+
}
164+
}
165+
166+
[TestMethod]
167+
public void Clear_EmptiesCollection()
168+
{
169+
var (conn, cmd) = Open();
170+
using (conn)
171+
using (cmd)
172+
{
173+
_ = cmd.Parameters.Add(MakeParam(cmd, "@a", 1));
174+
_ = cmd.Parameters.Add(MakeParam(cmd, "@b", 2));
175+
cmd.Parameters.Clear();
176+
IsEmpty(cmd.Parameters);
177+
}
178+
}
179+
180+
[TestMethod]
181+
public void CopyTo_CopiesIntoArray()
182+
{
183+
var (conn, cmd) = Open();
184+
using (conn)
185+
using (cmd)
186+
{
187+
_ = cmd.Parameters.Add(MakeParam(cmd, "@a", 1));
188+
_ = cmd.Parameters.Add(MakeParam(cmd, "@b", 2));
189+
var dest = new DbParameter[2];
190+
cmd.Parameters.CopyTo(dest, 0);
191+
AreEqual("@a", dest[0].ParameterName);
192+
AreEqual("@b", dest[1].ParameterName);
193+
}
194+
}
195+
196+
[TestMethod]
197+
public void Enumerator_YieldsParametersInOrder()
198+
{
199+
var (conn, cmd) = Open();
200+
using (conn)
201+
using (cmd)
202+
{
203+
_ = cmd.Parameters.Add(MakeParam(cmd, "@a", 1));
204+
_ = cmd.Parameters.Add(MakeParam(cmd, "@b", 2));
205+
var names = new List<string>();
206+
foreach (DbParameter p in cmd.Parameters)
207+
names.Add(p.ParameterName);
208+
CollectionAssert.AreEqual(new[] { "@a", "@b" }, names);
209+
}
210+
}
211+
212+
[TestMethod]
213+
public void SyncRoot_NotNull() =>
214+
IsNotNull(new Simulation().CreateOpenConnection().CreateCommand().Parameters.SyncRoot);
215+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
using static SqlServerSimulator.TestHelpers;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Parser-tree-balancing tests for binary operator precedence. Each row
8+
/// places a specific operator on the right of another two-sided
9+
/// expression — that's the path that reads the right-side child's
10+
/// <c>Precedence</c> property in <c>TwoSidedExpression.AdjustForPrecedence</c>.
11+
/// Without these chains, the per-subclass <c>Precedence</c> overrides on
12+
/// Bitwise* / Divide / Modulus / Subtract sit at 0% because the simpler
13+
/// <c>a OP b</c> shape doesn't require precedence comparison.
14+
/// </summary>
15+
[TestClass]
16+
public sealed class OperatorPrecedenceTests
17+
{
18+
[TestMethod]
19+
[DataRow("1 + 2 - 3", 0)] // Subtract on the right of Add (same precedence, left-assoc swap).
20+
[DataRow("8 - 4 + 2", 6)] // Add on the right of Subtract.
21+
[DataRow("1 + 2 & 3", 3)] // BitwiseAnd on the right of Add.
22+
[DataRow("1 + 2 | 4", 7)] // BitwiseOr on the right of Add.
23+
[DataRow("1 + 2 ^ 5", 6)] // BitwiseExclusiveOr on the right of Add: (1+2)^5 = 3^5 = 6.
24+
[DataRow("10 + 8 / 4", 12)] // Divide on the right of Add (Divide is higher precedence; doesn't swap).
25+
[DataRow("1 + 5 % 3", 3)] // Modulus on the right of Add (same precedence): (1+5)%3 = 6%3 = 0... wait, swap means Modulus parent: 1+(5%3) = 1+2 = 3.
26+
[DataRow("2 * 3 + 4", 10)] // Add on the right of Multiply (Add lower precedence; swap).
27+
public void OperatorChainProducesExpected(string expr, int expected) =>
28+
AreEqual(expected, ExecuteScalar($"select {expr}"));
29+
}

0 commit comments

Comments
 (0)