Skip to content

Commit

Permalink
Merge pull request #3704 from sharwell/generate-tests
Browse files Browse the repository at this point in the history
Generate and validate derived test classes
  • Loading branch information
sharwell committed Sep 29, 2023
2 parents 57b15ad + d1d8ee9 commit 0548969
Show file tree
Hide file tree
Showing 400 changed files with 1,721 additions and 1,301 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.PrivateAnalyzers
{
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;

[Generator]
internal sealed class DerivedTestGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var testData = context.CompilationProvider.Select((compilation, cancellationToken) =>
{
var currentAssemblyName = compilation.AssemblyName ?? string.Empty;
if (!Regex.IsMatch(currentAssemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$"))
{
// This is not a test project where derived test classes are expected
return null;
}
var currentVersion = int.Parse(currentAssemblyName["StyleCop.Analyzers.Test.CSharp".Length..]);
var currentTestString = "CSharp" + currentVersion;
var previousTestString = currentVersion switch
{
7 => string.Empty,
_ => "CSharp" + (currentVersion - 1).ToString(),
};
var previousAssemblyName = previousTestString switch
{
"" => "StyleCop.Analyzers.Test",
_ => "StyleCop.Analyzers.Test." + previousTestString,
};
return new TestData(previousTestString, previousAssemblyName, currentTestString, currentAssemblyName);
});

var testTypes = context.CompilationProvider.Combine(testData).SelectMany((compilationAndTestData, cancellationToken) =>
{
var (compilation, testData) = compilationAndTestData;
if (testData is null)
{
return ImmutableArray<string>.Empty;
}
var previousAssembly = compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First(
symbol => symbol.Identity.Name == testData.PreviousAssemblyName);
if (previousAssembly is null)
{
return ImmutableArray<string>.Empty;
}
var collector = new TestClassCollector(testData.PreviousTestString);
var previousTests = collector.Visit(previousAssembly);
return previousTests.ToImmutableArray();
});

context.RegisterSourceOutput(
testTypes.Combine(testData),
(context, testTypeAndData) =>
{
var (testType, testData) = testTypeAndData;
if (testData is null)
{
throw new InvalidOperationException("Not reachable");
}
string expectedTest;
if (testData.PreviousTestString is "")
{
expectedTest = testType.Replace(testData.PreviousAssemblyName, testData.CurrentAssemblyName).Replace("UnitTests", testData.CurrentTestString + "UnitTests");
}
else
{
expectedTest = testType.Replace(testData.PreviousTestString, testData.CurrentTestString);
}
var lastDot = testType.LastIndexOf('.');
var baseNamespaceName = testType["global::".Length..lastDot];
var baseTypeName = testType[(lastDot + 1)..];
lastDot = expectedTest.LastIndexOf('.');
var namespaceName = expectedTest["global::".Length..lastDot];
var typeName = expectedTest[(lastDot + 1)..];
var content =
$@"// <auto-generated/>
#nullable enable
namespace {namespaceName};
using {baseNamespaceName};
public partial class {typeName}
: {baseTypeName}
{{
}}
";
context.AddSource(
typeName + ".cs",
content);
});
}

private sealed record TestData(string PreviousTestString, string PreviousAssemblyName, string CurrentTestString, string CurrentAssemblyName);

private sealed class TestClassCollector : SymbolVisitor<ImmutableSortedSet<string>>
{
private readonly string testString;

public TestClassCollector(string testString)
{
this.testString = testString;
}

public override ImmutableSortedSet<string> Visit(ISymbol? symbol)
=> base.Visit(symbol) ?? throw new InvalidOperationException("Not reachable");

public override ImmutableSortedSet<string>? DefaultVisit(ISymbol symbol)
=> ImmutableSortedSet<string>.Empty;

public override ImmutableSortedSet<string> VisitAssembly(IAssemblySymbol symbol)
{
return this.Visit(symbol.GlobalNamespace);
}

public override ImmutableSortedSet<string> VisitNamespace(INamespaceSymbol symbol)
{
var result = ImmutableSortedSet<string>.Empty;
foreach (var member in symbol.GetMembers())
{
result = result.Union(this.Visit(member)!);
}

return result;
}

public override ImmutableSortedSet<string> VisitNamedType(INamedTypeSymbol symbol)
{
if (this.testString is "")
{
if (symbol.Name.EndsWith("UnitTests"))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
else if (symbol.Name.Contains(this.testString))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.PrivateAnalyzers;

using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class IncludeTestClassesAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Descriptor =
new(PrivateDiagnosticIds.SP0001, "Include all test classes", "Expected test class '{0}' was not found", "Correctness", DiagnosticSeverity.Warning, isEnabledByDefault: true, customTags: new[] { "CompilationEnd" });

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Descriptor);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(context =>
{
var assemblyName = context.Compilation.AssemblyName ?? string.Empty;
if (!Regex.IsMatch(assemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$"))
{
// This is not a test project where derived test classes are expected
return;
}
// Map actual test class in current project to base type
var testClasses = new ConcurrentDictionary<string, string>();
context.RegisterSymbolAction(
context =>
{
var namedType = (INamedTypeSymbol)context.Symbol;
if (namedType.TypeKind != TypeKind.Class)
{
return;
}
testClasses[namedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)] = namedType.BaseType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? string.Empty;
},
SymbolKind.NamedType);
context.RegisterCompilationEndAction(context =>
{
var currentVersion = int.Parse(assemblyName["StyleCop.Analyzers.Test.CSharp".Length..]);
var currentTestString = "CSharp" + currentVersion;
var previousTestString = currentVersion switch
{
7 => string.Empty,
_ => "CSharp" + (currentVersion - 1).ToString(),
};
var previousAssemblyName = previousTestString switch
{
"" => "StyleCop.Analyzers.Test",
_ => "StyleCop.Analyzers.Test." + previousTestString,
};
var previousAssembly = context.Compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First(
symbol => symbol.Identity.Name == previousAssemblyName);
if (previousAssembly is null)
{
return;
}
var reportingLocation = context.Compilation.SyntaxTrees.FirstOrDefault()?.GetLocation(new TextSpan(0, 0)) ?? Location.None;
var collector = new TestClassCollector(previousTestString);
var previousTests = collector.Visit(previousAssembly);
foreach (var previousTest in previousTests)
{
string expectedTest;
if (previousTestString is "")
{
expectedTest = previousTest.Replace(previousAssemblyName, assemblyName).Replace("UnitTests", currentTestString + "UnitTests");
}
else
{
expectedTest = previousTest.Replace(previousTestString, currentTestString);
}
if (testClasses.TryGetValue(expectedTest, out var actualTest)
&& actualTest == previousTest)
{
continue;
}
context.ReportDiagnostic(Diagnostic.Create(Descriptor, reportingLocation, expectedTest));
}
});
});
}

private sealed class TestClassCollector : SymbolVisitor<ImmutableSortedSet<string>>
{
private readonly string testString;

public TestClassCollector(string testString)
{
this.testString = testString;
}

public override ImmutableSortedSet<string> Visit(ISymbol? symbol)
=> base.Visit(symbol) ?? throw new InvalidOperationException("Not reachable");

public override ImmutableSortedSet<string>? DefaultVisit(ISymbol symbol)
=> ImmutableSortedSet<string>.Empty;

public override ImmutableSortedSet<string> VisitAssembly(IAssemblySymbol symbol)
{
return this.Visit(symbol.GlobalNamespace);
}

public override ImmutableSortedSet<string> VisitNamespace(INamespaceSymbol symbol)
{
var result = ImmutableSortedSet<string>.Empty;
foreach (var member in symbol.GetMembers())
{
result = result.Union(this.Visit(member)!);
}

return result;
}

public override ImmutableSortedSet<string> VisitNamedType(INamedTypeSymbol symbol)
{
if (this.testString is "")
{
if (symbol.Name.EndsWith("UnitTests"))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
else if (symbol.Name.Contains(this.testString))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace StyleCop.Analyzers.PrivateAnalyzers
{
internal static class PrivateDiagnosticIds
{
/// <summary>
/// SP0001: Include all test classes.
/// </summary>
public const string SP0001 = nameof(SP0001);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!-- RS2008: Enable analyzer release tracking -->
<NoWarn>$(NoWarn),RS2008</NoWarn>
</PropertyGroup>

<PropertyGroup>
<CodeAnalysisRuleSet>..\StyleCop.Analyzers.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\build\keys\StyleCopAnalyzers.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="TunnelVisionLabs.LanguageTypes.SourceGenerator" Version="0.1.20-beta" />
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all" />
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.1.0]" />
</ItemGroup>

<ItemGroup>
<!-- The .generated file is excluded by default, but we want to show the items in Solution Explorer so we included it as None -->
<None Include="Lightup\.generated\**" />
</ItemGroup>

</Project>

0 comments on commit 0548969

Please sign in to comment.