Simplify instrumentation confirmations in `RazorPageExecutionInstrumentationTest`

- test class can now use the `MvcTestFixture`
  - #3139 part 3 of 3
- dump instrumentation data at end of `_Layout.cshtml`
  - include `FilePath` in display
  - compare against new `.html` resource

nits:
- normalize line endings to CRLF for consistency with other tests
- add `InstrumentionData` to avoid `Tuple`
- make `TestPageExecutionContext` class `private`
- remove newly-unused actions and associated `.cshtml` files
This commit is contained in:
Doug Bunting 2015-10-28 10:03:34 -07:00
parent dc32f8ae33
commit a69a7a6940
12 changed files with 154 additions and 257 deletions

View File

@ -1,213 +1,43 @@
// 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.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.Extensions.DependencyInjection;
using RazorPageExecutionInstrumentationWebSite;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class RazorPageExecutionInstrumentationTest
public class RazorPageExecutionInstrumentationTest : IClassFixture<MvcTestFixture<Startup>>
{
private const string SiteName = nameof(RazorPageExecutionInstrumentationWebSite);
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
private readonly Action<IServiceCollection> _configureServices = new Startup().ConfigureServices;
private static readonly Assembly _resourcesAssembly =
typeof(RazorPageExecutionInstrumentationTest).GetTypeInfo().Assembly;
public static IEnumerable<object[]> InstrumentationData
public RazorPageExecutionInstrumentationTest(MvcTestFixture<Startup> fixture)
{
get
{
var expected = @"<div>
2147483647
viewstart-content
<p class=""Hello world"">
page-content
</p>
</div>";
var expectedLineMappings = new[]
{
Tuple.Create(92, 16, false),
Tuple.Create(108, 1, true),
Tuple.Create(0, 2, true),
Tuple.Create(2, 8, true),
Tuple.Create(10, 16, false),
Tuple.Create(26, 1, true),
Tuple.Create(27, 19, true),
Tuple.Create(0, 6, true),
Tuple.Create(7, 12, false),
Tuple.Create(19, 1, true),
Tuple.Create(21, 12, false),
Tuple.Create(33, 7, true),
};
yield return new object[] { "FullPath", expected, expectedLineMappings };
yield return new object[] { "ViewDiscoveryPath", expected, expectedLineMappings };
var expected2 = @"<div>
2147483647
viewstart-content
view-with-partial-content
<p class=""class"">partial-content</p>
<p class=""class"">partial-content</p>
</div>";
var expectedLineMappings2 = new[]
{
Tuple.Create(92, 16, false),
Tuple.Create(108, 1, true),
Tuple.Create(0, 26, true),
Tuple.Create(27, 39, false),
// Html.PartialAsync()
Tuple.Create(28, 2, true),
Tuple.Create(30, 8, true),
Tuple.Create(38, 4, false),
Tuple.Create(42, 1, true),
Tuple.Create(43, 20, true),
Tuple.Create(66, 1, true),
// Html.RenderPartial()
Tuple.Create(28, 2, true),
Tuple.Create(30, 8, true),
Tuple.Create(38, 4, false),
Tuple.Create(42, 1, true),
Tuple.Create(43, 20, true),
Tuple.Create(0, 6, true),
Tuple.Create(7, 12, false),
Tuple.Create(19, 1, true),
Tuple.Create(21, 12, false),
Tuple.Create(33, 7, true)
};
yield return new object[] { "ViewWithPartial", expected2, expectedLineMappings2 };
}
Client = fixture.Client;
}
[Theory]
[MemberData(nameof(InstrumentationData))]
public async Task ViewsAreServedWithoutInstrumentationByDefault(
string actionName,
string expected,
IEnumerable<Tuple<int, int, bool>> ignored)
{
// Arrange
var context = new TestPageExecutionContext();
var server = TestHelper.CreateServer(_app, SiteName, services =>
{
services.AddInstance(context);
_configureServices(services);
});
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/Home/" + actionName);
// Assert
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
Assert.Empty(context.Values);
}
[Theory]
[MemberData(nameof(InstrumentationData))]
public async Task ViewsAreInstrumentedWhenPageExecutionListenerFeatureIsEnabled(
string actionName,
string expected,
IEnumerable<Tuple<int, int, bool>> expectedLineMappings)
{
// Arrange
var context = new TestPageExecutionContext();
var server = TestHelper.CreateServer(_app, SiteName, services =>
{
services.AddInstance(context);
_configureServices(services);
});
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true");
// Act
var body = await client.GetStringAsync("http://localhost/Home/" + actionName);
// Assert
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
Assert.Equal(expectedLineMappings, context.Values);
}
[Theory]
[MemberData(nameof(InstrumentationData))]
public async Task ViewsCanSwitchFromRegularToInstrumented(
string actionName,
string expected,
IEnumerable<Tuple<int, int, bool>> expectedLineMappings)
{
// Arrange - 1
var context = new TestPageExecutionContext();
var server = TestHelper.CreateServer(_app, SiteName, services =>
{
services.AddInstance(context);
_configureServices(services);
});
var client = server.CreateClient();
// Act - 1
var body = await client.GetStringAsync("http://localhost/Home/" + actionName);
// Assert - 1
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
Assert.Empty(context.Values);
// Arrange - 2
client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true");
// Act - 2
body = await client.GetStringAsync("http://localhost/Home/" + actionName);
// Assert - 2
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
Assert.Equal(expectedLineMappings, context.Values);
}
public HttpClient Client { get; }
[Fact]
public async Task SwitchingFromNonInstrumentedToInstrumentedWorksForLayoutAndViewStarts()
public async Task InstrumentedViews_RenderAsExpected()
{
// Arrange - 1
var expectedLineMappings = new[]
{
Tuple.Create(92, 16, false),
Tuple.Create(108, 1, true),
Tuple.Create(0, 2, true),
Tuple.Create(2, 8, true),
Tuple.Create(10, 16, false),
Tuple.Create(26, 1, true),
Tuple.Create(27, 19, true),
Tuple.Create(0, 6, true),
Tuple.Create(7, 12, false),
Tuple.Create(19, 1, true),
Tuple.Create(21, 12, false),
Tuple.Create(33, 7, true),
};
var context = new TestPageExecutionContext();
var server = TestHelper.CreateServer(_app, SiteName, services =>
{
services.AddInstance(context);
_configureServices(services);
});
var client = server.CreateClient();
// Arrange
var outputFile = "compiler/resources/RazorPageExecutionInstrumentationWebSite.Home.ViewWithPartial.html";
var expectedContent =
await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false);
// Act - 1
var body = await client.GetStringAsync("http://localhost/Home/FullPath");
// Act
var content = await Client.GetStringAsync("http://localhost/Home/ViewWithPartial");
// Assert - 1
Assert.Empty(context.Values);
// Arrange - 2
client.DefaultRequestHeaders.Add("ENABLE-RAZOR-INSTRUMENTATION", "true");
// Act - 2
body = await client.GetStringAsync("http://localhost/Home/ViewDiscoveryPath");
// Assert - 2
Assert.Equal(expectedLineMappings, context.Values);
// Assert
#if GENERATE_BASELINES
ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, content);
#else
Assert.Equal(expectedContent, content, ignoreLineEndingDifferences: true);
#endif
}
}
}

View File

@ -0,0 +1,27 @@
<div>
2147483647
viewstart-content
view-with-partial-content
<p class="class">partial-content</p>
<p class="class">partial-content</p>
</div>
<p>/Views/_ViewStart.cshtml: Non-literal at 96 contains 16 characters.</p>
<p>/Views/_ViewStart.cshtml: Literal at 112 contains 2 characters.</p>
<p>/Views/Home/ViewWithPartial.cshtml: Literal at 0 contains 27 characters.</p>
<p>/Views/Home/ViewWithPartial.cshtml: Non-literal at 28 contains 39 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 31 contains 2 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 33 contains 8 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Non-literal at 41 contains 4 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 45 contains 1 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 46 contains 20 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 67 contains 2 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 31 contains 2 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 33 contains 8 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Non-literal at 41 contains 4 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 45 contains 1 characters.</p>
<p>/Views/Home/_PartialView.cshtml: Literal at 46 contains 20 characters.</p>
<p>/Views/_Layout.cshtml: Literal at 82 contains 7 characters.</p>
<p>/Views/_Layout.cshtml: Non-literal at 90 contains 12 characters.</p>
<p>/Views/_Layout.cshtml: Literal at 102 contains 2 characters.</p>
<p>/Views/_Layout.cshtml: Non-literal at 105 contains 12 characters.</p>
<p>/Views/_Layout.cshtml: Literal at 117 contains 10 characters.</p>

View File

@ -7,16 +7,6 @@ namespace RazorPageExecutionInstrumentationWebSite
{
public class HomeController : Controller
{
public ActionResult FullPath()
{
return View("/Views/Home/FullPath.cshtml");
}
public ActionResult ViewDiscoveryPath()
{
return View();
}
public ActionResult ViewWithPartial()
{
return View();

View File

@ -0,0 +1,14 @@
// 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.Collections.Generic;
namespace RazorPageExecutionInstrumentationWebSite
{
public interface IHoldInstrumentationData
{
IEnumerable<InstrumentationData> Values { get; }
void IgnoreFurtherData();
}
}

View File

@ -0,0 +1,31 @@
// 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.
namespace RazorPageExecutionInstrumentationWebSite
{
public class InstrumentationData
{
public InstrumentationData(string filePath, int position, int length, bool isLiteral)
{
FilePath = filePath;
Position = position;
Length = length;
IsLiteral = isLiteral;
}
public string FilePath { get; }
public int Position { get; }
public int Length { get; }
public bool IsLiteral { get; }
public override string ToString()
{
var literal = IsLiteral ? "Literal" : "Non-literal";
return $"{FilePath}: {literal} at {Position} contains {Length} characters.";
}
}
}

View File

@ -9,7 +9,6 @@ using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace RazorPageExecutionInstrumentationWebSite
{
@ -19,26 +18,28 @@ namespace RazorPageExecutionInstrumentationWebSite
public void ConfigureServices(IServiceCollection services)
{
// Normalize line endings to avoid changes in instrumentation locations between systems.
services.TryAdd(ServiceDescriptor.Transient<IRazorCompilationService, TestRazorCompilationService>());
services.AddTransient<IRazorCompilationService, TestRazorCompilationService>();
// Add MVC services to the services container
// Add MVC services to the services container.
services.AddMvc();
// Make instrumentation data available in views.
services.AddScoped<TestPageExecutionListenerFeature, TestPageExecutionListenerFeature>();
services.AddScoped<IHoldInstrumentationData>(
provider => provider.GetRequiredService<TestPageExecutionListenerFeature>().Holder);
}
public void Configure(IApplicationBuilder app)
{
app.UseCultureReplacer();
app.Use(async (HttpContext context, Func<Task> next) =>
// Execute views with instrumentation enabled.
app.Use((HttpContext context, Func<Task> next) =>
{
if (!string.IsNullOrEmpty(context.Request.Headers["ENABLE-RAZOR-INSTRUMENTATION"]))
{
var pageExecutionContext = context.ApplicationServices.GetRequiredService<TestPageExecutionContext>();
var listenerFeature = new TestPageExecutionListenerFeature(pageExecutionContext);
context.Features.Set<IPageExecutionListenerFeature>(listenerFeature);
}
var listenerFeature = context.RequestServices.GetRequiredService<TestPageExecutionListenerFeature>();
context.Features.Set<IPageExecutionListenerFeature>(listenerFeature);
await next();
return next();
});
// Add MVC to the request pipeline

View File

@ -1,24 +0,0 @@
// 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 Microsoft.AspNet.PageExecutionInstrumentation;
namespace RazorPageExecutionInstrumentationWebSite
{
public class TestPageExecutionContext : IPageExecutionContext
{
public List<Tuple<int, int, bool>> Values { get; }
= new List<Tuple<int, int, bool>>();
public void BeginContext(int position, int length, bool isLiteral)
{
Values.Add(Tuple.Create(position, length, isLiteral));
}
public void EndContext()
{
}
}
}

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.IO;
using Microsoft.AspNet.PageExecutionInstrumentation;
@ -8,12 +9,9 @@ namespace RazorPageExecutionInstrumentationWebSite
{
public class TestPageExecutionListenerFeature : IPageExecutionListenerFeature
{
private readonly IPageExecutionContext _context;
private readonly TestPageExecutionContext _executionContext = new TestPageExecutionContext();
public TestPageExecutionListenerFeature(IPageExecutionContext context)
{
_context = context;
}
public IHoldInstrumentationData Holder => _executionContext;
public TextWriter DecorateWriter(TextWriter writer)
{
@ -22,7 +20,38 @@ namespace RazorPageExecutionInstrumentationWebSite
public IPageExecutionContext GetContext(string sourceFilePath, TextWriter writer)
{
return _context;
_executionContext.FilePath = sourceFilePath;
return _executionContext;
}
private class TestPageExecutionContext : IHoldInstrumentationData, IPageExecutionContext
{
private readonly List<InstrumentationData> _values = new List<InstrumentationData>();
private bool _ignoreNewData;
public string FilePath { get; set; }
public IEnumerable<InstrumentationData> Values => _values;
public void IgnoreFurtherData()
{
_ignoreNewData = true;
}
public void BeginContext(int position, int length, bool isLiteral)
{
if (_ignoreNewData)
{
return;
}
_values.Add(new InstrumentationData(FilePath, position, length, isLiteral));
}
public void EndContext()
{
}
}
}
}

View File

@ -22,16 +22,13 @@ namespace RazorPageExecutionInstrumentationWebSite
protected override GeneratorResults GenerateCode(string relativePath, Stream inputStream)
{
// Normalize line endings to '\n' (LF). This removes core.autocrlf, core.eol, core.safecrlf, and
// .gitattributes from the equation and treats "\r\n", "\r", and "\n" as equivalent. Does not handle
// some obscure line endings (e.g. "\n\r") but otherwise ensures instrumentation locations are
// consistent.
// Normalize line endings to '\r\n' (CRLF). This removes core.autocrlf, core.eol, core.safecrlf, and
// .gitattributes from the equation and treats "\r\n" and "\n" as equivalent. Does not handle
// some line endings like "\r" but otherwise ensures checksums and line mappings are consistent.
string text;
using (var streamReader = new StreamReader(inputStream))
{
text = streamReader.ReadToEnd()
.Replace("\r\n", "\n") // Windows line endings
.Replace("\r", "\n"); // Older Mac OS line endings
text = streamReader.ReadToEnd().Replace("\r", "").Replace("\n", "\r\n");
}
var bytes = Encoding.UTF8.GetBytes(text);

View File

@ -1,3 +0,0 @@
<p class="@("Hello world")">
page-content
</p>

View File

@ -1,3 +0,0 @@
<p class="@("Hello world")">
page-content
</p>

View File

@ -1,4 +1,12 @@
<div>
@inject RazorPageExecutionInstrumentationWebSite.IHoldInstrumentationData Holder
<div>
@int.MaxValue
@RenderBody()
</div>
</div>
@{
Holder.IgnoreFurtherData();
foreach (var data in Holder.Values)
{
<p>@data.ToString()</p>
}
}