-
Notifications
You must be signed in to change notification settings - Fork 317
[DRAFT] Add tests for HasRows + Mutliple INFO tokens #3744
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
bd9afc2
bb2bc34
d3508b4
5140157
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
|
|
||
| using Microsoft.SqlServer.TDS; | ||
| using Microsoft.SqlServer.TDS.ColMetadata; | ||
| using Microsoft.SqlServer.TDS.Done; | ||
| using Microsoft.SqlServer.TDS.EndPoint; | ||
| using Microsoft.SqlServer.TDS.Info; | ||
| using Microsoft.SqlServer.TDS.Row; | ||
| using Microsoft.SqlServer.TDS.Servers; | ||
| using Microsoft.SqlServer.TDS.SQLBatch; | ||
|
|
||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace Microsoft.Data.SqlClient.Tests.UnitTests.SimulatedServerTests; | ||
|
|
||
| public sealed class HasRowsTests : IDisposable | ||
| { | ||
| // The query engine used by the server. | ||
| private readonly InfoQueryEngine _engine; | ||
|
|
||
| // The TDS server we will connect to. | ||
| private readonly TdsServer _server; | ||
|
|
||
| // The connection to the server; always open post-construction. | ||
| private readonly SqlConnection _connection; | ||
|
|
||
| // The list of INFO message text received. | ||
| private readonly List<string> _infoText = new(); | ||
|
|
||
| // Construct to setup the server and connection. | ||
| public HasRowsTests(ITestOutputHelper output) | ||
| { | ||
| // Use our log writer to capture TDS Server logs to the xUnit output. | ||
| _engine = new(new() { Log = new LogWriter(output) }); | ||
|
|
||
| // Start the TDS server. | ||
| _server = new(_engine); | ||
| _server.Start(); | ||
|
|
||
| // Use the server's endpoint to build a connection string. | ||
| var connStr = new SqlConnectionStringBuilder() | ||
| { | ||
| DataSource = $"localhost,{_server.EndPoint.Port}", | ||
| Encrypt = SqlConnectionEncryptOption.Optional, | ||
| }.ConnectionString; | ||
|
|
||
| // Create the connection. | ||
| _connection = new SqlConnection(connStr); | ||
|
|
||
| // Add a handler for INFO messages to capture them. | ||
| _connection.InfoMessage += new( | ||
| (object sender, SqlInfoMessageEventArgs imevent) => | ||
| { | ||
| // The informational messages are exposed as Errors for some | ||
| // reason. Capture them in order. | ||
| for (int i = 0; i < imevent.Errors.Count; i++) | ||
| { | ||
| _infoText.Add(imevent.Errors[i].Message); | ||
| } | ||
| }); | ||
|
|
||
| // Open the connection. | ||
| _connection.Open(); | ||
| } | ||
|
|
||
| // Dispose of resources. | ||
| public void Dispose() | ||
| { | ||
| _connection.Dispose(); | ||
| _server.Dispose(); | ||
| } | ||
|
|
||
| // Verify that HasRows is not set when we only receive INFO tokens. | ||
| [Fact] | ||
| public void OnlyInfo() | ||
| { | ||
| using SqlCommand command = new( | ||
| // Use command text that isn't recognized by the query engine. This | ||
| // should elicit a response that includes 2 INFO tokens and no row | ||
| // results. | ||
| // | ||
| // See QueryEngine.CreateQueryResponse()'s else block. | ||
| // | ||
| "select 'Hello, World!'", | ||
| _connection); | ||
| using SqlDataReader reader = command.ExecuteReader(); | ||
|
|
||
| // We should not have detected any rows. | ||
| Assert.False(reader.HasRows); | ||
|
|
||
| // Verify that we received the expected 2 INFO messages. | ||
| Assert.Equal(2, _infoText.Count); | ||
| Assert.Equal("select 'Hello, World!'", _infoText[0]); | ||
| Assert.Equal( | ||
| "Received query is not recognized by the query engine. Please " + | ||
| "ask a very specific question.", | ||
| _infoText[1]); | ||
|
|
||
| // Confirm that we really didn't get any rows. | ||
| Assert.False(reader.Read()); | ||
| } | ||
|
|
||
| // Verify that HasRows is true when a variable number of INFO tokens is | ||
| // included in the response to a SQL batch that returns rows. | ||
| [Theory] | ||
| [InlineData(0)] | ||
| [InlineData(1)] | ||
| [InlineData(2)] | ||
| [InlineData(3)] | ||
| [InlineData(10)] | ||
| public void InfoAndRows(ushort infoCount) | ||
| { | ||
| // Configure the engine to include the desired number of INFO tokens | ||
| // with its response. | ||
| _engine.InfoCount = infoCount; | ||
|
|
||
| using SqlCommand command = new( | ||
| // Use command text that is intercepted by our engine. | ||
| InfoQueryEngine.CommandText, | ||
| _connection); | ||
| using SqlDataReader reader = command.ExecuteReader(); | ||
|
|
||
| // We should have read past the INFO tokens and determined that there | ||
| // are row results. | ||
| Assert.True(reader.HasRows); | ||
|
|
||
| // Verify that we received the expected INFO messages. | ||
| Assert.Equal(infoCount, (ushort)_infoText.Count); | ||
| for (ushort i = 0; i < infoCount; i++) | ||
| { | ||
| Assert.Equal($"{InfoQueryEngine.InfoPreamble}{i}", _infoText[i]); | ||
| } | ||
|
|
||
| // Verify that we can read the single row. | ||
| Assert.True(reader.Read()); | ||
| Assert.Equal(InfoQueryEngine.RowData, reader.GetString(0)); | ||
| Assert.False(reader.Read()); | ||
| } | ||
| } | ||
|
|
||
| // A writer compatible with TdsUtilities.Log() that pumps accumulated log | ||
| // messages to an xUnit output helper. | ||
| internal sealed class LogWriter : StringWriter | ||
| { | ||
| private readonly ITestOutputHelper _output; | ||
|
|
||
| public LogWriter(ITestOutputHelper output) | ||
| { | ||
| _output = output; | ||
| } | ||
|
|
||
| // The TDSUtilities.Log() method calls Flush() after each operation, so we | ||
| // can use that to emit the accumulated messages here. | ||
| public override void Flush() | ||
| { | ||
| // Get the accumulated buffer. | ||
| var builder = GetStringBuilder(); | ||
|
|
||
| // Trim trailing whitespace, since _output always appends a newline. | ||
| var text = builder.ToString().TrimEnd(); | ||
|
|
||
| // Emit if there's anything worthwhile. | ||
| if (text.Length > 0) | ||
| { | ||
| _output.WriteLine(text); | ||
| base.Flush(); | ||
| } | ||
|
|
||
| // Clear the buffer for the next accumulation. | ||
| builder.Clear(); | ||
| } | ||
| } | ||
|
|
||
| // A query engine that can include INFO tokens in its response. | ||
| internal sealed class InfoQueryEngine : QueryEngine | ||
| { | ||
| // The query text that this engine recognizes and will trigger the | ||
| // inclusion of InfoCount INFO tokens in the response. | ||
| internal const string CommandText = "select Foo from Bar"; | ||
|
|
||
| // The row data that this engine will return for the recognized query. | ||
| internal const string RowData = "Foo Value"; | ||
|
|
||
| // The preamble for all INFO message text. The 0-based index of the INFO | ||
| // token will be appended to this to form the complete message text. | ||
| internal const string InfoPreamble = "Info message "; | ||
|
|
||
| // The number of INFO tokens to include in the response. | ||
| internal ushort InfoCount { get; set; } = 0; | ||
|
|
||
| // Construct with server arguments. | ||
| internal InfoQueryEngine(TdsServerArguments arguments) | ||
| : base(arguments) | ||
| { | ||
| } | ||
|
|
||
| // Override to provide our INFO token response. | ||
| // | ||
| // Calls the base implementation for unrecognized commands. | ||
| // | ||
| protected override TDSMessageCollection CreateQueryResponse( | ||
| ITDSServerSession session, | ||
| TDSSQLBatchToken batchRequest) | ||
| { | ||
| // Defer to the base implementation for unrecognized commands. | ||
| if (batchRequest.Text != CommandText) | ||
| { | ||
| return base.CreateQueryResponse(session, batchRequest); | ||
| } | ||
|
|
||
| // Build a response with the desired number of INFO tokens and then | ||
| // one row result. | ||
| TDSMessage response = new(TDSMessageType.Response); | ||
|
|
||
| // Add the INFO tokens first. | ||
| for (ushort i = 0; i < InfoCount; i++) | ||
| { | ||
| // Choose an error code outside the reserved range. | ||
| TDSInfoToken token = new(30000u + i, 0, 0, $"{InfoPreamble}{i}"); | ||
| response.Add(token); | ||
| TDSUtilities.Log(Log, "INFO Response", token); | ||
| } | ||
|
|
||
| // Add the column metadata. | ||
| TDSColumnData column = new() | ||
| { | ||
| DataType = TDSDataType.NVarChar, | ||
| // Magic foo copied from QueryEngine. | ||
| DataTypeSpecific = new TDSShilohVarCharColumnSpecific( | ||
| 256, new TDSColumnDataCollation(13632521, 52)) | ||
| }; | ||
| column.Flags.Updatable = TDSColumnDataUpdatableFlag.ReadOnly; | ||
|
|
||
| TDSColMetadataToken metadataToken = new(); | ||
| metadataToken.Columns.Add(column); | ||
| response.Add(metadataToken); | ||
|
|
||
| TDSUtilities.Log(Log, "INFO Response", metadataToken); | ||
|
|
||
| // Add the row result data. | ||
| TDSRowToken rowToken = new(metadataToken); | ||
| rowToken.Data.Add(RowData); | ||
| response.Add(rowToken); | ||
| TDSUtilities.Log(Log, "INFO Response", rowToken); | ||
|
|
||
| // Add the done token. | ||
| TDSDoneToken doneToken = new( | ||
| TDSDoneTokenStatusType.Final | | ||
| TDSDoneTokenStatusType.Count, | ||
| TDSDoneTokenCommandType.Select, | ||
| 1); | ||
| response.Add(doneToken); | ||
| TDSUtilities.Log(Log, "INFO Response", doneToken); | ||
|
|
||
| return new(response); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ public class QueryEngine | |
| /// </summary> | ||
| public QueryEngine(TdsServerArguments arguments) | ||
| { | ||
| Log = arguments.Log; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ensures the engine has its Log set, regardless of what TdsServer may decide. |
||
| ServerArguments = arguments; | ||
| } | ||
|
|
||
|
|
@@ -327,8 +328,9 @@ protected virtual TDSMessageCollection CreateQueryResponse(ITDSServerSession ses | |
| } | ||
| else | ||
| { | ||
| // Create an info token that contains the query received | ||
| TDSInfoToken infoToken = new TDSInfoToken(2012, 2, 0, lowerBatchText); | ||
| // Create an info token that contains the verbatim query text we | ||
| // received (not the lower-cased version). | ||
| TDSInfoToken infoToken = new TDSInfoToken(2012, 2, 0, batchRequest.Text); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now the query text in the INFO matches what the test actually sent. |
||
|
|
||
| // Log response | ||
| TDSUtilities.Log(Log, "Response", infoToken); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,15 @@ public TdsServer(TdsServerArguments arguments) : base(arguments) | |
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Constructor with a query engine. Uses the engine's servers arguments. | ||
| /// </summary> | ||
| /// <param name="queryEngine">Query engine</param> | ||
| public TdsServer(QueryEngine queryEngine) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convenience constructor to avoid having to specify ServerArguments twice in the test. |
||
| : base(queryEngine.ServerArguments, queryEngine) | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Constructor with arguments and query engine | ||
| /// </summary> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was expecting this to fail for
infoCount > 1based on #3018, but it passes. I will investigate further.