Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Xml;
using Microsoft.VisualStudio.Sdk.TestFramework.Xunit;

Check failure on line 2 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/LspSnippetConverterVsTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)

Check failure on line 2 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/LspSnippetConverterVsTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)
using Snipper.VisualStudio;
using Xunit;

Expand All @@ -11,7 +12,7 @@
[Collection(nameof(MockedVS))]
public class LspSnippetConverterVsTests
{
[Fact]
[VsFact]

Check failure on line 15 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/LspSnippetConverterVsTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFact' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 15 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/LspSnippetConverterVsTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFactAttribute' could not be found (are you missing a using directive or an assembly reference?)
public void Output_IsWellFormedXml()
{
var xml = LspSnippetConverter.ToVsSnippetXml("public ${1:ClassName}()\n{\n $0\n}");
Expand All @@ -21,15 +22,15 @@
Assert.Equal("CodeSnippets", doc.DocumentElement!.LocalName);
}

[Fact]
[VsFact]
public void Output_ContainsCorrectSchemaNamespace()
{
var xml = LspSnippetConverter.ToVsSnippetXml("foo $0");

Assert.Contains("http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet", xml);
}

[Fact]
[VsFact]
public void ScaffoldConstructor_DeclaresClassNameLiteral()
{
var body = "public ${1:ClassName}()\n{\n $0\n}";
Expand All @@ -44,7 +45,7 @@
Assert.NotNull(literal);
}

[Fact]
[VsFact]
public void ImplementInterface_AllThreeLiterals()
{
var body = "public ${1:ReturnType} ${2:MethodName}(${3:params})\n{\n throw new System.NotImplementedException();\n}";
Expand All @@ -62,15 +63,15 @@
Assert.Equal("params", literals[2]!.InnerText);
}

[Fact]
[VsFact]
public void CodeBody_ContainsCdataSection()
{
var xml = LspSnippetConverter.ToVsSnippetXml("Console.WriteLine($end$);");

Assert.Contains("<![CDATA[", xml);
}

[Fact]
[VsFact]
public void NoTabstops_EmptyDeclarations()
{
var xml = LspSnippetConverter.ToVsSnippetXml("Console.WriteLine(\"hello\");");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- net472 matches the VS process host; tests run on windows-latest CI. -->
<TargetFramework>net472</TargetFramework>
<!-- net8.0-windows: TestFramework.Xunit 17.11+ ships net8.0-windows assets only;
plain net8.0 falls back to a netstandard2.0 stub that omits VsFact/GlobalServiceProvider.
The -windows TFM is also required for COM-based VS interop types at runtime. -->
<TargetFramework>net8.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>

<ItemGroup>
Expand All @@ -15,28 +15,43 @@
no full VS IDE install required. [VsFact] runs on the mock UI thread. -->
<PackageReference Include="Microsoft.VisualStudio.Sdk.TestFramework" Version="17.11.*" />
<PackageReference Include="Microsoft.VisualStudio.Sdk.TestFramework.Xunit" Version="17.11.*" />
<!-- VSSDK reference assemblies (ExcludeAssets=runtime — not loaded at test time) -->
<!-- VSSDK reference assemblies for ILanguageClient, IVsTextView, etc. -->
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="17.14.40265" ExcludeAssets="runtime" />
<PackageReference Include="xunit" Version="2.*" />
<!-- Restores VSSDK.BuildTools to the NuGet cache so the HintPath reference to
Microsoft.Internal.VisualStudio.Interop below resolves at compile time. -->
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="17.14.2142">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>none</IncludeAssets>
</PackageReference>
<!-- StreamJsonRpc runtime needed for SnipperCommandRpcTests fake-server setup. -->
<PackageReference Include="StreamJsonRpc" Version="2.*" />
<PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<!-- Microsoft.Internal.VisualStudio.Interop lives under the VSSDK NuGet package at
tools\VSSDK\VisualStudioIntegration\Common\Assemblies\v4.0\. VSSDK.BuildTools is
listed above with IncludeAssets="none" purely so NuGet restores the package and
populates this path before compilation. -->
<Reference Include="Microsoft.Internal.VisualStudio.Interop">
<HintPath>$(NuGetPackageRoot)microsoft.vssdk.buildtools\17.14.2142\tools\vssdk\bin\lib\Microsoft.Internal.VisualStudio.Interop.dll</HintPath>
<HintPath>$(NuGetPackageRoot)microsoft.vssdk.buildtools\17.14.2142\tools\VSSDK\VisualStudioIntegration\Common\Assemblies\v4.0\Microsoft.Internal.VisualStudio.Interop.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>

<!-- Link the pure-BCL and VS-light source files for testing without a project reference
<!-- Link source files for testing without a project reference
(VSIX projects produce non-standard outputs that break test discovery). -->
<ItemGroup>
<Compile Include="..\Snipper.VisualStudio\LspSnippetConverter.cs" Link="src\LspSnippetConverter.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperBinaryLocator.cs" Link="src\SnipperBinaryLocator.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperOptionsPage.cs" Link="src\SnipperOptionsPage.cs" />
<Compile Include="..\Snipper.VisualStudio\TabstopInserter.cs" Link="src\TabstopInserter.cs" />
<Compile Include="..\Snipper.VisualStudio\LspSnippetConverter.cs" Link="src\LspSnippetConverter.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperBinaryLocator.cs" Link="src\SnipperBinaryLocator.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperOptionsPage.cs" Link="src\SnipperOptionsPage.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperPackage.cs" Link="src\SnipperPackage.cs" />
<Compile Include="..\Snipper.VisualStudio\TabstopInserter.cs" Link="src\TabstopInserter.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperLanguageServerProvider.cs" Link="src\SnipperLanguageClient.cs" />
<Compile Include="..\Snipper.VisualStudio\SnipperLspRpc.cs" Link="src\SnipperLspRpc.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using Microsoft.VisualStudio.Sdk.TestFramework.Xunit;

Check failure on line 2 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)

Check failure on line 2 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)
using Snipper.VisualStudio;
using Xunit;

Expand All @@ -7,7 +8,7 @@
[Collection(nameof(MockedVS))]
public class SnipperBinaryLocatorTests
{
[Fact]
[VsFact]

Check failure on line 11 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFact' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 11 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFactAttribute' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 11 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFact' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 11 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFactAttribute' could not be found (are you missing a using directive or an assembly reference?)
public void Resolve_ExistingConfiguredPath_ReturnsThatPath()
{
var tmp = Path.GetTempFileName();
Expand All @@ -22,21 +23,21 @@
}
}

[Fact]
[VsFact]

Check failure on line 26 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFact' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 26 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperBinaryLocatorTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'VsFactAttribute' could not be found (are you missing a using directive or an assembly reference?)
public void Resolve_NonExistentConfiguredPath_DoesNotReturn()
{
// Should skip the explicit path and fall through; not throw.
var result = SnipperBinaryLocator.Resolve(@"C:\definitely\does\not\exist\snipper-lsp.exe");
// result is null (not on PATH in test environment) or a valid PATH hit — just no throw.
}

[Fact]
[VsFact]
public void Resolve_NullConfiguredPath_DoesNotThrow()
{
var _ = SnipperBinaryLocator.Resolve(null);
}

[Fact]
[VsFact]
public void Resolve_EmptyConfiguredPath_DoesNotThrow()
{
var _ = SnipperBinaryLocator.Resolve(string.Empty);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Snipper.VisualStudio;
using StreamJsonRpc;
using Xunit;

namespace Snipper.VisualStudio.IntegrationTests;

/// <summary>
/// Tests for SnipperLspRpc.ExecuteCommandAsync using an in-memory fake LSP server.
/// No real snipper-lsp binary or VS install required.
///
/// Two separate unidirectional Pipe objects (client→server and server→client) are
/// used so that swapping the stream arguments in HeaderDelimitedMessageHandler
/// immediately breaks communication — a single FullDuplexStream passed for both
/// parameters would mask the argument order entirely.
/// </summary>
public class SnipperCommandRpcTests
{
[Fact]
public async Task ExecuteCommandAsync_ValidCommand_ReturnsSnippetBody()
{
const string expectedBody = "public ${1:ClassName}()\n{\n $0\n}";
var (serverOutput, serverInput, serverRead, serverWrite) = CreatePipes();

var serverTask = RunFakeServerAsync(serverRead, serverWrite, expectedBody);

var result = await SnipperLspRpc.ExecuteCommandAsync(
serverOutput, serverInput,
"snipper.scaffold-constructor",
CancellationToken.None);

await serverTask;

Assert.Equal(expectedBody, result);
}

[Fact]
public async Task ExecuteCommandAsync_ServerReturnsNull_ReturnsNull()
{
var (serverOutput, serverInput, serverRead, serverWrite) = CreatePipes();

var serverTask = RunFakeServerAsync(serverRead, serverWrite, snippetBody: null);

var result = await SnipperLspRpc.ExecuteCommandAsync(
serverOutput, serverInput,
"snipper.unknown-command",
CancellationToken.None);

await serverTask;

Assert.Null(result);
}

[Fact]
public async Task ExecuteCommandAsync_SendsInitializeBeforeCommand()
{
var (serverOutput, serverInput, serverRead, serverWrite) = CreatePipes();
bool initializeCalled = false;

var serverTask = Task.Run(async () =>
{
var handler = new HeaderDelimitedMessageHandler(
serverWrite, serverRead, new JsonMessageFormatter());
using var rpc = new JsonRpc(handler);
rpc.AddLocalRpcMethod("initialize",
new Func<object?, object>(_ => { initializeCalled = true; return new { capabilities = new { } }; }));
rpc.AddLocalRpcMethod("workspace/executeCommand",
new Func<object?, string?>(_ => "body"));
rpc.AddLocalRpcMethod("shutdown",
new Func<object?, object?>(_ => null));
rpc.AddLocalRpcMethod("initialized", new Action<object?>(_ => { }));
rpc.AddLocalRpcMethod("exit", new Action<object?>(_ => { }));
rpc.StartListening();
await rpc.Completion;
});

await SnipperLspRpc.ExecuteCommandAsync(
serverOutput, serverInput,
"snipper.scaffold-constructor",
CancellationToken.None);

await serverTask;

Assert.True(initializeCalled, "initialize was not sent before workspace/executeCommand");
}

// ── Helpers ─────────────────────────────────────────────────────────────

/// <summary>
/// Returns four named streams from two directed pipes.
/// serverOutput / serverInput are passed to ExecuteCommandAsync (client side).
/// serverRead / serverWrite are passed to the fake-server handler.
/// </summary>
private static (Stream serverOutput, Stream serverInput, Stream serverRead, Stream serverWrite)
CreatePipes()
{
var clientToServer = new Pipe();
var serverToClient = new Pipe();
return (
serverToClient.Reader.AsStream(), // client reads responses from here
clientToServer.Writer.AsStream(), // client writes requests to here
clientToServer.Reader.AsStream(), // server reads requests from here
serverToClient.Writer.AsStream() // server writes responses to here
);
}

private static Task RunFakeServerAsync(Stream readStream, Stream writeStream, string? snippetBody)
{
return Task.Run(async () =>
{
var handler = new HeaderDelimitedMessageHandler(
writeStream, readStream, new JsonMessageFormatter());
using var rpc = new JsonRpc(handler);

rpc.AddLocalRpcMethod("initialize",
new Func<object?, object>(_ => new { capabilities = new { } }));
rpc.AddLocalRpcMethod("workspace/executeCommand",
new Func<object?, string?>(_ => snippetBody));
rpc.AddLocalRpcMethod("shutdown",
new Func<object?, object?>(_ => null));
rpc.AddLocalRpcMethod("initialized", new Action<object?>(_ => { }));
rpc.AddLocalRpcMethod("exit", new Action<object?>(_ => { }));

rpc.StartListening();
await rpc.Completion;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Snipper.VisualStudio;
using Xunit;

namespace Snipper.VisualStudio.IntegrationTests;

/// <summary>
/// Tests for SnipperLanguageClient using plain [Fact] — no VS service mock needed
/// since ActivateAsync/OnLoadedAsync don't call into VS services during tests.
/// </summary>
public class SnipperLanguageClientTests
{
[Fact]
public async Task ActivateAsync_BinaryNotFound_ReturnsNull()
{
// SnipperPackage.Instance is null in tests → Resolve(null) tries default
// paths but finds nothing in the test environment.
var client = new SnipperLanguageClient();
var conn = await client.ActivateAsync(CancellationToken.None);
Assert.Null(conn);
}

[Fact]
public async Task OnLoadedAsync_WithStartAsyncHandler_FiresEvent()
{
var client = new SnipperLanguageClient();
bool fired = false;
client.StartAsync += (_, _) => { fired = true; return Task.CompletedTask; };

await client.OnLoadedAsync();

Assert.True(fired);
}

[Fact]
public async Task OnLoadedAsync_NoHandlers_DoesNotThrow()
{
var client = new SnipperLanguageClient();
await client.OnLoadedAsync(); // must not throw even if no subscribers
}

[Fact]
public async Task OnServerInitializeFailedAsync_ReturnsNull()
{
var client = new SnipperLanguageClient();
var result = await client.OnServerInitializeFailedAsync(null!);
Assert.Null(result);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.VisualStudio.Sdk.TestFramework.Xunit;

Check failure on line 1 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperOptionsPageTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)

Check failure on line 1 in extensions/snipper-vs/Snipper.VisualStudio.IntegrationTests/SnipperOptionsPageTests.cs

View workflow job for this annotation

GitHub Actions / vs-integration-tests

The type or namespace name 'Xunit' does not exist in the namespace 'Microsoft.VisualStudio.Sdk.TestFramework' (are you missing an assembly reference?)
using Snipper.VisualStudio;
using Xunit;

Expand All @@ -6,29 +7,29 @@
[Collection(nameof(MockedVS))]
public class SnipperOptionsPageTests
{
[Fact]
[VsFact]
public void DefaultServerPath_IsEmpty()
{
var page = new SnipperOptionsPage();
Assert.Equal(string.Empty, page.ServerPath);
}

[Fact]
[VsFact]
public void DefaultRoslynPath_IsEmpty()
{
var page = new SnipperOptionsPage();
Assert.Equal(string.Empty, page.RoslynPath);
}

[Fact]
[VsFact]
public void ServerPath_RoundTrips()
{
var page = new SnipperOptionsPage();
page.ServerPath = @"C:\tools\snipper-lsp.exe";
Assert.Equal(@"C:\tools\snipper-lsp.exe", page.ServerPath);
}

[Fact]
[VsFact]
public void RoslynPath_RoundTrips()
{
var page = new SnipperOptionsPage();
Expand Down
Loading
Loading