diff --git a/Microsoft.AspNet.SignalR.sln b/Microsoft.AspNet.SignalR.sln index 4d7f59261c..1171ab9edd 100644 --- a/Microsoft.AspNet.SignalR.sln +++ b/Microsoft.AspNet.SignalR.sln @@ -96,6 +96,7 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Owin.Hosting", "katana\src\Microsoft.Owin.Hosting\Microsoft.Owin.Hosting.csproj", "{C225EB2E-E7A7-463F-B058-1705F204978E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Owin.Samples", "samples\Microsoft.AspNet.Owin.Samples\Microsoft.AspNet.Owin.Samples.csproj", "{5D69B6BE-062B-408D-913E-5CD2F21D5D17}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Tests.Common", "tests\Microsoft.AspNet.SignalR.Tests.Common\Microsoft.AspNet.SignalR.Tests.Common.csproj", "{F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}" EndProject Global @@ -570,13 +571,17 @@ Global {5D69B6BE-062B-408D-913E-5CD2F21D5D17}.Release|x86.ActiveCfg = Release|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|ARM.ActiveCfg = Debug|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|x64.ActiveCfg = Debug|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Debug|x86.ActiveCfg = Debug|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|Any CPU.Build.0 = Release|Any CPU + {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|ARM.ActiveCfg = Release|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|x64.ActiveCfg = Release|Any CPU {F18A8896-10BD-469E-9AA1-AEBACEF4D7B7}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/RegisterHubs.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/RegisterHubs.cs new file mode 100644 index 0000000000..0cd72e74c1 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/RegisterHubs.cs @@ -0,0 +1,73 @@ +using System; +using System.Configuration; +using System.Web; +using System.Web.Routing; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Hubs; + +[assembly: PreApplicationStartMethod(typeof(Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure.IIS.RegisterHubs), "Start")] + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure.IIS +{ + public static class RegisterHubs + { + public static void Start() + { + string keepAliveRaw = ConfigurationManager.AppSettings["keepAlive"]; + string connectionTimeoutRaw = ConfigurationManager.AppSettings["connectionTimeout"]; + string disconnectTimeoutRaw = ConfigurationManager.AppSettings["disconnectTimeout"]; + string heartbeatIntervalRaw = ConfigurationManager.AppSettings["heartbeatInterval"]; + string enableRejoiningGroupsRaw = ConfigurationManager.AppSettings["enableRejoiningGroups"]; + + int keepAlive; + if (Int32.TryParse(keepAliveRaw, out keepAlive)) + { + GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(keepAlive); + } + else + { + GlobalHost.Configuration.KeepAlive = null; + } + + int connectionTimeout; + if (Int32.TryParse(connectionTimeoutRaw, out connectionTimeout)) + { + GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(connectionTimeout); + } + + int disconnectTimeout; + if (Int32.TryParse(disconnectTimeoutRaw, out disconnectTimeout)) + { + GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(disconnectTimeout); + } + + int heartbeatInterval; + if (Int32.TryParse(heartbeatIntervalRaw, out heartbeatInterval)) + { + GlobalHost.Configuration.HeartbeatInterval = TimeSpan.FromSeconds(heartbeatInterval); + } + + bool enableRejoiningGroups; + if (Boolean.TryParse(enableRejoiningGroupsRaw, out enableRejoiningGroups) && + enableRejoiningGroups) + { + GlobalHost.HubPipeline.EnableAutoRejoiningGroups(); + } + + // Register the default hubs route: ~/signalr/hubs + RouteTable.Routes.MapHubs(); + RouteTable.Routes.MapConnection("errors-are-fun", "ErrorsAreFun/{*operation}"); + RouteTable.Routes.MapConnection("group-echo", "group-echo/{*operation}"); + RouteTable.Routes.MapConnection("multisend", "multisend/{*operation}"); + RouteTable.Routes.MapConnection("my-reconnect", "my-reconnect/{*operation}"); + RouteTable.Routes.MapConnection("groups", "groups/{*operation}"); + RouteTable.Routes.MapConnection("rejoin-groups", "rejoin-groups/{*operation}"); + RouteTable.Routes.MapConnection("filter", "filter/{*operation}"); + RouteTable.Routes.MapConnection("items", "items/{*operation}"); + RouteTable.Routes.MapConnection("sync-error", "sync-error/{*operation}"); + + // End point to hit to verify the webserver is up + RouteTable.Routes.Add("test-endpoint", new Route("ping", new TestEndPoint())); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/TestEndPoint.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/TestEndPoint.cs new file mode 100644 index 0000000000..237560dd0b --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/App_Start/TestEndPoint.cs @@ -0,0 +1,26 @@ +using System.Web; +using System.Web.Routing; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class TestEndPoint : IRouteHandler, IHttpHandler + { + public IHttpHandler GetHttpHandler(RequestContext requestContext) + { + return this; + } + + public bool IsReusable + { + get + { + return false; + } + } + + public void ProcessRequest(HttpContext context) + { + context.Response.Write("Pong"); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Build/StartIISTask.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Build/StartIISTask.cs new file mode 100644 index 0000000000..45d0f75163 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Build/StartIISTask.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNet.SignalR.Tests.Common +{ + public class StartIISTask : Task + { + [Required] + public ITaskItem[] HostLocation { get; set; } + + public override bool Execute() + { + var myHost = new IISExpressTestHost(HostLocation[0].ToString()); + + myHost.Initialize(15, 120, 10, 1, false); + + return true; + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/ConnectionThatUsesItems.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/ConnectionThatUsesItems.cs new file mode 100644 index 0000000000..a78028331e --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/ConnectionThatUsesItems.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class ConnectionThatUsesItems : PersistentConnection + { + protected override Task OnConnectedAsync(IRequest request, string connectionId) + { + return PrintEnvironment("OnConnectedAsync", request, connectionId); + } + + protected override Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + return PrintEnvironment("OnReceivedAsync", request, connectionId); + } + + protected override Task OnDisconnectAsync(IRequest request, string connectionId) + { + return PrintEnvironment("OnDisconnectAsync", request, connectionId); + } + + private Task PrintEnvironment(string method, IRequest request, string connectionId) + { + object owinEnv; + if (request.Items.TryGetValue("owin.environment", out owinEnv)) + { + var env = (IDictionary)owinEnv; + return Connection.Broadcast(new + { + method = method, + count = env.Count, + owinKeys = env.Keys, + keys = request.Items.Keys + }); + } + + return Connection.Broadcast(new + { + method = method, + count = 0, + keys = new string[0], + owinKeys = new string[0], + }); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/FilteredConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/FilteredConnection.cs new file mode 100644 index 0000000000..0737049851 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/FilteredConnection.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class FilteredConnection : PersistentConnection + { + protected override Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + return Connection.Broadcast(data, connectionId); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyBadConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyBadConnection.cs new file mode 100644 index 0000000000..7e54f5ff39 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyBadConnection.cs @@ -0,0 +1,16 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MyBadConnection : PersistentConnection + { + protected override Task OnConnectedAsync(IRequest request, string connectionId) + { + // Should throw 404 + using (HttpWebRequest.Create("http://www.microsoft.com/mairyhadalittlelambbut_shelikedhertwinkling_littlestar_better").GetResponse()) { } + + return base.OnConnectedAsync(request, connectionId); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupConnection.cs new file mode 100644 index 0000000000..a5d37e0754 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupConnection.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MyGroupConnection : PersistentConnection + { + protected override Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + JObject operation = JObject.Parse(data); + int type = operation.Value("type"); + string group = operation.Value("group"); + + if (type == 1) + { + return Groups.Add(connectionId, group); + } + else if (type == 2) + { + return Groups.Remove(connectionId, group); + } + else if (type == 3) + { + return Groups.Send(group, operation.Value("message")); + } + + return base.OnReceivedAsync(request, connectionId, data); + } + } + +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupEchoConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupEchoConnection.cs new file mode 100644 index 0000000000..9adbc2d38b --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyGroupEchoConnection.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MyGroupEchoConnection : PersistentConnection + { + protected override Task OnConnectedAsync(IRequest request, string connectionId) + { + return Groups.Send("test", "hey"); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyReconnect.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyReconnect.cs new file mode 100644 index 0000000000..ffca6f55af --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyReconnect.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MyReconnect : PersistentConnection + { + public int Reconnects { get; set; } + + protected override Task OnConnectedAsync(IRequest request, string connectionId) + { + return null; + } + + protected override Task OnReconnectedAsync(IRequest request, string connectionId) + { + Reconnects++; + return base.OnReconnectedAsync(request, connectionId); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyRejoinGroupsConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyRejoinGroupsConnection.cs new file mode 100644 index 0000000000..33fec07476 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MyRejoinGroupsConnection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MyRejoinGroupsConnection : MyGroupConnection + { + protected override IEnumerable OnRejoiningGroups(IRequest request, IEnumerable groups, string connectionId) + { + return groups; + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MySendingConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MySendingConnection.cs new file mode 100644 index 0000000000..0dbafc5d66 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/MySendingConnection.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class MySendingConnection : PersistentConnection + { + protected override Task OnConnectedAsync(IRequest request, string connectionId) + { + Connection.Send(connectionId, "OnConnectedAsync1"); + Connection.Send(connectionId, "OnConnectedAsync2"); + + return base.OnConnectedAsync(request, connectionId); + } + + protected override Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + Connection.Send(connectionId, "OnReceivedAsync1"); + Connection.Send(connectionId, "OnReceivedAsync2"); + + return base.OnReceivedAsync(request, connectionId, data); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/SyncErrorConnection.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/SyncErrorConnection.cs new file mode 100644 index 0000000000..19a53322bd --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Connections/SyncErrorConnection.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.SignalR.FunctionalTests +{ + public class SyncErrorConnection : PersistentConnection + { + protected override Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + throw new InvalidOperationException("This is a bug!"); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/CountDownRange.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/CountDownRange.cs new file mode 100644 index 0000000000..d477d15f48 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/CountDownRange.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Microsoft.AspNet.SignalR.Tests.Infrastructure +{ + internal class CountDownRange + { + private HashSet _items; + private HashSet _seen; + private ManualResetEventSlim _wh = new ManualResetEventSlim(false); + + public CountDownRange(IEnumerable range) + { + _items = new HashSet(range); + _seen = new HashSet(); + } + + public int Count + { + get + { + lock (_items) + { + return _items.Count; + } + } + } + + public IEnumerable Seen + { + get + { + lock (_seen) + { + return _seen.ToList(); + } + } + } + + public IEnumerable Left + { + get + { + lock (_items) + { + return _items.ToList(); + } + } + } + + public bool Mark(T item) + { + lock (_items) + { + if (_items.Remove(item)) + { + if (_items.Count == 0) + { + _wh.Set(); + } + + _seen.Add(item); + + return true; + } + } + + return false; + } + + public bool Wait(TimeSpan timeout) + { + return _wh.Wait(timeout); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostType.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostType.cs new file mode 100644 index 0000000000..cb1ef8d1ec --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostType.cs @@ -0,0 +1,8 @@ +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public enum HostType + { + IISExpress, + Memory + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostedTest.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostedTest.cs new file mode 100644 index 0000000000..8831eec035 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/HostedTest.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.AspNet.SignalR.Client.Http; +using Microsoft.AspNet.SignalR.Client.Transports; +using Microsoft.AspNet.SignalR.Hosting.Memory; + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public abstract class HostedTest : IDisposable + { + protected ITestHost CreateHost(HostType hostType, TransportType transportType) + { + ITestHost host = null; + + switch (hostType) + { + case HostType.IISExpress: + host = new IISExpressTestHost(); + host.TransportFactory = () => CreateTransport(transportType); + host.Transport = host.TransportFactory(); + break; + case HostType.Memory: + var mh = new MemoryHost(); + host = new MemoryTestHost(mh); + host.TransportFactory = () => CreateTransport(transportType, mh); + host.Transport = host.TransportFactory(); + break; + default: + break; + } + + return host; + } + + protected IClientTransport CreateTransport(TransportType transportType) + { + return CreateTransport(transportType, new DefaultHttpClient()); + } + + protected IClientTransport CreateTransport(TransportType transportType, IHttpClient client) + { + switch (transportType) + { + case TransportType.Websockets: + return new WebSocketTransport(client); + case TransportType.ServerSentEvents: + return new ServerSentEventsTransport(client) + { + ConnectionTimeout = TimeSpan.FromSeconds(10) + }; + case TransportType.ForeverFrame: + break; + case TransportType.LongPolling: + return new LongPollingTransport(client); + default: + break; + } + + throw new NotSupportedException("Transport not supported"); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + + public virtual void Dispose() + { + Dispose(true); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/SiteManager.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/SiteManager.cs new file mode 100644 index 0000000000..82f7e64a11 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/SiteManager.cs @@ -0,0 +1,229 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Management; +using System.Net; +using System.Threading; +using Microsoft.Web.Administration; + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure.IIS +{ + public class SiteManager + { + private readonly string _path; + private readonly string _appHostConfigPath; + private readonly ServerManager _serverManager; + + private static Process _iisExpressProcess; + private static int? _existingIISExpressProcessId; + + private static readonly string IISExpressPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "IIS Express", + "iisexpress.exe"); + + private const string TestSiteName = "signalr-test-site"; + private const int TestSitePort = 1337; + private static string TestSiteUrl = String.Format("http://localhost:{0}", TestSitePort); + private static string PingUrl = TestSiteUrl + "/ping"; + + public SiteManager(string path) + { + _path = Path.GetFullPath(path); + _appHostConfigPath = Path.GetFullPath(Path.Combine(_path, "bin", "config", "applicationHost.config")); + _serverManager = new ServerManager(_appHostConfigPath); + } + + public string GetSiteUrl() + { + Site site = _serverManager.Sites[TestSiteName]; + + if (site == null) + { + if (TryGetRunningIIsExpress()) + { + // Kill existing IIS Express process mapping to this application + KillProcess(); + } + + site = _serverManager.Sites.Add(TestSiteName, "http", "*:" + TestSitePort + ":localhost", _path); + site.TraceFailedRequestsLogging.Enabled = true; + + _serverManager.CommitChanges(); + } + + EnsureIISExpressProcess(); + + PingServerUrl(); + + return TestSiteUrl; + } + + private void PingServerUrl() + { + const int retryAttempts = 5; + const int delay = 250; + + int attempt = retryAttempts; + + while (true) + { + var request = HttpWebRequest.Create(PingUrl); + try + { + var response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + break; + } + } + catch + { + if (attempt == 0) + { + throw; + } + } + + attempt--; + Thread.Sleep(delay); + } + } + + public void StopSite() + { + KillProcess(); + } + + private void KillProcess() + { + Process process = _iisExpressProcess; + if (process == null) + { + if (_existingIISExpressProcessId == null) + { + return; + } + + try + { + process = Process.GetProcessById(_existingIISExpressProcessId.Value); + } + catch (ArgumentException) + { + return; + } + catch (InvalidOperationException) + { + return; + } + } + + if (process != null) + { + process.Kill(); + + // Sleep a little + Thread.Sleep(500); + } + } + + private void EnsureIISExpressProcess() + { + if (TryGetRunningIIsExpress()) + { + return; + } + + Process oldProcess = Interlocked.CompareExchange(ref _iisExpressProcess, CreateIISExpressProcess(), null); + if (oldProcess == null) + { + _iisExpressProcess.Start(); + + // Give it a little time to start up the webserver + Thread.Sleep(250); + return; + } + } + + private bool TryGetRunningIIsExpress() + { + // If we have a cached IISExpress id then just use it + if (_existingIISExpressProcessId != null) + { + try + { + var process = Process.GetProcessById(_existingIISExpressProcessId.Value); + + // Make sure it's iis express (Can process ids be reused?) + if (process.ProcessName.Equals("iisexpress")) + { + return true; + } + } + catch (ArgumentException) + { + // The process specified by the processId parameter is not running. The identifier might be expired. + } + catch (InvalidOperationException) + { + // The process ended + } + + _existingIISExpressProcessId = null; + } + + foreach (Process process in Process.GetProcessesByName("iisexpress")) + { + try + { + using (var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id)) + { + foreach (ManagementObject processObj in searcher.Get()) + { + string commandLine = (string)processObj["CommandLine"]; + if (!String.IsNullOrEmpty(commandLine) && + (commandLine.Contains(_appHostConfigPath) || + commandLine.Contains("/site:" + TestSiteName))) + { + _existingIISExpressProcessId = process.Id; + return true; + } + } + } + } + catch (Win32Exception ex) + { + if ((uint)ex.ErrorCode != 0x80004005) + { + throw; + } + } + } + + return false; + } + + private Process CreateIISExpressProcess() + { + if (!File.Exists(IISExpressPath)) + { + throw new InvalidOperationException("Unable to locate IIS Express on the machine"); + } + + var iisExpressProcess = new Process(); + iisExpressProcess.StartInfo = new ProcessStartInfo(IISExpressPath, "/config:\"" + _appHostConfigPath + "\" /site:" + TestSiteName + " /systray:false"); + iisExpressProcess.StartInfo.CreateNoWindow = true; + iisExpressProcess.StartInfo.UseShellExecute = false; + iisExpressProcess.EnableRaisingEvents = true; + iisExpressProcess.Exited += OnIIsExpressQuit; + + return iisExpressProcess; + } + + private void OnIIsExpressQuit(object sender, EventArgs e) + { + Interlocked.Exchange(ref _iisExpressProcess, null); + } + } +} \ No newline at end of file diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/site.web.config b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/site.web.config new file mode 100644 index 0000000000..0cdbd78ac9 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IIS/site.web.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IISExpressTestHost.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IISExpressTestHost.cs new file mode 100644 index 0000000000..19373c6ac1 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/IISExpressTestHost.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNet.SignalR.Client.Transports; +using Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure.IIS; +using System; +using System.IO; + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public class IISExpressTestHost : ITestHost + { + private readonly SiteManager _siteManager; + private readonly string _path; + private readonly string _webConfigPath; + + private static readonly Lazy _webConfigTemplate = new Lazy(() => GetConfig()); + + public IISExpressTestHost() + : this(Path.Combine(Directory.GetCurrentDirectory(), "..")) + { + } + + public IISExpressTestHost(string path) + { + // The path to the site is the test path. + // We treat the test output path just like a site. This makes it super + // cheap to create and tear down sites. We don't need to copy any files. + // The downside is that we can't run tests in parallel anymore. + _path = path; + + // Set the web.config path for this app + _webConfigPath = Path.Combine(_path, "web.config"); + + // Create the site manager + _siteManager = new SiteManager(_path); + } + + public string Url { get; private set; } + + public IClientTransport Transport { get; set; } + + public Func TransportFactory { get; set; } + + public void Initialize(int? keepAlive, + int? connectionTimeout, + int? disconnectTimeout, + int? hearbeatInterval, + bool enableAutoRejoiningGroups) + { + Url = _siteManager.GetSiteUrl(); + + // Use a configuration file to specify values + string content = String.Format(_webConfigTemplate.Value, + keepAlive, + connectionTimeout, + disconnectTimeout, + hearbeatInterval, + enableAutoRejoiningGroups); + + File.WriteAllText(_webConfigPath, content); + } + + public void Dispose() + { + Shutdown(); + } + + public void Shutdown() + { + _siteManager.StopSite(); + } + + private static string GetConfig() + { + using (Stream resourceStream = typeof(IISExpressTestHost).Assembly.GetManifestResourceStream("Microsoft.AspNet.SignalR.Tests.Common.Infrastructure.IIS.site.web.config")) + { + var reader = new StreamReader(resourceStream); + return reader.ReadToEnd(); + } + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/ITestHost.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/ITestHost.cs new file mode 100644 index 0000000000..f89dfb4ef7 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/ITestHost.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNet.SignalR.Client.Transports; + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public interface ITestHost : IDisposable + { + string Url { get; } + + IClientTransport Transport { get; set; } + + Func TransportFactory { get; set; } + + void Initialize(int? keepAlive = 15, + int? connectionTimeout = 120, + int? disconnectTimeout = 40, + int? hearbeatInterval = 10, + bool enableAutoRejoiningGroups = false); + + void Shutdown(); + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/MemoryTestHost.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/MemoryTestHost.cs new file mode 100644 index 0000000000..5d5e293f6b --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/MemoryTestHost.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.AspNet.SignalR.Hubs; +using Microsoft.AspNet.SignalR.Client.Transports; +using Microsoft.AspNet.SignalR.Hosting.Memory; + +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public class MemoryTestHost : ITestHost + { + private readonly MemoryHost _host; + + public MemoryTestHost(MemoryHost host) + { + _host = host; + } + + public string Url + { + get + { + return "http://memoryhost"; + } + } + + public IClientTransport Transport { get; set; } + + public Func TransportFactory { get; set; } + + public void Initialize(int? keepAlive, + int? connectionTimeout, + int? disconnectTimeout, + int? hearbeatInterval, + bool enableAutoRejoiningGroups) + { + if (keepAlive != null) + { + _host.Configuration.KeepAlive = TimeSpan.FromSeconds(keepAlive.Value); + } + else + { + _host.Configuration.KeepAlive = null; + } + + if (connectionTimeout != null) + { + _host.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(connectionTimeout.Value); + } + + if (disconnectTimeout != null) + { + _host.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(disconnectTimeout.Value); + } + + if (hearbeatInterval != null) + { + _host.Configuration.HeartbeatInterval = TimeSpan.FromSeconds(hearbeatInterval.Value); + } + + if (enableAutoRejoiningGroups) + { + _host.HubPipeline.EnableAutoRejoiningGroups(); + } + + _host.MapHubs(); + _host.MapConnection("/ErrorsAreFun"); + _host.MapConnection("/group-echo"); + _host.MapConnection("/multisend"); + _host.MapConnection("/my-reconnect"); + _host.MapConnection("/groups"); + _host.MapConnection("/rejoin-groups"); + _host.MapConnection("/filter"); + _host.MapConnection("/sync-error"); + } + + public void Dispose() + { + _host.Dispose(); + } + + public void Shutdown() + { + Dispose(); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/RequestItemsResponse.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/RequestItemsResponse.cs new file mode 100644 index 0000000000..92a137bbe7 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/RequestItemsResponse.cs @@ -0,0 +1,20 @@ +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public class RequestItemsResponse + { + public string Method { get; set; } + public int Count { get; set; } + public string[] OwinKeys { get; set; } + public string[] Keys { get; set; } + + public override int GetHashCode() + { + return Method.GetHashCode(); + } + + public override bool Equals(object obj) + { + return Method.Equals(((RequestItemsResponse)obj).Method); + } + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/TransportType.cs b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/TransportType.cs new file mode 100644 index 0000000000..5535f39c79 --- /dev/null +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Infrastructure/TransportType.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNet.SignalR.FunctionalTests.Infrastructure +{ + public enum TransportType + { + Websockets, + ServerSentEvents, + ForeverFrame, + LongPolling, + } +} diff --git a/tests/Microsoft.AspNet.SignalR.Tests.Common/Microsoft.AspNet.SignalR.Tests.Common.csproj b/tests/Microsoft.AspNet.SignalR.Tests.Common/Microsoft.AspNet.SignalR.Tests.Common.csproj index 76a7ee79fe..73967b5c80 100644 --- a/tests/Microsoft.AspNet.SignalR.Tests.Common/Microsoft.AspNet.SignalR.Tests.Common.csproj +++ b/tests/Microsoft.AspNet.SignalR.Tests.Common/Microsoft.AspNet.SignalR.Tests.Common.csproj @@ -11,6 +11,8 @@ Microsoft.AspNet.SignalR.Tests.Common v4.5 512 + ..\..\ + true true @@ -30,6 +32,15 @@ 4 + + + + False + + + False + ..\..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + @@ -76,8 +87,29 @@ Hubs\DemoHub.cs + + + + + + + + + + + + + + + + + + + + + @@ -105,12 +137,15 @@ {202fa02d-cc05-40d2-a574-4bd34e47f575} Microsoft.AspNet.SignalR.SystemWeb45 - - {fba09237-84cc-4383-bd12-cdf58e4020e8} - Microsoft.AspNet.SignalR.Tests - + + + + + + +