diff --git a/.gitignore b/.gitignore index 82ada0f8a..ac968be1b 100644 --- a/.gitignore +++ b/.gitignore @@ -297,3 +297,9 @@ functions-extensions/ .vscode/ .ionide/ /src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml + +# E2E Tests build output +/test/DotnetIsolatedE2ETests/Azure.Functions.Cli/* +/src/Worker.Extensions.DurableTask/out/* +/src/WebJobs.Extensions.DurableTask/out/* +/test/DotnetIsolatedE2ETests/azurite/* \ No newline at end of file diff --git a/WebJobs.Extensions.DurableTask.sln b/WebJobs.Extensions.DurableTask.sln index edda04460..63d9608c9 100644 --- a/WebJobs.Extensions.DurableTask.sln +++ b/WebJobs.Extensions.DurableTask.sln @@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DFPerfScenariosV4", "test\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.DurableTask.Tests", "test\Worker.Extensions.DurableTask.Tests\Worker.Extensions.DurableTask.Tests.csproj", "{76DEC17C-BF6A-498A-8E8A-7D6CB2E03284}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetIsolatedE2ETests", "test\DotnetIsolatedE2ETests\DotnetIsolatedE2ETests.csproj", "{63628712-4196-4865-B268-5BA3D8F08DE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +157,10 @@ Global {76DEC17C-BF6A-498A-8E8A-7D6CB2E03284}.Debug|Any CPU.Build.0 = Debug|Any CPU {76DEC17C-BF6A-498A-8E8A-7D6CB2E03284}.Release|Any CPU.ActiveCfg = Release|Any CPU {76DEC17C-BF6A-498A-8E8A-7D6CB2E03284}.Release|Any CPU.Build.0 = Release|Any CPU + {63628712-4196-4865-B268-5BA3D8F08DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63628712-4196-4865-B268-5BA3D8F08DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63628712-4196-4865-B268-5BA3D8F08DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63628712-4196-4865-B268-5BA3D8F08DE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -184,6 +190,7 @@ Global {7387E723-E153-4B7A-B105-8C67BFBD48CF} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5} {FC8AD123-F949-4D21-B817-E5A4BBF7F69B} = {7387E723-E153-4B7A-B105-8C67BFBD48CF} {76DEC17C-BF6A-498A-8E8A-7D6CB2E03284} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5} + {63628712-4196-4865-B268-5BA3D8F08DE1} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5E9AC327-DE18-41A5-A55D-E44CB4281943} diff --git a/test/DotnetIsolatedE2EApps/MainApp/.gitignore b/test/DotnetIsolatedE2EApps/MainApp/.gitignore new file mode 100644 index 000000000..ff5b00c50 --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/test/DotnetIsolatedE2EApps/MainApp/HelloCities.cs b/test/DotnetIsolatedE2EApps/MainApp/HelloCities.cs new file mode 100644 index 000000000..bf803de92 --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/HelloCities.cs @@ -0,0 +1,55 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Durable.Tests.E2E +{ + public static class HelloCities + { + [Function(nameof(HelloCities))] + public static async Task> RunOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + ILogger logger = context.CreateReplaySafeLogger(nameof(HelloCities)); + logger.LogInformation("Saying hello."); + var outputs = new List(); + + // Replace name and input with values relevant for your Durable Functions Activity + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Tokyo")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "Seattle")); + outputs.Add(await context.CallActivityAsync(nameof(SayHello), "London")); + + // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"] + return outputs; + } + + [Function(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext) + { + ILogger logger = executionContext.GetLogger("SayHello"); + logger.LogInformation("Saying hello to {name}.", name); + return $"Hello {name}!"; + } + + [Function("HelloCities_HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + ILogger logger = executionContext.GetLogger("HelloCities_HttpStart"); + + // Function input comes from the request content. + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + nameof(HelloCities)); + + logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + + // Returns an HTTP 202 response with an instance management payload. + // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + } +} diff --git a/test/DotnetIsolatedE2EApps/MainApp/Program.cs b/test/DotnetIsolatedE2EApps/MainApp/Program.cs new file mode 100644 index 000000000..eeca9f796 --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); + +// Bool.parse +if (Environment.GetEnvironmentVariable("DURABLE_ATTACH_DEBUGGER") == "True") { + Debugger.Launch(); +} + +host.Run(); diff --git a/test/DotnetIsolatedE2EApps/MainApp/app.csproj b/test/DotnetIsolatedE2EApps/MainApp/app.csproj new file mode 100644 index 000000000..31029f41f --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/app.csproj @@ -0,0 +1,32 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/test/DotnetIsolatedE2EApps/MainApp/host.json b/test/DotnetIsolatedE2EApps/MainApp/host.json new file mode 100644 index 000000000..5df170b64 --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file diff --git a/test/DotnetIsolatedE2EApps/MainApp/nuget.config b/test/DotnetIsolatedE2EApps/MainApp/nuget.config new file mode 100644 index 000000000..ae24a00e0 --- /dev/null +++ b/test/DotnetIsolatedE2EApps/MainApp/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/DotnetIsolatedE2ETests/Constants.cs b/test/DotnetIsolatedE2ETests/Constants.cs new file mode 100644 index 000000000..83c6537a9 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Constants.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + internal class Constants + { + public static IConfiguration Configuration = TestUtility.GetTestConfiguration(); + + internal static string FunctionsHostUrl = Configuration["FunctionAppUrl"] ?? "http://localhost:7071"; + + internal const string FunctionAppCollectionName = "DurableTestsCollection"; + } +} diff --git a/test/DotnetIsolatedE2ETests/DotnetIsolatedE2ETests.csproj b/test/DotnetIsolatedE2ETests/DotnetIsolatedE2ETests.csproj new file mode 100644 index 000000000..5ba0ccf7a --- /dev/null +++ b/test/DotnetIsolatedE2ETests/DotnetIsolatedE2ETests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + latest + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/test/DotnetIsolatedE2ETests/Fixtures/FixtureHelpers.cs b/test/DotnetIsolatedE2ETests/Fixtures/FixtureHelpers.cs new file mode 100644 index 000000000..0c172d065 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Fixtures/FixtureHelpers.cs @@ -0,0 +1,89 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E + +{ + public static class FixtureHelpers + { + public static Process GetFuncHostProcess(bool enableAuth = false) + { + var funcProcess = new Process(); + var rootDir = Path.GetFullPath(@"../../../../.."); + var e2eAppBinPath = Path.Combine(rootDir, @"test/DotnetIsolatedE2EApps/MainApp/bin"); + string? e2eHostJson = Directory.GetFiles(e2eAppBinPath, "host.json", SearchOption.AllDirectories).FirstOrDefault(); + + if (e2eHostJson == null) + { + throw new InvalidOperationException($"Could not find a built worker app under '{e2eAppBinPath}'"); + } + + var e2eAppPath = Path.GetDirectoryName(e2eHostJson); + + var cliPath = Path.Combine(rootDir, @"test/DotnetIsolatedE2ETests/Azure.Functions.Cli/func"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + cliPath += ".exe"; + } + + if (!File.Exists(cliPath)) + { + throw new InvalidOperationException($"Could not find '{cliPath}'. Try running '{Path.Combine(rootDir, "setup-e2e-tests.ps1")}' to install it."); + } + + funcProcess.StartInfo.UseShellExecute = false; + funcProcess.StartInfo.RedirectStandardError = true; + funcProcess.StartInfo.RedirectStandardOutput = true; + funcProcess.StartInfo.CreateNoWindow = true; + funcProcess.StartInfo.WorkingDirectory = e2eAppPath; + funcProcess.StartInfo.FileName = cliPath; + funcProcess.StartInfo.ArgumentList.Add("host"); + funcProcess.StartInfo.ArgumentList.Add("start"); + funcProcess.StartInfo.ArgumentList.Add("--csharp"); + funcProcess.StartInfo.ArgumentList.Add("--verbose"); + + if (enableAuth) + { + funcProcess.StartInfo.ArgumentList.Add("--enableAuth"); + } + + return funcProcess; + } + + public static void StartProcessWithLogging(Process funcProcess, ILogger logger) + { + funcProcess.ErrorDataReceived += (sender, e) => logger.LogError(e?.Data); + funcProcess.OutputDataReceived += (sender, e) => logger.LogInformation(e?.Data); + + funcProcess.Start(); + + logger.LogInformation($"Started '{funcProcess.StartInfo.FileName}'"); + + funcProcess.BeginErrorReadLine(); + funcProcess.BeginOutputReadLine(); + } + + public static void KillExistingProcessesMatchingName(string processName) + { + foreach (var process in Process.GetProcessesByName(processName)) + { + try + { + process.Kill(); + } + catch + { + // Best effort + } + } + } + } +} diff --git a/test/DotnetIsolatedE2ETests/Fixtures/FunctionAppFixture.cs b/test/DotnetIsolatedE2ETests/Fixtures/FunctionAppFixture.cs new file mode 100644 index 000000000..c71dc0c05 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Fixtures/FunctionAppFixture.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + public class FunctionAppFixture : IAsyncLifetime + { + private readonly ILogger _logger; + private bool _disposed; + private Process _funcProcess; + + private JobObjectRegistry _jobObjectRegistry; + + public FunctionAppFixture(IMessageSink messageSink) + { + // initialize logging + ILoggerFactory loggerFactory = new LoggerFactory(); + TestLogs = new TestLoggerProvider(messageSink); + loggerFactory.AddProvider(TestLogs); + _logger = loggerFactory.CreateLogger(); + } + + public async Task InitializeAsync() + { + // start host via CLI if testing locally + if (Constants.FunctionsHostUrl.Contains("localhost")) + { + // kill existing func processes + _logger.LogInformation("Shutting down any running functions hosts.."); + FixtureHelpers.KillExistingProcessesMatchingName("func"); + + // start functions process + _logger.LogInformation($"Starting functions host for {Constants.FunctionAppCollectionName}..."); + _funcProcess = FixtureHelpers.GetFuncHostProcess(); + string workingDir = _funcProcess.StartInfo.WorkingDirectory; + _logger.LogInformation($" Working dir: '${workingDir}' Exists: '{Directory.Exists(workingDir)}'"); + string fileName = _funcProcess.StartInfo.FileName; + _logger.LogInformation($" File name: '${fileName}' Exists: '{File.Exists(fileName)}'"); + + //TODO: This may be added back if we want cosmos tests + //await CosmosDBHelpers.TryCreateDocumentCollectionsAsync(_logger); + + //TODO: WORKER ATTACH ISSUES + // Abandoning this attach method for now - It seems like Debugger.Launch() from the app can't detect the running VS instance. + // Not sure if this is because VS is the parent process, or because it is already attached to testhost.exe, but for now we + // will rely on manual attach. Some possible solution with DTE might exist but for now, it relies on a specific VS version + //if (Debugger.IsAttached) + //{ + // _funcProcess.StartInfo.EnvironmentVariables["DURABLE_ATTACH_DEBUGGER"] = "True"; + //} + + FixtureHelpers.StartProcessWithLogging(_funcProcess, _logger); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // ensure child processes are cleaned up + _jobObjectRegistry = new JobObjectRegistry(); + _jobObjectRegistry.Register(_funcProcess); + } + + var httpClient = new HttpClient(); + _logger.LogInformation("Waiting for host to be running..."); + await TestUtility.RetryAsync(async () => + { + try + { + var response = await httpClient.GetAsync($"{Constants.FunctionsHostUrl}/admin/host/status"); + var content = await response.Content.ReadAsStringAsync(); + var doc = JsonDocument.Parse(content); + if (doc.RootElement.TryGetProperty("state", out JsonElement value) && + value.GetString() == "Running") + { + _logger.LogInformation($" Current state: Running"); + return true; + } + + _logger.LogInformation($" Current state: {value}"); + return false; + } + catch + { + if (_funcProcess.HasExited) + { + // Something went wrong starting the host - check the logs + _logger.LogInformation($" Current state: process exited - something may have gone wrong."); + return false; + } + + // Can get exceptions before host is running. + _logger.LogInformation($" Current state: process starting"); + return false; + } + }, userMessageCallback: () => string.Join(System.Environment.NewLine, TestLogs.CoreToolsLogs)); + } + + //TODO: This line would launch the jit debugger for func - still some issues here, however. + // ISSUE 1: Windows only implementation + // ISSUE 2: For some reason, the loaded symbols for the WebJobs extension + // a) don't load automatically + // b) don't match the version from the local repo + // ISSUE 3: See the worker attach comments above + //Process.Start("cmd.exe", "/C vsjitdebugger.exe -p " + _funcProcess.Id.ToString()); + } + + internal TestLoggerProvider TestLogs { get; private set; } + + + public Task DisposeAsync() + { + if (!_disposed) + { + if (_funcProcess != null) + { + try + { + _funcProcess.Kill(); + _funcProcess.Dispose(); + } + catch + { + // process may not have started + } + } + + _jobObjectRegistry?.Dispose(); + } + + _disposed = true; + + return Task.CompletedTask; + } + } + + [CollectionDefinition(Constants.FunctionAppCollectionName)] + public class FunctionAppCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} diff --git a/test/DotnetIsolatedE2ETests/Helpers/HttpHelpers.cs b/test/DotnetIsolatedE2ETests/Helpers/HttpHelpers.cs new file mode 100644 index 000000000..2fe2b74b0 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Helpers/HttpHelpers.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + class HttpHelpers + { + public static async Task InvokeHttpTrigger(string functionName, string queryString = "") + { + // Basic http request + HttpRequestMessage request = GetTestRequest(functionName, queryString); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); + return await GetResponseMessage(request); + } + + public static async Task InvokeHttpTriggerWithBody(string functionName, string body, string mediaType) + { + HttpRequestMessage request = GetTestRequest(functionName); + request.Content = new StringContent(body); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType)); + return await GetResponseMessage(request); + } + + public static async Task InvokeHttpTrigger(string functionName, string queryString, HttpStatusCode expectedStatusCode, string expectedMessage, int expectedCode = 0) + { + string uri = $"{Constants.FunctionsHostUrl}/api/{functionName}{queryString}"; + using (var request = new HttpRequestMessage(HttpMethod.Get, uri)) + { + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); + var response = await GetResponseMessage(request); + + Console.WriteLine( + $"InvokeHttpTrigger: {functionName}{queryString} : {response.StatusCode} : {response.ReasonPhrase}"); + if (expectedStatusCode != response.StatusCode && expectedCode != (int)response.StatusCode) + { + return false; + } + + if (!string.IsNullOrEmpty(expectedMessage)) + { + string actualMessage = await response.Content.ReadAsStringAsync(); + Console.WriteLine( + $"InvokeHttpTrigger: expectedMessage : {expectedMessage}, actualMessage : {actualMessage}"); + return actualMessage.Contains(expectedMessage); + } + + return true; + } + } + + private static HttpRequestMessage GetTestRequest(string functionName, string queryString = "") + { + return new HttpRequestMessage + { + RequestUri = new Uri($"{Constants.FunctionsHostUrl}/api/{functionName}{queryString}"), + Method = HttpMethod.Post + }; + } + + private static async Task GetResponseMessage(HttpRequestMessage request) + { + HttpResponseMessage response = null; + using (var httpClient = new HttpClient()) + { + response = await httpClient.SendAsync(request); + } + + return response; + } + } +} diff --git a/test/DotnetIsolatedE2ETests/Helpers/TestLoggerProvider.cs b/test/DotnetIsolatedE2ETests/Helpers/TestLoggerProvider.cs new file mode 100644 index 000000000..4f654cee0 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Helpers/TestLoggerProvider.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + internal class TestLoggerProvider : ILoggerProvider, ILogger + { + private readonly IMessageSink _messageSink; + private ITestOutputHelper _currentTestOutput; + IList _logs = new List(); + + public TestLoggerProvider(IMessageSink messageSink) + { + _messageSink = messageSink; + } + + public IEnumerable CoreToolsLogs => _logs.ToArray(); + + // This needs to be created/disposed per-test so we can associate logs + // with the specific running test. + public IDisposable UseTestLogger(ITestOutputHelper testOutput) + { + // reset these every test + _currentTestOutput = testOutput; + return new DisposableOutput(this); + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public ILogger CreateLogger(string categoryName) + { + return this; + } + + public void Dispose() + { + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + string formattedString = formatter(state, exception); + _messageSink.OnMessage(new DiagnosticMessage(formattedString)); + _logs.Add(formattedString); + _currentTestOutput?.WriteLine(formattedString); + } + + private class DisposableOutput : IDisposable + { + private readonly TestLoggerProvider _xunitLogger; + + public DisposableOutput(TestLoggerProvider xunitLogger) + { + _xunitLogger = xunitLogger; + } + + public void Dispose() + { + _xunitLogger._currentTestOutput = null; + } + } + } +} diff --git a/test/DotnetIsolatedE2ETests/Helpers/TestUtility.cs b/test/DotnetIsolatedE2ETests/Helpers/TestUtility.cs new file mode 100644 index 000000000..90780f72a --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Helpers/TestUtility.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + public static class TestUtility + { + public static IConfiguration GetTestConfiguration() + { + return new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddTestSettings() + .Build(); + } + + public static IConfigurationBuilder AddTestSettings(this IConfigurationBuilder builder) + { + string configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azurefunctions", "appsettings.tests.json"); + return builder.AddJsonFile(configPath, true); + } + + public static async Task RetryAsync(Func> condition, int timeout = 60 * 1000, int pollingInterval = 2 * 1000, bool throwWhenDebugging = false, Func userMessageCallback = null) + { + DateTime start = DateTime.Now; + while (!await condition()) + { + await Task.Delay(pollingInterval); + + bool shouldThrow = !Debugger.IsAttached || (Debugger.IsAttached && throwWhenDebugging); + if (shouldThrow && (DateTime.Now - start).TotalMilliseconds > timeout) + { + string error = "Condition not reached within timeout."; + if (userMessageCallback != null) + { + error += " " + userMessageCallback(); + } + throw new ApplicationException(error); + } + } + } + } +} diff --git a/test/DotnetIsolatedE2ETests/JobObjectRegistry.cs b/test/DotnetIsolatedE2ETests/JobObjectRegistry.cs new file mode 100644 index 000000000..0af66aac7 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/JobObjectRegistry.cs @@ -0,0 +1,146 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + // Taken from: https://github.com/Azure/azure-functions-host/blob/69111926ee920d4ba10829c8fa34303bb8165a42/src/WebJobs.Script/Workers/ProcessManagement/JobObjectRegistry.cs + // This kills child func.exe even if tests are killed from VS mid-run. + + // Registers processes on windows with a job object to ensure disposal after parent exit. + internal class JobObjectRegistry : IDisposable + { + private IntPtr _handle; + private bool _disposed = false; + + public JobObjectRegistry() + { + _handle = CreateJobObject(null, null); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION + { + LimitFlags = 0x2000 + }; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + BasicLimitInformation = info + }; + + int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) + { + throw new Exception(string.Format("Unable to set information. Error: {0}", Marshal.GetLastWin32Error())); + } + } + + public bool Register(Process proc) + { + return AssignProcessToJobObject(_handle, proc.Handle); + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateJobObject(object a, string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr job); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // Dispose of managed resources. + } + + Close(); + _disposed = true; + } + + public void Close() + { + if (_handle != IntPtr.Zero) + { + CloseHandle(_handle); + } + _handle = IntPtr.Zero; + } + } + + public enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IO_COUNTERS + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public uint nLength; + public IntPtr lpSecurityDescriptor; + public int bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } +} diff --git a/test/DotnetIsolatedE2ETests/Tests/HelloCitiesTest.cs b/test/DotnetIsolatedE2ETests/Tests/HelloCitiesTest.cs new file mode 100644 index 000000000..0da1e5e67 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/Tests/HelloCitiesTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E +{ + [Collection(Constants.FunctionAppCollectionName )] + public class HttpEndToEndTests + { + private readonly FunctionAppFixture _fixture; + + public HttpEndToEndTests(FunctionAppFixture fixture, ITestOutputHelper testOutputHelper) + { + _fixture = fixture; + _fixture.TestLogs.UseTestLogger(testOutputHelper); + } + + [Theory] + [InlineData("HelloCities_HttpStart", "", HttpStatusCode.Accepted, "")] + public async Task HttpTriggerTests(string functionName, string queryString, HttpStatusCode expectedStatusCode, string expectedMessage) + { + HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger(functionName, queryString); + string actualMessage = await response.Content.ReadAsStringAsync(); + + Assert.Equal(expectedStatusCode, response.StatusCode); + + if (!string.IsNullOrEmpty(expectedMessage)) + { + Assert.False(string.IsNullOrEmpty(actualMessage)); + //Assert.Contains(expectedMessage, actualMessage); + } + } + } +} diff --git a/test/DotnetIsolatedE2ETests/build-e2e-test.ps1 b/test/DotnetIsolatedE2ETests/build-e2e-test.ps1 new file mode 100644 index 000000000..ce9adf01f --- /dev/null +++ b/test/DotnetIsolatedE2ETests/build-e2e-test.ps1 @@ -0,0 +1,178 @@ +#!/usr/bin/env pwsh +# +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +[CmdletBinding()] +param( + [switch] + $Clean, + + [Switch] + $SkipStorageEmulator, + + [Switch] + $SkipCosmosDBEmulator, + + [Switch] + $SkipCoreTools, + + [Switch] + $UseCoreToolsBuildFromIntegrationTests, + + [Switch] + $SkipBuildOnPack +) + +$ProjectBaseDirectory = "$PSScriptRoot\..\..\" +$WebJobsExtensionProjectDirectory = Join-Path $ProjectBaseDirectory "src\WebJobs.Extensions.DurableTask" +$WorkerExtensionProjectDirectory = Join-Path $ProjectBaseDirectory "src\Worker.Extensions.DurableTask" +# $E2ETestProjectDirectory = "$ProjectBaseDirectory\test\DotnetIsolatedE2ETests" +$E2EAppProjectDirectory = Join-Path $ProjectBaseDirectory "test\DotnetIsolatedE2EApps/MainApp" + +$FunctionsRuntimeVersion = 4 + +# A function that checks exit codes and fails script if an error is found +function StopOnFailedExecution { + if ($LastExitCode) + { + exit $LastExitCode + } +} + +$FUNC_CLI_DIRECTORY = Join-Path $PSScriptRoot 'Azure.Functions.Cli' +if($SkipCoreTool -or (Test-Path $FUNC_CLI_DIRECTORY)) +{ + Write-Host "---Skipping Core Tools download---" +} +elseif (Test-Path $FUNC_CLI_DIRECTORY) +{ + Write-Host "---Skipping Core Tools download---" +} +else +{ + $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() + if ($IsWindows) { + $os = "win" + $coreToolsURL = $env:CORE_TOOLS_URL + } + else { + if ($IsMacOS) { + $os = "osx" + } else { + $os = "linux" + $coreToolsURL = $env:CORE_TOOLS_URL_LINUX + } + } + + if ($UseCoreToolsBuildFromIntegrationTests.IsPresent) + { + Write-Host "" + Write-Host "Install the Core Tools for Integration Tests..." + $coreToolsURL = "https://functionsintegclibuilds.blob.core.windows.net/builds/$FunctionsRuntimeVersion/latest/Azure.Functions.Cli.$os-$arch.zip" + $versionUrl = "https://functionsintegclibuilds.blob.core.windows.net/builds/$FunctionsRuntimeVersion/latest/version.txt" + } + else + { + if ([string]::IsNullOrWhiteSpace($coreToolsURL)) + { + $coreToolsURL = "https://functionsclibuilds.blob.core.windows.net/builds/$FunctionsRuntimeVersion/latest/Azure.Functions.Cli.$os-$arch.zip" + $versionUrl = "https://functionsclibuilds.blob.core.windows.net/builds/$FunctionsRuntimeVersion/latest/version.txt" + } + } + + Write-Host "" + Write-Host "---Downloading the Core Tools for Functions V$FunctionsRuntimeVersion---" + Write-Host "Core Tools download url: $coreToolsURL" + + Write-Host 'Deleting Functions Core Tools if exists...' + Remove-Item -Force "$FUNC_CLI_DIRECTORY.zip" -ErrorAction Ignore + Remove-Item -Recurse -Force $FUNC_CLI_DIRECTORY -ErrorAction Ignore + + if ($versionUrl) + { + $version = Invoke-RestMethod -Uri $versionUrl + Write-Host "Downloading Functions Core Tools (Version: $version)..." + } + + $output = "$FUNC_CLI_DIRECTORY.zip" + Invoke-RestMethod -Uri $coreToolsURL -OutFile $output + + Write-Host 'Extracting Functions Core Tools...' + Expand-Archive $output -DestinationPath $FUNC_CLI_DIRECTORY + + if ($IsMacOS -or $IsLinux) + { + & "chmod" "a+x" "$FUNC_CLI_DIRECTORY/func" + } + + Write-Host "------" +} + +Write-Host "Removing old packages from test app" +Set-Location $E2EAppProjectDirectory +Get-ChildItem -Path ./packages -Include * -File -Recurse | ForEach-Object { $_.Delete()} + +Write-Host "Building WebJobs extension project" + +Set-Location $WebJobsExtensionProjectDirectory +if (!(Test-Path "./out")) { + mkdir ./out +} +Get-ChildItem -Path ./out -Include * -File -Recurse | ForEach-Object { $_.Delete()} +dotnet build -c Debug "$WebJobsExtensionProjectDirectory\WebJobs.Extensions.DurableTask.csproj" --output ./out + +Write-Host "Moving nupkg from WebJobs extension to $E2EAppProjectDirectory/packages" +Set-Location ./out +dotnet nuget push *.nupkg --source "$E2EAppProjectDirectory/packages" + +Write-Host "Building worker extension project" + +Set-Location $WorkerExtensionProjectDirectory +if (!(Test-Path "./out")) { + mkdir ./out +} +Get-ChildItem -Path ./out -Include * -File -Recurse | ForEach-Object { $_.Delete()} +dotnet build -c Debug "$WorkerExtensionProjectDirectory\Worker.Extensions.DurableTask.csproj" --output ./out + +Write-Host "Moving nupkg from worker extension to $E2EAppProjectDirectory/packages" +Set-Location ./out +dotnet nuget push *.nupkg --source "$E2EAppProjectDirectory/packages" + +Write-Host "Updating app .csproj to reference built package versions" +Set-Location $E2EAppProjectDirectory +$files = Get-ChildItem -Path ./packages -Include * -File -Recurse +$files | ForEach-Object { + if ($_.Name -match 'Microsoft.Azure.Functions.Worker.Extensions.DurableTask') + { + $webJobsExtensionVersion = $_.Name -replace 'Microsoft.Azure.Functions.Worker.Extensions.DurableTask\.|\.nupkg' + Write-Host "Updating Worker.Extensions.DurableTask version to $webJobsExtensionVersion" + dotnet add app.csproj package Microsoft.Azure.Functions.Worker.Extensions.DurableTask --version $webJobsExtensionVersion + } + if ($_.Name -match 'Microsoft.Azure.WebJobs.Extensions.DurableTask') + { + $webJobsExtensionVersion = $_.Name -replace 'Microsoft.Azure.WebJobs.Extensions.DurableTask\.|\.nupkg' + Write-Host "Updating WebJobs.Extensions.DurableTask version to $webJobsExtensionVersion" + dotnet add app.csproj package Microsoft.Azure.WebJobs.Extensions.DurableTask --version $webJobsExtensionVersion + } +} + +Write-Host "Building app project" +dotnet clean app.csproj +dotnet build app.csproj + +Set-Location $PSScriptRoot + +if ($SkipStorageEmulator -And $SkipCosmosDBEmulator) +{ + Write-Host + Write-Host "---Skipping emulator startup---" + Write-Host +} +else +{ + .\start-emulators.ps1 -SkipStorageEmulator:$SkipStorageEmulator -SkipCosmosDBEmulator:$true +} + +StopOnFailedExecution diff --git a/test/DotnetIsolatedE2ETests/start-emulators.ps1 b/test/DotnetIsolatedE2ETests/start-emulators.ps1 new file mode 100644 index 000000000..31fd7c936 --- /dev/null +++ b/test/DotnetIsolatedE2ETests/start-emulators.ps1 @@ -0,0 +1,188 @@ +param( + [Parameter(Mandatory=$false)] + [Switch] + $SkipStorageEmulator, + [Parameter(Mandatory=$false)] + [Switch] + $StartCosmosDBEmulator, + [Parameter(Mandatory=$false)] + [Switch] + $NoWait +) + +$DebugPreference = 'Continue' + +Write-Host "Start CosmosDB Emulator: $StartCosmosDBEmulator" +Write-Host "Skip Storage Emulator: $SkipStorageEmulator" + +$startedCosmos = $false +$startedStorage = $false + +# Reassigning $IsWindows no longer allowed - might need to refactor this logic +# if (!$IsWindows -and !$IsLinux -and !$IsMacOs) +# { +# # For pre-PS6 +# Write-Host "Could not resolve OS. Assuming Windows." +# $IsWindows = $true +# } + +if (!$IsWindows) +{ + Write-Host "Skipping CosmosDB emulator because it is not supported on non-Windows OS." + $StartCosmosDBEmulator = $false +} + +if ($StartCosmosDBEmulator) +{ + # Locally, you may need to run PowerShell with administrative privileges + Add-MpPreference -ExclusionPath "$env:ProgramFiles\Azure Cosmos DB Emulator" + Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" +} + +function IsStorageEmulatorRunning() +{ + try + { + $response = Invoke-WebRequest -Uri "http://127.0.0.1:10000/" + $StatusCode = $Response.StatusCode + } + catch + { + $StatusCode = $_.Exception.Response.StatusCode.value__ + } + + if ($StatusCode -eq 400) + { + return $true + } + + return $false +} + +if ($StartCosmosDBEmulator) +{ + Write-Host "" + Write-Host "---Starting CosmosDB emulator---" + $cosmosStatus = Get-CosmosDbEmulatorStatus + Write-Host "CosmosDB emulator status: $cosmosStatus" + + if ($cosmosStatus -eq "StartPending") + { + $startedCosmos = $true + } + elseif ($cosmosStatus -ne "Running") + { + Write-Host "CosmosDB emulator is not running. Starting emulator." + Start-Process "$env:ProgramFiles\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/NoExplorer /NoUI /DisableRateLimiting /PartitionCount=50 /Consistency=Strong /EnablePreview /EnableSqlComputeEndpoint" -Verb RunAs + $startedCosmos = $true + } + else + { + Write-Host "CosmosDB emulator is already running." + } +} + +if (!$SkipStorageEmulator) +{ + Write-Host "------" + Write-Host "" + Write-Host "---Starting Storage emulator---" + $storageEmulatorRunning = IsStorageEmulatorRunning + + if ($storageEmulatorRunning -eq $false) + { + if ($IsWindows) + { + npm install -g azurite + mkdir "./azurite" + Start-Process azurite.cmd -WorkingDirectory "./azurite" -ArgumentList "--silent" + } + else + { + sudo npm install -g azurite + sudo mkdir azurite + sudo azurite --silent --location azurite --debug azurite\debug.log & + } + + $startedStorage = $true + } + else + { + Write-Host "Storage emulator is already running." + } + + Write-Host "------" + Write-Host +} + +if ($NoWait -eq $true) +{ + Write-Host "'NoWait' specified. Exiting." + Write-Host + exit 0 +} + +if ($StartCosmosDBEmulator -and $startedCosmos -eq $true) +{ + Write-Host "---Waiting for CosmosDB emulator to be running---" + $cosmosStatus = Get-CosmosDbEmulatorStatus + Write-Host "CosmosDB emulator status: $cosmosStatus" + + $waitSuccess = Wait-CosmosDbEmulator -Status Running -Timeout 60 -ErrorAction Continue + + if ($waitSuccess -ne $true) + { + Write-Host "CosmosDB emulator not yet running after waiting 60 seconds. Restarting." + Write-Host "Shutting down and restarting" + Start-Process "$env:ProgramFiles\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/Shutdown" -Verb RunAs + sleep 30; + + for ($j=0; $j -lt 3; $j++) { + Write-Host "Attempt $j" + Start-Process "$env:ProgramFiles\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/NoExplorer /NoUI /DisableRateLimiting /PartitionCount=50 /Consistency=Strong /EnablePreview /EnableSqlComputeEndpoint" -Verb RunAs + + for ($i=0; $i -lt 5; $i++) { + $status = Get-CosmosDbEmulatorStatus + Write-Host "Cosmos DB Emulator Status: $status" + + if ($status -ne "Running") { + sleep 30; + } + else { + break; + } + } + + if ($status -ne "Running") { + Write-Host "Shutting down and restarting" + Start-Process "$env:ProgramFiles\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/Shutdown" -Verb RunAs + sleep 30; + } + else { + break; + } + } + + if ($status -ne "Running") { + Write-Error "Emulator failed to start" + } + } + + Write-Host "------" + Write-Host +} + +if (!$SkipStorageEmulator -and $startedStorage -eq $true) +{ + Write-Host "---Waiting for Storage emulator to be running---" + $storageEmulatorRunning = IsStorageEmulatorRunning + while ($storageEmulatorRunning -eq $false) + { + Write-Host "Storage emulator not ready." + Start-Sleep -Seconds 5 + $storageEmulatorRunning = IsStorageEmulatorRunning + } + Write-Host "Storage emulator ready." + Write-Host "------" + Write-Host +} \ No newline at end of file diff --git a/test/E2E/app/.gitignore b/test/E2E/app/.gitignore new file mode 100644 index 000000000..ff5b00c50 --- /dev/null +++ b/test/E2E/app/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/test/E2E/app/Program.cs b/test/E2E/app/Program.cs new file mode 100644 index 000000000..9389455ba --- /dev/null +++ b/test/E2E/app/Program.cs @@ -0,0 +1,13 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); + +host.Run(); diff --git a/test/E2E/app/app.csproj b/test/E2E/app/app.csproj new file mode 100644 index 000000000..aa5cb0cbe --- /dev/null +++ b/test/E2E/app/app.csproj @@ -0,0 +1,30 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/test/E2E/app/host.json b/test/E2E/app/host.json new file mode 100644 index 000000000..ee5cf5f83 --- /dev/null +++ b/test/E2E/app/host.json @@ -0,0 +1,12 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + } +} \ No newline at end of file