Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test mechanism to automatically rewrite baselines in source code #26723

Merged
merged 1 commit into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The typical workflow for contributing to EF Core is outlined below. This is not
* Make appropriate code and test changes. Follow the patterns and code style that you see in the existing code. Make sure to add tests that fail without the change and then pass with the change.
* Consider other scenarios where your change may have an impact and add more testing. We always prefer having too many tests to having not enough of them.
* When you are done with changes, make sure _all_ existing tests are still passing. (Again, typically by running `test` at a command prompt.)
* EF Core tests contain many "SQL assertions" - these verify that the precise expected SQL is generated for all scenarios. Some changes can cause SQL alterations to ripple across many scenarios, and changing all expected SQL in assertions is quite laborious. If you set the `EF_TEST_REWRITE_BASELINES` environment variable to `1` and then run the tests, the SQL assertions in the source code will be automatically changed to contain the new SQL baselines.
* Commit changes to your branch and push the branch to your GitHub fork.
* Go to the main [EF Core repo](https://github.com/dotnet/efcore/pulls) and you should see a yellow box suggesting you create a PR from your fork. Do this, or [create the PR by some other mechanism](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
* Sign the [contributor license agreement](https://cla.dotnetfoundation.org/) if you have not already done so.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<Compile Include="..\..\src\Shared\*.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\EFCore.Relational\EFCore.Relational.csproj" />
<ProjectReference Include="..\EFCore.Specification.Tests\EFCore.Specification.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Xunit;
Expand All @@ -23,6 +29,8 @@ public class TestSqlLoggerFactory : ListLoggerFactory

private static readonly object _queryBaselineFileLock = new();
private static readonly HashSet<string> _overriddenMethods = new();
private static readonly object _queryBaselineRewritingLock = new();
private static readonly ConcurrentDictionary<string, object> _queryBaselineRewritingLocks = new();

public TestSqlLoggerFactory()
: this(_ => true)
Expand Down Expand Up @@ -97,8 +105,14 @@ public void AssertBaseline(string[] expected, bool assertOrder = true)
var testInfo = testName + " : " + lineNumber + FileNewLine;
const string indent = FileNewLine + " ";

if (Environment.GetEnvironmentVariable("EF_TEST_REWRITE_BASELINES")?.ToUpper() is "1" or "TRUE")
{
RewriteSourceWithNewBaseline(fileName, lineNumber);
}

var sql = string.Join(
"," + indent + "//" + indent, SqlStatements.Take(9).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""));

var newBaseLine = $@" AssertSql(
{string.Join("," + indent + "//" + indent, SqlStatements.Take(20).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))});

Expand Down Expand Up @@ -159,6 +173,132 @@ public void AssertBaseline(string[] expected, bool assertOrder = true)

throw;
}

void RewriteSourceWithNewBaseline(string fileName, int lineNumber)
{
var fileLock = _queryBaselineRewritingLocks.GetOrAdd(fileName, _ => new());
lock (fileLock)
{
// Parse the file to find the line where the relevant AssertSql is
try
{
// First have Roslyn parse the file
SyntaxTree syntaxTree;
using (var stream = File.OpenRead(fileName))
{
syntaxTree = CSharpSyntaxTree.ParseText(SourceText.From(stream));
}

// Read through the source file, copying contents to a temp file (with the baseline changE)
using (var inputStream = File.OpenRead(fileName))
using (var outputStream = File.Open(fileName + ".tmp", FileMode.Create, FileAccess.Write))
{
// Detect whether a byte-order mark (BOM) exists, to write out the same
var buffer = new byte[3];
inputStream.Read(buffer, 0, 3);
inputStream.Position = 0;

var hasUtf8ByteOrderMark = (buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF);

using var reader = new StreamReader(inputStream);
using var writer = new StreamWriter(outputStream, new UTF8Encoding(hasUtf8ByteOrderMark));

// First find the char position where our line starts
var pos = 0;
for (var i = 0; i < lineNumber - 1; i++)
{
while (true)
{
if (reader.Peek() == -1)
{
return;
}

pos++;
var ch = (char)reader.Read();
writer.Write(ch);
if (ch == '\n') // Unix
{
break;
}

if (ch == '\r')
{
// Mac (just \r) or Windows (\r\n)
if (reader.Peek() >= 0 && (char)reader.Peek() == '\n')
{
_ = reader.Read();
writer.Write('\n');
pos++;
}

break;
}
}
}

// We have the character position of the line start. Skip over whitespace (that's the indent) to find the invocation
var indentBuilder = new StringBuilder();
while (true)
{
var i = reader.Peek();
if (i == -1)
{
return;
}

var ch = (char)i;

if (ch == ' ')
{
pos++;
indentBuilder.Append(' ');
reader.Read();
writer.Write(ch);
}
else
{
break;
}
}

// We are now at the start of the invocation.
var node = syntaxTree.GetRoot().FindNode(TextSpan.FromBounds(pos, pos));

// Node should be pointing at the AssertSql identifier. Go up and find the text span for the entire method invocation.
if (node is not IdentifierNameSyntax { Parent: InvocationExpressionSyntax invocation })
{
return;
}

// Skip over the invocation on the read side, and write the new baseline invocation
var tempBuf = new char[Math.Max(1024, invocation.Span.Length)];
reader.ReadBlock(tempBuf, 0, invocation.Span.Length);

indentBuilder.Append(" ");
var indent = indentBuilder.ToString();
var newBaseLine = $@"AssertSql(
{indent}{string.Join(",\n" + indent + "//\n" + indent, SqlStatements.Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))})";

writer.Write(newBaseLine);

// Copy the rest of the file contents as-is
int count;
while ((count = reader.ReadBlock(tempBuf, 0, 1024)) > 0)
{
writer.Write(tempBuf, 0, count);
}
}
}
catch
{
File.Delete(fileName + ".tmp");
throw;
}

File.Move(fileName + ".tmp", fileName, overwrite: true);
}
}
}

protected class TestSqlLogger : ListLogger
Expand Down