From 9cce26fcbdb6d5d4a5a05353fce017e4c7ab0f13 Mon Sep 17 00:00:00 2001 From: Benjin Dubishar Date: Wed, 25 Jan 2023 14:16:15 -0800 Subject: [PATCH] Initial check-in of SqlProjects service addition to SqlToolsService (#1805) * initial commit * Initial SqlProjects service + tests * Added SqlObject script tests; PR feedback * Added comments for contracts * Swapping SqlProjectResult for ResultStatus * Updating tests * Added automatic test base that provides a working directory and automatic cleanup. --- Packages.props | 1 + ...rver.DacFx.Projects.161.8056.0-alpha.nupkg | Bin 0 -> 48817 bytes .../HostLoader.cs | 4 + .../Microsoft.SqlTools.ServiceLayer.csproj | 1 + .../Contracts/Projects/CloseSqlProject.cs | 15 ++ .../Contracts/Projects/NewSqlProject.cs | 39 ++++ .../Contracts/Projects/OpenSqlProject.cs | 15 ++ .../SqlObjects/AddSqlObjectScript.cs | 15 ++ .../SqlObjects/DeleteSqlObjectScript.cs | 15 ++ .../SqlObjects/ExcludeSqlObjectScript.cs | 15 ++ .../SqlProjects/Contracts/SqlProjectParams.cs | 31 +++ .../SqlProjects/SqlProjectsService.cs | 140 ++++++++++++++ .../SqlProjects/SqlProjectsServiceTests.cs | 182 ++++++++++++++++++ .../Utility/TestBase.cs | 47 +++++ .../Utility/TestContextHelpers.cs | 19 ++ .../RequestContextMocks.cs | 15 +- 16 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs create mode 100644 test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs diff --git a/Packages.props b/Packages.props index 576642f1..ea0ac32b 100644 --- a/Packages.props +++ b/Packages.props @@ -20,6 +20,7 @@ + diff --git a/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg b/bin/nuget/Microsoft.SqlServer.DacFx.Projects.161.8056.0-alpha.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..467c2e00c134b0409c3e2213012c6f1d7b44be8e GIT binary patch literal 48817 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 + diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs new file mode 100644 index 00000000..2b08003d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/CloseSqlProject.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + public class CloseSqlProjectRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/closeProject"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs new file mode 100644 index 00000000..069e3c7f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/NewSqlProject.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Dac.Projects; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + /// + /// Parameters for creating a new SQL Project + /// + public class NewSqlProjectParams : SqlProjectParams + { + /// + /// Type of SQL Project: SDK-style or Legacy + /// + public ProjectType SqlProjectType { get; set; } + + /// + /// Database schema provider for the project, in the format + /// "Microsoft.Data.Tools.Schema.Sql.SqlXYZDatabaseSchemaProvider". + /// Case sensitive. + /// + public string? DatabaseSchemaProvider { get; set; } + + /// + /// Version of the Microsoft.Build.Sql SDK for the project, if overriding the default + /// + public string? BuildSdkVersion { get; set; } + } + + public class NewSqlProjectRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/newProject"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs new file mode 100644 index 00000000..33c77aea --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/Projects/OpenSqlProject.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + public class OpenSqlProjectRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/openProject"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs new file mode 100644 index 00000000..5f6ea9df --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/AddSqlObjectScript.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + public class AddSqlObjectScriptRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/addSqlObjectScript"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs new file mode 100644 index 00000000..409e0422 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/DeleteSqlObjectScript.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +using Microsoft.SqlTools.ServiceLayer.Utility; +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + public class DeleteSqlObjectScriptRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/deleteSqlObjectScript"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs new file mode 100644 index 00000000..d75aa75d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlObjects/ExcludeSqlObjectScript.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + public class ExcludeSqlObjectScriptRequest + { + public static readonly RequestType Type = RequestType.Create("sqlprojects/excludeSqlObjectScript"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs new file mode 100644 index 00000000..22a60e2d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/Contracts/SqlProjectParams.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts +{ + /// + /// Parameters for a generic SQL Project operation + /// + public class SqlProjectParams : GeneralRequestDetails + { + /// + /// Absolute path of the project, including .sqlproj + /// + public string ProjectUri { get; set; } + } + + /// + /// Parameters for a SQL Project operation that targets a script + /// + public class SqlProjectScriptParams : SqlProjectParams + { + /// + /// Path of the script, including .sql, relative to the .sqlproj + /// + public string Path { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs new file mode 100644 index 00000000..4e6b661c --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlProjects/SqlProjectsService.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.SqlServer.Dac.Projects; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlProjects +{ + /// + /// Main class for SqlProjects service + /// + class SqlProjectsService + { + private static readonly Lazy instance = new Lazy(() => new SqlProjectsService()); + + /// + /// Gets the singleton instance object + /// + public static SqlProjectsService Instance => instance.Value; + + private Lazy> projects = new Lazy>(() => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)); + + /// + /// that maps Project URI to Project + /// + public ConcurrentDictionary Projects => projects.Value; + + /// + /// Initializes the service instance + /// + /// + public void InitializeService(ServiceHost serviceHost) + { + // Project-level functions + serviceHost.SetRequestHandler(OpenSqlProjectRequest.Type, HandleOpenSqlProjectRequest, isParallelProcessingSupported: true); + serviceHost.SetRequestHandler(CloseSqlProjectRequest.Type, HandleCloseSqlProjectRequest, isParallelProcessingSupported: true); + serviceHost.SetRequestHandler(NewSqlProjectRequest.Type, HandleNewSqlProjectRequest, isParallelProcessingSupported: true); + + // SQL object script calls + serviceHost.SetRequestHandler(AddSqlObjectScriptRequest.Type, HandleAddSqlObjectScriptRequest, isParallelProcessingSupported: false); + serviceHost.SetRequestHandler(DeleteSqlObjectScriptRequest.Type, HandleDeleteSqlObjectScriptRequest, isParallelProcessingSupported: false); + serviceHost.SetRequestHandler(ExcludeSqlObjectScriptRequest.Type, HandleExcludeSqlObjectScriptRequest, isParallelProcessingSupported: false); + } + + #region Handlers + + #region Project-level functions + + internal async Task HandleOpenSqlProjectRequest(SqlProjectParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri), requestContext); + } + + internal async Task HandleCloseSqlProjectRequest(SqlProjectParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => Projects.TryRemove(requestParams.ProjectUri, out _), requestContext); + } + + internal async Task HandleNewSqlProjectRequest(NewSqlProjectParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(async () => + { + await SqlProject.CreateProjectAsync(requestParams.ProjectUri, requestParams.SqlProjectType, requestParams.DatabaseSchemaProvider); + GetProject(requestParams.ProjectUri); // load into the cache + + }, requestContext); + } + + #endregion + + #region Sql object script calls + + internal async Task HandleAddSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Add(new SqlObjectScript(requestParams.Path)), requestContext); + } + + internal async Task HandleDeleteSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Delete(requestParams.Path), requestContext); + } + + internal async Task HandleExcludeSqlObjectScriptRequest(SqlProjectScriptParams requestParams, RequestContext requestContext) + { + await RunWithErrorHandling(() => GetProject(requestParams.ProjectUri).SqlObjectScripts.Exclude(requestParams.Path), requestContext); + } + + #endregion + + #endregion + + #region Helper methods + + private async Task RunWithErrorHandling(Action action, RequestContext requestContext) + { + await RunWithErrorHandling(async () => await Task.Run(action), requestContext); + } + + private async Task RunWithErrorHandling(Func action, RequestContext requestContext) + { + try + { + await action(); + + await requestContext.SendResult(new ResultStatus() + { + Success = true, + ErrorMessage = null + }); + } + catch (Exception ex) + { + await requestContext.SendResult(new ResultStatus() + { + Success = false, + ErrorMessage = ex.Message + }); + } + } + + private SqlProject GetProject(string projectUri) + { + if (!Projects.ContainsKey(projectUri)) + { + Projects[projectUri] = new SqlProject(projectUri); + } + + return Projects[projectUri]; + } + + #endregion + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs new file mode 100644 index 00000000..00cad9a2 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlProjects/SqlProjectsServiceTests.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.IO; +using System.Threading.Tasks; +using Microsoft.SqlServer.Dac.Projects; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.SqlProjects; +using Microsoft.SqlTools.ServiceLayer.SqlProjects.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking; +using Microsoft.SqlTools.ServiceLayer.Utility; +using NUnit.Framework; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlProjects +{ + public class SqlProjectsServiceTests : TestBase + { + [Test] + public async Task TestErrorDuringExecution() + { + SqlProjectsService service = new(); + string projectUri = await service.CreateSqlProject(); // validates result.Success == true + + // Validate that result indicates failure when there's an exception + MockRequest requestMock = new(); + await service.HandleNewSqlProjectRequest(new NewSqlProjectParams() + { + ProjectUri = projectUri, + SqlProjectType = ProjectType.SdkStyle + + }, requestMock.Object); + + Assert.IsFalse(requestMock.Result.Success); + Assert.IsTrue(requestMock.Result.ErrorMessage!.Contains("Cannot create a new SQL project")); + } + + [Test] + public async Task TestOpenCloseProject() + { + // Setup + string sdkProjectUri = TestContext.CurrentContext.GetTestProjectPath(nameof(TestOpenCloseProject) + "Sdk"); + string legacyProjectUri = TestContext.CurrentContext.GetTestProjectPath(nameof(TestOpenCloseProject) + "Legacy"); + + if (File.Exists(sdkProjectUri)) File.Delete(sdkProjectUri); + if (File.Exists(legacyProjectUri)) File.Delete(legacyProjectUri); + + SqlProjectsService service = new(); + + Assert.AreEqual(0, service.Projects.Count); + + // Validate creating SDK-style project + MockRequest requestMock = new(); + await service.HandleNewSqlProjectRequest(new NewSqlProjectParams() + { + ProjectUri = sdkProjectUri, + SqlProjectType = ProjectType.SdkStyle + + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(1, service.Projects.Count); + Assert.IsTrue(service.Projects.ContainsKey(sdkProjectUri)); + Assert.AreEqual(service.Projects[sdkProjectUri].SqlProjStyle, ProjectType.SdkStyle); + + // Validate creating Legacy-style project + requestMock = new(); + await service.HandleNewSqlProjectRequest(new NewSqlProjectParams() + { + ProjectUri = legacyProjectUri, + SqlProjectType = ProjectType.LegacyStyle + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(2, service.Projects.Count); + Assert.IsTrue(service.Projects.ContainsKey(legacyProjectUri)); + Assert.AreEqual(service.Projects[legacyProjectUri].SqlProjStyle, ProjectType.LegacyStyle); + + // Validate closing a project + requestMock = new(); + await service.HandleCloseSqlProjectRequest(new SqlProjectParams() { ProjectUri = sdkProjectUri }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(1, service.Projects.Count); + Assert.IsTrue(!service.Projects.ContainsKey(sdkProjectUri)); + + // Validate opening a project + requestMock = new(); + await service.HandleOpenSqlProjectRequest(new SqlProjectParams() { ProjectUri = sdkProjectUri }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(2, service.Projects.Count); + Assert.IsTrue(service.Projects.ContainsKey(sdkProjectUri)); + } + + [Test] + public async Task TestSqlObjectScriptAddDeleteExclude() + { + // Setup + SqlProjectsService service = new(); + string projectUri = await service.CreateSqlProject(); + Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count); + + // Validate adding a SQL object script + MockRequest requestMock = new(); + string scriptRelativePath = "MyTable.sql"; + string scriptFullPath = Path.Join(Path.GetDirectoryName(projectUri), scriptRelativePath); + await File.WriteAllTextAsync(scriptFullPath, "CREATE TABLE [MyTable] ([Id] INT)"); + + await service.HandleAddSqlObjectScriptRequest(new SqlProjectScriptParams() + { + ProjectUri = projectUri, + Path = scriptRelativePath + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(1, service.Projects[projectUri].SqlObjectScripts.Count); + Assert.IsTrue(service.Projects[projectUri].SqlObjectScripts.Contains(scriptRelativePath)); + + // Validate excluding a SQL object script + requestMock = new(); + await service.HandleExcludeSqlObjectScriptRequest(new SqlProjectScriptParams() + { + ProjectUri = projectUri, + Path = scriptRelativePath + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count); + Assert.IsTrue(File.Exists(scriptFullPath)); + + // Re-add to set up for Delete + requestMock = new(); + await service.HandleAddSqlObjectScriptRequest(new SqlProjectScriptParams() + { + ProjectUri = projectUri, + Path = scriptRelativePath + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(1, service.Projects[projectUri].SqlObjectScripts.Count); + + // Validate deleting a SQL object script + requestMock = new(); + await service.HandleDeleteSqlObjectScriptRequest(new SqlProjectScriptParams() + { + ProjectUri = projectUri, + Path = scriptRelativePath + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + Assert.AreEqual(0, service.Projects[projectUri].SqlObjectScripts.Count); + Assert.IsFalse(File.Exists(scriptFullPath)); + } + } + + internal static class SqlProjectsExtensions + { + /// + /// Uses the service to create a new SQL project + /// + /// + /// + public async static Task CreateSqlProject(this SqlProjectsService service) + { + string projectUri = TestContext.CurrentContext.GetTestProjectPath(); + + MockRequest requestMock = new(); + await service.HandleNewSqlProjectRequest(new NewSqlProjectParams() + { + ProjectUri = projectUri, + SqlProjectType = ProjectType.SdkStyle + + }, requestMock.Object); + + Assert.IsTrue(requestMock.Result.Success); + + return projectUri; + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs new file mode 100644 index 00000000..a4b51a5a --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestBase.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.IO; +using NUnit.Framework; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility +{ + [TestFixture] + public abstract class TestBase + { + static TestBase() + { + RunTimestamp = DateTime.Now.ToString("yyyyMMdd-HHmmssffff"); + } + + public static string RunTimestamp + { + get; + private set; + } + + public static string TestRunFolder => Path.Join(TestContext.CurrentContext.WorkDirectory, "SqlToolsServiceTestRuns", $"Run{RunTimestamp}"); + + + [OneTimeSetUp] + public void SetUp() + { + if (!Directory.Exists(TestRunFolder)) + { + Directory.CreateDirectory(TestRunFolder); + } + } + + [OneTimeTearDown] + public void TearDown() + { + if (Directory.Exists(TestRunFolder)) + { + Directory.Delete(TestRunFolder, recursive: true); + } + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs new file mode 100644 index 00000000..85e8845c --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Utility/TestContextHelpers.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.IO; +using NUnit.Framework; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility +{ + public static class TestContextHelpers + { + private static string TestName => TestContext.CurrentContext.Test.Name; + + public static string GetTestWorkingFolder(this TestContext context) => Path.Join(TestBase.TestRunFolder, TestName); + + public static string GetTestProjectPath(this TestContext context, string? projectName = null) => Path.Join(context.GetTestWorkingFolder(), $"{projectName ?? TestName}.sqlproj"); + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs index 68f2bfad..ea102224 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/RequestContextMocking/RequestContextMocks.cs @@ -13,7 +13,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking { public static class RequestContextMocks { - public static Mock> Create(Action resultCallback) { var requestContext = new Mock>(); @@ -61,4 +60,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking return mock; } } + + public class MockRequest + { + private T? result; + public T Result => result ?? throw new InvalidOperationException("No result has been sent for the request"); + + public Mock> Mock; + public RequestContext Object => Mock.Object; + + public MockRequest() + { + Mock = RequestContextMocks.Create(actual => result = actual); + } + } }