diff --git a/CHANGELOG.md b/CHANGELOG.md index bace3ca..a75f41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,89 +1,117 @@ # Code Editor Package for Visual Studio +## [2.0.7] - 2021-02-02 + +Integration: + +Remove com.unity.nuget.newtonsoft-json dependency in favor of the built-in JsonUtility for the VS Test Runner. + + +## [2.0.6] - 2021-01-20 + +Project generation: + +- Improved language version detection. + +Integration: + +- Added support for the VS Test Runner. +- Added initial support for displaying asset usage. +- Fixed remaining issues with special characters in file/path. + ## [2.0.5] - 2020-10-30 Integration: -Disable legacy pdb symbol checking for Unity packages +- Disable legacy pdb symbol checking for Unity packages. +## [2.0.4] - 2020-10-15 + +Project generation: + +- Added support for embedded Roslyn analyzer DLLs and ruleset files. +- Warn the user when the opened script is not part of the generation scope. +- Warn the user when the selected Visual Studio installation is not found. +- Generate a .vsconfig file to ensure Visual Studio installation is compatible. + +Integration: + +- Fix automation issues on MacOS, where a new Visual Studio instance is opened every time. ## [2.0.3] - 2020-09-09 Project generation: -Added C#8 language support. -Added UnityProjectGeneratorVersion property. -Local and Embedded packages are now selected by default for generation. -Added support for asmdef root namespace. +- Added C#8 language support. +- Added UnityProjectGeneratorVersion property. +- Local and Embedded packages are now selected by default for generation. +- Added support for asmdef root namespace. Integration: -When the user disabled auto-refresh in Unity, do not try to force refresh the Asset database. -Fix Visual Studio detection issues with languages using special characters. +- When the user disabled auto-refresh in Unity, do not try to force refresh the Asset database. +- Fix Visual Studio detection issues with languages using special characters. ## [2.0.2] - 2020-05-27 -Added support for solution folders. -Only bind the messenger when the VS editor is selected. -Warn when unable to create the messenger. -Fixed an initialization issue triggering legacy code generation. -Allow package source in assembly to be generated when referenced from asmref. +- Added support for solution folders. +- Only bind the messenger when the VS editor is selected. +- Warn when unable to create the messenger. +- Fixed an initialization issue triggering legacy code generation. +- Allow package source in assembly to be generated when referenced from asmref. ## [2.0.1] - 2020-03-19 -When Visual Studio installation is compatible with C# 8.0, setup the language version to not prompt the user with unsupported constructs. (So far Unity only supports C# 7.3). -Use Unity's TypeCache to improve project generation speed. -Properly check for a managed assembly before displaying a warning regarding legacy PDB usage. -Add support for selective project generation (embedded, local, registry, git, builtin, player). - +- When Visual Studio installation is compatible with C# 8.0, setup the language version to not prompt the user with unsupported constructs. (So far Unity only supports C# 7.3). +- Use Unity's TypeCache to improve project generation speed. +- Properly check for a managed assembly before displaying a warning regarding legacy PDB usage. +- Add support for selective project generation (embedded, local, registry, git, builtin, player). ## [2.0.0] - 2019-11-06 -- Improved Visual Studio and Visual Studio for Mac automatic discovery -- Added support for the VSTU messaging system (start/stop features from Visual Studio) -- Added support for solution roundtrip (preserves references to external projects and solution properties) -- Added support for VSTU Analyzers (requires Visual Studio 2019 16.3, Visual Studio for Mac 8.3) +- Improved Visual Studio and Visual Studio for Mac automatic discovery. +- Added support for the VSTU messaging system (start/stop features from Visual Studio). +- Added support for solution roundtrip (preserves references to external projects and solution properties). +- Added support for VSTU Analyzers (requires Visual Studio 2019 16.3, Visual Studio for Mac 8.3). - Added a warning when using legacy pdb symbol files. -- Fixed issues while Opening Visual Studio on Windows -- Fixed issues while Opening Visual Studio on Mac +- Fixed issues while Opening Visual Studio on Windows. +- Fixed issues while Opening Visual Studio on Mac. ## [1.1.1] - 2019-05-29 -Fix Bridge assembly loading with non VS2017 editors +- Fix Bridge assembly loading with non VS2017 editors. ## [1.1.0] - 2019-05-27 -Move internal extension handling to package. +- Move internal extension handling to package. ## [1.0.11] - 2019-05-21 -Fix detection of visual studio for mac installation. +- Fix detection of visual studio for mac installation. ## [1.0.10] - 2019-05-04 -Fix ignored comintegration executable - +- Fix ignored comintegration executable. ## [1.0.9] - 2019-03-05 -Updated MonoDevelop support, to pass correct arguments, and not import VSTU plugin -Use release build of COMIntegration for Visual Studio - +- Updated MonoDevelop support, to pass correct arguments, and not import VSTU plugin. +- Use release build of COMIntegration for Visual Studio. ## [1.0.7] - 2019-04-30 -Ensure asset database is refreshed when generating csproj and solution files. +- Ensure asset database is refreshed when generating csproj and solution files. ## [1.0.6] - 2019-04-27 -Add support for generating all csproj files. +- Add support for generating all csproj files. ## [1.0.5] - 2019-04-18 -Fix relative package paths. -Fix opening editor on mac. +- Fix relative package paths. +- Fix opening editor on mac. ## [1.0.4] - 2019-04-12 @@ -94,4 +122,4 @@ Fix opening editor on mac. ### This is the first release of *Unity Package visualstudio_editor*. -Using the newly created api to integrate Visual Studio with Unity. +- Using the newly created api to integrate Visual Studio with Unity. diff --git a/Editor/COMIntegration/Release/COMIntegration.exe b/Editor/COMIntegration/Release/COMIntegration.exe index a644e95..2ead0a7 100644 Binary files a/Editor/COMIntegration/Release/COMIntegration.exe and b/Editor/COMIntegration/Release/COMIntegration.exe differ diff --git a/Editor/FileUtility.cs b/Editor/FileUtility.cs index 3e0817f..f23b720 100644 --- a/Editor/FileUtility.cs +++ b/Editor/FileUtility.cs @@ -57,7 +57,17 @@ namespace Microsoft.Unity.VisualStudio.Editor return path.Replace(WinSeparator, UnixSeparator); } - internal static bool IsFileInProjectDirectory(string fileName) + internal static bool IsFileInProjectRootDirectory(string fileName) + { + var relative = MakeRelativeToProjectPath(fileName); + if (string.IsNullOrEmpty(relative)) + return false; + + return relative == Path.GetFileName(relative); + } + + // returns null if outside of the project scope + internal static string MakeRelativeToProjectPath(string fileName) { var basePath = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); fileName = Normalize(fileName); @@ -65,7 +75,13 @@ namespace Microsoft.Unity.VisualStudio.Editor if (!Path.IsPathRooted(fileName)) fileName = Path.Combine(basePath, fileName); - return string.Equals(Path.GetDirectoryName(fileName), basePath, StringComparison.OrdinalIgnoreCase); + if (!fileName.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) + return null; + + return fileName + .Substring(basePath.Length) + .Trim(Path.DirectorySeparatorChar); } + } } diff --git a/Editor/Messaging/Deserializer.cs b/Editor/Messaging/Deserializer.cs index 039dc8f..d033e56 100644 --- a/Editor/Messaging/Deserializer.cs +++ b/Editor/Messaging/Deserializer.cs @@ -11,9 +11,6 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging { private readonly BinaryReader _reader; - // Max UDP packet size is 65507 - private const int MaxStringLength = 65000; - public Deserializer(byte[] buffer) { _reader = new BinaryReader(new MemoryStream(buffer)); @@ -27,7 +24,7 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging public string ReadString() { var length = ReadInt32(); - return length > 0 && length <= MaxStringLength + return length > 0 ? Encoding.UTF8.GetString(_reader.ReadBytes(length)) : ""; } diff --git a/Editor/Messaging/MessageType.cs b/Editor/Messaging/MessageType.cs index 9850e68..ae15bb6 100644 --- a/Editor/Messaging/MessageType.cs +++ b/Editor/Messaging/MessageType.cs @@ -30,5 +30,19 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging UpdatePackage, ProjectPath, + + // This message is a technical one for big messages, not intended to be used directly + Tcp, + + RunStarted, + RunFinished, + TestStarted, + TestFinished, + TestListRetrieved, + + RetrieveTestList, + ExecuteTests, + + ShowUsage } } diff --git a/Editor/Messaging/Messenger.cs b/Editor/Messaging/Messenger.cs index 49ae086..d88071e 100644 --- a/Editor/Messaging/Messenger.cs +++ b/Editor/Messaging/Messenger.cs @@ -71,7 +71,23 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging if (message != null) { message.Origin = (IPEndPoint)endPoint; - ReceiveMessage?.Invoke(this, new MessageEventArgs(message)); + + int port; + int bufferSize; + if (IsValidTcpMessage(message, out port, out bufferSize)) + { + // switch to TCP mode to handle big messages + TcpClient.Queue(message.Origin.Address, port, bufferSize, buffer => + { + var originalMessage = DeserializeMessage(buffer); + originalMessage.Origin = message.Origin; + ReceiveMessage?.Invoke(this, new MessageEventArgs(originalMessage)); + }); + } + else + { + ReceiveMessage?.Invoke(this, new MessageEventArgs(message)); + } } } catch (ObjectDisposedException) @@ -86,6 +102,22 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging BeginReceiveMessage(); } + private static bool IsValidTcpMessage(Message message, out int port, out int bufferSize) + { + port = 0; + bufferSize = 0; + if (message.Value == null) + return false; + if (message.Type != MessageType.Tcp) + return false; + var parts = message.Value.Split(':'); + if (parts.Length != 2) + return false; + if (!int.TryParse(parts[0], out port)) + return false; + return int.TryParse(parts[1], out bufferSize); + } + private void RaiseMessagerException(Exception e) { MessagerException?.Invoke(this, new ExceptionEventArgs(e)); @@ -108,6 +140,18 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging if (_disposed) return; + if (buffer.Length >= UdpSocket.BufferSize) + { + // switch to TCP mode to handle big messages + var port = TcpListener.Queue(buffer); + if (port > 0) + { + // success, replace original message with "switch to tcp" marker + port information + buffer length + message = MessageFor(MessageType.Tcp, string.Concat(port, ':', buffer.Length)); + buffer = SerializeMessage(message); + } + } + _socket.BeginSendTo(buffer, 0, Math.Min(buffer.Length, UdpSocket.BufferSize), SocketFlags.None, target, SendMessageCallback, null); } } diff --git a/Editor/Messaging/TcpClient.cs b/Editor/Messaging/TcpClient.cs new file mode 100644 index 0000000..ef25af6 --- /dev/null +++ b/Editor/Messaging/TcpClient.cs @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Microsoft.Unity.VisualStudio.Editor.Messaging +{ + internal class TcpClient + { + private const int ConnectOrReadTimeoutMilliseconds = 5000; + + private class State + { + public System.Net.Sockets.TcpClient TcpClient; + public NetworkStream NetworkStream; + public byte[] Buffer; + public Action OnBufferAvailable; + } + + public static void Queue(IPAddress address, int port, int bufferSize, Action onBufferAvailable) + { + var client = new System.Net.Sockets.TcpClient(); + var state = new State {OnBufferAvailable = onBufferAvailable, TcpClient = client, Buffer = new byte[bufferSize]}; + + try + { + ThreadPool.QueueUserWorkItem(_ => + { + var handle = client.BeginConnect(address, port, OnClientConnected, state); + if (!handle.AsyncWaitHandle.WaitOne(ConnectOrReadTimeoutMilliseconds)) + Cleanup(state); + }); + } + catch (Exception) + { + Cleanup(state); + } + } + + private static void OnClientConnected(IAsyncResult result) + { + var state = (State)result.AsyncState; + + try + { + state.TcpClient.EndConnect(result); + state.NetworkStream = state.TcpClient.GetStream(); + var handle = state.NetworkStream.BeginRead(state.Buffer, 0, state.Buffer.Length, OnEndRead, state); + if (!handle.AsyncWaitHandle.WaitOne(ConnectOrReadTimeoutMilliseconds)) + Cleanup(state); + } + catch (Exception) + { + Cleanup(state); + } + } + + private static void OnEndRead(IAsyncResult result) + { + var state = (State)result.AsyncState; + + try + { + var count = state.NetworkStream.EndRead(result); + if (count == state.Buffer.Length) + state.OnBufferAvailable?.Invoke(state.Buffer); + } + catch (Exception) + { + // Ignore and cleanup + } + finally + { + Cleanup(state); + } + } + + private static void Cleanup(State state) + { + state.NetworkStream?.Dispose(); + state.TcpClient?.Close(); + + state.NetworkStream = null; + state.TcpClient = null; + state.Buffer = null; + state.OnBufferAvailable = null; + } + } +} diff --git a/Editor/Messaging/TcpClient.cs.meta b/Editor/Messaging/TcpClient.cs.meta new file mode 100644 index 0000000..ea31c00 --- /dev/null +++ b/Editor/Messaging/TcpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6674c38820d12a49ac116d416521d85 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Messaging/TcpListener.cs b/Editor/Messaging/TcpListener.cs new file mode 100644 index 0000000..ebeb17e --- /dev/null +++ b/Editor/Messaging/TcpListener.cs @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + using System; +using System.Net; +using System.Threading; + +namespace Microsoft.Unity.VisualStudio.Editor.Messaging +{ + internal class TcpListener + { + private const int ListenTimeoutMilliseconds = 5000; + + private class State + { + public System.Net.Sockets.TcpListener TcpListener; + public byte[] Buffer; + } + + public static int Queue(byte[] buffer) + { + var tcpListener = new System.Net.Sockets.TcpListener(IPAddress.Any, 0); + var state = new State {Buffer = buffer, TcpListener = tcpListener}; + + try + { + tcpListener.Start(); + + int port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; + + ThreadPool.QueueUserWorkItem(_ => + { + bool listening = true; + + while (listening) + { + var handle = tcpListener.BeginAcceptTcpClient(OnIncomingConnection, state); + listening = handle.AsyncWaitHandle.WaitOne(ListenTimeoutMilliseconds); + } + + Cleanup(state); + }); + + return port; + } + catch (Exception) + { + Cleanup(state); + return -1; + } + } + + private static void OnIncomingConnection(IAsyncResult result) + { + var state = (State)result.AsyncState; + + try + { + using (var client = state.TcpListener.EndAcceptTcpClient(result)) + { + using (var stream = client.GetStream()) + { + stream.Write(state.Buffer, 0, state.Buffer.Length); + } + } + } + catch (Exception) + { + // Ignore and cleanup + } + } + + private static void Cleanup(State state) + { + state.TcpListener?.Stop(); + + state.TcpListener = null; + state.Buffer = null; + } + } +} diff --git a/Editor/Messaging/TcpListener.cs.meta b/Editor/Messaging/TcpListener.cs.meta new file mode 100644 index 0000000..da211fe --- /dev/null +++ b/Editor/Messaging/TcpListener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ded625cf0d03fa94c9f939fd13ced18d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Messaging/UdpSocket.cs b/Editor/Messaging/UdpSocket.cs index 2432dba..f094780 100644 --- a/Editor/Messaging/UdpSocket.cs +++ b/Editor/Messaging/UdpSocket.cs @@ -10,6 +10,8 @@ namespace Microsoft.Unity.VisualStudio.Editor.Messaging { internal class UdpSocket : Socket { + // Maximum UDP payload is 65507 bytes. + // TCP mode will be used when the payload is bigger than this BufferSize public const int BufferSize = 1024 * 8; internal UdpSocket() diff --git a/Editor/ProjectGeneration/ProjectGeneration.cs b/Editor/ProjectGeneration/ProjectGeneration.cs index f3d3a45..c96f26b 100644 --- a/Editor/ProjectGeneration/ProjectGeneration.cs +++ b/Editor/ProjectGeneration/ProjectGeneration.cs @@ -560,15 +560,13 @@ namespace Microsoft.Unity.VisualStudio.Editor var targetFrameworkVersion = "v4.7.1"; var targetLanguageVersion = "latest"; // danger: latest is not the same absolute value depending on the VS version. - if (m_CurrentInstallation != null && m_CurrentInstallation.SupportsCSharp8) + if (m_CurrentInstallation != null) { - // Current VS installation is compatible with C# 8. + var vsLanguageSupport = m_CurrentInstallation.LatestLanguageVersionSupported; + var unityLanguageSupport = UnityInstallation.LatestLanguageVersionSupported(assembly); -#if !UNITY_2020_2_OR_NEWER - // Unity 2020.2.0a12 added support for C# 8 - // <=2020.1 has no support for C# 8 constructs, so tell the compiler to accept only C# 7.3 or lower. - targetLanguageVersion = "7.3"; -#endif + // Use the minimal supported version between VS and Unity, so that compilation will work in both + targetLanguageVersion = (vsLanguageSupport <= unityLanguageSupport ? vsLanguageSupport : unityLanguageSupport).ToString(2); // (major, minor) only } var projectType = ProjectTypeOf(assembly.name); @@ -801,7 +799,7 @@ namespace Microsoft.Unity.VisualStudio.Editor { // Add all projects that were previously in the solution and that are not generated by Unity, nor generated in the project root directory var externalProjects = previousSolution.Projects - .Where(p => p.IsSolutionFolderProjectFactory() || !FileUtility.IsFileInProjectDirectory(p.FileName)) + .Where(p => p.IsSolutionFolderProjectFactory() || !FileUtility.IsFileInProjectRootDirectory(p.FileName)) .Where(p => generatedProjects.All(gp => gp.FileName != p.FileName)); projects.AddRange(externalProjects); diff --git a/Editor/Testing.meta b/Editor/Testing.meta new file mode 100644 index 0000000..4f5eceb --- /dev/null +++ b/Editor/Testing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f9f1d015d7a8ba46b7d71acfcda3ae7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Testing/TestAdaptor.cs b/Editor/Testing/TestAdaptor.cs new file mode 100644 index 0000000..63b5320 --- /dev/null +++ b/Editor/Testing/TestAdaptor.cs @@ -0,0 +1,39 @@ +using System; + +using UnityEditor.TestTools.TestRunner.Api; + +namespace Microsoft.Unity.VisualStudio.Editor.Testing +{ + [Serializable] + internal class TestAdaptorContainer + { + public TestAdaptor[] TestAdaptors; + } + + [Serializable] + internal class TestAdaptor + { + public string Id; + public string Name; + public string FullName; + + public string Type; + public string Method; + public string Assembly; + + public int Parent; + + public TestAdaptor(ITestAdaptor testAdaptor, int parent) + { + Id = testAdaptor.Id; + Name = testAdaptor.Name; + FullName = testAdaptor.FullName; + + Type = testAdaptor.TypeInfo?.FullName; + Method = testAdaptor?.Method?.Name; + Assembly = testAdaptor.TypeInfo?.Assembly?.Location; + + Parent = parent; + } + } +} diff --git a/Editor/Testing/TestAdaptor.cs.meta b/Editor/Testing/TestAdaptor.cs.meta new file mode 100644 index 0000000..a24bbff --- /dev/null +++ b/Editor/Testing/TestAdaptor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b73b3de0d473d4a1c887ab31f69b1a8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Testing/TestResultAdaptor.cs b/Editor/Testing/TestResultAdaptor.cs new file mode 100644 index 0000000..335e411 --- /dev/null +++ b/Editor/Testing/TestResultAdaptor.cs @@ -0,0 +1,60 @@ +using System; + +using UnityEditor.TestTools.TestRunner.Api; + +namespace Microsoft.Unity.VisualStudio.Editor.Testing +{ + [Serializable] + internal class TestResultAdaptorContainer + { + public TestResultAdaptor[] TestResultAdaptors; + } + + [Serializable] + internal class TestResultAdaptor + { + public string Name; + public string FullName; + + public int PassCount; + public int FailCount; + public int InconclusiveCount; + public int SkipCount; + + public string ResultState; + public string StackTrace; + + public TestStatusAdaptor TestStatus; + + public int Parent; + + public TestResultAdaptor(ITestResultAdaptor testResultAdaptor, int parent) + { + Name = testResultAdaptor.Name; + FullName = testResultAdaptor.FullName; + + PassCount = testResultAdaptor.PassCount; + FailCount = testResultAdaptor.FailCount; + InconclusiveCount = testResultAdaptor.InconclusiveCount; + SkipCount = testResultAdaptor.SkipCount; + + switch (testResultAdaptor.TestStatus) + { + case UnityEditor.TestTools.TestRunner.Api.TestStatus.Passed: + TestStatus = TestStatusAdaptor.Passed; + break; + case UnityEditor.TestTools.TestRunner.Api.TestStatus.Skipped: + TestStatus = TestStatusAdaptor.Skipped; + break; + case UnityEditor.TestTools.TestRunner.Api.TestStatus.Inconclusive: + TestStatus = TestStatusAdaptor.Inconclusive; + break; + case UnityEditor.TestTools.TestRunner.Api.TestStatus.Failed: + TestStatus = TestStatusAdaptor.Failed; + break; + } + + Parent = parent; + } + } +} diff --git a/Editor/Testing/TestResultAdaptor.cs.meta b/Editor/Testing/TestResultAdaptor.cs.meta new file mode 100644 index 0000000..3bd7400 --- /dev/null +++ b/Editor/Testing/TestResultAdaptor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f47f2d030bc1d415a8d15a51dbcc39a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Testing/TestRunnerApiListener.cs b/Editor/Testing/TestRunnerApiListener.cs new file mode 100644 index 0000000..a4db5f0 --- /dev/null +++ b/Editor/Testing/TestRunnerApiListener.cs @@ -0,0 +1,52 @@ +using System; +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +namespace Microsoft.Unity.VisualStudio.Editor.Testing +{ + [InitializeOnLoad] + internal class TestRunnerApiListener + { + private static TestRunnerApi _testRunnerApi; + private static TestRunnerCallbacks _testRunnerCallbacks; + + static TestRunnerApiListener() + { + _testRunnerApi = ScriptableObject.CreateInstance(); + _testRunnerCallbacks = new TestRunnerCallbacks(); + + _testRunnerApi.RegisterCallbacks(_testRunnerCallbacks); + } + + public static void RetrieveTestList(string mode) + { + RetrieveTestList((TestMode) Enum.Parse(typeof(TestMode), mode)); + } + + private static void RetrieveTestList(TestMode mode) + { + _testRunnerApi.RetrieveTestList(mode, (ta) => _testRunnerCallbacks.TestListRetrieved(mode, ta)); + } + + public static void ExecuteTests(string command) + { + // ExecuteTests format: + // TestMode:FullName + + var index = command.IndexOf(':'); + if (index < 0) + return; + + var testMode = (TestMode)Enum.Parse(typeof(TestMode), command.Substring(0, index)); + var filter = command.Substring(index + 1); + + ExecuteTests(new Filter() { testMode = testMode, testNames = new string[] { filter } }); + } + + private static void ExecuteTests(Filter filter) + { + _testRunnerApi.Execute(new ExecutionSettings(filter)); + } + } +} diff --git a/Editor/Testing/TestRunnerApiListener.cs.meta b/Editor/Testing/TestRunnerApiListener.cs.meta new file mode 100644 index 0000000..a35bc9d --- /dev/null +++ b/Editor/Testing/TestRunnerApiListener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b59b40c84c6a5348a188c16b17c7b40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Testing/TestRunnerCallbacks.cs b/Editor/Testing/TestRunnerCallbacks.cs new file mode 100644 index 0000000..a02bc9e --- /dev/null +++ b/Editor/Testing/TestRunnerCallbacks.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +namespace Microsoft.Unity.VisualStudio.Editor.Testing +{ + internal class TestRunnerCallbacks : ICallbacks + { + private string Serialize( + TSource source, + Func createAdaptor, + Func> children, + Func container) + { + var adaptors = new List(); + + void AddAdaptor(TSource item, int parentIndex) + { + var index = adaptors.Count; + adaptors.Add(createAdaptor(item, parentIndex)); + foreach (var child in children(item)) + AddAdaptor(child, index); + } + + AddAdaptor(source, -1); + + return JsonUtility.ToJson(container(adaptors.ToArray())); + } + + private string Serialize(ITestAdaptor testAdaptor) + { + return Serialize( + testAdaptor, + (a, parentIndex) => new TestAdaptor(a, parentIndex), + (a) => a.Children, + (r) => new TestAdaptorContainer { TestAdaptors = r }); + } + + private string Serialize(ITestResultAdaptor testResultAdaptor) + { + return Serialize( + testResultAdaptor, + (a, parentIndex) => new TestResultAdaptor(a, parentIndex), + (a) => a.Children, + (r) => new TestResultAdaptorContainer { TestResultAdaptors = r }); + } + + public void RunFinished(ITestResultAdaptor testResultAdaptor) + { + VisualStudioIntegration.BroadcastMessage(Messaging.MessageType.RunFinished, Serialize(testResultAdaptor)); + } + + public void RunStarted(ITestAdaptor testAdaptor) + { + VisualStudioIntegration.BroadcastMessage(Messaging.MessageType.RunStarted, Serialize(testAdaptor)); + } + + public void TestFinished(ITestResultAdaptor testResultAdaptor) + { + VisualStudioIntegration.BroadcastMessage(Messaging.MessageType.TestFinished, Serialize(testResultAdaptor)); + } + + public void TestStarted(ITestAdaptor testAdaptor) + { + VisualStudioIntegration.BroadcastMessage(Messaging.MessageType.TestStarted, Serialize(testAdaptor)); + } + + private static string TestModeName(TestMode testMode) + { + switch (testMode) + { + case TestMode.EditMode: return "EditMode"; + case TestMode.PlayMode: return "PlayMode"; + } + + throw new ArgumentOutOfRangeException(); + } + + + internal void TestListRetrieved(TestMode testMode, ITestAdaptor testAdaptor) + { + // TestListRetrieved format: + // TestMode:Json + + var value = TestModeName(testMode) + ":" + Serialize(testAdaptor); + VisualStudioIntegration.BroadcastMessage(Messaging.MessageType.TestListRetrieved, value); + } + } +} diff --git a/Editor/Testing/TestRunnerCallbacks.cs.meta b/Editor/Testing/TestRunnerCallbacks.cs.meta new file mode 100644 index 0000000..8a37a95 --- /dev/null +++ b/Editor/Testing/TestRunnerCallbacks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fae6007c1ac2cc744b2891fd4d279c96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Testing/TestStatusAdaptor.cs b/Editor/Testing/TestStatusAdaptor.cs new file mode 100644 index 0000000..75f27bb --- /dev/null +++ b/Editor/Testing/TestStatusAdaptor.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.Unity.VisualStudio.Editor.Testing +{ + [Serializable] + internal enum TestStatusAdaptor + { + Passed, + Skipped, + Inconclusive, + Failed, + } +} diff --git a/Editor/Testing/TestStatusAdaptor.cs.meta b/Editor/Testing/TestStatusAdaptor.cs.meta new file mode 100644 index 0000000..f933c7c --- /dev/null +++ b/Editor/Testing/TestStatusAdaptor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0719f1a8b2a284e1182b352e6c8c3c60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UnityInstallation.cs b/Editor/UnityInstallation.cs new file mode 100644 index 0000000..74797cd --- /dev/null +++ b/Editor/UnityInstallation.cs @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +using System; +using UnityEditor.Compilation; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal static class UnityInstallation + { + public static Version LatestLanguageVersionSupported(Assembly assembly) + { +#if UNITY_2020_2_OR_NEWER + if (assembly?.compilerOptions != null && Version.TryParse(assembly.compilerOptions.LanguageVersion, out var result)) + return result; + + // if parsing fails, we know at least we have support for 8.0 + return new Version(8, 0); +#else + return new Version(7, 3); +#endif + } + + } +} diff --git a/Editor/UnityInstallation.cs.meta b/Editor/UnityInstallation.cs.meta new file mode 100644 index 0000000..78b6601 --- /dev/null +++ b/Editor/UnityInstallation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8c76505bcc613640ade706bdb0f1cba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UsageUtility.cs b/Editor/UsageUtility.cs new file mode 100644 index 0000000..9928524 --- /dev/null +++ b/Editor/UsageUtility.cs @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +using System; +using System.IO; +using System.Linq; + +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine.SceneManagement; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + [Serializable] + internal class FileUsage + { + public string Path; + public string[] GameObjectPath; + } + + internal static class UsageUtility + { + internal static void ShowUsage(string json) + { + try + { + var usage = JsonUtility.FromJson(json); + ShowUsage(usage.Path, usage.GameObjectPath); + } + catch (Exception) + { + // ignore malformed request + } + } + + internal static void ShowUsage(string path, string[] gameObjectPath) + { + path = FileUtility.MakeRelativeToProjectPath(path); + if (path == null) + return; + + path = FileUtility.NormalizeWindowsToUnix(path); + var extension = Path.GetExtension(path).ToLower(); + + EditorUtility.FocusProjectWindow(); + + switch (extension) + { + case ".unity": + ShowSceneUsage(path, gameObjectPath); + break; + default: + var asset = AssetDatabase.LoadMainAssetAtPath(path); + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + break; + } + } + + private static void ShowSceneUsage(string scenePath, string[] gameObjectPath) + { + var scene = SceneManager.GetSceneByPath(scenePath.Replace(Path.DirectorySeparatorChar, '/')); + if (!scene.isLoaded) + { + var result = UnityEditor.EditorUtility.DisplayDialogComplex("Show Usage", + $"Do you want to open \"{Path.GetFileName(scenePath)}\"?", + "Open Scene", + "Cancel", + "Open Scene (additive)"); + + switch (result) + { + case 0: + EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); + scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); + break; + case 1: + return; + case 2: + scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); + break; + } + } + + ShowSceneUsage(scene, gameObjectPath); + } + + private static void ShowSceneUsage(Scene scene, string[] gameObjectPath) + { + if (gameObjectPath == null || gameObjectPath.Length == 0) + return; + + var go = scene.GetRootGameObjects().FirstOrDefault(g => g.name == gameObjectPath[0]); + if (go == null) + return; + + for (var ni = 1; ni < gameObjectPath.Length; ni++) + { + var transform = go.transform; + for (var i = 0; i < transform.childCount; i++) + { + var child = transform.GetChild(i); + var childgo = child.gameObject; + if (childgo.name == gameObjectPath[ni]) + { + go = childgo; + break; + } + } + } + + Selection.activeObject = go; + EditorGUIUtility.PingObject(go); + } + } +} diff --git a/Editor/UsageUtility.cs.meta b/Editor/UsageUtility.cs.meta new file mode 100644 index 0000000..8f9c64b --- /dev/null +++ b/Editor/UsageUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a7aba2d3d458e04eb4210c0303fbf64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VersionPair.cs b/Editor/VersionPair.cs new file mode 100644 index 0000000..8a10721 --- /dev/null +++ b/Editor/VersionPair.cs @@ -0,0 +1,16 @@ +using System; + +namespace Microsoft.Unity.VisualStudio.Editor +{ + internal struct VersionPair + { + public Version IdeVersion; + public Version LanguageVersion; + + public VersionPair(int idemajor, int ideminor, int languageMajor, int languageMinor) + { + IdeVersion = new Version(idemajor, ideminor); + LanguageVersion = new Version(languageMajor, languageMinor); + } + } +} diff --git a/Editor/VersionPair.cs.meta b/Editor/VersionPair.cs.meta new file mode 100644 index 0000000..b299d4e --- /dev/null +++ b/Editor/VersionPair.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffe1bdf971d321f4db593c4c6ebd6e47 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/VisualStudioEditor.cs b/Editor/VisualStudioEditor.cs index a102468..c1b0dce 100644 --- a/Editor/VisualStudioEditor.cs +++ b/Editor/VisualStudioEditor.cs @@ -11,6 +11,11 @@ using UnityEditor; using UnityEngine; using Unity.CodeEditor; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Unity.VisualStudio.EditorTests")] +[assembly: InternalsVisibleTo("Unity.VisualStudio.Standalone.EditorTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace Microsoft.Unity.VisualStudio.Editor { diff --git a/Editor/VisualStudioInstallation.cs b/Editor/VisualStudioInstallation.cs index 2b1d5f7..a4ede5a 100644 --- a/Editor/VisualStudioInstallation.cs +++ b/Editor/VisualStudioInstallation.cs @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ using System; using System.IO; +using System.Linq; using Microsoft.Win32; using Unity.CodeEditor; using IOPath = System.IO.Path; @@ -14,7 +15,7 @@ namespace Microsoft.Unity.VisualStudio.Editor { string Path { get; } bool SupportsAnalyzers { get; } - bool SupportsCSharp8 { get; } + Version LatestLanguageVersionSupported { get; } string[] GetAnalyzers(); CodeEditor.Installation ToCodeEditorInstallation(); } @@ -40,17 +41,52 @@ namespace Microsoft.Unity.VisualStudio.Editor } } - public bool SupportsCSharp8 + // C# language version support for Visual Studio + private static VersionPair[] WindowsVersionTable = + { + // VisualStudio 2019 + new VersionPair(16,8, /* => */ 9,0), + new VersionPair(16,0, /* => */ 8,0), + + // VisualStudio 2017 + new VersionPair(15,7, /* => */ 7,3), + new VersionPair(15,5, /* => */ 7,2), + new VersionPair(15,3, /* => */ 7,1), + new VersionPair(15,0, /* => */ 7,0), + }; + + // C# language version support for Visual Studio for Mac + private static VersionPair[] OSXVersionTable = + { + // VisualStudio for Mac 8.x + new VersionPair(8,8, /* => */ 9,0), + new VersionPair(8,3, /* => */ 8,0), + new VersionPair(8,0, /* => */ 7,3), + }; + + public Version LatestLanguageVersionSupported { get { + VersionPair[] versions = null; + if (VisualStudioEditor.IsWindows) - return Version >= new Version(16, 0); + versions = WindowsVersionTable; if (VisualStudioEditor.IsOSX) - return Version >= new Version(8, 2); + versions = OSXVersionTable; - return false; + if (versions != null) + { + foreach(var entry in versions) + { + if (Version >= entry.IdeVersion) + return entry.LanguageVersion; + } + } + + // default to 7.0 given we support at least VS 2017 + return new Version(7,0); } } diff --git a/Editor/VisualStudioIntegration.cs b/Editor/VisualStudioIntegration.cs index a07cdd1..08616ab 100644 --- a/Editor/VisualStudioIntegration.cs +++ b/Editor/VisualStudioIntegration.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; using Microsoft.Unity.VisualStudio.Editor.Messaging; +using Microsoft.Unity.VisualStudio.Editor.Testing; using UnityEditor; using UnityEngine; using MessageType = Microsoft.Unity.VisualStudio.Editor.Messaging.MessageType; @@ -17,10 +19,18 @@ namespace Microsoft.Unity.VisualStudio.Editor [InitializeOnLoad] internal class VisualStudioIntegration { + class Client + { + public IPEndPoint EndPoint { get; set; } + public DateTime LastMessage { get; set; } + } + private static Messager _messager; - private static readonly Queue Incoming = new Queue(); - private static readonly object IncomingLock = new object(); + private static readonly Queue _incoming = new Queue(); + private static readonly Dictionary _clients = new Dictionary(); + private static readonly object _incomingLock = new object(); + private static readonly object _clientsLock = new object(); static VisualStudioIntegration() { @@ -90,25 +100,39 @@ namespace Microsoft.Unity.VisualStudio.Editor private static void OnUpdate() { - lock (IncomingLock) + lock (_incomingLock) { - while (Incoming.Count > 0) + while (_incoming.Count > 0) { - ProcessIncoming(Incoming.Dequeue()); + ProcessIncoming(_incoming.Dequeue()); + } + } + + lock (_clientsLock) + { + foreach (var client in _clients.Values.ToArray()) + { + if (DateTime.Now.Subtract(client.LastMessage) > TimeSpan.FromMilliseconds(4000)) + _clients.Remove(client.EndPoint); } } } private static void AddMessage(Message message) { - lock (IncomingLock) + lock (_incomingLock) { - Incoming.Enqueue(message); + _incoming.Enqueue(message); } } private static void ProcessIncoming(Message message) { + lock (_clientsLock) + { + CheckClient(message); + } + switch (message.Type) { case MessageType.Ping: @@ -142,6 +166,36 @@ namespace Microsoft.Unity.VisualStudio.Editor case MessageType.ProjectPath: Answer(message, MessageType.ProjectPath, Path.GetFullPath(Path.Combine(Application.dataPath, ".."))); break; + case MessageType.ExecuteTests: + TestRunnerApiListener.ExecuteTests(message.Value); + break; + case MessageType.RetrieveTestList: + TestRunnerApiListener.RetrieveTestList(message.Value); + break; + case MessageType.ShowUsage: + UsageUtility.ShowUsage(message.Value); + break; + } + } + + private static void CheckClient(Message message) + { + Client client; + var endPoint = message.Origin; + + if (!_clients.TryGetValue(endPoint, out client)) + { + client = new Client + { + EndPoint = endPoint, + LastMessage = DateTime.Now + }; + + _clients.Add(endPoint, client); + } + else + { + client.LastMessage = DateTime.Now; } } @@ -165,6 +219,11 @@ namespace Microsoft.Unity.VisualStudio.Editor AddMessage(message); } + private static void Answer(Client client, MessageType answerType, string answerValue) + { + Answer(client.EndPoint, answerType, answerValue); + } + private static void Answer(Message message, MessageType answerType, string answerValue = "") { var targetEndPoint = message.Origin; @@ -189,5 +248,16 @@ namespace Microsoft.Unity.VisualStudio.Editor _messager.Dispose(); _messager = null; } + + internal static void BroadcastMessage(MessageType type, string value) + { + lock (_clientsLock) + { + foreach (var client in _clients.Values.ToArray()) + { + Answer(client, type, value); + } + } + } } } diff --git a/Editor/com.unity.ide.visualstudio.asmdef b/Editor/com.unity.ide.visualstudio.asmdef index 6f75d98..2586722 100644 --- a/Editor/com.unity.ide.visualstudio.asmdef +++ b/Editor/com.unity.ide.visualstudio.asmdef @@ -1,9 +1,17 @@ { "name": "Unity.VisualStudio.Editor", "references": [], - "optionalUnityReferences": [], "includePlatforms": [ "Editor" ], - "excludePlatforms": [] -} + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/package.json b/package.json index d7fbb87..df34025 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,21 @@ "name": "com.unity.ide.visualstudio", "displayName": "Visual Studio Editor", "description": "Code editor integration for supporting Visual Studio as code editor for unity. Adds support for generating csproj files for intellisense purposes, auto discovery of installations, etc.", - "version": "2.0.5", + "version": "2.0.7", "unity": "2020.1", "unityRelease": "0a12", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, "relatedPackages": { - "com.unity.ide.visualstudio.tests": "2.0.5" + "com.unity.ide.visualstudio.tests": "2.0.7" }, "upmCi": { - "footprint": "848c02b3f0fe476a599004cd972346a89e39d26f" + "footprint": "b6515ac9d75224fe45e288270d26a9e031c550a8" }, "repository": { "url": "https://github.cds.internal.unity3d.com/unity/com.unity.ide.visualstudio.git", "type": "git", - "revision": "83ca94e82bb6da515dc57e0d860b6b2224f56991" + "revision": "dec282022c7a95fada560c36f53da9dd155a142c" } }