Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
222 changes: 222 additions & 0 deletions src/TaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Shouldly;
using Xunit;
using static Microsoft.Build.TaskAuthoring.Analyzer.Tests.TestHelpers;
Expand Down Expand Up @@ -1870,6 +1873,225 @@ public override bool Execute()
// Scope option tests
// ═══════════════════════════════════════════════════════════════════════

[Fact]
public async Task Scope_Default_PlainTask_DoesNotGetScopedDiagnostics()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using System.IO;
using System.Reflection;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Console.WriteLine("test");
var value = Environment.GetEnvironmentVariable("KEY");
Assembly.Load("Test");
return File.Exists("relative.txt");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.CriticalError).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldBeEmpty();
}

[Fact]
public async Task Scope_Default_MultiThreadableTask_GetsAllScopedDiagnostics()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using System.IO;
using System.Reflection;
using Microsoft.Build.Framework;
public class MtTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
Console.WriteLine("test");
var value = Environment.GetEnvironmentVariable("KEY");
Assembly.Load("Test");
return File.Exists("relative.txt");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.CriticalError).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Error);
diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
}

[Fact]
public async Task Scope_All_PlainTask_GetsAllScopedDiagnostics()
{
var diags = await GetDiagnosticsWithScopeAsync("""
using System;
using System.IO;
using System.Reflection;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Console.WriteLine("test");
var value = Environment.GetEnvironmentVariable("KEY");
Assembly.Load("Test");
return File.Exists("relative.txt");
}
}
""", SharedAnalyzerHelpers.ScopeAll);

diags.Where(d => d.Id == DiagnosticIds.CriticalError).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Error);
diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldHaveSingleItem()
.Severity.ShouldBe(DiagnosticSeverity.Warning);
}

[Fact]
public async Task Scope_GlobalConfig_All_AnalyzesPlainTask()
{
var test = new CSharpAnalyzerTest<MultiThreadableTaskAnalyzer, DefaultVerifier>
{
TestCode = """
using System;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = {|#0:Environment.GetEnvironmentVariable("KEY")|};
return true;
}
}
""",
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
};
test.TestState.Sources.Add(("Stubs.cs", FrameworkStubs));
test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", """
is_global = true
msbuild_task_analyzer.scope = all
"""));
test.ExpectedDiagnostics.Add(
new DiagnosticResult(DiagnosticIds.TaskEnvironmentRequired, DiagnosticSeverity.Warning).WithLocation(0));

await test.RunAsync();
}

[Fact]
public async Task Scope_UnrecognizedValue_UsesDefault()
{
var diags = await GetDiagnosticsWithScopeAsync("""
using System;
using System.Reflection;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Console.WriteLine("test");
var value = Environment.GetEnvironmentVariable("KEY");
Assembly.Load("Test");
return true;
}
}
""", "unrecognized");

diags.Where(d => d.Id == DiagnosticIds.CriticalError).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldBeEmpty();
}

[Fact]
public async Task Scope_Default_PlainTask_TreatWarningsAsErrors_DoesNotGetScopedDiagnostics()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using System.Reflection;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Console.WriteLine("test");
Assembly.Load("Test");
return true;
}
}
""", ReportDiagnostic.Error);

diags.Where(d => d.Id == DiagnosticIds.CriticalError).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldBeEmpty();
}

[Fact]
public async Task Scope_Default_MultiThreadableTask_TreatWarningsAsErrors_PromotesWarning()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System.Reflection;
using Microsoft.Build.Framework;
public class MtTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
Assembly.Load("Test");
return true;
}
}
""", ReportDiagnostic.Error);

Diagnostic diagnostic = diags.Where(d => d.Id == DiagnosticIds.PotentialIssue).ShouldHaveSingleItem();
diagnostic.Severity.ShouldBe(DiagnosticSeverity.Error);
diagnostic.IsWarningAsError.ShouldBeTrue();
}

[Fact]
public async Task Scope_Default_MultiThreadableAttribute_OptsTaskIn()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using Microsoft.Build.Framework;
[MSBuildMultiThreadableTask]
public class MtTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return true;
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_Default_AnalyzedAttribute_OptsHelperIn()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using Microsoft.Build.Framework;
[MSBuildMultiThreadableTaskAnalyzed]
public class MtHelper
{
public void Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_MultithreadableOnly_PlainTask_NoDiagnostic()
{
Expand Down
93 changes: 69 additions & 24 deletions src/TaskAnalyzer.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,34 +138,16 @@ public static string FullyQualifiedPath(string tail) =>
public static MetadataReference[] GetCoreReferences() => s_coreReferences;

/// <summary>
/// Runs the MultiThreadableTaskAnalyzer on the given source code and returns analyzer diagnostics.
/// Source is combined with framework stubs automatically.
/// Runs the MultiThreadableTaskAnalyzer in explicit all-task migration mode.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzer = new MultiThreadableTaskAnalyzer();
var compilationWithAnalyzers = compilation.WithAnalyzers(
ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));

var allDiags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return allDiags;
}
public static System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source) =>
GetDiagnosticsWithScopeAsync(source, SharedAnalyzerHelpers.ScopeAll);
Comment thread
VolPlita marked this conversation as resolved.
Outdated

/// <summary>
/// Runs BOTH the direct and transitive analyzers on the given source code.
/// Runs both the direct and transitive analyzers in explicit all-task migration mode.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);

var allDiags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return allDiags;
}
public static System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(string source) =>
GetAllDiagnosticsWithScopeAsync(source, SharedAnalyzerHelpers.ScopeAll);

/// <summary>
/// Runs compiler diagnostics together with analyzers and suppressors and returns
Expand Down Expand Up @@ -260,6 +242,69 @@ public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetD
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs the MultiThreadableTaskAnalyzer without a scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsWithDefaultScopeAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzer = new MultiThreadableTaskAnalyzer();
var compilationWithAnalyzers = compilation.WithAnalyzers(
ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs the MultiThreadableTaskAnalyzer without a scope option and applies a general diagnostic action.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsWithDefaultScopeAsync(
string source,
ReportDiagnostic generalDiagnosticOption)
{
var compilation = CreateCompilation(source);
compilation = compilation.WithOptions(
((CSharpCompilationOptions)compilation.Options).WithGeneralDiagnosticOption(generalDiagnosticOption));

var analyzer = new MultiThreadableTaskAnalyzer();
var compilationWithAnalyzers = compilation.WithAnalyzers(
ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs both the direct and transitive analyzers with a specific scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsWithScopeAsync(string source, string scope)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());

var globalOptions = new Dictionary<string, string>
{
{ $"build_property.{SharedAnalyzerHelpers.ScopeOptionKey}", scope }
};
var optionsProvider = new TestAnalyzerConfigOptionsProvider(globalOptions);
var options = new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty, optionsProvider);

var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, options);
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs both the direct and transitive analyzers without a scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsWithDefaultScopeAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

private static MetadataReference[] CreateCoreReferences()
{
// Reference the core runtime assemblies needed
Expand Down
Loading
Loading