Skip to content

Commit 4ad1365

Browse files
authored
Merge pull request #1959 from paulvanbrenk/AnyCodeUnitTest
Open Folder Unit Test
2 parents bfaefdf + f7ae463 commit 4ad1365

25 files changed

+1006
-574
lines changed

Nodejs/Product/Nodejs/Nodejs.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@
8484
<Reference Include="envdte80, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
8585
<EmbedInteropTypes>False</EmbedInteropTypes>
8686
</Reference>
87+
<Reference Include="Microsoft.VisualStudio.TestWindow.Interfaces">
88+
<HintPath>$(DevEnvDir)CommonExtensions\Microsoft\TestWindow\Microsoft.VisualStudio.TestWindow.Interfaces.dll</HintPath>
89+
<Private>False</Private>
90+
</Reference>
91+
<Reference Include="Microsoft.VisualStudio.TestPlatform.ObjectModel">
92+
<HintPath>$(DevEnvDir)CommonExtensions\Microsoft\TestWindow\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll</HintPath>
93+
<Private>False</Private>
94+
</Reference>
8795
<Reference Include="microsoft.visualstudio.textmanager.interop.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
8896
<EmbedInteropTypes>True</EmbedInteropTypes>
8997
</Reference>
@@ -198,6 +206,8 @@
198206
<Compile Include="Workspace\BaseFileScanner.cs" />
199207
<Compile Include="Workspace\ContextMenuProvider.cs" />
200208
<Compile Include="Workspace\LaunchDebugTargetProvider.cs" />
209+
<Compile Include="Workspace\PackageJsonTestContainer.cs" />
210+
<Compile Include="Workspace\PackageJsonTestContainerDiscoverer.cs" />
201211
<Compile Include="Workspace\NodeJsDebugLaunchTargetProvider.cs" />
202212
<Compile Include="GeneratedHelpAboutVersion.cs">
203213
<AutoGen>True</AutoGen>

Nodejs/Product/Nodejs/NodejsConstants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public static bool ContainsNodeModulesOrBowerComponentsFolder(string path)
7474

7575
public const string ExecutorUriString = "executor://NodejsTestExecutor/v1";
7676
public static readonly Uri ExecutorUri = new Uri(ExecutorUriString);
77+
78+
public const string PackageJsonExecutorUriString = "executor://PackageJsonTestExecutor/v1";
79+
public static readonly Uri PackageJsonExecutorUri = new Uri(PackageJsonExecutorUriString);
80+
81+
private const string TestRootDataValueGuidString = "{FF41BE7F-6D8C-4D27-91D4-51E4233BC7E4}";
82+
public readonly static Guid TestRootDataValueGuid = new Guid(TestRootDataValueGuidString);
83+
84+
public const string TestRootDataValueName = nameof(TestRootDataValueName);
7785
}
7886

7987
internal static class NodeProjectProperty

Nodejs/Product/Nodejs/Workspace/PackageJsonScannerFactory.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ protected override Task<List<FileDataValue>> ComputeFileDataValuesAsync(string f
8787
// (See Microsoft.VisualStudio.Workspace.VSIntegration.UI.FileContextActionsCommandHandlersProvider.Provider.GetActionProviderForProjectConfiguration)
8888
fileDataValues.Add(new FileDataValue(DebugLaunchActionContext.ContextTypeGuid, main, null, target: null));
8989
}
90+
91+
var testRoot = packageJson.TestRoot;
92+
if (!string.IsNullOrEmpty(testRoot))
93+
{
94+
fileDataValues.Add(new FileDataValue(NodejsConstants.TestRootDataValueGuid, NodejsConstants.TestRootDataValueName, testRoot));
95+
}
96+
9097
return Task.FromResult(fileDataValues);
9198
}
9299
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.Threading;
8+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
9+
using Microsoft.VisualStudio.TestWindow.Extensibility;
10+
using Microsoft.VisualStudio.TestWindow.Extensibility.Model;
11+
12+
namespace Microsoft.NodejsTools.Workspace
13+
{
14+
public sealed class PackageJsonTestContainer : ITestContainer
15+
{
16+
private int version;
17+
18+
public PackageJsonTestContainer(ITestContainerDiscoverer discoverer, string source, string testRoot)
19+
{
20+
this.Discoverer = discoverer;
21+
this.Source = source;
22+
this.TestRoot = testRoot;
23+
}
24+
25+
private PackageJsonTestContainer(PackageJsonTestContainer other) :
26+
this(other.Discoverer, other.Source, other.TestRoot)
27+
{
28+
this.version = other.version;
29+
}
30+
31+
public ITestContainerDiscoverer Discoverer { get; }
32+
33+
public string Source { get; }
34+
35+
public string TestRoot { get; }
36+
37+
public IEnumerable<Guid> DebugEngines => Array.Empty<Guid>();
38+
39+
public FrameworkVersion TargetFramework => FrameworkVersion.None;
40+
41+
public Architecture TargetPlatform => Architecture.Default;
42+
43+
public bool IsAppContainerTestContainer => false;
44+
45+
public int CompareTo(ITestContainer other)
46+
{
47+
Debug.Assert(other is PackageJsonTestContainer, "Only test containers based on package.json are expected.");
48+
49+
var testContainer = (PackageJsonTestContainer)other;
50+
51+
if (this.version != testContainer.version)
52+
{
53+
return this.version - testContainer.version;
54+
}
55+
56+
var sourceCompare = StringComparer.OrdinalIgnoreCase.Compare(this.Source, testContainer.Source);
57+
58+
return sourceCompare != 0 ? sourceCompare : StringComparer.OrdinalIgnoreCase.Compare(this.TestRoot, testContainer.TestRoot);
59+
}
60+
61+
public IDeploymentData DeployAppContainer() => null;
62+
63+
public ITestContainer Snapshot() => new PackageJsonTestContainer(this);
64+
65+
public bool IsContained(string javaScriptFilePath)
66+
{
67+
Debug.Assert(!string.IsNullOrEmpty(javaScriptFilePath) && Path.IsPathRooted(javaScriptFilePath), "Expected a rooted path.");
68+
69+
return javaScriptFilePath.StartsWith(this.TestRoot, StringComparison.OrdinalIgnoreCase);
70+
}
71+
72+
public void IncreaseVersion()
73+
{
74+
Interlocked.Increment(ref this.version);
75+
}
76+
}
77+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel.Composition;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.VisualStudio.TestWindow.Extensibility;
10+
using Microsoft.VisualStudio.Workspace;
11+
using Microsoft.VisualStudio.Workspace.Indexing;
12+
using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts;
13+
14+
namespace Microsoft.NodejsTools.Workspace
15+
{
16+
[Export(typeof(ITestContainerDiscoverer))]
17+
public sealed class PackageJsonTestContainerDiscoverer : ITestContainerDiscoverer
18+
{
19+
private readonly IVsFolderWorkspaceService workspaceService;
20+
21+
private readonly List<PackageJsonTestContainer> containers = new List<PackageJsonTestContainer>();
22+
private readonly object containerLock = new object();
23+
24+
private IWorkspace activeWorkspace;
25+
26+
[ImportingConstructor]
27+
public PackageJsonTestContainerDiscoverer(IVsFolderWorkspaceService workspaceService)
28+
{
29+
this.workspaceService = workspaceService;
30+
this.workspaceService.OnActiveWorkspaceChanged += this.OnActiveWorkspaceChangedAsync;
31+
32+
if (this.workspaceService.CurrentWorkspace != null)
33+
{
34+
this.activeWorkspace = this.workspaceService.CurrentWorkspace;
35+
this.RegisterEvents();
36+
37+
this.activeWorkspace.JTF.RunAsync(async () =>
38+
{
39+
// Yield so we don't do this now. Don't want to block the constructor.
40+
await Task.Yield();
41+
42+
// See if we have an update
43+
await AttemptUpdateAsync();
44+
});
45+
}
46+
}
47+
48+
public Uri ExecutorUri => NodejsConstants.PackageJsonExecutorUri;
49+
50+
public IEnumerable<ITestContainer> TestContainers
51+
{
52+
get
53+
{
54+
lock (this.containerLock)
55+
{
56+
return this.containers.ToArray();
57+
}
58+
}
59+
}
60+
61+
public event EventHandler TestContainersUpdated;
62+
63+
private async Task AttemptUpdateAsync()
64+
{
65+
var workspace = this.activeWorkspace;
66+
if (workspace != null)
67+
{
68+
var indexService = workspace.GetIndexWorkspaceService();
69+
var filesDataValues = await indexService.GetFilesDataValuesAsync<string>(NodejsConstants.TestRootDataValueGuid);
70+
71+
lock (this.containerLock)
72+
{
73+
this.containers.Clear();
74+
foreach (var dataValue in filesDataValues)
75+
{
76+
var rootFilePath = workspace.MakeRooted(dataValue.Key);
77+
var testRoot = dataValue.Value.Where(f => f.Name == NodejsConstants.TestRootDataValueName).FirstOrDefault()?.Value;
78+
79+
if (!string.IsNullOrEmpty(testRoot))
80+
{
81+
var testRootPath = workspace.MakeRooted(testRoot);
82+
this.containers.Add(new PackageJsonTestContainer(this, rootFilePath, testRootPath));
83+
}
84+
}
85+
}
86+
}
87+
this.TestContainersUpdated?.Invoke(this, EventArgs.Empty);
88+
}
89+
90+
private async Task OnActiveWorkspaceChangedAsync(object sender, EventArgs e)
91+
{
92+
this.UnRegisterEvents();
93+
94+
this.activeWorkspace = this.workspaceService.CurrentWorkspace;
95+
96+
this.RegisterEvents();
97+
98+
await AttemptUpdateAsync();
99+
}
100+
101+
private void RegisterEvents()
102+
{
103+
var workspace = this.activeWorkspace;
104+
if (workspace != null)
105+
{
106+
var fileWatcherService = workspace.GetFileWatcherService();
107+
if (fileWatcherService != null)
108+
{
109+
fileWatcherService.OnFileSystemChanged += this.FileSystemChangedAsync;
110+
}
111+
112+
var indexService = workspace.GetIndexWorkspaceService();
113+
if (indexService != null)
114+
{
115+
indexService.OnFileScannerCompleted += this.FileScannerCompletedAsync;
116+
}
117+
}
118+
}
119+
120+
private void UnRegisterEvents()
121+
{
122+
var fileWatcherService = this.activeWorkspace?.GetFileWatcherService();
123+
if (fileWatcherService != null)
124+
{
125+
fileWatcherService.OnFileSystemChanged -= this.FileSystemChangedAsync;
126+
}
127+
128+
var indexService = this.activeWorkspace?.GetIndexWorkspaceService();
129+
if (indexService != null)
130+
{
131+
indexService.OnFileScannerCompleted -= this.FileScannerCompletedAsync;
132+
}
133+
}
134+
135+
private Task FileSystemChangedAsync(object sender, FileSystemEventArgs args)
136+
{
137+
// We only need to raise the containers updated event in case a js file contained in the
138+
// test root is updated.
139+
// Any changes to the 'package.json' will be handled by the FileScannerCompleted event.
140+
if (IsJavaScriptFile(args.FullPath) || args.IsDirectoryChanged())
141+
{
142+
// use a flag so we don't raise the event while under the lock
143+
var testsUpdated = false;
144+
lock (this.containerLock)
145+
{
146+
foreach (var container in this.containers)
147+
{
148+
if (container.IsContained(args.FullPath))
149+
{
150+
container.IncreaseVersion();
151+
testsUpdated = true;
152+
break;
153+
}
154+
}
155+
}
156+
if (testsUpdated)
157+
{
158+
this.TestContainersUpdated?.Invoke(this, EventArgs.Empty);
159+
}
160+
}
161+
162+
return Task.CompletedTask;
163+
}
164+
165+
private async Task FileScannerCompletedAsync(object sender, FileScannerEventArgs args)
166+
{
167+
await AttemptUpdateAsync();
168+
}
169+
170+
private static bool IsJavaScriptFile(string path)
171+
{
172+
var ext = Path.GetExtension(path);
173+
if (StringComparer.OrdinalIgnoreCase.Equals(ext, ".js") || StringComparer.OrdinalIgnoreCase.Equals(ext, ".jsx"))
174+
{
175+
return true;
176+
}
177+
178+
return false;
179+
}
180+
}
181+
}

Nodejs/Product/Nodejs/Workspace/WorkspaceExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static async Task<TsConfigJson> IsContainedByTsConfig(this IWorkspace wor
3232
return null;
3333
}
3434

35-
private sealed class FileCollector : IProgress<string>
35+
public sealed class FileCollector : IProgress<string>
3636
{
3737
public readonly List<string> FoundFiles = new List<string>();
3838

Nodejs/Product/Npm/IPackageJson.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ public interface IPackageJson
2121
IEnumerable<string> RequiredBy { get; }
2222
string Main { get; }
2323
IPackageJsonScript[] Scripts { get; }
24+
string TestRoot { get; }
2425
}
2526
}

Nodejs/Product/Npm/SPI/PackageJson.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public PackageJson(dynamic package)
3131
this.Author = package.author == null ? null : Person.CreateFromJsonSource(package.author.ToString());
3232
this.Main = package.main?.ToString();
3333
this.Scripts = LoadScripts(package);
34+
this.TestRoot = LoadVsTestOptions(package);
3435
}
3536

3637
private static PackageJsonException WrapRuntimeBinderException(string errorProperty, RuntimeBinderException rbe)
@@ -44,7 +45,6 @@ private static PackageJsonException WrapRuntimeBinderException(string errorPrope
4445
errorProperty,
4546
rbe));
4647
}
47-
4848
private static IKeywords LoadKeywords(dynamic package)
4949
{
5050
try
@@ -172,6 +172,23 @@ private static IPackageJsonScript[] LoadScripts(dynamic package)
172172
}
173173
}
174174

175+
private string LoadVsTestOptions(dynamic package)
176+
{
177+
try
178+
{
179+
if (package["vsTest"] is JObject vsTest && vsTest["testRoot"] is JValue testRoot)
180+
{
181+
return testRoot.ToString();
182+
}
183+
184+
return null;
185+
}
186+
catch (RuntimeBinderException rbe)
187+
{
188+
throw WrapRuntimeBinderException("vsTestOptions", rbe);
189+
}
190+
}
191+
175192
public string Name { get; }
176193

177194
public SemverVersion Version
@@ -202,5 +219,6 @@ public SemverVersion Version
202219
public IEnumerable<string> RequiredBy { get; }
203220
public string Main { get; }
204221
public IPackageJsonScript[] Scripts { get; }
222+
public string TestRoot { get; }
205223
}
206224
}

0 commit comments

Comments
 (0)