Skip to content

Commit

Permalink
Merge pull request #1959 from paulvanbrenk/AnyCodeUnitTest
Browse files Browse the repository at this point in the history
Open Folder Unit Test
  • Loading branch information
paulvanbrenk committed Jun 1, 2018
2 parents bfaefdf + f7ae463 commit 4ad1365
Show file tree
Hide file tree
Showing 25 changed files with 1,006 additions and 574 deletions.
10 changes: 10 additions & 0 deletions Nodejs/Product/Nodejs/Nodejs.csproj
Expand Up @@ -84,6 +84,14 @@
<Reference Include="envdte80, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestWindow.Interfaces">
<HintPath>$(DevEnvDir)CommonExtensions\Microsoft\TestWindow\Microsoft.VisualStudio.TestWindow.Interfaces.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestPlatform.ObjectModel">
<HintPath>$(DevEnvDir)CommonExtensions\Microsoft\TestWindow\Microsoft.VisualStudio.TestPlatform.ObjectModel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="microsoft.visualstudio.textmanager.interop.11.0, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
Expand Down Expand Up @@ -198,6 +206,8 @@
<Compile Include="Workspace\BaseFileScanner.cs" />
<Compile Include="Workspace\ContextMenuProvider.cs" />
<Compile Include="Workspace\LaunchDebugTargetProvider.cs" />
<Compile Include="Workspace\PackageJsonTestContainer.cs" />
<Compile Include="Workspace\PackageJsonTestContainerDiscoverer.cs" />
<Compile Include="Workspace\NodeJsDebugLaunchTargetProvider.cs" />
<Compile Include="GeneratedHelpAboutVersion.cs">
<AutoGen>True</AutoGen>
Expand Down
8 changes: 8 additions & 0 deletions Nodejs/Product/Nodejs/NodejsConstants.cs
Expand Up @@ -74,6 +74,14 @@ public static bool ContainsNodeModulesOrBowerComponentsFolder(string path)

public const string ExecutorUriString = "executor://NodejsTestExecutor/v1";
public static readonly Uri ExecutorUri = new Uri(ExecutorUriString);

public const string PackageJsonExecutorUriString = "executor://PackageJsonTestExecutor/v1";
public static readonly Uri PackageJsonExecutorUri = new Uri(PackageJsonExecutorUriString);

private const string TestRootDataValueGuidString = "{FF41BE7F-6D8C-4D27-91D4-51E4233BC7E4}";
public readonly static Guid TestRootDataValueGuid = new Guid(TestRootDataValueGuidString);

public const string TestRootDataValueName = nameof(TestRootDataValueName);
}

internal static class NodeProjectProperty
Expand Down
7 changes: 7 additions & 0 deletions Nodejs/Product/Nodejs/Workspace/PackageJsonScannerFactory.cs
Expand Up @@ -87,6 +87,13 @@ protected override Task<List<FileDataValue>> ComputeFileDataValuesAsync(string f
// (See Microsoft.VisualStudio.Workspace.VSIntegration.UI.FileContextActionsCommandHandlersProvider.Provider.GetActionProviderForProjectConfiguration)
fileDataValues.Add(new FileDataValue(DebugLaunchActionContext.ContextTypeGuid, main, null, target: null));
}

var testRoot = packageJson.TestRoot;
if (!string.IsNullOrEmpty(testRoot))
{
fileDataValues.Add(new FileDataValue(NodejsConstants.TestRootDataValueGuid, NodejsConstants.TestRootDataValueName, testRoot));
}

return Task.FromResult(fileDataValues);
}
}
Expand Down
77 changes: 77 additions & 0 deletions Nodejs/Product/Nodejs/Workspace/PackageJsonTestContainer.cs
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestWindow.Extensibility;
using Microsoft.VisualStudio.TestWindow.Extensibility.Model;

namespace Microsoft.NodejsTools.Workspace
{
public sealed class PackageJsonTestContainer : ITestContainer
{
private int version;

public PackageJsonTestContainer(ITestContainerDiscoverer discoverer, string source, string testRoot)
{
this.Discoverer = discoverer;
this.Source = source;
this.TestRoot = testRoot;
}

private PackageJsonTestContainer(PackageJsonTestContainer other) :
this(other.Discoverer, other.Source, other.TestRoot)
{
this.version = other.version;
}

public ITestContainerDiscoverer Discoverer { get; }

public string Source { get; }

public string TestRoot { get; }

public IEnumerable<Guid> DebugEngines => Array.Empty<Guid>();

public FrameworkVersion TargetFramework => FrameworkVersion.None;

public Architecture TargetPlatform => Architecture.Default;

public bool IsAppContainerTestContainer => false;

public int CompareTo(ITestContainer other)
{
Debug.Assert(other is PackageJsonTestContainer, "Only test containers based on package.json are expected.");

var testContainer = (PackageJsonTestContainer)other;

if (this.version != testContainer.version)
{
return this.version - testContainer.version;
}

var sourceCompare = StringComparer.OrdinalIgnoreCase.Compare(this.Source, testContainer.Source);

return sourceCompare != 0 ? sourceCompare : StringComparer.OrdinalIgnoreCase.Compare(this.TestRoot, testContainer.TestRoot);
}

public IDeploymentData DeployAppContainer() => null;

public ITestContainer Snapshot() => new PackageJsonTestContainer(this);

public bool IsContained(string javaScriptFilePath)
{
Debug.Assert(!string.IsNullOrEmpty(javaScriptFilePath) && Path.IsPathRooted(javaScriptFilePath), "Expected a rooted path.");

return javaScriptFilePath.StartsWith(this.TestRoot, StringComparison.OrdinalIgnoreCase);
}

public void IncreaseVersion()
{
Interlocked.Increment(ref this.version);
}
}
}
181 changes: 181 additions & 0 deletions Nodejs/Product/Nodejs/Workspace/PackageJsonTestContainerDiscoverer.cs
@@ -0,0 +1,181 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestWindow.Extensibility;
using Microsoft.VisualStudio.Workspace;
using Microsoft.VisualStudio.Workspace.Indexing;
using Microsoft.VisualStudio.Workspace.VSIntegration.Contracts;

namespace Microsoft.NodejsTools.Workspace
{
[Export(typeof(ITestContainerDiscoverer))]
public sealed class PackageJsonTestContainerDiscoverer : ITestContainerDiscoverer
{
private readonly IVsFolderWorkspaceService workspaceService;

private readonly List<PackageJsonTestContainer> containers = new List<PackageJsonTestContainer>();
private readonly object containerLock = new object();

private IWorkspace activeWorkspace;

[ImportingConstructor]
public PackageJsonTestContainerDiscoverer(IVsFolderWorkspaceService workspaceService)
{
this.workspaceService = workspaceService;
this.workspaceService.OnActiveWorkspaceChanged += this.OnActiveWorkspaceChangedAsync;

if (this.workspaceService.CurrentWorkspace != null)
{
this.activeWorkspace = this.workspaceService.CurrentWorkspace;
this.RegisterEvents();

this.activeWorkspace.JTF.RunAsync(async () =>
{
// Yield so we don't do this now. Don't want to block the constructor.
await Task.Yield();
// See if we have an update
await AttemptUpdateAsync();
});
}
}

public Uri ExecutorUri => NodejsConstants.PackageJsonExecutorUri;

public IEnumerable<ITestContainer> TestContainers
{
get
{
lock (this.containerLock)
{
return this.containers.ToArray();
}
}
}

public event EventHandler TestContainersUpdated;

private async Task AttemptUpdateAsync()
{
var workspace = this.activeWorkspace;
if (workspace != null)
{
var indexService = workspace.GetIndexWorkspaceService();
var filesDataValues = await indexService.GetFilesDataValuesAsync<string>(NodejsConstants.TestRootDataValueGuid);

lock (this.containerLock)
{
this.containers.Clear();
foreach (var dataValue in filesDataValues)
{
var rootFilePath = workspace.MakeRooted(dataValue.Key);
var testRoot = dataValue.Value.Where(f => f.Name == NodejsConstants.TestRootDataValueName).FirstOrDefault()?.Value;

if (!string.IsNullOrEmpty(testRoot))
{
var testRootPath = workspace.MakeRooted(testRoot);
this.containers.Add(new PackageJsonTestContainer(this, rootFilePath, testRootPath));
}
}
}
}
this.TestContainersUpdated?.Invoke(this, EventArgs.Empty);
}

private async Task OnActiveWorkspaceChangedAsync(object sender, EventArgs e)
{
this.UnRegisterEvents();

this.activeWorkspace = this.workspaceService.CurrentWorkspace;

this.RegisterEvents();

await AttemptUpdateAsync();
}

private void RegisterEvents()
{
var workspace = this.activeWorkspace;
if (workspace != null)
{
var fileWatcherService = workspace.GetFileWatcherService();
if (fileWatcherService != null)
{
fileWatcherService.OnFileSystemChanged += this.FileSystemChangedAsync;
}

var indexService = workspace.GetIndexWorkspaceService();
if (indexService != null)
{
indexService.OnFileScannerCompleted += this.FileScannerCompletedAsync;
}
}
}

private void UnRegisterEvents()
{
var fileWatcherService = this.activeWorkspace?.GetFileWatcherService();
if (fileWatcherService != null)
{
fileWatcherService.OnFileSystemChanged -= this.FileSystemChangedAsync;
}

var indexService = this.activeWorkspace?.GetIndexWorkspaceService();
if (indexService != null)
{
indexService.OnFileScannerCompleted -= this.FileScannerCompletedAsync;
}
}

private Task FileSystemChangedAsync(object sender, FileSystemEventArgs args)
{
// We only need to raise the containers updated event in case a js file contained in the
// test root is updated.
// Any changes to the 'package.json' will be handled by the FileScannerCompleted event.
if (IsJavaScriptFile(args.FullPath) || args.IsDirectoryChanged())
{
// use a flag so we don't raise the event while under the lock
var testsUpdated = false;
lock (this.containerLock)
{
foreach (var container in this.containers)
{
if (container.IsContained(args.FullPath))
{
container.IncreaseVersion();
testsUpdated = true;
break;
}
}
}
if (testsUpdated)
{
this.TestContainersUpdated?.Invoke(this, EventArgs.Empty);
}
}

return Task.CompletedTask;
}

private async Task FileScannerCompletedAsync(object sender, FileScannerEventArgs args)
{
await AttemptUpdateAsync();
}

private static bool IsJavaScriptFile(string path)
{
var ext = Path.GetExtension(path);
if (StringComparer.OrdinalIgnoreCase.Equals(ext, ".js") || StringComparer.OrdinalIgnoreCase.Equals(ext, ".jsx"))
{
return true;
}

return false;
}
}
}
2 changes: 1 addition & 1 deletion Nodejs/Product/Nodejs/Workspace/WorkspaceExtensions.cs
Expand Up @@ -32,7 +32,7 @@ public static async Task<TsConfigJson> IsContainedByTsConfig(this IWorkspace wor
return null;
}

private sealed class FileCollector : IProgress<string>
public sealed class FileCollector : IProgress<string>
{
public readonly List<string> FoundFiles = new List<string>();

Expand Down
1 change: 1 addition & 0 deletions Nodejs/Product/Npm/IPackageJson.cs
Expand Up @@ -21,5 +21,6 @@ public interface IPackageJson
IEnumerable<string> RequiredBy { get; }
string Main { get; }
IPackageJsonScript[] Scripts { get; }
string TestRoot { get; }
}
}
20 changes: 19 additions & 1 deletion Nodejs/Product/Npm/SPI/PackageJson.cs
Expand Up @@ -31,6 +31,7 @@ public PackageJson(dynamic package)
this.Author = package.author == null ? null : Person.CreateFromJsonSource(package.author.ToString());
this.Main = package.main?.ToString();
this.Scripts = LoadScripts(package);
this.TestRoot = LoadVsTestOptions(package);
}

private static PackageJsonException WrapRuntimeBinderException(string errorProperty, RuntimeBinderException rbe)
Expand All @@ -44,7 +45,6 @@ private static PackageJsonException WrapRuntimeBinderException(string errorPrope
errorProperty,
rbe));
}

private static IKeywords LoadKeywords(dynamic package)
{
try
Expand Down Expand Up @@ -172,6 +172,23 @@ private static IPackageJsonScript[] LoadScripts(dynamic package)
}
}

private string LoadVsTestOptions(dynamic package)
{
try
{
if (package["vsTest"] is JObject vsTest && vsTest["testRoot"] is JValue testRoot)
{
return testRoot.ToString();
}

return null;
}
catch (RuntimeBinderException rbe)
{
throw WrapRuntimeBinderException("vsTestOptions", rbe);
}
}

public string Name { get; }

public SemverVersion Version
Expand Down Expand Up @@ -202,5 +219,6 @@ public SemverVersion Version
public IEnumerable<string> RequiredBy { get; }
public string Main { get; }
public IPackageJsonScript[] Scripts { get; }
public string TestRoot { get; }
}
}

0 comments on commit 4ad1365

Please sign in to comment.