From 6f5baf033dfe83a3c9a2cc2b4822649f551ac75d Mon Sep 17 00:00:00 2001 From: Chris R Date: Mon, 10 Oct 2016 11:38:52 -0700 Subject: [PATCH] Add ResponseCompression integration tests --- .../NoCompression.conf | 36 + .../NoCompression.config | 1021 +++++++++++++++++ .../ResponseCompressionTests.cs | 238 ++++ .../nginx.conf | 1 + .../project.json | 8 +- .../project.json | 1 + .../StartupResponseCompression.cs | 77 ++ test/ServerComparison.TestSites/project.json | 1 + 8 files changed, 1381 insertions(+), 2 deletions(-) create mode 100644 test/ServerComparison.FunctionalTests/NoCompression.conf create mode 100644 test/ServerComparison.FunctionalTests/NoCompression.config create mode 100644 test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs create mode 100644 test/ServerComparison.TestSites/StartupResponseCompression.cs diff --git a/test/ServerComparison.FunctionalTests/NoCompression.conf b/test/ServerComparison.FunctionalTests/NoCompression.conf new file mode 100644 index 0000000000..1fa5e2f1e7 --- /dev/null +++ b/test/ServerComparison.FunctionalTests/NoCompression.conf @@ -0,0 +1,36 @@ +error_log [errorlog]; +user [user]; +worker_processes 4; +pid [pidFile]; + +events { + worker_connections 768; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 10; + types_hash_max_size 2048; + + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + + access_log [accesslog]; + + gzip off; + + server { + listen [listenPort]; + location / { + proxy_pass [redirectUri]; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + } +} diff --git a/test/ServerComparison.FunctionalTests/NoCompression.config b/test/ServerComparison.FunctionalTests/NoCompression.config new file mode 100644 index 0000000000..a5deeb6f72 --- /dev/null +++ b/test/ServerComparison.FunctionalTests/NoCompression.config @@ -0,0 +1,1021 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs b/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs new file mode 100644 index 0000000000..363d4858fc --- /dev/null +++ b/test/ServerComparison.FunctionalTests/ResponseCompressionTests.cs @@ -0,0 +1,238 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Xunit; +using Xunit.Sdk; + +namespace ServerComparison.FunctionalTests +{ + // Uses ports ranging 5100 - 5130. + public class ResponseCompressionTests + { + // NGinx's default min size is 20 bytes + private static readonly string HelloWorldBody = "Hello World;" + new string('a', 20); + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.Clr, RuntimeArchitecture.x64, "http://localhost:5100/", ApplicationType.Portable)] + [InlineData(ServerType.WebListener, RuntimeFlavor.Clr, RuntimeArchitecture.x64, "http://localhost:5101/", ApplicationType.Portable)] + public Task ResponseCompression_Windows_NoCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckNoCompressionAsync, applicationType, hostCompression: false); + } + + [Theory] + [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5102/", ApplicationType.Portable)] + [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5103/", ApplicationType.Standalone)] + public Task ResponseCompression_Kestrel_NoCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckNoCompressionAsync, applicationType, hostCompression: false); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Windows)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5103/", ApplicationType.Portable)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5104/", ApplicationType.Standalone)] + public Task ResponseCompression_Nginx_NoCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckNoCompressionAsync, applicationType, hostCompression: false); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.Clr, RuntimeArchitecture.x64, "http://localhost:5105/", ApplicationType.Portable)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5106/", ApplicationType.Standalone)] + public Task ResponseCompression_Windows_HostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckHostCompressionAsync, applicationType, hostCompression: true); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Windows)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5107/", ApplicationType.Portable)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5108/", ApplicationType.Standalone)] + public Task ResponseCompression_Nginx_HostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckHostCompressionAsync, applicationType, hostCompression: true); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5109/", ApplicationType.Standalone)] + [InlineData(ServerType.WebListener, RuntimeFlavor.Clr, RuntimeArchitecture.x64, "http://localhost:5110/", ApplicationType.Portable)] + public Task ResponseCompression_Windows_AppCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckAppCompressionAsync, applicationType, hostCompression: false); + } + + [Theory] + [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5111/", ApplicationType.Portable)] + [InlineData(ServerType.Kestrel, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5112/", ApplicationType.Standalone)] + public Task ResponseCompression_Kestrel_AppCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckAppCompressionAsync, applicationType, hostCompression: false); + } + + [ConditionalTheory(Skip = "No pass-through compression https://github.com/aspnet/BasicMiddleware/issues/123")] + [OSSkipCondition(OperatingSystems.Windows)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5113/", ApplicationType.Portable)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5114/", ApplicationType.Standalone)] + public Task ResponseCompression_Nginx_AppCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckHostCompressionAsync, applicationType, hostCompression: false); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5115/", ApplicationType.Standalone)] + [InlineData(ServerType.IISExpress, RuntimeFlavor.Clr, RuntimeArchitecture.x64, "http://localhost:5116/", ApplicationType.Portable)] + public Task ResponseCompression_Windows_AppAndHostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckAppCompressionAsync, applicationType, hostCompression: true); + } + + [ConditionalTheory] + [OSSkipCondition(OperatingSystems.Windows)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5117/", ApplicationType.Portable)] + [InlineData(ServerType.Nginx, RuntimeFlavor.CoreClr, RuntimeArchitecture.x64, "http://localhost:5118/", ApplicationType.Standalone)] + public Task ResponseCompression_Nginx_AppAndHostCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, ApplicationType applicationType) + { + return ResponseCompression(serverType, runtimeFlavor, architecture, applicationBaseUrl, CheckAppCompressionAsync, applicationType, hostCompression: true); + } + + public async Task ResponseCompression(ServerType serverType, RuntimeFlavor runtimeFlavor, RuntimeArchitecture architecture, string applicationBaseUrl, Func scenario, ApplicationType applicationType, bool hostCompression) + { + var logger = new LoggerFactory() + .AddConsole() + .CreateLogger(string.Format("ResponseCompression:{0}:{1}:{2}:{3}", serverType, runtimeFlavor, architecture, applicationType)); + + using (logger.BeginScope("ResponseCompressionTest")) + { + var deploymentParameters = new DeploymentParameters(Helpers.GetApplicationPath(applicationType), serverType, runtimeFlavor, architecture) + { + ApplicationBaseUriHint = applicationBaseUrl, + EnvironmentName = "ResponseCompression", + ServerConfigTemplateContent = Helpers.GetConfigContent(serverType, + hostCompression ? "http.config" : "NoCompression.config", + hostCompression ? "nginx.conf" : "NoCompression.conf"), + SiteName = "HttpTestSite", // This is configured in the Http.config + TargetFramework = runtimeFlavor == RuntimeFlavor.Clr ? "net451" : "netcoreapp1.0", + ApplicationType = applicationType + }; + + using (var deployer = ApplicationDeployerFactory.Create(deploymentParameters, logger)) + { + var deploymentResult = deployer.Deploy(); + var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.None }; + Assert.True(httpClientHandler.SupportsAutomaticDecompression); + var httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(deploymentResult.ApplicationBaseUri) }; + + // Request to base address and check if various parts of the body are rendered & measure the cold startup time. + var response = await RetryHelper.RetryRequest(() => + { + return httpClient.GetAsync(string.Empty); + }, logger, deploymentResult.HostShutdownToken); + + var responseText = await response.Content.ReadAsStringAsync(); + try + { + Assert.Equal("Running", responseText); + } + catch (XunitException) + { + logger.LogWarning(response.ToString()); + logger.LogWarning(responseText); + throw; + } + + await scenario(httpClient, logger); + } + } + } + + private static async Task CheckNoCompressionAsync(HttpClient client, ILogger logger) + { + var request = new HttpRequestMessage(HttpMethod.Get, "NoAppCompression"); + request.Headers.AcceptEncoding.ParseAdd("gzip,deflate"); + var response = await client.SendAsync(request); + var responseText = await response.Content.ReadAsStringAsync(); + try + { + Assert.Equal(HelloWorldBody, responseText); + Assert.Equal(HelloWorldBody.Length.ToString(), GetContentLength(response)); + Assert.Equal(0, response.Content.Headers.ContentEncoding.Count); + } + catch (XunitException) + { + logger.LogWarning(response.ToString()); + logger.LogWarning(responseText); + throw; + } + } + + private static Task CheckHostCompressionAsync(HttpClient client, ILogger logger) + { + return CheckCompressionAsync(client, "NoAppCompression", logger); + } + + private static Task CheckAppCompressionAsync(HttpClient client, ILogger logger) + { + return CheckCompressionAsync(client, "AppCompression", logger); + } + + private static async Task CheckCompressionAsync(HttpClient client, string url, ILogger logger) + { + // Manage the compression manually because HttpClient removes the Content-Encoding header when decompressing. + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.AcceptEncoding.ParseAdd("gzip,deflate"); + var response = await client.SendAsync(request); + var responseText = await response.Content.ReadAsStringAsync(); + try + { + responseText = await ReadCompressedAsStringAsync(response.Content); + Assert.Equal(HelloWorldBody, responseText); + Assert.Equal(1, response.Content.Headers.ContentEncoding.Count); + Assert.Equal("gzip", response.Content.Headers.ContentEncoding.First()); + } + catch (XunitException) + { + logger.LogWarning(response.ToString()); + logger.LogWarning(responseText); + throw; + } + } + + private static string GetContentLength(HttpResponseMessage response) + { + // Don't use response.Content.Headers.ContentLength, it will dynamically calculate the value if it can. + IEnumerable values; + return response.Content.Headers.TryGetValues(HeaderNames.ContentLength, out values) ? values.FirstOrDefault() : null; + } + + private static async Task ReadCompressedAsStringAsync(HttpContent content) + { + using (var stream = await content.ReadAsStreamAsync()) + using (var compressStream = new GZipStream(stream, CompressionMode.Decompress)) + using (var reader = new StreamReader(compressStream)) + { + return await reader.ReadToEndAsync(); + } + } + } +} diff --git a/test/ServerComparison.FunctionalTests/nginx.conf b/test/ServerComparison.FunctionalTests/nginx.conf index 6a4d512f14..1f77013ccc 100644 --- a/test/ServerComparison.FunctionalTests/nginx.conf +++ b/test/ServerComparison.FunctionalTests/nginx.conf @@ -22,6 +22,7 @@ http { access_log [accesslog]; gzip on; + gzip_types text/plain; gzip_disable "msie6"; server { diff --git a/test/ServerComparison.FunctionalTests/project.json b/test/ServerComparison.FunctionalTests/project.json index 5d94093cff..193d39b2d2 100644 --- a/test/ServerComparison.FunctionalTests/project.json +++ b/test/ServerComparison.FunctionalTests/project.json @@ -5,7 +5,9 @@ "include": [ "Http.config", "nginx.conf", - "NtlmAuthentication.config" + "NtlmAuthentication.config", + "NoCompression.config", + "NoCompression.conf" ] } }, @@ -23,7 +25,9 @@ "include": [ "Http.config", "nginx.conf", - "NtlmAuthentication.config" + "NtlmAuthentication.config", + "NoCompression.config", + "NoCompression.conf" ] }, "frameworks": { diff --git a/test/ServerComparison.TestSites.Standalone/project.json b/test/ServerComparison.TestSites.Standalone/project.json index 44f9c8265c..7568511658 100644 --- a/test/ServerComparison.TestSites.Standalone/project.json +++ b/test/ServerComparison.TestSites.Standalone/project.json @@ -1,6 +1,7 @@ { "version": "1.1.0-*", "dependencies": { + "Microsoft.AspNetCore.ResponseCompression": "0.1.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", "Microsoft.AspNetCore.Server.WebListener": "1.1.0-*", diff --git a/test/ServerComparison.TestSites/StartupResponseCompression.cs b/test/ServerComparison.TestSites/StartupResponseCompression.cs new file mode 100644 index 0000000000..c73b67b437 --- /dev/null +++ b/test/ServerComparison.TestSites/StartupResponseCompression.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ServerComparison.TestSites +{ + public class StartupResponseCompression + { + public void ConfigureServices(IServiceCollection services) + { + services.AddResponseCompression("text/plain"); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(minLevel: LogLevel.Warning); + + // NGinx's default min size is 20 bytes + var helloWorldBody = "Hello World;" + new string('a', 20); + + app.Map("/NoAppCompression", subApp => + { + subApp.Run(context => + { + context.Response.ContentType = "text/plain"; + context.Response.ContentLength = helloWorldBody.Length; + return context.Response.WriteAsync(helloWorldBody); + }); + }); + + app.Map("/AppCompression", subApp => + { + subApp.UseResponseCompression(); + subApp.Run(context => + { + context.Response.ContentType = "text/plain"; + context.Response.ContentLength = helloWorldBody.Length; + return context.Response.WriteAsync(helloWorldBody); + }); + }); + /* If we implement DisableResponseBuffering on IISMiddleware + app.Map("/NoBuffer", subApp => + { + subApp.UseResponseCompression(); + subApp.Run(context => + { + context.Features.Get().DisableResponseBuffering(); + context.Response.ContentType = "text/plain"; + context.Response.ContentLength = helloWorldBody.Length; + return context.Response.WriteAsync(helloWorldBody); + }); + }); + */ + app.Run(context => + { + context.Response.ContentType = "text/plain"; + string body; + if (context.Request.Path.Value == "/") + { + body = "Running"; + } + else + { + body = "Not Implemented: " + context.Request.Path; + } + + context.Response.ContentLength = body.Length; + return context.Response.WriteAsync(body); + }); + } + } +} \ No newline at end of file diff --git a/test/ServerComparison.TestSites/project.json b/test/ServerComparison.TestSites/project.json index 98eda87abf..ff368cac84 100644 --- a/test/ServerComparison.TestSites/project.json +++ b/test/ServerComparison.TestSites/project.json @@ -1,6 +1,7 @@ { "version": "1.1.0-*", "dependencies": { + "Microsoft.AspNetCore.ResponseCompression": "0.1.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", "Microsoft.AspNetCore.Server.WebListener": "1.1.0-*",