From 2906d670da14e09e81b96100a10347723f986437 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Sat, 15 Dec 2018 14:41:12 -0800 Subject: [PATCH 1/2] Refactor the code base to make it easy to support concurrency in future --- src/FunctionLoader.cs | 9 +- src/PowerShell/PowerShellManager.cs | 70 ++++---- src/PowerShell/PowerShellManagerPool.cs | 63 +++++++ src/Public/FunctionMetadata.cs | 17 ++ src/RequestProcessor.cs | 38 ++--- .../Unit/PowerShell/PowerShellManagerTests.cs | 155 +++++++++++------- test/Unit/Utility/TypeExtensionsTests.cs | 2 +- 7 files changed, 229 insertions(+), 125 deletions(-) create mode 100644 src/PowerShell/PowerShellManagerPool.cs 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..cc3cca7f 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,31 +70,15 @@ 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(); } /// - /// 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 Function App's profile.ps1. /// - internal void PerformRunspaceLevelInitialization() + internal void InvokeProfile() { Exception exception = null; string profilePath = FunctionLoader.FunctionAppProfilePath; @@ -195,25 +204,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..866d9c60 100644 --- a/test/Unit/PowerShell/PowerShellManagerTests.cs +++ b/test/Unit/PowerShell/PowerShellManagerTests.cs @@ -14,44 +14,76 @@ 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); + } + + 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); + } + + 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 +92,66 @@ 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"); + TestUtils.Break(); - var functionInfo = GetAzFunctionInfo(path, string.Empty); + string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunction.ps1"); + + 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"); + TestUtils.Break(); + + 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"); + TestUtils.Break(); + + 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); + TestUtils.Break(); + + 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 +162,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,20 +177,20 @@ public void ProfileShouldWork() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileBasic", "Func1"); + var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); + funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileBasic", "Func1"); try { FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); + _testManager.InvokeProfile(); Assert.Single(_testLogger.FullLog); Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]); } finally { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); + FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); } } @@ -166,20 +199,20 @@ public void ProfileDoesNotExist() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); + var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); funcLoadReq.Metadata.Directory = AppDomain.CurrentDomain.BaseDirectory; try { FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); + _testManager.InvokeProfile(); Assert.Single(_testLogger.FullLog); Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]); } finally { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); + FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); } } @@ -188,20 +221,20 @@ public void ProfileWithTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithTerminatingError", "Func1"); + var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); + funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileWithTerminatingError", "Func1"); try { FunctionLoader.SetupWellKnownPaths(funcLoadReq); - Assert.Throws(() => _testManager.PerformRunspaceLevelInitialization()); + Assert.Throws(() => _testManager.InvokeProfile()); 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); + FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); } } @@ -210,13 +243,13 @@ public void ProfileWithNonTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = _functionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithNonTerminatingError", "Func1"); + var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); + funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileWithNonTerminatingError", "Func1"); try { FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.PerformRunspaceLevelInitialization(); + _testManager.InvokeProfile(); Assert.Equal(2, _testLogger.FullLog.Count); Assert.Equal("Error: ERROR: help me!", _testLogger.FullLog[0]); @@ -224,7 +257,7 @@ public void ProfileWithNonTerminatingError() } finally { - FunctionLoader.SetupWellKnownPaths(_functionLoadRequest); + FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); } } } 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 From 7297a9e590cc79f46d8cb9fed6eab99e09405750 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 17 Dec 2018 00:45:54 -0800 Subject: [PATCH 2/2] Fix test failure and address comments --- src/PowerShell/PowerShellManager.cs | 7 +- .../Unit/PowerShell/PowerShellManagerTests.cs | 86 ++++--------------- 2 files changed, 22 insertions(+), 71 deletions(-) diff --git a/src/PowerShell/PowerShellManager.cs b/src/PowerShell/PowerShellManager.cs index cc3cca7f..1a929a78 100644 --- a/src/PowerShell/PowerShellManager.cs +++ b/src/PowerShell/PowerShellManager.cs @@ -72,16 +72,15 @@ internal PowerShellManager(ILogger logger) _pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding; // Initialize the Runspace - InvokeProfile(); + InvokeProfile(FunctionLoader.FunctionAppProfilePath); } /// - /// This method invokes the Function App's profile.ps1. + /// This method invokes the FunctionApp's profile.ps1. /// - internal void InvokeProfile() + 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}"); diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs index 866d9c60..f74bcf2e 100644 --- a/test/Unit/PowerShell/PowerShellManagerTests.cs +++ b/test/Unit/PowerShell/PowerShellManagerTests.cs @@ -41,6 +41,8 @@ static TestUtils() 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); @@ -54,15 +56,14 @@ internal static AzFunctionInfo NewAzFunctionInfo(string scriptFile, string entry 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(); - */ } } @@ -95,8 +96,6 @@ public PowerShellManagerTests() [Fact] public void InvokeBasicFunctionWorks() { - TestUtils.Break(); - string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunction.ps1"); var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); @@ -108,8 +107,6 @@ public void InvokeBasicFunctionWorks() [Fact] public void InvokeBasicFunctionWithTriggerMetadataWorks() { - TestUtils.Break(); - string path = Path.Join(TestUtils.FunctionDirectory, "testBasicFunctionWithTriggerMetadata.ps1"); Hashtable triggerMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase) { @@ -125,8 +122,6 @@ public void InvokeBasicFunctionWithTriggerMetadataWorks() [Fact] public void InvokeFunctionWithEntryPointWorks() { - TestUtils.Break(); - string path = Path.Join(TestUtils.FunctionDirectory, "testFunctionWithEntryPoint.psm1"); var functionInfo = TestUtils.NewAzFunctionInfo(path, "Run"); Hashtable result = _testManager.InvokeFunction(functionInfo, null, _testInputData); @@ -137,8 +132,6 @@ public void InvokeFunctionWithEntryPointWorks() [Fact] public void FunctionShouldCleanupVariableTable() { - TestUtils.Break(); - string path = Path.Join(TestUtils.FunctionDirectory, "testFunctionCleanup.ps1"); var functionInfo = TestUtils.NewAzFunctionInfo(path, string.Empty); @@ -177,21 +170,11 @@ public void ProfileShouldWork() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileBasic", "Func1"); - - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.InvokeProfile(); + var profilePath = Path.Join(TestUtils.FunctionDirectory, "ProfileBasic", "profile.ps1"); + _testManager.InvokeProfile(profilePath); - Assert.Single(_testLogger.FullLog); - Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); - } + Assert.Single(_testLogger.FullLog); + Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]); } [Fact] @@ -199,21 +182,10 @@ public void ProfileDoesNotExist() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = AppDomain.CurrentDomain.BaseDirectory; + _testManager.InvokeProfile(null); - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.InvokeProfile(); - - Assert.Single(_testLogger.FullLog); - Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]); - } - finally - { - FunctionLoader.SetupWellKnownPaths(TestUtils.FunctionLoadRequest); - } + Assert.Single(_testLogger.FullLog); + Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]); } [Fact] @@ -221,21 +193,11 @@ public void ProfileWithTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileWithTerminatingError", "Func1"); - - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); + var profilePath = Path.Join(TestUtils.FunctionDirectory, "ProfileWithTerminatingError", "profile.ps1"); - Assert.Throws(() => _testManager.InvokeProfile()); - 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(TestUtils.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] @@ -243,22 +205,12 @@ public void ProfileWithNonTerminatingError() { //initialize fresh log _testLogger.FullLog.Clear(); - var funcLoadReq = TestUtils.FunctionLoadRequest.Clone(); - funcLoadReq.Metadata.Directory = Path.Join(TestUtils.FunctionDirectory, "ProfileWithNonTerminatingError", "Func1"); - - try - { - FunctionLoader.SetupWellKnownPaths(funcLoadReq); - _testManager.InvokeProfile(); + 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(TestUtils.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]); } } }