From 536e1af5f924eb74c29adcc5886344c127f23b8f Mon Sep 17 00:00:00 2001 From: MJendza Date: Sun, 3 Jan 2021 01:40:55 +0100 Subject: [PATCH 1/9] alpha version --- .idea/.idea.SandboxFunctions/.idea/.name | 1 + .../.idea/contentModel.xml | 50 +++++++ .../.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 ++ .../.idea.SandboxFunctions/.idea/modules.xml | 8 ++ .../.idea/projectSettingsUpdater.xml | 6 + .../.idea/riderModule.iml | 13 ++ .idea/.idea.SandboxFunctions/.idea/vcs.xml | 6 + .../.idea/workspace.xml | 112 +++++++++++++++ AcceptanceTests/AcceptanceTests.csproj | 32 +++++ AcceptanceTests/CustomerAcceptanceTests.cs | 131 ++++++++++++++++++ AcceptanceTests/local.settings.json | 8 ++ CustomerFunctions/CustomerFunction.cs | 2 +- SandboxFunctions.sln | 8 +- 14 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 .idea/.idea.SandboxFunctions/.idea/.name create mode 100644 .idea/.idea.SandboxFunctions/.idea/contentModel.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/encodings.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/indexLayout.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/modules.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/projectSettingsUpdater.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/riderModule.iml create mode 100644 .idea/.idea.SandboxFunctions/.idea/vcs.xml create mode 100644 .idea/.idea.SandboxFunctions/.idea/workspace.xml create mode 100644 AcceptanceTests/AcceptanceTests.csproj create mode 100644 AcceptanceTests/CustomerAcceptanceTests.cs create mode 100644 AcceptanceTests/local.settings.json diff --git a/.idea/.idea.SandboxFunctions/.idea/.name b/.idea/.idea.SandboxFunctions/.idea/.name new file mode 100644 index 0000000..3d7a599 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/.name @@ -0,0 +1 @@ +SandboxFunctions \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/contentModel.xml b/.idea/.idea.SandboxFunctions/.idea/contentModel.xml new file mode 100644 index 0000000..2b32b15 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/contentModel.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/encodings.xml b/.idea/.idea.SandboxFunctions/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/indexLayout.xml b/.idea/.idea.SandboxFunctions/.idea/indexLayout.xml new file mode 100644 index 0000000..27ba142 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/modules.xml b/.idea/.idea.SandboxFunctions/.idea/modules.xml new file mode 100644 index 0000000..00d3818 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/projectSettingsUpdater.xml b/.idea/.idea.SandboxFunctions/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..4bb9f4d --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/projectSettingsUpdater.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/riderModule.iml b/.idea/.idea.SandboxFunctions/.idea/riderModule.iml new file mode 100644 index 0000000..fbc44a8 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/riderModule.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/vcs.xml b/.idea/.idea.SandboxFunctions/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SandboxFunctions/.idea/workspace.xml b/.idea/.idea.SandboxFunctions/.idea/workspace.xml new file mode 100644 index 0000000..d31c003 --- /dev/null +++ b/.idea/.idea.SandboxFunctions/.idea/workspace.xml @@ -0,0 +1,112 @@ + + + + CustomerFunctions/CustomerFunctions.csproj + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1609631825791 + + + + + + + + + + + + + + + + + file://$PROJECT_DIR$/AcceptanceTests/CustomerAcceptanceTests.cs + 92 + + + + + + + + + + + \ No newline at end of file diff --git a/AcceptanceTests/AcceptanceTests.csproj b/AcceptanceTests/AcceptanceTests.csproj new file mode 100644 index 0000000..9f2ad08 --- /dev/null +++ b/AcceptanceTests/AcceptanceTests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + + + + PreserveNewest + Never + + + + diff --git a/AcceptanceTests/CustomerAcceptanceTests.cs b/AcceptanceTests/CustomerAcceptanceTests.cs new file mode 100644 index 0000000..26a3ea1 --- /dev/null +++ b/AcceptanceTests/CustomerAcceptanceTests.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CustomerFunctions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Extensions.Logging; +using RestSharp; + +namespace AcceptanceTests +{ + public static class TestLogger + { + public static ILogger Create() + { + var logger = new ConsoleUnitLogger(); + return logger; + } + + class ConsoleUnitLogger : ILogger, IDisposable + { + private readonly Action output = Console.WriteLine; + + public void Dispose() + { + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) => output(formatter(state, exception)); + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) => this; + } + } + public abstract class FunctionTest + { + + protected ILogger logger = TestLogger.Create(); + + public HttpRequest HttpRequestSetup(Dictionary query, string body) + { + var reqMock = new Mock(); + + reqMock.Setup(req => req.Query).Returns(new QueryCollection(query)); + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(body); + writer.Flush(); + stream.Position = 0; + reqMock.Setup(req => req.Body).Returns(stream); + return reqMock.Object; + } + + } + + public class AsyncCollector : IAsyncCollector + { + public readonly List Items = new List(); + + public Task AddAsync(T item, CancellationToken cancellationToken = default(CancellationToken)) + { + + Items.Add(item); + + return Task.FromResult(true); + } + + public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(true); + } + + } + + public static class AzureFunctionInvoker + { + public static async Task Invoke(Func> func, params object[] data) + { + switch (Environment.GetEnvironmentVariable("stage")) + { + case "remote": + { + var url = Environment.GetEnvironmentVariable("url"); + var client = new RestClient(); + var request = new RestRequest($"customer", Method.GET); + var result = await client.ExecuteAsync(request); + return new ActionResult(result.Content).Result; + } + case "localhost": + { + var url = $"http://localhost:7071/api/"; + var client = new RestClient(); + var request = new RestRequest($"customer", Method.GET); + var result = await client.ExecuteAsync(request); + return await func(data[0] as HttpRequest); + } + case "debug": + { + return await func(data[0] as HttpRequest); + } + default: + { + throw new ArgumentException("Please set stage variable to run acceptance tests."); + } + } + throw new ArgumentException("Please set stage variable to run acceptance tests."); + } + } + [TestClass] + public class CustomerAcceptanceTests: FunctionTest + { + [TestMethod] + public async Task WhenCallCustomerGet_ShouldReturn201() + { + var query = new Dictionary(); + var body = "{\"name\":\"yamada\"}"; + var req = HttpRequestSetup(query, body); + var result = await AzureFunctionInvoker.Invoke((request) => CustomerFunction.Run(request, logger), req, logger); + var resultObject = (OkObjectResult)result; + Assert.AreEqual("Hello, yamada", resultObject.Value); + } + } +} diff --git a/AcceptanceTests/local.settings.json b/AcceptanceTests/local.settings.json new file mode 100644 index 0000000..6d0998c --- /dev/null +++ b/AcceptanceTests/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "environment": "localhost" + } +} \ No newline at end of file diff --git a/CustomerFunctions/CustomerFunction.cs b/CustomerFunctions/CustomerFunction.cs index f39fb68..1140615 100644 --- a/CustomerFunctions/CustomerFunction.cs +++ b/CustomerFunctions/CustomerFunction.cs @@ -10,7 +10,7 @@ namespace CustomerFunctions { - public static class Function1 + public static class CustomerFunction { [FunctionName("customer")] public static async Task Run( diff --git a/SandboxFunctions.sln b/SandboxFunctions.sln index 2deb33f..2da7eeb 100644 --- a/SandboxFunctions.sln +++ b/SandboxFunctions.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomerFunctions", "CustomerFunctions\CustomerFunctions.csproj", "{47F2C452-9AC5-492B-962E-AB9A4765196F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomerFunctions", "CustomerFunctions\CustomerFunctions.csproj", "{47F2C452-9AC5-492B-962E-AB9A4765196F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AcceptanceTests", "AcceptanceTests\AcceptanceTests.csproj", "{1E18BAC8-E356-467E-9219-562A77D7BED8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {47F2C452-9AC5-492B-962E-AB9A4765196F}.Debug|Any CPU.Build.0 = Debug|Any CPU {47F2C452-9AC5-492B-962E-AB9A4765196F}.Release|Any CPU.ActiveCfg = Release|Any CPU {47F2C452-9AC5-492B-962E-AB9A4765196F}.Release|Any CPU.Build.0 = Release|Any CPU + {1E18BAC8-E356-467E-9219-562A77D7BED8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E18BAC8-E356-467E-9219-562A77D7BED8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E18BAC8-E356-467E-9219-562A77D7BED8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E18BAC8-E356-467E-9219-562A77D7BED8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 6b24fef4d644c79162f9b67731101e998ecc397d Mon Sep 17 00:00:00 2001 From: MJendza Date: Wed, 6 Jan 2021 17:42:41 +0100 Subject: [PATCH 2/9] debug version works --- AcceptanceTests/AcceptanceTests.csproj | 8 +- AcceptanceTests/AzureFunctionInvoker.cs | 104 +++++++++++++++++++++ AcceptanceTests/CustomerAcceptanceTests.cs | 98 +------------------ AcceptanceTests/FunctionTest.cs | 31 ++++++ AcceptanceTests/TestLogger.cs | 30 ++++++ AcceptanceTests/appsettings.json | 10 ++ AcceptanceTests/local.settings.json | 8 -- 7 files changed, 184 insertions(+), 105 deletions(-) create mode 100644 AcceptanceTests/AzureFunctionInvoker.cs create mode 100644 AcceptanceTests/FunctionTest.cs create mode 100644 AcceptanceTests/TestLogger.cs create mode 100644 AcceptanceTests/appsettings.json delete mode 100644 AcceptanceTests/local.settings.json diff --git a/AcceptanceTests/AcceptanceTests.csproj b/AcceptanceTests/AcceptanceTests.csproj index 9f2ad08..0b7cdaa 100644 --- a/AcceptanceTests/AcceptanceTests.csproj +++ b/AcceptanceTests/AcceptanceTests.csproj @@ -23,10 +23,10 @@ - - PreserveNewest - Never - + + + Always + diff --git a/AcceptanceTests/AzureFunctionInvoker.cs b/AcceptanceTests/AzureFunctionInvoker.cs new file mode 100644 index 0000000..fa11157 --- /dev/null +++ b/AcceptanceTests/AzureFunctionInvoker.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using RestSharp; + +namespace AcceptanceTests +{ + public static class AzureFunctionInvoker + { + public static async Task Invoke(Expression>> func, + HttpRequest data) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + + var config = builder.Build(); + var stage = config["environment"]; + switch (stage) + { + case "remote": + { + var url = Environment.GetEnvironmentVariable("url"); + return await RestCall(data, url); + } + case "localhost": + { + var url = $"http://localhost:7071/api/"; + return await RestCall(data, url); + } + case "debug": + { + return await func.Compile().Invoke(data); + } + default: + { + throw new ArgumentException("Please set stage variable to run acceptance tests."); + } + } + } + + private static async Task RestCall(HttpRequest data, string url) + { + if (string.IsNullOrEmpty(url)) + { + throw new ApplicationException("Remote call exception url not defined"); + } + + var client = new RestClient(url); + var request = new RestRequest($"customer", FromString(data.Method)); + StreamReader reader = new StreamReader(data.Body); + string text = await reader.ReadToEndAsync(); + request.AddJsonBody(text); + var result = await client.ExecuteAsync(request); + return new ActionResult(result.Content).Result; + } + + private static Method FromString(string method) + { + switch (method.ToUpper()) + { + case "POST": + return Method.POST; + case "GET": + return Method.GET; + case "PUT": + return Method.PUT; + } + + throw new NotImplementedException($"Not Supported Method: {method}"); + } + + private static FunctionParameters GetParams(Expression>> func) + { + var debug = GetDebugView(func); + return new FunctionParameters() + { + Endpoint = "customer", + Verbs = new[] {"get", "post"} + }; + } + + public static string GetDebugView(Expression exp) + { + if (exp == null) + return null; + + var propertyInfo = + typeof(Expression).GetProperty("DebugView", BindingFlags.Instance | BindingFlags.NonPublic); + return propertyInfo.GetValue(exp) as string; + } + } + + public class FunctionParameters + { + public string Endpoint { get; set; } + public string[] Verbs { get; set; } + } +} \ No newline at end of file diff --git a/AcceptanceTests/CustomerAcceptanceTests.cs b/AcceptanceTests/CustomerAcceptanceTests.cs index 26a3ea1..ec77e04 100644 --- a/AcceptanceTests/CustomerAcceptanceTests.cs +++ b/AcceptanceTests/CustomerAcceptanceTests.cs @@ -1,73 +1,20 @@ -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Primitives; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; using CustomerFunctions; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; -using Microsoft.Extensions.Logging; -using RestSharp; namespace AcceptanceTests { - public static class TestLogger - { - public static ILogger Create() - { - var logger = new ConsoleUnitLogger(); - return logger; - } - - class ConsoleUnitLogger : ILogger, IDisposable - { - private readonly Action output = Console.WriteLine; - - public void Dispose() - { - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func formatter) => output(formatter(state, exception)); - - public bool IsEnabled(LogLevel logLevel) => true; - - public IDisposable BeginScope(TState state) => this; - } - } - public abstract class FunctionTest - { - - protected ILogger logger = TestLogger.Create(); - - public HttpRequest HttpRequestSetup(Dictionary query, string body) - { - var reqMock = new Mock(); - - reqMock.Setup(req => req.Query).Returns(new QueryCollection(query)); - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(body); - writer.Flush(); - stream.Position = 0; - reqMock.Setup(req => req.Body).Returns(stream); - return reqMock.Object; - } - - } - public class AsyncCollector : IAsyncCollector { public readonly List Items = new List(); public Task AddAsync(T item, CancellationToken cancellationToken = default(CancellationToken)) { - Items.Add(item); return Task.FromResult(true); @@ -77,45 +24,10 @@ public class AsyncCollector : IAsyncCollector { return Task.FromResult(true); } - } - public static class AzureFunctionInvoker - { - public static async Task Invoke(Func> func, params object[] data) - { - switch (Environment.GetEnvironmentVariable("stage")) - { - case "remote": - { - var url = Environment.GetEnvironmentVariable("url"); - var client = new RestClient(); - var request = new RestRequest($"customer", Method.GET); - var result = await client.ExecuteAsync(request); - return new ActionResult(result.Content).Result; - } - case "localhost": - { - var url = $"http://localhost:7071/api/"; - var client = new RestClient(); - var request = new RestRequest($"customer", Method.GET); - var result = await client.ExecuteAsync(request); - return await func(data[0] as HttpRequest); - } - case "debug": - { - return await func(data[0] as HttpRequest); - } - default: - { - throw new ArgumentException("Please set stage variable to run acceptance tests."); - } - } - throw new ArgumentException("Please set stage variable to run acceptance tests."); - } - } [TestClass] - public class CustomerAcceptanceTests: FunctionTest + public class CustomerAcceptanceTests : FunctionTest { [TestMethod] public async Task WhenCallCustomerGet_ShouldReturn201() @@ -123,9 +35,9 @@ public async Task WhenCallCustomerGet_ShouldReturn201() var query = new Dictionary(); var body = "{\"name\":\"yamada\"}"; var req = HttpRequestSetup(query, body); - var result = await AzureFunctionInvoker.Invoke((request) => CustomerFunction.Run(request, logger), req, logger); - var resultObject = (OkObjectResult)result; - Assert.AreEqual("Hello, yamada", resultObject.Value); + var result = await AzureFunctionInvoker.Invoke((request) => CustomerFunction.Run(request, logger), req); + var resultObject = (OkObjectResult) result; + Assert.AreEqual("Hello, yamada. This HTTP triggered function executed successfully.", resultObject.Value); } } -} +} \ No newline at end of file diff --git a/AcceptanceTests/FunctionTest.cs b/AcceptanceTests/FunctionTest.cs new file mode 100644 index 0000000..6094f9d --- /dev/null +++ b/AcceptanceTests/FunctionTest.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Moq; + +namespace AcceptanceTests +{ + public abstract class FunctionTest + { + + protected ILogger logger = TestLogger.Create(); + + public HttpRequest HttpRequestSetup(Dictionary query, string body) + { + var reqMock = new Mock(); + + reqMock.Setup(req => req.Query).Returns(new QueryCollection(query)); + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(body); + writer.Flush(); + stream.Position = 0; + reqMock.Setup(req => req.Body).Returns(stream); + return reqMock.Object; + } + + } +} \ No newline at end of file diff --git a/AcceptanceTests/TestLogger.cs b/AcceptanceTests/TestLogger.cs new file mode 100644 index 0000000..48342d8 --- /dev/null +++ b/AcceptanceTests/TestLogger.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace AcceptanceTests +{ + public static class TestLogger + { + public static ILogger Create() + { + var logger = new ConsoleUnitLogger(); + return logger; + } + + class ConsoleUnitLogger : ILogger, IDisposable + { + private readonly Action output = Console.WriteLine; + + public void Dispose() + { + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) => output(formatter(state, exception)); + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) => this; + } + } +} \ No newline at end of file diff --git a/AcceptanceTests/appsettings.json b/AcceptanceTests/appsettings.json new file mode 100644 index 0000000..73e9de5 --- /dev/null +++ b/AcceptanceTests/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "environment": "debug" +} \ No newline at end of file diff --git a/AcceptanceTests/local.settings.json b/AcceptanceTests/local.settings.json deleted file mode 100644 index 6d0998c..0000000 --- a/AcceptanceTests/local.settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "environment": "localhost" - } -} \ No newline at end of file From c0e88871672ea0c333fab3ad437582a664dc88de Mon Sep 17 00:00:00 2001 From: MJendza Date: Wed, 6 Jan 2021 21:49:32 +0100 Subject: [PATCH 3/9] dedicated project for shared package, powershell works need to improve the call, should beginInvoke and do not wait for end --- AcceptanceTests/AcceptanceTests.csproj | 4 + AcceptanceTests/AzureFunctionInvoker.cs | 6 +- AcceptanceTests/CustomerAcceptanceTests.cs | 2 +- AcceptanceTests/FunctionTest.cs | 3 +- AcceptanceTests/appsettings.Debug.json | 10 ++ AcceptanceTests/appsettings.json | 2 +- .../AzureFunctionCliInvoker.cs | 36 ++++ ...zureFunctions.AcceptanceTest.Runner.csproj | 11 ++ .../HostedRunspace.cs | 155 ++++++++++++++++++ SandboxFunctions.sln | 6 + 10 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 AcceptanceTests/appsettings.Debug.json create mode 100644 AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs create mode 100644 AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj create mode 100644 AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs diff --git a/AcceptanceTests/AcceptanceTests.csproj b/AcceptanceTests/AcceptanceTests.csproj index 0b7cdaa..b915bc5 100644 --- a/AcceptanceTests/AcceptanceTests.csproj +++ b/AcceptanceTests/AcceptanceTests.csproj @@ -19,10 +19,14 @@ + + + Always + Always diff --git a/AcceptanceTests/AzureFunctionInvoker.cs b/AcceptanceTests/AzureFunctionInvoker.cs index fa11157..2bd7395 100644 --- a/AcceptanceTests/AzureFunctionInvoker.cs +++ b/AcceptanceTests/AzureFunctionInvoker.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using AzureFunctions.AcceptanceTest.Runner; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -25,12 +26,15 @@ public static async Task Invoke(Expression(); var body = "{\"name\":\"yamada\"}"; - var req = HttpRequestSetup(query, body); + var req = HttpRequestSetup(query, body, "get"); var result = await AzureFunctionInvoker.Invoke((request) => CustomerFunction.Run(request, logger), req); var resultObject = (OkObjectResult) result; Assert.AreEqual("Hello, yamada. This HTTP triggered function executed successfully.", resultObject.Value); diff --git a/AcceptanceTests/FunctionTest.cs b/AcceptanceTests/FunctionTest.cs index 6094f9d..95e0904 100644 --- a/AcceptanceTests/FunctionTest.cs +++ b/AcceptanceTests/FunctionTest.cs @@ -13,7 +13,7 @@ public abstract class FunctionTest protected ILogger logger = TestLogger.Create(); - public HttpRequest HttpRequestSetup(Dictionary query, string body) + public HttpRequest HttpRequestSetup(Dictionary query, string body, string verb) { var reqMock = new Mock(); @@ -24,6 +24,7 @@ public HttpRequest HttpRequestSetup(Dictionary query, stri writer.Flush(); stream.Position = 0; reqMock.Setup(req => req.Body).Returns(stream); + reqMock.Setup(x => x.Method).Returns(verb); return reqMock.Object; } diff --git a/AcceptanceTests/appsettings.Debug.json b/AcceptanceTests/appsettings.Debug.json new file mode 100644 index 0000000..73e9de5 --- /dev/null +++ b/AcceptanceTests/appsettings.Debug.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "environment": "debug" +} \ No newline at end of file diff --git a/AcceptanceTests/appsettings.json b/AcceptanceTests/appsettings.json index 73e9de5..8f7d855 100644 --- a/AcceptanceTests/appsettings.json +++ b/AcceptanceTests/appsettings.json @@ -6,5 +6,5 @@ "Microsoft": "Information" } }, - "environment": "debug" + "environment": "localhost" } \ No newline at end of file diff --git a/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs new file mode 100644 index 0000000..43e23cf --- /dev/null +++ b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace AzureFunctions.AcceptanceTest.Runner +{ + public class AzureFunctionCliInvoker + { + private string RunFunction => "func start --build"; + + public async Task RunAzureFunction() + { + using var hosted = new HostedRunspace(); + var scriptContents = new StringBuilder(); + scriptContents.AppendLine("Param($StrParam, $IntParam)"); + scriptContents.AppendLine(""); + scriptContents.AppendLine("Write-Output \"Starting script\""); + scriptContents.AppendLine("Write-Output \"This is the value from the first param: $StrParam\""); + scriptContents.AppendLine("Write-Output \"This is the value from the second param: $IntParam\""); + scriptContents.AppendLine(""); + scriptContents.AppendLine(@"set-location ..\..\..\..\CustomerFunctions\"); + scriptContents.AppendLine(RunFunction); + scriptContents.AppendLine(""); + + var scriptParameters = new Dictionary() + { + { "StrParam", "Hello from script" }, + { "IntParam", 7 } + }; + + var result = await hosted.RunScript(scriptContents.ToString(), scriptParameters); + Console.Write($"PowerShell result: {string.Join(Environment.NewLine, result)}"); + } + } +} \ No newline at end of file diff --git a/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj b/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj new file mode 100644 index 0000000..8c63a9d --- /dev/null +++ b/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs b/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs new file mode 100644 index 0000000..2f7db60 --- /dev/null +++ b/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading.Tasks; + +namespace AzureFunctions.AcceptanceTest.Runner +{ + /// + /// Contains functionality for executing PowerShell scripts. + /// + public class HostedRunspace : IDisposable + { + public HostedRunspace() + { + InitializeRunspaces(2, 10, new string[]{}); + } + /// + /// The PowerShell runspace pool. + /// + private RunspacePool RsPool { get; set; } + + /// + /// Initialize the runspace pool. + /// + /// + /// + public void InitializeRunspaces(int minRunspaces, int maxRunspaces, string[] modulesToLoad) + { + // create the default session state. + // session state can be used to set things like execution policy, language constraints, etc. + // optionally load any modules (by name) that were supplied. + + var defaultSessionState = InitialSessionState.CreateDefault(); + defaultSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; + + foreach (var moduleName in modulesToLoad) + { + defaultSessionState.ImportPSModule(moduleName); + } + + // use the runspace factory to create a pool of runspaces + // with a minimum and maximum number of runspaces to maintain. + + RsPool = RunspaceFactory.CreateRunspacePool(defaultSessionState); + RsPool.SetMinRunspaces(minRunspaces); + RsPool.SetMaxRunspaces(maxRunspaces); + + // set the pool options for thread use. + // we can throw away or re-use the threads depending on the usage scenario. + + RsPool.ThreadOptions = PSThreadOptions.UseNewThread; + + // open the pool. + // this will start by initializing the minimum number of runspaces. + + RsPool.Open(); + } + + /// + /// Runs a PowerShell script with parameters and prints the resulting pipeline objects to the console output. + /// + /// The script file contents. + /// A dictionary of parameter names and parameter values. + public async Task> RunScript(string scriptContents, Dictionary scriptParameters) + { + if (RsPool == null) + { + throw new ApplicationException("Runspace Pool must be initialized before calling RunScript()."); + } + + // create a new hosted PowerShell instance using a custom runspace. + // wrap in a using statement to ensure resources are cleaned up. + using (PowerShell ps = PowerShell.Create()) + { + // use the runspace pool. + ps.RunspacePool = RsPool; + + // specify the script code to run. + ps.AddScript(scriptContents); + + // specify the parameters to pass into the script. + ps.AddParameters(scriptParameters); + + // subscribe to events from some of the streams + ps.Streams.Error.DataAdded += Error_DataAdded; + ps.Streams.Warning.DataAdded += Warning_DataAdded; + ps.Streams.Information.DataAdded += Information_DataAdded; + + // execute the script and await the result. + var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false); + + // print the resulting pipeline objects to the console. + Console.WriteLine("----- Pipeline Output below this point -----"); + foreach (var item in pipelineObjects) + { + Console.WriteLine(item.BaseObject.ToString()); + } + + return pipelineObjects; + } + } + + /// + /// Handles data-added events for the information stream. + /// + /// + /// Note: Write-Host and Write-Information messages will end up in the information stream. + /// + /// + /// + private void Information_DataAdded(object sender, DataAddedEventArgs e) + { + var streamObjectsReceived = sender as PSDataCollection; + var currentStreamRecord = streamObjectsReceived[e.Index]; + + Console.WriteLine($"InfoStreamEvent: {currentStreamRecord.MessageData}"); + } + + /// + /// Handles data-added events for the warning stream. + /// + /// + /// + private void Warning_DataAdded(object sender, DataAddedEventArgs e) + { + var streamObjectsReceived = sender as PSDataCollection; + var currentStreamRecord = streamObjectsReceived[e.Index]; + + Console.WriteLine($"WarningStreamEvent: {currentStreamRecord.Message}"); + } + + /// + /// Handles data-added events for the error stream. + /// + /// + /// Note: Uncaught terminating errors will stop the pipeline completely. + /// Non-terminating errors will be written to this stream and execution will continue. + /// + /// + /// + private void Error_DataAdded(object sender, DataAddedEventArgs e) + { + var streamObjectsReceived = sender as PSDataCollection; + var currentStreamRecord = streamObjectsReceived[e.Index]; + + Console.WriteLine($"ErrorStreamEvent: {currentStreamRecord.Exception}"); + } + + public void Dispose() + { + RsPool.Close(); + } + } +} \ No newline at end of file diff --git a/SandboxFunctions.sln b/SandboxFunctions.sln index 2da7eeb..fb088de 100644 --- a/SandboxFunctions.sln +++ b/SandboxFunctions.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomerFunctions", "Custom EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AcceptanceTests", "AcceptanceTests\AcceptanceTests.csproj", "{1E18BAC8-E356-467E-9219-562A77D7BED8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunctions.AcceptanceTest.Runner", "AzureFunctions.AcceptanceTest.Runner\AzureFunctions.AcceptanceTest.Runner.csproj", "{1C603F55-2144-4B7B-89AE-4E2E5FE82AF7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {1E18BAC8-E356-467E-9219-562A77D7BED8}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E18BAC8-E356-467E-9219-562A77D7BED8}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E18BAC8-E356-467E-9219-562A77D7BED8}.Release|Any CPU.Build.0 = Release|Any CPU + {1C603F55-2144-4B7B-89AE-4E2E5FE82AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C603F55-2144-4B7B-89AE-4E2E5FE82AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C603F55-2144-4B7B-89AE-4E2E5FE82AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C603F55-2144-4B7B-89AE-4E2E5FE82AF7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 096401eea59e4742a1475ce06ccb89fa82c1dee5 Mon Sep 17 00:00:00 2001 From: MJendza Date: Wed, 6 Jan 2021 23:35:49 +0100 Subject: [PATCH 4/9] try to run async as sync --- AcceptanceTests/AzureFunctionInvoker.cs | 4 +- AcceptanceTests/FunctionTest.cs | 14 ++- .../AzureFunctionCliInvoker.cs | 93 +++++++++++++++++-- .../HostedRunspace.cs | 34 +++++-- 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/AcceptanceTests/AzureFunctionInvoker.cs b/AcceptanceTests/AzureFunctionInvoker.cs index 2bd7395..a0c427c 100644 --- a/AcceptanceTests/AzureFunctionInvoker.cs +++ b/AcceptanceTests/AzureFunctionInvoker.cs @@ -35,7 +35,9 @@ public static async Task Invoke(Expression query, string body, string verb) diff --git a/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs index 43e23cf..4bca90b 100644 --- a/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs +++ b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs @@ -1,17 +1,21 @@ using System; using System.Collections.Generic; +using System.Management.Automation; using System.Text; +using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; namespace AzureFunctions.AcceptanceTest.Runner { public class AzureFunctionCliInvoker { + private HostedRunspace _hosted; private string RunFunction => "func start --build"; public async Task RunAzureFunction() { - using var hosted = new HostedRunspace(); + _hosted = new HostedRunspace(); var scriptContents = new StringBuilder(); scriptContents.AppendLine("Param($StrParam, $IntParam)"); scriptContents.AppendLine(""); @@ -25,12 +29,89 @@ public async Task RunAzureFunction() var scriptParameters = new Dictionary() { - { "StrParam", "Hello from script" }, - { "IntParam", 7 } + {"StrParam", "Hello from script"}, + {"IntParam", 7} }; + var options = new PowerShellParams() + { + ScriptContents = scriptContents.ToString(), + ScriptParameters = scriptParameters + }; + await RunAndWaitToFunction(options) ; + + //Console.Write($"PowerShell result: {string.Join(Environment.NewLine, result)}"); + } + + private async Task RunAndWaitToFunction(PowerShellParams options) + { + + var tcs = new TaskCompletionSource(); + options.ErrorAdded += (sender, eventArgs) => + { + var list = (PSDataCollection)sender; + var message = list[eventArgs.Index]; + TrySetResult(tcs, message.Exception?.Message ?? message.TargetObject?.ToString()); + }; + + var psCallTask = _hosted.RunScript(options) + .ContinueWith(result => + { + return result.Result; + }, new CancellationToken(), TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); + + string resultMessage = string.Empty; + try + { + var readyTask = await Task.WhenAny(psCallTask, tcs.Task); + resultMessage = readyTask == tcs.Task ? tcs.Task.Result : string.Join(Environment.NewLine, psCallTask.Result); + } + catch (Exception ex) + { + if (ex.InnerException is PSInvalidOperationException) + { + throw ex.InnerException; + } + else if (IsError(ex.Message)) + { + throw; + } + } + + return resultMessage; + + } + private void TrySetResult(TaskCompletionSource tcs, string data) + { + if (IsFunctionStarted(data) && !tcs.Task.IsCompleted) + { + //Log.Write(this, TraceLevel.Info, $"powershell az login partial result - {data}"); + tcs.SetResult(data); + return; + } + + if (IsError(data)) + { + //Log.Write(this, TraceLevel.Info, $"return error from current execution - {data}"); + if (!tcs.Task.IsCompleted) + tcs.SetException(new PSInvalidOperationException(data)); - var result = await hosted.RunScript(scriptContents.ToString(), scriptParameters); - Console.Write($"PowerShell result: {string.Join(Environment.NewLine, result)}"); - } + } + + //Log.Write(this, TraceLevel.Info, $"add to error list - {data}"); + } + + private bool IsFunctionStarted(string data) + { + return data.Contains("For detailed output, run func with --verbose flag."); + } + + private bool IsError(string message) + { + return !string.IsNullOrEmpty(message) && message.Contains("error", StringComparison.OrdinalIgnoreCase); + } + public void End() + { + _hosted.Finish(); + } } } \ No newline at end of file diff --git a/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs b/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs index 2f7db60..64db0d3 100644 --- a/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs +++ b/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs @@ -6,11 +6,19 @@ namespace AzureFunctions.AcceptanceTest.Runner { + public class PowerShellParams + { + public string ScriptContents { get; set; } + public Dictionary ScriptParameters { get; set; } + public EventHandler ErrorAdded { get; set; } + } /// /// Contains functionality for executing PowerShell scripts. /// - public class HostedRunspace : IDisposable + public class HostedRunspace { + private IAsyncResult _result; + public HostedRunspace() { InitializeRunspaces(2, 10, new string[]{}); @@ -19,7 +27,7 @@ public HostedRunspace() /// The PowerShell runspace pool. /// private RunspacePool RsPool { get; set; } - + private PowerShell ps { get; set; } /// /// Initialize the runspace pool. /// @@ -62,7 +70,7 @@ public void InitializeRunspaces(int minRunspaces, int maxRunspaces, string[] mod /// /// The script file contents. /// A dictionary of parameter names and parameter values. - public async Task> RunScript(string scriptContents, Dictionary scriptParameters) + public async Task> RunScript(PowerShellParams options) { if (RsPool == null) { @@ -71,23 +79,28 @@ public async Task> RunScript(string scriptContents, D // create a new hosted PowerShell instance using a custom runspace. // wrap in a using statement to ensure resources are cleaned up. - using (PowerShell ps = PowerShell.Create()) - { + ps = PowerShell.Create(); + // use the runspace pool. ps.RunspacePool = RsPool; // specify the script code to run. - ps.AddScript(scriptContents); + ps.AddScript(options.ScriptContents); // specify the parameters to pass into the script. - ps.AddParameters(scriptParameters); + ps.AddParameters(options.ScriptParameters); // subscribe to events from some of the streams ps.Streams.Error.DataAdded += Error_DataAdded; + ps.Streams.Error.DataAdded += options.ErrorAdded; ps.Streams.Warning.DataAdded += Warning_DataAdded; + ps.Streams.Warning.DataAdded += options.ErrorAdded; ps.Streams.Information.DataAdded += Information_DataAdded; + ps.Streams.Information.DataAdded += options.ErrorAdded; + // execute the script and await the result. + var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false); // print the resulting pipeline objects to the console. @@ -98,8 +111,8 @@ public async Task> RunScript(string scriptContents, D } return pipelineObjects; - } - } + } + /// /// Handles data-added events for the information stream. @@ -147,8 +160,9 @@ private void Error_DataAdded(object sender, DataAddedEventArgs e) Console.WriteLine($"ErrorStreamEvent: {currentStreamRecord.Exception}"); } - public void Dispose() + public void Finish() { + ps.Stop(); RsPool.Close(); } } From 44c52704e7ac8c2624123a22f7d5ef8451b27552 Mon Sep 17 00:00:00 2001 From: MJendza Date: Fri, 8 Jan 2021 23:02:14 +0100 Subject: [PATCH 5/9] cleaned up and move shared code to library --- AcceptanceTests/CustomerAcceptanceTests.cs | 20 +-- .../AzureFunctionCliInvoker.cs | 117 ------------ .../AzureFunctionInvoker.cs | 32 +--- ...zureFunctions.AcceptanceTest.Runner.csproj | 9 + .../FunctionTest.cs | 18 +- .../HostedRunspace.cs | 169 ------------------ .../TestLogger.cs | 2 +- 7 files changed, 16 insertions(+), 351 deletions(-) delete mode 100644 AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs rename {AcceptanceTests => AzureFunctions.AcceptanceTest.Runner}/AzureFunctionInvoker.cs (70%) rename {AcceptanceTests => AzureFunctions.AcceptanceTest.Runner}/FunctionTest.cs (64%) delete mode 100644 AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs rename {AcceptanceTests => AzureFunctions.AcceptanceTest.Runner}/TestLogger.cs (94%) diff --git a/AcceptanceTests/CustomerAcceptanceTests.cs b/AcceptanceTests/CustomerAcceptanceTests.cs index 749f2ae..ea0feb1 100644 --- a/AcceptanceTests/CustomerAcceptanceTests.cs +++ b/AcceptanceTests/CustomerAcceptanceTests.cs @@ -3,31 +3,15 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using AzureFunctions.AcceptanceTest.Runner; using CustomerFunctions; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; namespace AcceptanceTests { - public class AsyncCollector : IAsyncCollector - { - public readonly List Items = new List(); - - public Task AddAsync(T item, CancellationToken cancellationToken = default(CancellationToken)) - { - Items.Add(item); - - return Task.FromResult(true); - } - - public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(true); - } - } - [TestClass] - public class CustomerAcceptanceTests : FunctionTest + public class CustomerAcceptanceTests : BaseFunctionAcceptanceTests { [TestMethod] public async Task WhenCallCustomerGet_ShouldReturn201() diff --git a/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs deleted file mode 100644 index 4bca90b..0000000 --- a/AzureFunctions.AcceptanceTest.Runner/AzureFunctionCliInvoker.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Management.Automation; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace AzureFunctions.AcceptanceTest.Runner -{ - public class AzureFunctionCliInvoker - { - private HostedRunspace _hosted; - private string RunFunction => "func start --build"; - - public async Task RunAzureFunction() - { - _hosted = new HostedRunspace(); - var scriptContents = new StringBuilder(); - scriptContents.AppendLine("Param($StrParam, $IntParam)"); - scriptContents.AppendLine(""); - scriptContents.AppendLine("Write-Output \"Starting script\""); - scriptContents.AppendLine("Write-Output \"This is the value from the first param: $StrParam\""); - scriptContents.AppendLine("Write-Output \"This is the value from the second param: $IntParam\""); - scriptContents.AppendLine(""); - scriptContents.AppendLine(@"set-location ..\..\..\..\CustomerFunctions\"); - scriptContents.AppendLine(RunFunction); - scriptContents.AppendLine(""); - - var scriptParameters = new Dictionary() - { - {"StrParam", "Hello from script"}, - {"IntParam", 7} - }; - var options = new PowerShellParams() - { - ScriptContents = scriptContents.ToString(), - ScriptParameters = scriptParameters - }; - await RunAndWaitToFunction(options) ; - - //Console.Write($"PowerShell result: {string.Join(Environment.NewLine, result)}"); - } - - private async Task RunAndWaitToFunction(PowerShellParams options) - { - - var tcs = new TaskCompletionSource(); - options.ErrorAdded += (sender, eventArgs) => - { - var list = (PSDataCollection)sender; - var message = list[eventArgs.Index]; - TrySetResult(tcs, message.Exception?.Message ?? message.TargetObject?.ToString()); - }; - - var psCallTask = _hosted.RunScript(options) - .ContinueWith(result => - { - return result.Result; - }, new CancellationToken(), TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); - - string resultMessage = string.Empty; - try - { - var readyTask = await Task.WhenAny(psCallTask, tcs.Task); - resultMessage = readyTask == tcs.Task ? tcs.Task.Result : string.Join(Environment.NewLine, psCallTask.Result); - } - catch (Exception ex) - { - if (ex.InnerException is PSInvalidOperationException) - { - throw ex.InnerException; - } - else if (IsError(ex.Message)) - { - throw; - } - } - - return resultMessage; - - } - private void TrySetResult(TaskCompletionSource tcs, string data) - { - if (IsFunctionStarted(data) && !tcs.Task.IsCompleted) - { - //Log.Write(this, TraceLevel.Info, $"powershell az login partial result - {data}"); - tcs.SetResult(data); - return; - } - - if (IsError(data)) - { - //Log.Write(this, TraceLevel.Info, $"return error from current execution - {data}"); - if (!tcs.Task.IsCompleted) - tcs.SetException(new PSInvalidOperationException(data)); - - } - - //Log.Write(this, TraceLevel.Info, $"add to error list - {data}"); - } - - private bool IsFunctionStarted(string data) - { - return data.Contains("For detailed output, run func with --verbose flag."); - } - - private bool IsError(string message) - { - return !string.IsNullOrEmpty(message) && message.Contains("error", StringComparison.OrdinalIgnoreCase); - } - public void End() - { - _hosted.Finish(); - } - } -} \ No newline at end of file diff --git a/AcceptanceTests/AzureFunctionInvoker.cs b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionInvoker.cs similarity index 70% rename from AcceptanceTests/AzureFunctionInvoker.cs rename to AzureFunctions.AcceptanceTest.Runner/AzureFunctionInvoker.cs index a0c427c..b511c3a 100644 --- a/AcceptanceTests/AzureFunctionInvoker.cs +++ b/AzureFunctions.AcceptanceTest.Runner/AzureFunctionInvoker.cs @@ -1,15 +1,13 @@ using System; using System.IO; using System.Linq.Expressions; -using System.Reflection; using System.Threading.Tasks; -using AzureFunctions.AcceptanceTest.Runner; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using RestSharp; -namespace AcceptanceTests +namespace AzureFunctions.AcceptanceTest.Runner { public static class AzureFunctionInvoker { @@ -31,13 +29,7 @@ public static async Task Invoke(Expression>> func) - { - var debug = GetDebugView(func); - return new FunctionParameters() - { - Endpoint = "customer", - Verbs = new[] {"get", "post"} - }; - } - - public static string GetDebugView(Expression exp) - { - if (exp == null) - return null; - - var propertyInfo = - typeof(Expression).GetProperty("DebugView", BindingFlags.Instance | BindingFlags.NonPublic); - return propertyInfo.GetValue(exp) as string; - } } public class FunctionParameters diff --git a/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj b/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj index 8c63a9d..7372176 100644 --- a/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj +++ b/AzureFunctions.AcceptanceTest.Runner/AzureFunctions.AcceptanceTest.Runner.csproj @@ -5,7 +5,16 @@ + + + + + + + + + diff --git a/AcceptanceTests/FunctionTest.cs b/AzureFunctions.AcceptanceTest.Runner/FunctionTest.cs similarity index 64% rename from AcceptanceTests/FunctionTest.cs rename to AzureFunctions.AcceptanceTest.Runner/FunctionTest.cs index be132a8..e4bf6f4 100644 --- a/AcceptanceTests/FunctionTest.cs +++ b/AzureFunctions.AcceptanceTest.Runner/FunctionTest.cs @@ -1,28 +1,15 @@ using System.Collections.Generic; using System.IO; -using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace AcceptanceTests +namespace AzureFunctions.AcceptanceTest.Runner { - public abstract class FunctionTest + public abstract class BaseFunctionAcceptanceTests { - [ClassInitialize] - public void ClassSetUp() - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - } - - [TestInitialize] - public void TestSetUp() - { - SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); - } protected ILogger logger = TestLogger.Create(); public HttpRequest HttpRequestSetup(Dictionary query, string body, string verb) @@ -39,6 +26,5 @@ public HttpRequest HttpRequestSetup(Dictionary query, stri reqMock.Setup(x => x.Method).Returns(verb); return reqMock.Object; } - } } \ No newline at end of file diff --git a/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs b/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs deleted file mode 100644 index 64db0d3..0000000 --- a/AzureFunctions.AcceptanceTest.Runner/HostedRunspace.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Threading.Tasks; - -namespace AzureFunctions.AcceptanceTest.Runner -{ - public class PowerShellParams - { - public string ScriptContents { get; set; } - public Dictionary ScriptParameters { get; set; } - public EventHandler ErrorAdded { get; set; } - } - /// - /// Contains functionality for executing PowerShell scripts. - /// - public class HostedRunspace - { - private IAsyncResult _result; - - public HostedRunspace() - { - InitializeRunspaces(2, 10, new string[]{}); - } - /// - /// The PowerShell runspace pool. - /// - private RunspacePool RsPool { get; set; } - private PowerShell ps { get; set; } - /// - /// Initialize the runspace pool. - /// - /// - /// - public void InitializeRunspaces(int minRunspaces, int maxRunspaces, string[] modulesToLoad) - { - // create the default session state. - // session state can be used to set things like execution policy, language constraints, etc. - // optionally load any modules (by name) that were supplied. - - var defaultSessionState = InitialSessionState.CreateDefault(); - defaultSessionState.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; - - foreach (var moduleName in modulesToLoad) - { - defaultSessionState.ImportPSModule(moduleName); - } - - // use the runspace factory to create a pool of runspaces - // with a minimum and maximum number of runspaces to maintain. - - RsPool = RunspaceFactory.CreateRunspacePool(defaultSessionState); - RsPool.SetMinRunspaces(minRunspaces); - RsPool.SetMaxRunspaces(maxRunspaces); - - // set the pool options for thread use. - // we can throw away or re-use the threads depending on the usage scenario. - - RsPool.ThreadOptions = PSThreadOptions.UseNewThread; - - // open the pool. - // this will start by initializing the minimum number of runspaces. - - RsPool.Open(); - } - - /// - /// Runs a PowerShell script with parameters and prints the resulting pipeline objects to the console output. - /// - /// The script file contents. - /// A dictionary of parameter names and parameter values. - public async Task> RunScript(PowerShellParams options) - { - if (RsPool == null) - { - throw new ApplicationException("Runspace Pool must be initialized before calling RunScript()."); - } - - // create a new hosted PowerShell instance using a custom runspace. - // wrap in a using statement to ensure resources are cleaned up. - ps = PowerShell.Create(); - - // use the runspace pool. - ps.RunspacePool = RsPool; - - // specify the script code to run. - ps.AddScript(options.ScriptContents); - - // specify the parameters to pass into the script. - ps.AddParameters(options.ScriptParameters); - - // subscribe to events from some of the streams - ps.Streams.Error.DataAdded += Error_DataAdded; - ps.Streams.Error.DataAdded += options.ErrorAdded; - ps.Streams.Warning.DataAdded += Warning_DataAdded; - ps.Streams.Warning.DataAdded += options.ErrorAdded; - ps.Streams.Information.DataAdded += Information_DataAdded; - ps.Streams.Information.DataAdded += options.ErrorAdded; - - - // execute the script and await the result. - - var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false); - - // print the resulting pipeline objects to the console. - Console.WriteLine("----- Pipeline Output below this point -----"); - foreach (var item in pipelineObjects) - { - Console.WriteLine(item.BaseObject.ToString()); - } - - return pipelineObjects; - } - - - /// - /// Handles data-added events for the information stream. - /// - /// - /// Note: Write-Host and Write-Information messages will end up in the information stream. - /// - /// - /// - private void Information_DataAdded(object sender, DataAddedEventArgs e) - { - var streamObjectsReceived = sender as PSDataCollection; - var currentStreamRecord = streamObjectsReceived[e.Index]; - - Console.WriteLine($"InfoStreamEvent: {currentStreamRecord.MessageData}"); - } - - /// - /// Handles data-added events for the warning stream. - /// - /// - /// - private void Warning_DataAdded(object sender, DataAddedEventArgs e) - { - var streamObjectsReceived = sender as PSDataCollection; - var currentStreamRecord = streamObjectsReceived[e.Index]; - - Console.WriteLine($"WarningStreamEvent: {currentStreamRecord.Message}"); - } - - /// - /// Handles data-added events for the error stream. - /// - /// - /// Note: Uncaught terminating errors will stop the pipeline completely. - /// Non-terminating errors will be written to this stream and execution will continue. - /// - /// - /// - private void Error_DataAdded(object sender, DataAddedEventArgs e) - { - var streamObjectsReceived = sender as PSDataCollection; - var currentStreamRecord = streamObjectsReceived[e.Index]; - - Console.WriteLine($"ErrorStreamEvent: {currentStreamRecord.Exception}"); - } - - public void Finish() - { - ps.Stop(); - RsPool.Close(); - } - } -} \ No newline at end of file diff --git a/AcceptanceTests/TestLogger.cs b/AzureFunctions.AcceptanceTest.Runner/TestLogger.cs similarity index 94% rename from AcceptanceTests/TestLogger.cs rename to AzureFunctions.AcceptanceTest.Runner/TestLogger.cs index 48342d8..461f984 100644 --- a/AcceptanceTests/TestLogger.cs +++ b/AzureFunctions.AcceptanceTest.Runner/TestLogger.cs @@ -1,7 +1,7 @@ using System; using Microsoft.Extensions.Logging; -namespace AcceptanceTests +namespace AzureFunctions.AcceptanceTest.Runner { public static class TestLogger { From 41a85e38ce39cd642e7077cc2a7837605efc0700 Mon Sep 17 00:00:00 2001 From: MJendza Date: Mon, 18 Jan 2021 21:39:55 +0100 Subject: [PATCH 6/9] try to build and publish nuget --- .github/workflows/build.yaml | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e3c6f54..4ad0eee 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,4 +25,43 @@ jobs: with: name: functions path: ${{ env.OUTPUT_PATH }} + publish: + needs: [build] + runs-on: ubuntu-latest + name: ci/github/publish + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get Version + id: get_version + run: | + echo "::set-output name=branch::${GITHUB_REF:10}" + + dotnet tool restore + version=$(dotnet tool run minver -- --tag-prefix=v) + echo "::set-output name=version::${version}" + - shell: bash + run: | + git fetch --prune --unshallow + - name: Setup Dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Dotnet Pack + shell: bash + run: | + dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release --output=./packages \ + + - name: Publish Artifacts + uses: actions/upload-artifact@v1 + with: + path: packages + name: nuget-packages + - name: Dotnet Push to Nuget.org + shell: bash + if: contains(steps.get_version.outputs.branch, 'v') + run: | + dotnet tool restore + find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json + \ No newline at end of file From 9dac0e38bdee8a0761cf290d43fb4d9a228efb1f Mon Sep 17 00:00:00 2001 From: MJendza Date: Mon, 18 Jan 2021 21:46:43 +0100 Subject: [PATCH 7/9] added vulnerability-scan --- .github/workflows/build.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4ad0eee..dcf075b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,6 +6,18 @@ env: on: [push] jobs: + vulnerability-scan: + runs-on: ubuntu-latest + name: ci/github/scan-vulnerabilities + container: mcr.microsoft.com/dotnet/core/sdk:3.1-bionic + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Scan for Vulnerabilities + run: | + dotnet tool restore + dotnet restore + dotnet tool run dotnet-retire build: runs-on: ubuntu-latest steps: @@ -26,7 +38,7 @@ jobs: name: functions path: ${{ env.OUTPUT_PATH }} publish: - needs: [build] + needs: [vulnerability-scan, build] runs-on: ubuntu-latest name: ci/github/publish steps: From 15808eb17a2c2b0ce563286579705b2a30aa5a67 Mon Sep 17 00:00:00 2001 From: MJendza Date: Mon, 18 Jan 2021 21:53:38 +0100 Subject: [PATCH 8/9] added tools --- .github/workflows/.config/dotnet-tools.json | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/.config/dotnet-tools.json diff --git a/.github/workflows/.config/dotnet-tools.json b/.github/workflows/.config/dotnet-tools.json new file mode 100644 index 0000000..0180b9f --- /dev/null +++ b/.github/workflows/.config/dotnet-tools.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "minver-cli": { + "version": "2.2.0", + "commands": [ + "minver" + ] + }, + "dotnet-retire": { + "version": "4.0.1", + "commands": [ + "dotnet-retire" + ] + }, + "gpr": { + "version": "0.1.122", + "commands": [ + "gpr" + ] + } + } + } \ No newline at end of file From e11b9d32e79095f92996d62e7639733dba8da65e Mon Sep 17 00:00:00 2001 From: MJendza Date: Mon, 18 Jan 2021 21:56:52 +0100 Subject: [PATCH 9/9] moved config for tools to the root --- {.github/workflows/.config => .config}/dotnet-tools.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github/workflows/.config => .config}/dotnet-tools.json (100%) diff --git a/.github/workflows/.config/dotnet-tools.json b/.config/dotnet-tools.json similarity index 100% rename from .github/workflows/.config/dotnet-tools.json rename to .config/dotnet-tools.json