|
| 1 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 2 | + |
| 3 | +namespace SqlServerSimulator; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Line-number, server-name, and procedure attribution on |
| 7 | +/// <see cref="SimulatedSqlException"/>, mirroring the values real SqlClient |
| 8 | +/// surfaces (probe-confirmed against SQL Server 2025, 2026-07-18). Semantics: |
| 9 | +/// runtime / bind errors report the failing statement's start line, syntax |
| 10 | +/// errors (severity 15) report the offending token's line, procedure-body |
| 11 | +/// errors report a line relative to the whole <c>CREATE</c> statement plus the |
| 12 | +/// schema-qualified procedure name, and dynamic SQL reports a line relative to |
| 13 | +/// the dynamic batch. <see cref="SimulatedError.Server"/> matches the |
| 14 | +/// connection data source (real <c>SqlException.Server</c> carries the data |
| 15 | +/// source, not <c>@@SERVERNAME</c>). See <c>docs/claude/errors.md</c>. |
| 16 | +/// </summary> |
| 17 | +[TestClass] |
| 18 | +public sealed class ErrorDiagnosticsTests |
| 19 | +{ |
| 20 | + [TestMethod] |
| 21 | + public void RuntimeError_ReportsFailingStatementStartLine() |
| 22 | + => AreEqual(2, new Simulation().AssertSqlError("declare @x int\nset @x = 1 / 0", 8134).LineNumber); |
| 23 | + |
| 24 | + /// <summary> |
| 25 | + /// The SET spans lines 2-3; the divide-by-zero token sits on line 3, but |
| 26 | + /// the reported line is the statement start (line 2), matching real. |
| 27 | + /// </summary> |
| 28 | + [TestMethod] |
| 29 | + public void RuntimeError_SpanningLines_ReportsStatementStartNotExpressionLine() |
| 30 | + => AreEqual(2, new Simulation().AssertSqlError("declare @x int\nset @x =\n 1 / 0", 8134).LineNumber); |
| 31 | + |
| 32 | + [TestMethod] |
| 33 | + public void BindError_MissingTable_ReportsStatementStartLine() |
| 34 | + => AreEqual(2, new Simulation().AssertSqlError("declare @x int\nselect * from no_such_table_xyz", 208).LineNumber); |
| 35 | + |
| 36 | + [TestMethod] |
| 37 | + public void ConstraintViolation_ReportsFailingInsertLine() |
| 38 | + { |
| 39 | + var sim = new Simulation(); |
| 40 | + _ = sim.ExecuteNonQuery("create table t (id int primary key); insert t values (1)"); |
| 41 | + AreEqual(2, sim.AssertSqlError("declare @x int\ninsert t values (1)", 2627).LineNumber); |
| 42 | + } |
| 43 | + |
| 44 | + /// <summary> |
| 45 | + /// Severity-15 syntax errors report the token line, not the statement |
| 46 | + /// start: the batch's first statement is on line 1, the bad token on 2. |
| 47 | + /// </summary> |
| 48 | + [TestMethod] |
| 49 | + public void SyntaxError_ReportsOffendingTokenLine() |
| 50 | + => AreEqual(2, new Simulation().AssertSqlError("declare @x int\nselect )", 102).LineNumber); |
| 51 | + |
| 52 | + [TestMethod] |
| 53 | + public void Error_ServerMatchesConnectionDataSource() |
| 54 | + { |
| 55 | + var sim = new Simulation(); |
| 56 | + using var connection = sim.CreateDbConnection(); |
| 57 | + var ex = sim.AssertSqlError("select 1 / 0", 8134); |
| 58 | + AreEqual(connection.DataSource, ex.Server); |
| 59 | + AreEqual(connection.DataSource, ex.Errors[0].Server); |
| 60 | + } |
| 61 | + |
| 62 | + [TestMethod] |
| 63 | + public void TopLevelError_HasNoProcedure() |
| 64 | + => IsEmpty(new Simulation().AssertSqlError("select 1 / 0", 8134).Procedure); |
| 65 | + |
| 66 | + [TestMethod] |
| 67 | + public void ProcedureBodyError_ReportsCreateRelativeLineAndQualifiedName() |
| 68 | + { |
| 69 | + var sim = new Simulation(); |
| 70 | + sim.ExecuteBatches("create procedure dbo.p_boom as\nbegin\n declare @x int\n set @x = 1 / 0\nend"); |
| 71 | + var ex = sim.AssertSqlError("exec dbo.p_boom", 8134); |
| 72 | + // CREATE line 1, BEGIN line 2, DECLARE line 3, failing SET line 4. |
| 73 | + AreEqual(4, ex.LineNumber); |
| 74 | + AreEqual("dbo.p_boom", ex.Procedure); |
| 75 | + } |
| 76 | + |
| 77 | + [TestMethod] |
| 78 | + public void NestedProcedureError_ReportsInnermostFrame() |
| 79 | + { |
| 80 | + var sim = new Simulation(); |
| 81 | + sim.ExecuteBatches( |
| 82 | + "create procedure dbo.inner_boom as\nbegin\n declare @x int\n set @x = 1 / 0\nend", |
| 83 | + "create procedure dbo.outer_p as\nbegin\n exec dbo.inner_boom\nend"); |
| 84 | + var ex = sim.AssertSqlError("exec dbo.outer_p", 8134); |
| 85 | + AreEqual("dbo.inner_boom", ex.Procedure); |
| 86 | + AreEqual(4, ex.LineNumber); |
| 87 | + } |
| 88 | + |
| 89 | + [TestMethod] |
| 90 | + public void DynamicSql_ReportsLineRelativeToDynamicBatch_NoProcedure() |
| 91 | + { |
| 92 | + var ex = new Simulation().AssertSqlError("exec('declare @x int\nset @x = 1 / 0')", 8134); |
| 93 | + AreEqual(2, ex.LineNumber); |
| 94 | + IsEmpty(ex.Procedure); |
| 95 | + } |
| 96 | + |
| 97 | + [TestMethod] |
| 98 | + public void ThrowValueForm_ReportsThrowStatementLine() |
| 99 | + { |
| 100 | + var ex = new Simulation().AssertSqlError("declare @x int\nthrow 51000, N'boom', 1", 51000); |
| 101 | + AreEqual(2, ex.LineNumber); |
| 102 | + } |
| 103 | + |
| 104 | + [TestMethod] |
| 105 | + public void ThrowReRaise_PreservesOriginalErrorLine() |
| 106 | + { |
| 107 | + // A bare THROW; re-raising a divide-by-zero from line 3 preserves that |
| 108 | + // line rather than reporting the THROW's own line (probe-confirmed). |
| 109 | + var ex = new Simulation().AssertSqlError( |
| 110 | + "declare @x int\nbegin try\n set @x = 1 / 0\nend try\nbegin catch\n throw\nend catch", |
| 111 | + 8134); |
| 112 | + AreEqual(3, ex.LineNumber); |
| 113 | + } |
| 114 | +} |
0 commit comments