diff --git a/.reporoot b/.reporoot new file mode 100644 index 000000000..96003cbd2 --- /dev/null +++ b/.reporoot @@ -0,0 +1 @@ +File to mark repo root. Do not edit. \ No newline at end of file diff --git a/samples/FunctionApp/FunctionApp.csproj b/samples/FunctionApp/FunctionApp.csproj index e0c363aa4..0683410bf 100644 --- a/samples/FunctionApp/FunctionApp.csproj +++ b/samples/FunctionApp/FunctionApp.csproj @@ -9,7 +9,7 @@ - + diff --git a/sdk/Sdk/ExtensionsCsprojGenerator.cs b/sdk/Sdk/ExtensionsCsprojGenerator.cs index b8ba95584..4b5b0a3de 100644 --- a/sdk/Sdk/ExtensionsCsprojGenerator.cs +++ b/sdk/Sdk/ExtensionsCsprojGenerator.cs @@ -10,8 +10,6 @@ namespace Microsoft.Azure.Functions.Worker.Sdk { internal class ExtensionsCsprojGenerator { - internal const string ExtensionsProjectName = "WorkerExtensions.csproj"; - private readonly IDictionary _extensions; private readonly string _outputPath; private readonly string _targetFrameworkIdentifier; @@ -29,32 +27,20 @@ public ExtensionsCsprojGenerator(IDictionary extensions, string public void Generate() { - var extensionsCsprojFilePath = Path.Combine(_outputPath, ExtensionsProjectName); - string csproj = GetCsProjContent(); - if (File.Exists(extensionsCsprojFilePath)) + if (File.Exists(_outputPath)) { - string existing = File.ReadAllText(extensionsCsprojFilePath); + string existing = File.ReadAllText(_outputPath); if (string.Equals(csproj, existing, StringComparison.Ordinal)) { // If contents are the same, only touch the file to update timestamp. - File.SetLastWriteTimeUtc(extensionsCsprojFilePath, DateTime.UtcNow); + File.SetLastWriteTimeUtc(_outputPath, DateTime.UtcNow); return; } } - RecreateDirectory(_outputPath); - File.WriteAllText(extensionsCsprojFilePath, csproj); - } - - private void RecreateDirectory(string directoryPath) - { - if (Directory.Exists(directoryPath)) - { - Directory.Delete(directoryPath, recursive: true); - } - - Directory.CreateDirectory(directoryPath); + Directory.CreateDirectory(Path.GetDirectoryName(_outputPath)); + File.WriteAllText(_outputPath, csproj); } internal string GetCsProjContent() @@ -70,7 +56,7 @@ internal string GetCsProjContent() } } - string netSdkVersion = _azureFunctionsVersion.StartsWith(Constants.AzureFunctionsVersion3, StringComparison.OrdinalIgnoreCase) ? "3.1.2" : "4.3.0"; + string netSdkVersion = _azureFunctionsVersion.StartsWith(Constants.AzureFunctionsVersion3, StringComparison.OrdinalIgnoreCase) ? "3.1.2" : "4.6.0"; return $@" diff --git a/sdk/Sdk/ExtensionsMetadata.cs b/sdk/Sdk/ExtensionsMetadata.cs index f7ed7912f..2fcd1ae18 100644 --- a/sdk/Sdk/ExtensionsMetadata.cs +++ b/sdk/Sdk/ExtensionsMetadata.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; @@ -9,6 +9,6 @@ namespace Microsoft.Azure.Functions.Worker.Sdk public class ExtensionsMetadata { [JsonPropertyName("extensions")] - public IEnumerable? Extensions { get; set; } + public List Extensions { get; set; } = new List(); } } diff --git a/sdk/Sdk/ExtensionsMetadataEnhancer.cs b/sdk/Sdk/ExtensionsMetadataEnhancer.cs index 9259dbc70..11266b74a 100644 --- a/sdk/Sdk/ExtensionsMetadataEnhancer.cs +++ b/sdk/Sdk/ExtensionsMetadataEnhancer.cs @@ -2,7 +2,11 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; +using Mono.Cecil; namespace Microsoft.Azure.Functions.Worker.Sdk { @@ -25,6 +29,34 @@ public static void AddHintPath(IEnumerable extensions) } } + public static IEnumerable GetWebJobsExtensions(string fileName) + { + // NOTE: this is an incomplete approach to getting extensions and is intended only for our usages. + // Running this with arbitrary assemblies (especially user supplied) can lead to exceptions. + AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(fileName); + IEnumerable attributes = assembly.Modules.SelectMany(p => p.GetCustomAttributes()) + .Where(a => a.AttributeType.FullName == "Microsoft.Azure.WebJobs.Hosting.WebJobsStartupAttribute"); + + foreach (CustomAttribute attribute in attributes) + { + CustomAttributeArgument typeProperty = attribute.ConstructorArguments.ElementAtOrDefault(0); + CustomAttributeArgument nameProperty = attribute.ConstructorArguments.ElementAtOrDefault(1); + + TypeDefinition typeDef = (TypeDefinition)typeProperty.Value; + string assemblyQualifiedName = Assembly.CreateQualifiedName( + typeDef.Module.Assembly.FullName, GetReflectionFullName(typeDef)); + + string name = GetName((string)nameProperty.Value, typeDef); + + yield return new ExtensionReference + { + Name = name, + TypeName = assemblyQualifiedName, + HintPath = $@"{ExtensionsBinaryDirectoryPath}/{Path.GetFileName(fileName)}", + }; + } + } + private static string? GetAssemblyNameOrNull(string? typeName) { if (typeName == null) @@ -41,5 +73,33 @@ public static void AddHintPath(IEnumerable extensions) return null; } + + // Copying the WebJobsStartup constructor logic from: + // https://github.com/Azure/azure-webjobs-sdk/blob/e5417775bcb8c8d3d53698932ca8e4e265eac66d/src/Microsoft.Azure.WebJobs.Host/Hosting/WebJobsStartupAttribute.cs#L33-L47. + private static string GetName(string name, TypeDefinition startupTypeDef) + { + if (string.IsNullOrEmpty(name)) + { + // for a startup class named 'CustomConfigWebJobsStartup' or 'CustomConfigStartup', + // default to a name 'CustomConfig' + name = startupTypeDef.Name; + int idx = name.IndexOf("WebJobsStartup"); + if (idx < 0) + { + idx = name.IndexOf("Startup"); + } + if (idx > 0) + { + name = name.Substring(0, idx); + } + } + + return name; + } + + private static string GetReflectionFullName(TypeReference typeRef) + { + return typeRef.FullName.Replace("/", "+"); + } } } diff --git a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.targets b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.targets index 40f57986f..5b057d1b6 100644 --- a/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.targets +++ b/sdk/Sdk/Targets/Microsoft.Azure.Functions.Worker.Sdk.targets @@ -10,11 +10,33 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and --> + + + <_FunctionsTaskFramework Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 + <_FunctionsTaskFramework Condition="'$(_FunctionsTaskFramework)' == ''">net472 + <_FunctionsTasksDir Condition="'$(_FunctionsTasksDir)'==''">$(MSBuildThisFileDirectory)..\tools\$(_FunctionsTaskFramework)\ + <_FunctionsTaskAssemblyFullPath Condition=" '$(_FunctionsTaskAssemblyFullPath)'=='' ">$(_FunctionsTasksDir)\Microsoft.Azure.Functions.Worker.Sdk.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\Managed.Functions\ + + + + + true + <_FunctionsExtensionsDirectory>.azurefunctions + <_FunctionsExtensionsJsonName>extensions.json + <_FunctionsWorkerConfigInputFile>$([MSBuild]::NormalizePath($(MSBuildThisFileDirectory)\..\tools\worker.config.json)) + <_FunctionsMetadataLoaderExtensionFile>$([MSBuild]::NormalizePath($(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll)) + <_FunctionsExtensionCommonProps>ImportDirectoryBuildProps=false;ImportDirectoryBuildTargets=false;ImportDirectoryPackagesProps=false + <_FunctionsExtensionRemoveProps>TargetFramework;RuntimeIdentifier;SelfContained;PublishSingleFile;PublishReadyToRun;UseCurrentRuntimeIdentifier;WebPublishMethod;PublishProfile;DeployOnBuild;PublishDir;OutDir;OutputPath; + + + - <_ToolingSuffix> <_DefaultAzureFunctionsVersion>v4 <_AzureFunctionsVersionNotSet Condition="'$(AzureFunctionsVersion)' == ''">true $(_DefaultAzureFunctionsVersion) + true + <_ToolingSuffix Condition="($(AzureFunctionsVersion.StartsWith('v3',StringComparison.OrdinalIgnoreCase)) Or $(AzureFunctionsVersion.StartsWith('v4',StringComparison.OrdinalIgnoreCase))) And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(TargetFrameworkVersion)' == 'v5.0'">net5-isolated <_ToolingSuffix Condition="$(AzureFunctionsVersion.StartsWith('v4',StringComparison.OrdinalIgnoreCase)) And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(TargetFrameworkVersion)' == 'v6.0'">net6-isolated <_ToolingSuffix Condition="$(AzureFunctionsVersion.StartsWith('v4',StringComparison.OrdinalIgnoreCase)) And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(TargetFrameworkVersion)' == 'v7.0'">net7-isolated @@ -22,19 +44,6 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and <_ToolingSuffix Condition="$(AzureFunctionsVersion.StartsWith('v4',StringComparison.OrdinalIgnoreCase)) And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(TargetFrameworkVersion)' == 'v9.0'">net9-isolated <_ToolingSuffix Condition="$(AzureFunctionsVersion.StartsWith('v4',StringComparison.OrdinalIgnoreCase)) And '$(TargetFrameworkIdentifier)' == '.NETFramework'">netfx-isolated $(_ToolingSuffix) - <_FunctionsTaskFramework Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 - <_FunctionsTaskFramework Condition="'$(_FunctionsTaskFramework)' == ''">net472 - <_FunctionsTasksDir Condition="'$(_FunctionsTasksDir)'==''">$(MSBuildThisFileDirectory)..\tools\$(_FunctionsTaskFramework)\ - <_FunctionsTaskAssemblyFullPath Condition=" '$(_FunctionsTaskAssemblyFullPath)'=='' ">$(_FunctionsTasksDir)\Microsoft.Azure.Functions.Worker.Sdk.dll - - <_FunctionsExtensionCommonProps>ImportDirectoryBuildProps=false;ImportDirectoryBuildTargets=false;ImportDirectoryPackagesProps=false - <_FunctionsExtensionRemoveProps>TargetFramework;Platform;RuntimeIdentifier;SelfContained;PublishSingleFile;PublishReadyToRun;UseCurrentRuntimeIdentifier;WebPublishMethod;PublishProfile;DeployOnBuild;PublishDir - <_FunctionsWorkerConfigInputFile>$(MSBuildThisFileDirectory)\..\tools\worker.config.json - - <_FunctionsMetadataLoaderExtensionFile>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll - <_FunctionsExtensionsDirectory>.azurefunctions - <_FunctionsExtensionsJsonName>extensions.json - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\Managed.Functions\ false true @@ -44,13 +53,13 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and true true - <_FunctionsGenerateExtensionProject Condition="'$(DesignTimeBuild)' != 'true'">true + <_FunctionsBuildEnabled Condition="'$(DesignTimeBuild)' != 'true'">true $(RootNamespace.Replace("-", "_")) <_FunctionsVersion Include="v3" InSupport="false" /> - <_FunctionsVersion Include="$(_DefaultAzureFunctionsVersion)" InSupport="true" /> + <_FunctionsVersion Include="v4" InSupport="true" /> @@ -61,23 +70,42 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and + + + $([System.IO.Path]::GetDirectoryName($(ExtensionsCsProj))) + $(IntermediateOutputPath)WorkerExtensions + $([System.IO.Path]::GetFullPath($(ExtensionsCsProjDirectory))) + $([System.IO.Path]::Combine($(ExtensionsCsProjDirectory), WorkerExtensions.csproj)) + + + + <_FunctionsMetadataPath>$(IntermediateOutputPath)functions.metadata + <_FunctionsWorkerConfigPath>$(IntermediateOutputPath)worker.config.json + + + - + <_AzureFunctionsVersionStandardized>$(AzureFunctionsVersion.ToLowerInvariant().Split('-')[0]) - true <_SelectedFunctionVersion Include="@(_FunctionsVersion)" Condition="'%(_FunctionsVersion.Identity)' == '$(_AzureFunctionsVersionStandardized)'" /> + + + + + + - - + + @@ -90,68 +118,72 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and + + + + DependsOnTargets="_WorkerExtensionsRestore;_WorkerExtensionsBuild" /> - - + + + + - - + + - <_FunctionsMetadataPath>$(IntermediateOutputPath)functions.metadata - <_FunctionsWorkerConfigPath>$(IntermediateOutputPath)worker.config.json - $(IntermediateOutputPath)WorkerExtensions - $([System.IO.Path]::GetFullPath($(ExtensionsCsProjDirectory))) - $([System.IO.Path]::Combine($(ExtensionsCsProjDirectory), WorkerExtensions.csproj)) - <_FunctionsIntermediateExtensionJsonPath>$(ExtensionsCsProjDirectory)\buildout\bin\$(_FunctionsExtensionsJsonName) + <_WorkerExtensionTarget>Build + <_WorkerExtensionTarget Condition="'$(FunctionsGenerateExtensionProject)' == 'false'">GetTargetPath + <_WorkerExtensionProperties Condition="'$(FunctionsGenerateExtensionProject)' == 'true'">Configuration=Release;$(_FunctionsExtensionCommonProps) + + + + + + + + <_FunctionsExtensionsOutputPath>$([System.IO.Path]::GetDirectoryName($(_WorkerExtensionsAssembly))) + <_FunctionsIntermediateExtensionJsonPath>$(_FunctionsExtensionsOutputPath)/bin/$(_FunctionsExtensionsJsonName) <_FunctionsIntermediateExtensionUpdatedJsonPath>$(IntermediateOutputPath)$(_FunctionsExtensionsJsonName) - - - - - - - - + + + DependsOnTargets="_FunctionsGenerateCommon;_FunctionsGenerateWorkerConfig" /> + + + <_TargetExtensionsCsProj Condition="'$(FunctionsGenerateExtensionProject)' == 'true'">$(ExtensionsCsProj) + + - - - - - @@ -171,27 +203,12 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and Overwrite="true" /> - - - - - - - - - - - + + OutputPath="$(_FunctionsIntermediateExtensionUpdatedJsonPath)" + AdditionalExtensions="$(_FunctionsMetadataLoaderExtensionFile)" /> - <_ExtensionBinaries Include="$(ExtensionsCsProjDirectory)\buildout\bin\**" - Exclude="$(ExtensionsCsProjDirectory)\buildout\bin\runtimes\**;$(_FunctionsIntermediateExtensionJsonPath)" - CopyToOutputDirectory="PreserveNewest" - CopyToPublishDirectory="PreserveNewest" /> - <_ExtensionRuntimeBinaries Include="$(ExtensionsCsProjDirectory)\buildout\runtimes\**" - CopyToOutputDirectory="PreserveNewest" - CopyToPublishDirectory="PreserveNewest" /> + <_ExtensionBinaries Include="$(_FunctionsExtensionsOutputPath)\bin\**" + Exclude="$(_FunctionsExtensionsOutputPath)\bin\runtimes\**;$(_FunctionsIntermediateExtensionJsonPath)" /> + <_ExtensionRuntimeBinaries Include="$(_FunctionsExtensionsOutputPath)\runtimes\**" /> - + - + - <_NoneWithTargetPath Include="@(_ExtensionFilesWithTargetPath)" TargetPath="$(_FunctionsExtensionsDirectory)/%(_ExtensionFilesWithTargetPath.TargetPath)" /> + <_FunctionsAdditionalFile Include="$(_FunctionsMetadataPath);$(_FunctionsWorkerConfigPath);$(_FunctionsIntermediateExtensionUpdatedJsonPath)" /> + <_FunctionsAdditionalFile Include="$(_FunctionsMetadataLoaderExtensionFile)" SubPath="$(_FunctionsExtensionsDirectory)/" /> + <_NoneWithTargetPath Include="@(_FunctionsAdditionalFile)" + TargetPath="%(_FunctionsAdditionalFile.SubPath)%(Filename)%(Extension)" + CopyToOutputDirectory="PreserveNewest" + CopyToPublishDirectory="PreserveNewest"/> + <_NoneWithTargetPath Include="@(_ExtensionFilesWithTargetPath)" + TargetPath="$(_FunctionsExtensionsDirectory)/%(_ExtensionFilesWithTargetPath.TargetPath)" + CopyToOutputDirectory="PreserveNewest" + CopyToPublishDirectory="PreserveNewest"/> - <_WorkerExtFilesToClean Include="$(ExtensionsCsProjDirectory)\**" Condition="'$(ExtensionsCsProjDirectory)' != ''" /> - <_WorkerExtFilesToClean Include="$(TargetDir)$(_FunctionsExtensionsDirectory)\**" /> + <_WorkerExtFilesToClean Include="$(ExtensionsCsProjDirectory)/**" Condition="'$(ExtensionsCsProjDirectory)' != '' AND '$(FunctionsGenerateExtensionProject)' == 'true'" /> + <_WorkerExtFilesToClean Include="$(TargetDir)$(_FunctionsExtensionsDirectory)/**" /> <_WorkerExtFilesToClean Include="$(_FunctionsMetadataPath)" /> <_WorkerExtFilesToClean Include="$(_FunctionsWorkerConfigPath)" /> <_WorkerExtFilesToClean Include="$(TargetDir)worker.config.json" /> @@ -239,10 +261,19 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and - + + + + + + + + + true + - \ No newline at end of file + diff --git a/sdk/Sdk/Tasks/EnhanceExtensionsMetadata.cs b/sdk/Sdk/Tasks/EnhanceExtensionsMetadata.cs index c76bb4871..4d0460761 100644 --- a/sdk/Sdk/Tasks/EnhanceExtensionsMetadata.cs +++ b/sdk/Sdk/Tasks/EnhanceExtensionsMetadata.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; @@ -24,12 +25,19 @@ public class EnhanceExtensionsMetadata : Task [Required] public string? OutputPath { get; set; } + public ITaskItem[]? AdditionalExtensions { get; set; } + public override bool Execute() { string json = File.ReadAllText(ExtensionsJsonPath); - var extensionsMetadata = JsonSerializer.Deserialize(json); - ExtensionsMetadataEnhancer.AddHintPath(extensionsMetadata?.Extensions ?? Enumerable.Empty()); + var extensionsMetadata = JsonSerializer.Deserialize(json) ?? new ExtensionsMetadata(); + ExtensionsMetadataEnhancer.AddHintPath(extensionsMetadata.Extensions); + + foreach (ITaskItem item in AdditionalExtensions ?? Enumerable.Empty()) + { + extensionsMetadata.Extensions.AddRange(ExtensionsMetadataEnhancer.GetWebJobsExtensions(item.ItemSpec)); + } string newJson = JsonSerializer.Serialize(extensionsMetadata, _serializerOptions); File.WriteAllText(OutputPath, newJson); diff --git a/sdk/Sdk/Tasks/GenerateFunctionMetadata.cs b/sdk/Sdk/Tasks/GenerateFunctionMetadata.cs index e8e42cee2..e54aa995a 100644 --- a/sdk/Sdk/Tasks/GenerateFunctionMetadata.cs +++ b/sdk/Sdk/Tasks/GenerateFunctionMetadata.cs @@ -24,7 +24,6 @@ public class GenerateFunctionMetadata : Task [Required] public string? OutputPath { get; set; } - [Required] public string? ExtensionsCsProjFilePath { get; set; } [Required] @@ -45,12 +44,16 @@ public override bool Execute() { var functionGenerator = new FunctionMetadataGenerator(MSBuildLogger); - var functions = functionGenerator.GenerateFunctionMetadata(AssemblyPath!, ReferencePaths ?? Enumerable.Empty()); + IEnumerable functions = functionGenerator.GenerateFunctionMetadata(AssemblyPath!, ReferencePaths ?? Enumerable.Empty()); + IDictionary extensions = functionGenerator.Extensions; - var extensions = functionGenerator.Extensions; - var extensionsCsProjGenerator = new ExtensionsCsprojGenerator(extensions, ExtensionsCsProjFilePath!, AzureFunctionsVersion!, TargetFrameworkIdentifier!, TargetFrameworkVersion!); + if (!string.IsNullOrEmpty(ExtensionsCsProjFilePath)) + { + // Null/empty ExtensionsCsProjFilePath means the extension project is externally provided. + var extensionsCsProjGenerator = new ExtensionsCsprojGenerator(extensions, ExtensionsCsProjFilePath!, AzureFunctionsVersion!, TargetFrameworkIdentifier!, TargetFrameworkVersion!); + extensionsCsProjGenerator.Generate(); + } - extensionsCsProjGenerator.Generate(); WriteMetadataWithRetry(functions); } catch (FunctionsMetadataGenerationException) diff --git a/test/FunctionMetadataGeneratorTests/ExtensionsCsProjGeneratorTests.cs b/test/FunctionMetadataGeneratorTests/ExtensionsCsProjGeneratorTests.cs index 6576e99d7..e5326296b 100644 --- a/test/FunctionMetadataGeneratorTests/ExtensionsCsProjGeneratorTests.cs +++ b/test/FunctionMetadataGeneratorTests/ExtensionsCsProjGeneratorTests.cs @@ -4,25 +4,42 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Sdk; using Xunit; namespace Microsoft.Azure.Functions.SdkTests { - public class ExtensionsCsProjGeneratorTests + public sealed class ExtensionsCsProjGeneratorTests : IDisposable { + private HashSet _directoriesToCleanup = new(); + public enum FuncVersion { V3, V4, } + public void Dispose() + { + foreach (string directory in _directoriesToCleanup) + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, true); + } + } + + _directoriesToCleanup.Clear(); + } + [Theory] [InlineData(FuncVersion.V3)] [InlineData(FuncVersion.V4)] public void GetCsProjContent_Succeeds(FuncVersion version) { - var generator = GetGenerator(version); + var generator = GetGenerator(version, "TestExtension.csproj"); string actual = generator.GetCsProjContent().Replace("\r\n", "\n"); string expected = ExpectedCsproj(version).Replace("\r\n", "\n"); Assert.Equal(expected, actual); @@ -31,40 +48,159 @@ public void GetCsProjContent_Succeeds(FuncVersion version) [Theory] [InlineData(FuncVersion.V3)] [InlineData(FuncVersion.V4)] - public void GetCsProjContent_IncrementalSupport(FuncVersion version) + public void Generate_IncrementalSupport(FuncVersion version) { - DateTime RunGenerate(string subPath, out string contents) + DateTime RunGenerate(string project, out string contents) { - var generator = GetGenerator(version, subPath); + _directoriesToCleanup.Add(Path.GetDirectoryName(project)); + var generator = GetGenerator(version, project); generator.Generate(); - string path = Path.Combine(subPath, ExtensionsCsprojGenerator.ExtensionsProjectName); - contents = File.ReadAllText(path); - var csproj = new FileInfo(Path.Combine(subPath, ExtensionsCsprojGenerator.ExtensionsProjectName)); + contents = File.ReadAllText(project); + var csproj = new FileInfo(project); return csproj.LastWriteTimeUtc; } - string subPath = Guid.NewGuid().ToString(); - DateTime firstRun = RunGenerate(subPath, out string first); - DateTime secondRun = RunGenerate(subPath, out string second); + string project = Path.Combine(Guid.NewGuid().ToString(), "TestExtension.csproj"); + DateTime firstRun = RunGenerate(project, out string first); + DateTime secondRun = RunGenerate(project, out string second); Assert.NotEqual(firstRun, secondRun); Assert.Equal(first, second); } - static ExtensionsCsprojGenerator GetGenerator(FuncVersion version, string subPath = "") + [Fact] + public async Task Generate_Updates() { - IDictionary extensions = new Dictionary + DateTime RunGenerate(string project, IDictionary extensions, out string contents) + { + _directoriesToCleanup.Add(Path.GetDirectoryName(project)); + var generator = GetGenerator(FuncVersion.V4, project, extensions); + generator.Generate(); + + contents = File.ReadAllText(project); + var csproj = new FileInfo(project); + return csproj.LastWriteTimeUtc; + } + + Dictionary extensions = new() { { "Microsoft.Azure.WebJobs.Extensions.Storage", "4.0.3" }, { "Microsoft.Azure.WebJobs.Extensions.Http", "3.0.0" }, { "Microsoft.Azure.WebJobs.Extensions", "2.0.0" }, }; + string project = Path.Combine(Guid.NewGuid().ToString(), "TestExtension.csproj"); + DateTime firstRun = RunGenerate(project, extensions, out string first); + + await Task.Delay(10); // to ensure timestamps progress. + extensions.Remove(extensions.Keys.First()); + DateTime secondRun = RunGenerate(project, extensions, out string second); + + Assert.NotEqual(firstRun.Ticks, secondRun.Ticks); + Assert.NotEqual(first, second); + } + + [Fact] + public async Task Generate_Subdirectory_CreatesAll() + { + DateTime RunGenerate(string project, out string contents) + { + _directoriesToCleanup.Add(Path.GetDirectoryName(project)); + var generator = GetGenerator(FuncVersion.V4, project); + generator.Generate(); + + contents = File.ReadAllText(project); + var csproj = new FileInfo(project); + return csproj.LastWriteTimeUtc; + } + + DateTime earliest = DateTime.UtcNow; + + await Task.Delay(10); + string project = Path.Combine(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "TestExtension.csproj"); + DateTime time = RunGenerate(project, out string contents); + + Assert.True(time.Ticks >= earliest.Ticks, $"expected last write time {time.Ticks} to be greater than {earliest.Ticks}."); + Assert.NotNull(contents); + } + + [Fact] + public async Task Generate_Subdirectory_CreatesPartial() + { + DateTime RunGenerate(string project, out string contents) + { + _directoriesToCleanup.Add(Path.GetDirectoryName(project)); + var generator = GetGenerator(FuncVersion.V4, project); + generator.Generate(); + + contents = File.ReadAllText(project); + var csproj = new FileInfo(project); + return csproj.LastWriteTimeUtc; + } + + DateTime earliest = DateTime.UtcNow; + string parent = Guid.NewGuid().ToString(); + Directory.CreateDirectory(parent); + _directoriesToCleanup.Add(parent); + + await Task.Delay(10); + string project = Path.Combine(parent, Guid.NewGuid().ToString(), "TestExtension.csproj"); + DateTime time = RunGenerate(project, out string contents); + + Assert.True(time.Ticks >= earliest.Ticks, $"expected last write time {time.Ticks} to be greater than {earliest.Ticks}."); + Assert.NotNull(contents); + } + + [Fact] + public async Task Generate_ExistingDirectory_DoesNotOverwrite() + { + DateTime RunGenerate(string project, out string contents) + { + _directoriesToCleanup.Add(Path.GetDirectoryName(project)); + var generator = GetGenerator(FuncVersion.V4, project); + generator.Generate(); + + contents = File.ReadAllText(project); + var csproj = new FileInfo(project); + return csproj.LastWriteTimeUtc; + } + + string parent = Guid.NewGuid().ToString(); + Directory.CreateDirectory(parent); + _directoriesToCleanup.Add(parent); + + string existing = Path.Combine(parent, "existing.txt"); + File.WriteAllText(existing, ""); + DateTime expectedWriteTime = new FileInfo(existing).LastWriteTimeUtc; + + await Task.Delay(10); + string project = Path.Combine(parent, "TestExtension.csproj"); + DateTime time = RunGenerate(project, out string contents); + + Assert.True(time.Ticks >= expectedWriteTime.Ticks, $"expected last write time {time.Ticks} to be greater than {expectedWriteTime.Ticks}."); + Assert.NotNull(contents); + Assert.Equal(expectedWriteTime, new FileInfo(existing).LastWriteTimeUtc); + } + + static ExtensionsCsprojGenerator GetGenerator(FuncVersion version, string outputPath) + { + Dictionary extensions = new() + { + { "Microsoft.Azure.WebJobs.Extensions.Storage", "4.0.3" }, + { "Microsoft.Azure.WebJobs.Extensions.Http", "3.0.0" }, + { "Microsoft.Azure.WebJobs.Extensions", "2.0.0" }, + }; + + return GetGenerator(version, outputPath, extensions); + } + + static ExtensionsCsprojGenerator GetGenerator(FuncVersion version, string outputPath, IDictionary extensions) + { return version switch { - FuncVersion.V3 => new ExtensionsCsprojGenerator(extensions, subPath, "v3", Constants.NetCoreApp, Constants.NetCoreVersion31), - FuncVersion.V4 => new ExtensionsCsprojGenerator(extensions, subPath, "v4", Constants.NetCoreApp, Constants.NetCoreVersion6), + FuncVersion.V3 => new ExtensionsCsprojGenerator(extensions, outputPath, "v3", Constants.NetCoreApp, Constants.NetCoreVersion31), + FuncVersion.V4 => new ExtensionsCsprojGenerator(extensions, outputPath, "v4", Constants.NetCoreApp, Constants.NetCoreVersion6), _ => throw new ArgumentOutOfRangeException(nameof(version)), }; } @@ -119,7 +255,7 @@ private static string ExpectedCsProjV4() - + diff --git a/test/FunctionMetadataGeneratorTests/ExtensionsMetadataEnhancerTests.cs b/test/FunctionMetadataGeneratorTests/ExtensionsMetadataEnhancerTests.cs index 8b8b21402..0fcf68854 100644 --- a/test/FunctionMetadataGeneratorTests/ExtensionsMetadataEnhancerTests.cs +++ b/test/FunctionMetadataGeneratorTests/ExtensionsMetadataEnhancerTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.Azure.Functions.Worker.Sdk; using Xunit; @@ -37,6 +38,24 @@ public void AddHintPath_DoesNotAdd_WhenAlreadyPresent() ValidateAllEqual(GetBasicReferences_WithPresetHintPath(), extensionsPreset); } + [Fact] + public void GetWebJobsExtensions_FindsExtensions() + { + string assembly = Path.Combine(Tests.TestUtility.RepoRoot, "sdk", "FunctionMetadataLoaderExtension", "bin", Tests.TestUtility.Config, "netstandard2.0", "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"); + var extensions = ExtensionsMetadataEnhancer.GetWebJobsExtensions(assembly); + + ValidateAllEqual( + [ + new ExtensionReference() + { + Name = "Startup", + TypeName = "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c", + HintPath = "./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll", + } + ], + extensions); + } + private static void ValidateAllEqual(IEnumerable expected, IEnumerable actual) { Assert.Equal(expected.Count(), actual.Count()); diff --git a/test/SdkE2ETests/InnerBuildTests.cs b/test/SdkE2ETests/InnerBuildTests.cs index 1f993bd24..3d271f99b 100644 --- a/test/SdkE2ETests/InnerBuildTests.cs +++ b/test/SdkE2ETests/InnerBuildTests.cs @@ -33,11 +33,6 @@ public async Task Build_ScansReferences() JToken extensionsJsonContents = JObject.Parse(File.ReadAllText(extensionsJsonPath)); JToken expectedExtensionsJson = JObject.Parse(@"{ ""extensions"": [ - { - ""name"": ""Startup"", - ""typeName"": ""Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c"", - ""hintPath"": ""./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"" - }, { ""name"": ""AzureStorageBlobs"", ""typeName"": ""Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageBlobsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.1.0, Culture=neutral, PublicKeyToken=92742159e12e44c8"", @@ -47,7 +42,12 @@ public async Task Build_ScansReferences() ""name"": ""AzureStorageQueues"", ""typeName"": ""Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageQueuesWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Queues, Version=5.1.3.0, Culture=neutral, PublicKeyToken=92742159e12e44c8"", ""hintPath"": ""./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll"" - } + }, + { + ""name"": ""Startup"", + ""typeName"": ""Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c"", + ""hintPath"": ""./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"" + }, ] }"); diff --git a/test/SdkE2ETests/PublishTests.cs b/test/SdkE2ETests/PublishTests.cs index 5a7a76923..10b50f39f 100644 --- a/test/SdkE2ETests/PublishTests.cs +++ b/test/SdkE2ETests/PublishTests.cs @@ -88,15 +88,15 @@ private async Task RunPublishTest(string outputDir, string additionalParams = nu { extensions = new[] { - new Extension("Startup", - "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c", - @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"), new Extension("AzureStorageBlobs", "Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageBlobsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.1.0, Culture=neutral, PublicKeyToken=92742159e12e44c8", @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.dll"), new Extension("AzureStorageQueues", "Microsoft.Azure.WebJobs.Extensions.Storage.AzureStorageQueuesWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.Storage.Queues, Version=5.3.1.0, Culture=neutral, PublicKeyToken=92742159e12e44c8", - @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll") + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.Storage.Queues.dll"), + new Extension("Startup", + "Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.Startup, Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c", + @"./.azurefunctions/Microsoft.Azure.WebJobs.Extensions.FunctionMetadataLoader.dll"), } }); Assert.True(JToken.DeepEquals(extensionsJsonContents, expected), $"Actual: {extensionsJsonContents}{Environment.NewLine}Expected: {expected}"); diff --git a/test/TestUtility/TestUtility.cs b/test/TestUtility/TestUtility.cs index dd4eb98ba..cf5df14e3 100644 --- a/test/TestUtility/TestUtility.cs +++ b/test/TestUtility/TestUtility.cs @@ -11,6 +11,25 @@ namespace Microsoft.Azure.Functions.Tests { public static class TestUtility { +#if DEBUG + public static readonly string Config = "Debug"; +#else + public static readonly string Config = "Release"; +#endif + + public static readonly string RepoRoot = GetDirectoryOfFileAbove(".reporoot"); + + public static string GetDirectoryOfFileAbove(string fileName) + { + string current = Directory.GetCurrentDirectory(); + while (!File.Exists(Path.Combine(current, fileName))) + { + current = Directory.GetParent(current).FullName; + } + + return current; + } + public static IConfiguration GetTestConfiguration() { return new ConfigurationBuilder()