diff --git a/src/WebJobs.Script/Description/DotNet/Compilation/CSharp/CSharpCompilationService.cs b/src/WebJobs.Script/Description/DotNet/Compilation/CSharp/CSharpCompilationService.cs index a7e0ba1cd5..9338357dd6 100644 --- a/src/WebJobs.Script/Description/DotNet/Compilation/CSharp/CSharpCompilationService.cs +++ b/src/WebJobs.Script/Description/DotNet/Compilation/CSharp/CSharpCompilationService.cs @@ -71,10 +71,11 @@ private Compilation GetScriptCompilation(Script script, FunctionMetadata if (_optimizationLevel == OptimizationLevel.Debug) { - SyntaxTree scriptTree = compilation.SyntaxTrees.FirstOrDefault(t => string.IsNullOrEmpty(t.FilePath)); + string scriptFileName = Path.GetFileName(functionMetadata.ScriptFile); + SyntaxTree scriptTree = compilation.SyntaxTrees.FirstOrDefault(t => string.Equals(t.FilePath, scriptFileName)); var debugTree = SyntaxFactory.SyntaxTree(scriptTree.GetRoot(), encoding: UTF8WithNoBOM, - path: Path.GetFileName(functionMetadata.ScriptFile), + path: scriptFileName, options: new CSharpParseOptions(kind: SourceCodeKind.Script)); compilation = compilation diff --git a/src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs b/src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs index 3ac62d0125..2675cda13d 100644 --- a/src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs +++ b/src/WebJobs.Script/Description/DotNet/DotNetFunctionInvoker.cs @@ -79,8 +79,7 @@ internal DotNetFunctionInvoker(ScriptHost host, private static IFunctionMetadataResolver CreateMetadataResolver(ScriptHost host, FunctionMetadata functionMetadata, TraceWriter traceWriter) { - string functionScriptDirectory = Path.GetDirectoryName(functionMetadata.ScriptFile); - return new FunctionMetadataResolver(functionScriptDirectory, host.ScriptConfig.BindingProviders, + return new FunctionMetadataResolver(functionMetadata.ScriptFile, host.ScriptConfig.BindingProviders, traceWriter, host.ScriptConfig.HostConfig.LoggerFactory); } diff --git a/src/WebJobs.Script/Description/DotNet/FunctionMetadataResolver.cs b/src/WebJobs.Script/Description/DotNet/FunctionMetadataResolver.cs index f96940dadc..4b74b5d77f 100644 --- a/src/WebJobs.Script/Description/DotNet/FunctionMetadataResolver.cs +++ b/src/WebJobs.Script/Description/DotNet/FunctionMetadataResolver.cs @@ -26,6 +26,7 @@ public sealed class FunctionMetadataResolver : MetadataReferenceResolver, IFunct { private readonly string _privateAssembliesPath; private readonly string _scriptFileDirectory; + private readonly string _scriptFilePath; private readonly string[] _assemblyExtensions = new[] { ".exe", ".dll" }; private readonly string _id = Guid.NewGuid().ToString(); private readonly TraceWriter _traceWriter; @@ -75,12 +76,13 @@ public sealed class FunctionMetadataResolver : MetadataReferenceResolver, IFunct "Microsoft.Extensions.Logging" }; - public FunctionMetadataResolver(string scriptFileDirectory, ICollection bindingProviders, TraceWriter traceWriter, ILoggerFactory loggerFactory) + public FunctionMetadataResolver(string scriptFilePath, ICollection bindingProviders, TraceWriter traceWriter, ILoggerFactory loggerFactory) { - _scriptFileDirectory = scriptFileDirectory; + _scriptFileDirectory = Path.GetDirectoryName(scriptFilePath); + _scriptFilePath = scriptFilePath; _traceWriter = traceWriter; - _packageAssemblyResolver = new PackageAssemblyResolver(scriptFileDirectory); - _privateAssembliesPath = GetBinDirectory(scriptFileDirectory); + _packageAssemblyResolver = new PackageAssemblyResolver(_scriptFileDirectory); + _privateAssembliesPath = GetBinDirectory(_scriptFileDirectory); _scriptResolver = ScriptMetadataResolver.Default.WithSearchPaths(_privateAssembliesPath); _extensionSharedAssemblyProvider = new ExtensionSharedAssemblyProvider(bindingProviders); _loggerFactory = loggerFactory; @@ -91,10 +93,11 @@ public ScriptOptions CreateScriptOptions() _externalReferences.Clear(); return ScriptOptions.Default - .WithMetadataResolver(this) - .WithReferences(GetCompilationReferences()) - .WithImports(DefaultNamespaceImports) - .WithSourceResolver(new SourceFileResolver(ImmutableArray.Empty, _scriptFileDirectory)); + .WithFilePath(Path.GetFileName(_scriptFilePath)) + .WithMetadataResolver(this) + .WithReferences(GetCompilationReferences()) + .WithImports(DefaultNamespaceImports) + .WithSourceResolver(new SourceFileResolver(ImmutableArray.Empty, _scriptFileDirectory)); } /// diff --git a/src/WebJobs.Script/Description/FunctionInvokerBase.cs b/src/WebJobs.Script/Description/FunctionInvokerBase.cs index 083f17ae40..71ea9107ce 100644 --- a/src/WebJobs.Script/Description/FunctionInvokerBase.cs +++ b/src/WebJobs.Script/Description/FunctionInvokerBase.cs @@ -168,6 +168,32 @@ internal void TraceCompilationDiagnostics(ImmutableArray diagnostics { traceWriter.Trace(diagnostic.ToString(), diagnostic.Severity.ToTraceLevel(), properties); } + + if (Host.InDebugMode && Host.IsPrimary) + { + Host.EventManager.Publish(new StructuredLogEntryEvent(() => + { + var logEntry = new StructuredLogEntry("codediagnostic"); + logEntry.AddProperty("functionName", Metadata.Name); + logEntry.AddProperty("diagnostics", diagnostics.Select(d => + { + FileLinePositionSpan span = d.Location.GetMappedLineSpan(); + return new + { + code = d.Id, + message = d.GetMessage(), + source = Path.GetFileName(d.Location.SourceTree?.FilePath ?? span.Path ?? string.Empty), + severity = d.Severity, + startLineNumber = span.StartLinePosition.Line + 1, + startColumn = span.StartLinePosition.Character + 1, + endLine = span.EndLinePosition.Line + 1, + endColumn = span.EndLinePosition.Character + 1, + }; + })); + + return logEntry; + })); + } } protected virtual void Dispose(bool disposing) diff --git a/src/WebJobs.Script/Diagnostics/FileTraceWriter.cs b/src/WebJobs.Script/Diagnostics/FileTraceWriter.cs index 915bc8697c..8d0558c502 100644 --- a/src/WebJobs.Script/Diagnostics/FileTraceWriter.cs +++ b/src/WebJobs.Script/Diagnostics/FileTraceWriter.cs @@ -23,6 +23,7 @@ public class FileTraceWriter : TraceWriter, IDisposable internal const int MaxLogLinesPerFlushInterval = 250; private readonly string _logFilePath; private readonly string _instanceId; + private readonly Func _messageFormatter; private readonly DirectoryInfo _logDirectory; private static object _syncLock = new object(); @@ -32,10 +33,11 @@ public class FileTraceWriter : TraceWriter, IDisposable private Timer _flushTimer; private ConcurrentQueue _logBuffer = new ConcurrentQueue(); - public FileTraceWriter(string logFilePath, TraceLevel level) : base(level) + public FileTraceWriter(string logFilePath, TraceLevel level, Func messageFormatter = null) : base(level) { _logFilePath = logFilePath; _instanceId = GetInstanceId(); + _messageFormatter = messageFormatter ?? FormatMessage; _logDirectory = new DirectoryInfo(logFilePath); if (!_logDirectory.Exists) @@ -209,10 +211,14 @@ protected virtual void AppendLine(string line) // add the line to the current buffer batch, which is flushed // on a timer - line = string.Format(CultureInfo.InvariantCulture, "{0} {1}", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), line.Trim()); + line = _messageFormatter(line); + _logBuffer.Enqueue(line); } + private string FormatMessage(string message) + => string.Format(CultureInfo.InvariantCulture, "{0} {1}", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), message.Trim()); + private void OnFlushLogs(object sender, ElapsedEventArgs e) { Flush(); diff --git a/src/WebJobs.Script/Diagnostics/StructuredLogWriter.cs b/src/WebJobs.Script/Diagnostics/StructuredLogWriter.cs new file mode 100644 index 0000000000..052ca47013 --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/StructuredLogWriter.cs @@ -0,0 +1,56 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Script.Eventing; + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics +{ + public sealed class StructuredLogWriter : IDisposable + { + private readonly IDisposable _subscription; + private readonly FileTraceWriter _traceWriter; + private bool _disposedValue = false; + + public StructuredLogWriter(IScriptEventManager eventManager, string baseLogPath) + { + string logPath = Path.Combine(baseLogPath, "structured"); + _traceWriter = new FileTraceWriter(logPath, TraceLevel.Verbose, s => s); + + _subscription = eventManager.OfType() + .Subscribe(OnLogEntry); + } + + private void OnLogEntry(StructuredLogEntryEvent logEvent) + { + string message = logEvent.LogEntry.ToJsonLineString(); + _traceWriter.Trace(message, TraceLevel.Verbose, null); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _subscription?.Dispose(); + _traceWriter?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntry.cs b/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntry.cs new file mode 100644 index 0000000000..af5fc536e3 --- /dev/null +++ b/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntry.cs @@ -0,0 +1,74 @@ +// 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.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.WebJobs.Script.Eventing +{ + public sealed class StructuredLogEntry + { + private readonly Dictionary _properties = new Dictionary(); + + public StructuredLogEntry(string name) + : this(Guid.NewGuid(), name) + { + } + + public StructuredLogEntry(Guid id, string name) + { + Id = id; + Name = name; + _properties = new Dictionary(); + } + + /// + /// Gets the event ID. This uniquely identifies this instance. + /// + /// Gets the event name. This identifies the type of event represented by the instance. + /// + public string Name { get; } + + /// + /// Adds a log entry property. + /// + /// The property name. + /// The property value. + public void AddProperty(string name, object value) + { + if (string.Equals(nameof(Name), name, StringComparison.OrdinalIgnoreCase) || + string.Equals(nameof(Id), name, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"{name} is an invalid property name.", nameof(name)); + } + + _properties.Add(name, value); + } + + /// + /// Returns a JSON string representation of this object in a single line. + /// + /// A JSON string representation of this object in a single line. + public string ToJsonLineString() + { + var resultObject = new JObject + { + ["name"] = Name, + ["id"] = Id + }; + foreach (var item in _properties) + { + resultObject.Add(item.Key, JToken.FromObject(item.Value)); + } + + return resultObject.ToString(Formatting.None); + } + } +} \ No newline at end of file diff --git a/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntryEvent.cs b/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntryEvent.cs new file mode 100644 index 0000000000..798c542f06 --- /dev/null +++ b/src/WebJobs.Script/Eventing/StructuredLogging/StructuredLogEntryEvent.cs @@ -0,0 +1,36 @@ +// 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.Threading; + +namespace Microsoft.Azure.WebJobs.Script.Eventing +{ + public sealed class StructuredLogEntryEvent : ScriptEvent + { + private readonly Lazy _logEntry; + + public StructuredLogEntryEvent(StructuredLogEntry logEntry) + : this(logEntry, string.Empty) + { + } + + public StructuredLogEntryEvent(StructuredLogEntry logEntry, string source) + : this(() => logEntry, source) + { + } + + public StructuredLogEntryEvent(Func logEntryFactory) + : this(logEntryFactory, string.Empty) + { + } + + public StructuredLogEntryEvent(Func logEntryFactory, string source) + : base(nameof(StructuredLogEntryEvent), source) + { + _logEntry = new Lazy(logEntryFactory, LazyThreadSafetyMode.ExecutionAndPublication); + } + + public StructuredLogEntry LogEntry => _logEntry.Value; + } +} diff --git a/src/WebJobs.Script/Description/DotNet/DiagnosticExtensions.cs b/src/WebJobs.Script/Extensions/DiagnosticExtensions.cs similarity index 85% rename from src/WebJobs.Script/Description/DotNet/DiagnosticExtensions.cs rename to src/WebJobs.Script/Extensions/DiagnosticExtensions.cs index 1573819cd8..95a16e8ab7 100644 --- a/src/WebJobs.Script/Description/DotNet/DiagnosticExtensions.cs +++ b/src/WebJobs.Script/Extensions/DiagnosticExtensions.cs @@ -4,9 +4,10 @@ using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.Azure.WebJobs.Script.Eventing; using Microsoft.CodeAnalysis; -namespace Microsoft.Azure.WebJobs.Script.Description.DotNet.CSharp.Analyzers +namespace Microsoft.Azure.WebJobs.Script.Description { internal static class DiagnosticExtensions { diff --git a/src/WebJobs.Script/GlobalSuppressions.cs b/src/WebJobs.Script/GlobalSuppressions.cs index 6d70118b16..cbf42a56a3 100644 --- a/src/WebJobs.Script/GlobalSuppressions.cs +++ b/src/WebJobs.Script/GlobalSuppressions.cs @@ -207,3 +207,8 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sku", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptConstants.#DynamicSkuConnectionLimit")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#AddDirectTypes(System.Collections.Generic.List`1,System.Collections.ObjectModel.Collection`1)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#LoadCustomExtensions(System.String)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.DefaultLoggerFactoryBuilder.#AddLoggerProviders(Microsoft.Extensions.Logging.ILoggerFactory,Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Eventing.StructuredLogging.StructuredLogEntry.#ToJson()")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Eventing.StructuredLogging.StructuredLogEntry.#ToJson()")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_traceWriter", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Diagnostics.StructuredLogWriter.#Dispose(System.Boolean)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FileTraceWriter.#.ctor(System.String,System.Diagnostics.TraceLevel,System.Func`2)")] \ No newline at end of file diff --git a/src/WebJobs.Script/Host/ScriptHostManager.cs b/src/WebJobs.Script/Host/ScriptHostManager.cs index 2319a2fdbd..c8b313c23c 100644 --- a/src/WebJobs.Script/Host/ScriptHostManager.cs +++ b/src/WebJobs.Script/Host/ScriptHostManager.cs @@ -29,6 +29,7 @@ public class ScriptHostManager : IScriptHostEnvironment, IDisposable private readonly IScriptHostFactory _scriptHostFactory; private readonly IScriptHostEnvironment _environment; private readonly IDisposable _fileEventSubscription; + private readonly StructuredLogWriter _structuredLogWriter; private ScriptHost _currentInstance; // ScriptHosts are not thread safe, so be clear that only 1 thread at a time operates on each instance. @@ -72,6 +73,8 @@ public ScriptHostManager(ScriptHostConfiguration config, _scriptHostFactory = scriptHostFactory; EventManager = eventManager ?? new ScriptEventManager(); + + _structuredLogWriter = new StructuredLogWriter(eventManager, config.RootLogPath); } protected IScriptEventManager EventManager { get; } @@ -376,6 +379,7 @@ protected virtual void Dispose(bool disposing) _stopEvent.Dispose(); _restartDelayTokenSource?.Dispose(); _fileEventSubscription?.Dispose(); + _structuredLogWriter.Dispose(); _restartHostEvent.Dispose(); _disposed = true; diff --git a/src/WebJobs.Script/WebJobs.Script.csproj b/src/WebJobs.Script/WebJobs.Script.csproj index e4d5ecb7dc..f4ee5bd8ca 100644 --- a/src/WebJobs.Script/WebJobs.Script.csproj +++ b/src/WebJobs.Script/WebJobs.Script.csproj @@ -423,7 +423,6 @@ - @@ -496,6 +495,7 @@ + @@ -504,7 +504,10 @@ + + + @@ -631,7 +634,9 @@ - + + + diff --git a/test/WebJobs.Script.Tests/Description/DotNet/FunctionAssemblyLoaderTests.cs b/test/WebJobs.Script.Tests/Description/DotNet/FunctionAssemblyLoaderTests.cs index 091ea89738..b9b41b3db0 100644 --- a/test/WebJobs.Script.Tests/Description/DotNet/FunctionAssemblyLoaderTests.cs +++ b/test/WebJobs.Script.Tests/Description/DotNet/FunctionAssemblyLoaderTests.cs @@ -28,7 +28,7 @@ public void ResolveAssembly_WithIndirectPrivateDependency_IsResolved() mockResolver.Setup(m => m.ResolveAssembly("MyTestAssembly.dll")) .Returns(new TestAssembly(new AssemblyName("MyTestAssembly"))); - resolver.CreateOrUpdateContext(metadata1, this.GetType().Assembly, new FunctionMetadataResolver(metadata1Directory, new Collection(), traceWriter, null), traceWriter, null); + resolver.CreateOrUpdateContext(metadata1, this.GetType().Assembly, new FunctionMetadataResolver(metadata1.ScriptFile, new Collection(), traceWriter, null), traceWriter, null); resolver.CreateOrUpdateContext(metadata2, this.GetType().Assembly, mockResolver.Object, traceWriter, null); Assembly result = resolver.ResolveAssembly(null, new System.ResolveEventArgs("MyTestAssembly.dll", @@ -51,7 +51,7 @@ public void ResolveAssembly_WithIndirectPrivateDependency_LogsIfResolutionFails( mockResolver.Setup(m => m.ResolveAssembly("MyTestAssembly.dll")) .Returns(null); - resolver.CreateOrUpdateContext(metadata1, this.GetType().Assembly, new FunctionMetadataResolver(metadata1Directory, new Collection(), traceWriter, null), traceWriter, null); + resolver.CreateOrUpdateContext(metadata1, this.GetType().Assembly, new FunctionMetadataResolver(metadata1.ScriptFile, new Collection(), traceWriter, null), traceWriter, null); resolver.CreateOrUpdateContext(metadata2, this.GetType().Assembly, mockResolver.Object, traceWriter, null); Assembly result = resolver.ResolveAssembly(AppDomain.CurrentDomain, new System.ResolveEventArgs("MyTestAssembly.dll", diff --git a/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryEventTests.cs b/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryEventTests.cs new file mode 100644 index 0000000000..9afeb258c6 --- /dev/null +++ b/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryEventTests.cs @@ -0,0 +1,27 @@ +// 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 Microsoft.Azure.WebJobs.Script.Eventing; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Eventing +{ + public class StructuredLogEntryEventTests + { + [Fact] + public void WhenEventsAreNotConsumed_FactoryIsNotInvoked() + { + bool factoryInvoked = false; + Func factory = () => + { + factoryInvoked = true; + return new StructuredLogEntry("test"); + }; + + var logEntryEvent = new StructuredLogEntryEvent(factory); + + Assert.False(factoryInvoked); + } + } +} diff --git a/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryTests.cs b/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryTests.cs new file mode 100644 index 0000000000..ab2da9cac6 --- /dev/null +++ b/test/WebJobs.Script.Tests/Eventing/StructuredLogEntryTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Script.Eventing; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Eventing +{ + public class StructuredLogEntryTests + { + [Fact] + public void MultiLineValues_ReturnsSingleLine() + { + string entryName = "testlog"; + var logEntry = new StructuredLogEntry(entryName); + + logEntry.AddProperty("test", "multi\r\nline\r\nproperty"); + logEntry.AddProperty("jobject", new JObject { { "prop1", "val1" }, { "prop2", "val2" } }); + + string result = logEntry.ToJsonLineString(); + + Assert.DoesNotContain("\n", result); + } + } +} diff --git a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj index 7303869654..17ad32de31 100644 --- a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj +++ b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj @@ -498,6 +498,8 @@ + +