Skip to content

Commit 6b7ed31

Browse files
committed
fixing bugs and adding relevant e2e tests
1 parent 0a7f737 commit 6b7ed31

28 files changed

+661
-198
lines changed

eng/ci/templates/jobs/test-e2e-linux.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_linux_x64:
2323
languageWorker: 'Python'
2424
runtime: 'linux-x64'
25+
custom_linux_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'linux-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1

eng/ci/templates/jobs/test-e2e-osx.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_osx_x64:
2323
languageWorker: 'Python'
2424
runtime: 'osx-x64'
25+
custom_osx_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'osx-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1

eng/ci/templates/jobs/test-e2e-windows.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_win_x64:
2323
languageWorker: 'Python'
2424
runtime: 'win-x64'
25+
custom_win_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'win-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1 -NoWait

eng/ci/templates/steps/run-e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ steps:
3030
Write-Host "##vso[task.setvariable variable=DURABLE_FUNCTION_PATH]$(Build.SourcesDirectory)/test/Azure.Functions.Cli.Tests/Resources/DurableTestFolder"
3131
Write-Host "##vso[task.setvariable variable=INPROC_RUN_SETTINGS]$(Build.SourcesDirectory)/test/Cli/Func.E2ETests/.runsettings/start_tests/ci_pipeline/dotnet_inproc.runsettings"
3232
Write-Host "##vso[task.setvariable variable=TEST_PROJECT_PATH]$(Build.SourcesDirectory)/test/TestFunctionApps"
33+
Write-Host "##vso[task.setvariable variable=FUNCTIONS_PYTHON_DOCKER_IMAGE]mcr.microsoft.com/azure-functions/python:4-python3.11-buildenv"
3334
displayName: 'Set environment variables for E2E tests'
3435

3536

src/Cli/func/Actions/HelpAction.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,21 +256,38 @@ private void DisplayActionsHelp(IEnumerable<ActionType> actions)
256256

257257
private void DisplaySubCommandHelp(ActionType subCommand)
258258
{
259+
// Ensure subCommand is valid
260+
if (subCommand == null)
261+
{
262+
return;
263+
}
264+
259265
// Extract the runtime name from the full command name
260266
// E.g., "pack dotnet" -> "Dotnet"
261-
var fullCommandName = subCommand.Names.First();
262-
var parts = fullCommandName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
263-
var runtimeName = parts.Length > 1 ?
264-
char.ToUpper(parts[1][0]) + parts[1].Substring(1).ToLower() :
265-
fullCommandName;
267+
var fullCommandName = subCommand.Names?.FirstOrDefault();
266268

267-
var description = subCommand.Type.GetCustomAttributes<ActionAttribute>()?.FirstOrDefault()?.HelpText;
269+
string runtimeName = null;
270+
if (!string.IsNullOrWhiteSpace(fullCommandName))
271+
{
272+
var parts = fullCommandName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
273+
runtimeName = parts.Length > 1 && !string.IsNullOrEmpty(parts[1])
274+
? char.ToUpper(parts[1][0]) + parts[1].Substring(1).ToLower()
275+
: fullCommandName;
276+
}
277+
278+
// Fall back to a safe default if we couldn't determine a runtime name
279+
runtimeName ??= subCommand.Type?.Name ?? "subcommand";
280+
281+
var description = subCommand.Type?.GetCustomAttributes<ActionAttribute>()?.FirstOrDefault()?.HelpText;
268282

269283
// Display indented subcommand header
270284
ColoredConsole.WriteLine($" {runtimeName.DarkCyan()} {description}");
271285

272286
// Display subcommand switches with extra indentation
273-
DisplaySwitches(subCommand);
287+
if (subCommand.Type != null)
288+
{
289+
DisplaySwitches(subCommand);
290+
}
274291
}
275292

276293
private void DisplaySwitches(ActionType actionType)

src/Cli/func/Actions/LocalActions/CreateFunctionAction.cs

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ internal class CreateFunctionAction : BaseAction
2828
private readonly IUserInputHandler _userInputHandler;
2929
private readonly InitAction _initAction;
3030
private readonly ITemplatesManager _templatesManager;
31-
private IEnumerable<Template> _templates;
32-
private IEnumerable<NewTemplate> _newTemplates;
33-
private IEnumerable<UserPrompt> _userPrompts;
31+
private readonly Lazy<IEnumerable<Template>> _templates;
32+
private readonly Lazy<IEnumerable<NewTemplate>> _newTemplates;
33+
private readonly Lazy<IEnumerable<UserPrompt>> _userPrompts;
3434
private WorkerRuntime _workerRuntime;
3535

3636
public CreateFunctionAction(ITemplatesManager templatesManager, ISecretsManager secretsManager, IContextHelpManager contextHelpManager)
@@ -40,6 +40,9 @@ public CreateFunctionAction(ITemplatesManager templatesManager, ISecretsManager
4040
_contextHelpManager = contextHelpManager;
4141
_initAction = new InitAction(_templatesManager, _secretsManager);
4242
_userInputHandler = new UserInputHandler(_templatesManager);
43+
_templates = new Lazy<IEnumerable<Template>>(() => { return _templatesManager.Templates.Result; });
44+
_newTemplates = new Lazy<IEnumerable<NewTemplate>>(() => { return _templatesManager.NewTemplates.Result; });
45+
_userPrompts = new Lazy<IEnumerable<UserPrompt>>(() => { return _templatesManager.UserPrompts.Result; });
4346
}
4447

4548
public string Language { get; set; }
@@ -110,18 +113,8 @@ public override async Task RunAsync()
110113
return;
111114
}
112115

113-
// Ensure that the _templates are loaded before we proceed
114-
_templates = await _templatesManager.Templates;
115-
116116
await UpdateLanguageAndRuntime();
117117

118-
// Depends on UpdateLanguageAndRuntime to set 'Language'
119-
if (IsNewPythonProgrammingModel())
120-
{
121-
_newTemplates = await _templatesManager.NewTemplates;
122-
_userPrompts = await _templatesManager.UserPrompts;
123-
}
124-
125118
if (WorkerRuntimeLanguageHelper.IsDotnet(_workerRuntime) && !Csx)
126119
{
127120
if (string.IsNullOrWhiteSpace(TemplateName))
@@ -155,7 +148,7 @@ public override async Task RunAsync()
155148
FileName = "function_app.py";
156149
}
157150

158-
var userPrompt = _userPrompts.First(x => string.Equals(x.Id, "app-selectedFileName", StringComparison.OrdinalIgnoreCase));
151+
var userPrompt = _userPrompts.Value.First(x => string.Equals(x.Id, "app-selectedFileName", StringComparison.OrdinalIgnoreCase));
159152
while (!_userInputHandler.ValidateResponse(userPrompt, FileName))
160153
{
161154
_userInputHandler.PrintInputLabel(userPrompt, PySteinFunctionAppPy);
@@ -192,12 +185,7 @@ public override async Task RunAsync()
192185
providedInputs[GetFileNameParamId] = FileName;
193186
}
194187

195-
var template = _newTemplates.FirstOrDefault(t => string.Equals(t.Name, TemplateName, StringComparison.CurrentCultureIgnoreCase) && string.Equals(t.Language, Language, StringComparison.CurrentCultureIgnoreCase));
196-
197-
if (template is null)
198-
{
199-
throw new CliException($"Can't find template \"{TemplateName}\" in \"{Language}\"");
200-
}
188+
var template = _newTemplates.Value.FirstOrDefault(t => string.Equals(t.Name, TemplateName, StringComparison.CurrentCultureIgnoreCase) && string.Equals(t.Language, Language, StringComparison.CurrentCultureIgnoreCase));
201189

202190
var templateJob = template.Jobs.Single(x => x.Type.Equals(jobName, StringComparison.OrdinalIgnoreCase));
203191

@@ -316,19 +304,18 @@ public async Task UpdateLanguageAndRuntime()
316304
if (_workerRuntime == WorkerRuntime.None)
317305
{
318306
SelectionMenuHelper.DisplaySelectionWizardPrompt("language");
319-
Language = SelectionMenuHelper.DisplaySelectionWizard(_templates.Select(t => t.Metadata.Language).Where(l => !l.Equals("python", StringComparison.OrdinalIgnoreCase)).Distinct());
307+
Language = SelectionMenuHelper.DisplaySelectionWizard(_templates.Value.Select(t => t.Metadata.Language).Where(l => !l.Equals("python", StringComparison.OrdinalIgnoreCase)).Distinct());
320308
_workerRuntime = WorkerRuntimeLanguageHelper.SetWorkerRuntime(_secretsManager, Language);
321309
}
322310
else if (!WorkerRuntimeLanguageHelper.IsDotnet(_workerRuntime) || Csx)
323311
{
324312
var languages = WorkerRuntimeLanguageHelper.LanguagesForWorker(_workerRuntime);
325-
var displayList = _templates?
313+
var displayList = _templates.Value
326314
.Select(t => t.Metadata.Language)
327315
.Where(l => languages.Contains(l, StringComparer.OrdinalIgnoreCase))
328316
.Distinct(StringComparer.OrdinalIgnoreCase)
329317
.ToArray();
330-
331-
if (displayList?.Length == 1)
318+
if (displayList.Length == 1)
332319
{
333320
Language = displayList.First();
334321
}
@@ -359,15 +346,15 @@ private IEnumerable<Template> GetLanguageTemplates(string templateLanguage, bool
359346
if (IsNewNodeJsProgrammingModel(_workerRuntime) ||
360347
(forNewModelHelp && (Languages.TypeScript.EqualsIgnoreCase(templateLanguage) || Languages.JavaScript.EqualsIgnoreCase(templateLanguage))))
361348
{
362-
return _templates.Where(t => t.Id.EndsWith("-4.x") && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
349+
return _templates.Value.Where(t => t.Id.EndsWith("-4.x") && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
363350
}
364351
else if (_workerRuntime == WorkerRuntime.Node)
365352
{
366353
// Ensuring that we only show v3 templates for node when the user has not opted into the new model
367-
return _templates.Where(t => !t.Id.EndsWith("-4.x") && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
354+
return _templates.Value.Where(t => !t.Id.EndsWith("-4.x") && t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
368355
}
369356

370-
return _templates.Where(t => t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
357+
return _templates.Value.Where(t => t.Metadata.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
371358
}
372359

373360
private IEnumerable<string> GetTriggerNamesFromNewTemplates(string templateLanguage, bool forNewModelHelp = false)
@@ -379,7 +366,7 @@ private IEnumerable<NewTemplate> GetNewTemplates(string templateLanguage, bool f
379366
{
380367
if (IsNewPythonProgrammingModel() || (Languages.Python.EqualsIgnoreCase(templateLanguage) && forNewModelHelp))
381368
{
382-
return _newTemplates.Where(t => t.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
369+
return _newTemplates.Value.Where(t => t.Language.Equals(templateLanguage, StringComparison.OrdinalIgnoreCase));
383370
}
384371

385372
throw new CliException("The new version of templates are only supported for Python.");
@@ -527,9 +514,9 @@ private bool IsNewNodeJsProgrammingModel(WorkerRuntime workerRuntime)
527514
{
528515
if (workerRuntime == WorkerRuntime.Node)
529516
{
530-
if (FileSystemHelpers.FileExists(PackageJsonFileName))
517+
if (FileSystemHelpers.FileExists(Constants.PackageJsonFileName))
531518
{
532-
var packageJsonData = FileSystemHelpers.ReadAllTextFromFile(PackageJsonFileName);
519+
var packageJsonData = FileSystemHelpers.ReadAllTextFromFile(Constants.PackageJsonFileName);
533520
var packageJson = JsonConvert.DeserializeObject<JToken>(packageJsonData);
534521
var funcPackageVersion = packageJson["dependencies"]["@azure/functions"];
535522
if (funcPackageVersion != null && new Regex("^[^0-9]*4").IsMatch(funcPackageVersion.ToString()))

src/Cli/func/Actions/LocalActions/PackAction/CustomPackSubcommandAction.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
88
{
99
[Action(Name = "pack custom", ParentCommandName = "pack", ShowInHelp = false, HelpText = "Arguments specific to custom worker runtime apps when running func pack")]
10-
internal class CustomPackSubcommandAction : BaseAction
10+
internal class CustomPackSubcommandAction : PackSubcommandAction
1111
{
1212
public override ICommandLineParserResult ParseArgs(string[] args)
1313
{
@@ -16,8 +16,13 @@ public override ICommandLineParserResult ParseArgs(string[] args)
1616

1717
public async Task RunAsync(PackOptions packOptions)
1818
{
19-
// No build or specific action required before zipping; just perform zip
20-
await PackHelpers.DefaultZip(packOptions, TelemetryCommandEvents);
19+
await ExecuteAsync(packOptions);
20+
}
21+
22+
protected override Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)
23+
{
24+
// Custom worker packs from the function app root without extra steps
25+
return Task.FromResult(functionAppRoot);
2126
}
2227

2328
public override Task RunAsync()

src/Cli/func/Actions/LocalActions/PackAction/DotnetPackSubcommandAction.cs

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using System.IO;
45
using Azure.Functions.Cli.Common;
56
using Azure.Functions.Cli.Helpers;
67
using Azure.Functions.Cli.Interfaces;
@@ -11,7 +12,7 @@
1112
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
1213
{
1314
[Action(Name = "pack dotnet", ParentCommandName = "pack", ShowInHelp = false, HelpText = "Arguments specific to .NET apps when running func pack")]
14-
internal class DotnetPackSubcommandAction : BaseAction
15+
internal class DotnetPackSubcommandAction : PackSubcommandAction
1516
{
1617
private readonly ISecretsManager _secretsManager;
1718

@@ -28,78 +29,76 @@ public override ICommandLineParserResult ParseArgs(string[] args)
2829

2930
public async Task RunAsync(PackOptions packOptions)
3031
{
31-
var functionAppRoot = PackHelpers.ResolveFunctionAppRoot(packOptions.FolderPath);
32-
string packingRoot = functionAppRoot;
32+
await ExecuteAsync(packOptions);
33+
}
3334

34-
if (!Directory.Exists(functionAppRoot))
35+
public override Task RunAsync()
36+
{
37+
// Keep this in case the customer tries to run func pack dotnet, since this subcommand is not meant to be run directly.
38+
return Task.CompletedTask;
39+
}
40+
41+
protected override void ValidateFunctionApp(string functionAppRoot, PackOptions options)
42+
{
43+
var requiredFiles = new[] { "host.json" };
44+
foreach (var file in requiredFiles)
3545
{
36-
throw new CliException($"Directory not found to pack: {functionAppRoot}");
46+
if (!FileSystemHelpers.FileExists(Path.Combine(functionAppRoot, file)))
47+
{
48+
throw new CliException($"Required file '{file}' not found in build output directory: {functionAppRoot}");
49+
}
3750
}
51+
}
3852

39-
if (packOptions.NoBuild)
53+
protected override async Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)
54+
{
55+
// ValidateFunctionApp
56+
PackHelpers.ValidateFunctionAppRoot(functionAppRoot);
57+
58+
// For --no-build, treat FolderPath as the build output directory
59+
if (options.NoBuild)
4060
{
41-
// For --no-build, treat FolderPath as the build output directory
42-
if (string.IsNullOrEmpty(packOptions.FolderPath))
61+
var packingRoot = functionAppRoot;
62+
63+
if (string.IsNullOrEmpty(options.FolderPath))
4364
{
4465
ColoredConsole.WriteLine(WarningColor("No folder path specified. Using current directory as build output directory."));
4566
packingRoot = Environment.CurrentDirectory;
4667
}
4768
else
4869
{
49-
packingRoot = Path.IsPathRooted(packOptions.FolderPath)
50-
? packOptions.FolderPath
51-
: Path.Combine(Environment.CurrentDirectory, packOptions.FolderPath);
70+
packingRoot = Path.IsPathRooted(options.FolderPath)
71+
? options.FolderPath
72+
: Path.Combine(Environment.CurrentDirectory, options.FolderPath);
5273
}
5374

5475
if (!Directory.Exists(packingRoot))
5576
{
5677
throw new CliException($"Build output directory not found: {packingRoot}");
5778
}
5879

59-
ValidateDotNetPublishDirectory(packingRoot);
80+
return packingRoot;
6081
}
6182
else
6283
{
63-
PackHelpers.ValidateFunctionAppRoot(functionAppRoot);
64-
65-
// Run dotnet publish
6684
ColoredConsole.WriteLine("Building .NET project...");
6785
await RunDotNetPublish(functionAppRoot);
6886

69-
// Update packing root to publish output
70-
packingRoot = Path.Combine(functionAppRoot, "output");
87+
return Path.Combine(functionAppRoot, "output");
7188
}
89+
}
7290

73-
var outputPath = PackHelpers.ResolveOutputPath(functionAppRoot, packOptions.OutputPath);
74-
PackHelpers.CleanupExistingPackage(outputPath);
75-
76-
await PackHelpers.CreatePackage(packingRoot, outputPath, packOptions.NoBuild, TelemetryCommandEvents);
77-
78-
if (!packOptions.NoBuild)
91+
protected override Task PerformCleanupAfterPackingAsync(string packingRoot, string functionAppRoot, PackOptions options)
92+
{
93+
if (!options.NoBuild)
7994
{
8095
// If not no-build, delete packing root after packing
8196
FileSystemHelpers.DeleteDirectorySafe(packingRoot);
8297
}
83-
}
8498

85-
public override Task RunAsync()
86-
{
87-
// Keep this in case the customer tries to run func pack dotnet, since this subcommand is not meant to be run directly.
8899
return Task.CompletedTask;
89100
}
90101

91-
private void ValidateDotNetPublishDirectory(string path)
92-
{
93-
var requiredFiles = new[] { "host.json" };
94-
foreach (var file in requiredFiles)
95-
{
96-
if (!FileSystemHelpers.FileExists(Path.Combine(path, file)))
97-
{
98-
throw new CliException($"Required file '{file}' not found in build output directory: {path}");
99-
}
100-
}
101-
}
102-
103102
private async Task RunDotNetPublish(string functionAppRoot)
104103
{
105104
DotnetHelpers.EnsureDotnet();

0 commit comments

Comments
 (0)