diff --git a/Directory.Build.targets b/Directory.Build.targets index 8905fec051..10729ddb49 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -2,4 +2,8 @@ + + $(ArtifactsPath)/log/$(ArtifactsProjectName)/tests_$(ArtifactsPivots)/ + + diff --git a/eng/ci/templates/jobs/run-unit-tests.yml b/eng/ci/templates/jobs/run-unit-tests.yml index 97ed382438..cb5743ac6c 100644 --- a/eng/ci/templates/jobs/run-unit-tests.yml +++ b/eng/ci/templates/jobs/run-unit-tests.yml @@ -10,7 +10,7 @@ jobs: displayName: Publish deps.json path: $(Build.ArtifactStagingDirectory) artifact: WebHost_Deps - condition: failed() + condition: and(failed(), eq(variables['System.JobAttempt'], 1)) # only publish on first attempt steps: - template: /eng/ci/templates/install-dotnet.yml@self @@ -20,7 +20,7 @@ jobs: inputs: command: test testRunTitle: Unit Tests - arguments: -v n + arguments: -v n --blame projects: | **\ExtensionsMetadataGeneratorTests.csproj **\WebJobs.Script.Tests.csproj diff --git a/release_notes.md b/release_notes.md index 30d5e2eaf6..cc087fe1cc 100644 --- a/release_notes.md +++ b/release_notes.md @@ -10,4 +10,5 @@ - Handles loading extensions.json with empty extensions(#11174) - Update HttpWorkerOptions to implement IOptionsFormatter (#11175) - Improved metadata binding validation (#11101) +- Throw exception instead of timing out when worker channel exits before initializing gRPC (#10937) - Skip logging errors on gRPC client disconnect (#10572) diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs index 7b897bfea0..e4ccecf94c 100644 --- a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs +++ b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs @@ -366,19 +366,36 @@ private void DispatchMessage(InboundGrpcEvent msg) public bool IsChannelReadyForInvocations() { - return !_disposing && !_disposed && _state.HasFlag(RpcWorkerChannelState.InvocationBuffersInitialized | RpcWorkerChannelState.Initialized); + return !_disposing && !_disposed + && _state.HasFlag( + RpcWorkerChannelState.InvocationBuffersInitialized | RpcWorkerChannelState.Initialized); } public async Task StartWorkerProcessAsync(CancellationToken cancellationToken) { - RegisterCallbackForNextGrpcMessage(MsgType.StartStream, _workerConfig.CountOptions.ProcessStartupTimeout, 1, SendWorkerInitRequest, HandleWorkerStartStreamError); - // note: it is important that the ^^^ StartStream is in place *before* we start process the loop, otherwise we get a race condition + RegisterCallbackForNextGrpcMessage( + MsgType.StartStream, + _workerConfig.CountOptions.ProcessStartupTimeout, + count: 1, + SendWorkerInitRequest, + HandleWorkerStartStreamError); + + // note: it is important that the ^^^ StartStream is in place *before* we start process the loop, + // otherwise we get a race condition _ = ProcessInbound(); _workerChannelLogger.LogDebug("Initiating Worker Process start up"); - await _rpcWorkerProcess.StartProcessAsync(); - _state = _state | RpcWorkerChannelState.Initializing; - await _workerInitTask.Task; + await _rpcWorkerProcess.StartProcessAsync(cancellationToken); + _state |= RpcWorkerChannelState.Initializing; + Task exited = _rpcWorkerProcess.WaitForExitAsync(cancellationToken); + Task winner = await Task.WhenAny(_workerInitTask.Task, exited).WaitAsync(cancellationToken); + await winner; + + if (winner == exited) + { + // process exited without throwing. We need to throw to indicate process is not running. + throw new WorkerProcessExitException("Worker process exited before initializing."); + } } public async Task GetWorkerStatusAsync() diff --git a/src/WebJobs.Script/Config/ExtensionRequirementOptions.cs b/src/WebJobs.Script/Config/ExtensionRequirementOptions.cs index ae6a996412..130e875894 100644 --- a/src/WebJobs.Script/Config/ExtensionRequirementOptions.cs +++ b/src/WebJobs.Script/Config/ExtensionRequirementOptions.cs @@ -1,10 +1,8 @@ // 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 Microsoft.Azure.WebJobs.Script.ExtensionRequirements; -using Microsoft.Azure.WebJobs.Script.Workers.Rpc; namespace Microsoft.Azure.WebJobs.Script.Config { diff --git a/src/WebJobs.Script/Config/ExtensionRequirementOptionsSetup.cs b/src/WebJobs.Script/Config/ExtensionRequirementOptionsSetup.cs index 0963e621de..b8c1454be2 100644 --- a/src/WebJobs.Script/Config/ExtensionRequirementOptionsSetup.cs +++ b/src/WebJobs.Script/Config/ExtensionRequirementOptionsSetup.cs @@ -1,13 +1,6 @@ // 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.Configuration; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Script.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; diff --git a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs index 953fc56429..55b95f1323 100644 --- a/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs +++ b/src/WebJobs.Script/DependencyInjection/ScriptStartupTypeLocator.cs @@ -36,29 +36,37 @@ public sealed class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator private readonly IExtensionBundleManager _extensionBundleManager; private readonly IFunctionMetadataManager _functionMetadataManager; private readonly IMetricsLogger _metricsLogger; - private readonly Lazy> _startupTypes; + private readonly IEnvironment _environment; private readonly IOptions _extensionRequirementOptions; + private readonly Lazy> _startupTypes; private static string[] _builtinExtensionAssemblies = GetBuiltinExtensionAssemblies(); - public ScriptStartupTypeLocator(string rootScriptPath, ILogger logger, IExtensionBundleManager extensionBundleManager, - IFunctionMetadataManager functionMetadataManager, IMetricsLogger metricsLogger, IOptions extensionRequirementOptions) + public ScriptStartupTypeLocator( + string rootScriptPath, + ILogger logger, + IExtensionBundleManager extensionBundleManager, + IFunctionMetadataManager functionMetadataManager, + IMetricsLogger metricsLogger, + IEnvironment environment, + IOptions extensionRequirementOptions) { _rootScriptPath = rootScriptPath ?? throw new ArgumentNullException(nameof(rootScriptPath)); _extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager)); _logger = logger; _functionMetadataManager = functionMetadataManager; _metricsLogger = metricsLogger; - _startupTypes = new Lazy>(() => GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult()); + _environment = environment; _extensionRequirementOptions = extensionRequirementOptions; + _startupTypes = new Lazy>(() => GetExtensionsStartupTypesAsync().ConfigureAwait(false).GetAwaiter().GetResult()); } private static string[] GetBuiltinExtensionAssemblies() { - return new[] - { + return + [ typeof(WebJobs.Extensions.Http.HttpWebJobsStartup).Assembly.GetName().Name, typeof(WebJobs.Extensions.ExtensionsWebJobsStartup).Assembly.GetName().Name - }; + ]; } public Type[] GetStartupTypes() @@ -102,11 +110,11 @@ public async Task> GetExtensionsStartupTypesAsync() } } - bool isDotnetIsolatedApp = Utility.IsDotnetIsolatedApp(SystemEnvironment.Instance, functionMetadataCollection); + bool isDotnetIsolatedApp = Utility.IsDotnetIsolatedApp(_environment, functionMetadataCollection); bool isDotnetApp = isPrecompiledFunctionApp || isDotnetIsolatedApp; - var isLogicApp = SystemEnvironment.Instance.IsLogicApp(); + var isLogicApp = _environment.IsLogicApp(); - if (SystemEnvironment.Instance.IsPlaceholderModeEnabled()) + if (_environment.IsPlaceholderModeEnabled()) { // Do not move this. // Calling this log statement in the placeholder mode to avoid jitting during specialization diff --git a/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs b/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs index 9dd7edc4e6..e44cb89f8f 100644 --- a/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs +++ b/src/WebJobs.Script/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensions.cs @@ -60,13 +60,13 @@ private static IOpenTelemetryBuilder ConfigureMetrics(this IOpenTelemetryBuilder return builder.WithMetrics(builder => { builder.AddAspNetCoreInstrumentation() - .AddRuntimeInstrumentation() - .AddProcessInstrumentation() - .AddMeter(HostMetrics.FaasMeterName) - .AddView(HostMetrics.FaasInvokeDuration, new ExplicitBucketHistogramConfiguration - { - Boundaries = new double[] { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 } - }); + .AddRuntimeInstrumentation() + .AddProcessInstrumentation() + .AddMeter(HostMetrics.FaasMeterName) + .AddView(HostMetrics.FaasInvokeDuration, new ExplicitBucketHistogramConfiguration + { + Boundaries = new double[] { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 } + }); // Avoid configuring the exporter in placeholder mode, as it will default to sending telemetry to the predefined endpoint. These transmissions will be unsuccessful and create unnecessary noise. if (telemetryMode != TelemetryMode.Placeholder) diff --git a/src/WebJobs.Script/Extensions/ExceptionExtensions.cs b/src/WebJobs.Script/Extensions/ExceptionExtensions.cs index d60286aa5f..da1d36e79d 100644 --- a/src/WebJobs.Script/Extensions/ExceptionExtensions.cs +++ b/src/WebJobs.Script/Extensions/ExceptionExtensions.cs @@ -1,7 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System.Reflection; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Threading; using Microsoft.Azure.WebJobs.Host.Diagnostics; @@ -32,12 +33,22 @@ or SEHException public static string ToFormattedString(this Exception exception) { - if (exception == null) + ArgumentNullException.ThrowIfNull(exception); + return ExceptionFormatter.GetFormattedException(exception); + } + + public static void ThrowIfErrorsPresent(IList exceptions, string message = null) + { + switch (exceptions) { - throw new ArgumentNullException(nameof(exception)); + case null or []: + return; + case [Exception e]: + ExceptionDispatchInfo.Capture(e).Throw(); + return; + default: + throw new AggregateException(message, exceptions); } - - return ExceptionFormatter.GetFormattedException(exception); } } } diff --git a/src/WebJobs.Script/Host/IWorkerFunctionMetadataProvider.cs b/src/WebJobs.Script/Host/IWorkerFunctionMetadataProvider.cs index ace3c61ed1..b19b890ec8 100644 --- a/src/WebJobs.Script/Host/IWorkerFunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/IWorkerFunctionMetadataProvider.cs @@ -9,14 +9,14 @@ namespace Microsoft.Azure.WebJobs.Script { /// - /// Defines an interface for fetching function metadata from Out-of-Proc language workers + /// Defines an interface for fetching function metadata from Out-of-Proc language workers. /// internal interface IWorkerFunctionMetadataProvider { ImmutableDictionary> FunctionErrors { get; } /// - /// Attempts to get function metadata from Out-of-Proc language workers + /// Attempts to get function metadata from Out-of-Proc language workers. /// /// FunctionMetadataResult that either contains the function metadata or indicates that a fall back option for fetching metadata should be used Task GetFunctionMetadataAsync(IEnumerable workerConfigs, bool forceRefresh = false); diff --git a/src/WebJobs.Script/Host/ScriptHostState.cs b/src/WebJobs.Script/Host/ScriptHostState.cs index 2816f4f706..4fe31b9f95 100644 --- a/src/WebJobs.Script/Host/ScriptHostState.cs +++ b/src/WebJobs.Script/Host/ScriptHostState.cs @@ -6,7 +6,7 @@ namespace Microsoft.Azure.WebJobs.Script public enum ScriptHostState { /// - /// The host has not yet been created + /// The host has not yet been created. /// Default, @@ -28,7 +28,7 @@ public enum ScriptHostState Running, /// - /// The host is in an error state + /// The host is in an error state. /// Error, @@ -43,7 +43,7 @@ public enum ScriptHostState Stopped, /// - /// The host is offline + /// The host is offline. /// Offline } diff --git a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs index 22c2c7a0b3..d61b68db10 100644 --- a/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs +++ b/src/WebJobs.Script/Host/WorkerFunctionMetadataProvider.cs @@ -78,7 +78,7 @@ public async Task GetFunctionMetadataAsync(IEnumerable GetFunctionMetadataAsync(IEnumerable errors = null; foreach (string workerId in channels.Keys.ToList()) { if (channels.TryGetValue(workerId, out TaskCompletionSource languageWorkerChannelTask)) @@ -128,7 +129,7 @@ public async Task GetFunctionMetadataAsync(IEnumerable ValidateFunctionAppFormat(_scriptOptions.CurrentValue.ScriptPath, _logger, _environment)); @@ -139,9 +140,13 @@ public async Task GetFunctionMetadataAsync(IEnumerable(); - var locator = new ScriptStartupTypeLocator(applicationOptions.ScriptPath, loggerFactory.CreateLogger(), bundleManager, metadataServiceManager, metricsLogger, extensionRequirementOptions); + var locator = new ScriptStartupTypeLocator( + applicationOptions.ScriptPath, + loggerFactory.CreateLogger(), + bundleManager, + metadataServiceManager, + metricsLogger, + SystemEnvironment.Instance, + extensionRequirementOptions); // The locator (and thus the bundle manager) need to be created now in order to configure app configuration. // Store them so they do not need to be re-created later when configuring services. diff --git a/src/WebJobs.Script/Workers/ProcessManagement/IWorkerProcess.cs b/src/WebJobs.Script/Workers/ProcessManagement/IWorkerProcess.cs index dd5222e341..a39dad9cf5 100644 --- a/src/WebJobs.Script/Workers/ProcessManagement/IWorkerProcess.cs +++ b/src/WebJobs.Script/Workers/ProcessManagement/IWorkerProcess.cs @@ -2,8 +2,8 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Script.Scale; namespace Microsoft.Azure.WebJobs.Script.Workers { @@ -13,7 +13,9 @@ public interface IWorkerProcess Process Process { get; } - Task StartProcessAsync(); + Task StartProcessAsync(CancellationToken cancellationToken = default); + + Task WaitForExitAsync(CancellationToken cancellationToken = default); void WaitForProcessExitInMilliSeconds(int waitTime); } diff --git a/src/WebJobs.Script/Workers/ProcessManagement/WorkerProcess.cs b/src/WebJobs.Script/Workers/ProcessManagement/WorkerProcess.cs index 94a5208486..139055c916 100644 --- a/src/WebJobs.Script/Workers/ProcessManagement/WorkerProcess.cs +++ b/src/WebJobs.Script/Workers/ProcessManagement/WorkerProcess.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Scale; using Microsoft.Azure.WebJobs.Logging; @@ -33,10 +34,11 @@ internal abstract class WorkerProcess : IWorkerProcess, IDisposable private readonly IEnvironment _environment; private readonly IOptionsMonitor _scriptApplicationHostOptions; - private bool _useStdErrorStreamForErrorsOnly; - private Queue _processStdErrDataQueue = new Queue(3); + private readonly object _syncLock = new(); + private readonly bool _useStdErrorStreamForErrorsOnly; + private Queue _processStdErrDataQueue = new(3); private IHostProcessMonitor _processMonitor; - private object _syncLock = new object(); + private TaskCompletionSource _processExit; // used to hold custom exceptions on non-success exit. internal WorkerProcess(IScriptEventManager eventManager, IProcessRegistry processRegistry, ILogger workerProcessLogger, IWorkerConsoleLogSource consoleLogSource, IMetricsLogger metricsLogger, IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IEnvironment environment, IOptionsMonitor scriptApplicationHostOptions, bool useStdErrStreamForErrorsOnly = false) @@ -69,8 +71,9 @@ internal WorkerProcess(IScriptEventManager eventManager, IProcessRegistry proces internal abstract Process CreateWorkerProcess(); - public Task StartProcessAsync() + public Task StartProcessAsync(CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); using (_metricsLogger.LatencyEvent(MetricEventNames.ProcessStart)) { Process = CreateWorkerProcess(); @@ -79,11 +82,12 @@ public Task StartProcessAsync() AssignUserExecutePermissionsIfNotExists(); } + _processExit = new(); try { - Process.ErrorDataReceived += (sender, e) => OnErrorDataReceived(sender, e); - Process.OutputDataReceived += (sender, e) => OnOutputDataReceived(sender, e); - Process.Exited += (sender, e) => OnProcessExited(sender, e); + Process.ErrorDataReceived += OnErrorDataReceived; + Process.OutputDataReceived += OnOutputDataReceived; + Process.Exited += OnProcessExited; Process.EnableRaisingEvents = true; string sanitizedArguments = Sanitizer.Sanitize(Process.StartInfo.Arguments); @@ -103,12 +107,24 @@ public Task StartProcessAsync() } catch (Exception ex) { + _processExit.TrySetException(ex); _workerProcessLogger.LogError(ex, $"Failed to start Worker Channel. Process fileName: {Process.StartInfo.FileName}"); return Task.FromException(ex); } } } + public Task WaitForExitAsync(CancellationToken cancellationToken = default) + { + if (_processExit is { } tcs) + { + // We use a TaskCompletionSource (and not Process.WaitForExitAsync) so we can propagate our custom exceptions. + return tcs.Task.WaitAsync(cancellationToken); + } + + throw new InvalidOperationException("Process has not been started yet."); + } + private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) { if (e.Data != null) @@ -159,42 +175,58 @@ private void OnProcessExited(object sender, EventArgs e) if (Disposing) { - // No action needed return; } try { - if (Process.ExitCode == WorkerConstants.SuccessExitCode) - { - Process.WaitForExit(); - Process.Close(); - } - else if (Process.ExitCode == WorkerConstants.IntentionalRestartExitCode) + ThrowIfExitError(); + + Process.WaitForExit(); + if (Process.ExitCode == WorkerConstants.IntentionalRestartExitCode) { HandleWorkerProcessRestart(); } - else - { - string exceptionMessage = string.Join(",", _processStdErrDataQueue.Where(s => !string.IsNullOrEmpty(s))); - string sanitizedExceptionMessage = Sanitizer.Sanitize(exceptionMessage); - var processExitEx = new WorkerProcessExitException($"{Process.StartInfo.FileName} exited with code {Process.ExitCode} (0x{Process.ExitCode.ToString("X")})", new Exception(sanitizedExceptionMessage)); - processExitEx.ExitCode = Process.ExitCode; - processExitEx.Pid = Process.Id; - HandleWorkerProcessExitError(processExitEx); - } + } + catch (WorkerProcessExitException processExitEx) + { + _processExit.TrySetException(processExitEx); + HandleWorkerProcessExitError(processExitEx); } catch (Exception exc) { - _workerProcessLogger?.LogDebug(exc, "Exception on worker process exit. Process id: {processId}", Process?.Id); // ignore process is already disposed + _processExit.TrySetException(exc); + _workerProcessLogger?.LogDebug(exc, "Exception on worker process exit. Process id: {processId}", Process?.Id); } finally { + _processExit.TrySetResult(); UnregisterFromProcessMonitor(); + Process.Close(); } } + private void ThrowIfExitError() + { + if (Process.ExitCode is WorkerConstants.SuccessExitCode or WorkerConstants.IntentionalRestartExitCode) + { + return; + } + + string exceptionMessage = string.Join(",", _processStdErrDataQueue.Where(s => !string.IsNullOrEmpty(s))); + string sanitizedExceptionMessage = Sanitizer.Sanitize(exceptionMessage); + WorkerProcessExitException processExitEx = new( + $"{Process.StartInfo.FileName} exited with code {Process.ExitCode} (0x{Process.ExitCode:X})", + new Exception(sanitizedExceptionMessage)) + { + ExitCode = Process.ExitCode, + Pid = Process.Id + }; + + throw processExitEx; + } + private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) { if (e.Data != null) diff --git a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs index 76a9766503..189cf58341 100644 --- a/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs +++ b/src/WebJobs.Script/Workers/Rpc/RpcWorkerProcess.cs @@ -26,22 +26,23 @@ internal class RpcWorkerProcess : WorkerProcess private readonly IOptions _hostingConfigOptions; private readonly IEnvironment _environment; - internal RpcWorkerProcess(string runtime, - string workerId, - string rootScriptPath, - Uri serverUri, - RpcWorkerConfig rpcWorkerConfig, - IScriptEventManager eventManager, - IWorkerProcessFactory processFactory, - IProcessRegistry processRegistry, - ILogger workerProcessLogger, - IWorkerConsoleLogSource consoleLogSource, - IMetricsLogger metricsLogger, - IServiceProvider serviceProvider, - IOptions hostingConfigOptions, - IEnvironment environment, - IOptionsMonitor scriptApplicationHostOptions, - ILoggerFactory loggerFactory) + internal RpcWorkerProcess( + string runtime, + string workerId, + string rootScriptPath, + Uri serverUri, + RpcWorkerConfig rpcWorkerConfig, + IScriptEventManager eventManager, + IWorkerProcessFactory processFactory, + IProcessRegistry processRegistry, + ILogger workerProcessLogger, + IWorkerConsoleLogSource consoleLogSource, + IMetricsLogger metricsLogger, + IServiceProvider serviceProvider, + IOptions hostingConfigOptions, + IEnvironment environment, + IOptionsMonitor scriptApplicationHostOptions, + ILoggerFactory loggerFactory) : base(eventManager, processRegistry, workerProcessLogger, consoleLogSource, metricsLogger, serviceProvider, loggerFactory, environment, scriptApplicationHostOptions, rpcWorkerConfig.Description.UseStdErrorStreamForErrorsOnly) { @@ -74,23 +75,33 @@ internal override Process CreateWorkerProcess() internal override void HandleWorkerProcessExitError(WorkerProcessExitException rpcWorkerProcessExitException) { + ArgumentNullException.ThrowIfNull(rpcWorkerProcessExitException); if (Disposing) { return; } - if (rpcWorkerProcessExitException == null) - { - throw new ArgumentNullException(nameof(rpcWorkerProcessExitException)); - } + // The subscriber of WorkerErrorEvent is expected to Dispose() the errored channel - _workerProcessLogger.LogError(rpcWorkerProcessExitException, $"Language Worker Process exited. Pid={rpcWorkerProcessExitException.Pid}.", _workerProcessArguments.ExecutablePath); - _eventManager.Publish(new WorkerErrorEvent(_runtime, _workerId, rpcWorkerProcessExitException)); + _workerProcessLogger.LogError(rpcWorkerProcessExitException, $"Language Worker Process exited. Pid={rpcWorkerProcessExitException.Pid}.", _workerProcessArguments?.ExecutablePath); + PublishNoThrow(new WorkerErrorEvent(_runtime, _workerId, rpcWorkerProcessExitException)); } internal override void HandleWorkerProcessRestart() { _workerProcessLogger?.LogInformation("Language Worker Process exited and needs to be restarted."); - _eventManager.Publish(new WorkerRestartEvent(_runtime, _workerId)); + PublishNoThrow(new WorkerRestartEvent(_runtime, _workerId)); + } + + private void PublishNoThrow(RpcChannelEvent @event) + { + try + { + _eventManager.Publish(@event); + } + catch (Exception ex) + { + _workerProcessLogger.LogWarning(ex, "Failed to publish RpcChannelEvent."); + } } } -} \ No newline at end of file +} diff --git a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs index 317c33d4c7..649674b965 100644 --- a/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs +++ b/test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs @@ -104,35 +104,35 @@ public TestFunctionHost(string scriptPath, string logPath, string testDataPath = .AddFilter("Azure.Core", LogLevel.Warning); }) .ConfigureServices(services => - { - services.Replace(new ServiceDescriptor(typeof(ISecretManagerProvider), new TestSecretManagerProvider(new TestSecretManager()))); - services.Replace(new ServiceDescriptor(typeof(IOptions), sp => - { - _hostOptions.RootServiceProvider = sp; - return new OptionsWrapper(_hostOptions); - }, ServiceLifetime.Singleton)); - services.Replace(new ServiceDescriptor(typeof(IOptionsMonitor), sp => - { - _hostOptions.RootServiceProvider = sp; - return TestHelpers.CreateOptionsMonitor(_hostOptions); - }, ServiceLifetime.Singleton)); - services.Replace(new ServiceDescriptor(typeof(IExtensionBundleManager), new TestExtensionBundleManager())); - services.Replace(new ServiceDescriptor(typeof(IFunctionMetadataManager), sp => - { - var montior = sp.GetService>(); - var scriptManager = sp.GetService(); - var loggerFactory = sp.GetService(); - var environment = sp.GetService(); - - return GetMetadataManager(montior, scriptManager, loggerFactory, environment); - }, ServiceLifetime.Singleton)); - - services.AddSingleton(); - services.SkipDependencyValidation(); - - // Allows us to configure services as the last step, thereby overriding anything - services.AddSingleton(new PostConfigureServices(configureWebHostServices)); - }) + { + services.Replace(new ServiceDescriptor(typeof(ISecretManagerProvider), new TestSecretManagerProvider(new TestSecretManager()))); + services.Replace(new ServiceDescriptor(typeof(IOptions), sp => + { + _hostOptions.RootServiceProvider = sp; + return new OptionsWrapper(_hostOptions); + }, ServiceLifetime.Singleton)); + services.Replace(new ServiceDescriptor(typeof(IOptionsMonitor), sp => + { + _hostOptions.RootServiceProvider = sp; + return TestHelpers.CreateOptionsMonitor(_hostOptions); + }, ServiceLifetime.Singleton)); + services.Replace(new ServiceDescriptor(typeof(IExtensionBundleManager), new TestExtensionBundleManager())); + services.Replace(new ServiceDescriptor(typeof(IFunctionMetadataManager), sp => + { + var montior = sp.GetService>(); + var scriptManager = sp.GetService(); + var loggerFactory = sp.GetService(); + var environment = sp.GetService(); + + return GetMetadataManager(montior, scriptManager, loggerFactory, environment); + }, ServiceLifetime.Singleton)); + + services.AddSingleton(); + services.SkipDependencyValidation(); + + // Allows us to configure services as the last step, thereby overriding anything + services.AddSingleton(new PostConfigureServices(configureWebHostServices)); + }) .ConfigureScriptHostWebJobsBuilder(scriptHostWebJobsBuilder => { /// REVIEW THIS diff --git a/test/WebJobs.Script.Tests.Shared/TempDirectory.cs b/test/WebJobs.Script.Tests.Shared/TempDirectory.cs index 22acb59fa5..4cb858159b 100644 --- a/test/WebJobs.Script.Tests.Shared/TempDirectory.cs +++ b/test/WebJobs.Script.Tests.Shared/TempDirectory.cs @@ -19,25 +19,9 @@ public TempDirectory(string path) Directory.CreateDirectory(path); } - ~TempDirectory() - { - Dispose(false); - } - public string Path { get; } public void Dispose() - { - GC.SuppressFinalize(this); - Dispose(true); - } - - private void Dispose(bool disposing) - { - DeleteDirectory(); - } - - private void DeleteDirectory() { try { diff --git a/test/WebJobs.Script.Tests.Shared/TestHelpers.cs b/test/WebJobs.Script.Tests.Shared/TestHelpers.cs index ca98a157e7..dae1a68104 100644 --- a/test/WebJobs.Script.Tests.Shared/TestHelpers.cs +++ b/test/WebJobs.Script.Tests.Shared/TestHelpers.cs @@ -289,23 +289,26 @@ private static async Task ReadAllLinesSafeAsync(string logFile) public static async Task ReadStreamToEnd(Stream stream) { stream.Position = 0; - using (var sr = new StreamReader(stream)) - { - return await sr.ReadToEndAsync(); - } + using var sr = new StreamReader(stream); + return await sr.ReadToEndAsync(); } - public static IList GetTestWorkerConfigs(bool includeDllWorker = false, int processCountValue = 1, - TimeSpan? processStartupInterval = null, TimeSpan? processRestartInterval = null, TimeSpan? processShutdownTimeout = null, bool workerIndexing = false) + public static IList GetTestWorkerConfigs( + bool includeDllWorker = false, + int processCountValue = 1, + TimeSpan? processStartupInterval = null, + TimeSpan? processRestartInterval = null, + TimeSpan? processShutdownTimeout = null, + bool workerIndexing = false) { var defaultCountOptions = new WorkerProcessCountOptions(); TimeSpan startupInterval = processStartupInterval ?? defaultCountOptions.ProcessStartupInterval; TimeSpan restartInterval = processRestartInterval ?? defaultCountOptions.ProcessRestartInterval; TimeSpan shutdownTimeout = processShutdownTimeout ?? defaultCountOptions.ProcessShutdownTimeout; - var workerConfigs = new List - { - new RpcWorkerConfig + List workerConfigs = + [ + new() { Description = GetTestWorkerDescription("node", ".js", workerIndexing), CountOptions = new WorkerProcessCountOptions @@ -316,7 +319,7 @@ public static IList GetTestWorkerConfigs(bool includeDllWorker ProcessShutdownTimeout = shutdownTimeout } }, - new RpcWorkerConfig + new() { Description = GetTestWorkerDescription("java", ".jar", workerIndexing), CountOptions = new WorkerProcessCountOptions @@ -327,7 +330,7 @@ public static IList GetTestWorkerConfigs(bool includeDllWorker ProcessShutdownTimeout = shutdownTimeout } } - }; + ]; // Allow tests to have a worker that claims the .dll extension. if (includeDllWorker) diff --git a/test/WebJobs.Script.Tests/Configuration/DefaultDependencyValidatorTests.cs b/test/WebJobs.Script.Tests/Configuration/DefaultDependencyValidatorTests.cs index 476bbc33af..3de0c8e665 100644 --- a/test/WebJobs.Script.Tests/Configuration/DefaultDependencyValidatorTests.cs +++ b/test/WebJobs.Script.Tests/Configuration/DefaultDependencyValidatorTests.cs @@ -28,7 +28,7 @@ public class DefaultDependencyValidatorTests [Fact] public async Task Validator_AllValid() { - LogMessage invalidServicesMessage = await RunTest(); + LogMessage invalidServicesMessage = await RunTestAsync(); string msg = $"If you have registered new dependencies, make sure to update the DependencyValidator. Invalid Services:{Environment.NewLine}"; Assert.True(invalidServicesMessage == null, msg + invalidServicesMessage?.Exception?.ToString()); @@ -37,7 +37,7 @@ public async Task Validator_AllValid() [Fact] public async Task Validator_InvalidServices_ThrowsException() { - LogMessage invalidServicesMessage = await RunTest(configureJobHost: s => + LogMessage invalidServicesMessage = await RunTestAsync(configureJobHost: s => { s.AddSingleton(); s.AddSingleton(); @@ -61,7 +61,7 @@ public async Task Validator_InvalidServices_ThrowsException() [Fact] public async Task Validator_NoJobHost() { - LogMessage invalidServicesMessage = await RunTest(configureWebHost: s => + LogMessage invalidServicesMessage = await RunTestAsync(configureWebHost: s => { // This will force us to skip host startup (which removes the JobHost) s.AddSingleton(); @@ -71,10 +71,10 @@ public async Task Validator_NoJobHost() Assert.True(invalidServicesMessage == null, msg + invalidServicesMessage?.Exception?.ToString()); } - private async Task RunTest(Action configureWebHost = null, Action configureJobHost = null, bool expectSuccess = true) + private async Task RunTestAsync(Action configureWebHost = null, Action configureJobHost = null, bool expectSuccess = true) { LogMessage invalidServicesMessage = null; - TestLoggerProvider loggerProvider = new TestLoggerProvider(); + TestLoggerProvider loggerProvider = new(); var builder = Program.CreateWebHostBuilder(null) .ConfigureLogging(b => diff --git a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsSetupTest.cs b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsSetupTest.cs index bac23b01c4..bf5dc64ca8 100644 --- a/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsSetupTest.cs +++ b/test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsSetupTest.cs @@ -46,7 +46,7 @@ public void Configure_DoesNotSet_Options_IfFileEmpty() [Fact] public void Configure_DoesNotSet_Options_IfFileDoesNotExist() { - string fileName = Path.Combine("C://settings.txt"); + string fileName = Path.Combine(Path.GetTempPath(), "settings.txt"); IConfiguration configuraton = GetConfiguration(fileName, string.Empty); FunctionsHostingConfigOptionsSetup setup = new FunctionsHostingConfigOptionsSetup(configuraton); FunctionsHostingConfigOptions options = new FunctionsHostingConfigOptions(); diff --git a/test/WebJobs.Script.Tests/Description/FunctionInvokerBaseTests.cs b/test/WebJobs.Script.Tests/Description/FunctionInvokerBaseTests.cs index ef62d61f7e..b957a0e53c 100644 --- a/test/WebJobs.Script.Tests/Description/FunctionInvokerBaseTests.cs +++ b/test/WebJobs.Script.Tests/Description/FunctionInvokerBaseTests.cs @@ -9,53 +9,49 @@ using Microsoft.Azure.WebJobs.Host.Loggers; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Diagnostics; -using Microsoft.Azure.WebJobs.Script.Eventing; using Microsoft.Azure.WebJobs.Script.Metrics; using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics; -using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.WebJobs.Script.Tests; -using Moq; using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.Azure.WebJobs.Script.Tests { - public class FunctionInvokerBaseTests : IDisposable + public sealed class FunctionInvokerBaseTests : IDisposable { - private MockInvoker _invoker; - private IHost _host; - private ScriptHost _scriptHost; - private TestMetricsLogger _metricsLogger; - private TestLoggerProvider _testLoggerProvider; + private readonly TestMetricsLogger _metricsLogger = new(); + private readonly TestLoggerProvider _testLoggerProvider = new(); + private readonly MockInvoker _invoker; + private readonly IHost _host; + private readonly ScriptHost _scriptHost; public FunctionInvokerBaseTests() { - _metricsLogger = new TestMetricsLogger(); - _testLoggerProvider = new TestLoggerProvider(); - - ILoggerFactory loggerFactory = new LoggerFactory(); + LoggerFactory loggerFactory = new(); loggerFactory.AddProvider(_testLoggerProvider); - var eventManager = new ScriptEventManager(); - - var metadata = new FunctionMetadata + FunctionMetadata metadata = new() { Name = "TestFunction", ScriptFile = "index.js", Language = "node" }; + JObject binding = JObject.FromObject(new { type = "manualTrigger", name = "manual", direction = "in" }); + metadata.Bindings.Add(BindingMetadata.Create(binding)); - var metadataManager = new MockMetadataManager(new[] { metadata }); + MockMetadataManager metadataManager = new([metadata]); + + // TODO: Can we instantiate a ScriptHost directly? _host = new HostBuilder() .ConfigureDefaultTestWebScriptHost() .ConfigureServices(s => @@ -66,10 +62,7 @@ public FunctionInvokerBaseTests() .Build(); _scriptHost = _host.GetScriptHost(); - _scriptHost.InitializeAsync().Wait(); - var hostMetrics = _host.Services.GetService(); - _invoker = new MockInvoker(_scriptHost, _metricsLogger, hostMetrics, metadataManager, metadata, loggerFactory); } @@ -182,17 +175,16 @@ await Assert.ThrowsAsync(async () => Assert.Equal(startLatencyEvent, completedLatencyEvent); } - protected virtual void Dispose(bool disposing) + public void Dispose() { - if (disposing) + try { _host?.Dispose(); } - } - - public void Dispose() - { - Dispose(true); + catch (Exception) + { + // this might throw due to invalid setup. + } } private class MockInvoker : FunctionInvokerBase diff --git a/test/WebJobs.Script.Tests/Extensions/EnvironmentExtensionsTests.cs b/test/WebJobs.Script.Tests/Extensions/EnvironmentExtensionsTests.cs index 9ea8bcf5d5..f0743813bd 100644 --- a/test/WebJobs.Script.Tests/Extensions/EnvironmentExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Extensions/EnvironmentExtensionsTests.cs @@ -14,16 +14,24 @@ namespace Microsoft.Azure.WebJobs.Script.Tests.Extensions public class EnvironmentExtensionsTests { [Fact] - public void GetEffectiveCoresCount_ReturnsExpectedResult() + public void GetEffectiveCoresCount_NoSku_ReturnsExpectedResult() { - TestEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); Assert.Equal(Environment.ProcessorCount, EnvironmentExtensions.GetEffectiveCoresCount(env)); + } - env.Clear(); + [Fact] + public void GetEffectiveCoresCount_DynamicSku_ReturnsExpectedResult() + { + TestEnvironment env = new(); env.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteSku, ScriptConstants.DynamicSku); Assert.Equal(1, EnvironmentExtensions.GetEffectiveCoresCount(env)); + } - env.Clear(); + [Fact] + public void GetEffectiveCoresCount_DynamicSkuWithInstanceId_ReturnsExpectedResult() + { + TestEnvironment env = new(); env.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteSku, ScriptConstants.DynamicSku); env.SetEnvironmentVariable(EnvironmentSettingNames.RoleInstanceId, "dw0SmallDedicatedWebWorkerRole_hr0HostRole-0-VM-1"); Assert.Equal(Environment.ProcessorCount, EnvironmentExtensions.GetEffectiveCoresCount(env)); @@ -33,7 +41,7 @@ public void GetEffectiveCoresCount_ReturnsExpectedResult() [Trait(TestTraits.Group, TestTraits.AdminIsolationTests)] public void IsAdminIsolationEnabled_ReturnsExpectedResult() { - TestEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); Assert.False(EnvironmentExtensions.IsAdminIsolationEnabled(env)); env.SetEnvironmentVariable(EnvironmentSettingNames.FunctionsAdminIsolationEnabled, "0"); @@ -47,9 +55,9 @@ public void IsAdminIsolationEnabled_ReturnsExpectedResult() [InlineData("dw0SmallDedicatedWebWorkerRole_hr0HostRole-0-VM-1", true)] [InlineData(null, false)] [InlineData("", false)] - public void IsVMSS_RetrunsExpectedResult(string roleInstanceId, bool expected) + public void IsVMSS_ReturnsExpectedResult(string roleInstanceId, bool expected) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (roleInstanceId != null) { env.SetEnvironmentVariable(EnvironmentSettingNames.RoleInstanceId, roleInstanceId); @@ -67,7 +75,7 @@ public void IsVMSS_RetrunsExpectedResult(string roleInstanceId, bool expected) [InlineData("Foo,Bar", false)] public void IsAzureMonitorEnabled_ReturnsExpectedResult(string value, bool expected) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (value != null) { env.SetEnvironmentVariable(EnvironmentSettingNames.AzureMonitorCategories, value); @@ -82,7 +90,7 @@ public void IsAzureMonitorEnabled_ReturnsExpectedResult(string value, bool expec [InlineData(null, "")] public void GetAntaresComputerName_ReturnsExpectedResult(string computerName, string expectedComputerName) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (!string.IsNullOrEmpty(expectedComputerName)) { env.SetEnvironmentVariable(EnvironmentSettingNames.AntaresComputerName, computerName); @@ -102,7 +110,7 @@ public void GetAntaresComputerName_ReturnsExpectedResult(string computerName, st [InlineData(false, "", null, "")] public void GetInstanceId_ReturnsExpectedResult(bool isLinuxConsumption, string containerName, string websiteInstanceId, string expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (isLinuxConsumption) { if (!string.IsNullOrEmpty(containerName)) @@ -133,7 +141,7 @@ public void GetInstanceId_ReturnsExpectedResult(bool isLinuxConsumption, string [InlineData(false, false, "89.0.7.73", null, "")] public void GetAntaresVersion_ReturnsExpectedResult(bool isLinuxConsumption, bool isLinuxAppService, string platformVersionLinux, string platformVersionWindows, string expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (isLinuxConsumption) { env.SetEnvironmentVariable(EnvironmentSettingNames.ContainerName, "RandomContainerName"); @@ -166,7 +174,7 @@ public void GetAntaresVersion_ReturnsExpectedResult(bool isLinuxConsumption, boo [InlineData(true, true, false, true)] public void IsConsumptionSku_ReturnsExpectedResult(bool isLinuxConsumption, bool isWindowsConsumption, bool isFlexConsumption, bool expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (isLinuxConsumption) { env.SetEnvironmentVariable(EnvironmentSettingNames.ContainerName, "RandomContainerName"); @@ -197,7 +205,7 @@ public void IsConsumptionSku_ReturnsExpectedResult(bool isLinuxConsumption, bool [InlineData("", "", "", "", false)] public void IsFlexConsumptionSku_ReturnsExpectedResult(string sku, string websiteInstanceId, string containerName, string legionServiceHost, bool expected) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); env.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteSku, sku); env.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteInstanceId, websiteInstanceId); env.SetEnvironmentVariable(EnvironmentSettingNames.ContainerName, containerName); @@ -218,7 +226,7 @@ public void IsFlexConsumptionSku_ReturnsExpectedResult(string sku, string websit [InlineData("RandomContainerName", null, null, true, null, false)] // Managed App Environment public void IsAnyLinuxConsumption_ReturnsExpectedResult(string containerName, string podName, string legionServiceHostName, bool isManagedAppEnvironment, string sku, bool expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (!string.IsNullOrEmpty(containerName)) { env.SetEnvironmentVariable(ContainerName, containerName); @@ -253,7 +261,7 @@ public void IsAnyLinuxConsumption_ReturnsExpectedResult(string containerName, st [InlineData(false, false, false)] public void IsAnyKubernetesEnvironment_ReturnsExpectedResult(bool isKubernetesManagedHosting, bool isManagedAppEnvironment, bool expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (isKubernetesManagedHosting) { env.SetEnvironmentVariable(KubernetesServiceHost, "10.0.0.1"); @@ -273,7 +281,7 @@ public void IsAnyKubernetesEnvironment_ReturnsExpectedResult(bool isKubernetesMa [InlineData(false, false)] public void IsManagedAppEnvironment_ReturnsExpectedResult(bool isManagedAppEnvironment, bool expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (isManagedAppEnvironment) { env.SetEnvironmentVariable(EnvironmentSettingNames.ManagedEnvironment, "true"); @@ -295,7 +303,7 @@ public void IsManagedAppEnvironment_ReturnsExpectedResult(bool isManagedAppEnvir [InlineData("RandomPodName", "RandomLegionServiceHostName", ScriptConstants.DynamicSku, null, true)] public void IsLinuxConsumptionOnLegion_ReturnsExpectedResult(string websitePodName, string legionServiceHostName, string websiteSku, string websiteSkuName, bool expectedValue) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (!string.IsNullOrEmpty(websitePodName)) { @@ -330,7 +338,7 @@ public void IsLinuxConsumptionOnLegion_ReturnsExpectedResult(string websitePodNa [InlineData(null, null, false)] public void IsV2CompatMode(string extensionVersion, string compatMode, bool expected) { - IEnvironment env = new TestEnvironment(); + TestEnvironment env = new(); if (extensionVersion != null) { @@ -353,7 +361,7 @@ public void IsV2CompatMode(string extensionVersion, string compatMode, bool expe [InlineData("k8se-apps", "10.0.0.1", true, false)] public void IsKubernetesManagedHosting_ReturnsExpectedResult(string podNamespace, string kubernetesServiceHost, bool isManagedAppEnvironment, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(KubernetesServiceHost, kubernetesServiceHost); environment.SetEnvironmentVariable(PodNamespace, podNamespace); if (isManagedAppEnvironment) @@ -374,7 +382,7 @@ public void IsKubernetesManagedHosting_ReturnsExpectedResult(string podNamespace [InlineData("", "")] public void Returns_WorkerRuntime(string workerRuntime, string expectedWorkerRuntime) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(FunctionWorkerRuntime, workerRuntime); Assert.Equal(expectedWorkerRuntime, environment.GetFunctionsWorkerRuntime()); } @@ -388,9 +396,9 @@ public void Returns_WorkerRuntime(string workerRuntime, string expectedWorkerRun [InlineData("", "")] public void Returns_FunctionsExtensionVersion(string functionsExtensionVersion, string functionsExtensionVersionExpected) { - var enviroment = new TestEnvironment(); - enviroment.SetEnvironmentVariable(FunctionsExtensionVersion, functionsExtensionVersion); - Assert.Equal(functionsExtensionVersionExpected, enviroment.GetFunctionsExtensionVersion()); + TestEnvironment environment = new(); + environment.SetEnvironmentVariable(FunctionsExtensionVersion, functionsExtensionVersion); + Assert.Equal(functionsExtensionVersionExpected, environment.GetFunctionsExtensionVersion()); } [Theory] @@ -407,7 +415,7 @@ public void Returns_FunctionsExtensionVersion(string functionsExtensionVersion, [InlineData("", false, false, false)] public void Returns_SupportsAzureFileShareMount(string workerRuntime, bool useLowerCase, bool useUpperCase, bool supportsAzureFileShareMount) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); if (useLowerCase && !useUpperCase) { workerRuntime = workerRuntime.ToLowerInvariant(); @@ -427,7 +435,7 @@ public void Returns_SupportsAzureFileShareMount(string workerRuntime, bool useLo [InlineData("", "")] public void Returns_GetHttpLeaderEndpoint(string httpLeaderEndpoint, string expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); if (!string.IsNullOrEmpty(httpLeaderEndpoint)) { @@ -448,7 +456,7 @@ public void Returns_GetHttpLeaderEndpoint(string httpLeaderEndpoint, string expe [InlineData("10.0.0.1", null, true)] public void IsDrainOnApplicationStopping_ReturnsExpectedResult(string serviceHostValue, string drainOnStoppingValue, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(KubernetesServiceHost, serviceHostValue); environment.SetEnvironmentVariable(DrainOnApplicationStopping, drainOnStoppingValue); Assert.Equal(expected, environment.DrainOnApplicationStoppingEnabled()); @@ -461,7 +469,7 @@ public void IsDrainOnApplicationStopping_ReturnsExpectedResult(string serviceHos [InlineData("true", null, true)] public void IsWorkerDynamicConcurrencyEnabled_ReturnsExpectedResult(string concurrencyEnabledValue, string processCountValue, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionsWorkerDynamicConcurrencyEnabled, concurrencyEnabledValue); environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionsWorkerProcessCountSettingName, processCountValue); Assert.Equal(expected, environment.IsWorkerDynamicConcurrencyEnabled()); @@ -476,7 +484,7 @@ public void IsWorkerDynamicConcurrencyEnabled_ReturnsExpectedResult(string concu [InlineData("node", "", "node")] public void GetLanguageWorkerListToStartInPlaceholder_ReturnsExpectedResult(string workerRuntime, string workerRuntimeList, string expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerRuntimeSettingName, workerRuntime); environment.SetEnvironmentVariable(RpcWorkerConstants.FunctionWorkerPlaceholderModeListSettingName, workerRuntimeList); var resultSet = environment.GetLanguageWorkerListToStartInPlaceholder(); @@ -496,7 +504,7 @@ public void GetLanguageWorkerListToStartInPlaceholder_ReturnsExpectedResult(stri [InlineData("test", "test", true)] public void AzureFilesAppSettingsExist_ReturnsExpectedResult(string connectionString, string contentShare, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); environment.SetEnvironmentVariable(AzureFilesConnectionString, connectionString); environment.SetEnvironmentVariable(AzureFilesContentShare, contentShare); Assert.Equal(expected, environment.AzureFilesAppSettingsExist()); @@ -512,7 +520,7 @@ public void AzureFilesAppSettingsExist_ReturnsExpectedResult(string connectionSt [InlineData("node", false)] public void IsInProc_ReturnsExpectedResult(string value, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); if (value != null) { environment.SetEnvironmentVariable(FunctionWorkerRuntime, value); @@ -530,7 +538,7 @@ public void IsInProc_ReturnsExpectedResult(string value, bool expected) [InlineData("node", false)] public void IsInProc_WithRuntimeParameter_ReturnsExpectedResult(string value, bool expected) { - var environment = new TestEnvironment(); + TestEnvironment environment = new(); Assert.Equal(expected, environment.IsInProc(value)); } diff --git a/test/WebJobs.Script.Tests/FunctionMetadataProviderTests.cs b/test/WebJobs.Script.Tests/FunctionMetadataProviderTests.cs index 277de03c16..eca068cd5f 100644 --- a/test/WebJobs.Script.Tests/FunctionMetadataProviderTests.cs +++ b/test/WebJobs.Script.Tests/FunctionMetadataProviderTests.cs @@ -1,11 +1,8 @@ // 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.Collections.Immutable; -using System.IO; -using System.Linq; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Description; @@ -31,95 +28,97 @@ public FunctionMetadataProviderTests() } [Fact] - public void GetFunctionMetadataAsync_WorkerIndexing_HostFallback() + public async Task GetFunctionMetadataAsync_WorkerIndexing_HostFallback() { // Arrange _logger.ClearLogMessages(); + ImmutableArray functionMetadataCollection = GetTestFunctionMetadata(); + IList workerConfigs = TestHelpers.GetTestWorkerConfigs(); + foreach (RpcWorkerConfig config in workerConfigs) + { + config.Description.WorkerIndexing = "true"; + } - var function = GetTestRawFunctionMetadata(useDefaultMetadataIndexing: true); - IEnumerable rawFunctionMetadataCollection = new List() { function }; - var functionMetadataCollection = new List(); - functionMetadataCollection.Add(GetTestFunctionMetadata()); - - var workerConfigs = TestHelpers.GetTestWorkerConfigs().ToImmutableArray(); - workerConfigs.ToList().ForEach(config => config.Description.WorkerIndexing = "true"); - var scriptjobhostoptions = new ScriptJobHostOptions(); - scriptjobhostoptions.RootScriptPath = Path.Combine(Environment.CurrentDirectory, @"..", "..", "..", "..", "sample", "node"); - - var environment = SystemEnvironment.Instance; + TestEnvironment environment = new(); environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "node"); environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, "EnableWorkerIndexing"); - var defaultProvider = new FunctionMetadataProvider(_logger, _workerFunctionMetadataProvider.Object, _hostFunctionMetadataProvider.Object, new OptionsWrapper(new FunctionsHostingConfigOptions()), SystemEnvironment.Instance); + FunctionMetadataProvider defaultProvider = new( + _logger, + _workerFunctionMetadataProvider.Object, + _hostFunctionMetadataProvider.Object, + new OptionsWrapper(new FunctionsHostingConfigOptions()), + environment); - FunctionMetadataResult result = new FunctionMetadataResult(true, functionMetadataCollection.ToImmutableArray()); - _workerFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)).Returns(Task.FromResult(result)); - _hostFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)).Returns(Task.FromResult(functionMetadataCollection.ToImmutableArray())); + FunctionMetadataResult result = new(true, functionMetadataCollection); + _workerFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)) + .ReturnsAsync(result); + _hostFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)) + .ReturnsAsync(functionMetadataCollection); // Act - var functions = defaultProvider.GetFunctionMetadataAsync(workerConfigs, false).GetAwaiter().GetResult(); + ImmutableArray functions = await defaultProvider + .GetFunctionMetadataAsync(workerConfigs, false); // Assert Assert.Equal(1, functions.Length); - var traces = _logger.GetLogMessages(); - var functionLoadLogs = traces.Where(m => string.Equals(m.FormattedMessage, "Fallback to host indexing as worker denied indexing")); - Assert.True(functionLoadLogs.Any()); + Assert.Contains( + _logger.GetLogMessages(), + m => string.Equals(m.FormattedMessage, "Fallback to host indexing as worker denied indexing")); } [Fact] - public void GetFunctionMetadataAsync_HostIndexing() + public async Task GetFunctionMetadataAsync_HostIndexing() { // Arrange _logger.ClearLogMessages(); + ImmutableArray functionMetadataCollection = GetTestFunctionMetadata(); + IList workerConfigs = TestHelpers.GetTestWorkerConfigs(); + foreach (RpcWorkerConfig config in workerConfigs) + { + config.Description.WorkerIndexing = "true"; + } - var function = GetTestRawFunctionMetadata(useDefaultMetadataIndexing: true); - IEnumerable rawFunctionMetadataCollection = new List() { function }; - var functionMetadataCollection = new List(); - functionMetadataCollection.Add(GetTestFunctionMetadata()); - - var workerConfigs = TestHelpers.GetTestWorkerConfigs().ToImmutableArray(); - workerConfigs.ToList().ForEach(config => config.Description.WorkerIndexing = "true"); - var scriptjobhostoptions = new ScriptJobHostOptions(); - scriptjobhostoptions.RootScriptPath = Path.Combine(Environment.CurrentDirectory, @"..", "..", "..", "..", "sample", "node"); - - var environment = SystemEnvironment.Instance; + TestEnvironment environment = new(); environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "node"); environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, string.Empty); - var optionsMonitor = TestHelpers.CreateOptionsMonitor(new FunctionsHostingConfigOptions()); - var workerMetadataProvider = new Mock(); - workerMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(It.IsAny>(), false)).Returns(Task.FromResult(new FunctionMetadataResult(true, ImmutableArray.Empty))); + Mock workerMetadataProvider = new(); + workerMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(It.IsAny>(), false)) + .ReturnsAsync(new FunctionMetadataResult(true, [])); - var defaultProvider = new FunctionMetadataProvider(_logger, workerMetadataProvider.Object, _hostFunctionMetadataProvider.Object, new OptionsWrapper(new FunctionsHostingConfigOptions()), SystemEnvironment.Instance); + FunctionMetadataProvider defaultProvider = new( + _logger, + workerMetadataProvider.Object, + _hostFunctionMetadataProvider.Object, + new OptionsWrapper(new FunctionsHostingConfigOptions()), + environment); - FunctionMetadataResult result = new FunctionMetadataResult(true, functionMetadataCollection.ToImmutableArray()); - _hostFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)).Returns(Task.FromResult(functionMetadataCollection.ToImmutableArray())); + FunctionMetadataResult result = new(true, functionMetadataCollection); + _hostFunctionMetadataProvider.Setup(m => m.GetFunctionMetadataAsync(workerConfigs, false)) + .ReturnsAsync(functionMetadataCollection); // Act - var functions = defaultProvider.GetFunctionMetadataAsync(workerConfigs, false).GetAwaiter().GetResult(); + ImmutableArray functions = await defaultProvider + .GetFunctionMetadataAsync(workerConfigs, false); // Assert Assert.Equal(1, functions.Length); - var traces = _logger.GetLogMessages(); - var functionLoadLogs = traces.Where(m => string.Equals(m.FormattedMessage, "Fallback to host indexing as worker denied indexing")); - Assert.True(functionLoadLogs.Any()); + Assert.Contains( + _logger.GetLogMessages(), + m => string.Equals(m.FormattedMessage, "Fallback to host indexing as worker denied indexing")); } - private static RawFunctionMetadata GetTestRawFunctionMetadata(bool useDefaultMetadataIndexing) + private static ImmutableArray GetTestFunctionMetadata(string name = "testFunction") { - return new RawFunctionMetadata() - { - UseDefaultMetadataIndexing = useDefaultMetadataIndexing - }; - } - - private static FunctionMetadata GetTestFunctionMetadata(string name = "testFunction") - { - return new FunctionMetadata() - { - Name = name, - Language = "node" - }; + return + [ + new FunctionMetadata() + { + Name = name, + Language = "node" + } + ]; } } } \ No newline at end of file diff --git a/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs b/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs index f43eef9209..4bf3e53d2a 100644 --- a/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs +++ b/test/WebJobs.Script.Tests/ScriptStartupTypeDiscovererTests.cs @@ -28,39 +28,43 @@ namespace Microsoft.Azure.WebJobs.Script.Tests { - public class ScriptStartupTypeDiscovererTests + public sealed class ScriptStartupTypeDiscovererTests : IDisposable { + private readonly TempDirectory _directory = new(); + private readonly TestMetricsLogger _metricsLogger = new(); + private readonly TestLoggerProvider _loggerProvider = new(); + private readonly TestEnvironment _environment = new(); + private readonly Mock _bundleManager = new(); + private readonly Mock _metadataManager = new(); + + public ScriptStartupTypeDiscovererTests() + { + SetupMetadataManager(null); + } + + public void Dispose() + { + _directory.Dispose(); + } + [Fact] public async Task GetExtensionsStartupTypes_UsesDefaultMinVersion() { - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails("2.1.0"))); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - var traces = testLoggerProvider.GetAllLogMessages(); - - // Assert - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 2.1.0 does not meet the required minimum version of 2.6.1. Update your extension bundle reference in host.json to reference 2.6.1 or later."))); - } + InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + var binPath = Path.Combine(_directory.Path, "bin"); + + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails("2.1.0")); + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var exception = await Assert.ThrowsAsync(discoverer.GetExtensionsStartupTypesAsync); + var traces = _loggerProvider.GetAllLogMessages(); + + // Assert + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 2.1.0 does not meet the required minimum version of 2.6.1. Update your extension bundle reference in host.json to reference 2.6.1 or later."))); } [Theory] @@ -68,46 +72,32 @@ public async Task GetExtensionsStartupTypes_UsesDefaultMinVersion() [InlineData("2.6.1", "2.1.0")] public async Task GetExtensionsStartupTypes_RejectsBundleConfiguredviaHostingEnvConfig(string expectedBundleVersion, string actualBundleVersion) { - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); + var binPath = Path.Combine(_directory.Path, "bin"); - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails(actualBundleVersion))); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails(actualBundleVersion)); - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - - var extensionRequirementOptions = new Config.ExtensionRequirementOptions(); - extensionRequirementOptions.Bundles = new List() - { - new BundleRequirement() + ExtensionRequirementOptions extensionRequirementOptions = new() + { + Bundles = + [ + new() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = expectedBundleVersion } - }; - - OptionsWrapper optionsWrapper = new(extensionRequirementOptions); - - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); + ] + }; - // Act - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(extensionRequirements: extensionRequirementOptions); + var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); + var traces = _loggerProvider.GetAllLogMessages(); - // Assert - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version {actualBundleVersion} does not meet the required minimum version of {expectedBundleVersion}. Update your extension bundle reference in host.json to reference {expectedBundleVersion} or later."))); - } + // Assert + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version {actualBundleVersion} does not meet the required minimum version of {expectedBundleVersion}. Update your extension bundle reference in host.json to reference {expectedBundleVersion} or later."))); } [Theory] @@ -118,72 +108,38 @@ public async Task GetExtensionsStartupTypes_RejectsBundleConfiguredviaHostingEnv public async Task GetExtensionsStartupTypes_AcceptsRequiredBundleVersions(string minBundleVersion, string actualBundleVersion, string minExtensionVersion) { // "TypeName": , Microsoft.Azure.WebJobs.Extensions.Storage, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - - if (string.IsNullOrEmpty(actualBundleVersion)) - { - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - } - else - { - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - } - - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails(actualBundleVersion))); + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); + if (string.IsNullOrEmpty(actualBundleVersion)) + { + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); + } + else + { + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + } - var extensionRequirementOptions = new Config.ExtensionRequirementOptions(); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails(actualBundleVersion)); - IEnumerable bundleRequirment = string.IsNullOrEmpty(minBundleVersion) - ? null - : new List() { new BundleRequirement() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = minBundleVersion } }; + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(extensionRequirements: GetExtensionRequirementOptions(minBundleVersion, minExtensionVersion)); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + var traces = _loggerProvider.GetAllLogMessages(); - IEnumerable extensionRequirements = string.IsNullOrEmpty(minExtensionVersion) - ? null - : new List() - { - new ExtensionStartupTypeRequirement() - { - Name = "AzureStorageWebJobsStartup", - AssemblyName = "Microsoft.Azure.WebJobs.Extensions.Storage", - MinimumAssemblyVersion = minExtensionVersion - } - }; - - extensionRequirementOptions.Bundles = bundleRequirment; - extensionRequirementOptions.Extensions = extensionRequirements; - - OptionsWrapper optionsWrapper = new(extensionRequirementOptions); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - var types = await discoverer.GetExtensionsStartupTypesAsync(); - // Act - var traces = testLoggerProvider.GetAllLogMessages(); - - if (string.IsNullOrEmpty(actualBundleVersion)) - { - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); - } - else - { - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading extension bundle"))); - } - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading startup extension 'Storage"))); - AssertNoErrors(traces); + // Assert + if (string.IsNullOrEmpty(actualBundleVersion)) + { + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); + } + else + { + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading extension bundle"))); } + + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading startup extension 'Storage"))); } [Theory] @@ -193,65 +149,29 @@ public async Task GetExtensionsStartupTypes_AcceptsRequiredBundleVersions(string public async Task GetExtensionsStartupTypes_RejectsRequiredBundleVersions(string minBundleVersion, string actualBundleVersion, string minExtensionVersion) { // "TypeName": , Microsoft.Azure.WebJobs.Extensions.Storage, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", - using (var directory = GetTempDirectory()) + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + if (string.IsNullOrEmpty(actualBundleVersion)) { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - - if (string.IsNullOrEmpty(actualBundleVersion)) - { - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - } - else - { - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - } - - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails(actualBundleVersion))); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); + } + else + { + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + } - var extensionRequirementOptions = new Config.ExtensionRequirementOptions(); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails(actualBundleVersion)); - IEnumerable bundleRequirment = string.IsNullOrEmpty(minBundleVersion) - ? null - : new List() { new BundleRequirement() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = minBundleVersion } }; + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(extensionRequirements: GetExtensionRequirementOptions(minBundleVersion, minExtensionVersion)); + var exception = await Assert.ThrowsAsync(discoverer.GetExtensionsStartupTypesAsync); + var traces = _loggerProvider.GetAllLogMessages(); - IEnumerable extensionRequirements = string.IsNullOrEmpty(minExtensionVersion) - ? null - : new List() - { - new ExtensionStartupTypeRequirement() - { - Name = "AzureStorageWebJobsStartup", - AssemblyName = "Microsoft.Azure.WebJobs.Extensions.Storage", - MinimumAssemblyVersion = minExtensionVersion - } - }; - - extensionRequirementOptions.Bundles = bundleRequirment; - extensionRequirementOptions.Extensions = extensionRequirements; - - OptionsWrapper optionsWrapper = new(extensionRequirementOptions); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - var traces = testLoggerProvider.GetAllLogMessages(); - - if (!string.IsNullOrEmpty(minBundleVersion)) - { - // Assert - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 4.9.0 does not meet the required minimum version of 4.12.0. Update your extension bundle reference in host.json to reference 4.12.0 or later."))); - } + // Assert + if (!string.IsNullOrEmpty(minBundleVersion)) + { + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 4.9.0 does not meet the required minimum version of 4.12.0. Update your extension bundle reference in host.json to reference 4.12.0 or later."))); } } @@ -263,54 +183,23 @@ public async Task GetExtensionsStartupTypes_RejectsRequiredBundleVersions(string public async Task GetExtensionsStartupTypes_AcceptsRequiredExtensionVersions(string minBundleVersion, bool extensionConfigured, string minExtensionVersion) { // "TypeName": , Microsoft.Azure.WebJobs.Extensions.Storage, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", - using (var directory = GetTempDirectory(extensionConfigured)) + if (extensionConfigured) { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); + InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + } - var extensionRequirementOptions = new Config.ExtensionRequirementOptions(); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - IEnumerable bundleRequirment = string.IsNullOrEmpty(minBundleVersion) - ? null - : new List() { new BundleRequirement() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = minBundleVersion } }; + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(extensionRequirements: GetExtensionRequirementOptions(minBundleVersion, minExtensionVersion)); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + var traces = _loggerProvider.GetAllLogMessages(); - IEnumerable extensionRequirements = string.IsNullOrEmpty(minExtensionVersion) - ? null - : new List() - { - new ExtensionStartupTypeRequirement() - { - Name = "AzureStorageWebJobsStartup", - AssemblyName = "Microsoft.Azure.WebJobs.Extensions.Storage", - MinimumAssemblyVersion = minExtensionVersion - } - }; - - extensionRequirementOptions.Bundles = bundleRequirment; - extensionRequirementOptions.Extensions = extensionRequirements; - - OptionsWrapper optionsWrapper = new(extensionRequirementOptions); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - var types = await discoverer.GetExtensionsStartupTypesAsync(); - // Act - var traces = testLoggerProvider.GetAllLogMessages(); - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); - if (extensionConfigured) - { - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading startup extension 'Storage"))); - } + // Assert + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); + if (extensionConfigured) + { + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Loading startup extension 'Storage"))); AssertNoErrors(traces); } } @@ -320,235 +209,95 @@ public async Task GetExtensionsStartupTypes_AcceptsRequiredExtensionVersions(str [InlineData("4.12.0", "4.0.6")] public async Task GetExtensionsStartupTypes_RejectsRequiredExtensionVersions(string minBundleVersion, string minExtensionVersion) { - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - var mockExtensionBundleManager = new Mock(); - - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - - var extensionRequirementOptions = new Config.ExtensionRequirementOptions(); - - IEnumerable bundleRequirment = string.IsNullOrEmpty(minBundleVersion) - ? null - : new List() { new BundleRequirement() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = minBundleVersion } }; + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(extensionRequirements: GetExtensionRequirementOptions(minBundleVersion, minExtensionVersion)); + var exception = await Assert.ThrowsAsync(discoverer.GetExtensionsStartupTypesAsync); - IEnumerable extensionRequirements = string.IsNullOrEmpty(minExtensionVersion) - ? null - : new List() - { - new ExtensionStartupTypeRequirement() - { - Name = "AzureStorageWebJobsStartup", - AssemblyName = "Microsoft.Azure.WebJobs.Extensions.Storage", - MinimumAssemblyVersion = minExtensionVersion - } - }; - - extensionRequirementOptions.Bundles = bundleRequirment; - extensionRequirementOptions.Extensions = extensionRequirements; - - OptionsWrapper optionsWrapper = new(extensionRequirementOptions); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - - // Act - var traces = testLoggerProvider.GetAllLogMessages(); - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); - Assert.True(traces.Any(m => m.FormattedMessage.Contains($"ExtensionStartupType AzureStorageWebJobsStartup from assembly 'Microsoft.Azure.WebJobs.Extensions.Storage, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' does not meet the required minimum version of 4.0.6"))); - } + // Assert + var traces = _loggerProvider.GetAllLogMessages(); + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"Extension Bundle not loaded"))); + Assert.True(traces.Any(m => m.FormattedMessage.Contains($"ExtensionStartupType AzureStorageWebJobsStartup from assembly 'Microsoft.Azure.WebJobs.Extensions.Storage, Version=4.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' does not meet the required minimum version of 4.0.6"))); } [Fact] public async Task GetExtensionsStartupTypes_FiltersBuiltinExtensionsAsync() { - var references = new[] - { - new ExtensionReference { Name = "Http", TypeName = typeof(HttpWebJobsStartup).AssemblyQualifiedName }, - new ExtensionReference { Name = "Timers", TypeName = typeof(ExtensionsWebJobsStartup).AssemblyQualifiedName }, - new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }, - }; - - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - var mockExtensionBundleManager = new Mock(); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); + InstallExtensions(ExtensionInstall.Http(), ExtensionInstall.Timers(), ExtensionInstall.Storage()); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } - - CopyToBin(typeof(HttpWebJobsStartup).Assembly.Location); - CopyToBin(typeof(ExtensionsWebJobsStartup).Assembly.Location); - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + var traces = _loggerProvider.GetAllLogMessages(); - // Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{references[0].TypeName}' belongs to a builtin extension"))); - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{references[1].TypeName}' belongs to a builtin extension"))); - AssertNoErrors(traces); - } + // Assert + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{typeof(HttpWebJobsStartup).AssemblyQualifiedName}' belongs to a builtin extension"))); + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{typeof(ExtensionsWebJobsStartup).AssemblyQualifiedName}' belongs to a builtin extension"))); + AssertNoErrors(traces); } [Fact] public async Task GetExtensionsStartupTypes_ExtensionBundleReturnsNullPath_ReturnsNull() { - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundlePath()).ReturnsAsync(string.Empty); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundlePath()).ReturnsAsync(string.Empty); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); - using (var directory = new TempDirectory()) - { - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); - - // Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.NotNull(types); - Assert.Equal(types.Count(), 0); - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Unable to find or download extension bundle"))); - } + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + var traces = _loggerProvider.GetAllLogMessages(); + + // Assert + AreExpectedMetricsGenerated(); + Assert.NotNull(types); + Assert.Equal(types.Count(), 0); + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Unable to find or download extension bundle"))); } [Fact] public async Task GetExtensionsStartupTypes_ValidExtensionBundle_FiltersBuiltinExtensionsAsync() { - var references = new[] - { - new ExtensionReference { Name = "Http", TypeName = typeof(HttpWebJobsStartup).AssemblyQualifiedName }, - new ExtensionReference { Name = "Timers", TypeName = typeof(ExtensionsWebJobsStartup).AssemblyQualifiedName }, - new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }, - }; - - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } + string binPath = InstallExtensions(ExtensionInstall.Http(), ExtensionInstall.Timers(), ExtensionInstall.Storage()); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); - CopyToBin(typeof(HttpWebJobsStartup).Assembly.Location); - CopyToBin(typeof(ExtensionsWebJobsStartup).Assembly.Location); - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + var traces = _loggerProvider.GetAllLogMessages(); - // Assert - Assert.Single(types); - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{references[0].TypeName}' belongs to a builtin extension"))); - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{references[1].TypeName}' belongs to a builtin extension"))); - AssertNoErrors(traces); - } + // Assert + Assert.Single(types); + AreExpectedMetricsGenerated(); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{typeof(HttpWebJobsStartup).AssemblyQualifiedName}' belongs to a builtin extension"))); + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"The extension startup type '{typeof(ExtensionsWebJobsStartup).AssemblyQualifiedName}' belongs to a builtin extension"))); + AssertNoErrors(traces); } [Fact] public async Task GetExtensionsStartupTypes_UnableToDownloadExtensionBundle_ReturnsNull() { - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundlePath()).ReturnsAsync(string.Empty); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(string.Empty, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + _bundleManager.Setup(e => e.GetExtensionBundlePath()).ReturnsAsync(string.Empty); // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(string.Empty); var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + var traces = _loggerProvider.GetAllLogMessages(); // Assert Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Unable to find or download extension bundle"))); - AreExpectedMetricsGenerated(testMetricsLogger); + AreExpectedMetricsGenerated(); Assert.NotNull(types); Assert.Equal(types.Count(), 0); } @@ -556,164 +305,56 @@ public async Task GetExtensionsStartupTypes_UnableToDownloadExtensionBundle_Retu [Fact] public async Task GetExtensionsStartupTypes_BundlesConfiguredBindingsNotConfigured_LoadsAllExtensions() { - var storageExtensionReference = new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - var sendGridExtensionReference = new ExtensionReference { Name = "SendGrid", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - var references = new[] { storageExtensionReference, sendGridExtensionReference }; - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); + string binPath = InstallExtensions(ExtensionInstall.Storage(), ExtensionInstall.SendGrid()); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } - - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - // Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Equal(types.Count(), 2); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName); - AssertNoErrors(traces); - } + // Assert + AreExpectedMetricsGenerated(); + Assert.Equal(types.Count(), 2); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); } [Fact] public async Task GetExtensionsStartupTypes_BundlesNotConfiguredBindingsNotConfigured_LoadsAllExtensions() { - var references = new[] - { - new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName } - }; - - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - var mockExtensionBundleManager = new Mock(); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); + InstallExtensions(ExtensionInstall.Storage()); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } - - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - // mock Function metadata - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - // Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - AssertNoErrors(traces); - } + // Assert + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); } [Fact] public async Task GetExtensionsStartupTypes_BundlesConfiguredBindingsConfigured_PerformsSelectiveLoading() { - var storageExtensionReference = new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - storageExtensionReference.Bindings.Add("blob"); - var sendGridExtensionReference = new ExtensionReference { Name = "SendGrid", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - sendGridExtensionReference.Bindings.Add("sendGrid"); - var references = new[] { storageExtensionReference, sendGridExtensionReference }; - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } - - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); + InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(Path.Combine(_directory.Path, "bin")); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - //Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - AssertNoErrors(traces); - } + //Assert + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); } [Theory] @@ -721,83 +362,51 @@ void CopyToBin(string path) [InlineData(true)] public async Task GetExtensionsStartupTypes_LegacyBundles_UsesExtensionBundleBinaries(bool hasPrecompiledFunctions) { - using (var directory = GetTempDirectory()) + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + + if (hasPrecompiledFunctions) { - var binPath = Path.Combine(directory.Path, "bin"); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - var testLogger = GetTestLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, hasPrecompiledFunction: hasPrecompiledFunctions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - - //Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + SetupMetadataManager(DotNetScriptTypes.DotNetAssembly); } + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + + //Assert + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); } [Fact] public async Task GetExtensionsStartupTypes_WorkerRuntimeNotSetForNodeApp_LoadsExtensionBundle() { - var vars = new Dictionary(); - - using (var directory = GetTempDirectory()) - using (var env = new TestScopedEnvironmentVariable(vars)) - { - var binPath = Path.Combine(directory.Path, "bin"); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - - RpcWorkerConfig nodeWorkerConfig = new RpcWorkerConfig() { Description = TestHelpers.GetTestWorkerDescription("node", "none", false) }; - RpcWorkerConfig dotnetIsolatedWorkerConfig = new RpcWorkerConfig() { Description = TestHelpers.GetTestWorkerDescription("dotnet-isolated", "none", false) }; - - var tempOptions = new LanguageWorkerOptions(); - tempOptions.WorkerConfigs = new List(); - tempOptions.WorkerConfigs.Add(nodeWorkerConfig); - tempOptions.WorkerConfigs.Add(dotnetIsolatedWorkerConfig); + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + SetupMetadataManager(RpcWorkerConstants.NodeLanguageWorkerName); - var optionsMonitor = new TestOptionsMonitor(tempOptions); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(optionsMonitor, hasNodeFunctions: true); - - var languageWorkerOptions = new TestOptionsMonitor(tempOptions); - - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - //Assert - var traces = testLoggerProvider.GetAllLogMessages(); - var traceMessage = traces.FirstOrDefault(val => string.Equals(val.EventId.Name, "ScriptStartNotLoadingExtensionBundle")); - bool loadingExtensionBundle = traceMessage == null; + //Assert + var traces = _loggerProvider.GetAllLogMessages(); + var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("ScriptStartNotLoadingExtensionBundle")); + bool loadingExtensionBundle = traceMessage == null; - Assert.True(loadingExtensionBundle); - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - } + Assert.True(loadingExtensionBundle); + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); } [Theory(Skip = "This test is failing on CI and needs to be fixed.")] @@ -807,73 +416,44 @@ public async Task GetExtensionsStartupTypes_WorkerRuntimeNotSetForNodeApp_LoadsE [InlineData(false, false)] public async Task GetExtensionsStartupTypes_DotnetIsolated_ExtensionBundleConfigured(bool isLogicApp, bool workerRuntimeSet) { - var vars = new Dictionary(); - if (isLogicApp) { - vars.Add(EnvironmentSettingNames.AppKind, ScriptConstants.WorkFlowAppKind); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AppKind, ScriptConstants.WorkFlowAppKind); } if (workerRuntimeSet) { - vars.Add(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated"); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated"); } - using (var directory = GetTempDirectory()) - using (var env = new TestScopedEnvironmentVariable(vars)) - { - var binPath = Path.Combine(directory.Path, "bin"); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - - RpcWorkerConfig workerConfig = new RpcWorkerConfig() { Description = TestHelpers.GetTestWorkerDescription("dotnet-isolated", "none", true) }; - var tempOptions = new LanguageWorkerOptions(); - tempOptions.WorkerConfigs = new List(); - tempOptions.WorkerConfigs.Add(workerConfig); - - RpcWorkerConfig nodeWorkerConfig = new RpcWorkerConfig() { Description = TestHelpers.GetTestWorkerDescription("node", "none", false) }; - tempOptions.WorkerConfigs.Add(nodeWorkerConfig); - - var optionsMonitor = new TestOptionsMonitor(tempOptions); - - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(optionsMonitor, hasDotnetIsolatedFunctions: true); - - var languageWorkerOptions = new TestOptionsMonitor(tempOptions); - - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); + var binPath = Path.Combine(_directory.Path, "bin"); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + SetupMetadataManager(RpcWorkerConstants.DotNetIsolatedLanguageWorkerName); - //Assert - var traces = testLoggerProvider.GetAllLogMessages(); - var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("ScriptStartNotLoadingExtensionBundle")); - - bool loadingExtensionBundle = traceMessage == null; + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - if (isLogicApp) - { - Assert.True(loadingExtensionBundle); - } - else - { - Assert.False(loadingExtensionBundle); - } + //Assert + var traces = _loggerProvider.GetAllLogMessages(); + var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("ScriptStartNotLoadingExtensionBundle")); + bool loadingExtensionBundle = traceMessage == null; - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); + if (isLogicApp) + { + Assert.True(loadingExtensionBundle); + } + else + { + Assert.False(loadingExtensionBundle); } + + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); } [Theory] @@ -881,141 +461,62 @@ public async Task GetExtensionsStartupTypes_DotnetIsolated_ExtensionBundleConfig [InlineData(true)] public async Task GetExtensionsStartupTypes_NonLegacyBundles_UsesBundlesForNonPrecompiledFunctions(bool hasPrecompiledFunctions) { - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - var testLogger = GetTestLogger(); - - string bundlePath = hasPrecompiledFunctions ? "FakePath" : directory.Path; - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails())); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, hasPrecompiledFunction: hasPrecompiledFunctions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - - //Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Single(types); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); - } + InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + string bundlePath = hasPrecompiledFunctions ? "FakePath" : _directory.Path; + var binPath = Path.Combine(_directory.Path, "bin"); + + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails()); + SetupMetadataManager(DotNetScriptTypes.DotNetAssembly); + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + + //Assert + AreExpectedMetricsGenerated(); + Assert.Single(types); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.Single().FullName); } [Fact] public async Task GetExtensionsStartupTypes_BundlesNotConfiguredBindingsConfigured_LoadsAllExtensions() { - var storageExtensionReference = new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - storageExtensionReference.Bindings.Add("blob"); - var sendGridExtensionReference = new ExtensionReference { Name = "SendGrid", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - sendGridExtensionReference.Bindings.Add("sendGrid"); - var references = new[] { storageExtensionReference, sendGridExtensionReference }; - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - var extensions = new JObject - { - { "extensions", JArray.FromObject(references) } - }; - - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } - - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); + InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); - // Assert - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Equal(types.Count(), 2); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName); - } + // Assert + AreExpectedMetricsGenerated(); + Assert.Equal(types.Count(), 2); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName); } [Fact] public async Task GetExtensionsStartupTypes_NoBindings_In_ExtensionJson() { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - using var directory = new TempDirectory(); - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) + ExtensionInstall storage1 = new("AzureStorageBlobs", typeof(AzureStorageWebJobsStartup)); + ExtensionInstall storage2 = new("Storage", typeof(AzureStorageWebJobsStartup)) { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } + HintPath = "Microsoft.Azure.WebJobs.Extensions.Storage.dll" + }; - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - string extensionJson = $$""" - { - "extensions": [ - { - "name": "Storage", - "typeName": "{{typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName}}", - "hintPath": "Microsoft.Azure.WebJobs.Extensions.Storage.dll" - }, - { - "Name": "AzureStorageBlobs", - "TypeName": "{{typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName}}" - } - ] - } - """; - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensionJson); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" })); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); + string binPath = InstallExtensions(storage1, storage2); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" }); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); // Act var types = await discoverer.GetExtensionsStartupTypesAsync(); // Assert - AreExpectedMetricsGenerated(testMetricsLogger); + AreExpectedMetricsGenerated(); Assert.Equal(types.Count(), 2); Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.FirstOrDefault().FullName); } @@ -1023,273 +524,214 @@ void CopyToBin(string path) [Fact] public async Task GetExtensionsStartupTypes_RejectsBundleBelowMinimumVersion() { - using (var directory = GetTempDirectory()) - { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var binPath = Path.Combine(directory.Path, "bin"); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - mockExtensionBundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(GetBundleDetails("2.1.0"))); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - var traces = testLoggerProvider.GetAllLogMessages(); - - // Assert - Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 2.1.0 does not meet the required minimum version of 2.6.1. Update your extension bundle reference in host.json to reference 2.6.1 or later."))); - } + var binPath = Path.Combine(_directory.Path, "bin"); + + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + _bundleManager.Setup(e => e.IsLegacyExtensionBundle()).Returns(false); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(GetBundleDetails("2.1.0")); + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var exception = await Assert.ThrowsAsync(discoverer.GetExtensionsStartupTypesAsync); + var traces = _loggerProvider.GetAllLogMessages(); + + // Assert + Assert.True(traces.Any(m => string.Equals(m.FormattedMessage, $"Referenced bundle Microsoft.Azure.Functions.ExtensionBundle of version 2.1.0 does not meet the required minimum version of 2.6.1. Update your extension bundle reference in host.json to reference 2.6.1 or later."))); } [Fact] public async Task GetExtensionsStartupTypes_RejectsExtensionsBelowMinimumVersion() { - var mockExtensionBundleManager = new Mock(); - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(false); - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); + var binPath = Path.Combine(_directory.Path, "bin"); + Directory.CreateDirectory(binPath); - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } + void CopyToBin(string path) + { + File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); + } - // create a bin folder that has out of date extensions - var extensionBinPath = Path.Combine(Environment.CurrentDirectory, @"TestScripts\OutOfDateExtension\bin"); - foreach (var f in Directory.GetFiles(extensionBinPath)) - { - CopyToBin(f); - } - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions, ImmutableArray.Empty); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var exception = await Assert.ThrowsAsync(async () => await discoverer.GetExtensionsStartupTypesAsync()); - var traces = testLoggerProvider.GetAllLogMessages(); - - // Assert - - var storageTrace = traces.FirstOrDefault(m => m.FormattedMessage.StartsWith("ExtensionStartupType AzureStorageWebJobsStartup")); - Assert.NotNull(storageTrace); - Assert.Equal("ExtensionStartupType AzureStorageWebJobsStartup from assembly 'Microsoft.Azure.WebJobs.Extensions.Storage, Version=3.0.10.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' does not meet the required minimum version of 4.0.4.0. Update your NuGet package reference for Microsoft.Azure.WebJobs.Extensions.Storage to 4.0.4 or later.", - storageTrace.FormattedMessage); + // create a bin folder that has out of date extensions + var extensionBinPath = Path.Combine(Environment.CurrentDirectory, @"TestScripts\OutOfDateExtension\bin"); + foreach (var f in Directory.GetFiles(extensionBinPath)) + { + CopyToBin(f); } + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var exception = await Assert.ThrowsAsync(discoverer.GetExtensionsStartupTypesAsync); + var traces = _loggerProvider.GetAllLogMessages(); + + // Assert + var storageTrace = traces.FirstOrDefault(m => m.FormattedMessage.StartsWith("ExtensionStartupType AzureStorageWebJobsStartup")); + Assert.NotNull(storageTrace); + Assert.Equal("ExtensionStartupType AzureStorageWebJobsStartup from assembly 'Microsoft.Azure.WebJobs.Extensions.Storage, Version=3.0.10.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' does not meet the required minimum version of 4.0.4.0. Update your NuGet package reference for Microsoft.Azure.WebJobs.Extensions.Storage to 4.0.4 or later.", + storageTrace.FormattedMessage); } [Fact] public async Task GetExtensionsStartupTypes_WorkerIndexing_PerformsSelectiveLoading() { - var storageExtensionReference = new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - storageExtensionReference.Bindings.Add("blob"); - var sendGridExtensionReference = new ExtensionReference { Name = "SendGrid", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - sendGridExtensionReference.Bindings.Add("sendGrid"); - var references = new[] { storageExtensionReference, sendGridExtensionReference }; - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - var extensions = new JObject + string binPath = InstallExtensions(ExtensionInstall.Storage(true), ExtensionInstall.SendGrid(true)); + _bundleManager.Setup(e => e.GetExtensionBundleDetails()).ReturnsAsync(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" }); + _bundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); + _bundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).ReturnsAsync(binPath); + + _environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); + _environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "python"); + + // Act + ScriptStartupTypeLocator discoverer = CreateSystemUnderTest(); + var types = await discoverer.GetExtensionsStartupTypesAsync(); + + //Assert that filtering did not take place because of worker indexing + Assert.True(types.Count() == 1); + Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.ElementAt(0).FullName); + AssertNoErrors(_loggerProvider.GetAllLogMessages()); + } + + private static ExtensionBundleDetails GetBundleDetails(string version = "2.7.0") + { + return new ExtensionBundleDetails { - { "extensions", JArray.FromObject(references) } + Id = "Microsoft.Azure.Functions.ExtensionBundle", + Version = version }; + } - using (var directory = new TempDirectory()) - { - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); + private static void AssertNoErrors(IList traces) + { + Assert.False(traces.Any(m => m.Level == LogLevel.Error || m.Level == LogLevel.Critical)); + } - void CopyToBin(string path) - { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); - } + private static ExtensionRequirementOptions GetExtensionRequirementOptions(string minBundleVersion, string minExtensionVersion) + { + ExtensionRequirementOptions extensionRequirementOptions = new(); + IEnumerable bundleRequirment = string.IsNullOrEmpty(minBundleVersion) + ? null + : [new() { Id = "Microsoft.Azure.Functions.ExtensionBundle", MinimumVersion = minBundleVersion }]; + + IEnumerable extensionRequirements = string.IsNullOrEmpty(minExtensionVersion) + ? null : + [ + new() + { + Name = "AzureStorageWebJobsStartup", + AssemblyName = "Microsoft.Azure.WebJobs.Extensions.Storage", + MinimumAssemblyVersion = minExtensionVersion + } + ]; - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); - - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" })); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); - - // mock worker config and environment variables to make host choose worker indexing - RpcWorkerConfig workerConfig = new RpcWorkerConfig() { Description = TestHelpers.GetTestWorkerDescription("python", "none", true) }; - var tempOptions = new LanguageWorkerOptions(); - tempOptions.WorkerConfigs = new List(); - tempOptions.WorkerConfigs.Add(workerConfig); - var optionsMonitor = new TestOptionsMonitor(tempOptions); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(optionsMonitor); - - var languageWorkerOptions = new TestOptionsMonitor(tempOptions); - Environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, ScriptConstants.FeatureFlagEnableWorkerIndexing); - Environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "python"); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); - - // Act - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); - Environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsFeatureFlags, null); - Environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, null); - - //Assert that filtering did not take place because of worker indexing - Assert.True(types.Count() == 1); - Assert.Equal(typeof(AzureStorageWebJobsStartup).FullName, types.ElementAt(0).FullName); - AssertNoErrors(traces); - } + extensionRequirementOptions.Bundles = bundleRequirment; + extensionRequirementOptions.Extensions = extensionRequirements; + return extensionRequirementOptions; } - [Fact] - public async Task GetExtensionsStartupTypes_EmptyExtensionsArray() + private string InstallExtensions(params ExtensionInstall[] extensions) { - TestMetricsLogger testMetricsLogger = new TestMetricsLogger(); - - using var directory = new TempDirectory(); - var binPath = Path.Combine(directory.Path, "bin"); + string binPath = Path.Combine(_directory.Path, "bin"); Directory.CreateDirectory(binPath); - // extensions.json file with an empty extensions array (simulating extensions.json produced by in-proc app) - string extensionJson = """ + JArray jArray = []; + foreach (ExtensionInstall e in extensions) { - "extensions": [] + ExtensionReference reference = e.GetReference(); + jArray.Add(JObject.FromObject(reference)); + e.CopyTo(binPath); } - """; - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensionJson); - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); + JObject jObject = new() + { + { "extensions", jArray }, + }; - var mockExtensionBundleManager = new Mock(); - mockExtensionBundleManager.Setup(e => e.IsExtensionBundleConfigured()).Returns(true); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleDetails()).Returns(Task.FromResult(new ExtensionBundleDetails() { Id = "bundleID", Version = "1.0.0" })); - mockExtensionBundleManager.Setup(e => e.GetExtensionBundleBinPathAsync()).Returns(Task.FromResult(binPath)); + File.WriteAllText(Path.Combine(binPath, "extensions.json"), jObject.ToString()); + return binPath; + } - var languageWorkerOptions = new TestOptionsMonitor(new LanguageWorkerOptions()); - var mockFunctionMetadataManager = GetTestFunctionMetadataManager(languageWorkerOptions); - OptionsWrapper optionsWrapper = new(new ExtensionRequirementOptions()); - var discoverer = new ScriptStartupTypeLocator(directory.Path, testLogger, mockExtensionBundleManager.Object, mockFunctionMetadataManager, testMetricsLogger, optionsWrapper); + private void SetupMetadataManager(string language) + { + FunctionMetadata functionMetadata = new(); + functionMetadata.Bindings.Add(new BindingMetadata() { Type = "blob" }); + functionMetadata.Language = language; + ImmutableArray result = [functionMetadata]; + _metadataManager.Setup(m => m.GetFunctionMetadata(true, true, false)).Returns(result); + } - var types = await discoverer.GetExtensionsStartupTypesAsync(); - var traces = testLoggerProvider.GetAllLogMessages(); + private ScriptStartupTypeLocator CreateSystemUnderTest(string rootPath = null, ExtensionRequirementOptions extensionRequirements = null) + { + LoggerFactory factory = new(); + factory.AddProvider(_loggerProvider); + OptionsWrapper optionsWrapper = new(extensionRequirements ?? new()); + return new( + rootPath ?? _directory.Path, + factory.CreateLogger(), + _bundleManager.Object, + _metadataManager.Object, + _metricsLogger, + _environment, + optionsWrapper); + } - AreExpectedMetricsGenerated(testMetricsLogger); - Assert.Empty(types); // Ensure no types are loaded because the extensions array is empty - AssertNoErrors(traces); + private bool AreExpectedMetricsGenerated() + { + return _metricsLogger.EventsBegan.Contains(MetricEventNames.ParseExtensions) && _metricsLogger.EventsEnded.Contains(MetricEventNames.ParseExtensions); } - private IFunctionMetadataManager GetTestFunctionMetadataManager(IOptionsMonitor options, ICollection metadataCollection = null, bool hasPrecompiledFunction = false, bool hasNodeFunctions = false, bool hasDotnetIsolatedFunctions = false) + private class ExtensionInstall(string name, Type startupType, params string[] bindings) { - var functionMetadata = new FunctionMetadata(); - functionMetadata.Bindings.Add(new BindingMetadata() { Type = "blob" }); + public string HintPath { get; init; } - if (hasPrecompiledFunction) + public static ExtensionInstall Storage(bool includeBinding = false) { - functionMetadata.Language = DotNetScriptTypes.DotNetAssembly; + string[] bindings = includeBinding ? ["blob"] : []; + return new("Storage", typeof(AzureStorageWebJobsStartup), bindings); } - if (hasNodeFunctions) + + public static ExtensionInstall SendGrid(bool includeBinding = false) { - functionMetadata.Language = RpcWorkerConstants.NodeLanguageWorkerName; + string[] bindings = includeBinding ? ["sendGrid"] : []; + return new("SendGrid", typeof(AzureStorageWebJobsStartup), bindings); } - if (hasDotnetIsolatedFunctions) + public static ExtensionInstall Timers() { - functionMetadata.Language = RpcWorkerConstants.DotNetIsolatedLanguageWorkerName; + return new("Timers", typeof(ExtensionsWebJobsStartup)); } - var functionMetadataCollection = metadataCollection ?? new List() { functionMetadata }; - - var functionMetadataManager = new Mock(); - functionMetadataManager.Setup(e => e.GetFunctionMetadata(true, true, false)).Returns(functionMetadataCollection.ToImmutableArray()); - return functionMetadataManager.Object; - } - - private bool AreExpectedMetricsGenerated(TestMetricsLogger metricsLogger) - { - return metricsLogger.EventsBegan.Contains(MetricEventNames.ParseExtensions) && metricsLogger.EventsEnded.Contains(MetricEventNames.ParseExtensions); - } - - private TempDirectory GetTempDirectory(bool copyExtensionsToBin = true) - { - var directory = new TempDirectory(); - - if (copyExtensionsToBin) + public static ExtensionInstall Http() { - var storageExtensionReference = new ExtensionReference { Name = "Storage", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - storageExtensionReference.Bindings.Add("blob"); - var sendGridExtensionReference = new ExtensionReference { Name = "SendGrid", TypeName = typeof(AzureStorageWebJobsStartup).AssemblyQualifiedName }; - sendGridExtensionReference.Bindings.Add("sendGrid"); - var references = new[] { storageExtensionReference, sendGridExtensionReference }; + return new("Http", typeof(HttpWebJobsStartup)); + } - var extensions = new JObject + public ExtensionReference GetReference() + { + ExtensionReference reference = new() { - { "extensions", JArray.FromObject(references) } + Name = name, + TypeName = startupType.AssemblyQualifiedName, + HintPath = HintPath, }; - - var binPath = Path.Combine(directory.Path, "bin"); - Directory.CreateDirectory(binPath); - - void CopyToBin(string path) + foreach (string binding in bindings ?? Enumerable.Empty()) { - File.Copy(path, Path.Combine(binPath, Path.GetFileName(path))); + reference.Bindings.Add(binding); } - CopyToBin(typeof(AzureStorageWebJobsStartup).Assembly.Location); - - File.WriteAllText(Path.Combine(binPath, "extensions.json"), extensions.ToString()); + return reference; } - return directory; - } - - private ExtensionBundleDetails GetBundleDetails(string version = "2.7.0") - { - return new ExtensionBundleDetails + public void CopyTo(string path) { - Id = "Microsoft.Azure.Functions.ExtensionBundle", - Version = version - }; - } - - private ILogger GetTestLogger() - { - TestLoggerProvider testLoggerProvider = new TestLoggerProvider(); - LoggerFactory factory = new LoggerFactory(); - factory.AddProvider(testLoggerProvider); - var testLogger = factory.CreateLogger(); - return testLogger; - } - - private static void AssertNoErrors(IList traces) - { - Assert.False(traces.Any(m => m.Level == LogLevel.Error || m.Level == LogLevel.Critical)); + string file = startupType.Assembly.Location; + string destination = Path.Combine(path, Path.GetFileName(file)); + if (!File.Exists(destination)) + { + File.Copy(file, destination); + } + } } } } diff --git a/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs b/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs index 7fc0737a8e..86fa090147 100644 --- a/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs +++ b/test/WebJobs.Script.Tests/WorkerFunctionMetadataProviderTests.cs @@ -193,11 +193,11 @@ public async void ValidateFunctionMetadata_Logging() { }); - var environment = SystemEnvironment.Instance; + TestEnvironment environment = new(); environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "node"); - var workerFunctionMetadataProvider = new WorkerFunctionMetadataProvider(optionsMonitor, logger, SystemEnvironment.Instance, - mockWebHostRpcWorkerChannelManager.Object, mockScriptHostManager.Object); + var workerFunctionMetadataProvider = new WorkerFunctionMetadataProvider( + optionsMonitor, logger, environment, mockWebHostRpcWorkerChannelManager.Object, mockScriptHostManager.Object); await workerFunctionMetadataProvider.GetFunctionMetadataAsync(workerConfigs, false); var traces = logger.GetLogMessages(); diff --git a/test/WebJobs.Script.Tests/Workers/Http/HttpWorkerChannelTests.cs b/test/WebJobs.Script.Tests/Workers/Http/HttpWorkerChannelTests.cs index 4429e589fc..d102ba42b2 100644 --- a/test/WebJobs.Script.Tests/Workers/Http/HttpWorkerChannelTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Http/HttpWorkerChannelTests.cs @@ -35,7 +35,7 @@ public async Task TestStartWorkerProcess(bool isWorkerReady) Mock httpWorkerService = new Mock(); httpWorkerService.Setup(a => a.IsWorkerReady(It.IsAny())).Returns(Task.FromResult(isWorkerReady)); - workerProcess.Setup(a => a.StartProcessAsync()).Returns(Task.CompletedTask); + workerProcess.Setup(a => a.StartProcessAsync(default)).Returns(Task.CompletedTask); TestLogger logger = new TestLogger("HttpWorkerChannel"); IHttpWorkerChannel testWorkerChannel = new HttpWorkerChannel("RandomWorkerId", _eventManager, workerProcess.Object, httpWorkerService.Object, logger, _metricsLogger, 3); Task resultTask = null; @@ -61,7 +61,7 @@ public async Task TestStartWorkerProcess_WorkerServiceThrowsException() IMetricsLogger metricsLogger = new TestMetricsLogger(); httpWorkerService.Setup(a => a.IsWorkerReady(It.IsAny())).Throws(new Exception("RandomException")); - workerProcess.Setup(a => a.StartProcessAsync()).Returns(Task.CompletedTask); + workerProcess.Setup(a => a.StartProcessAsync(default)).Returns(Task.CompletedTask); TestLogger logger = new TestLogger("HttpWorkerChannel"); IHttpWorkerChannel testWorkerChannel = new HttpWorkerChannel("RandomWorkerId", _eventManager, workerProcess.Object, httpWorkerService.Object, logger, _metricsLogger, 3); Task resultTask = null; @@ -85,7 +85,7 @@ public async Task TestStartWorkerProcess_WorkerProcessThrowsException() IMetricsLogger metricsLogger = new TestMetricsLogger(); httpWorkerService.Setup(a => a.IsWorkerReady(It.IsAny())).Throws(new Exception("RandomException")); - workerProcess.Setup(a => a.StartProcessAsync()).Throws(new Exception("RandomException")); + workerProcess.Setup(a => a.StartProcessAsync(default)).Throws(new Exception("RandomException")); TestLogger logger = new TestLogger("HttpWorkerChannel"); IHttpWorkerChannel testWorkerChannel = new HttpWorkerChannel("RandomWorkerId", _eventManager, workerProcess.Object, httpWorkerService.Object, logger, _metricsLogger, 3); Task resultTask = null; diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs index 767b9e232d..8a9a7422fe 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs @@ -37,9 +37,9 @@ public class GrpcWorkerChannelTests : IDisposable private static string _expectedSystemLogMessage = "Random system log message"; private static string _expectedLoadMsgPartial = "Sending FunctionLoadRequest for "; - private readonly Mock _mockrpcWorkerProcess = new Mock(); + private readonly Mock _mockRpcWorkerProcess = new Mock(); private readonly string _workerId = "testWorkerId"; - private readonly string _scriptRootPath = "c:\testdir"; + private readonly string _scriptRootPath = "c:\\testdir"; private readonly IScriptEventManager _eventManager = new ScriptEventManager(); private readonly Mock _mockScriptHostManager = new Mock(MockBehavior.Strict); private readonly TestMetricsLogger _metricsLogger = new TestMetricsLogger(); @@ -55,7 +55,6 @@ public class GrpcWorkerChannelTests : IDisposable private readonly IOptionsMonitor _hostOptionsMonitor; private readonly IMemoryMappedFileAccessor _mapAccessor; private readonly ISharedMemoryManager _sharedMemoryManager; - private readonly IFunctionDataCache _functionDataCache; private readonly IOptions _workerConcurrencyOptions; private readonly ITestOutputHelper _testOutput; private readonly IOptions _hostingConfigOptions; @@ -74,24 +73,24 @@ public GrpcWorkerChannelTests(ITestOutputHelper testOutput) _testWorkerConfig.CountOptions.InitializationTimeout = TimeSpan.FromSeconds(5); _testWorkerConfig.CountOptions.EnvironmentReloadTimeout = TimeSpan.FromSeconds(5); - _mockrpcWorkerProcess.Setup(m => m.StartProcessAsync()).Returns(Task.CompletedTask); - _mockrpcWorkerProcess.Setup(m => m.Id).Returns(910); + _mockRpcWorkerProcess.Setup(m => m.StartProcessAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockRpcWorkerProcess.Setup(m => m.WaitForExitAsync(It.IsAny())).Returns(Task.Delay(Timeout.Infinite)); + _mockRpcWorkerProcess.Setup(m => m.Id).Returns(910); _testEnvironment = new TestEnvironment(); _testEnvironment.SetEnvironmentVariable(FunctionDataCacheConstants.FunctionDataCacheEnabledSettingName, "1"); _workerConcurrencyOptions = Options.Create(new WorkerConcurrencyOptions()); _workerConcurrencyOptions.Value.CheckInterval = TimeSpan.FromSeconds(1); - ILogger mmapAccessorLogger = NullLogger.Instance; + ILogger mMapAccessorLogger = NullLogger.Instance; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _mapAccessor = new MemoryMappedFileAccessorWindows(mmapAccessorLogger); + _mapAccessor = new MemoryMappedFileAccessorWindows(mMapAccessorLogger); } else { - _mapAccessor = new MemoryMappedFileAccessorUnix(mmapAccessorLogger, _testEnvironment); + _mapAccessor = new MemoryMappedFileAccessorUnix(mMapAccessorLogger, _testEnvironment); } _sharedMemoryManager = new SharedMemoryManager(_loggerFactory, _mapAccessor); - _functionDataCache = new FunctionDataCache(_sharedMemoryManager, _loggerFactory, _testEnvironment); var hostOptions = new ScriptApplicationHostOptions { @@ -125,7 +124,7 @@ private Task CreateDefaultWorkerChannel(bool autoStart = true, IDictionary(async () => }); } + [Fact] + public async Task StartWorkerProcessAsync_ProcessExits_Throws() + { + _mockRpcWorkerProcess.Setup(m => m.WaitForExitAsync(It.IsAny())) + .Returns(Task.CompletedTask); + await CreateDefaultWorkerChannel(autoStart: false); + + WorkerProcessExitException ex = await Assert.ThrowsAsync( + () => _workerChannel.StartWorkerProcessAsync(default)) + .WaitAsync(TimeSpan.FromMilliseconds(100)); + Assert.Equal(0, ex.ExitCode); + Assert.Equal("Worker process exited before initializing.", ex.Message); + } + + [Fact] + public async Task StartWorkerProcessAsync_ProcessFaults_Throws() + { + WorkerProcessExitException expected = new("Process has faulted.") { ExitCode = -1 }; + _mockRpcWorkerProcess.Setup(m => m.WaitForExitAsync(It.IsAny())) + .ThrowsAsync(expected); + await CreateDefaultWorkerChannel(autoStart: false); + + WorkerProcessExitException actual = await Assert.ThrowsAsync( + () => _workerChannel.StartWorkerProcessAsync(default)) + .WaitAsync(TimeSpan.FromMilliseconds(100)); + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StartWorkerProcessAsync_TimesOut() + { + await CreateDefaultWorkerChannel(autoStart: false); // suppress for timeout + var initTask = _workerChannel.StartWorkerProcessAsync(CancellationToken.None); + await Assert.ThrowsAsync(async () => await initTask); + } + + [Fact] + public async Task StartWorkerProcessAsync_WorkerProcess_Throws() + { + // note: uses custom worker channel + Mock mockrpcWorkerProcessThatThrows = new Mock(); + mockrpcWorkerProcessThatThrows.Setup(m => m.StartProcessAsync(default)).Throws(); + + _workerChannel = new GrpcWorkerChannel( + _workerId, + _eventManager, + _mockScriptHostManager.Object, + _testWorkerConfig, + mockrpcWorkerProcessThatThrows.Object, + _logger, + _metricsLogger, + 0, + _testEnvironment, + _hostOptionsMonitor, + _sharedMemoryManager, + _workerConcurrencyOptions, + _hostingConfigOptions, + _httpProxyService); + await Assert.ThrowsAsync(async () => await _workerChannel.StartWorkerProcessAsync(CancellationToken.None)); + } + + [Fact] + public async Task StartWorkerProcessAsync_Invoked_SetupFunctionBuffers_Verify_ReadyForInvocation() + { + await CreateDefaultWorkerChannel(); + _mockRpcWorkerProcess.Verify(m => m.StartProcessAsync(default), Times.Once); + Assert.False(_workerChannel.IsChannelReadyForInvocations()); + _workerChannel.SetupFunctionInvocationBuffers(GetTestFunctionsList("node")); + Assert.True(_workerChannel.IsChannelReadyForInvocations()); + } + [Fact] public async Task WorkerChannel_Dispose_With_WorkerTerminateCapability() { @@ -219,16 +289,6 @@ public async Task WorkerChannel_Dispose_Without_WorkerTerminateCapability() Assert.False(traces.Any(m => string.Equals(m.FormattedMessage, expectedLogMsg))); } - [Fact] - public async Task StartWorkerProcessAsync_Invoked_SetupFunctionBuffers_Verify_ReadyForInvocation() - { - await CreateDefaultWorkerChannel(); - _mockrpcWorkerProcess.Verify(m => m.StartProcessAsync(), Times.Once); - Assert.False(_workerChannel.IsChannelReadyForInvocations()); - _workerChannel.SetupFunctionInvocationBuffers(GetTestFunctionsList("node")); - Assert.True(_workerChannel.IsChannelReadyForInvocations()); - } - [Fact] public async Task DisposingChannel_NotReadyForInvocation() { @@ -257,14 +317,6 @@ public void SetupFunctionBuffers_Verify_ReadyForInvocation_Returns_False() Assert.False(_workerChannel.IsChannelReadyForInvocations()); } - [Fact] - public async Task StartWorkerProcessAsync_TimesOut() - { - await CreateDefaultWorkerChannel(autoStart: false); // suppress for timeout - var initTask = _workerChannel.StartWorkerProcessAsync(CancellationToken.None); - await Assert.ThrowsAsync(async () => await initTask); - } - [Fact] public async Task SendEnvironmentReloadRequest_Generates_ExpectedMetrics() { @@ -281,31 +333,6 @@ public async Task SendEnvironmentReloadRequest_Generates_ExpectedMetrics() Assert.True(_metricsLogger.EventsBegan.Contains(MetricEventNames.SpecializationEnvironmentReloadRequestResponse)); } - [Fact] - public async Task StartWorkerProcessAsync_WorkerProcess_Throws() - { - // note: uses custom worker channel - Mock mockrpcWorkerProcessThatThrows = new Mock(); - mockrpcWorkerProcessThatThrows.Setup(m => m.StartProcessAsync()).Throws(); - - _workerChannel = new GrpcWorkerChannel( - _workerId, - _eventManager, - _mockScriptHostManager.Object, - _testWorkerConfig, - mockrpcWorkerProcessThatThrows.Object, - _logger, - _metricsLogger, - 0, - _testEnvironment, - _hostOptionsMonitor, - _sharedMemoryManager, - _workerConcurrencyOptions, - _hostingConfigOptions, - _httpProxyService); - await Assert.ThrowsAsync(async () => await _workerChannel.StartWorkerProcessAsync(CancellationToken.None)); - } - [Fact] public async Task SendWorkerInitRequest_PublishesOutboundEvent() { @@ -545,7 +572,7 @@ public async Task Drain_Verify() _eventManager, _mockScriptHostManager.Object, _testWorkerConfig, - _mockrpcWorkerProcess.Object, + _mockRpcWorkerProcess.Object, _logger, _metricsLogger, 0, @@ -1269,7 +1296,7 @@ public async Task GetLatencies_StartsTimer_WhenDynamicConcurrencyEnabled() _eventManager, _mockScriptHostManager.Object, config, - _mockrpcWorkerProcess.Object, + _mockRpcWorkerProcess.Object, _logger, _metricsLogger, 0, @@ -1310,7 +1337,7 @@ public async Task GetLatencies_DoesNot_StartTimer_WhenDynamicConcurrencyDisabled _eventManager, _mockScriptHostManager.Object, config, - _mockrpcWorkerProcess.Object, + _mockRpcWorkerProcess.Object, _logger, _metricsLogger, 0, diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs index e2df3f80df..08d9f6d696 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerConfigFactoryTests.cs @@ -245,10 +245,10 @@ public void GetWorkerProcessCount_Tests(bool defaultWorkerConfig, bool setProces _testEnvironment.SetEnvironmentVariable(RpcWorkerConstants.FunctionsWorkerProcessCountSettingName, "7"); } - var config = new ConfigurationBuilder().Build(); - var testLogger = new TestLogger("test"); + IConfiguration config = new ConfigurationBuilder().Build(); + TestLogger testLogger = new("test"); - RpcWorkerConfigFactory rpcWorkerConfigFactory = new RpcWorkerConfigFactory(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); + RpcWorkerConfigFactory rpcWorkerConfigFactory = new(config, testLogger, _testSysRuntimeInfo, _testEnvironment, new TestMetricsLogger(), _testWorkerProfileManager); var result = rpcWorkerConfigFactory.GetWorkerProcessCount(workerConfig); if (defaultWorkerConfig) diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerProcessTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerProcessTests.cs index 3757a50a9f..88599e1e02 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerProcessTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/RpcWorkerProcessTests.cs @@ -2,10 +2,9 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Scale; using Microsoft.Azure.WebJobs.Script.Config; using Microsoft.Azure.WebJobs.Script.Eventing; @@ -260,5 +259,64 @@ public void WorkerProcess_WaitForExit_AfterExit_DoesNotThrow() Exception ex = traces.Single().Exception; Assert.IsType(ex); } + + [Fact] + public async Task WorkerProcess_WaitForExit_NotStarted_Throws() + { + using var rpcWorkerProcess = GetRpcWorkerConfigProcess( + TestHelpers.GetTestWorkerConfigsWithExecutableWorkingDirectory().ElementAt(0)); + await Assert.ThrowsAsync(() => rpcWorkerProcess.WaitForExitAsync()); + } + + [Fact] + public async Task WorkerProcess_WaitForExit_Success_TaskCompletes() + { + // arrange + using Process process = GetProcess(exitCode: 0); + _hostProcessMonitorMock.Setup(m => m.RegisterChildProcess(process)); + _hostProcessMonitorMock.Setup(m => m.UnregisterChildProcess(process)); + _workerProcessFactory.Setup(m => m.CreateWorkerProcess(It.IsNotNull())).Returns(process); + using var rpcWorkerProcess = GetRpcWorkerConfigProcess( + TestHelpers.GetTestWorkerConfigsWithExecutableWorkingDirectory().ElementAt(0)); + + // act + await rpcWorkerProcess.StartProcessAsync(); + + // assert + await rpcWorkerProcess.WaitForExitAsync(); + } + + [Fact] + public async Task WorkerProcess_WaitForExit_Error_Rethrows() + { + // arrange + using Process process = GetProcess(exitCode: -1); + _hostProcessMonitorMock.Setup(m => m.RegisterChildProcess(process)); + _hostProcessMonitorMock.Setup(m => m.UnregisterChildProcess(process)); + _workerProcessFactory.Setup(m => m.CreateWorkerProcess(It.IsNotNull())).Returns(process); + using var rpcWorkerProcess = GetRpcWorkerConfigProcess( + TestHelpers.GetTestWorkerConfigsWithExecutableWorkingDirectory().ElementAt(0)); + + // act + await rpcWorkerProcess.StartProcessAsync(); + + // assert + await Assert.ThrowsAnyAsync(() => rpcWorkerProcess.WaitForExitAsync()); + } + + private static Process GetProcess(int exitCode) + { + return new() + { + StartInfo = new() + { + WindowStyle = ProcessWindowStyle.Hidden, + FileName = OperatingSystem.IsWindows() ? "cmd" : "bash", + Arguments = OperatingSystem.IsWindows() ? $"/C exit {exitCode}" : $"-c \"exit {exitCode}\"", + RedirectStandardError = true, + RedirectStandardOutput = true, + } + }; + } } -} \ No newline at end of file +}