Skip to content

Commit 1abb7b1

Browse files
committed
Add simple fake MySQL Server.
This will allow unit tests of the internal state of the connector to be created, without needing a full MySQL Server running.
1 parent ad25cb4 commit 1abb7b1

File tree

5 files changed

+318
-1
lines changed

5 files changed

+318
-1
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.IO;
2+
using System.Text;
3+
4+
namespace MySqlConnector.Tests
5+
{
6+
public static class BinaryWriterExtensions
7+
{
8+
public static void WriteRaw(this BinaryWriter writer, string value) => writer.Write(Encoding.UTF8.GetBytes(value));
9+
10+
public static void WriteNullTerminated(this BinaryWriter writer, string value)
11+
{
12+
writer.Write(Encoding.UTF8.GetBytes(value));
13+
writer.Write((byte) 0);
14+
}
15+
}
16+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
using MySql.Data.MySqlClient;
5+
using Xunit;
6+
7+
namespace MySqlConnector.Tests
8+
{
9+
public class ConnectionTests : IDisposable
10+
{
11+
public ConnectionTests()
12+
{
13+
m_server = new FakeMySqlServer();
14+
m_server.Start();
15+
16+
m_csb = new MySqlConnectionStringBuilder
17+
{
18+
Server = "localhost",
19+
Port = (uint) m_server.Port,
20+
};
21+
}
22+
23+
public void Dispose()
24+
{
25+
m_server.Stop();
26+
}
27+
28+
[Fact]
29+
public void PooledConnectionIsReturnedToPool()
30+
{
31+
Assert.Equal(0, m_server.ActiveConnections);
32+
33+
m_csb.Pooling = true;
34+
using (var connection = new MySqlConnection(m_csb.ConnectionString))
35+
{
36+
connection.Open();
37+
Assert.Equal(1, m_server.ActiveConnections);
38+
39+
Assert.Equal(m_server.ServerVersion, connection.ServerVersion);
40+
connection.Close();
41+
Assert.Equal(1, m_server.ActiveConnections);
42+
}
43+
44+
Assert.Equal(1, m_server.ActiveConnections);
45+
}
46+
47+
[Fact]
48+
public async Task UnpooledConnectionIsClosed()
49+
{
50+
Assert.Equal(0, m_server.ActiveConnections);
51+
52+
m_csb.Pooling = false;
53+
using (var connection = new MySqlConnection(m_csb.ConnectionString))
54+
{
55+
await connection.OpenAsync();
56+
Assert.Equal(1, m_server.ActiveConnections);
57+
58+
Assert.Equal(m_server.ServerVersion, connection.ServerVersion);
59+
connection.Close();
60+
61+
await WaitForConditionAsync(0, () => m_server.ActiveConnections);
62+
}
63+
}
64+
65+
private static async Task WaitForConditionAsync<T>(T expected, Func<T> getValue)
66+
{
67+
var sw = Stopwatch.StartNew();
68+
while (sw.ElapsedMilliseconds < 1000 && !expected.Equals(getValue()))
69+
await Task.Delay(50);
70+
Assert.Equal(expected, getValue());
71+
}
72+
73+
readonly FakeMySqlServer m_server;
74+
readonly MySqlConnectionStringBuilder m_csb;
75+
}
76+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net;
4+
using System.Net.Sockets;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace MySqlConnector.Tests
9+
{
10+
public sealed class FakeMySqlServer
11+
{
12+
public FakeMySqlServer()
13+
{
14+
m_tcpListener = new TcpListener(IPAddress.Any, 0);
15+
m_tasks = new List<Task>();
16+
}
17+
18+
public void Start()
19+
{
20+
m_activeConnections = 0;
21+
m_cts = new CancellationTokenSource();
22+
m_tcpListener.Start();
23+
m_tasks.Add(AcceptConnectionsAsync());
24+
}
25+
26+
public void Stop()
27+
{
28+
m_cts.Cancel();
29+
m_tcpListener.Stop();
30+
try
31+
{
32+
Task.WaitAll(m_tasks.ToArray());
33+
}
34+
catch (AggregateException)
35+
{
36+
}
37+
m_tasks.Clear();
38+
m_cts.Dispose();
39+
m_cts = null;
40+
}
41+
42+
public int Port => ((IPEndPoint) m_tcpListener.LocalEndpoint).Port;
43+
44+
public int ActiveConnections => m_activeConnections;
45+
46+
public string ServerVersion { get; set; } = "5.7.10-test";
47+
48+
internal void ClientDisconnected() => Interlocked.Decrement(ref m_activeConnections);
49+
50+
private async Task AcceptConnectionsAsync()
51+
{
52+
while (true)
53+
{
54+
var tcpClient = await m_tcpListener.AcceptTcpClientAsync();
55+
Interlocked.Increment(ref m_activeConnections);
56+
lock (m_tasks)
57+
{
58+
var connection = new FakeMySqlServerConnection(this, m_tasks.Count);
59+
m_tasks.Add(connection.RunAsync(tcpClient, m_cts.Token));
60+
}
61+
}
62+
}
63+
64+
readonly TcpListener m_tcpListener;
65+
readonly List<Task> m_tasks;
66+
CancellationTokenSource m_cts;
67+
int m_activeConnections;
68+
}
69+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.IO;
3+
using System.Net.Sockets;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using MySql.Data.MySqlClient;
8+
using MySql.Data.Serialization;
9+
10+
namespace MySqlConnector.Tests
11+
{
12+
internal sealed class FakeMySqlServerConnection
13+
{
14+
public FakeMySqlServerConnection(FakeMySqlServer server, int connectionId)
15+
{
16+
m_server = server ?? throw new ArgumentNullException(nameof(server));
17+
m_connectionId = connectionId;
18+
}
19+
20+
public async Task RunAsync(TcpClient client, CancellationToken token)
21+
{
22+
try
23+
{
24+
using (token.Register(client.Dispose))
25+
using (client)
26+
using (var stream = client.GetStream())
27+
{
28+
await SendAsync(stream, 0, WriteInitialHandshake);
29+
await ReadPayloadAsync(stream, token); // handshake response
30+
await SendAsync(stream, 2, WriteOk);
31+
32+
var keepRunning = true;
33+
while (keepRunning)
34+
{
35+
var bytes = await ReadPayloadAsync(stream, token);
36+
switch ((CommandKind) bytes[0])
37+
{
38+
case CommandKind.Quit:
39+
await SendAsync(stream, 1, WriteOk);
40+
keepRunning = false;
41+
break;
42+
43+
case CommandKind.Ping:
44+
case CommandKind.Query:
45+
case CommandKind.ResetConnection:
46+
await SendAsync(stream, 1, WriteOk);
47+
break;
48+
49+
default:
50+
Console.WriteLine("** UNHANDLED ** {0}", (CommandKind) bytes[0]);
51+
await SendAsync(stream, 1, WriteError);
52+
break;
53+
}
54+
}
55+
}
56+
}
57+
finally
58+
{
59+
m_server.ClientDisconnected();
60+
}
61+
}
62+
63+
private static async Task SendAsync(Stream stream, int sequenceNumber, Action<BinaryWriter> writePayload)
64+
{
65+
var packet = MakePayload(sequenceNumber, writePayload);
66+
await stream.WriteAsync(packet, 0, packet.Length);
67+
}
68+
69+
private static byte[] MakePayload(int sequenceNumber, Action<BinaryWriter> writePayload)
70+
{
71+
using (var memoryStream = new MemoryStream())
72+
{
73+
using (var writer = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
74+
{
75+
writer.Write(default(int));
76+
writePayload(writer);
77+
memoryStream.Position = 0;
78+
writer.Write(((int) (memoryStream.Length - 4)) | ((sequenceNumber % 256) << 24));
79+
}
80+
return memoryStream.ToArray();
81+
}
82+
}
83+
84+
private static async Task<byte[]> ReadPayloadAsync(Stream stream, CancellationToken token)
85+
{
86+
var header = await ReadBytesAsync(stream, 4, token);
87+
var length = header[0] | (header[1] << 8) | (header[2] << 16);
88+
var sequenceNumber = header[3];
89+
return await ReadBytesAsync(stream, length, token);
90+
}
91+
92+
private static async Task<byte[]> ReadBytesAsync(Stream stream, int count, CancellationToken token)
93+
{
94+
var bytes = new byte[count];
95+
for (var bytesRead = 0; bytesRead < count;)
96+
bytesRead += await stream.ReadAsync(bytes, bytesRead, count - bytesRead, token);
97+
return bytes;
98+
}
99+
100+
private void WriteInitialHandshake(BinaryWriter writer)
101+
{
102+
var random = new Random(1);
103+
var authData = new byte[20];
104+
random.NextBytes(authData);
105+
var capabilities =
106+
ProtocolCapabilities.LongPassword |
107+
ProtocolCapabilities.FoundRows |
108+
ProtocolCapabilities.LongFlag |
109+
ProtocolCapabilities.IgnoreSpace |
110+
ProtocolCapabilities.Protocol41 |
111+
ProtocolCapabilities.Transactions |
112+
ProtocolCapabilities.SecureConnection |
113+
ProtocolCapabilities.MultiStatements |
114+
ProtocolCapabilities.MultiResults |
115+
ProtocolCapabilities.PluginAuth |
116+
ProtocolCapabilities.ConnectionAttributes |
117+
ProtocolCapabilities.PluginAuthLengthEncodedClientData;
118+
119+
writer.Write((byte) 10); // protocol version
120+
writer.WriteNullTerminated(m_server.ServerVersion); // server version
121+
writer.Write(m_connectionId); // conection ID
122+
writer.Write(authData, 0, 8); // auth plugin data part 1
123+
writer.Write((byte) 0); // filler
124+
writer.Write((ushort) capabilities);
125+
writer.Write((byte) CharacterSet.Utf8Binary); // character set
126+
writer.Write((ushort) 0); // status flags
127+
writer.Write((ushort) ((uint) capabilities >> 16));
128+
writer.Write((byte) authData.Length);
129+
writer.Write(new byte[10]); // reserved
130+
writer.Write(authData, 8, authData.Length - 8);
131+
if (authData.Length - 8 < 13)
132+
writer.Write(new byte[13 - (authData.Length - 8)]); // have to write at least 13 bytes
133+
writer.WriteNullTerminated("mysql_native_password");
134+
}
135+
136+
private static void WriteOk(BinaryWriter writer)
137+
{
138+
writer.Write((byte) 0); // signature
139+
writer.Write((byte) 0); // 0 rows affected
140+
writer.Write((byte) 0); // last insert ID
141+
writer.Write((ushort) 0); // server status
142+
writer.Write((ushort) 0); // warning count
143+
}
144+
145+
private static void WriteError(BinaryWriter writer)
146+
{
147+
writer.Write((byte) 0xFF); // signature
148+
writer.Write((ushort) MySqlErrorCode.UnknownError); // error code
149+
writer.WriteRaw("#ERROR");
150+
writer.WriteRaw("An unknown error occurred");
151+
}
152+
153+
readonly FakeMySqlServer m_server;
154+
readonly int m_connectionId;
155+
}
156+
}

tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
<ItemGroup Condition=" '$(Configuration)' == 'Baseline' ">
3333
<PackageReference Include="MySql.Data" Version="6.9.9" />
34-
<Compile Remove="NormalizeTests.cs;TypeMapperTests.cs;Utf8Tests.cs" />
34+
<Compile Remove="ConnectionTests.cs;FakeMySqlServer.cs;FakeMySqlServerConnection.cs;NormalizeTests.cs;TypeMapperTests.cs;Utf8Tests.cs" />
3535
</ItemGroup>
3636

3737
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">

0 commit comments

Comments
 (0)