|
| 1 | +using System.Data.Common; |
| 2 | +using System.Diagnostics; |
| 3 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 4 | + |
| 5 | +namespace SqlServerSimulator; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// Tests for <c>WAITFOR DELAY</c>. The simulator uses |
| 9 | +/// <see cref="Thread.Sleep(TimeSpan)"/> on the calling thread, matching real |
| 10 | +/// SQL Server's "blocks the connection" semantics. To keep CI fast, only |
| 11 | +/// one test actually sleeps a non-trivial duration; everything else |
| 12 | +/// exercises parsing / dispatch / skip-mode / error paths with a |
| 13 | +/// <c>'00:00:00'</c> operand. Behavior probed against SQL Server 2025 |
| 14 | +/// (2026-05-11). |
| 15 | +/// </summary> |
| 16 | +[TestClass] |
| 17 | +public sealed class WaitForDelayTests |
| 18 | +{ |
| 19 | + [TestMethod] |
| 20 | + public void Delay_Zero_StringLiteral_ReturnsImmediately() |
| 21 | + => _ = new Simulation().ExecuteNonQuery("waitfor delay '00:00:00'"); |
| 22 | + |
| 23 | + [TestMethod] |
| 24 | + public void Delay_EmptyString_ReturnsImmediately() |
| 25 | + { |
| 26 | + // Probe-confirmed: empty string is silently accepted as zero delay. |
| 27 | + _ = new Simulation().ExecuteNonQuery("waitfor delay ''"); |
| 28 | + } |
| 29 | + |
| 30 | + [TestMethod] |
| 31 | + public void Delay_Variable_String_ReturnsImmediately() |
| 32 | + => _ = new Simulation().ExecuteNonQuery( |
| 33 | + "declare @t varchar(20) = '00:00:00'; waitfor delay @t"); |
| 34 | + |
| 35 | + [TestMethod] |
| 36 | + public void Delay_Variable_NullValue_ReturnsImmediately() |
| 37 | + { |
| 38 | + // Probe-confirmed: a NULL-valued variable is silently accepted as |
| 39 | + // zero delay (distinct from the bare NULL literal which is a Msg 156 |
| 40 | + // syntax error at parse time — different code path). |
| 41 | + _ = new Simulation().ExecuteNonQuery( |
| 42 | + "declare @t varchar(20); waitfor delay @t"); |
| 43 | + } |
| 44 | + |
| 45 | + /// <summary> |
| 46 | + /// The only test that actually sleeps. ~50ms is well under the user's |
| 47 | + /// 100ms ceiling; the assertion checks the sleep was at least nearly |
| 48 | + /// the requested span (giving 10ms of OS scheduler slack on the lower |
| 49 | + /// bound) and not absurdly long. |
| 50 | + /// </summary> |
| 51 | + [TestMethod] |
| 52 | + public void Delay_50ms_ActuallySleeps() |
| 53 | + { |
| 54 | + var start = Stopwatch.GetTimestamp(); |
| 55 | + _ = new Simulation().ExecuteNonQuery("waitfor delay '00:00:00.050'"); |
| 56 | + var elapsed = Stopwatch.GetElapsedTime(start); |
| 57 | + IsGreaterThanOrEqualTo(40, elapsed.TotalMilliseconds, |
| 58 | + $"Expected ≥40ms sleep, got {elapsed.TotalMilliseconds}ms"); |
| 59 | + IsLessThan(500, elapsed.TotalMilliseconds, |
| 60 | + $"Expected <500ms sleep, got {elapsed.TotalMilliseconds}ms"); |
| 61 | + } |
| 62 | + |
| 63 | + [TestMethod] |
| 64 | + public void Delay_InUntakenIf_DoesNotSleep() |
| 65 | + { |
| 66 | + // The string operand here would otherwise sleep 10 seconds — verify |
| 67 | + // skip-mode suppresses the actual sleep. |
| 68 | + var start = Stopwatch.GetTimestamp(); |
| 69 | + _ = new Simulation().ExecuteNonQuery("if 1=0 waitfor delay '00:00:10'"); |
| 70 | + var elapsed = Stopwatch.GetElapsedTime(start); |
| 71 | + IsLessThan(100, elapsed.TotalMilliseconds, |
| 72 | + $"Expected near-instant skip, got {elapsed.TotalMilliseconds}ms"); |
| 73 | + } |
| 74 | + |
| 75 | + [TestMethod] |
| 76 | + public void Delay_AfterReturn_DoesNotSleep() |
| 77 | + { |
| 78 | + var start = Stopwatch.GetTimestamp(); |
| 79 | + _ = new Simulation().ExecuteNonQuery("return; waitfor delay '00:00:10'"); |
| 80 | + var elapsed = Stopwatch.GetElapsedTime(start); |
| 81 | + IsLessThan(100, elapsed.TotalMilliseconds, |
| 82 | + $"Expected near-instant skip, got {elapsed.TotalMilliseconds}ms"); |
| 83 | + } |
| 84 | + |
| 85 | + [TestMethod] |
| 86 | + public void Delay_AfterBreak_DoesNotSleep() |
| 87 | + { |
| 88 | + var start = Stopwatch.GetTimestamp(); |
| 89 | + _ = new Simulation().ExecuteNonQuery(""" |
| 90 | + while 1=1 |
| 91 | + begin |
| 92 | + break; |
| 93 | + waitfor delay '00:00:10'; |
| 94 | + end |
| 95 | + """); |
| 96 | + var elapsed = Stopwatch.GetElapsedTime(start); |
| 97 | + IsLessThan(100, elapsed.TotalMilliseconds, |
| 98 | + $"Expected near-instant skip, got {elapsed.TotalMilliseconds}ms"); |
| 99 | + } |
| 100 | + |
| 101 | + [TestMethod] |
| 102 | + public void Delay_ResetsRowCountToZero() |
| 103 | + { |
| 104 | + using var reader = new Simulation().ExecuteReader(""" |
| 105 | + select 1 union all select 2 union all select 3; |
| 106 | + waitfor delay '00:00:00'; |
| 107 | + select @@rowcount as rc |
| 108 | + """); |
| 109 | + while (reader.Read()) { } |
| 110 | + IsTrue(reader.NextResult()); |
| 111 | + IsTrue(reader.Read()); |
| 112 | + AreEqual(0, reader.GetInt32(0)); |
| 113 | + } |
| 114 | + |
| 115 | + [TestMethod] |
| 116 | + public void Delay_MalformedString_Msg148() |
| 117 | + => new Simulation().AssertSqlError( |
| 118 | + "waitfor delay 'not a time'", 148, |
| 119 | + "Incorrect time syntax in time string 'not a time' used with WAITFOR."); |
| 120 | + |
| 121 | + [TestMethod] |
| 122 | + public void Delay_Over24h_Msg148() |
| 123 | + => new Simulation().AssertSqlError("waitfor delay '25:00:00'", 148); |
| 124 | + |
| 125 | + [TestMethod] |
| 126 | + public void Delay_NegativeString_Msg148() |
| 127 | + => new Simulation().AssertSqlError("waitfor delay '-00:00:01'", 148); |
| 128 | + |
| 129 | + [TestMethod] |
| 130 | + public void Delay_DayComponent_Msg148() |
| 131 | + => new Simulation().AssertSqlError("waitfor delay '99:00:00:00'", 148); |
| 132 | + |
| 133 | + [TestMethod] |
| 134 | + public void Delay_BadVariableValue_Msg148() |
| 135 | + => new Simulation().AssertSqlError( |
| 136 | + "declare @t varchar(20) = 'bogus'; waitfor delay @t", 148, |
| 137 | + "Incorrect time syntax in time string 'bogus' used with WAITFOR."); |
| 138 | + |
| 139 | + [TestMethod] |
| 140 | + public void Delay_TimeTypedVariable_Msg9815() |
| 141 | + => new Simulation().AssertSqlError( |
| 142 | + "declare @t time = '00:00:00.100'; waitfor delay @t", 9815, |
| 143 | + "Waitfor delay and waitfor time cannot be of type time."); |
| 144 | + |
| 145 | + [TestMethod] |
| 146 | + public void Delay_IntegerLiteralOperand_SyntaxError() |
| 147 | + { |
| 148 | + // Real SQL Server raises Msg 102 here; the simulator routes through |
| 149 | + // the same Msg-102 factory (SyntaxErrorNear). |
| 150 | + var ex = Throws<DbException>( |
| 151 | + () => _ = new Simulation().ExecuteNonQuery("waitfor delay 1")); |
| 152 | + AreEqual("102", ex.Data["HelpLink.EvtID"]); |
| 153 | + } |
| 154 | + |
| 155 | + [TestMethod] |
| 156 | + public void Delay_BareNullLiteralOperand_SyntaxError() |
| 157 | + { |
| 158 | + // Real SQL Server: Msg 156 "Incorrect syntax near the keyword 'null'." |
| 159 | + // Simulator: Msg 102 (NULL falls through to the operand's |
| 160 | + // SyntaxErrorNear catch-all). Wording differs but rejection is |
| 161 | + // consistent — a hand-typed `waitfor delay null` is a programmer |
| 162 | + // error either way. |
| 163 | + var ex = Throws<DbException>( |
| 164 | + () => _ = new Simulation().ExecuteNonQuery("waitfor delay null")); |
| 165 | + var num = ex.Data["HelpLink.EvtID"] as string; |
| 166 | + IsTrue(num is "102" or "156", $"Expected Msg 102 or 156, got {num}"); |
| 167 | + } |
| 168 | + |
| 169 | + [TestMethod] |
| 170 | + public void Delay_CastOperand_SyntaxError() |
| 171 | + => _ = Throws<DbException>( |
| 172 | + () => _ = new Simulation().ExecuteNonQuery( |
| 173 | + "waitfor delay cast('00:00:00.050' as time)")); |
| 174 | + |
| 175 | + [TestMethod] |
| 176 | + public void WaitforTime_NotSupported() |
| 177 | + => _ = Throws<NotSupportedException>( |
| 178 | + () => _ = new Simulation().ExecuteNonQuery("waitfor time '23:59:59'")); |
| 179 | + |
| 180 | + [TestMethod] |
| 181 | + public void Delay_BetweenSelects_BothRun() |
| 182 | + { |
| 183 | + using var reader = new Simulation().ExecuteReader( |
| 184 | + "select 'before' as v; waitfor delay '00:00:00'; select 'after' as v"); |
| 185 | + IsTrue(reader.Read()); |
| 186 | + AreEqual("before", reader.GetString(0)); |
| 187 | + IsTrue(reader.NextResult()); |
| 188 | + IsTrue(reader.Read()); |
| 189 | + AreEqual("after", reader.GetString(0)); |
| 190 | + } |
| 191 | + |
| 192 | + [TestMethod] |
| 193 | + public void Delay_InWhile_RunsEachIteration() |
| 194 | + { |
| 195 | + // Three iterations, each waiting 0 — exercises dispatch in the loop |
| 196 | + // body. The loop completes in nominal time. |
| 197 | + var start = Stopwatch.GetTimestamp(); |
| 198 | + _ = new Simulation().ExecuteNonQuery(""" |
| 199 | + declare @i int = 0; |
| 200 | + while @i < 3 |
| 201 | + begin |
| 202 | + set @i = @i + 1; |
| 203 | + waitfor delay '00:00:00'; |
| 204 | + end |
| 205 | + """); |
| 206 | + var elapsed = Stopwatch.GetElapsedTime(start); |
| 207 | + IsLessThan(500, elapsed.TotalMilliseconds, |
| 208 | + $"Expected fast loop, got {elapsed.TotalMilliseconds}ms"); |
| 209 | + } |
| 210 | +} |
0 commit comments