Skip to content

Commit 313e44f

Browse files
authored
Adding Azure.Functions.Cli.TestFramework.csproj to have E2E tests run on (#4363)
1 parent 22e8118 commit 313e44f

17 files changed

+961
-0
lines changed

Azure.Functions.Cli.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.Cli.Abstrac
2121
EndProject
2222
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreToolsHost", "src\CoreToolsHost\CoreToolsHost.csproj", "{0333D5B6-B628-4605-A51E-D0AEE4C3F1FC}"
2323
EndProject
24+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Functions.Cli.TestFramework", "test\Cli\TestFramework\Azure.Functions.Cli.TestFramework.csproj", "{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}"
25+
EndProject
2426
Global
2527
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2628
Debug|Any CPU = Debug|Any CPU
@@ -51,6 +53,10 @@ Global
5153
{0333D5B6-B628-4605-A51E-D0AEE4C3F1FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
5254
{0333D5B6-B628-4605-A51E-D0AEE4C3F1FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
5355
{0333D5B6-B628-4605-A51E-D0AEE4C3F1FC}.Release|Any CPU.Build.0 = Release|Any CPU
56+
{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57+
{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Debug|Any CPU.Build.0 = Debug|Any CPU
58+
{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09}.Release|Any CPU.Build.0 = Release|Any CPU
5460
EndGlobalSection
5561
GlobalSection(SolutionProperties) = preSolution
5662
HideSolutionNode = FALSE
@@ -63,6 +69,7 @@ Global
6369
{78231B55-D243-46F1-9C7F-7831B40ED2D8} = {154FDAF2-0E86-450E-BE57-4E3D410B0FAC}
6470
{BC78165E-CE5B-4303-BB8E-BC172E5B86E0} = {154FDAF2-0E86-450E-BE57-4E3D410B0FAC}
6571
{0333D5B6-B628-4605-A51E-D0AEE4C3F1FC} = {5F51C958-39C0-4E0C-9165-71D0BCE647BC}
72+
{3A8E1907-E3A2-1CE0-BA8B-805B655FAF09} = {6EE1D011-2334-44F2-9D41-608B969DAE6D}
6673
EndGlobalSection
6774
GlobalSection(ExtensibilityGlobals) = postSolution
6875
SolutionGuid = {FA1E01D6-A57B-4061-A333-EDC511D283C0}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
// Based off of: https://github.com/dotnet/sdk/blob/e793aa4709d28cd783712df40413448250e26fea/test/Microsoft.NET.TestFramework/Assertions/CommandResultAssertions.cs
5+
using Azure.Functions.Cli.Abstractions.Command;
6+
using FluentAssertions;
7+
using FluentAssertions.Execution;
8+
9+
namespace Azure.Functions.Cli.TestFramework.Assertions
10+
{
11+
public class CommandResultAssertions(CommandResult commandResult)
12+
{
13+
private readonly CommandResult _commandResult = commandResult;
14+
15+
public CommandResultAssertions ExitWith(int expectedExitCode)
16+
{
17+
Execute.Assertion.ForCondition(_commandResult.ExitCode == expectedExitCode)
18+
.FailWith($"Expected command to exit with {expectedExitCode} but it did not. Error message: {_commandResult.StdErr}");
19+
return this;
20+
}
21+
22+
public AndConstraint<CommandResultAssertions> HaveStdOutContaining(string pattern)
23+
{
24+
Execute.Assertion.ForCondition(_commandResult.StdOut is not null && _commandResult.StdOut.Contains(pattern))
25+
.FailWith($"The command output did not contain expected result: {pattern}{Environment.NewLine}");
26+
return new AndConstraint<CommandResultAssertions>(this);
27+
}
28+
29+
public AndConstraint<CommandResultAssertions> HaveStdErrContaining(string pattern)
30+
{
31+
Execute.Assertion.ForCondition(_commandResult.StdErr is not null && _commandResult.StdErr.Contains(pattern))
32+
.FailWith($"The command output did not contain expected result: {pattern}{Environment.NewLine}");
33+
return new AndConstraint<CommandResultAssertions>(this);
34+
}
35+
36+
public AndConstraint<CommandResultAssertions> NotHaveStdOutContaining(string pattern)
37+
{
38+
Execute.Assertion.ForCondition(_commandResult.StdOut is not null && !_commandResult.StdOut.Contains(pattern))
39+
.FailWith($"The command output did contain expected result: {pattern}{Environment.NewLine}");
40+
return new AndConstraint<CommandResultAssertions>(this);
41+
}
42+
}
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
// Copied from: https://github.com/dotnet/sdk/blob/e793aa4709d28cd783712df40413448250e26fea/test/Microsoft.NET.TestFramework/Assertions/CommandResultExtensions.cs
5+
using Azure.Functions.Cli.Abstractions.Command;
6+
7+
namespace Azure.Functions.Cli.TestFramework.Assertions
8+
{
9+
public static class CommandResultExtensions
10+
{
11+
public static CommandResultAssertions Should(this CommandResult commandResult) => new(commandResult);
12+
}
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectCapability Remove="TestContainer" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Azure.Storage.Queues" />
14+
<PackageReference Include="xunit" />
15+
<PackageReference Include="FluentAssertions"/>
16+
<PackageReference Include="StyleCop.Analyzers" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="$(RepoSrcRoot)Cli\Abstractions\Azure.Functions.Cli.Abstractions.csproj" />
21+
</ItemGroup>
22+
23+
</Project>

test/Cli/TestFramework/CommandInfo.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
// Based off of: https://github.com/dotnet/sdk/blob/e793aa4709d28cd783712df40413448250e26fea/test/Microsoft.NET.TestFramework/Commands/SdkCommandSpec.cs
5+
using System.Diagnostics;
6+
using Azure.Functions.Cli.Abstractions.Command;
7+
8+
namespace Azure.Functions.Cli.TestFramework
9+
{
10+
public class CommandInfo
11+
{
12+
public required string FileName { get; set; }
13+
14+
public List<string> Arguments { get; set; } = [];
15+
16+
public Dictionary<string, string> Environment { get; set; } = [];
17+
18+
public List<string> EnvironmentToRemove { get; } = [];
19+
20+
public required string WorkingDirectory { get; set; }
21+
22+
public string? TestName { get; set; }
23+
24+
public Command ToCommand()
25+
{
26+
var process = new Process()
27+
{
28+
StartInfo = ToProcessStartInfo()
29+
};
30+
31+
return new Command(process, trimTrailingNewlines: true);
32+
}
33+
34+
public ProcessStartInfo ToProcessStartInfo()
35+
{
36+
var psi = new ProcessStartInfo
37+
{
38+
FileName = FileName,
39+
Arguments = string.Join(" ", Arguments),
40+
UseShellExecute = false
41+
};
42+
43+
foreach (KeyValuePair<string, string> kvp in Environment)
44+
{
45+
psi.Environment[kvp.Key] = kvp.Value;
46+
}
47+
48+
foreach (string envToRemove in EnvironmentToRemove)
49+
{
50+
psi.Environment.Remove(envToRemove);
51+
}
52+
53+
if (WorkingDirectory is not null)
54+
{
55+
psi.WorkingDirectory = WorkingDirectory;
56+
}
57+
58+
return psi;
59+
}
60+
}
61+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
// Based off of: https://github.com/dotnet/sdk/blob/e793aa4709d28cd783712df40413448250e26fea/test/Microsoft.NET.TestFramework/Commands/TestCommand.cs
5+
using System.Diagnostics;
6+
using Azure.Functions.Cli.Abstractions.Command;
7+
using Xunit.Abstractions;
8+
9+
namespace Azure.Functions.Cli.TestFramework.Commands
10+
{
11+
public abstract class FuncCommand(ITestOutputHelper log)
12+
{
13+
private readonly Dictionary<string, string> _environment = [];
14+
15+
public ITestOutputHelper Log { get; } = log;
16+
17+
public string? WorkingDirectory { get; set; }
18+
19+
public List<string> Arguments { get; set; } = [];
20+
21+
public List<string> EnvironmentToRemove { get; } = [];
22+
23+
// These only work via Execute(), not when using GetProcessStartInfo()
24+
public Action<string>? CommandOutputHandler { get; set; }
25+
26+
public Func<Process, Task>? ProcessStartedHandler { get; set; }
27+
28+
public StreamWriter? FileWriter { get; private set; } = null;
29+
30+
public string? LogFilePath { get; private set; }
31+
32+
protected abstract CommandInfo CreateCommand(IEnumerable<string> args);
33+
34+
public FuncCommand WithEnvironmentVariable(string name, string value)
35+
{
36+
_environment[name] = value;
37+
return this;
38+
}
39+
40+
public FuncCommand WithWorkingDirectory(string workingDirectory)
41+
{
42+
WorkingDirectory = workingDirectory;
43+
return this;
44+
}
45+
46+
private CommandInfo CreateCommandInfo(IEnumerable<string> args)
47+
{
48+
CommandInfo commandInfo = CreateCommand(args);
49+
foreach (KeyValuePair<string, string> kvp in _environment)
50+
{
51+
commandInfo.Environment[kvp.Key] = kvp.Value;
52+
}
53+
54+
foreach (string envToRemove in EnvironmentToRemove)
55+
{
56+
commandInfo.EnvironmentToRemove.Add(envToRemove);
57+
}
58+
59+
if (WorkingDirectory is not null)
60+
{
61+
commandInfo.WorkingDirectory = WorkingDirectory;
62+
}
63+
64+
if (Arguments.Count != 0)
65+
{
66+
commandInfo.Arguments = [.. Arguments, .. commandInfo.Arguments];
67+
}
68+
69+
return commandInfo;
70+
}
71+
72+
public ProcessStartInfo GetProcessStartInfo(params string[] args)
73+
{
74+
CommandInfo commandSpec = CreateCommandInfo(args);
75+
return commandSpec.ToProcessStartInfo();
76+
}
77+
78+
public virtual CommandResult Execute(IEnumerable<string> args)
79+
{
80+
CommandInfo spec = CreateCommandInfo(args);
81+
ICommand command = spec
82+
.ToCommand()
83+
.CaptureStdOut()
84+
.CaptureStdErr();
85+
86+
string? funcExeDirectory = Path.GetDirectoryName(spec.FileName);
87+
88+
if (!string.IsNullOrEmpty(funcExeDirectory))
89+
{
90+
Directory.SetCurrentDirectory(funcExeDirectory);
91+
}
92+
93+
string? directoryToLogTo = Environment.GetEnvironmentVariable("DirectoryToLogTo");
94+
if (string.IsNullOrEmpty(directoryToLogTo))
95+
{
96+
directoryToLogTo = Directory.GetCurrentDirectory();
97+
}
98+
99+
// Ensure directory exists
100+
Directory.CreateDirectory(directoryToLogTo);
101+
102+
// Create a more unique filename to avoid conflicts
103+
string uniqueId = Guid.NewGuid().ToString("N")[..8];
104+
LogFilePath = Path.Combine(
105+
directoryToLogTo,
106+
$"func_{spec.Arguments.First()}_{spec.TestName}_{DateTime.Now:yyyyMMdd_HHmmss}_{uniqueId}.log");
107+
108+
// Make sure we're only opening the file once
109+
try
110+
{
111+
// Open with FileShare.Read to allow others to read but not write
112+
var fileStream = new FileStream(LogFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
113+
FileWriter = new StreamWriter(fileStream)
114+
{
115+
AutoFlush = true
116+
};
117+
118+
// Write initial information
119+
FileWriter.WriteLine($"=== Test started at {DateTime.Now} ===");
120+
FileWriter.WriteLine($"Test Name: {spec.TestName}");
121+
string? display = $"func {string.Join(" ", spec.Arguments)}";
122+
FileWriter.WriteLine($"Command: {display}");
123+
FileWriter.WriteLine($"Working Directory: {spec.WorkingDirectory ?? "not specified"}");
124+
FileWriter.WriteLine("====================================");
125+
126+
command.OnOutputLine(line =>
127+
{
128+
try
129+
{
130+
// Write to the file if it's still open
131+
if (FileWriter is not null && FileWriter.BaseStream is not null)
132+
{
133+
FileWriter.WriteLine($"[STDOUT] {line}");
134+
FileWriter.Flush();
135+
}
136+
137+
Log.WriteLine($"》 {line}");
138+
CommandOutputHandler?.Invoke(line);
139+
}
140+
catch (Exception ex)
141+
{
142+
Log.WriteLine($"Error writing to log file: {ex.Message}");
143+
}
144+
});
145+
146+
command.OnErrorLine(line =>
147+
{
148+
try
149+
{
150+
// Write to the file if it's still open
151+
if (FileWriter is not null && FileWriter.BaseStream is not null)
152+
{
153+
FileWriter.WriteLine($"[STDERR] {line}");
154+
FileWriter.Flush();
155+
}
156+
157+
if (!string.IsNullOrEmpty(line))
158+
{
159+
Log.WriteLine($"❌ {line}");
160+
}
161+
}
162+
catch (Exception ex)
163+
{
164+
Log.WriteLine($"Error writing to log file: {ex.Message}");
165+
}
166+
});
167+
168+
Log.WriteLine($"Executing '{display}':");
169+
Log.WriteLine($"Output being captured to: {LogFilePath}");
170+
171+
CommandResult result = ((Command)command).Execute(ProcessStartedHandler, FileWriter);
172+
173+
FileWriter.WriteLine("====================================");
174+
FileWriter.WriteLine($"Command exited with code: {result.ExitCode}");
175+
FileWriter.WriteLine($"=== Test ended at {DateTime.Now} ===");
176+
177+
Log.WriteLine($"Command '{display}' exited with exit code {result.ExitCode}.");
178+
179+
return result;
180+
}
181+
finally
182+
{
183+
// Make sure to close and dispose the writer
184+
if (FileWriter is not null)
185+
{
186+
try
187+
{
188+
FileWriter.Close();
189+
FileWriter.Dispose();
190+
}
191+
catch (Exception ex)
192+
{
193+
Log.WriteLine($"Error closing log file: {ex.Message}");
194+
}
195+
}
196+
}
197+
}
198+
199+
public static void LogCommandResult(ITestOutputHelper log, CommandResult result)
200+
{
201+
log.WriteLine($"> {result.StartInfo.FileName} {result.StartInfo.Arguments}");
202+
log.WriteLine(result.StdOut);
203+
204+
if (!string.IsNullOrEmpty(result.StdErr))
205+
{
206+
log.WriteLine(string.Empty);
207+
log.WriteLine("StdErr:");
208+
log.WriteLine(result.StdErr);
209+
}
210+
211+
if (result.ExitCode != 0)
212+
{
213+
log.WriteLine($"Exit Code: {result.ExitCode}");
214+
}
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)