diff --git a/src/FunctionLoader.cs b/src/FunctionLoader.cs index 3d666cce..cb77b2fe 100644 --- a/src/FunctionLoader.cs +++ b/src/FunctionLoader.cs @@ -21,7 +21,7 @@ internal class FunctionLoader internal static string FunctionAppRootPath { get; private set; } internal static string FunctionAppProfilePath { get; private set; } - internal static string FunctionAppModulesPath { get; private set; } + internal static string FunctionModulePath { get; private set; } /// /// Query for function metadata can happen in parallel. @@ -51,9 +51,14 @@ internal void LoadFunction(FunctionLoadRequest request) /// internal static void SetupWellKnownPaths(FunctionLoadRequest request) { + // Resolve the FunctionApp root path FunctionAppRootPath = Path.GetFullPath(Path.Join(request.Metadata.Directory, "..")); - FunctionAppModulesPath = Path.Join(FunctionAppRootPath, "Modules"); + // Resolve module paths + var appLevelModulesPath = Path.Join(FunctionAppRootPath, "Modules"); + var workerLevelModulesPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"); + FunctionModulePath = $"{appLevelModulesPath}{Path.PathSeparator}{workerLevelModulesPath}"; + // Resolve the FunctionApp profile path var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var profiles = Directory.EnumerateFiles(FunctionAppRootPath, "profile.ps1", options); FunctionAppProfilePath = profiles.FirstOrDefault(); diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index a61ebda3..1a929a78 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -23,9 +23,33 @@ internal class PowerShellManager private readonly ILogger _logger; private readonly PowerShell _pwsh; + /// + /// Gets the Runspace InstanceId. + /// + internal Guid InstanceId => _pwsh.Runspace.InstanceId; + + static PowerShellManager() + { + // Set the type accelerators for 'HttpResponseContext' and 'HttpResponseContext'. + // We probably will expose more public types from the worker in future for the interop between worker and the 'PowerShellWorker' module. + // But it's most likely only 'HttpResponseContext' and 'HttpResponseContext' are supposed to be used directly by users, so we only add + // type accelerators for these two explicitly. + var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators"); + var addMethod = accelerator.GetMethod("Add", new Type[] { typeof(string), typeof(Type) }); + addMethod.Invoke(null, new object[] { "HttpResponseContext", typeof(HttpResponseContext) }); + addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) }); + } + internal PowerShellManager(ILogger logger) { + if (FunctionLoader.FunctionAppRootPath == null) + { + throw new InvalidOperationException($"The FunctionApp root hasn't been resolved yet!"); + } + var initialSessionState = InitialSessionState.CreateDefault(); + initialSessionState.EnvironmentVariables.Add( + new SessionStateVariableEntry("PSModulePath", FunctionLoader.FunctionModulePath, null)); // Setting the execution policy on macOS and Linux throws an exception so only update it on Windows if(Platform.IsWindows) @@ -34,8 +58,9 @@ internal PowerShellManager(ILogger logger) // Windows client versions. This is needed if a user is testing their function locally with the func CLI initialSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; } - _pwsh = PowerShell.Create(initialSessionState); + _logger = logger; + _pwsh = PowerShell.Create(initialSessionState); // Setup Stream event listeners var streamHandler = new StreamHandler(logger); @@ -45,34 +70,17 @@ internal PowerShellManager(ILogger logger) _pwsh.Streams.Progress.DataAdding += streamHandler.ProgressDataAdding; _pwsh.Streams.Verbose.DataAdding += streamHandler.VerboseDataAdding; _pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding; - } - /// - /// This method performs the one-time initialization at the worker process level. - /// - internal void PerformWorkerLevelInitialization() - { - // Set the type accelerators for 'HttpResponseContext' and 'HttpResponseContext'. - // We probably will expose more public types from the worker in future for the interop between worker and the 'PowerShellWorker' module. - // But it's most likely only 'HttpResponseContext' and 'HttpResponseContext' are supposed to be used directly by users, so we only add - // type accelerators for these two explicitly. - var accelerator = typeof(PSObject).Assembly.GetType("System.Management.Automation.TypeAccelerators"); - var addMethod = accelerator.GetMethod("Add", new Type[] { typeof(string), typeof(Type) }); - addMethod.Invoke(null, new object[] { "HttpResponseContext", typeof(HttpResponseContext) }); - addMethod.Invoke(null, new object[] { "HttpRequestContext", typeof(HttpRequestContext) }); - - // Set the PSModulePath - var workerModulesPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"); - Environment.SetEnvironmentVariable("PSModulePath", $"{FunctionLoader.FunctionAppModulesPath}{Path.PathSeparator}{workerModulesPath}"); + // Initialize the Runspace + InvokeProfile(FunctionLoader.FunctionAppProfilePath); } /// - /// This method performs initialization that has to be done for each Runspace, e.g. running the Function App's profile.ps1. + /// This method invokes the FunctionApp's profile.ps1. /// - internal void PerformRunspaceLevelInitialization() + internal void InvokeProfile(string profilePath) { Exception exception = null; - string profilePath = FunctionLoader.FunctionAppProfilePath; if (profilePath == null) { _logger.Log(LogLevel.Trace, $"No 'profile.ps1' is found at the FunctionApp root folder: {FunctionLoader.FunctionAppRootPath}"); @@ -195,25 +203,6 @@ internal string ConvertToJson(object fromObj) .InvokeAndClearCommands()[0]; } - /// - /// Helper method to set the output binding metadata for the function that is about to run. - /// - internal void RegisterFunctionMetadata(AzFunctionInfo functionInfo) - { - var outputBindings = functionInfo.OutputBindings; - FunctionMetadata.OutputBindingCache.AddOrUpdate(_pwsh.Runspace.InstanceId, - outputBindings, - (key, value) => outputBindings); - } - - /// - /// Helper method to clear the output binding metadata for the function that has done running. - /// - internal void UnregisterFunctionMetadata() - { - FunctionMetadata.OutputBindingCache.TryRemove(_pwsh.Runspace.InstanceId, out _); - } - private void ResetRunspace(string moduleName) { // Reset the runspace to the Initial Session State diff --git a/src/PowerShell/PowerShellManagerPool.cs b/src/PowerShell/PowerShellManagerPool.cs new file mode 100644 index 00000000..06b0c583 --- /dev/null +++ b/src/PowerShell/PowerShellManagerPool.cs @@ -0,0 +1,63 @@ +// +// 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 Microsoft.Azure.Functions.PowerShellWorker.Utility; + +namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell +{ + using System.Management.Automation; + + /// + /// The PowerShellManager pool for the in-proc concurrency support. + /// + internal class PowerShellManagerPool + { + private readonly ILogger _logger; + // Today we don't really support the in-proc concurrency. We just hold an instance of PowerShellManager in this field. + private PowerShellManager _psManager; + + /// + /// Constructor of the pool. + /// + internal PowerShellManagerPool(ILogger logger) + { + _logger = logger; + } + + /// + /// Initialize the pool and populate it with PowerShellManager instances. + /// When it's time to really implement this pool, we probably should instantiate PowerShellManager instances in a lazy way. + /// Maybe start from size 1 and increase the number of workers as needed. + /// + internal void Initialize() + { + _psManager = new PowerShellManager(_logger); + } + + /// + /// Checkout an idle PowerShellManager instance. + /// When it's time to really implement this pool, this method is supposed to block when there is no idle instance available. + /// + internal PowerShellManager CheckoutIdleWorker(AzFunctionInfo functionInfo) + { + // Register the function with the Runspace before returning the idle PowerShellManager. + FunctionMetadata.RegisterFunctionMetadata(_psManager.InstanceId, functionInfo); + return _psManager; + } + + /// + /// Return a used PowerShellManager instance to the pool. + /// + internal void ReclaimUsedWorker(PowerShellManager psManager) + { + if (psManager != null) + { + // Unregister the Runspace before reclaiming the used PowerShellManager. + FunctionMetadata.UnregisterFunctionMetadata(psManager.InstanceId); + } + } + } +} diff --git a/src/Public/FunctionMetadata.cs b/src/Public/FunctionMetadata.cs index 442f5f61..0fcf942a 100644 --- a/src/Public/FunctionMetadata.cs +++ b/src/Public/FunctionMetadata.cs @@ -26,5 +26,22 @@ public static ReadOnlyDictionary GetOutputBindingIn OutputBindingCache.TryGetValue(runspaceInstanceId, out outputBindings); return outputBindings; } + + /// + /// Helper method to set the output binding metadata for the function that is about to run. + /// + internal static void RegisterFunctionMetadata(Guid instanceId, AzFunctionInfo functionInfo) + { + var outputBindings = functionInfo.OutputBindings; + OutputBindingCache.AddOrUpdate(instanceId, outputBindings, (key, value) => outputBindings); + } + + /// + /// Helper method to clear the output binding metadata for the function that has done running. + /// + internal static void UnregisterFunctionMetadata(Guid instanceId) + { + OutputBindingCache.TryRemove(instanceId, out _); + } } } diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs index 4d38309c..6f005d2e 100644 --- a/src/RequestProcessor.cs +++ b/src/RequestProcessor.cs @@ -20,7 +20,7 @@ internal class RequestProcessor private readonly FunctionLoader _functionLoader; private readonly RpcLogger _logger; private readonly MessagingStream _msgStream; - private readonly PowerShellManager _powerShellManager; + private readonly PowerShellManagerPool _powershellPool; // Indicate whether the FunctionApp has been initialized. private bool _isFunctionAppInitialized; @@ -29,7 +29,7 @@ internal RequestProcessor(MessagingStream msgStream) { _msgStream = msgStream; _logger = new RpcLogger(msgStream); - _powerShellManager = new PowerShellManager(_logger); + _powershellPool = new PowerShellManagerPool(_logger); _functionLoader = new FunctionLoader(); } @@ -98,9 +98,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) if (!_isFunctionAppInitialized) { FunctionLoader.SetupWellKnownPaths(functionLoadRequest); - _powerShellManager.PerformWorkerLevelInitialization(); - _powerShellManager.PerformRunspaceLevelInitialization(); - + _powershellPool.Initialize(); _isFunctionAppInitialized = true; } @@ -122,6 +120,7 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request) /// internal StreamingMessage ProcessInvocationRequest(StreamingMessage request) { + PowerShellManager psManager = null; InvocationRequest invocationRequest = request.InvocationRequest; StreamingMessage response = NewStreamingMessageTemplate( @@ -130,18 +129,18 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request) out StatusResult status); response.InvocationResponse.InvocationId = invocationRequest.InvocationId; - // Invoke powershell logic and return hashtable of out binding data try { // Load information about the function var functionInfo = _functionLoader.GetFunctionInfo(invocationRequest.FunctionId); - _powerShellManager.RegisterFunctionMetadata(functionInfo); + psManager = _powershellPool.CheckoutIdleWorker(functionInfo); + // Invoke the function and return a hashtable of out binding data Hashtable results = functionInfo.Type == AzFunctionType.OrchestrationFunction - ? InvokeOrchestrationFunction(functionInfo, invocationRequest) - : InvokeSingleActivityFunction(functionInfo, invocationRequest); + ? InvokeOrchestrationFunction(psManager, functionInfo, invocationRequest) + : InvokeSingleActivityFunction(psManager, functionInfo, invocationRequest); - BindOutputFromResult(response.InvocationResponse, functionInfo, results); + BindOutputFromResult(psManager, response.InvocationResponse, functionInfo, results); } catch (Exception e) { @@ -150,7 +149,7 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request) } finally { - _powerShellManager.UnregisterFunctionMetadata(); + _powershellPool.ReclaimUsedWorker(psManager); } return response; @@ -188,7 +187,7 @@ private StreamingMessage NewStreamingMessageTemplate(string requestId, Streaming /// /// Invoke an orchestration function. /// - private Hashtable InvokeOrchestrationFunction(AzFunctionInfo functionInfo, InvocationRequest invocationRequest) + private Hashtable InvokeOrchestrationFunction(PowerShellManager psManager, AzFunctionInfo functionInfo, InvocationRequest invocationRequest) { throw new NotImplementedException("Durable function is not yet supported for PowerShell"); } @@ -196,7 +195,7 @@ private Hashtable InvokeOrchestrationFunction(AzFunctionInfo functionInfo, Invoc /// /// Invoke a regular function or an activity function. /// - private Hashtable InvokeSingleActivityFunction(AzFunctionInfo functionInfo, InvocationRequest invocationRequest) + private Hashtable InvokeSingleActivityFunction(PowerShellManager psManager, AzFunctionInfo functionInfo, InvocationRequest invocationRequest) { // Bundle all TriggerMetadata into Hashtable to send down to PowerShell var triggerMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase); @@ -210,16 +209,13 @@ private Hashtable InvokeSingleActivityFunction(AzFunctionInfo functionInfo, Invo } } - return _powerShellManager.InvokeFunction( - functionInfo, - triggerMetadata, - invocationRequest.InputData); + return psManager.InvokeFunction(functionInfo, triggerMetadata, invocationRequest.InputData); } /// /// Set the 'ReturnValue' and 'OutputData' based on the invocation results appropriately. /// - private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo functionInfo, Hashtable results) + private void BindOutputFromResult(PowerShellManager psManager, InvocationResponse response, AzFunctionInfo functionInfo, Hashtable results) { switch (functionInfo.Type) { @@ -231,14 +227,14 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu string outBindingName = binding.Key; if(string.Equals(outBindingName, AzFunctionInfo.DollarReturn, StringComparison.OrdinalIgnoreCase)) { - response.ReturnValue = results[outBindingName].ToTypedData(_powerShellManager); + response.ReturnValue = results[outBindingName].ToTypedData(psManager); continue; } ParameterBinding paramBinding = new ParameterBinding() { Name = outBindingName, - Data = results[outBindingName].ToTypedData(_powerShellManager) + Data = results[outBindingName].ToTypedData(psManager) }; response.OutputData.Add(paramBinding); @@ -247,7 +243,7 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu case AzFunctionType.OrchestrationFunction: case AzFunctionType.ActivityFunction: - response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(_powerShellManager); + response.ReturnValue = results[AzFunctionInfo.DollarReturn].ToTypedData(psManager); break; default: diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs index 89995a42..f74bcf2e 100644 --- a/test/Unit/PowerShell/PowerShellManagerTests.cs +++ b/test/Unit/PowerShell/PowerShellManagerTests.cs @@ -14,44 +14,77 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test { - public class PowerShellManagerTests + internal class TestUtils { - private const string TestInputBindingName = "req"; - private const string TestOutputBindingName = "res"; - private const string TestStringData = "Foo"; + internal const string TestInputBindingName = "req"; + internal const string TestOutputBindingName = "res"; - private readonly string _functionDirectory; - private readonly ConsoleLogger _testLogger; - private readonly PowerShellManager _testManager; - private readonly List _testInputData; - private readonly RpcFunctionMetadata _rpcFunctionMetadata; - private readonly FunctionLoadRequest _functionLoadRequest; + internal static readonly string FunctionDirectory; + internal static readonly RpcFunctionMetadata RpcFunctionMetadata; + internal static readonly FunctionLoadRequest FunctionLoadRequest; - public PowerShellManagerTests() + static TestUtils() { - _functionDirectory = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "PowerShell"); - _rpcFunctionMetadata = new RpcFunctionMetadata() + FunctionDirectory = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "PowerShell"); + RpcFunctionMetadata = new RpcFunctionMetadata() { Name = "TestFuncApp", - Directory = _functionDirectory, + Directory = FunctionDirectory, Bindings = { { TestInputBindingName , new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "httpTrigger" } }, { TestOutputBindingName, new BindingInfo { Direction = BindingInfo.Types.Direction.Out, Type = "http" } } } }; - _functionLoadRequest = new FunctionLoadRequest {FunctionId = "FunctionId", Metadata = _rpcFunctionMetadata}; - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); + FunctionLoadRequest = new FunctionLoadRequest {FunctionId = "FunctionId", Metadata = RpcFunctionMetadata}; + FunctionLoader.SetupWellKnownPaths(FunctionLoadRequest); + } + + // Have a single place to get a PowerShellManager for testing. + // This is to guarantee that the well known paths are setup before calling the constructor of PowerShellManager. + internal static PowerShellManager NewTestPowerShellManager(ConsoleLogger logger) + { + return new PowerShellManager(logger); + } + + internal static AzFunctionInfo NewAzFunctionInfo(string scriptFile, string entryPoint) + { + RpcFunctionMetadata.ScriptFile = scriptFile; + RpcFunctionMetadata.EntryPoint = entryPoint; + RpcFunctionMetadata.Directory = Path.GetDirectoryName(scriptFile); + return new AzFunctionInfo(RpcFunctionMetadata); + } + + // Helper method to wait for debugger to attach and set a breakpoint. + internal static void Break() + { + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(200); + } + System.Diagnostics.Debugger.Break(); + } + } + + public class PowerShellManagerTests + { + private const string TestStringData = "Foo"; + + private readonly ConsoleLogger _testLogger; + private readonly PowerShellManager _testManager; + private readonly List _testInputData; + + public PowerShellManagerTests() + { _testLogger = new ConsoleLogger(); - _testManager = new PowerShellManager(_testLogger); - _testManager.PerformWorkerLevelInitialization(); + _testManager = TestUtils.NewTestPowerShellManager(_testLogger); _testInputData = new List { new ParameterBinding { - Name = TestInputBindingName, + Name = TestUtils.TestInputBindingName, Data = new TypedData { String = TestStringData @@ -60,65 +93,58 @@ public PowerShellManagerTests() }; } - private AzFunctionInfo GetAzFunctionInfo(string scriptFile, string entryPoint) - { - _rpcFunctionMetadata.ScriptFile = scriptFile; - _rpcFunctionMetadata.EntryPoint = entryPoint; - return new AzFunctionInfo(_rpcFunctionMetadata); - } - [Fact] public void InvokeBasicFunctionWorks() { - string path = Path.Join(_functionDirectory, "testBasicFunction.ps1"); + string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunction.ps1"); - var functionInfo = GetAzFunctionInfo(path, string.Empty); + var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); Hashtable result = _testManager.InvokeFunction(functionInfo, null, _testInputData); - Assert.Equal(TestStringData, result[TestOutputBindingName]); + Assert.Equal(TestStringData, result[TestUtils.TestOutputBindingName]); } [Fact] public void InvokeBasicFunctionWithTriggerMetadataWorks() { - string path = Path.Join(_functionDirectory, "testBasicFunctionWithTriggerMetadata.ps1"); + string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunctionWithTriggerMetadata.ps1"); Hashtable triggerMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase) { - { TestInputBindingName, TestStringData } + { TestUtils.TestInputBindingName, TestStringData } }; - var functionInfo = GetAzFunctionInfo(path, string.Empty); + var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); Hashtable result = _testManager.InvokeFunction(functionInfo, triggerMetadata, _testInputData); - Assert.Equal(TestStringData, result[TestOutputBindingName]); + Assert.Equal(TestStringData, result[TestUtils.TestOutputBindingName]); } [Fact] public void InvokeFunctionWithEntryPointWorks() { - string path = Path.Join(_functionDirectory, "testFunctionWithEntryPoint.psm1"); - var functionInfo = GetAzFunctionInfo(path, "Run"); + string path = Path.Join(TestUtils.FunctionDirectory, "testFunctionWithEntryPoint.psm1"); + var functionInfo = TestUtils.NewAzFunctionInfo(path, "Run"); Hashtable result = _testManager.InvokeFunction(functionInfo, null, _testInputData); - Assert.Equal(TestStringData, result[TestOutputBindingName]); + Assert.Equal(TestStringData, result[TestUtils.TestOutputBindingName]); } [Fact] public void FunctionShouldCleanupVariableTable() { - string path = Path.Join(_functionDirectory, "testFunctionCleanup.ps1"); - var functionInfo = GetAzFunctionInfo(path, string.Empty); + string path = Path.Join(TestUtils.FunctionDirectory, "testFunctionCleanup.ps1"); + var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); Hashtable result1 = _testManager.InvokeFunction(functionInfo, null, _testInputData); - Assert.Equal("is not set", result1[TestOutputBindingName]); + Assert.Equal("is not set", result1[TestUtils.TestOutputBindingName]); - // the value shoould not change if the variable table is properly cleaned up. + // the value should not change if the variable table is properly cleaned up. Hashtable result2 = _testManager.InvokeFunction(functionInfo, null, _testInputData); - Assert.Equal("is not set", result2[TestOutputBindingName]); + Assert.Equal("is not set", result2[TestUtils.TestOutputBindingName]); } [Fact] - public void ModulePathShouldBeSetByWorkerLevelInitialization() + public void ModulePathShouldBeSetCorrectly() { string workerModulePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"); string funcAppModulePath = Path.Join(FunctionLoader.FunctionAppRootPath, "Modules"); @@ -129,13 +155,13 @@ public void ModulePathShouldBeSetByWorkerLevelInitialization() [Fact] public void RegisterAndUnregisterFunctionMetadataShouldWork() { - string path = Path.Join(_functionDirectory, "testBasicFunction.ps1"); - var functionInfo = GetAzFunctionInfo(path, string.Empty); + string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunction.ps1"); + var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); Assert.Empty(FunctionMetadata.OutputBindingCache); - _testManager.RegisterFunctionMetadata(functionInfo); + FunctionMetadata.RegisterFunctionMetadata(_testManager.InstanceId, functionInfo); Assert.Single(FunctionMetadata.OutputBindingCache); - _testManager.UnregisterFunctionMetadata(); + FunctionMetadata.UnregisterFunctionMetadata(_testManager.InstanceId); Assert.Empty(FunctionMetadata.OutputBindingCache); } @@ -144,21 +170,11 @@ public void ProfileShouldWork() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileBasic", "Func1"); + var profilePath = Path.Join(TestUtils.FunctionDirectory, "ProfileBasic", "profile.ps1"); + _testManager.InvokeProfile(profilePath); - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); - - Assert.Single(_testLogger.FullLog); - Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); - } + Assert.Single(_testLogger.FullLog); + Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]); } [Fact] @@ -166,21 +182,10 @@ public void ProfileDoesNotExist() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = AppDomain.CurrentDomain.BaseDirectory; - - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); + _testManager.InvokeProfile(null); - Assert.Single(_testLogger.FullLog); - Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); - } + Assert.Single(_testLogger.FullLog); + Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]); } [Fact] @@ -188,21 +193,11 @@ public void ProfileWithTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithTerminatingError", "Func1"); + var profilePath = Path.Join(TestUtils.FunctionDirectory, "ProfileWithTerminatingError", "profile.ps1"); - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - - Assert.Throws(() => _testManager.PerformRunspaceLevelInitialization()); - Assert.Single(_testLogger.FullLog); - Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", _testLogger.FullLog[0]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); - } + Assert.Throws(() => _testManager.InvokeProfile(profilePath)); + Assert.Single(_testLogger.FullLog); + Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", _testLogger.FullLog[0]); } [Fact] @@ -210,22 +205,12 @@ public void ProfileWithNonTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithNonTerminatingError", "Func1"); - - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); + var profilePath = Path.Join(TestUtils.FunctionDirectory, "ProfileWithNonTerminatingError", "Profile.ps1"); + _testManager.InvokeProfile(profilePath); - Assert.Equal(2, _testLogger.FullLog.Count); - Assert.Equal("Error: ERROR: help me!", _testLogger.FullLog[0]); - Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", _testLogger.FullLog[1]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); - } + Assert.Equal(2, _testLogger.FullLog.Count); + Assert.Equal("Error: ERROR: help me!", _testLogger.FullLog[0]); + Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", _testLogger.FullLog[1]); } } } diff --git a/test/Unit/Utility/TypeExtensionsTests.cs b/test/Unit/Utility/TypeExtensionsTests.cs index ebf1f9a7..bf9e19d7 100644 --- a/test/Unit/Utility/TypeExtensionsTests.cs +++ b/test/Unit/Utility/TypeExtensionsTests.cs @@ -26,7 +26,7 @@ public class TypeExtensionsTests public TypeExtensionsTests() { _testLogger = new ConsoleLogger(); - _testManager = new PowerShellManager(_testLogger); + _testManager = TestUtils.NewTestPowerShellManager(_testLogger); } #region TypedDataToObject