From 92a1f5076ee8e8ee67caa9421fda96de5792cdd1 Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Thu, 10 Apr 2025 16:17:01 +0530 Subject: [PATCH 1/6] Publish warning message changes --- .../AzureActions/PublishFunctionAppAction.cs | 94 +++++++- src/Cli/func/Common/Constants.cs | 1 + src/Cli/func/Helpers/EolMessages.cs | 13 +- src/Cli/func/Helpers/JavaHelper.cs | 19 ++ src/Cli/func/Helpers/NodeJSHelpers.cs | 18 ++ src/Cli/func/Helpers/PowerShellHelper.cs | 26 ++ src/Cli/func/Helpers/PythonHelpers.cs | 6 + src/Cli/func/Helpers/StacksApiHelper.cs | 89 +++++++ .../PublishActionTests.cs | 225 ++++++++++++++++++ 9 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 src/Cli/func/Helpers/JavaHelper.cs diff --git a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs index 163b7b249..8a0a1ee46 100644 --- a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs @@ -199,6 +199,41 @@ public override async Task RunAsync() // We do not change the default targetFramework if no .csproj file is found } + // Show warning message for other worker runtimes (Node, Python, Powershell, Java) + if (workerRuntime != WorkerRuntime.dotnet && workerRuntime != WorkerRuntime.dotnetIsolated) + { + string workerRuntimeStr = Convert.ToString(workerRuntime); + string runtimeVersion = GetWorkerRuntimeVersion(workerRuntime, functionAppRoot); + if (!string.IsNullOrEmpty(runtimeVersion)) + { + // Get runtime stacks + var stacks = await AzureHelper.GetFunctionsStacks(AccessToken, ManagementURL); + DateTime currentDate = DateTime.Now; + if (workerRuntime == WorkerRuntime.python) + { + var linuxRuntimeSettings = stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, out bool isLinuxLTS, s => s.LinuxRuntimeSettings); + DateTime eolDate = linuxRuntimeSettings.EndOfLifeDate.Value; + DateTime warningThresholdDate = eolDate.AddMonths(-6); + if (currentDate > eolDate || currentDate >= warningThresholdDate) + { + //Show EOL warning message + ShowEolMessageForOtherStack(stacks, linuxRuntimeSettings.EndOfLifeDate.Value, workerRuntimeStr, runtimeVersion); + } + } + else + { + var runtimeSettings = stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, out bool isWindowsLTS, s => s.WindowsRuntimeSettings); + DateTime eolDate = runtimeSettings.EndOfLifeDate.Value; + DateTime warningThresholdDate = eolDate.AddMonths(-6); + if (currentDate > eolDate || currentDate >= warningThresholdDate) + { + //Show EOL warning message + ShowEolMessageForOtherStack(stacks, runtimeSettings.EndOfLifeDate.Value, workerRuntimeStr, runtimeVersion); + } + } + } + } + // Check for any additional conditions or app settings that need to change // before starting any of the publish activity. var additionalAppSettings = await ValidateFunctionAppPublish(functionApp, workerRuntime, functionAppRoot); @@ -1437,7 +1472,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value); if (nextDotnetVersion != null) { - var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); + var warningMessage = EolMessages.GetAfterEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade); ColoredConsole.WriteLine(WarningColor(warningMessage)); } } @@ -1446,7 +1481,7 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre var nextDotnetVersion = stacks.GetNextDotnetVersion(majorDotnetVersion.Value); if (nextDotnetVersion != null) { - var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); + var warningMessage = EolMessages.GetEarlyEolUpdateMessageDotNet(majorDotnetVersion.ToString(), nextDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value, Constants.FunctionsStackUpgrade); ColoredConsole.WriteLine(WarningColor(warningMessage)); } } @@ -1456,5 +1491,60 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre // ignore. Failure to show the EOL message should not fail the deployment. } } + + /// + /// Determines the version of the specified worker runtime. + /// + private string GetWorkerRuntimeVersion(WorkerRuntime workerRuntime, string functionAppRoot) + { + switch (workerRuntime) + { + case WorkerRuntime.node: + return NodeJSHelpers.GetNodeVersion(functionAppRoot); + case WorkerRuntime.python: + return PythonHelpers.GetPythonVersion(functionAppRoot).GetAwaiter().GetResult(); + case WorkerRuntime.powershell: + return PowerShellHelper.GetPowerShellVersion(functionAppRoot); + case WorkerRuntime.java: + return JavaHelper.GetJavaVersion(functionAppRoot); + default: + return null; + } + } + + private void ShowEolMessageForOtherStack(FunctionsStacks stacks, DateTime eolDate, string workerRuntime, string runtimeVersion) + { + try + { + string nextVersion, displayName, warningMessage = string.Empty; + (nextVersion, displayName) = workerRuntime switch + { + var wr when wr.Equals(WorkerRuntime.python.ToString(), StringComparison.OrdinalIgnoreCase) || wr.Equals(WorkerRuntime.powershell.ToString(), StringComparison.OrdinalIgnoreCase) || wr.Equals(WorkerRuntime.java.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion(workerRuntime, runtimeVersion, + properties => properties.MajorVersions + .SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value), isNumericVersion: false), + var wr when wr.Equals(WorkerRuntime.node.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion(workerRuntime, runtimeVersion, + properties => properties.MajorVersions + .Select(mv => mv.Value), + isNumericVersion: true), + _ => (null, workerRuntime) // Default case: No next version available + }; + if (StacksApiHelper.ExpiresInNextSixMonths(eolDate)) + { + warningMessage = EolMessages.GetEarlyEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade); + ColoredConsole.WriteLine(WarningColor(warningMessage)); + } + else + { + warningMessage = EolMessages.GetAfterEolUpdateMessage(displayName, runtimeVersion, nextVersion, eolDate, Constants.FunctionsStackUpgrade); + ColoredConsole.WriteLine(WarningColor(warningMessage)); + } + } + catch (Exception) + { + // ignore. Failure to show the EOL message should not fail the deployment. + } + } } } diff --git a/src/Cli/func/Common/Constants.cs b/src/Cli/func/Common/Constants.cs index 83f80bee3..1fe86c244 100644 --- a/src/Cli/func/Common/Constants.cs +++ b/src/Cli/func/Common/Constants.cs @@ -96,6 +96,7 @@ internal static class Constants public const string AzureDevSessionsRemoteHostName = "AzureDevSessionsRemoteHostName"; public const string AzureDevSessionsPortSuffixPlaceholder = ""; public const string GitHubReleaseApiUrl = "https://api.github.com/repos/Azure/azure-functions-core-tools/releases/latest"; + public const string FunctionsStackUpgrade = "https://aka.ms/FunctionsStackUpgrade"; // Sample format https://n12abc3t-.asse.devtunnels.ms/ diff --git a/src/Cli/func/Helpers/EolMessages.cs b/src/Cli/func/Helpers/EolMessages.cs index 2cbc7f958..a021d4437 100644 --- a/src/Cli/func/Helpers/EolMessages.cs +++ b/src/Cli/func/Helpers/EolMessages.cs @@ -17,12 +17,21 @@ public static string GetAfterEolCreateMessageDotNet(string stackVersion, DateTim public static string GetEarlyEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") { - return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} will reach EOL on {FormatDate(eol)} and will no longer be supported. {link}"; + return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}"; } public static string GetAfterEolUpdateMessageDotNet(string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") { - return $"Upgrade your app to .NET {nextStackVersion} as .NET {currentStackVersion} has reached EOL on {FormatDate(eol)} and is no longer supported. {link}"; + return $"Upgrade to .NET {nextStackVersion} as .NET {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}"; + } + + public static string GetEarlyEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") + { + return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}"; + } + public static string GetAfterEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") + { + return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}"; } private static string FormatDate(DateTime dateTime) diff --git a/src/Cli/func/Helpers/JavaHelper.cs b/src/Cli/func/Helpers/JavaHelper.cs new file mode 100644 index 000000000..8c29e689f --- /dev/null +++ b/src/Cli/func/Helpers/JavaHelper.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; + +namespace Azure.Functions.Cli.Helpers +{ + public static class JavaHelper + { + public static string GetJavaVersion(string functionAppRoot) + { + string pomXmlPath = Path.Combine(functionAppRoot, "pom.xml"); + if (File.Exists(pomXmlPath)) + { + var xmlDoc = XDocument.Load(pomXmlPath); + var versionElement = xmlDoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "java.version"); + return versionElement?.Value; + } + return null; + } + } +} diff --git a/src/Cli/func/Helpers/NodeJSHelpers.cs b/src/Cli/func/Helpers/NodeJSHelpers.cs index f40e59225..638e45343 100644 --- a/src/Cli/func/Helpers/NodeJSHelpers.cs +++ b/src/Cli/func/Helpers/NodeJSHelpers.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Azure.Functions.Cli.Common; using Colors.Net; +using Newtonsoft.Json.Linq; using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Helpers @@ -58,5 +59,22 @@ public static async Task SetupProject(ProgrammingModel programmingModel, string await FileSystemHelpers.WriteFileIfNotExists("tsconfig.json", await StaticResources.TsConfig); } } + + public static string GetNodeVersion(string functionAppRoot) + { + string packageJsonPath = Path.Combine(functionAppRoot, "package.json"); + if (!File.Exists(packageJsonPath)) + { + return null; + } + var packageJson = JObject.Parse(File.ReadAllText(packageJsonPath)); + // Check if "engines" field specifies Node.js version + string nodeVersion = packageJson["engines"]?["node"]?.ToString(); + if (!string.IsNullOrEmpty(nodeVersion)) + { + return nodeVersion; + } + return null; + } } } diff --git a/src/Cli/func/Helpers/PowerShellHelper.cs b/src/Cli/func/Helpers/PowerShellHelper.cs index e68446eb8..2ff0c81a4 100644 --- a/src/Cli/func/Helpers/PowerShellHelper.cs +++ b/src/Cli/func/Helpers/PowerShellHelper.cs @@ -4,6 +4,7 @@ using Azure.Functions.Cli.Common; using System.Net.Http; using System.Xml; +using System.Text.Json; namespace Azure.Functions.Cli.Helpers { @@ -93,5 +94,30 @@ await RetryHelper.Retry(async () => return latestMajorVersion; } + + public static string GetPowerShellVersion(string functionAppRoot) + { + // Check environment variable (for Azure) + string runtimeVersion = Environment.GetEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME_VERSION"); + if (!string.IsNullOrEmpty(runtimeVersion)) + { + return runtimeVersion; + } + // Fallback: Check local.settings.json (for local development) + string settingsPath = Path.Combine(functionAppRoot, "local.settings.json"); + if (File.Exists(settingsPath)) + { + var jsonText = File.ReadAllText(settingsPath); + using (JsonDocument doc = JsonDocument.Parse(jsonText)) + { + if (doc.RootElement.TryGetProperty("Values", out JsonElement values) && + values.TryGetProperty("FUNCTIONS_WORKER_RUNTIME_VERSION", out JsonElement versionElement)) + { + return versionElement.GetString(); + } + } + } + return null; + } } } diff --git a/src/Cli/func/Helpers/PythonHelpers.cs b/src/Cli/func/Helpers/PythonHelpers.cs index 65595232e..cc364ebfc 100644 --- a/src/Cli/func/Helpers/PythonHelpers.cs +++ b/src/Cli/func/Helpers/PythonHelpers.cs @@ -653,5 +653,11 @@ public static bool HasPySteinFile() { return FileSystemHelpers.FileExists(Path.Combine(Environment.CurrentDirectory, Constants.PySteinFunctionAppPy)); } + + public static async Task GetPythonVersion(string functionAppRoot) + { + var versionInfo = await PythonHelpers.GetVersion(); + return versionInfo?.Version; + } } } diff --git a/src/Cli/func/Helpers/StacksApiHelper.cs b/src/Cli/func/Helpers/StacksApiHelper.cs index 83255e43f..0ffe70280 100644 --- a/src/Cli/func/Helpers/StacksApiHelper.cs +++ b/src/Cli/func/Helpers/StacksApiHelper.cs @@ -58,5 +58,94 @@ public static bool IsInNextSixMonths(this DateTime? date) else return date < DateTime.Now.AddMonths(6); } + + public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, out bool isLTS, Func settingsSelector) + { + if (WorkerRuntime.java.ToString() == workerRuntime) + { + if (runtimeVersion.StartsWith("1.")) + { + runtimeVersion = runtimeVersion.Substring(2); // Removes "1." + } + } + + var languageStack = stacks?.Languages + .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase)); + + var majorVersion = languageStack?.Properties.MajorVersions? + .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase)); + + var minorVersion = majorVersion?.MinorVersions? + .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase)) + ?? majorVersion?.MinorVersions?.LastOrDefault(); + + isLTS = minorVersion?.Value?.Contains("LTS") == true; + + return settingsSelector(minorVersion?.StackSettings); + } + + public static (string nextVersion, string displayText) GetNextRuntimeVersion( + this FunctionsStacks stacks, + string workerRuntime, + string currentRuntimeVersion, + Func> versionSelector, + bool isNumericVersion = false) // Handle Node.js separately as it has integer versions + { + var runtimeStack = stacks?.Languages + .FirstOrDefault(x => x.Name.Equals(workerRuntime, StringComparison.InvariantCultureIgnoreCase)); + if (runtimeStack?.Properties == null) + { + return (null, null); // No matching runtime found + } + string displayName = runtimeStack.Properties.DisplayText; + // Extract and sort supported versions using the provided selector function + var supportedVersions = versionSelector(runtimeStack.Properties)? + .Where(v => !string.IsNullOrEmpty(v)) + .ToList(); + if (supportedVersions == null || supportedVersions.Count == 0) + { + return (null, displayName); // No valid versions found + } + if (isNumericVersion) + { + // Special case for Node.js: Versions are integers + var numericVersions = supportedVersions + .Select(v => int.TryParse(v, out int version) ? version : (int?)null) + .Where(v => v.HasValue) + .OrderByDescending(v => v) + .ToList(); + if (!int.TryParse(currentRuntimeVersion, out int currentMajorVersion)) + { + return (null, displayName); // Invalid current version + } + var nextVersion = numericVersions.FirstOrDefault(v => v > currentMajorVersion); + return ((nextVersion ?? numericVersions.First()).ToString(), displayName); + } + else + { + // Standard versioning (Python, Java, PowerShell) + var parsedVersions = supportedVersions + .Where(v => Version.TryParse(v, out _)) + .Select(v => Version.Parse(v)) + .OrderByDescending(v => v) + .ToList(); + if (!Version.TryParse(currentRuntimeVersion, out Version currentVersion)) + { + return (null, displayName); // Invalid current version + } + var nextVersion = parsedVersions.FirstOrDefault(v => v > currentVersion); + return ((nextVersion ?? parsedVersions.First()).ToString(), displayName); + } + } + + public static bool ExpiresInNextSixMonths(this DateTime? date) + { + if (!date.HasValue) return false; // Null check + + DateTime currentDate = DateTime.UtcNow; + DateTime sixMonthsFromNow = currentDate.AddMonths(6); + + return currentDate <= date.Value && date.Value <= sixMonthsFromNow; + } } } diff --git a/test/Azure.Functions.Cli.Tests/PublishActionTests.cs b/test/Azure.Functions.Cli.Tests/PublishActionTests.cs index 2dcc68477..6fca730a2 100644 --- a/test/Azure.Functions.Cli.Tests/PublishActionTests.cs +++ b/test/Azure.Functions.Cli.Tests/PublishActionTests.cs @@ -6,6 +6,7 @@ using Azure.Functions.Cli.Arm.Models; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Helpers; +using Azure.Functions.Cli.StacksApi; using Xunit; using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction; @@ -187,6 +188,230 @@ public void ValidateFunctionAppPublish_ThrowException_WhenWorkerRuntimeIsNone() Assert.Equal($"Worker runtime cannot be '{WorkerRuntime.None}'. Please set a valid runtime.", ex.Message); } + private FunctionsStacks GetMockFunctionStacks() + { + return new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "python", + Properties = new Properties + { + DisplayText = "Python", + MajorVersions = new List + { + new MajorVersion + { + Value = "3", + MinorVersions = new List + { + new MinorVersion + { + Value = "3.8", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.8" + } + } + }, + new MinorVersion + { + Value = "3.12", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.12" + } + } + } + } + } + } + } + }, + new Language + { + Name = "node", +Properties = new Properties +{ + DisplayText = "Node.js", + MajorVersions = new List + { + new MajorVersion + { + Value = "14", + MinorVersions = new List + { + new MinorVersion + { + Value = "14.17", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.17" + } + } + }, + new MinorVersion + { + Value = "14.20 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.20 LTS" + } + } + } + } + }, + new MajorVersion + { + Value = "22", + MinorVersions = new List + { + new MinorVersion + { + Value = "22.0", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0" + } + } + }, + new MinorVersion + { + Value = "22.0 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0 LTS" + } + } + } + } + } + } +} + }, + new Language +{ + Name = "powershell", + Properties = new Properties + { + DisplayText = "PowerShell", + MajorVersions = new List + { + new MajorVersion + { + Value = "7", + MinorVersions = new List + { + new MinorVersion + { + Value = "7", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7" + } + } + }, + new MinorVersion + { + Value = "7.2 LTS", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7.2 LTS" + } + } + } + } + } + } + } +} + } + }; + } + + [Theory] + [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22 + public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12 + public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value)); + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("node", "14.17", true)] // Test for a known valid version + [InlineData("node", "14.20 LTS", true)] // Test for an LTS version + public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + bool isLTS; + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.WindowsRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings + public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + bool isLTS; + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.LinuxRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings + [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings + public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + bool isLTS; + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.WindowsRuntimeSettings); + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + private class TestAzureHelperService : AzureHelperService { public Dictionary UpdatedSettings { get; private set; } From 3b74ef2387741e3f3bd39803d7f5787e4a34b114 Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Mon, 14 Apr 2025 16:15:38 +0530 Subject: [PATCH 2/6] Removed unused code and added test cases --- .../AzureActions/PublishFunctionAppAction.cs | 28 +++++---- src/Cli/func/Helpers/StacksApiHelper.cs | 4 +- .../PublishActionTests.cs | 57 ++++++++++++++++--- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs index 8a0a1ee46..d2a8864fb 100644 --- a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs @@ -204,31 +204,29 @@ public override async Task RunAsync() { string workerRuntimeStr = Convert.ToString(workerRuntime); string runtimeVersion = GetWorkerRuntimeVersion(workerRuntime, functionAppRoot); + if (!string.IsNullOrEmpty(runtimeVersion)) { // Get runtime stacks var stacks = await AzureHelper.GetFunctionsStacks(AccessToken, ManagementURL); DateTime currentDate = DateTime.Now; - if (workerRuntime == WorkerRuntime.python) + + object runtimeSettings = (workerRuntime == WorkerRuntime.python) ? stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.LinuxRuntimeSettings) : stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.WindowsRuntimeSettings); + + DateTime? eolDate = runtimeSettings switch { - var linuxRuntimeSettings = stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, out bool isLinuxLTS, s => s.LinuxRuntimeSettings); - DateTime eolDate = linuxRuntimeSettings.EndOfLifeDate.Value; - DateTime warningThresholdDate = eolDate.AddMonths(-6); - if (currentDate > eolDate || currentDate >= warningThresholdDate) - { - //Show EOL warning message - ShowEolMessageForOtherStack(stacks, linuxRuntimeSettings.EndOfLifeDate.Value, workerRuntimeStr, runtimeVersion); - } - } - else + LinuxRuntimeSettings linux => linux.EndOfLifeDate, + WindowsRuntimeSettings windows => windows.EndOfLifeDate, + _ => null + }; + + if (eolDate.HasValue) { - var runtimeSettings = stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, out bool isWindowsLTS, s => s.WindowsRuntimeSettings); - DateTime eolDate = runtimeSettings.EndOfLifeDate.Value; - DateTime warningThresholdDate = eolDate.AddMonths(-6); + DateTime warningThresholdDate = eolDate.Value.AddMonths(-6); if (currentDate > eolDate || currentDate >= warningThresholdDate) { //Show EOL warning message - ShowEolMessageForOtherStack(stacks, runtimeSettings.EndOfLifeDate.Value, workerRuntimeStr, runtimeVersion); + ShowEolMessageForOtherStack(stacks, eolDate.Value, workerRuntimeStr, runtimeVersion); } } } diff --git a/src/Cli/func/Helpers/StacksApiHelper.cs b/src/Cli/func/Helpers/StacksApiHelper.cs index 0ffe70280..4b72f20aa 100644 --- a/src/Cli/func/Helpers/StacksApiHelper.cs +++ b/src/Cli/func/Helpers/StacksApiHelper.cs @@ -59,7 +59,7 @@ public static bool IsInNextSixMonths(this DateTime? date) return date < DateTime.Now.AddMonths(6); } - public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, out bool isLTS, Func settingsSelector) + public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, Func settingsSelector) { if (WorkerRuntime.java.ToString() == workerRuntime) { @@ -79,8 +79,6 @@ public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string w .FirstOrDefault(mv => runtimeVersion.StartsWith(mv.Value, StringComparison.InvariantCultureIgnoreCase)) ?? majorVersion?.MinorVersions?.LastOrDefault(); - isLTS = minorVersion?.Value?.Contains("LTS") == true; - return settingsSelector(minorVersion?.StackSettings); } diff --git a/test/Azure.Functions.Cli.Tests/PublishActionTests.cs b/test/Azure.Functions.Cli.Tests/PublishActionTests.cs index 6fca730a2..e1e5dbe88 100644 --- a/test/Azure.Functions.Cli.Tests/PublishActionTests.cs +++ b/test/Azure.Functions.Cli.Tests/PublishActionTests.cs @@ -172,8 +172,8 @@ public void NormalizeWorkerRuntime_InvalidInput(string inputString) Assert.StartsWith($"Worker runtime cannot be null or empty.", exception.Message); Assert.Equal("workerRuntime", exception.ParamName); } - else - { + else + { var exception = Assert.Throws(() => WorkerRuntimeLanguageHelper.NormalizeWorkerRuntime(inputString)); Assert.Equal($"Worker runtime '{inputString}' is not a valid option. Options are {WorkerRuntimeLanguageHelper.AvailableWorkersRuntimeString}", exception.Message); } @@ -378,9 +378,8 @@ public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, { // Arrange var stacks = GetMockFunctionStacks(); - bool isLTS; // Act - var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.WindowsRuntimeSettings); + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); // Assert Assert.Equal(expectedNotNull, settings != null); } @@ -391,9 +390,8 @@ public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime { // Arrange var stacks = GetMockFunctionStacks(); - bool isLTS; // Act - var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.LinuxRuntimeSettings); + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings); // Assert Assert.Equal(expectedNotNull, settings != null); } @@ -405,9 +403,8 @@ public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string run { // Arrange var stacks = GetMockFunctionStacks(); - bool isLTS; // Act - var settings = stacks.GetOtherRuntimeSettings(runtime, version, out isLTS, s => s.WindowsRuntimeSettings); + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); // Assert Assert.Equal(expectedNotNull, settings != null); } @@ -427,5 +424,49 @@ public override Task> UpdateWebSettings(Site function return Task.FromResult(new HttpResult(string.Empty)); } } + + [Theory] + [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself + [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself + public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + var selector = runtime == "node" + ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value)) + : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value)); + bool isNumeric = runtime == "node"; + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric); + // Assert + Assert.Equal(expectedVersion, nextVersion); + } + + [Fact] + public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable() + { + // Arrange + var stacks = new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "java", + Properties = new Properties + { + DisplayText = "Java", + MajorVersions = new List() // No versions + } + } + } + }; + // Act + var (nextVersion, displayName) = stacks.GetNextRuntimeVersion( + "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + // Assert + Assert.Null(nextVersion); + Assert.Equal("Java", displayName); + } } } From 894a4539cd533d52de39f14945f3eeb00ccca440 Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Tue, 22 Apr 2025 15:47:59 +0530 Subject: [PATCH 3/6] Update appropriate culture to display user. --- src/Cli/func/Helpers/EolMessages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli/func/Helpers/EolMessages.cs b/src/Cli/func/Helpers/EolMessages.cs index a021d4437..ccffb7932 100644 --- a/src/Cli/func/Helpers/EolMessages.cs +++ b/src/Cli/func/Helpers/EolMessages.cs @@ -36,7 +36,7 @@ public static string GetAfterEolUpdateMessage(string displayName, string current private static string FormatDate(DateTime dateTime) { - return dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return dateTime.ToString(CultureInfo.CurrentCulture); } } } From 8c5394963b4ce718fb3f0d20129e5c04298cf96e Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Tue, 1 Jul 2025 15:54:05 +0530 Subject: [PATCH 4/6] Fixed warning errors --- .../AzureActions/PublishFunctionAppAction.cs | 82 ++++++++++--------- src/Cli/func/Helpers/EolMessages.cs | 1 + src/Cli/func/Helpers/JavaHelper.cs | 6 +- src/Cli/func/Helpers/NodeJSHelpers.cs | 3 + src/Cli/func/Helpers/PowerShellHelper.cs | 6 +- src/Cli/func/Helpers/StacksApiHelper.cs | 14 +++- 6 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs index 2a60d25f8..7b47a6037 100644 --- a/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs +++ b/src/Cli/func/Actions/AzureActions/PublishFunctionAppAction.cs @@ -210,7 +210,7 @@ public override async Task RunAsync() } // Show warning message for other worker runtimes (Node, Python, Powershell, Java) - if (workerRuntime != WorkerRuntime.dotnet && workerRuntime != WorkerRuntime.dotnetIsolated) + if (workerRuntime != WorkerRuntime.Dotnet && workerRuntime != WorkerRuntime.DotnetIsolated) { string workerRuntimeStr = Convert.ToString(workerRuntime); string runtimeVersion = GetWorkerRuntimeVersion(workerRuntime, functionAppRoot); @@ -221,7 +221,7 @@ public override async Task RunAsync() var stacks = await AzureHelper.GetFunctionsStacks(AccessToken, ManagementURL); DateTime currentDate = DateTime.Now; - object runtimeSettings = (workerRuntime == WorkerRuntime.python) ? stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.LinuxRuntimeSettings) : stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.WindowsRuntimeSettings); + object runtimeSettings = (workerRuntime == WorkerRuntime.Python) ? stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.LinuxRuntimeSettings) : stacks.GetOtherRuntimeSettings(workerRuntimeStr, runtimeVersion, s => s.WindowsRuntimeSettings); DateTime? eolDate = runtimeSettings switch { @@ -235,7 +235,7 @@ public override async Task RunAsync() DateTime warningThresholdDate = eolDate.Value.AddMonths(-6); if (currentDate > eolDate || currentDate >= warningThresholdDate) { - //Show EOL warning message + // Show EOL warning message ShowEolMessageForOtherStack(stacks, eolDate.Value, workerRuntimeStr, runtimeVersion); } } @@ -1506,28 +1506,6 @@ private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings curre } } - // For testing - internal class AzureHelperService - { - private readonly string _accessToken; - private readonly string _managementUrl; - - public AzureHelperService(string accessToken, string managementUrl) - { - _accessToken = accessToken; - _managementUrl = managementUrl; - } - - public virtual Task> UpdateWebSettings(Site functionApp, Dictionary updatedSettings) => - AzureHelper.UpdateWebSettings(functionApp, updatedSettings, _accessToken, _managementUrl); - - public virtual Task UpdateFlexRuntime(Site functionApp, string runtimeName, string runtimeVersion) => - AzureHelper.UpdateFlexRuntime(functionApp, runtimeName, runtimeVersion, _accessToken, _managementUrl); - - public virtual Task GetFlexFunctionsStacks(string runtime, string region) => - AzureHelper.GetFlexFunctionsStacks(_accessToken, _managementUrl, runtime, region); - } - /// /// Determines the version of the specified worker runtime. /// @@ -1535,13 +1513,13 @@ private string GetWorkerRuntimeVersion(WorkerRuntime workerRuntime, string funct { switch (workerRuntime) { - case WorkerRuntime.node: + case WorkerRuntime.Node: return NodeJSHelpers.GetNodeVersion(functionAppRoot); - case WorkerRuntime.python: + case WorkerRuntime.Python: return PythonHelpers.GetPythonVersion(functionAppRoot).GetAwaiter().GetResult(); - case WorkerRuntime.powershell: + case WorkerRuntime.Powershell: return PowerShellHelper.GetPowerShellVersion(functionAppRoot); - case WorkerRuntime.java: + case WorkerRuntime.Java: return JavaHelper.GetJavaVersion(functionAppRoot); default: return null; @@ -1555,15 +1533,23 @@ private void ShowEolMessageForOtherStack(FunctionsStacks stacks, DateTime eolDat string nextVersion, displayName, warningMessage = string.Empty; (nextVersion, displayName) = workerRuntime switch { - var wr when wr.Equals(WorkerRuntime.python.ToString(), StringComparison.OrdinalIgnoreCase) || wr.Equals(WorkerRuntime.powershell.ToString(), StringComparison.OrdinalIgnoreCase) || wr.Equals(WorkerRuntime.java.ToString(), StringComparison.OrdinalIgnoreCase) - => stacks.GetNextRuntimeVersion(workerRuntime, runtimeVersion, - properties => properties.MajorVersions - .SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value), isNumericVersion: false), - var wr when wr.Equals(WorkerRuntime.node.ToString(), StringComparison.OrdinalIgnoreCase) - => stacks.GetNextRuntimeVersion(workerRuntime, runtimeVersion, - properties => properties.MajorVersions + var wr when wr.Equals(WorkerRuntime.Python.ToString(), StringComparison.OrdinalIgnoreCase) + || wr.Equals(WorkerRuntime.Powershell.ToString(), StringComparison.OrdinalIgnoreCase) + || wr.Equals(WorkerRuntime.Java.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion( + workerRuntime, + runtimeVersion, + properties => properties.MajorVersions + .SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value), + isNumericVersion: false), + + var wr when wr.Equals(WorkerRuntime.Node.ToString(), StringComparison.OrdinalIgnoreCase) + => stacks.GetNextRuntimeVersion( + workerRuntime, + runtimeVersion, + properties => properties.MajorVersions .Select(mv => mv.Value), - isNumericVersion: true), + isNumericVersion: true), _ => (null, workerRuntime) // Default case: No next version available }; if (StacksApiHelper.ExpiresInNextSixMonths(eolDate)) @@ -1582,5 +1568,27 @@ var wr when wr.Equals(WorkerRuntime.node.ToString(), StringComparison.OrdinalIgn // ignore. Failure to show the EOL message should not fail the deployment. } } + + // For testing + internal class AzureHelperService + { + private readonly string _accessToken; + private readonly string _managementUrl; + + public AzureHelperService(string accessToken, string managementUrl) + { + _accessToken = accessToken; + _managementUrl = managementUrl; + } + + public virtual Task> UpdateWebSettings(Site functionApp, Dictionary updatedSettings) => + AzureHelper.UpdateWebSettings(functionApp, updatedSettings, _accessToken, _managementUrl); + + public virtual Task UpdateFlexRuntime(Site functionApp, string runtimeName, string runtimeVersion) => + AzureHelper.UpdateFlexRuntime(functionApp, runtimeName, runtimeVersion, _accessToken, _managementUrl); + + public virtual Task GetFlexFunctionsStacks(string runtime, string region) => + AzureHelper.GetFlexFunctionsStacks(_accessToken, _managementUrl, runtime, region); + } } } diff --git a/src/Cli/func/Helpers/EolMessages.cs b/src/Cli/func/Helpers/EolMessages.cs index 0640c1cc9..8cff18af1 100644 --- a/src/Cli/func/Helpers/EolMessages.cs +++ b/src/Cli/func/Helpers/EolMessages.cs @@ -31,6 +31,7 @@ public static string GetEarlyEolUpdateMessage(string displayName, string current { return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} will reach end-of-life on {FormatDate(eol)} and will no longer be supported. Learn more: {link}"; } + public static string GetAfterEolUpdateMessage(string displayName, string currentStackVersion, string nextStackVersion, DateTime eol, string link = "") { return $"Upgrade to {displayName} {nextStackVersion} as {displayName} {currentStackVersion} has reached end-of-life on {FormatDate(eol)} and is no longer supported. Learn more: {link}"; diff --git a/src/Cli/func/Helpers/JavaHelper.cs b/src/Cli/func/Helpers/JavaHelper.cs index 8c29e689f..9c0752c5f 100644 --- a/src/Cli/func/Helpers/JavaHelper.cs +++ b/src/Cli/func/Helpers/JavaHelper.cs @@ -1,4 +1,7 @@ -using System.Xml.Linq; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Xml.Linq; namespace Azure.Functions.Cli.Helpers { @@ -13,6 +16,7 @@ public static string GetJavaVersion(string functionAppRoot) var versionElement = xmlDoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "java.version"); return versionElement?.Value; } + return null; } } diff --git a/src/Cli/func/Helpers/NodeJSHelpers.cs b/src/Cli/func/Helpers/NodeJSHelpers.cs index a738b733c..587e4231e 100644 --- a/src/Cli/func/Helpers/NodeJSHelpers.cs +++ b/src/Cli/func/Helpers/NodeJSHelpers.cs @@ -70,13 +70,16 @@ public static string GetNodeVersion(string functionAppRoot) { return null; } + var packageJson = JObject.Parse(File.ReadAllText(packageJsonPath)); + // Check if "engines" field specifies Node.js version string nodeVersion = packageJson["engines"]?["node"]?.ToString(); if (!string.IsNullOrEmpty(nodeVersion)) { return nodeVersion; } + return null; } } diff --git a/src/Cli/func/Helpers/PowerShellHelper.cs b/src/Cli/func/Helpers/PowerShellHelper.cs index cc932af59..97e7d1615 100644 --- a/src/Cli/func/Helpers/PowerShellHelper.cs +++ b/src/Cli/func/Helpers/PowerShellHelper.cs @@ -1,11 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Text.Json; using System.Xml; using Azure.Functions.Cli.Common; -using System.Net.Http; -using System.Xml; -using System.Text.Json; namespace Azure.Functions.Cli.Helpers { @@ -107,6 +105,7 @@ public static string GetPowerShellVersion(string functionAppRoot) { return runtimeVersion; } + // Fallback: Check local.settings.json (for local development) string settingsPath = Path.Combine(functionAppRoot, "local.settings.json"); if (File.Exists(settingsPath)) @@ -121,6 +120,7 @@ public static string GetPowerShellVersion(string functionAppRoot) } } } + return null; } } diff --git a/src/Cli/func/Helpers/StacksApiHelper.cs b/src/Cli/func/Helpers/StacksApiHelper.cs index 676fd6f5f..c699704b1 100644 --- a/src/Cli/func/Helpers/StacksApiHelper.cs +++ b/src/Cli/func/Helpers/StacksApiHelper.cs @@ -63,7 +63,7 @@ public static bool IsInNextSixMonths(this DateTime? date) public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string workerRuntime, string runtimeVersion, Func settingsSelector) { - if (WorkerRuntime.java.ToString() == workerRuntime) + if (WorkerRuntime.Java.ToString() == workerRuntime) { if (runtimeVersion.StartsWith("1.")) { @@ -84,7 +84,7 @@ public static T GetOtherRuntimeSettings(this FunctionsStacks stacks, string w return settingsSelector(minorVersion?.StackSettings); } - public static (string nextVersion, string displayText) GetNextRuntimeVersion( + public static (string NextVersion, string DisplayText) GetNextRuntimeVersion( this FunctionsStacks stacks, string workerRuntime, string currentRuntimeVersion, @@ -97,7 +97,9 @@ public static (string nextVersion, string displayText) GetNextRuntimeVersion( { return (null, null); // No matching runtime found } + string displayName = runtimeStack.Properties.DisplayText; + // Extract and sort supported versions using the provided selector function var supportedVersions = versionSelector(runtimeStack.Properties)? .Where(v => !string.IsNullOrEmpty(v)) @@ -106,6 +108,7 @@ public static (string nextVersion, string displayText) GetNextRuntimeVersion( { return (null, displayName); // No valid versions found } + if (isNumericVersion) { // Special case for Node.js: Versions are integers @@ -118,6 +121,7 @@ public static (string nextVersion, string displayText) GetNextRuntimeVersion( { return (null, displayName); // Invalid current version } + var nextVersion = numericVersions.FirstOrDefault(v => v > currentMajorVersion); return ((nextVersion ?? numericVersions.First()).ToString(), displayName); } @@ -133,6 +137,7 @@ public static (string nextVersion, string displayText) GetNextRuntimeVersion( { return (null, displayName); // Invalid current version } + var nextVersion = parsedVersions.FirstOrDefault(v => v > currentVersion); return ((nextVersion ?? parsedVersions.First()).ToString(), displayName); } @@ -140,7 +145,10 @@ public static (string nextVersion, string displayText) GetNextRuntimeVersion( public static bool ExpiresInNextSixMonths(this DateTime? date) { - if (!date.HasValue) return false; // Null check + if (!date.HasValue) + { + return false; // Null check + } DateTime currentDate = DateTime.UtcNow; DateTime sixMonthsFromNow = currentDate.AddMonths(6); From 773653416f447427cdb76f78a7ddb2eacc8fa10b Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Wed, 2 Jul 2025 15:22:29 +0530 Subject: [PATCH 5/6] Added unit test cases for publish warning message --- .../ActionsTests/PublishActionTests.cs | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs diff --git a/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs new file mode 100644 index 000000000..fdc38a48a --- /dev/null +++ b/test/Cli/Func.Unit.Tests/ActionsTests/PublishActionTests.cs @@ -0,0 +1,316 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Functions.Cli.Actions.AzureActions; +using Azure.Functions.Cli.Arm.Models; +using Azure.Functions.Cli.Common; +using Azure.Functions.Cli.Helpers; +using Azure.Functions.Cli.StacksApi; +using Xunit; +using static Azure.Functions.Cli.Actions.AzureActions.PublishFunctionAppAction; + +namespace Azure.Functions.Cli.Unit.Test.ActionsTests +{ + public class PublishActionTests + { + private FunctionsStacks GetMockFunctionStacks() + { + return new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "python", + Properties = new Properties + { + DisplayText = "Python", + MajorVersions = new List + { + new MajorVersion + { + Value = "3", + MinorVersions = new List + { + new MinorVersion + { + Value = "3.8", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.8" + } + } + }, + new MinorVersion + { + Value = "3.12", + StackSettings = new StackSettings + { + LinuxRuntimeSettings = new LinuxRuntimeSettings + { + RuntimeVersion = "Python|3.12" + } + } + } + } + } + } + } + }, + new Language + { + Name = "node", + Properties = new Properties +{ + DisplayText = "Node.js", + MajorVersions = new List + { + new MajorVersion + { + Value = "14", + MinorVersions = new List + { + new MinorVersion + { + Value = "14.17", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.17" + } + } + }, + new MinorVersion + { + Value = "14.20 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|14.20 LTS" + } + } + } + } + }, + new MajorVersion + { + Value = "22", + MinorVersions = new List + { + new MinorVersion + { + Value = "22.0", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0" + } + } + }, + new MinorVersion + { + Value = "22.0 LTS", // Ensure an LTS version exists + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "Node|22.0 LTS" + } + } + } + } + } + } +} + }, + new Language +{ + Name = "powershell", + Properties = new Properties + { + DisplayText = "PowerShell", + MajorVersions = new List + { + new MajorVersion + { + Value = "7", + MinorVersions = new List + { + new MinorVersion + { + Value = "7", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7" + } + } + }, + new MinorVersion + { + Value = "7.2 LTS", + StackSettings = new StackSettings + { + WindowsRuntimeSettings = new WindowsRuntimeSettings + { + RuntimeVersion = "PowerShell|7.2 LTS" + } + } + } + } + } + } + } +} + } + }; + } + + [Theory] + [InlineData("node", "14", "22")] // Node.js 14 should return next supported 22 + public void GetNextRuntimeNodeVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("python", "3.8", "3.12")] // Python 3.8 should return next supported 3.12 + public void GetNextRuntimePythonVersion_ShouldReturnCorrectVersion(string runtime, string currentVersion, string expectedNextVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, p => p.MajorVersions.SelectMany(mv => mv.MinorVersions, (major, minor) => minor.Value)); + + // Assert + Assert.Equal(expectedNextVersion, nextVersion); + } + + [Theory] + [InlineData("node", "14.17", true)] // Test for a known valid version + [InlineData("node", "14.20 LTS", true)] // Test for an LTS version + public void GetRuntimeSettingsForNode_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("python", "3.8", true)] // Python 3.8 should return runtime settings + public void GetRuntimeSettingsForPython_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.LinuxRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("powershell", "7", true)] // PowerShell 7 should return runtime settings + [InlineData("powershell", "7.2 LTS", true)] // PowerShell 7.2 LTS should return runtime settings + public void GetRuntimeSettingsForPowerShell_ShouldReturnValidSettings(string runtime, string version, bool expectedNotNull) + { + // Arrange + var stacks = GetMockFunctionStacks(); + + // Act + var settings = stacks.GetOtherRuntimeSettings(runtime, version, s => s.WindowsRuntimeSettings); + + // Assert + Assert.Equal(expectedNotNull, settings != null); + } + + [Theory] + [InlineData("node", "22", "22")] // Node.js 22 is highest → should return itself + [InlineData("python", "3.12", "3.12")] // Python 3.12 is highest → should return itself + public void GetNextRuntimeVersion_ShouldReturnCurrentIfNoNewerExists(string runtime, string currentVersion, string expectedVersion) + { + // Arrange + var stacks = GetMockFunctionStacks(); + var selector = runtime == "node" + ? (Func>)(p => p.MajorVersions.Select(mv => mv.Value)) + : p => p.MajorVersions.SelectMany(mv => mv.MinorVersions.Select(minor => minor.Value)); + bool isNumeric = runtime == "node"; + + // Act + var (nextVersion, _) = stacks.GetNextRuntimeVersion(runtime, currentVersion, selector, isNumeric); + + // Assert + Assert.Equal(expectedVersion, nextVersion); + } + + [Fact] + public void GetNextRuntimeVersion_ShouldReturnNull_WhenNoVersionsAvailable() + { + // Arrange + var stacks = new FunctionsStacks + { + Languages = new List + { + new Language + { + Name = "java", + Properties = new Properties + { + DisplayText = "Java", + MajorVersions = new List() // No versions + } + } + } + }; + + // Act + var (nextVersion, displayName) = stacks.GetNextRuntimeVersion( + "java", "11", p => p.MajorVersions.Select(mv => mv.Value), isNumericVersion: true); + + // Assert + Assert.Null(nextVersion); + Assert.Equal("Java", displayName); + } + + private class TestAzureHelperService : AzureHelperService + { + public TestAzureHelperService() + : base(null, null) + { + UpdatedSettings = new Dictionary(); + } + + public Dictionary UpdatedSettings { get; private set; } + + public override Task> UpdateWebSettings(Site functionApp, Dictionary updatedSettings) + { + UpdatedSettings = updatedSettings; + return Task.FromResult(new HttpResult(string.Empty)); + } + } + } +} From 9dda6d0d8bb4f5fba95eb0b64183c87e77c31e21 Mon Sep 17 00:00:00 2001 From: v-umangsr Date: Tue, 26 Aug 2025 22:39:27 +0530 Subject: [PATCH 6/6] chore: re-run CI