diff --git a/examples/PSCoreApp/Profile.ps1 b/examples/PSCoreApp/profile.ps1
similarity index 100%
rename from examples/PSCoreApp/Profile.ps1
rename to examples/PSCoreApp/profile.ps1
diff --git a/src/FunctionInfo.cs b/src/FunctionInfo.cs
new file mode 100644
index 00000000..3846c624
--- /dev/null
+++ b/src/FunctionInfo.cs
@@ -0,0 +1,225 @@
+//
+// 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.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Management.Automation.Language;
+using System.Text;
+
+using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
+
+namespace Microsoft.Azure.Functions.PowerShellWorker
+{
+ ///
+ /// This type represents the metadata of an Azure PowerShell Function.
+ ///
+ internal class AzFunctionInfo
+ {
+ private const string OrchestrationTrigger = "orchestrationTrigger";
+ private const string ActivityTrigger = "activityTrigger";
+
+ internal const string TriggerMetadata = "TriggerMetadata";
+ internal const string DollarReturn = "$return";
+
+ internal readonly string FuncDirectory;
+ internal readonly string FuncName;
+ internal readonly string EntryPoint;
+ internal readonly string ScriptPath;
+ internal readonly HashSet FuncParameters;
+ internal readonly AzFunctionType Type;
+ internal readonly ReadOnlyDictionary AllBindings;
+ internal readonly ReadOnlyDictionary InputBindings;
+ internal readonly ReadOnlyDictionary OutputBindings;
+
+ ///
+ /// Construct an object of AzFunctionInfo from the 'RpcFunctionMetadata'.
+ /// Necessary validations are done on the metadata and script.
+ ///
+ internal AzFunctionInfo(RpcFunctionMetadata metadata)
+ {
+ FuncName = metadata.Name;
+ FuncDirectory = metadata.Directory;
+ EntryPoint = metadata.EntryPoint;
+ ScriptPath = metadata.ScriptFile;
+
+ // Support 'entryPoint' only if 'scriptFile' is a .psm1 file;
+ // Support .psm1 'scriptFile' only if 'entryPoint' is specified.
+ bool isScriptFilePsm1 = ScriptPath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase);
+ if (string.IsNullOrEmpty(EntryPoint))
+ {
+ if (isScriptFilePsm1)
+ {
+ throw new ArgumentException($"The 'entryPoint' property needs to be specified when 'scriptFile' points to a PowerShell module script file (.psm1).");
+ }
+ }
+ else if (!isScriptFilePsm1)
+ {
+ throw new ArgumentException($"The 'entryPoint' property is supported only if 'scriptFile' points to a PowerShell module script file (.psm1).");
+ }
+
+ // Get the parameter names of the script or function.
+ FuncParameters = GetParameters(ScriptPath, EntryPoint);
+ var parametersCopy = new HashSet(FuncParameters, StringComparer.OrdinalIgnoreCase);
+ parametersCopy.Remove(TriggerMetadata);
+
+ var allBindings = new Dictionary();
+ var inputBindings = new Dictionary();
+ var outputBindings = new Dictionary();
+
+ var inputsMissingFromParams = new List();
+ foreach (var binding in metadata.Bindings)
+ {
+ string bindingName = binding.Key;
+ var bindingInfo = new ReadOnlyBindingInfo(binding.Value);
+
+ allBindings.Add(bindingName, bindingInfo);
+
+ if (bindingInfo.Direction == BindingInfo.Types.Direction.In)
+ {
+ Type = GetAzFunctionType(bindingInfo);
+ inputBindings.Add(bindingName, bindingInfo);
+
+ // If the input binding name is in the set, we remove it;
+ // otherwise, the binding name is missing from the params.
+ if (!parametersCopy.Remove(bindingName))
+ {
+ inputsMissingFromParams.Add(bindingName);
+ }
+ }
+ else if (bindingInfo.Direction == BindingInfo.Types.Direction.Out)
+ {
+ outputBindings.Add(bindingName, bindingInfo);
+ }
+ else
+ {
+ // PowerShell doesn't support the 'InOut' type binding
+ throw new InvalidOperationException($"The binding '{bindingName}' is declared with 'InOut' direction, which is not supported by PowerShell functions.");
+ }
+ }
+
+ if (inputsMissingFromParams.Count != 0 || parametersCopy.Count != 0)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ foreach (string inputBindingName in inputsMissingFromParams)
+ {
+ stringBuilder.AppendLine($"No parameter defined in the script or function for the input binding '{inputBindingName}'.");
+ }
+
+ foreach (string param in parametersCopy)
+ {
+ stringBuilder.AppendLine($"No input binding defined for the parameter '{param}' that is declared in the script or function.");
+ }
+
+ string errorMsg = stringBuilder.ToString();
+ throw new InvalidOperationException(errorMsg);
+ }
+
+ AllBindings = new ReadOnlyDictionary(allBindings);
+ InputBindings = new ReadOnlyDictionary(inputBindings);
+ OutputBindings = new ReadOnlyDictionary(outputBindings);
+ }
+
+ private AzFunctionType GetAzFunctionType(ReadOnlyBindingInfo bindingInfo)
+ {
+ switch (bindingInfo.Type)
+ {
+ case OrchestrationTrigger:
+ return AzFunctionType.OrchestrationFunction;
+ case ActivityTrigger:
+ return AzFunctionType.ActivityFunction;
+ default:
+ // All other triggers are considered regular functions
+ return AzFunctionType.RegularFunction;
+ }
+ }
+
+ private HashSet GetParameters(string scriptFile, string entryPoint)
+ {
+ ScriptBlockAst sbAst = Parser.ParseFile(scriptFile, out _, out ParseError[] errors);
+ if (errors != null && errors.Length > 0)
+ {
+ var stringBuilder = new StringBuilder(15);
+ foreach (var error in errors)
+ {
+ stringBuilder.AppendLine(error.Message);
+ }
+
+ string errorMsg = stringBuilder.ToString();
+ throw new ArgumentException($"The script file '{scriptFile}' has parsing errors:\n{errorMsg}");
+ }
+
+ ReadOnlyCollection paramAsts = null;
+ if (string.IsNullOrEmpty(entryPoint))
+ {
+ paramAsts = sbAst.ParamBlock?.Parameters;
+ }
+ else
+ {
+ var asts = sbAst.FindAll(
+ ast => ast is FunctionDefinitionAst func && entryPoint.Equals(func.Name, StringComparison.OrdinalIgnoreCase),
+ searchNestedScriptBlocks: false).ToList();
+
+ if (asts.Count == 1)
+ {
+ var funcAst = (FunctionDefinitionAst) asts[0];
+ paramAsts = funcAst.Parameters ?? funcAst.Body.ParamBlock?.Parameters;
+ }
+ else
+ {
+ string errorMsg = asts.Count == 0
+ ? $"Cannot find the function '{entryPoint}' defined in '{scriptFile}'"
+ : $"More than one functions named '{entryPoint}' are found in '{scriptFile}'";
+ throw new ArgumentException(errorMsg);
+ }
+ }
+
+ HashSet parameters = new HashSet(StringComparer.OrdinalIgnoreCase);
+ if (paramAsts != null)
+ {
+ foreach (var paramAst in paramAsts)
+ {
+ parameters.Add(paramAst.Name.VariablePath.UserPath);
+ }
+ }
+
+ return parameters;
+ }
+ }
+
+ ///
+ /// Type of the Azure Function.
+ ///
+ internal enum AzFunctionType
+ {
+ None = 0,
+ RegularFunction = 1,
+ OrchestrationFunction = 2,
+ ActivityFunction = 3
+ }
+
+ ///
+ /// A read-only type that represents a BindingInfo.
+ ///
+ public class ReadOnlyBindingInfo
+ {
+ internal ReadOnlyBindingInfo(BindingInfo bindingInfo)
+ {
+ Type = bindingInfo.Type;
+ Direction = bindingInfo.Direction;
+ }
+
+ ///
+ /// The type of the binding.
+ ///
+ public readonly string Type;
+
+ ///
+ /// The direction of the binding.
+ ///
+ public readonly BindingInfo.Types.Direction Direction;
+ }
+}
diff --git a/src/FunctionLoader.cs b/src/FunctionLoader.cs
index 4b8e4228..3d666cce 100644
--- a/src/FunctionLoader.cs
+++ b/src/FunctionLoader.cs
@@ -8,19 +8,24 @@
using System.IO;
using System.Linq;
-using Google.Protobuf.Collections;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
namespace Microsoft.Azure.Functions.PowerShellWorker
{
+ ///
+ /// FunctionLoader holds metadata of functions.
+ ///
internal class FunctionLoader
{
- private readonly MapField _loadedFunctions = new MapField();
+ private readonly Dictionary _loadedFunctions = new Dictionary();
- internal static string FunctionAppRootPath { get; set; }
- internal static string FunctionAppProfilePath { get; set; } = null;
- internal static string FunctionAppModulesPath { get; set; } = null;
+ internal static string FunctionAppRootPath { get; private set; }
+ internal static string FunctionAppProfilePath { get; private set; }
+ internal static string FunctionAppModulesPath { get; private set; }
+ ///
+ /// Query for function metadata can happen in parallel.
+ ///
internal AzFunctionInfo GetFunctionInfo(string functionId)
{
if (_loadedFunctions.TryGetValue(functionId, out AzFunctionInfo funcInfo))
@@ -32,100 +37,26 @@ internal AzFunctionInfo GetFunctionInfo(string functionId)
}
///
- /// Runs once per Function in a Function App. Loads the Function info into the Function Loader
+ /// This method runs once per 'FunctionLoadRequest' during the code start of the worker.
+ /// It will always run synchronously because we process 'FunctionLoadRequest' synchronously.
///
internal void LoadFunction(FunctionLoadRequest request)
{
- // TODO: catch "load" issues at "func start" time.
- // ex. Script doesn't exist, entry point doesn't exist
_loadedFunctions.Add(request.FunctionId, new AzFunctionInfo(request.Metadata));
}
///
- /// Sets up well-known paths like the Function App root,
- /// the Function App 'Modules' folder,
- /// and the Function App's profile.ps1
+ /// Setup the well known paths about the FunctionApp.
+ /// This method is called only once during the code start.
///
- internal static void SetupWellKnownPaths(string functionAppRootLocation)
- {
- FunctionLoader.FunctionAppRootPath = functionAppRootLocation;
- FunctionLoader.FunctionAppModulesPath = Path.Combine(functionAppRootLocation, "Modules");
-
- // Find the profile.ps1 in the Function App root if it exists
- List profiles = Directory.EnumerateFiles(functionAppRootLocation, "profile.ps1", new EnumerationOptions {
- MatchCasing = MatchCasing.CaseInsensitive
- }).ToList();
- if (profiles.Count() > 0)
- {
- FunctionLoader.FunctionAppProfilePath = profiles[0];
- }
- }
- }
-
- internal enum AzFunctionType
- {
- None = 0,
- RegularFunction = 1,
- OrchestrationFunction = 2,
- ActivityFunction = 3
- }
-
- internal class AzFunctionInfo
- {
- private const string OrchestrationTrigger = "orchestrationTrigger";
- private const string ActivityTrigger = "activityTrigger";
-
- internal const string TriggerMetadata = "TriggerMetadata";
- internal const string DollarReturn = "$return";
-
- internal readonly string Directory;
- internal readonly string EntryPoint;
- internal readonly string FunctionName;
- internal readonly string ScriptPath;
- internal readonly AzFunctionType Type;
- internal readonly MapField AllBindings;
- internal readonly MapField OutputBindings;
-
- internal AzFunctionInfo(RpcFunctionMetadata metadata)
+ internal static void SetupWellKnownPaths(FunctionLoadRequest request)
{
- FunctionName = metadata.Name;
- Directory = metadata.Directory;
- EntryPoint = metadata.EntryPoint;
- ScriptPath = metadata.ScriptFile;
-
- AllBindings = new MapField();
- OutputBindings = new MapField();
+ FunctionAppRootPath = Path.GetFullPath(Path.Join(request.Metadata.Directory, ".."));
+ FunctionAppModulesPath = Path.Join(FunctionAppRootPath, "Modules");
- foreach (var binding in metadata.Bindings)
- {
- string bindingName = binding.Key;
- BindingInfo bindingInfo = binding.Value;
-
- AllBindings.Add(bindingName, bindingInfo);
-
- // PowerShell doesn't support the 'InOut' type binding
- if (bindingInfo.Direction == BindingInfo.Types.Direction.In)
- {
- switch (bindingInfo.Type)
- {
- case OrchestrationTrigger:
- Type = AzFunctionType.OrchestrationFunction;
- break;
- case ActivityTrigger:
- Type = AzFunctionType.ActivityFunction;
- break;
- default:
- Type = AzFunctionType.RegularFunction;
- break;
- }
- continue;
- }
-
- if (bindingInfo.Direction == BindingInfo.Types.Direction.Out)
- {
- OutputBindings.Add(bindingName, bindingInfo);
- }
- }
+ 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 91a51d4a..a61ebda3 100644
--- a/src/PowerShell/PowerShellManager.cs
+++ b/src/PowerShell/PowerShellManager.cs
@@ -47,50 +47,61 @@ internal PowerShellManager(ILogger logger)
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
}
- internal void InitializeRunspace()
+ ///
+ /// This method performs the one-time initialization at the worker process level.
+ ///
+ internal void PerformWorkerLevelInitialization()
{
- // Add HttpResponseContext namespace so users can reference
- // HttpResponseContext without needing to specify the full namespace
- _pwsh.AddScript($"using namespace {typeof(HttpResponseContext).Namespace}").InvokeAndClearCommands();
+ // 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
- Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"));
+ var workerModulesPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules");
+ Environment.SetEnvironmentVariable("PSModulePath", $"{FunctionLoader.FunctionAppModulesPath}{Path.PathSeparator}{workerModulesPath}");
}
- internal void InvokeProfile()
+ ///
+ /// This method performs initialization that has to be done for each Runspace, e.g. running the Function App's profile.ps1.
+ ///
+ internal void PerformRunspaceLevelInitialization()
{
- string functionAppProfileLocation = FunctionLoader.FunctionAppProfilePath;
- if (functionAppProfileLocation == null)
+ 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}");
return;
}
-
+
try
{
// Import-Module on a .ps1 file will evaluate the script in the global scope.
_pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module")
- .AddParameter("Name", functionAppProfileLocation).AddParameter("PassThru", true)
- .AddCommand("Microsoft.PowerShell.Core\\Remove-Module")
- .AddParameter("Force", true).AddParameter("ErrorAction", "SilentlyContinue")
- .InvokeAndClearCommands();
+ .AddParameter("Name", profilePath)
+ .AddParameter("PassThru", true)
+ .AddCommand("Microsoft.PowerShell.Core\\Remove-Module")
+ .AddParameter("Force", true)
+ .AddParameter("ErrorAction", "SilentlyContinue")
+ .InvokeAndClearCommands();
}
catch (Exception e)
{
- _logger.Log(
- LogLevel.Error,
- $"Fail to run profile.ps1. See logs for detailed errors. Profile location: {functionAppProfileLocation}",
- e,
- isUserLog: true);
+ exception = e;
throw;
}
-
- if (_pwsh.HadErrors)
+ finally
{
- _logger.Log(
- LogLevel.Error,
- $"Fail to run profile.ps1. See logs for detailed errors. Profile location: {functionAppProfileLocation}",
- isUserLog: true);
+ if (_pwsh.HadErrors)
+ {
+ string errorMsg = $"Fail to run profile.ps1. See logs for detailed errors. Profile location: {profilePath}";
+ _logger.Log(LogLevel.Error, errorMsg, exception, isUserLog: true);
+ }
}
}
@@ -108,22 +119,32 @@ internal Hashtable InvokeFunction(
try
{
- // If an entry point is defined, we load the script as a module and invoke the function with that name.
- // We also need to fetch the ParameterMetadata to know what to pass in as arguments.
- var parameterMetadata = RetriveParameterMetadata(functionInfo, out moduleName);
- _pwsh.AddCommand(String.IsNullOrEmpty(entryPoint) ? scriptPath : entryPoint);
+ if (string.IsNullOrEmpty(entryPoint))
+ {
+ _pwsh.AddCommand(scriptPath);
+ }
+ else
+ {
+ // If an entry point is defined, we import the script module.
+ moduleName = Path.GetFileNameWithoutExtension(scriptPath);
+ _pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module")
+ .AddParameter("Name", scriptPath)
+ .InvokeAndClearCommands();
+
+ _pwsh.AddCommand(entryPoint);
+ }
// Set arguments for each input binding parameter
foreach (ParameterBinding binding in inputData)
{
- if (parameterMetadata.ContainsKey(binding.Name))
+ if (functionInfo.FuncParameters.Contains(binding.Name))
{
_pwsh.AddParameter(binding.Name, binding.Data.ToObject());
}
}
// Gives access to additional Trigger Metadata if the user specifies TriggerMetadata
- if(parameterMetadata.ContainsKey(AzFunctionInfo.TriggerMetadata))
+ if(functionInfo.FuncParameters.Contains(AzFunctionInfo.TriggerMetadata))
{
_logger.Log(LogLevel.Debug, "Parameter '-TriggerMetadata' found.");
_pwsh.AddParameter(AzFunctionInfo.TriggerMetadata, triggerMetadata);
@@ -137,7 +158,7 @@ internal Hashtable InvokeFunction(
}
var result = _pwsh.AddCommand("Microsoft.Azure.Functions.PowerShellWorker\\Get-OutputBinding")
- .AddParameter("Purge", true)
+ .AddParameter("Purge", true)
.InvokeAndClearCommands()[0];
/*
@@ -168,28 +189,18 @@ internal Hashtable InvokeFunction(
internal string ConvertToJson(object fromObj)
{
return _pwsh.AddCommand("Microsoft.PowerShell.Utility\\ConvertTo-Json")
- .AddParameter("InputObject", fromObj)
- .AddParameter("Depth", 3)
- .AddParameter("Compress", true)
+ .AddParameter("InputObject", fromObj)
+ .AddParameter("Depth", 3)
+ .AddParameter("Compress", true)
.InvokeAndClearCommands()[0];
}
- ///
- /// Helper method to prepend the FunctionApp module folder to the module path.
- ///
- internal void PrependToPSModulePath(string directory)
- {
- // Adds the passed in directory to the front of the PSModulePath using the path separator of the OS.
- string psModulePath = Environment.GetEnvironmentVariable("PSModulePath");
- Environment.SetEnvironmentVariable("PSModulePath", $"{directory}{Path.PathSeparator}{psModulePath}");
- }
-
///
/// Helper method to set the output binding metadata for the function that is about to run.
///
internal void RegisterFunctionMetadata(AzFunctionInfo functionInfo)
{
- var outputBindings = new ReadOnlyDictionary(functionInfo.OutputBindings);
+ var outputBindings = functionInfo.OutputBindings;
FunctionMetadata.OutputBindingCache.AddOrUpdate(_pwsh.Runspace.InstanceId,
outputBindings,
(key, value) => outputBindings);
@@ -203,43 +214,19 @@ internal void UnregisterFunctionMetadata()
FunctionMetadata.OutputBindingCache.TryRemove(_pwsh.Runspace.InstanceId, out _);
}
- private Dictionary RetriveParameterMetadata(AzFunctionInfo functionInfo, out string moduleName)
- {
- moduleName = null;
- string scriptPath = functionInfo.ScriptPath;
- string entryPoint = functionInfo.EntryPoint;
-
- using (ExecutionTimer.Start(_logger, "Parameter metadata retrieved."))
- {
- if (String.IsNullOrEmpty(entryPoint))
- {
- return _pwsh.AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", scriptPath)
- .InvokeAndClearCommands()[0].Parameters;
- }
- else
- {
- moduleName = Path.GetFileNameWithoutExtension(scriptPath);
- return _pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", scriptPath)
- .AddStatement()
- .AddCommand("Microsoft.PowerShell.Core\\Get-Command").AddParameter("Name", entryPoint)
- .InvokeAndClearCommands()[0].Parameters;
- }
- }
- }
-
private void ResetRunspace(string moduleName)
{
// Reset the runspace to the Initial Session State
_pwsh.Runspace.ResetRunspaceState();
- if (!String.IsNullOrEmpty(moduleName))
+ if (!string.IsNullOrEmpty(moduleName))
{
// If the function had an entry point, this will remove the module that was loaded
_pwsh.AddCommand("Microsoft.PowerShell.Core\\Remove-Module")
- .AddParameter("Name", moduleName)
- .AddParameter("Force", true)
- .AddParameter("ErrorAction", "SilentlyContinue")
- .InvokeAndClearCommands();
+ .AddParameter("Name", moduleName)
+ .AddParameter("Force", true)
+ .AddParameter("ErrorAction", "SilentlyContinue")
+ .InvokeAndClearCommands();
}
}
}
diff --git a/src/Public/FunctionMetadata.cs b/src/Public/FunctionMetadata.cs
index 4dd9c20b..442f5f61 100644
--- a/src/Public/FunctionMetadata.cs
+++ b/src/Public/FunctionMetadata.cs
@@ -6,8 +6,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
-using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
-using Google.Protobuf.Collections;
namespace Microsoft.Azure.Functions.PowerShellWorker
{
@@ -16,15 +14,15 @@ namespace Microsoft.Azure.Functions.PowerShellWorker
///
public static class FunctionMetadata
{
- internal static ConcurrentDictionary> OutputBindingCache
- = new ConcurrentDictionary>();
+ internal static ConcurrentDictionary> OutputBindingCache
+ = new ConcurrentDictionary>();
///
/// Get the binding metadata for the given Runspace instance id.
///
- public static ReadOnlyDictionary GetOutputBindingInfo(Guid runspaceInstanceId)
+ public static ReadOnlyDictionary GetOutputBindingInfo(Guid runspaceInstanceId)
{
- ReadOnlyDictionary outputBindings = null;
+ ReadOnlyDictionary outputBindings = null;
OutputBindingCache.TryGetValue(runspaceInstanceId, out outputBindings);
return outputBindings;
}
diff --git a/src/RequestProcessor.cs b/src/RequestProcessor.cs
index aef33862..4d38309c 100644
--- a/src/RequestProcessor.cs
+++ b/src/RequestProcessor.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.IO;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.PowerShellWorker.Messaging;
@@ -23,10 +22,8 @@ internal class RequestProcessor
private readonly MessagingStream _msgStream;
private readonly PowerShellManager _powerShellManager;
- // This is somewhat of a workaround for the fact that the WorkerInitialize message does
- // not contain the file path of the Function App. Instead, we use this bool during the
- // FunctionLoad message to initialize the Function App since we have the path.
- private bool _initializedFunctionApp;
+ // Indicate whether the FunctionApp has been initialized.
+ private bool _isFunctionAppInitialized;
internal RequestProcessor(MessagingStream msgStream)
{
@@ -52,15 +49,12 @@ internal async Task ProcessRequestLoop()
case StreamingMessage.ContentOneofCase.WorkerInitRequest:
response = ProcessWorkerInitRequest(request);
break;
-
case StreamingMessage.ContentOneofCase.FunctionLoadRequest:
response = ProcessFunctionLoadRequest(request);
break;
-
case StreamingMessage.ContentOneofCase.InvocationRequest:
response = ProcessInvocationRequest(request);
break;
-
default:
throw new InvalidOperationException($"Not supportted message type: {request.ContentCase}");
}
@@ -77,19 +71,15 @@ internal StreamingMessage ProcessWorkerInitRequest(StreamingMessage request)
StreamingMessage.ContentOneofCase.WorkerInitResponse,
out StatusResult status);
- try
- {
- _powerShellManager.InitializeRunspace();
- }
- catch (Exception e)
- {
- status.Status = StatusResult.Types.Status.Failure;
- status.Exception = e.ToRpcException();
- }
-
return response;
}
+ ///
+ /// Method to process a FunctionLoadRequest.
+ /// FunctionLoadRequest should be processed sequentially. There is no point to process FunctionLoadRequest
+ /// concurrently as a FunctionApp doesn't include a lot functions in general. Having this step sequential
+ /// will make the Runspace-level initialization easier and more predictable.
+ ///
internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
{
FunctionLoadRequest functionLoadRequest = request.FunctionLoadRequest;
@@ -102,25 +92,19 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
try
{
- // This is the first opportunity we have to obtain the location of the Function App on the file system
- // so we run some additional setup including:
- // * Storing some well-known paths in the Function Loader
- // * Prepending the Function App 'Modules' path
- // * Invoking the Function App's profile.ps1
- if (!_initializedFunctionApp)
+ // Ideally, the initialization should happen when processing 'WorkerInitRequest', however, the 'WorkerInitRequest'
+ // message doesn't provide the file path of the FunctionApp. That information is not available until the first
+ // 'FunctionLoadRequest' comes in. Therefore, we run initialization here.
+ if (!_isFunctionAppInitialized)
{
- // We obtain the Function App root path by navigating up
- // one directory from the _Function_ directory we are given
- FunctionLoader.SetupWellKnownPaths(Path.GetFullPath(Path.Combine(functionLoadRequest.Metadata.Directory, "..")));
+ FunctionLoader.SetupWellKnownPaths(functionLoadRequest);
+ _powerShellManager.PerformWorkerLevelInitialization();
+ _powerShellManager.PerformRunspaceLevelInitialization();
- _powerShellManager.PrependToPSModulePath(FunctionLoader.FunctionAppModulesPath);
-
- _powerShellManager.InvokeProfile();
-
- _initializedFunctionApp = true;
+ _isFunctionAppInitialized = true;
}
- // Try loading the metadata of the function
+ // Load the metadata of the function.
_functionLoader.LoadFunction(functionLoadRequest);
}
catch (Exception e)
@@ -132,6 +116,10 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
return response;
}
+ ///
+ /// Method to process a InvocationRequest.
+ /// InvocationRequest should be processed in parallel eventually.
+ ///
internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
{
InvocationRequest invocationRequest = request.InvocationRequest;
@@ -237,7 +225,7 @@ private void BindOutputFromResult(InvocationResponse response, AzFunctionInfo fu
{
case AzFunctionType.RegularFunction:
// Set out binding data and return response to be sent back to host
- foreach (KeyValuePair binding in functionInfo.OutputBindings)
+ foreach (KeyValuePair binding in functionInfo.OutputBindings)
{
// if one of the bindings is '$return' we need to set the ReturnValue
string outBindingName = binding.Key;
diff --git a/src/worker.config.json b/src/worker.config.json
index 74e56413..f003c0c9 100644
--- a/src/worker.config.json
+++ b/src/worker.config.json
@@ -1,7 +1,7 @@
{
"description":{
"language":"powershell",
- "extensions":[".ps1"],
+ "extensions":[".ps1", ".psm1"],
"defaultExecutablePath":"dotnet",
"defaultWorkerPath":"Microsoft.Azure.Functions.PowerShellWorker.dll"
}
diff --git a/test/E2E/HttpTrigger.Tests.ps1 b/test/E2E/HttpTrigger.Tests.ps1
index a3098ee8..a9e03560 100644
--- a/test/E2E/HttpTrigger.Tests.ps1
+++ b/test/E2E/HttpTrigger.Tests.ps1
@@ -28,6 +28,18 @@ Describe 'HttpTrigger Tests' {
@{
FunctionName = 'TestBasicHttpTriggerWithProfile'
ExpectedContent = 'PROFILE'
+ },
+ @{
+ FunctionName = 'TestHttpTriggerWithEntryPoint'
+ ExpectedContent = 'Hello Atlas'
+ },
+ @{
+ FunctionName = 'TestHttpTriggerWithEntryPointAndTriggerMetadata'
+ ExpectedContent = 'Hello Atlas'
+ },
+ @{
+ FunctionName = 'TestHttpTriggerWithEntryPointAndProfile'
+ ExpectedContent = 'PROFILE'
}
) {
param ($FunctionName, $ExpectedContent)
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/function.json b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/function.json
new file mode 100644
index 00000000..16c0eea2
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/function.json
@@ -0,0 +1,22 @@
+{
+ "disabled": false,
+ "scriptFile": "module.psm1",
+ "entryPoint": "Run",
+ "bindings": [
+ {
+ "authLevel": "function",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "res"
+ }
+ ]
+}
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/module.psm1 b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/module.psm1
new file mode 100644
index 00000000..d5dce134
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPoint/module.psm1
@@ -0,0 +1,32 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+function Run
+{
+ # Input bindings are passed in via param block.
+ param($req)
+
+ # You can write to the Azure Functions log streams as you would in a normal PowerShell script.
+ Write-Verbose "PowerShell HTTP trigger function processed a request." -Verbose
+
+ # You can interact with query parameters, the body of the request, etc.
+ $name = $req.Query.Name
+ if (-not $name) { $name = $req.Body.Name }
+
+ if($name) {
+ # Cast the value to HttpResponseContext explicitly.
+ Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
+ StatusCode = 202
+ Body = "Hello " + $name
+ })
+ }
+ else {
+ # Convert value to HttpResponseContext implicitly for 'http' output.
+ Push-OutputBinding -Name res -Value @{
+ StatusCode = "400"
+ Body = "Please pass a name on the query string or in the request body."
+ }
+ }
+}
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/function.json b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/function.json
new file mode 100644
index 00000000..16c0eea2
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/function.json
@@ -0,0 +1,22 @@
+{
+ "disabled": false,
+ "scriptFile": "module.psm1",
+ "entryPoint": "Run",
+ "bindings": [
+ {
+ "authLevel": "function",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "res"
+ }
+ ]
+}
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/module.psm1 b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/module.psm1
new file mode 100644
index 00000000..fa773564
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndProfile/module.psm1
@@ -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.
+#
+
+function Run
+{
+ # Input bindings are passed in via param block.
+ param($req)
+
+ Push-OutputBinding -Name res -Value @{
+ StatusCode = 202
+ Body = (Get-ProfileString)
+ }
+}
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/function.json b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/function.json
new file mode 100644
index 00000000..16c0eea2
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/function.json
@@ -0,0 +1,22 @@
+{
+ "disabled": false,
+ "scriptFile": "module.psm1",
+ "entryPoint": "Run",
+ "bindings": [
+ {
+ "authLevel": "function",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "res"
+ }
+ ]
+}
diff --git a/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/module.psm1 b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/module.psm1
new file mode 100644
index 00000000..f1aab13b
--- /dev/null
+++ b/test/E2E/TestFunctionApp/TestHttpTriggerWithEntryPointAndTriggerMetadata/module.psm1
@@ -0,0 +1,32 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+function Run
+{
+ # Input bindings are passed in via param block.
+ param($req, $TriggerMetadata)
+
+ # You can write to the Azure Functions log streams as you would in a normal PowerShell script.
+ Write-Verbose "PowerShell HTTP trigger function processed a request." -Verbose
+
+ # You can interact with query parameters, the body of the request, etc.
+ $name = $TriggerMetadata.req.Query.Name
+ if (-not $name) { $name = $TriggerMetadata.req.Body.Name }
+
+ if($name) {
+ # Cast the value to HttpResponseContext explicitly.
+ Push-OutputBinding -Name res -Value @{
+ StatusCode = [System.Net.HttpStatusCode]::Accepted
+ Body = "Hello " + $name
+ }
+ }
+ else {
+ # Convert value to HttpResponseContext implicitly for 'http' output.
+ Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
+ StatusCode = 400
+ Body = "Please pass a name on the query string or in the request body."
+ })
+ }
+}
diff --git a/test/Microsoft.Azure.Functions.PowerShellWorker.Test.csproj b/test/Microsoft.Azure.Functions.PowerShellWorker.Test.csproj
index 77281ee6..d43d3298 100644
--- a/test/Microsoft.Azure.Functions.PowerShellWorker.Test.csproj
+++ b/test/Microsoft.Azure.Functions.PowerShellWorker.Test.csproj
@@ -19,26 +19,15 @@
-
+
+ TestScripts\PowerShell\%(RecursiveDir)\%(FileName)%(Extension)
PreserveNewest
+ PreserveNewest
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
-
+
+ TestScripts\Function\%(RecursiveDir)\%(FileName)%(Extension)
PreserveNewest
+ PreserveNewest
diff --git a/test/Unit/Function/FunctionLoaderTests.cs b/test/Unit/Function/FunctionLoaderTests.cs
index 29c70ed3..5e82d421 100644
--- a/test/Unit/Function/FunctionLoaderTests.cs
+++ b/test/Unit/Function/FunctionLoaderTests.cs
@@ -4,7 +4,7 @@
//
using System;
-using Microsoft.Azure.Functions.PowerShellWorker;
+using System.IO;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Xunit;
@@ -12,122 +12,281 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test
{
public class FunctionLoaderTests
{
- [Fact]
- public void TestFunctionLoaderGetFunc()
+ private readonly string _functionDirectory;
+ private readonly FunctionLoadRequest _functionLoadRequest;
+
+ public FunctionLoaderTests()
{
+ _functionDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "Function");
+
var functionId = Guid.NewGuid().ToString();
- var directory = "/Users/azure/PSCoreApp/MyHttpTrigger";
- var scriptPathExpected = $"{directory}/run.ps1";
var metadata = new RpcFunctionMetadata
{
Name = "MyHttpTrigger",
- EntryPoint = "",
- Directory = directory,
- ScriptFile = scriptPathExpected
+ Directory = _functionDirectory,
+ Bindings =
+ {
+ { "req", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "httpTrigger" } },
+ { "inputBlob", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "blobTrigger" } },
+ { "res", new BindingInfo { Direction = BindingInfo.Types.Direction.Out, Type = "http" } }
+ }
};
- metadata.Bindings.Add("req", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.In,
- Type = "httpTrigger"
- });
- metadata.Bindings.Add("res", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.Out,
- Type = "http"
- });
- var functionLoadRequest = new FunctionLoadRequest{
+ _functionLoadRequest = new FunctionLoadRequest
+ {
FunctionId = functionId,
Metadata = metadata
};
+ }
+
+ private FunctionLoadRequest GetFuncLoadRequest(string scriptFile, string entryPoint)
+ {
+ var functionLoadRequest = _functionLoadRequest.Clone();
+ functionLoadRequest.Metadata.ScriptFile = scriptFile;
+ functionLoadRequest.Metadata.EntryPoint = entryPoint;
+ return functionLoadRequest;
+ }
+
+ [Fact]
+ public void TestFunctionLoaderGetFunc()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScript.ps1");
+ var entryPointToUse = string.Empty;
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
var functionLoader = new FunctionLoader();
functionLoader.LoadFunction(functionLoadRequest);
- var funcInfo = functionLoader.GetFunctionInfo(functionId);
+ var funcInfo = functionLoader.GetFunctionInfo(functionLoadRequest.FunctionId);
+
+ Assert.Equal(scriptFileToUse, funcInfo.ScriptPath);
+ Assert.Equal(string.Empty, funcInfo.EntryPoint);
- Assert.Equal(scriptPathExpected, funcInfo.ScriptPath);
- Assert.Equal("", funcInfo.EntryPoint);
+ Assert.Equal(2, funcInfo.FuncParameters.Count);
+ Assert.Contains("req", funcInfo.FuncParameters);
+ Assert.Contains("inputBlob", funcInfo.FuncParameters);
+
+ Assert.Equal(3, funcInfo.AllBindings.Count);
+ Assert.Equal(2, funcInfo.InputBindings.Count);
+ Assert.Single(funcInfo.OutputBindings);
}
[Fact]
- public void TestFunctionLoaderGetFuncWithEntryPoint()
+ public void TestFunctionLoaderGetFuncWithTriggerMetadataParam()
{
- var functionId = Guid.NewGuid().ToString();
- var directory = "/Users/azure/PSCoreApp/MyHttpTrigger";
- var scriptPathExpected = $"{directory}/run.ps1";
- var entryPointExpected = "Foo";
- var metadata = new RpcFunctionMetadata
- {
- Name = "MyHttpTrigger",
- EntryPoint = entryPointExpected,
- Directory = directory,
- ScriptFile = scriptPathExpected
- };
- metadata.Bindings.Add("req", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.In,
- Type = "httpTrigger"
- });
- metadata.Bindings.Add("res", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.Out,
- Type = "http"
- });
-
- var functionLoadRequest = new FunctionLoadRequest{
- FunctionId = functionId,
- Metadata = metadata
- };
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScriptWithTriggerMetadata.ps1");
+ var entryPointToUse = string.Empty;
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
var functionLoader = new FunctionLoader();
functionLoader.LoadFunction(functionLoadRequest);
- var funcInfo = functionLoader.GetFunctionInfo(functionId);
+ var funcInfo = functionLoader.GetFunctionInfo(functionLoadRequest.FunctionId);
+
+ Assert.Equal(scriptFileToUse, funcInfo.ScriptPath);
+ Assert.Equal(string.Empty, funcInfo.EntryPoint);
+
+ Assert.Equal(3, funcInfo.FuncParameters.Count);
+ Assert.Contains("req", funcInfo.FuncParameters);
+ Assert.Contains("inputBlob", funcInfo.FuncParameters);
+ Assert.Contains("TriggerMetadata", funcInfo.FuncParameters);
- Assert.Equal(scriptPathExpected, funcInfo.ScriptPath);
- Assert.Equal(entryPointExpected, funcInfo.EntryPoint);
+ Assert.Equal(3, funcInfo.AllBindings.Count);
+ Assert.Equal(2, funcInfo.InputBindings.Count);
+ Assert.Single(funcInfo.OutputBindings);
}
[Fact]
- public void TestFunctionLoaderGetInfo()
+ public void TestFunctionLoaderGetFuncWithEntryPoint()
{
- var functionId = Guid.NewGuid().ToString();
- var directory = "/Users/azure/PSCoreApp/MyHttpTrigger";
- var scriptPathExpected = $"{directory}/run.ps1";
- var name = "MyHttpTrigger";
- var metadata = new RpcFunctionMetadata
- {
- Name = name,
- EntryPoint = "",
- Directory = directory,
- ScriptFile = scriptPathExpected
- };
- metadata.Bindings.Add("req", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.In,
- Type = "httpTrigger"
- });
- metadata.Bindings.Add("res", new BindingInfo
- {
- Direction = BindingInfo.Types.Direction.Out,
- Type = "http"
- });
-
- var functionLoadRequest = new FunctionLoadRequest{
- FunctionId = functionId,
- Metadata = metadata
- };
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPoint.psm1");
+ var entryPointToUse = "Run";
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
var functionLoader = new FunctionLoader();
functionLoader.LoadFunction(functionLoadRequest);
- var funcInfo = functionLoader.GetFunctionInfo(functionId);
+ var funcInfo = functionLoader.GetFunctionInfo(functionLoadRequest.FunctionId);
+
+ Assert.Equal(scriptFileToUse, funcInfo.ScriptPath);
+ Assert.Equal(entryPointToUse, funcInfo.EntryPoint);
+
+ Assert.Equal(2, funcInfo.FuncParameters.Count);
+ Assert.Contains("req", funcInfo.FuncParameters);
+ Assert.Contains("inputBlob", funcInfo.FuncParameters);
- Assert.Equal(directory, funcInfo.Directory);
- Assert.Equal(name, funcInfo.FunctionName);
- Assert.Equal(2, funcInfo.AllBindings.Count);
+ Assert.Equal(3, funcInfo.AllBindings.Count);
+ Assert.Equal(2, funcInfo.InputBindings.Count);
Assert.Single(funcInfo.OutputBindings);
}
+
+ [Fact]
+ public void EntryPointIsSupportedWithPsm1FileOnly()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScript.ps1");
+ var entryPointToUse = "Run";
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("entryPoint", exception.Message);
+ Assert.Contains("(.psm1)", exception.Message);
+ }
+
+ [Fact]
+ public void Psm1IsSupportedWithEntryPointOnly()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPoint.psm1");
+ var entryPointToUse = string.Empty;
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("entryPoint", exception.Message);
+ Assert.Contains("(.psm1)", exception.Message);
+ }
+
+ [Fact]
+ public void ParseErrorInScriptFileShouldBeDetected()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithParseError.ps1");
+ var entryPointToUse = string.Empty;
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("parsing errors", exception.Message);
+ }
+
+ [Fact]
+ public void EntryPointFunctionShouldExist()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPoint.psm1");
+ var entryPointToUse = "CallMe";
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("CallMe", exception.Message);
+ Assert.Contains("FuncWithEntryPoint.psm1", exception.Message);
+ }
+
+ [Fact]
+ public void MultipleEntryPointFunctionsShouldBeDetected()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithMultiEntryPoints.psm1");
+ var entryPointToUse = "Run";
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("Run", exception.Message);
+ Assert.Contains("FuncWithMultiEntryPoints.psm1", exception.Message);
+ }
+
+ [Fact]
+ public void ParametersShouldMatchInputBinding()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScript.ps1");
+ var entryPointToUse = string.Empty;
+
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+ functionLoadRequest.Metadata.Bindings.Add("inputTable", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "tableTrigger" });
+ functionLoadRequest.Metadata.Bindings.Remove("inputBlob");
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("inputTable", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
+
+ [Fact]
+ public void ParametersShouldMatchInputBindingWithTriggerMetadataParam()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScriptWithTriggerMetadata.ps1");
+ var entryPointToUse = string.Empty;
+
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+ functionLoadRequest.Metadata.Bindings.Add("inputTable", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "tableTrigger" });
+ functionLoadRequest.Metadata.Bindings.Remove("inputBlob");
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("inputTable", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
+
+ [Fact]
+ public void EntryPointParametersShouldMatchInputBinding()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPoint.psm1");
+ var entryPointToUse = "Run";
+
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+ functionLoadRequest.Metadata.Bindings.Add("inputTable", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "tableTrigger" });
+ functionLoadRequest.Metadata.Bindings.Remove("inputBlob");
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("inputTable", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
+
+ [Fact]
+ public void EntryPointParametersShouldMatchInputBindingWithTriggerMetadataParam()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPointAndTriggerMetadata.psm1");
+ var entryPointToUse = "Run";
+
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+ functionLoadRequest.Metadata.Bindings.Add("inputTable", new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "tableTrigger" });
+ functionLoadRequest.Metadata.Bindings.Remove("inputBlob");
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("inputTable", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
+
+ [Fact]
+ public void InOutBindingIsNotSupported()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "BasicFuncScript.ps1");
+ var entryPointToUse = string.Empty;
+
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+ functionLoadRequest.Metadata.Bindings.Add("inoutBinding", new BindingInfo { Direction = BindingInfo.Types.Direction.Inout, Type = "queue" });
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("inoutBinding", exception.Message);
+ Assert.Contains("InOut", exception.Message);
+ }
+
+ [Fact]
+ public void ScriptNeedToHaveParameters()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncHasNoParams.ps1");
+ var entryPointToUse = string.Empty;
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("req", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
+
+ [Fact]
+ public void EntryPointNeedToHaveParameters()
+ {
+ var scriptFileToUse = Path.Join(_functionDirectory, "FuncWithEntryPoint.psm1");
+ var entryPointToUse = "Zoo";
+ var functionLoadRequest = GetFuncLoadRequest(scriptFileToUse, entryPointToUse);
+
+ var exception = Assert.Throws(
+ () => new FunctionLoader().LoadFunction(functionLoadRequest));
+ Assert.Contains("req", exception.Message);
+ Assert.Contains("inputBlob", exception.Message);
+ }
}
}
diff --git a/test/Unit/Function/TestScripts/BasicFuncScript.ps1 b/test/Unit/Function/TestScripts/BasicFuncScript.ps1
new file mode 100644
index 00000000..0464c162
--- /dev/null
+++ b/test/Unit/Function/TestScripts/BasicFuncScript.ps1
@@ -0,0 +1,10 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+param($req, $inputBlob)
+
+function Run($param) { "Run" }
+
+"DoNothing"
diff --git a/test/Unit/Function/TestScripts/BasicFuncScriptWithTriggerMetadata.ps1 b/test/Unit/Function/TestScripts/BasicFuncScriptWithTriggerMetadata.ps1
new file mode 100644
index 00000000..d52615d8
--- /dev/null
+++ b/test/Unit/Function/TestScripts/BasicFuncScriptWithTriggerMetadata.ps1
@@ -0,0 +1,8 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+param($req, $inputBlob, $TriggerMetadata)
+
+"DoNothing"
diff --git a/test/Unit/Function/TestScripts/FuncHasNoParams.ps1 b/test/Unit/Function/TestScripts/FuncHasNoParams.ps1
new file mode 100644
index 00000000..16afd632
--- /dev/null
+++ b/test/Unit/Function/TestScripts/FuncHasNoParams.ps1
@@ -0,0 +1,8 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+function Run($param) { "Run" }
+
+"DoNothing"
diff --git a/test/Unit/Function/TestScripts/FuncWithEntryPoint.psm1 b/test/Unit/Function/TestScripts/FuncWithEntryPoint.psm1
new file mode 100644
index 00000000..5820b7bd
--- /dev/null
+++ b/test/Unit/Function/TestScripts/FuncWithEntryPoint.psm1
@@ -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.
+#
+
+function Bar($req)
+{
+ "Bar"
+}
+
+function Run($req, $inputBlob)
+{
+ "Run"
+}
+
+function Zoo
+{
+ "Bar"
+}
diff --git a/test/Unit/Function/TestScripts/FuncWithEntryPointAndTriggerMetadata.psm1 b/test/Unit/Function/TestScripts/FuncWithEntryPointAndTriggerMetadata.psm1
new file mode 100644
index 00000000..0ff1ab86
--- /dev/null
+++ b/test/Unit/Function/TestScripts/FuncWithEntryPointAndTriggerMetadata.psm1
@@ -0,0 +1,11 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+function Run
+{
+ param($req, $inputBlob, $TriggerMetadata)
+
+ "Run"
+}
diff --git a/test/Unit/Function/TestScripts/FuncWithMultiEntryPoints.psm1 b/test/Unit/Function/TestScripts/FuncWithMultiEntryPoints.psm1
new file mode 100644
index 00000000..ac55690b
--- /dev/null
+++ b/test/Unit/Function/TestScripts/FuncWithMultiEntryPoints.psm1
@@ -0,0 +1,14 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+function Run($req)
+{
+ "Run-1"
+}
+
+function Run($req, $inputBlob)
+{
+ "Run-2"
+}
diff --git a/test/Unit/Function/TestScripts/FuncWithParseError.ps1 b/test/Unit/Function/TestScripts/FuncWithParseError.ps1
new file mode 100644
index 00000000..903e2274
--- /dev/null
+++ b/test/Unit/Function/TestScripts/FuncWithParseError.ps1
@@ -0,0 +1,8 @@
+#
+# Copyright (c) Microsoft. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+param($req, $inputBlob)
+
+begin {} begin {}
diff --git a/test/Unit/PowerShell/PowerShellManagerTests.cs b/test/Unit/PowerShell/PowerShellManagerTests.cs
index 6269c0ae..89995a42 100644
--- a/test/Unit/PowerShell/PowerShellManagerTests.cs
+++ b/test/Unit/PowerShell/PowerShellManagerTests.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.IO;
using System.Management.Automation;
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
@@ -15,54 +16,64 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test
{
public class PowerShellManagerTests
{
- public const string TestInputBindingName = "req";
- public const string TestOutputBindingName = "res";
- public const string TestStringData = "Foo";
-
- internal static ConsoleLogger defaultTestLogger = new ConsoleLogger();
- internal static PowerShellManager defaultTestManager = new PowerShellManager(defaultTestLogger);
-
- public readonly List TestInputData = new List {
- new ParameterBinding {
- Name = TestInputBindingName,
- Data = new TypedData {
- String = TestStringData
+ private const string TestInputBindingName = "req";
+ private const string TestOutputBindingName = "res";
+ private const string TestStringData = "Foo";
+
+ private readonly string _functionDirectory;
+ private readonly ConsoleLogger _testLogger;
+ private readonly PowerShellManager _testManager;
+ private readonly List _testInputData;
+ private readonly RpcFunctionMetadata _rpcFunctionMetadata;
+ private readonly FunctionLoadRequest _functionLoadRequest;
+
+ public PowerShellManagerTests()
+ {
+ _functionDirectory = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "TestScripts", "PowerShell");
+ _rpcFunctionMetadata = new RpcFunctionMetadata()
+ {
+ Name = "TestFuncApp",
+ Directory = _functionDirectory,
+ Bindings =
+ {
+ { TestInputBindingName , new BindingInfo { Direction = BindingInfo.Types.Direction.In, Type = "httpTrigger" } },
+ { TestOutputBindingName, new BindingInfo { Direction = BindingInfo.Types.Direction.Out, Type = "http" } }
}
- }
- };
- public readonly RpcFunctionMetadata rpcFunctionMetadata = new RpcFunctionMetadata();
+ };
+ _functionLoadRequest = new FunctionLoadRequest {FunctionId = "FunctionId", Metadata = _rpcFunctionMetadata};
+ FunctionLoader.SetupWellKnownPaths(_functionLoadRequest);
- private AzFunctionInfo GetAzFunctionInfo(string scriptFile, string entryPoint)
- {
- rpcFunctionMetadata.ScriptFile = scriptFile;
- rpcFunctionMetadata.EntryPoint = entryPoint;
- return new AzFunctionInfo(rpcFunctionMetadata);
+ _testLogger = new ConsoleLogger();
+ _testManager = new PowerShellManager(_testLogger);
+ _testManager.PerformWorkerLevelInitialization();
+
+ _testInputData = new List
+ {
+ new ParameterBinding
+ {
+ Name = TestInputBindingName,
+ Data = new TypedData
+ {
+ String = TestStringData
+ }
+ }
+ };
}
- [Fact]
- public void InitializeRunspaceSuccess()
+ private AzFunctionInfo GetAzFunctionInfo(string scriptFile, string entryPoint)
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
- Assert.Empty(logger.FullLog);
+ _rpcFunctionMetadata.ScriptFile = scriptFile;
+ _rpcFunctionMetadata.EntryPoint = entryPoint;
+ return new AzFunctionInfo(_rpcFunctionMetadata);
}
[Fact]
public void InvokeBasicFunctionWorks()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
-
- manager.InitializeRunspace();
-
- string path = System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/testBasicFunction.ps1");
+ string path = Path.Join(_functionDirectory, "testBasicFunction.ps1");
var functionInfo = GetAzFunctionInfo(path, string.Empty);
- Hashtable result = manager.InvokeFunction(functionInfo, null, TestInputData);
+ Hashtable result = _testManager.InvokeFunction(functionInfo, null, _testInputData);
Assert.Equal(TestStringData, result[TestOutputBindingName]);
}
@@ -70,22 +81,14 @@ public void InvokeBasicFunctionWorks()
[Fact]
public void InvokeBasicFunctionWithTriggerMetadataWorks()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
-
- manager.InitializeRunspace();
-
- string path = System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/testBasicFunctionWithTriggerMetadata.ps1");
-
+ string path = Path.Join(_functionDirectory, "testBasicFunctionWithTriggerMetadata.ps1");
Hashtable triggerMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase)
{
{ TestInputBindingName, TestStringData }
};
var functionInfo = GetAzFunctionInfo(path, string.Empty);
- Hashtable result = manager.InvokeFunction(functionInfo, triggerMetadata, TestInputData);
+ Hashtable result = _testManager.InvokeFunction(functionInfo, triggerMetadata, _testInputData);
Assert.Equal(TestStringData, result[TestOutputBindingName]);
}
@@ -93,17 +96,9 @@ public void InvokeBasicFunctionWithTriggerMetadataWorks()
[Fact]
public void InvokeFunctionWithEntryPointWorks()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
-
- manager.InitializeRunspace();
-
- string path = System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/testFunctionWithEntryPoint.ps1");
-
+ string path = Path.Join(_functionDirectory, "testFunctionWithEntryPoint.psm1");
var functionInfo = GetAzFunctionInfo(path, "Run");
- Hashtable result = manager.InvokeFunction(functionInfo, null, TestInputData);
+ Hashtable result = _testManager.InvokeFunction(functionInfo, null, _testInputData);
Assert.Equal(TestStringData, result[TestOutputBindingName]);
}
@@ -111,52 +106,36 @@ public void InvokeFunctionWithEntryPointWorks()
[Fact]
public void FunctionShouldCleanupVariableTable()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
-
- manager.InitializeRunspace();
-
- string path = System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/testFunctionCleanup.ps1");
-
+ string path = Path.Join(_functionDirectory, "testFunctionCleanup.ps1");
var functionInfo = GetAzFunctionInfo(path, string.Empty);
- Hashtable result1 = manager.InvokeFunction(functionInfo, null, TestInputData);
+
+ Hashtable result1 = _testManager.InvokeFunction(functionInfo, null, _testInputData);
Assert.Equal("is not set", result1[TestOutputBindingName]);
// the value shoould not change if the variable table is properly cleaned up.
- Hashtable result2 = manager.InvokeFunction(functionInfo, null, TestInputData);
+ Hashtable result2 = _testManager.InvokeFunction(functionInfo, null, _testInputData);
Assert.Equal("is not set", result2[TestOutputBindingName]);
}
[Fact]
- public void PrependingToPSModulePathShouldWork()
+ public void ModulePathShouldBeSetByWorkerLevelInitialization()
{
- var data = "/some/unknown/directory";
-
- string modulePathBefore = Environment.GetEnvironmentVariable("PSModulePath");
- defaultTestManager.PrependToPSModulePath(data);
- try
- {
- // the data path should be ahead of anything else
- Assert.Equal($"{data}{System.IO.Path.PathSeparator}{modulePathBefore}", Environment.GetEnvironmentVariable("PSModulePath"));
- }
- finally
- {
- // Set the PSModulePath back to what it was before
- Environment.SetEnvironmentVariable("PSModulePath", modulePathBefore);
- }
+ string workerModulePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules");
+ string funcAppModulePath = Path.Join(FunctionLoader.FunctionAppRootPath, "Modules");
+ string expectedPath = $"{funcAppModulePath}{Path.PathSeparator}{workerModulePath}";
+ Assert.Equal(expectedPath, Environment.GetEnvironmentVariable("PSModulePath"));
}
[Fact]
public void RegisterAndUnregisterFunctionMetadataShouldWork()
{
- var functionInfo = GetAzFunctionInfo("dummy-path", string.Empty);
+ string path = Path.Join(_functionDirectory, "testBasicFunction.ps1");
+ var functionInfo = GetAzFunctionInfo(path, string.Empty);
Assert.Empty(FunctionMetadata.OutputBindingCache);
- defaultTestManager.RegisterFunctionMetadata(functionInfo);
+ _testManager.RegisterFunctionMetadata(functionInfo);
Assert.Single(FunctionMetadata.OutputBindingCache);
- defaultTestManager.UnregisterFunctionMetadata();
+ _testManager.UnregisterFunctionMetadata();
Assert.Empty(FunctionMetadata.OutputBindingCache);
}
@@ -164,74 +143,89 @@ public void RegisterAndUnregisterFunctionMetadataShouldWork()
public void ProfileShouldWork()
{
//initialize fresh log
- defaultTestLogger.FullLog.Clear();
+ _testLogger.FullLog.Clear();
+ var funcLoadReq = _functionLoadRequest.Clone();
+ funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileBasic", "Func1");
- CleanupFunctionLoaderStaticPaths();
- FunctionLoader.SetupWellKnownPaths(System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/ProfileBasic"));
-
- defaultTestManager.InvokeProfile();
+ try
+ {
+ FunctionLoader.SetupWellKnownPaths(funcLoadReq);
+ _testManager.PerformRunspaceLevelInitialization();
- Assert.Single(defaultTestLogger.FullLog);
- Assert.Equal("Information: INFORMATION: Hello PROFILE", defaultTestLogger.FullLog[0]);
+ Assert.Single(_testLogger.FullLog);
+ Assert.Equal("Information: INFORMATION: Hello PROFILE", _testLogger.FullLog[0]);
+ }
+ finally
+ {
+ FunctionLoader.SetupWellKnownPaths(_functionLoadRequest);
+ }
}
[Fact]
public void ProfileDoesNotExist()
{
//initialize fresh log
- defaultTestLogger.FullLog.Clear();
+ _testLogger.FullLog.Clear();
+ var funcLoadReq = _functionLoadRequest.Clone();
+ funcLoadReq.Metadata.Directory = AppDomain.CurrentDomain.BaseDirectory;
- CleanupFunctionLoaderStaticPaths();
- FunctionLoader.SetupWellKnownPaths(AppDomain.CurrentDomain.BaseDirectory);
-
- defaultTestManager.InvokeProfile();
+ try
+ {
+ FunctionLoader.SetupWellKnownPaths(funcLoadReq);
+ _testManager.PerformRunspaceLevelInitialization();
- Assert.Single(defaultTestLogger.FullLog);
- Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", defaultTestLogger.FullLog[0]);
+ Assert.Single(_testLogger.FullLog);
+ Assert.Matches("Trace: No 'profile.ps1' is found at the FunctionApp root folder: ", _testLogger.FullLog[0]);
+ }
+ finally
+ {
+ FunctionLoader.SetupWellKnownPaths(_functionLoadRequest);
+ }
}
[Fact]
public void ProfileWithTerminatingError()
{
//initialize fresh log
- defaultTestLogger.FullLog.Clear();
-
- CleanupFunctionLoaderStaticPaths();
- FunctionLoader.SetupWellKnownPaths(System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/ProfileWithTerminatingError"));
-
- Assert.Throws(() => defaultTestManager.InvokeProfile());
- Assert.Single(defaultTestLogger.FullLog);
- Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", defaultTestLogger.FullLog[0]);
+ _testLogger.FullLog.Clear();
+ var funcLoadReq = _functionLoadRequest.Clone();
+ funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithTerminatingError", "Func1");
+
+ 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);
+ }
}
[Fact]
public void ProfileWithNonTerminatingError()
{
//initialize fresh log
- defaultTestLogger.FullLog.Clear();
-
- CleanupFunctionLoaderStaticPaths();
- FunctionLoader.SetupWellKnownPaths(System.IO.Path.Join(
- AppDomain.CurrentDomain.BaseDirectory,
- "Unit/PowerShell/TestScripts/ProfileWithNonTerminatingError"));
-
- defaultTestManager.InvokeProfile();
-
- Assert.Equal(2, defaultTestLogger.FullLog.Count);
- Assert.Equal("Error: ERROR: help me!", defaultTestLogger.FullLog[0]);
- Assert.Matches("Error: Fail to run profile.ps1. See logs for detailed errors. Profile location: ", defaultTestLogger.FullLog[1]);
- }
+ _testLogger.FullLog.Clear();
+ var funcLoadReq = _functionLoadRequest.Clone();
+ funcLoadReq.Metadata.Directory = Path.Join(_functionDirectory, "ProfileWithNonTerminatingError", "Func1");
- // Helper function that sets all the well-known paths in the Function Loader back to null.
- private void CleanupFunctionLoaderStaticPaths()
- {
- FunctionLoader.FunctionAppRootPath = null;
- FunctionLoader.FunctionAppProfilePath = null;
- FunctionLoader.FunctionAppModulesPath = null;
+ try
+ {
+ FunctionLoader.SetupWellKnownPaths(funcLoadReq);
+ _testManager.PerformRunspaceLevelInitialization();
+
+ 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);
+ }
}
}
}
diff --git a/test/Unit/PowerShell/TestScripts/testFunctionWithEntryPoint.ps1 b/test/Unit/PowerShell/TestScripts/testFunctionWithEntryPoint.psm1
similarity index 100%
rename from test/Unit/PowerShell/TestScripts/testFunctionWithEntryPoint.ps1
rename to test/Unit/PowerShell/TestScripts/testFunctionWithEntryPoint.psm1
diff --git a/test/Unit/Utility/TypeExtensionsTests.cs b/test/Unit/Utility/TypeExtensionsTests.cs
index d559ff95..ebf1f9a7 100644
--- a/test/Unit/Utility/TypeExtensionsTests.cs
+++ b/test/Unit/Utility/TypeExtensionsTests.cs
@@ -10,8 +10,6 @@
using System.Management.Automation;
using Google.Protobuf;
-using Google.Protobuf.Collections;
-using Microsoft.Azure.Functions.PowerShellWorker;
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
@@ -22,6 +20,15 @@ namespace Microsoft.Azure.Functions.PowerShellWorker.Test
{
public class TypeExtensionsTests
{
+ private readonly ConsoleLogger _testLogger;
+ private readonly PowerShellManager _testManager;
+
+ public TypeExtensionsTests()
+ {
+ _testLogger = new ConsoleLogger();
+ _testManager = new PowerShellManager(_testLogger);
+ }
+
#region TypedDataToObject
[Fact]
public void TestTypedDataToObjectHttpRequestContextBasic()
@@ -282,6 +289,7 @@ public void TestTypedDataToObjectStream()
Assert.Equal(expected, (byte[])input.ToObject());
}
#endregion
+
#region ExceptionToRpcException
[Fact]
public void TestExceptionToRpcExceptionBasic()
@@ -314,6 +322,7 @@ public void TestExceptionToRpcExceptionExtraData()
Assert.Equal(expected, input.ToRpcException());
}
#endregion
+
#region ObjectToTypedData
[Fact]
public void TestObjectToTypedDataRpcHttpBasic()
@@ -495,10 +504,6 @@ public void TestObjectToTypedDataJsonString()
[Fact]
public void TestObjectToTypedDataJsonHashtable()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
var data = new Hashtable { { "foo", "bar" } };
var input = (object)data;
@@ -507,16 +512,12 @@ public void TestObjectToTypedDataJsonHashtable()
Json = "{\"foo\":\"bar\"}"
};
- Assert.Equal(expected, input.ToTypedData(manager));
+ Assert.Equal(expected, input.ToTypedData(_testManager));
}
[Fact]
public void TestObjectToTypedData_PSObjectToJson_1()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
var data = new Hashtable { { "foo", "bar" } };
object input = PSObject.AsPSObject(data);
@@ -525,16 +526,12 @@ public void TestObjectToTypedData_PSObjectToJson_1()
Json = "{\"foo\":\"bar\"}"
};
- Assert.Equal(expected, input.ToTypedData(manager));
+ Assert.Equal(expected, input.ToTypedData(_testManager));
}
[Fact]
public void TestObjectToTypedData_PSObjectToJson_2()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
using (var ps = System.Management.Automation.PowerShell.Create())
{
object input = ps.AddScript("[pscustomobject]@{foo = 'bar'}").Invoke()[0];
@@ -544,21 +541,17 @@ public void TestObjectToTypedData_PSObjectToJson_2()
Json = "{\"foo\":\"bar\"}"
};
- Assert.Equal(expected, input.ToTypedData(manager));
+ Assert.Equal(expected, input.ToTypedData(_testManager));
}
}
[Fact]
public void TestObjectToTypedData_PSObjectToBytes()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
var data = new byte[] { 12,23,34 };
object input = PSObject.AsPSObject(data);
- TypedData output = input.ToTypedData(manager);
+ TypedData output = input.ToTypedData(_testManager);
Assert.Equal(TypedData.DataOneofCase.Bytes, output.DataCase);
Assert.Equal(3, output.Bytes.Length);
@@ -567,14 +560,10 @@ public void TestObjectToTypedData_PSObjectToBytes()
[Fact]
public void TestObjectToTypedData_PSObjectToStream()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
using (var data = new MemoryStream(new byte[] { 12,23,34 }))
{
object input = PSObject.AsPSObject(data);
- TypedData output = input.ToTypedData(manager);
+ TypedData output = input.ToTypedData(_testManager);
Assert.Equal(TypedData.DataOneofCase.Stream, output.DataCase);
Assert.Equal(3, output.Stream.Length);
@@ -584,12 +573,8 @@ public void TestObjectToTypedData_PSObjectToStream()
[Fact]
public void TestObjectToTypedData_PSObjectToString()
{
- var logger = new ConsoleLogger();
- var manager = new PowerShellManager(logger);
- manager.InitializeRunspace();
-
object input = PSObject.AsPSObject("Hello World");
- TypedData output = input.ToTypedData(manager);
+ TypedData output = input.ToTypedData(_testManager);
Assert.Equal(TypedData.DataOneofCase.String, output.DataCase);
Assert.Equal("Hello World", output.String);