Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor Author

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 > 1 based on #3018, but it passes. I will investigate further.


// 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
Expand Up @@ -33,6 +33,7 @@ public class QueryEngine
/// </summary>
public QueryEngine(TdsServerArguments arguments)
{
Log = arguments.Log;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}

Expand Down Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
Expand Down
Loading
Loading