Skip to content

Commit

Permalink
Making some tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewlock committed Jan 21, 2024
1 parent 2fe842a commit 74082d6
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/NetEscapades.EnumGenerators/TrackingNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace NetEscapades.EnumGenerators;
/// <summary>
/// Names that are attached to incremental generator stages for tracking
/// </summary>
public static class TrackingNames
public class TrackingNames
{
public const string InitialExtraction = nameof(InitialExtraction);
public const string RemovingNulls = nameof(RemovingNulls);
Expand Down
183 changes: 146 additions & 37 deletions tests/NetEscapades.EnumGenerators.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand All @@ -9,17 +12,31 @@ namespace NetEscapades.EnumGenerators.Tests;

internal class TestHelpers
{
private static readonly string[] _trackingNames = typeof(TrackingNames)
.GetFields()
.Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string))
.Select(x => (string?)x.GetRawConstantValue())
.Where(x => !string.IsNullOrEmpty(x))
.ToArray()!;

public static (ImmutableArray<Diagnostic> Diagnostics, string Output) GetGeneratedOutput<T>(string source)
public static (ImmutableArray<Diagnostic> Diagnostics, string Output) GetGeneratedOutput<T>(params string[] source)
where T : IIncrementalGenerator, new()
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var (diagnostics, trees) = GetGeneratedTrees<T, TrackingNames>(source);
return (diagnostics, trees.LastOrDefault() ?? string.Empty);
}

public static (ImmutableArray<Diagnostic> Diagnostics, string[] Output) GetGeneratedTrees<TGenerator, TTrackingNames>(params string[] sources)
where TGenerator : IIncrementalGenerator, new()
{
// get all the const string fields
var trackingNames = typeof(TTrackingNames)
.GetFields()
.Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string))
.Select(x => (string?)x.GetRawConstantValue()!)
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();

return GetGeneratedTrees<TGenerator>(sources, trackingNames);
}

public static (ImmutableArray<Diagnostic> Diagnostics, string[] Output) GetGeneratedTrees<T>(string[] source, params string[] stages)
where T : IIncrementalGenerator, new()
{
var syntaxTrees = source.Select(static x => CSharpSyntaxTree.ParseText(x));
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location))
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
Expand All @@ -32,67 +49,159 @@ public static (ImmutableArray<Diagnostic> Diagnostics, string Output) GetGenerat

var compilation = CSharpCompilation.Create(
"generator",
new[] { syntaxTree },
syntaxTrees,
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var runResult = RunGeneratorAndAssertOutput<T>(compilation, _trackingNames);

var trees = runResult.GeneratedTrees;
return (runResult.Diagnostics, trees.IsDefaultOrEmpty ? string.Empty : trees[trees.Length - 1].ToString());
GeneratorDriverRunResult runResult = RunGeneratorAndAssertOutput<T>(compilation, stages);

return (runResult.Diagnostics, runResult.GeneratedTrees.Select(x => x.ToString()).ToArray());
}

private static GeneratorDriverRunResult RunGeneratorAndAssertOutput<T>(CSharpCompilation compilation, params string[] trackingNames)
private static GeneratorDriverRunResult RunGeneratorAndAssertOutput<T>(CSharpCompilation compilation, string[] trackingNames, bool assertOutput = true)
where T : IIncrementalGenerator, new()
{
var generator = new T().AsSourceGenerator();
ISourceGenerator generator = new T().AsSourceGenerator();

var opts = new GeneratorDriverOptions(
disabledOutputs: IncrementalGeneratorOutputKind.None,
trackIncrementalGeneratorSteps: true);

var driver = CSharpGeneratorDriver.Create([generator], driverOptions: opts);
GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], driverOptions: opts);

// Run twice, once with a clone
var runResult = driver.RunGenerators(compilation).GetRunResult();
var compilationClone = compilation.Clone();
var runResult2 = driver.RunGenerators(compilationClone).GetRunResult();
var clone = compilation.Clone();
// Run twice, once with a clone of the compilation
// Note that we store the returned drive value, as it contains cached previous outputs
driver = driver.RunGenerators(compilation);
GeneratorDriverRunResult runResult = driver.GetRunResult();

AssertRunsEqual(runResult, runResult2, trackingNames);
if (assertOutput)
{
// Run with a clone of the compilation
GeneratorDriverRunResult runResult2 = driver
.RunGenerators(clone)
.GetRunResult();

AssertRunsEqual(runResult, runResult2, trackingNames);

// verify the second run only generated cached source outputs
runResult2.Results[0]
.TrackedOutputSteps
.SelectMany(x => x.Value) // step executions
.SelectMany(x => x.Outputs) // execution results
.Should()
.OnlyContain(x => x.Reason == IncrementalStepRunReason.Cached);
}

return runResult;
}

private static void AssertRunsEqual(GeneratorDriverRunResult runResult1, GeneratorDriverRunResult runResult2, string[] trackingNames)
{
var trackedSteps1 = runResult1.Results[0].TrackedSteps;
var trackedSteps2 = runResult2.Results[0].TrackedSteps;
// We're given all the tracking names, but not all the stages have necessarily executed so filter
Dictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> trackedSteps1 = GetTrackedSteps(runResult1, trackingNames);
Dictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> trackedSteps2 = GetTrackedSteps(runResult2, trackingNames);

// these should be the same!
// These should be the same
trackedSteps1.Should()
.NotBeEmpty()
.And.HaveSameCount(trackedSteps2);
.NotBeEmpty()
.And.HaveSameCount(trackedSteps2)
.And.ContainKeys(trackedSteps2.Keys);

foreach (var trackingName in trackingNames)
foreach (var trackedStep in trackedSteps1)
{
AssertEqual(trackedSteps1, trackedSteps2, trackingName);
var trackingName = trackedStep.Key;
var runSteps1 = trackedStep.Value;
var runSteps2 = trackedSteps2[trackingName];
AssertEqual(runSteps1, runSteps2, trackingName);
}

return;

static Dictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> GetTrackedSteps(
GeneratorDriverRunResult runResult, string[] trackingNames) =>
runResult.Results[0]
.TrackedSteps
.Where(step => trackingNames.Contains(step.Key))
.ToDictionary(x => x.Key, x => x.Value);
}

private static void AssertEqual(
ImmutableDictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> trackedSteps1,
ImmutableDictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> trackedSteps2,
ImmutableArray<IncrementalGeneratorRunStep> runSteps1,
ImmutableArray<IncrementalGeneratorRunStep> runSteps2,
string stepName)
{
ImmutableArray<IncrementalGeneratorRunStep> runSteps1 = trackedSteps1.Should().ContainKey(stepName).WhoseValue;
ImmutableArray<IncrementalGeneratorRunStep> runSteps2 = trackedSteps2.Should().ContainKey(stepName).WhoseValue;
runSteps1.Should().HaveSameCount(runSteps2);

for (var i = 0; i < runSteps1.Length; i++)
{
ImmutableArray<(object Value, IncrementalStepRunReason Reason)> outputs1 = runSteps1[i].Outputs;
ImmutableArray<(object Value, IncrementalStepRunReason Reason)> outputs2 = runSteps2[i].Outputs;
var runStep1 = runSteps1[i];
var runStep2 = runSteps2[i];

// The outputs should be equal between different runs
IEnumerable<object> outputs1 = runStep1.Outputs.Select(x => x.Value);
IEnumerable<object> outputs2 = runStep2.Outputs.Select(x => x.Value);

outputs1.Should()
.Equal(outputs2, $"because {stepName} should produce cacheable outputs");

// Therefore, on the second run the results should always be cached or unchanged!
// - Unchanged is when the _input_ has changed, but the output hasn't
// - Cached is when the the input has not changed, so the cached output is used
runStep2.Outputs.Should()
.OnlyContain(
x => x.Reason == IncrementalStepRunReason.Cached || x.Reason == IncrementalStepRunReason.Unchanged,
$"{stepName} expected to have reason {IncrementalStepRunReason.Cached} or {IncrementalStepRunReason.Unchanged}");

// Make sure we're not using anything we shouldn't
AssertObjectGraph(runStep1, stepName);
AssertObjectGraph(runStep2, stepName);
}

outputs1.Should().Equal(outputs2, $"because {stepName} should produce cacheable outputs");
static void AssertObjectGraph(IncrementalGeneratorRunStep runStep, string stepName)
{
var because = $"{stepName} shouldn't contain banned symbols";
var visited = new HashSet<object>();

foreach (var (obj, _) in runStep.Outputs)
{
Visit(obj);
}

void Visit(object? node)
{
if (node is null || !visited.Add(node))
{
return;
}

node.Should()
.NotBeOfType<Compilation>(because)
.And.NotBeOfType<ISymbol>(because)
.And.NotBeOfType<SyntaxNode>(because);

Type type = node.GetType();
if (type.IsPrimitive || type.IsEnum || type == typeof(string))
{
return;
}

if (node is IEnumerable collection and not string)
{
foreach (object element in collection)
{
Visit(element);
}

return;
}

foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
object? fieldValue = field.GetValue(node);
Visit(fieldValue);
}
}
}
}
}
}

0 comments on commit 74082d6

Please sign in to comment.