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

Add RunTestsInContext Command #1782

Merged
merged 14 commits into from
May 11, 2020
1 change: 1 addition & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static class V2
public const string RunAllTestsInClass = "/v2/runtestsinclass";
public const string DebugTestGetStartInfo = "/v2/debugtest/getstartinfo";
public const string DebugTestLaunch = "/v2/debugtest/launch";
public const string DebugTestsInContextGetStartInfo = "/v2/debugtestsincontext/getstartinfo";
public const string DebugTestStop = "/v2/debugtest/stop";
public const string DebugTestsInClassGetStartInfo = "/v2/debugtestsinclass/getstartinfo";
public const string RunTestsInContext = "/v2/runtestsincontext";
Expand Down
12 changes: 11 additions & 1 deletion src/OmniSharp.DotNetTest/DebugSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using OmniSharp.DotNetTest.Models;
using OmniSharp.Utilities;
Expand Down Expand Up @@ -77,9 +78,18 @@ public void EndSession()
}
}

#nullable enable
public Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(int line, int column, Document contextDocument, string? runSettings, string? targetFrameworkVersion, CancellationToken cancellationToken)
{
VerifySession(isStarted: true);

return _testManager.DebugGetStartInfoAsync(line, column, contextDocument, runSettings, targetFrameworkVersion, cancellationToken);
}
#nullable restore

public Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string methodName, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken)
=> DebugGetStartInfoAsync(new string[] { methodName }, runSettings, testFrameworkName, targetFrameworkVersion, cancellationToken);

public Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string[] methodNames, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken)
{
VerifySession(isStarted: true);
Expand Down
17 changes: 17 additions & 0 deletions src/OmniSharp.DotNetTest/Models/BaseTestsInContextRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

#nullable enable

using OmniSharp.Mef;
using OmniSharp.Models;

namespace OmniSharp.DotNetTest.Models
{
public abstract class BaseTestsInContextRequest : Request
{
public string? RunSettings { get; set; }
/// <summary>
/// e.g. .NETCoreApp, Version=2.0
/// </summary>
public string? TargetFrameworkVersion { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

#nullable enable

using OmniSharp.Mef;

namespace OmniSharp.DotNetTest.Models
{
[OmniSharpEndpoint(OmniSharpEndpoints.V2.DebugTestsInContextGetStartInfo, typeof(DebugTestsInContextGetStartInfoRequest), typeof(DebugTestGetStartInfoResponse))]
public class DebugTestsInContextGetStartInfoRequest : BaseTestsInContextRequest
{
}
}
9 changes: 1 addition & 8 deletions src/OmniSharp.DotNetTest/Models/RunTestsInContextRequest.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
#nullable enable

using OmniSharp.Mef;
using OmniSharp.Models;

namespace OmniSharp.DotNetTest.Models
{
[OmniSharpEndpoint(OmniSharpEndpoints.V2.RunTestsInContext, typeof(RunTestsInContextRequest), typeof(RunTestResponse))]
public class RunTestsInContextRequest : Request
public class RunTestsInContextRequest : BaseTestsInContextRequest
{
public string? RunSettings { get; set; }
public string TestFrameworkName { get; set; } = null!;
/// <summary>
/// e.g. .NETCoreApp, Version=2.0
/// </summary>
public string? TargetFrameworkVersion { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/OmniSharp.DotNetTest/Services/DebugTestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal class DebugTestService : BaseTestService,
IRequestHandler<DebugTestLaunchRequest, DebugTestLaunchResponse>,
IRequestHandler<DebugTestStopRequest, DebugTestStopResponse>
{
private DebugSessionManager _debugSessionManager;
private readonly DebugSessionManager _debugSessionManager;

[ImportingConstructor]
public DebugTestService(DebugSessionManager debugSessionManager, OmniSharpWorkspace workspace, IDotNetCliService dotNetCli, IEventEmitter eventEmitter, ILoggerFactory loggerFactory)
Expand Down
45 changes: 45 additions & 0 deletions src/OmniSharp.DotNetTest/Services/DebugTestsInContextService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#nullable enable

using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using OmniSharp.DotNetTest.Models;
using OmniSharp.Eventing;
using OmniSharp.Mef;
using OmniSharp.Services;

namespace OmniSharp.DotNetTest.Services
{
[Shared]
[OmniSharpHandler(OmniSharpEndpoints.V2.DebugTestsInContextGetStartInfo, LanguageNames.CSharp)]
internal class DebugTestsInContextService : BaseTestService,
IRequestHandler<DebugTestsInContextGetStartInfoRequest, DebugTestGetStartInfoResponse>
{
private readonly DebugSessionManager _debugSessionManager;

[ImportingConstructor]
public DebugTestsInContextService(DebugSessionManager debugSessionManager, OmniSharpWorkspace workspace, IDotNetCliService dotNetCli, IEventEmitter eventEmitter, ILoggerFactory loggerFactory)
: base(workspace, dotNetCli, eventEmitter, loggerFactory)
{
_debugSessionManager = debugSessionManager;
}

public Task<DebugTestGetStartInfoResponse> Handle(DebugTestsInContextGetStartInfoRequest request)
{
var testManager = CreateTestManager(request.FileName);
if (testManager.IsConnected)
{
_debugSessionManager.StartSession(testManager);
return _debugSessionManager.DebugGetStartInfoAsync(
request.Line, request.Column, Workspace.GetDocument(request.FileName),
request.RunSettings, request.TargetFrameworkVersion,
CancellationToken.None);
}

throw new InvalidOperationException("The debugger could not be started");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem unfortunate, especially for the InContext version of the command, but the existing DebugTestGetStartInfoResponse doesn't have a place for a failure and failure message. I would have modified that to add a field, but I'm unsure if that will break existing clients. Is that a safe change to make, or should I add a new response that adds a failure/reason to it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure whether this would be a problem for other clients. Maybe @filipw knows definitively.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be fine - additive changes are generally fine since it's json based.
moreover, the debug tests protocl is - I am pretty sure - only used by VS Code extension since the debugger is proprietary

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task<RunTestResponse> Handle(RunTestsInContextRequest request)

if (testManager.IsConnected)
{
return await testManager.RunTestsInContextAsync(request.Line, request.Column, document, request.RunSettings, request.TestFrameworkName, request.TargetFrameworkVersion, CancellationToken.None);
return await testManager.RunTestsInContextAsync(request.Line, request.Column, document, request.RunSettings, request.TargetFrameworkVersion, CancellationToken.None);
}

var response = new RunTestResponse
Expand Down
2 changes: 1 addition & 1 deletion src/OmniSharp.DotNetTest/TestFeaturesDiscover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public IEnumerable<SyntaxFeature> Discover(SyntaxNode node, SemanticModel semant
{
if (node is MethodDeclarationSyntax method)
{
foreach (var framework in TestFramework.GetFrameworks())
foreach (var framework in TestFramework.Frameworks)
{
if (framework.IsTestMethod(method, semanticModel))
{
Expand Down
11 changes: 3 additions & 8 deletions src/OmniSharp.DotNetTest/TestFrameworks/TestFramework.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ namespace OmniSharp.DotNetTest.TestFrameworks
{
internal abstract class TestFramework
{
private static readonly ImmutableArray<TestFramework> s_frameworks;

static TestFramework()
{
var builder = ImmutableArray.CreateBuilder<TestFramework>();
Expand All @@ -21,12 +19,12 @@ static TestFramework()
builder.Add(xunit);
builder.Add(mstest);

s_frameworks = builder.ToImmutable();
Frameworks = builder.ToImmutable();
}

public static TestFramework GetFramework(string name)
{
foreach (var framework in s_frameworks)
foreach (var framework in Frameworks)
{
if (framework.Name == name)
{
Expand All @@ -37,10 +35,7 @@ public static TestFramework GetFramework(string name)
return null;
}

public static IEnumerable<TestFramework> GetFrameworks()
{
return s_frameworks;
}
public static ImmutableArray<TestFramework> Frameworks { get; }

public abstract string FeatureName { get; }
public abstract string Name { get; }
Expand Down
11 changes: 6 additions & 5 deletions src/OmniSharp.DotNetTest/TestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static TestManager Create(Project project, IDotNetCliService dotNetCli, I
protected abstract void VersionCheck();

#nullable enable
public abstract Task<RunTestResponse> RunTestsInContextAsync(int lineNumber, int column, Document contextDocument, string? runSettings, string testFrameWorkName, string? targetFrameworkVersion, CancellationToken cancellationToken);
public abstract Task<RunTestResponse> RunTestsInContextAsync(int lineNumber, int column, Document contextDocument, string? runSettings, string? targetFrameworkVersion, CancellationToken cancellationToken);
#nullable restore

public abstract Task<RunTestResponse> RunTestAsync(string methodName, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken);
Expand All @@ -84,12 +84,13 @@ public static TestManager Create(Project project, IDotNetCliService dotNetCli, I

public abstract Task<GetTestStartInfoResponse> GetTestStartInfoAsync(string methodName, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken);

#nullable enable
public abstract Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(int line, int column, Document contextDocument, string? runSettings, string? targetFrameworkVersion, CancellationToken cancellationToken);
#nullable restore

public abstract Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string methodName, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken);

public virtual Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string[] methodNames, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public abstract Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string[] methodNames, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken);

public abstract Task DebugLaunchAsync(CancellationToken cancellationToken);

Expand Down
2 changes: 1 addition & 1 deletion src/OmniSharp.DotNetTest/TestMethodPropertyProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal class TestMethodPropertyProvider : ICodeElementPropertyProvider
{
if (symbol is IMethodSymbol methodSymbol)
{
foreach (var framework in TestFramework.GetFrameworks())
foreach (var framework in TestFramework.Frameworks)
{
if (framework.IsTestMethod(methodSymbol))
{
Expand Down
65 changes: 50 additions & 15 deletions src/OmniSharp.DotNetTest/VSTestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -148,6 +147,22 @@ public override async Task<GetTestStartInfoResponse> GetTestStartInfoAsync(strin
};
}

#nullable enable
public override async Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(int line, int column, Document contextDocument, string? runSettings, string? targetFrameworkVersion, CancellationToken cancellationToken)
{
var (methodNames, testFramework) = await GetContextTestMethodNames(line, column, contextDocument, cancellationToken);

if (methodNames is null)
{
throw new InvalidOperationException("Could not find a test to debug");
}

Debug.Assert(testFramework is object);

return await DebugGetStartInfoAsync(methodNames, runSettings, testFramework, targetFrameworkVersion, cancellationToken);
}
#nullable restore

public override async Task<DebugTestGetStartInfoResponse> DebugGetStartInfoAsync(string methodName, string runSettings, string testFrameworkName, string targetFrameworkVersion, CancellationToken cancellationToken)
=> await DebugGetStartInfoAsync(new string[] { methodName }, runSettings, testFrameworkName, targetFrameworkVersion, cancellationToken);

Expand Down Expand Up @@ -209,27 +224,19 @@ public override async Task DebugLaunchAsync(CancellationToken cancellationToken)
}

#nullable enable
public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int column, Document contextDocument, string? runSettings, string testFrameworkName, string? targetFrameworkVersion, CancellationToken cancellationToken)
private async Task<(string[]? MethodNames, string? TestFramework)> GetContextTestMethodNames(int line, int column, Document contextDocument, CancellationToken cancellationToken)
{
Logger.LogDebug($"Loading info for {contextDocument.FilePath} {line}:{column}");
var syntaxTree = await contextDocument.GetSyntaxTreeAsync(cancellationToken);
if (syntaxTree is null)
{
return new RunTestResponse()
{
Pass = false,
Failure = $"Could not get syntax for {contextDocument.FilePath}"
};
return default;
}

var semanticModel = await contextDocument.GetSemanticModelAsync(cancellationToken);
if (semanticModel is null)
{
return new RunTestResponse()
{
Pass = false,
Failure = $"Could not get semantic model for {contextDocument.FilePath}"
};
return default;
}

var sourceText = await contextDocument.GetTextAsync();
Expand All @@ -238,6 +245,7 @@ public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int
var node = (await syntaxTree.GetRootAsync()).FindToken(position).Parent;

string[]? methodNames = null;
TestFramework? testFramework = null;

while (node is object)
{
Expand All @@ -261,7 +269,7 @@ public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int
continue;
}

if (isTestMethod(methodSymbol))
if (isTestMethod(methodSymbol, ref testFramework))
{
methodNames = new[] { methodSymbol.GetMetadataName() };
Logger.LogDebug($"Found test method {methodNames[0]}");
Expand All @@ -285,7 +293,7 @@ public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int

foreach (var member in members)
{
if (!(member is IMethodSymbol methodSymbol) || !isTestMethod(methodSymbol))
if (!(member is IMethodSymbol methodSymbol) || !isTestMethod(methodSymbol, ref testFramework))
{
continue;
}
Expand All @@ -309,6 +317,32 @@ public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int
node = node.Parent;
}

return (methodNames, testFramework?.Name);

static bool isTestMethod(IMethodSymbol methodSymbol, ref TestFramework? framework)
{
if (framework is object)
{
return framework.IsTestMethod(methodSymbol);
}

foreach (var f in TestFramework.Frameworks)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this means that you can't combine multiple testing framework types in a single file and have them run all at once. I think that's probably fine.

{
if (f.IsTestMethod(methodSymbol))
{
framework = f;
return true;
}
}

return false;
}
}

public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int column, Document contextDocument, string? runSettings, string? targetFrameworkVersion, CancellationToken cancellationToken)
{
var (methodNames, testFrameworkName) = await GetContextTestMethodNames(line, column, contextDocument, cancellationToken);

if (methodNames is null)
{
return new RunTestResponse
Expand All @@ -318,9 +352,10 @@ public override async Task<RunTestResponse> RunTestsInContextAsync(int line, int
};
}

Debug.Assert(testFrameworkName is object);

return await RunTestAsync(methodNames, runSettings, testFrameworkName, targetFrameworkVersion, cancellationToken);

static bool isTestMethod(IMethodSymbol methodSymbol) => TestFramework.GetFrameworks().Any(f => f.IsTestMethod(methodSymbol));
}
#nullable restore

Expand Down
Loading