This commit is contained in:
Vlada Shubina 2023-02-13 15:35:40 +01:00
Родитель 6baba9b9f1
Коммит 7434ad6d90
35 изменённых файлов: 2895 добавлений и 1 удалений

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public class CapturingLogger : ILogger
{
public LoggerVerbosity Verbosity { get => LoggerVerbosity.Diagnostic; set { } }
public string Parameters { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
private List<BuildMessageEventArgs> _messages = new();
public IReadOnlyList<BuildMessageEventArgs> Messages {get { return _messages; } }
private List<BuildWarningEventArgs> _warnings = new();
public IReadOnlyList<BuildWarningEventArgs> Warnings {get { return _warnings; } }
private List<BuildErrorEventArgs> _errors = new();
public IReadOnlyList<BuildErrorEventArgs> Errors {get { return _errors; } }
public void Initialize(IEventSource eventSource)
{
eventSource.MessageRaised += (o, e) => _messages.Add(e);
eventSource.WarningRaised += (o, e) => _warnings.Add(e);
eventSource.ErrorRaised += (o, e) => _errors.Add(e);
}
public void Shutdown()
{
}
}

Просмотреть файл

@ -0,0 +1,199 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
namespace Microsoft.DotNet.CommandUtils
{
internal static class ArgumentEscaper
{
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx .
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
internal static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable<string> args)
{
IEnumerable<string> escaped = EscapeArgArray(args);
return string.Join(" ", escaped);
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx .
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
internal static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable<string> args)
{
IEnumerable<string> escaped = EscapeArgArrayForCmd(args);
return string.Join(" ", escaped);
}
internal static string EscapeSingleArg(string arg)
{
var sb = new StringBuilder();
var length = arg.Length;
var needsQuotes = length == 0 || ShouldSurroundWithQuotes(arg);
var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
if (needsQuotes)
{
sb.Append('"');
}
for (int i = 0; i < length; ++i)
{
var backslashCount = 0;
// Consume All Backslashes
while (i < arg.Length && arg[i] == '\\')
{
backslashCount++;
i++;
}
// Escape any backslashes at the end of the arg
// when the argument is also quoted.
// This ensures the outside quote is interpreted as
// an argument delimiter
if (i == arg.Length && isQuoted)
{
sb.Append('\\', 2 * backslashCount);
}
// At then end of the arg, which isn't quoted,
// just add the backslashes, no need to escape
else if (i == arg.Length)
{
sb.Append('\\', backslashCount);
}
// Escape any preceding backslashes and the quote
else if (arg[i] == '"')
{
sb.Append('\\', (2 * backslashCount) + 1);
sb.Append('"');
}
// Output any consumed backslashes and the character
else
{
sb.Append('\\', backslashCount);
sb.Append(arg[i]);
}
}
if (needsQuotes)
{
sb.Append('"');
}
return sb.ToString();
}
internal static bool ShouldSurroundWithQuotes(string argument)
{
// Only quote if whitespace exists in the string
return ArgumentContainsWhitespace(argument);
}
internal static bool IsSurroundedWithQuotes(string argument)
{
return argument.StartsWith("\"", StringComparison.Ordinal) &&
argument.EndsWith("\"", StringComparison.Ordinal);
}
internal static bool ArgumentContainsWhitespace(string argument)
{
return argument.Contains(' ') || argument.Contains('\t') || argument.Contains('\n');
}
/// <summary>
/// Prepare as single argument to
/// roundtrip properly through cmd.
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx .
/// </summary>
/// <param name="argument"></param>
/// <returns></returns>
private static string EscapeArgForCmd(string argument)
{
var sb = new StringBuilder();
var quoted = ShouldSurroundWithQuotes(argument);
if (quoted)
{
sb.Append("^\"");
}
// Prepend every character with ^
// This is harmless when passing through cmd
// and ensures cmd metacharacters are not interpreted
// as such
foreach (var character in argument)
{
sb.Append('^');
sb.Append(character);
}
if (quoted)
{
sb.Append("^\"");
}
return sb.ToString();
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx .
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static List<string> EscapeArgArray(IEnumerable<string> args)
{
var escapedArgs = new List<string>();
foreach (var arg in args)
{
escapedArgs.Add(EscapeSingleArg(arg));
}
return escapedArgs;
}
/// <summary>
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx.
/// </summary>
/// <param name="arguments"></param>
/// <returns></returns>
private static List<string> EscapeArgArrayForCmd(IEnumerable<string> arguments)
{
var escapedArgs = new List<string>();
foreach (var arg in arguments)
{
escapedArgs.Add(EscapeArgForCmd(arg));
}
return escapedArgs;
}
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit.Abstractions;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class BasicCommand : TestCommand
{
private readonly string _processName;
internal BasicCommand(ITestOutputHelper? log, string processName, params string[] args) : base(log)
{
_processName = processName;
Arguments.AddRange(args.Where(a => !string.IsNullOrWhiteSpace(a)));
}
private protected override SdkCommandSpec CreateCommand(IEnumerable<string> args)
{
var sdkCommandSpec = new SdkCommandSpec()
{
FileName = _processName,
Arguments = args.ToList(),
WorkingDirectory = WorkingDirectory
};
return sdkCommandSpec;
}
}
}

Просмотреть файл

@ -0,0 +1,202 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class Command
{
private readonly Process _process;
private readonly bool _trimTrailingNewlines;
private StreamForwarder? _stdOut;
private StreamForwarder? _stdErr;
private bool _running;
public Command(Process process, bool trimtrailingNewlines = false)
{
_trimTrailingNewlines = trimtrailingNewlines;
_process = process ?? throw new ArgumentNullException(nameof(process));
}
public string CommandName => _process.StartInfo.FileName;
public string CommandArgs => _process.StartInfo.Arguments;
public CommandResult Execute()
{
return Execute(_ => { });
}
public CommandResult Execute(Action<Process>? processStarted)
{
Console.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}");
ThrowIfRunning();
_running = true;
_process.EnableRaisingEvents = true;
#if DEBUG
var sw = Stopwatch.StartNew();
Console.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}");
#endif
using (var reaper = new ProcessReaper(_process))
{
_process.Start();
processStarted?.Invoke(_process);
reaper.NotifyProcessStarted();
Console.WriteLine($"Process ID: {_process.Id}");
var taskOut = _stdOut?.BeginRead(_process.StandardOutput);
var taskErr = _stdErr?.BeginRead(_process.StandardError);
_process.WaitForExit();
taskOut?.Wait();
taskErr?.Wait();
}
var exitCode = _process.ExitCode;
#if DEBUG
var message = string.Format($"&lt; {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms");
if (exitCode == 0)
{
Console.WriteLine(message);
}
else
{
Console.WriteLine(message);
}
#endif
return new CommandResult(
_process.StartInfo,
exitCode,
_stdOut?.CapturedOutput,
_stdErr?.CapturedOutput);
}
public Command WorkingDirectory(string projectDirectory)
{
_process.StartInfo.WorkingDirectory = projectDirectory;
return this;
}
public Command EnvironmentVariable(string name, string value)
{
_process.StartInfo.Environment[name] = value;
return this;
}
public Command CaptureStdOut()
{
ThrowIfRunning();
EnsureStdOut();
_stdOut!.Capture(_trimTrailingNewlines);
return this;
}
public Command CaptureStdErr()
{
ThrowIfRunning();
EnsureStdErr();
_stdErr!.Capture(_trimTrailingNewlines);
return this;
}
public Command ForwardStdOut(TextWriter? to = null)
{
ThrowIfRunning();
EnsureStdOut();
if (to == null)
{
_stdOut!.ForwardTo(writeLine: Console.Out.WriteLine);
}
else
{
_stdOut!.ForwardTo(writeLine: to.WriteLine);
}
return this;
}
public Command ForwardStdErr(TextWriter? to = null)
{
ThrowIfRunning();
EnsureStdErr();
if (to == null)
{
_stdErr!.ForwardTo(writeLine: Console.Error.WriteLine);
}
else
{
_stdErr!.ForwardTo(writeLine: to.WriteLine);
}
return this;
}
public Command OnOutputLine(Action<string> handler)
{
ThrowIfRunning();
EnsureStdOut();
_stdOut!.ForwardTo(writeLine: handler);
return this;
}
public Command OnErrorLine(Action<string> handler)
{
ThrowIfRunning();
EnsureStdErr();
_stdErr!.ForwardTo(writeLine: handler);
return this;
}
public Command SetCommandArgs(string commandArgs)
{
_process.StartInfo.Arguments = commandArgs;
return this;
}
private static string FormatProcessInfo(ProcessStartInfo info)
{
if (string.IsNullOrWhiteSpace(info.Arguments))
{
return info.FileName;
}
return info.FileName + " " + info.Arguments;
}
private void EnsureStdOut()
{
_stdOut ??= new StreamForwarder();
_process.StartInfo.RedirectStandardOutput = true;
}
private void EnsureStdErr()
{
_stdErr ??= new StreamForwarder();
_process.StartInfo.RedirectStandardError = true;
}
private void ThrowIfRunning([CallerMemberName] string? memberName = null)
{
if (_running)
{
throw new InvalidOperationException($"Unable to invoke {memberName} after the command has been run.");
}
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
namespace Microsoft.DotNet.CommandUtils
{
internal readonly struct CommandResult
{
internal CommandResult(ProcessStartInfo startInfo, int exitCode, string? stdOut, string? stdErr)
{
StartInfo = startInfo;
ExitCode = exitCode;
StdOut = stdOut;
StdErr = stdErr;
}
internal ProcessStartInfo StartInfo { get; }
internal int ExitCode { get; }
internal string? StdOut { get; }
internal string? StdErr { get; }
}
}

Просмотреть файл

@ -0,0 +1,242 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.RegularExpressions;
using FluentAssertions;
using FluentAssertions.Execution;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class CommandResultAssertions
{
private readonly CommandResult _commandResult;
internal CommandResultAssertions(CommandResult commandResult)
{
_commandResult = commandResult;
}
internal AndConstraint<CommandResultAssertions> ExitWith(int expectedExitCode)
{
Execute.Assertion.ForCondition(_commandResult.ExitCode == expectedExitCode)
.FailWith(AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not."));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> Pass()
{
Execute.Assertion.ForCondition(_commandResult.ExitCode == 0)
.FailWith(AppendDiagnosticsTo($"Expected command to pass but it did not."));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> Fail()
{
Execute.Assertion.ForCondition(_commandResult.ExitCode != 0)
.FailWith(AppendDiagnosticsTo($"Expected command to fail but it did not."));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOut()
{
Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdOut))
.FailWith(AppendDiagnosticsTo("Command did not output anything to stdout"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOut(string expectedOutput)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(_commandResult.StdOut.Equals(expectedOutput, StringComparison.Ordinal))
.FailWith(AppendDiagnosticsTo($"Command did not output with Expected Output. Expected: {expectedOutput}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOutContaining(string pattern)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(_commandResult.StdOut.Contains(pattern))
.FailWith(AppendDiagnosticsTo($"The command output did not contain expected result: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOutContaining(Func<string, bool> predicate, string description = "")
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(predicate(_commandResult.StdOut))
.FailWith(AppendDiagnosticsTo($"The command output did not contain expected result: {description} {Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> NotHaveStdOutContaining(string pattern)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(!_commandResult.StdOut.Contains(pattern))
.FailWith(AppendDiagnosticsTo($"The command output contained a result it should not have contained: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOutContainingIgnoreSpaces(string pattern)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
string commandResultNoSpaces = _commandResult.StdOut.Replace(" ", string.Empty);
Execute.Assertion
.ForCondition(commandResultNoSpaces.Contains(pattern))
.FailWith(AppendDiagnosticsTo($"The command output did not contain expected result: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOutContainingIgnoreCase(string pattern)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(_commandResult.StdOut.Contains(pattern, StringComparison.OrdinalIgnoreCase))
.FailWith(AppendDiagnosticsTo($"The command output did not contain expected result (ignoring case): {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdOut, pattern, options).Success)
.FailWith(AppendDiagnosticsTo($"Matching the command output failed. Pattern: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> NotHaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None)
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(!Regex.Match(_commandResult.StdOut, pattern, options).Success)
.FailWith(AppendDiagnosticsTo($"The command output matched a pattern it should not have. Pattern: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdErr()
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdErr))
.FailWith(AppendDiagnosticsTo("Command did not output anything to StdErr."));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdErr(string expectedOutput)
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdErr for the command was not captured");
}
Execute.Assertion.ForCondition(_commandResult.StdErr.Equals(expectedOutput, StringComparison.Ordinal))
.FailWith(AppendDiagnosticsTo($"Command did not output the expected output to StdErr.{Environment.NewLine}Expected: {expectedOutput}{Environment.NewLine}Actual: {_commandResult.StdErr}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdErrContaining(string pattern)
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdErr for the command was not captured");
}
Execute.Assertion.ForCondition(_commandResult.StdErr.Contains(pattern))
.FailWith(AppendDiagnosticsTo($"The command error output did not contain expected result: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> NotHaveStdErrContaining(string pattern)
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdErr for the command was not captured");
}
Execute.Assertion.ForCondition(!_commandResult.StdErr.Contains(pattern))
.FailWith(AppendDiagnosticsTo($"The command error output contained a result it should not have contained: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None)
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdErr for the command was not captured");
}
Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdErr, pattern, options).Success)
.FailWith(AppendDiagnosticsTo($"Matching the command error output failed. Pattern: {pattern}{Environment.NewLine}"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> NotHaveStdOut()
{
if (_commandResult.StdOut is null)
{
throw new InvalidOperationException("StdOut for the command was not captured");
}
Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdOut))
.FailWith(AppendDiagnosticsTo($"Expected command to not output to stdout but it was not:"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> NotHaveStdErr()
{
if (_commandResult.StdErr is null)
{
throw new InvalidOperationException("StdErr for the command was not captured");
}
Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdErr))
.FailWith(AppendDiagnosticsTo("Expected command to not output to stderr but it was not:"));
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName)
{
_commandResult.StdOut.Should().Contain($"Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.");
return new AndConstraint<CommandResultAssertions>(this);
}
internal AndConstraint<CommandResultAssertions> HaveCompiledProject(string compiledProject, string frameworkFullName)
{
_commandResult.StdOut.Should().Contain($"Project {compiledProject} ({frameworkFullName}) will be compiled");
return new AndConstraint<CommandResultAssertions>(this);
}
private string AppendDiagnosticsTo(string s)
{
return (s + $"{Environment.NewLine}" +
$"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" +
$"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" +
$"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" +
$"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" +
$"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}")
//escape curly braces for String.Format
.Replace("{", "{{").Replace("}", "}}");
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.CommandUtils
{
internal static class CommandResultExtensions
{
internal static CommandResultAssertions Should(this CommandResult commandResult)
{
return new CommandResultAssertions(commandResult);
}
}
}

Просмотреть файл

@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit.Abstractions;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class DotnetCommand : TestCommand
{
private string _executableFilePath = "dotnet";
internal DotnetCommand(ITestOutputHelper log, string subcommand, params string[] args) : base(log)
{
Arguments.Add(subcommand);
Arguments.AddRange(args);
}
internal DotnetCommand WithoutTelemetry()
{
WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true");
return this;
}
internal DotnetCommand WithCustomExecutablePath(string? executableFilePath)
{
if (!string.IsNullOrEmpty(executableFilePath))
{
_executableFilePath = executableFilePath;
}
return this;
}
private protected override SdkCommandSpec CreateCommand(IEnumerable<string> args)
{
var sdkCommandSpec = new SdkCommandSpec()
{
FileName = _executableFilePath,
Arguments = args.ToList(),
WorkingDirectory = WorkingDirectory
};
return sdkCommandSpec;
}
}
}

Просмотреть файл

@ -0,0 +1,96 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace Microsoft.DotNet.CommandUtils
{
internal static class NativeMethods
{
internal static class Windows
{
internal const int ProcessBasicInformation = 0;
internal enum JobObjectInfoClass : uint
{
JobObjectExtendedLimitInformation = 9,
}
[Flags]
internal enum JobObjectLimitFlags : uint
{
JobObjectLimitKillOnJobClose = 0x2000,
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern SafeWaitHandle CreateJobObjectW(IntPtr lpJobAttributes, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass jobObjectInformationClass, IntPtr lpJobObjectInformation, uint cbJobObjectInformationLength);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static extern IntPtr GetCommandLine();
[StructLayout(LayoutKind.Sequential)]
internal struct JobObjectBasicLimitInformation
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public JobObjectLimitFlags LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
internal struct IoCounters
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JobObjectExtendedLimitInformation
{
public JobObjectBasicLimitInformation BasicLimitInformation;
public IoCounters IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_BASIC_INFORMATION
{
public uint ExitStatus;
public IntPtr PebBaseAddress;
public UIntPtr AffinityMask;
public int BasePriority;
public UIntPtr UniqueProcessId;
public UIntPtr InheritedFromUniqueProcessId;
}
}
internal static class Posix
{
internal const int SIGINT = 2;
internal const int SIGTERM = 15;
[DllImport("libc", SetLastError = true)]
internal static extern int kill(int pid, int sig);
}
}
}

Просмотреть файл

@ -0,0 +1,194 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace Microsoft.DotNet.CommandUtils
{
/// <summary>
/// Responsible for reaping a target process if the current process terminates.
/// </summary>
/// <remarks>
/// On Windows, a job object will be used to ensure the termination of the target
/// process (and its tree) even if the current process is rudely terminated.
///
/// On POSIX systems, the reaper will handle SIGTERM and attempt to forward the
/// signal to the target process only.
///
/// The reaper also suppresses SIGINT in the current process to allow the target
/// process to handle the signal.
/// </remarks>
internal sealed class ProcessReaper : IDisposable
{
private readonly Process _process;
private SafeWaitHandle? _job;
private Mutex? _shutdownMutex;
/// <summary>
/// Creates a new process reaper.
/// </summary>
/// <param name="process">The target process to reap if the current process terminates. The process should not yet be started.</param>
public ProcessReaper(Process process)
{
_process = process;
// The tests need the event handlers registered prior to spawning the child to prevent a race
// where the child writes output the test expects before the intermediate dotnet process
// has registered the event handlers to handle the signals the tests will generate.
Console.CancelKeyPress += HandleCancelKeyPress;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_shutdownMutex = new Mutex();
AppDomain.CurrentDomain.ProcessExit += HandleProcessExit;
}
}
/// <summary>
/// Call to notify the reaper that the process has started.
/// </summary>
public void NotifyProcessStarted()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Limit the use of job objects to versions of Windows that support nested jobs (i.e. Windows 8/2012 or later).
// Ideally, we would check for some new API export or OS feature instead of the OS version,
// but nested jobs are transparently implemented with respect to the Job Objects API.
// Note: Windows 8.1 and later may report as Windows 8 (see https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version).
// However, for the purpose of this check that is still sufficient.
if (Environment.OSVersion.Version.Major > 6 ||
(Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 2))
{
_job = AssignProcessToJobObject(_process.Handle);
}
}
}
public void Dispose()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (_job != null)
{
// Clear the kill on close flag because the child process terminated successfully
// If this fails, then we have no choice but to terminate any remaining processes in the job
SetKillOnJobClose(_job.DangerousGetHandle(), false);
_job.Dispose();
_job = null;
}
}
else
{
AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit;
// If there's been a shutdown via the process exit handler,
// this will block the current thread so we don't race with the CLR shutdown
// from the signal handler.
if (_shutdownMutex != null)
{
_shutdownMutex.WaitOne();
_shutdownMutex.ReleaseMutex();
_shutdownMutex.Dispose();
_shutdownMutex = null;
}
}
Console.CancelKeyPress -= HandleCancelKeyPress;
}
private static void HandleCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
// Ignore SIGINT/SIGQUIT so that the process can handle the signal
e.Cancel = true;
}
private static SafeWaitHandle? AssignProcessToJobObject(IntPtr process)
{
var job = NativeMethods.Windows.CreateJobObjectW(IntPtr.Zero, null);
if (job == null || job.IsInvalid)
{
return null;
}
if (!SetKillOnJobClose(job.DangerousGetHandle(), true))
{
job.Dispose();
return null;
}
if (!NativeMethods.Windows.AssignProcessToJobObject(job.DangerousGetHandle(), process))
{
job.Dispose();
return null;
}
return job;
}
private static bool SetKillOnJobClose(IntPtr job, bool value)
{
var information = new NativeMethods.Windows.JobObjectExtendedLimitInformation
{
BasicLimitInformation = new NativeMethods.Windows.JobObjectBasicLimitInformation
{
LimitFlags = value ? NativeMethods.Windows.JobObjectLimitFlags.JobObjectLimitKillOnJobClose : 0
}
};
var length = Marshal.SizeOf(typeof(NativeMethods.Windows.JobObjectExtendedLimitInformation));
var informationPtr = Marshal.AllocHGlobal(length);
try
{
Marshal.StructureToPtr(information, informationPtr, false);
if (!NativeMethods.Windows.SetInformationJobObject(
job,
NativeMethods.Windows.JobObjectInfoClass.JobObjectExtendedLimitInformation,
informationPtr,
(uint)length))
{
return false;
}
return true;
}
finally
{
Marshal.FreeHGlobal(informationPtr);
}
}
private void HandleProcessExit(object? sender, EventArgs args)
{
int processId;
try
{
processId = _process.Id;
}
catch (InvalidOperationException)
{
// The process hasn't started yet; nothing to signal
return;
}
// Take ownership of the shutdown mutex; this will ensure that the other
// thread also waiting on the process to exit won't complete CLR shutdown before
// this one does.
_shutdownMutex?.WaitOne();
if (!_process.WaitForExit(0) && NativeMethods.Posix.kill(processId, NativeMethods.Posix.SIGTERM) != 0)
{
// Couldn't send the signal, don't wait
return;
}
// If SIGTERM was ignored by the target, then we'll still wait
_process.WaitForExit();
Environment.ExitCode = _process.ExitCode;
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class SdkCommandSpec
{
public string? FileName { get; set; }
public List<string> Arguments { get; set; } = new List<string>();
public Dictionary<string, string> Environment { get; set; } = new Dictionary<string, string>();
public List<string> EnvironmentToRemove { get; } = new List<string>();
public string? WorkingDirectory { get; set; }
public Command ToCommand()
{
var process = new Process()
{
StartInfo = ToProcessStartInfo()
};
var ret = new Command(process, trimtrailingNewlines: true);
return ret;
}
public ProcessStartInfo ToProcessStartInfo()
{
var ret = new ProcessStartInfo
{
FileName = FileName,
Arguments = EscapeArgs(),
UseShellExecute = false
};
foreach (var kvp in Environment)
{
ret.Environment[kvp.Key] = kvp.Value;
}
foreach (var envToRemove in EnvironmentToRemove)
{
ret.Environment.Remove(envToRemove);
}
if (WorkingDirectory != null)
{
ret.WorkingDirectory = WorkingDirectory;
}
return ret;
}
private string EscapeArgs()
{
// Note: this doesn't handle invoking .cmd files via "cmd /c" on Windows, which probably won't be necessary here
// If it is, refer to the code in WindowsExePreferredCommandSpecFactory in Microsoft.DotNet.Cli.Utils
return ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(Arguments);
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.DotNet.CommandUtils;
/// <summary>
/// This is an abstraction so we can pass ITestOutputHelper to TestCommand constructor
/// when calling from class fixture.
/// </summary>
public class SharedTestOutputHelper : ITestOutputHelper
{
private readonly IMessageSink _sink;
public SharedTestOutputHelper(IMessageSink sink)
{
this._sink = sink;
}
public void WriteLine(string message)
{
_sink.OnMessage(new DiagnosticMessage(message));
}
public void WriteLine(string format, params object[] args)
{
_sink.OnMessage(new DiagnosticMessage(format, args));
}
}

Просмотреть файл

@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
namespace Microsoft.DotNet.CommandUtils
{
internal sealed class StreamForwarder
{
private const char FlushBuilderCharacter = '\n';
private static readonly char[] IgnoreCharacters = new char[] { '\r' };
private StringBuilder? _builder;
private StringWriter? _capture;
private Action<string>? _writeLine;
private bool _trimTrailingCapturedNewline;
public string? CapturedOutput
{
get
{
string? capture = _capture?.GetStringBuilder()?.ToString();
if (_trimTrailingCapturedNewline)
{
capture = capture?.TrimEnd('\r', '\n');
}
return capture;
}
}
public StreamForwarder Capture(bool trimTrailingNewline = false)
{
ThrowIfCaptureSet();
_capture = new StringWriter();
_trimTrailingCapturedNewline = trimTrailingNewline;
return this;
}
public StreamForwarder ForwardTo(Action<string> writeLine)
{
ThrowIfNull(writeLine);
ThrowIfForwarderSet();
_writeLine = writeLine;
return this;
}
public Task BeginRead(TextReader reader) => Task.Run(() => Read(reader));
public void Read(TextReader reader)
{
int bufferSize = 1;
char currentCharacter;
char[] buffer = new char[bufferSize];
_builder = new StringBuilder();
// Using Read with buffer size 1 to prevent looping endlessly
// like we would when using Read() with no buffer
while ((_ = reader.Read(buffer, 0, bufferSize)) > 0)
{
currentCharacter = buffer[0];
if (currentCharacter == FlushBuilderCharacter)
{
WriteBuilder();
}
else if (!IgnoreCharacters.Contains(currentCharacter))
{
_ = _builder.Append(currentCharacter);
}
}
// Flush anything else when the stream is closed
// Which should only happen if someone used console.Write
if (_builder.Length > 0)
{
WriteBuilder();
}
}
private void WriteBuilder()
{
WriteLine(_builder?.ToString());
_ = (_builder?.Clear());
}
private void WriteLine(string? str)
{
_capture?.WriteLine(str);
if (_writeLine != null && str != null)
{
_writeLine(str);
}
}
private static void ThrowIfNull(object obj)
{
ArgumentNullException.ThrowIfNull(obj);
}
private void ThrowIfForwarderSet()
{
if (_writeLine != null)
{
throw new InvalidOperationException("WriteLine forwarder set previously");
}
}
private void ThrowIfCaptureSet()
{
if (_capture != null)
{
throw new InvalidOperationException("Already capturing stream!");
}
}
}
}

Просмотреть файл

@ -0,0 +1,136 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using Xunit.Abstractions;
namespace Microsoft.DotNet.CommandUtils
{
internal abstract class TestCommand
{
private readonly ITestOutputHelper? _log;
protected TestCommand(ITestOutputHelper? log)
{
_log = log;
}
internal string? WorkingDirectory { get; set; }
internal List<string> Arguments { get; set; } = new List<string>();
internal List<string> EnvironmentToRemove { get; } = new List<string>();
// These only work via Execute(), not when using GetProcessStartInfo()
internal Action<string>? CommandOutputHandler { get; set; }
internal Action<Process>? ProcessStartedHandler { get; set; }
protected Dictionary<string, string> Environment { get; set; } = new Dictionary<string, string>();
internal TestCommand WithEnvironmentVariable(string name, string value)
{
Environment[name] = value;
return this;
}
internal TestCommand WithEnvironmentVariables(IReadOnlyDictionary<string, string>? variables)
{
if (variables != null)
{
foreach (KeyValuePair<string, string> pair in variables)
{
Environment[pair.Key] = pair.Value;
}
}
return this;
}
internal TestCommand WithWorkingDirectory(string workingDirectory)
{
WorkingDirectory = workingDirectory;
return this;
}
internal TestCommand WithNoUpdateCheck()
{
Arguments.Add("--no-update-check");
return this;
}
internal ProcessStartInfo GetProcessStartInfo(params string[] args)
{
SdkCommandSpec commandSpec = CreateCommandSpec(args);
var psi = commandSpec.ToProcessStartInfo();
return psi;
}
internal CommandResult Execute(params string[] args)
{
IEnumerable<string> enumerableArgs = args;
return Execute(enumerableArgs);
}
internal virtual CommandResult Execute(IEnumerable<string> args)
{
Command command = CreateCommandSpec(args)
.ToCommand()
.CaptureStdOut()
.CaptureStdErr();
if (CommandOutputHandler != null)
{
command.OnOutputLine(CommandOutputHandler);
}
var result = command.Execute(ProcessStartedHandler);
_log?.WriteLine($"> {result.StartInfo.FileName} {result.StartInfo.Arguments}");
_log?.WriteLine(result.StdOut);
if (!string.IsNullOrEmpty(result.StdErr))
{
_log?.WriteLine(string.Empty);
_log?.WriteLine("StdErr:");
_log?.WriteLine(result.StdErr);
}
if (result.ExitCode != 0)
{
_log?.WriteLine($"Exit Code: {result.ExitCode}");
}
return result;
}
private protected abstract SdkCommandSpec CreateCommand(IEnumerable<string> args);
private SdkCommandSpec CreateCommandSpec(IEnumerable<string> args)
{
var commandSpec = CreateCommand(args);
foreach (var kvp in Environment)
{
commandSpec.Environment[kvp.Key] = kvp.Value;
}
foreach (var envToRemove in EnvironmentToRemove)
{
commandSpec.EnvironmentToRemove.Add(envToRemove);
}
if (WorkingDirectory != null)
{
commandSpec.WorkingDirectory = WorkingDirectory;
}
if (Arguments.Any())
{
commandSpec.Arguments = Arguments.Concat(commandSpec.Arguments).ToList();
}
return commandSpec;
}
}
}

Просмотреть файл

@ -0,0 +1,186 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Utilities;
using Microsoft.DotNet.CommandUtils;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
using Microsoft.NET.Build.Containers.IntegrationTests;
namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests;
[Collection("Docker tests")]
public class CreateNewImageTests
{
private ITestOutputHelper _testOutput;
public CreateNewImageTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
}
[Fact]
public void CreateNewImage_Baseline()
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CreateNewImage_Baseline)));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetCommand(_testOutput, "new", "console", "-f", "net7.0")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "publish", "-c", "Release", "-r", "linux-arm64", "--no-self-contained")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
CreateNewImage task = new CreateNewImage();
task.BaseRegistry = "mcr.microsoft.com";
task.BaseImageName = "dotnet/runtime";
task.BaseImageTag = "7.0";
task.OutputRegistry = "localhost:5010";
task.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "Release", "net7.0", "linux-arm64", "publish");
task.ImageName = "dotnet/testimage";
task.ImageTags = new[] { "latest" };
task.WorkingDirectory = "app/";
task.ContainerRuntimeIdentifier = "linux-arm64";
task.Entrypoint = new TaskItem[] { new("dotnet"), new("build") };
task.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
Assert.True(task.Execute());
newProjectDir.Delete(true);
}
[Fact]
public void ParseContainerProperties_EndToEnd()
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(ParseContainerProperties_EndToEnd)));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetCommand(_testOutput, "new", "console", "-f", "net7.0")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "build", "--configuration", "release")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ParseContainerProperties pcp = new ParseContainerProperties();
pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:7.0";
pcp.ContainerRegistry = "localhost:5010";
pcp.ContainerImageName = "dotnet/testimage";
pcp.ContainerImageTags = new[] { "5.0", "latest" };
Assert.True(pcp.Execute());
Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage);
Assert.Equal("7.0", pcp.ParsedContainerTag);
Assert.Equal("dotnet/testimage", pcp.NewContainerImageName);
pcp.NewContainerTags.Should().BeEquivalentTo(new[] { "5.0", "latest" });
CreateNewImage cni = new CreateNewImage();
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.ImageName = pcp.NewContainerImageName;
cni.OutputRegistry = "localhost:5010";
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0");
cni.WorkingDirectory = "app/";
cni.Entrypoint = new TaskItem[] { new("ParseContainerProperties_EndToEnd") };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerRuntimeIdentifier = "linux-x64";
cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
Assert.True(cni.Execute());
newProjectDir.Delete(true);
}
/// <summary>
/// Creates a console app that outputs the environment variable added to the image.
/// </summary>
[Fact]
public void Tasks_EndToEnd_With_EnvironmentVariable_Validation()
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, nameof(Tasks_EndToEnd_With_EnvironmentVariable_Validation)));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
newProjectDir.Create();
new DotnetCommand(_testOutput, "new", "console", "-f", "net7.0")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), $"Console.Write(Environment.GetEnvironmentVariable(\"GoodEnvVar\"));");
new DotnetCommand(_testOutput, "build", "--configuration", "release", "/p:runtimeidentifier=linux-x64")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
ParseContainerProperties pcp = new ParseContainerProperties();
pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:6.0";
pcp.ContainerRegistry = "";
pcp.ContainerImageName = "dotnet/envvarvalidation";
pcp.ContainerImageTag = "latest";
Dictionary<string, string> dict = new Dictionary<string, string>();
dict.Add("Value", "Foo");
pcp.ContainerEnvironmentVariables = new[] { new TaskItem("B@dEnv.Var", dict), new TaskItem("GoodEnvVar", dict) };
Assert.True(pcp.Execute());
Assert.Equal("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.Equal("dotnet/runtime", pcp.ParsedContainerImage);
Assert.Equal("6.0", pcp.ParsedContainerTag);
Assert.Single(pcp.NewContainerEnvironmentVariables);
Assert.Equal("Foo", pcp.NewContainerEnvironmentVariables[0].GetMetadata("Value"));
Assert.Equal("dotnet/envvarvalidation", pcp.NewContainerImageName);
Assert.Equal("latest", pcp.NewContainerTags[0]);
CreateNewImage cni = new CreateNewImage();
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.ImageName = pcp.NewContainerImageName;
cni.OutputRegistry = pcp.NewContainerRegistry;
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0", "linux-x64");
cni.WorkingDirectory = "/app";
cni.Entrypoint = new TaskItem[] { new("/app/Tasks_EndToEnd_With_EnvironmentVariable_Validation") };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables;
cni.ContainerRuntimeIdentifier = "linux-x64";
cni.RuntimeIdentifierGraphPath = ToolsetUtils.GetRuntimeGraphFilePath();
cni.LocalContainerDaemon = global::Microsoft.NET.Build.Containers.KnownDaemonTypes.Docker;
Assert.True(cni.Execute());
new BasicCommand(_testOutput, "docker", "run", "--rm", $"{pcp.NewContainerImageName}:latest")
.Execute()
.Should().Pass()
.And.HaveStdOut("Foo");
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public static class CurrentFile
{
public static string Path([CallerFilePath] string file = "") => file;
public static string Relative(string relative, [CallerFilePath] string file = "") {
return global::System.IO.Path.Combine(global::System.IO.Path.GetDirectoryName(file)!, relative); // file known to be not-null due to the mechanics of CallerFilePath
}
}

Просмотреть файл

@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.CommandUtils;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public class DockerRegistryManager
{
public const string BaseImage = "dotnet/runtime";
public const string BaseImageSource = "mcr.microsoft.com/";
public const string Net6ImageTag = "6.0";
public const string Net7ImageTag = "7.0";
public const string LocalRegistry = "localhost:5010";
public const string FullyQualifiedBaseImageDefault = $"{BaseImageSource}{BaseImage}:{Net6ImageTag}";
private static string? s_registryContainerId;
public static void StartAndPopulateDockerRegistry(ITestOutputHelper testOutput)
{
testOutput.WriteLine("Spawning local registry");
CommandResult processResult = new BasicCommand(testOutput, "docker", "run", "--rm", "--publish", "5010:5000", "--detach", "registry:2").Execute();
processResult.Should().Pass().And.HaveStdOut();
using var reader = new StringReader(processResult.StdOut!);
s_registryContainerId = reader.ReadLine();
foreach (var tag in new[] { Net6ImageTag, Net7ImageTag })
{
new BasicCommand(testOutput, "docker", "pull", $"{BaseImageSource}{BaseImage}:{tag}")
.Execute()
.Should().Pass();
new BasicCommand(testOutput, "docker", "tag", $"{BaseImageSource}{BaseImage}:{tag}", $"{LocalRegistry}/{BaseImage}:{tag}")
.Execute()
.Should().Pass();
new BasicCommand(testOutput, "docker", "push", $"{LocalRegistry}/{BaseImage}:{tag}")
.Execute()
.Should().Pass();
}
}
public static void ShutdownDockerRegistry(ITestOutputHelper testOutput)
{
Assert.NotNull(s_registryContainerId);
new BasicCommand(testOutput, "docker", "stop", s_registryContainerId)
.Execute()
.Should().Pass();
}
}

Просмотреть файл

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
[CollectionDefinition("Docker tests")]
#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
public class DockerTestsCollection : ICollectionFixture<DockerTestsFixture>
#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}

Просмотреть файл

@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.CommandUtils;
using Xunit.Abstractions;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public sealed class DockerTestsFixture : IDisposable
{
private ITestOutputHelper _diagnosticOutput;
public DockerTestsFixture(IMessageSink messageSink)
{
_diagnosticOutput = new SharedTestOutputHelper(messageSink);
DockerRegistryManager.StartAndPopulateDockerRegistry(_diagnosticOutput);
ProjectInitializer.LocateMSBuild();
Directory.CreateDirectory(TestSettings.TestArtifactsDirectory);
}
public void Dispose()
{
DockerRegistryManager.ShutdownDockerRegistry(_diagnosticOutput);
ProjectInitializer.Cleanup();
//clean up tests artifacts
try
{
if (Directory.Exists(TestSettings.TestArtifactsDirectory))
{
Directory.Delete(TestSettings.TestArtifactsDirectory, true);
}
}
catch { }
}
}

Просмотреть файл

@ -0,0 +1,335 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.CommandUtils;
using Microsoft.NET.Build.Containers;
using System.Runtime.CompilerServices;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
[Collection("Docker tests")]
public class EndToEndTests
{
private ITestOutputHelper _testOutput;
public EndToEndTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
}
public static string NewImageName([CallerMemberName] string callerMemberName = "")
{
bool normalized = ContainerHelpers.NormalizeImageName(callerMemberName, out string? normalizedName);
if (!normalized)
{
return normalizedName!;
}
return callerMemberName;
}
[Fact]
public async Task ApiEndToEndWithRegistryPushAndPull()
{
string publishDirectory = BuildLocalApp();
// Build the image
Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry));
Image? x = await registry.GetImageManifest(
DockerRegistryManager.BaseImage,
DockerRegistryManager.Net6ImageTag,
"linux-x64",
ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false);
Assert.NotNull(x);
Layer l = Layer.FromDirectory(publishDirectory, "/app");
x.AddLayer(l);
x.SetEntrypoint(new[] { "/app/MinimalTestApp" });
// Push the image back to the local registry
var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag);
var destinationReference = new ImageReference(registry, NewImageName(), "latest");
await registry.Push(x, sourceReference, destinationReference, Console.WriteLine).ConfigureAwait(false);
// pull it back locally
new BasicCommand(_testOutput, "docker", "pull", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:latest")
.Execute()
.Should().Pass();
// Run the image
new BasicCommand(_testOutput, "docker", "run", "--rm", "--tty", $"{DockerRegistryManager.LocalRegistry}/{NewImageName()}:latest")
.Execute()
.Should().Pass();
}
[Fact]
public async Task ApiEndToEndWithLocalLoad()
{
string publishDirectory = BuildLocalApp();
// Build the image
Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry));
Image? x = await registry.GetImageManifest(
DockerRegistryManager.BaseImage,
DockerRegistryManager.Net6ImageTag,
"linux-x64",
ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false);
Assert.NotNull(x);
Layer l = Layer.FromDirectory(publishDirectory, "/app");
x.AddLayer(l);
x.SetEntrypoint(new[] { "/app/MinimalTestApp" });
// Load the image into the local Docker daemon
var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag);
var destinationReference = new ImageReference(registry, NewImageName(), "latest");
await new LocalDocker(Console.WriteLine).Load(x, sourceReference, destinationReference).ConfigureAwait(false);
// Run the image
new BasicCommand(_testOutput, "docker", "run", "--rm", "--tty", $"{NewImageName()}:latest")
.Execute()
.Should().Pass();
}
private string BuildLocalApp([CallerMemberName] string testName = "TestName", string tfm = "net6.0", string rid = "linux-x64")
{
string workingDirectory = Path.Combine(TestSettings.TestArtifactsDirectory, testName);
DirectoryInfo d = new DirectoryInfo(Path.Combine(workingDirectory, "MinimalTestApp"));
if (d.Exists)
{
d.Delete(recursive: true);
}
Directory.CreateDirectory(workingDirectory);
new DotnetCommand(_testOutput, "new", "console", "-f", tfm, "-o", "MinimalTestApp")
.WithWorkingDirectory(workingDirectory)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "publish", "-bl", "MinimalTestApp", "-r", rid, "-f", tfm)
.WithWorkingDirectory(workingDirectory)
.Execute()
.Should().Pass();
string publishDirectory = Path.Join(workingDirectory, "MinimalTestApp", "bin", "Debug", tfm, rid, "publish");
return publishDirectory;
}
[Fact]
public async Task EndToEnd_NoAPI()
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, "CreateNewImageTest"));
DirectoryInfo privateNuGetAssets = new DirectoryInfo(Path.Combine(TestSettings.TestArtifactsDirectory, "ContainerNuGet"));
if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}
if (privateNuGetAssets.Exists)
{
privateNuGetAssets.Delete(recursive: true);
}
newProjectDir.Create();
privateNuGetAssets.Create();
var repoGlobalJson = Path.Combine("..", "..", "..", "..", "global.json");
File.Copy(repoGlobalJson, Path.Combine(newProjectDir.FullName, "global.json"));
var packagedir = new DirectoryInfo(CurrentFile.Relative("./package"));
// do not pollute the primary/global NuGet package store with the private package(s)
var env = new (string, string)[] { new("NUGET_PACKAGES", privateNuGetAssets.FullName) };
// 🤢
FileInfo[] nupkgs = packagedir.GetFiles("*.nupkg");
if (nupkgs == null || nupkgs.Length == 0)
{
// Build Microsoft.NET.Build.Containers.csproj & wait.
// for now, fail.
Assert.Fail("No nupkg found in expected package folder. You may need to rerun the build");
}
new DotnetCommand(_testOutput, "new", "webapi", "-f", "net7.0")
.WithWorkingDirectory(newProjectDir.FullName)
// do not pollute the primary/global NuGet package store with the private package(s)
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "new", "nugetconfig")
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new DotnetCommand(_testOutput, "nuget", "add", "source", packagedir.FullName, "--name", "local-temp")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
// Add package to the project
new DotnetCommand(_testOutput, "add", "package", "Microsoft.NET.Build.Containers", "--prerelease", "-f", "net7.0")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
string imageName = NewImageName();
string imageTag = "1.0";
// Build & publish the project
new DotnetCommand(
_testOutput,
"publish",
"/p:publishprofile=DefaultContainer",
"/p:runtimeidentifier=linux-x64",
"/bl",
$"/p:ContainerBaseImage={DockerRegistryManager.FullyQualifiedBaseImageDefault}",
$"/p:ContainerRegistry={DockerRegistryManager.LocalRegistry}",
$"/p:ContainerImageName={imageName}",
$"/p:Version={imageTag}")
.WithEnvironmentVariable("NUGET_PACKAGES", privateNuGetAssets.FullName)
.WithWorkingDirectory(newProjectDir.FullName)
.Execute()
.Should().Pass();
new BasicCommand(_testOutput, "docker", "pull", $"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
.Execute()
.Should().Pass();
var containerName = "test-container-1";
CommandResult processResult = new BasicCommand(
_testOutput,
"docker",
"run",
"--rm",
"--name",
containerName,
"--publish",
"5017:80",
"--detach",
$"{DockerRegistryManager.LocalRegistry}/{imageName}:{imageTag}")
.Execute();
processResult.Should().Pass();
Assert.NotNull(processResult.StdOut);
string appContainerId = processResult.StdOut.Trim();
bool everSucceeded = false;
HttpClient client = new();
// Give the server a moment to catch up, but no more than necessary.
for (int retry = 0; retry < 10; retry++)
{
try
{
var response = await client.GetAsync("http://localhost:5017/weatherforecast").ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
everSucceeded = true;
break;
}
}
catch { }
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
new BasicCommand(_testOutput, "docker", "logs", appContainerId)
.Execute()
.Should().Pass();
Assert.True(everSucceeded, "http://localhost:5017/weatherforecast never responded.");
new BasicCommand(_testOutput, "docker", "stop", appContainerId)
.Execute()
.Should().Pass();
newProjectDir.Delete(true);
privateNuGetAssets.Delete(true);
}
// These two are commented because the Github Actions runners don't let us easily configure the Docker Buildx config -
// we need to configure it to allow emulation of other platforms on amd64 hosts before these two will run.
// They do run locally, however.
//[InlineData("linux-arm", false, "/app", "linux/arm/v7")] // packaging framework-dependent because emulating arm on x64 Docker host doesn't work
//[InlineData("linux-arm64", false, "/app", "linux/arm64/v8")] // packaging framework-dependent because emulating arm64 on x64 Docker host doesn't work
// this one should be skipped in all cases because we don't ship linux-x86 runtime packs, so we can't execute the 'apphost' version of the app
//[InlineData("linux-x86", false, "/app", "linux/386")] // packaging framework-dependent because missing runtime packs for x86 linux.
// This one should be skipped because containers can't be configured to run on Linux hosts :(
//[InlineData("win-x64", true, "C:\\app", "windows/amd64")]
// As a result, we only have one actual data-driven test
[InlineData("linux-x64", true, "/app", "linux/amd64")]
[Theory]
public async Task CanPackageForAllSupportedContainerRIDs(string rid, bool isRIDSpecific, string workingDir, string dockerPlatform)
{
string publishDirectory = isRIDSpecific ? BuildLocalApp(tfm: "net7.0", rid: rid) : BuildLocalApp(tfm: "net7.0");
// Build the image
Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.BaseImageSource));
Image? x = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag, rid, ToolsetUtils.GetRuntimeGraphFilePath()).ConfigureAwait(false);
Assert.NotNull(x);
Layer l = Layer.FromDirectory(publishDirectory, "/app");
x.AddLayer(l);
x.WorkingDirectory = workingDir;
var entryPoint = DecideEntrypoint(rid, isRIDSpecific, "MinimalTestApp", workingDir);
x.SetEntrypoint(entryPoint);
// Load the image into the local Docker daemon
var sourceReference = new ImageReference(registry, DockerRegistryManager.BaseImage, DockerRegistryManager.Net7ImageTag);
var destinationReference = new ImageReference(registry, NewImageName(), rid);
await new LocalDocker(Console.WriteLine).Load(x, sourceReference, destinationReference).ConfigureAwait(false);
// Run the image
new BasicCommand(
_testOutput,
"docker",
"run",
"--rm",
"--tty",
"--platform",
dockerPlatform,
$"{NewImageName()}:{rid}")
.Execute()
.Should()
.Pass();
string[] DecideEntrypoint(string rid, bool isRIDSpecific, string appName, string workingDir)
{
var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName;
if (isRIDSpecific)
{
return new[] { $"{workingDir}/{binary}" };
}
else
{
return new[] { "dotnet", $"{workingDir}/{binary}.dll" };
}
}
}
}

Просмотреть файл

@ -0,0 +1,136 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Formats.Tar;
using Microsoft.NET.Build.Containers;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Globalization;
using Xunit.Abstractions;
using Xunit;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public sealed class LayerEndToEndTests : IDisposable
{
private ITestOutputHelper _testOutput;
public LayerEndToEndTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
testSpecificArtifactRoot = new();
priorArtifactRoot = ContentStore.ArtifactRoot;
ContentStore.ArtifactRoot = testSpecificArtifactRoot.Path;
}
[Fact]
public void SingleFileInFolder()
{
using TransientTestFolder folder = new();
string testFilePath = Path.Join(folder.Path, "TestFile.txt");
string testString = $"Test content for {nameof(SingleFileInFolder)}";
File.WriteAllText(testFilePath, testString);
Layer l = Layer.FromDirectory(directory: folder.Path, containerPath: "/app");
Console.WriteLine(l.Descriptor);
//Assert.AreEqual("application/vnd.oci.image.layer.v1.tar", l.Descriptor.MediaType); // TODO: configurability
Assert.True(l.Descriptor.Size is >= 135 and <= 500, $"'l.Descriptor.Size' should be between 135 and 500, but is {l.Descriptor.Size}"); // TODO: determinism!
//Assert.AreEqual("sha256:26140bc75f2fcb3bf5da7d3b531d995c93d192837e37df0eb5ca46e2db953124", l.Descriptor.Digest); // TODO: determinism!
VerifyDescriptorInfo(l);
var allEntries = LoadAllTarEntries(l.BackingFile);
Assert.True(allEntries.TryGetValue("app/", out var appEntryType) && appEntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntryType) && fileEntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
}
[Fact]
public void TwoFilesInTwoFolders()
{
using TransientTestFolder folder = new();
string testFilePath = Path.Join(folder.Path, "TestFile.txt");
string testString = $"Test content for {nameof(TwoFilesInTwoFolders)}";
File.WriteAllText(testFilePath, testString);
using TransientTestFolder folder2 = new();
string testFilePath2 = Path.Join(folder2.Path, "TestFile.txt");
string testString2 = $"Test content 2 for {nameof(TwoFilesInTwoFolders)}";
File.WriteAllText(testFilePath2, testString2);
Layer l = Layer.FromFiles(new[]
{
(testFilePath, "/app/TestFile.txt"),
(testFilePath2, "/app/subfolder/TestFile.txt"),
});
Console.WriteLine(l.Descriptor);
//Assert.AreEqual("application/vnd.oci.image.layer.v1.tar", l.Descriptor.MediaType); // TODO: configurability
Assert.True(l.Descriptor.Size is >= 150 and <= 500, $"'l.Descriptor.Size' should be between 150 and 500, but is {l.Descriptor.Size}"); // TODO: determinism!
//Assert.AreEqual("sha256:26140bc75f2fcb3bf5da7d3b531d995c93d192837e37df0eb5ca46e2db953124", l.Descriptor.Digest); // TODO: determinism!
VerifyDescriptorInfo(l);
var allEntries = LoadAllTarEntries(l.BackingFile);
Assert.True(allEntries.TryGetValue("app/", out var appEntryType) && appEntryType == TarEntryType.Directory, "Missing app directory entry");
Assert.True(allEntries.TryGetValue("app/TestFile.txt", out var fileEntryType) && fileEntryType == TarEntryType.RegularFile, "Missing TestFile.txt file entry");
Assert.True(allEntries.TryGetValue("app/subfolder/", out var subfolderType) && subfolderType == TarEntryType.Directory, "Missing subfolder directory entry");
Assert.True(allEntries.TryGetValue("app/subfolder/TestFile.txt", out var subfolderFileEntryType) && subfolderFileEntryType == TarEntryType.RegularFile, "Missing subfolder/TestFile.txt file entry");
}
private static void VerifyDescriptorInfo(Layer l)
{
Assert.Equal(l.Descriptor.Size, new FileInfo(l.BackingFile).Length);
byte[] hashBytes;
byte[] uncompressedHashBytes;
using (FileStream fs = File.OpenRead(l.BackingFile))
{
hashBytes = SHA256.HashData(fs);
fs.Position = 0;
using (GZipStream decompressionStream = new GZipStream(fs, CompressionMode.Decompress))
{
uncompressedHashBytes = SHA256.HashData(decompressionStream);
}
}
Assert.Equal(Convert.ToHexString(hashBytes), l.Descriptor.Digest.Substring("sha256:".Length), ignoreCase: true);
Assert.Equal(Convert.ToHexString(uncompressedHashBytes), l.Descriptor.UncompressedDigest?.Substring("sha256:".Length), ignoreCase: true);
}
TransientTestFolder? testSpecificArtifactRoot;
string? priorArtifactRoot;
public void Dispose()
{
testSpecificArtifactRoot?.Dispose();
if (priorArtifactRoot is not null)
{
ContentStore.ArtifactRoot = priorArtifactRoot;
}
}
private static Dictionary<string, TarEntryType> LoadAllTarEntries(string file)
{
using var gzip = new GZipStream(File.OpenRead(file), CompressionMode.Decompress);
using var tar = new TarReader(gzip);
var entries = new Dictionary<string, TarEntryType>();
TarEntry? entry;
while ((entry = tar.GetNextEntry()) != null)
{
entries[entry.Name] = entry.EntryType;
}
return entries;
}
}

Просмотреть файл

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" />
<PackageReference Include="Microsoft.Build" />
<PackageReference Include="Microsoft.Build.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,101 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using FluentAssertions;
using Microsoft.NET.Build.Containers.IntegrationTests;
using Xunit;
using static Microsoft.NET.Build.Containers.KnownStrings;
using static Microsoft.NET.Build.Containers.KnownStrings.Properties;
namespace Microsoft.NET.Build.Containers.Tasks.IntegrationTests;
[Collection("Docker tests")]
public class ParseContainerPropertiesTests
{
[Fact]
public void Baseline()
{
var (project, _) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTags] = "7.0;latest"
});
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));
Assert.Equal("mcr.microsoft.com", instance.GetPropertyValue(ContainerBaseRegistry));
Assert.Equal("dotnet/runtime", instance.GetPropertyValue(ContainerBaseName));
Assert.Equal("7.0", instance.GetPropertyValue(ContainerBaseTag));
Assert.Equal("dotnet/testimage", instance.GetPropertyValue(ContainerImageName));
instance.GetItems(ContainerImageTags).Select(i => i.EvaluatedInclude).ToArray().Should().BeEquivalentTo(new[] { "7.0", "latest" });
instance.GetItems("ProjectCapability").Select(i => i.EvaluatedInclude).ToArray().Should().BeEquivalentTo(new[] { "NetSdkOCIImageBuild" });
}
[Fact]
public void SpacesGetReplacedWithDashes()
{
var (project, _) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr microsoft com/dotnet runtime:7.0",
[ContainerRegistry] = "localhost:5010"
});
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, null, null, out var outputs));
Assert.Equal("mcr-microsoft-com",instance.GetPropertyValue(ContainerBaseRegistry));
Assert.Equal("dotnet-runtime", instance.GetPropertyValue(ContainerBaseName));
Assert.Equal("7.0", instance.GetPropertyValue(ContainerBaseTag));
}
[Fact]
public void RegexCatchesInvalidContainerNames()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet testimage",
[ContainerImageTag] = "5.0"
});
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.True(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
Assert.Contains(logs.Messages, m => m.Code == ErrorCodes.CONTAINER001 && m.Importance == global::Microsoft.Build.Framework.MessageImportance.High);
}
[Fact]
public void RegexCatchesInvalidContainerTags()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTag] = "5 0"
});
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
Assert.True(logs.Errors.Count > 0);
Assert.Equal(logs.Errors[0].Code, ErrorCodes.CONTAINER004);
}
[Fact]
public void CanOnlySupplyOneOfTagAndTags()
{
var (project, logs) = ProjectInitializer.InitProject(new () {
[ContainerBaseImage] = "mcr.microsoft.com/dotnet/runtime:7.0",
[ContainerRegistry] = "localhost:5010",
[ContainerImageName] = "dotnet/testimage",
[ContainerImageTag] = "5.0",
[ContainerImageTags] = "latest;oldest"
});
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
Assert.False(instance.Build(new[]{ComputeContainerConfig}, new [] { logs }, null, out var outputs));
Assert.True(logs.Errors.Count > 0);
Assert.Equal(logs.Errors[0].Code, ErrorCodes.CONTAINER005);
}
}

Просмотреть файл

@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Xunit;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
public static class ProjectInitializer {
private static string? CombinedTargetsLocation;
private static string CombineFiles(string propsFile, string targetsFile)
{
var propsContent = File.ReadAllLines(propsFile);
var targetsContent = File.ReadAllLines(targetsFile);
var combinedContent = new List<string>();
combinedContent.AddRange(propsContent[..^1]);
combinedContent.AddRange(targetsContent[1..]);
var tempTargetLocation = Path.Combine(Path.GetTempPath(), "Containers", "Microsoft.NET.Build.Containers.targets");
string? directoryName = Path.GetDirectoryName(tempTargetLocation);
Assert.NotNull(directoryName);
Directory.CreateDirectory(directoryName);
File.WriteAllLines(tempTargetLocation, combinedContent);
return tempTargetLocation;
}
public static void LocateMSBuild()
{
var relativePath = Path.Combine("..", "packaging", "build", "Microsoft.NET.Build.Containers.targets");
var targetsFile = CurrentFile.Relative(relativePath);
var propsFile = Path.ChangeExtension(targetsFile, ".props");
CombinedTargetsLocation = CombineFiles(propsFile, targetsFile);
}
public static void Cleanup()
{
if (CombinedTargetsLocation != null) File.Delete(CombinedTargetsLocation);
}
public static (Project, CapturingLogger) InitProject(Dictionary<string, string> bonusProps, [CallerMemberName]string projectName = "")
{
var props = new Dictionary<string, string>();
// required parameters
props["TargetFileName"] = "foo.dll";
props["AssemblyName"] = "foo";
props["_TargetFrameworkVersionWithoutV"] = "7.0";
props["_NativeExecutableExtension"] = ".exe"; //TODO: windows/unix split here
props["Version"] = "1.0.0"; // TODO: need to test non-compliant version strings here
props["NetCoreSdkVersion"] = "7.0.100"; // TODO: float this to current SDK?
// test setup parameters so that we can load the props/targets/tasks
props["ContainerCustomTasksAssembly"] = Path.GetFullPath(Path.Combine(".", "Microsoft.NET.Build.Containers.dll"));
props["_IsTest"] = "true";
var safeBinlogFileName = projectName.Replace(" ", "_").Replace(":", "_").Replace("/", "_").Replace("\\", "_").Replace("*", "_");
var loggers = new List<ILogger>
{
new global::Microsoft.Build.Logging.BinaryLogger() {CollectProjectImports = global::Microsoft.Build.Logging.BinaryLogger.ProjectImportsCollectionMode.Embed, Verbosity = LoggerVerbosity.Diagnostic, Parameters = $"LogFile={safeBinlogFileName}.binlog" },
new global::Microsoft.Build.Logging.ConsoleLogger(LoggerVerbosity.Detailed)
};
CapturingLogger logs = new CapturingLogger();
loggers.Add(logs);
var collection = new ProjectCollection(null, loggers, ToolsetDefinitionLocations.Default);
foreach (var kvp in bonusProps)
{
props[kvp.Key] = kvp.Value;
}
return (collection.LoadProject(CombinedTargetsLocation, props, null), logs);
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
[Collection("Docker tests")]
public class RegistryTests
{
[Fact]
public async Task GetFromRegistry()
{
Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(DockerRegistryManager.LocalRegistry));
var ridgraphfile = ToolsetUtils.GetRuntimeGraphFilePath();
// Don't need rid graph for local registry image pulls - since we're only pushing single image manifests (not manifest lists)
// as part of our setup, we could put literally anything in here. The file at the passed-in path would only get read when parsing manifests lists.
Image? downloadedImage = await registry.GetImageManifest(DockerRegistryManager.BaseImage, DockerRegistryManager.Net6ImageTag, "linux-x64", ridgraphfile).ConfigureAwait(false);
Assert.NotNull(downloadedImage);
}
}

Просмотреть файл

@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using static Microsoft.NET.Build.Containers.KnownStrings.Properties;
using FluentAssertions;
using Microsoft.Build.Execution;
using Xunit;
using Microsoft.NET.Build.Containers.IntegrationTests;
namespace Microsoft.NET.Build.Containers.Targets.IntegrationTests;
[Collection("Docker tests")]
public class TargetsTests
{
[InlineData(true, "/app/foo.exe")]
[InlineData(false, "dotnet", "/app/foo.dll")]
[Theory]
public void CanSetEntrypointArgsToUseAppHost(bool useAppHost, params string[] entrypointArgs)
{
var (project, _) = ProjectInitializer.InitProject(new()
{
[UseAppHost] = useAppHost.ToString()
}, projectName: $"{nameof(CanSetEntrypointArgsToUseAppHost)}_{useAppHost}_{String.Join("_", entrypointArgs)}");
Assert.True(project.Build(ComputeContainerConfig));
var computedEntrypointArgs = project.GetItems(ContainerEntrypoint).Select(i => i.EvaluatedInclude).ToArray();
foreach (var (First, Second) in entrypointArgs.Zip(computedEntrypointArgs))
{
Assert.Equal(First, Second);
}
}
[InlineData("WebApplication44", "webapplication44", true)]
[InlineData("friendly-suspicious-alligator", "friendly-suspicious-alligator", true)]
[InlineData("*friendly-suspicious-alligator", "", false)]
[InlineData("web/app2+7", "web/app2-7", true)]
[InlineData("Microsoft.Apps.Demo.ContosoWeb", "microsoft-apps-demo-contosoweb", true)]
[Theory]
public void CanNormalizeInputContainerNames(string projectName, string expectedContainerImageName, bool shouldPass)
{
var (project, _) = ProjectInitializer.InitProject(new()
{
[AssemblyName] = projectName
}, projectName: $"{nameof(CanNormalizeInputContainerNames)}_{projectName}_{expectedContainerImageName}_{shouldPass}");
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().Be(shouldPass, "Build should have succeeded");
Assert.Equal(expectedContainerImageName, instance.GetPropertyValue(ContainerImageName));
}
[InlineData("7.0.100", true)]
[InlineData("8.0.100", true)]
[InlineData("7.0.100-preview.7", true)]
[InlineData("7.0.100-rc.1", true)]
[InlineData("6.0.100", false)]
[InlineData("7.0.100-preview.1", false)]
[Theory]
public void CanWarnOnInvalidSDKVersions(string sdkVersion, bool isAllowed)
{
var (project, _) = ProjectInitializer.InitProject(new()
{
["NETCoreSdkVersion"] = sdkVersion,
["PublishProfile"] = "DefaultContainer"
}, projectName: $"{nameof(CanWarnOnInvalidSDKVersions)}_{sdkVersion}_{isAllowed}");
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
var derivedIsAllowed = Boolean.Parse(project.GetProperty("_IsSDKContainerAllowedVersion").EvaluatedValue);
// var buildResult = instance.Build(new[]{"_ContainerVerifySDKVersion"}, null, null, out var outputs);
derivedIsAllowed.Should().Be(isAllowed, $"SDK version {(isAllowed ? "should" : "should not")} have been allowed ");
}
[InlineData(true)]
[InlineData(false)]
[Theory]
public void GetsConventionalLabelsByDefault(bool shouldEvaluateLabels)
{
var (project, _) = ProjectInitializer.InitProject(new()
{
[ContainerGenerateLabels] = shouldEvaluateLabels.ToString()
}, projectName: $"{nameof(GetsConventionalLabelsByDefault)}_{shouldEvaluateLabels}");
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
if (shouldEvaluateLabels)
{
instance.GetItems(ContainerLabel).Should().NotBeEmpty("Should have evaluated some labels by default");
}
else
{
instance.GetItems(ContainerLabel).Should().BeEmpty("Should not have evaluated any labels by default");
}
}
private static bool LabelMatch(string label, string value, ProjectItemInstance item) => item.EvaluatedInclude == label && item.GetMetadata("Value") is { } v && v.EvaluatedValue == value;
[InlineData(true)]
[InlineData(false)]
[Theory]
public void ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn(bool includeSourceControl)
{
var commitHash = "abcdef";
var repoUrl = "https://git.cosmere.com/shard/whimsy.git";
var (project, _) = ProjectInitializer.InitProject(new()
{
["PublishRepositoryUrl"] = includeSourceControl.ToString(),
["PrivateRepositoryUrl"] = repoUrl,
["SourceRevisionId"] = commitHash
}, projectName: $"{nameof(ShouldNotIncludeSourceControlLabelsUnlessUserOptsIn)}_{includeSourceControl}");
var instance = project.CreateProjectInstance(global::Microsoft.Build.Execution.ProjectInstanceSettings.None);
instance.Build(new[] { ComputeContainerConfig }, null, null, out var outputs).Should().BeTrue("Build should have succeeded");
var labels = instance.GetItems(ContainerLabel);
if (includeSourceControl)
{
labels.Should().NotBeEmpty("Should have evaluated some labels by default")
.And.ContainSingle(label => LabelMatch("org.opencontainers.image.source", repoUrl, label))
.And.ContainSingle(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ;
}
else
{
labels.Should().NotBeEmpty("Should have evaluated some labels by default")
.And.NotContain(label => LabelMatch("org.opencontainers.image.source", repoUrl, label))
.And.NotContain(label => LabelMatch("org.opencontainers.image.revision", commitHash, label)); ;
};
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
internal static class TestSettings
{
/// <summary>
/// Gets temporary location for test artifacts.
/// </summary>
internal static string TestArtifactsDirectory { get; } = Path.Combine(Path.GetTempPath(), "ContainersTests", DateTime.Now.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture));
}

Просмотреть файл

@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using Xunit;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
internal static class ToolsetUtils
{
/// <summary>
/// Returns the path with installed dotnet.
/// Order of processing:
/// - DOTNET_ROOT environment variable
/// - DOTNET_INSTALL_DIR environment variable
/// - resolving dotnet executable path.
/// </summary>
/// <exception cref="InvalidOperationException">on unexpected environment result, i.e. common environment variables are not set.</exception>
internal static string GetDotNetPath()
{
string? dotnetRootFromEnvironment = Environment.GetEnvironmentVariable("DOTNET_ROOT");
string? dotnetInstallDirFromEnvironment = Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR");
if (!string.IsNullOrEmpty(dotnetRootFromEnvironment) && Directory.Exists(dotnetRootFromEnvironment))
{
return dotnetRootFromEnvironment;
}
else if (!string.IsNullOrEmpty(dotnetInstallDirFromEnvironment) && Directory.Exists(dotnetInstallDirFromEnvironment))
{
return dotnetInstallDirFromEnvironment;
}
string dotnetExePath = ResolveCommand("dotnet");
string dotnetRoot = Path.GetDirectoryName(dotnetExePath) ?? throw new InvalidOperationException("dotnet executable is in the root?");
if (Directory.Exists(dotnetInstallDirFromEnvironment))
{
Assert.False(true, "'dotnet' was not found.");
}
return dotnetRoot;
}
/// <summary>
/// Gets path to RuntimeIdentifierGraph.json file.
/// </summary>
/// <returns></returns>
internal static string GetRuntimeGraphFilePath()
{
string dotnetRoot = GetDotNetPath();
DirectoryInfo sdksDir = new(Path.Combine(dotnetRoot, "sdk"));
var lastWrittenSdk = sdksDir.EnumerateDirectories().OrderByDescending(di => di.LastWriteTime).First();
return lastWrittenSdk.GetFiles("RuntimeIdentifierGraph.json").Single().FullName;
}
private static string ResolveCommand(string command)
{
char pathSplitChar;
string[] extensions = new string[] { string.Empty };
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string? pathExt = Environment.GetEnvironmentVariable("PATHEXT") ?? throw new InvalidOperationException("PATHEXT is not set.");
pathSplitChar = ';';
extensions = extensions
.Concat(pathExt.Split(pathSplitChar))
.ToArray();
}
else
{
pathSplitChar = ':';
}
string? path = Environment.GetEnvironmentVariable("PATH") ?? throw new InvalidOperationException("PATH is not set.");
var paths = path.Split(pathSplitChar);
string? result = extensions.SelectMany(ext => paths.Select(p => Path.Combine(p, command + ext)))
.FirstOrDefault(File.Exists);
return result ?? throw new InvalidOperationException("Could not resolve path to " + command);
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using static System.IO.Path;
namespace Microsoft.NET.Build.Containers.IntegrationTests;
/// <summary>
/// Helper class to clean up after tests that touch the filesystem.
/// </summary>
internal sealed class TransientTestFolder : IDisposable
{
public readonly string Path = Combine(TestSettings.TestArtifactsDirectory, GetRandomFileName());
public readonly DirectoryInfo DirectoryInfo;
public TransientTestFolder()
{
DirectoryInfo = Directory.CreateDirectory(Path);
}
public void Dispose()
{
Directory.Delete(Path, recursive: true);
}
}

Просмотреть файл

@ -0,0 +1,112 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit;
namespace Microsoft.NET.Build.Containers.UnitTests;
public class ContainerHelpersTests
{
[Theory]
// Valid Tests
[InlineData("mcr.microsoft.com", true)]
[InlineData("mcr.microsoft.com:5001", true)] // Registries can have ports
[InlineData("docker.io", true)] // default docker registry is considered valid
// // Invalid tests
[InlineData("mcr.mi-=crosoft.com", false)] // invalid url
[InlineData("mcr.microsoft.com/", false)] // invalid url
public void IsValidRegistry(string registry, bool expectedReturn)
{
Console.WriteLine($"Domain pattern is '{ReferenceParser.AnchoredDomainRegexp.ToString()}'");
Assert.Equal(expectedReturn, ContainerHelpers.IsValidRegistry(registry));
}
[Theory]
[InlineData("mcr.microsoft.com/dotnet/runtime:6.0", true, "mcr.microsoft.com", "dotnet/runtime", "6.0")]
[InlineData("mcr.microsoft.com/dotnet/runtime", true, "mcr.microsoft.com", "dotnet/runtime", null)]
[InlineData("mcr.microsoft.com/", false, null, null, null)] // no image = nothing resolves
// Ports tag along
[InlineData("mcr.microsoft.com:54/dotnet/runtime", true, "mcr.microsoft.com:54", "dotnet/runtime", null)]
// Even if nonsensical
[InlineData("mcr.microsoft.com:0/dotnet/runtime", true, "mcr.microsoft.com:0", "dotnet/runtime", null)]
// We don't allow hosts with missing ports when a port is anticipated
[InlineData("mcr.microsoft.com:/dotnet/runtime", false, null, null, null)]
[InlineData("ubuntu:jammy", true, ContainerHelpers.DefaultRegistry, "ubuntu", "jammy")]
public void TryParseFullyQualifiedContainerName(string fullyQualifiedName, bool expectedReturn, string expectedRegistry, string expectedImage, string expectedTag)
{
Assert.Equal(expectedReturn, ContainerHelpers.TryParseFullyQualifiedContainerName(fullyQualifiedName, out string? containerReg, out string? containerName, out string? containerTag, out string? containerDigest));
Assert.Equal(expectedRegistry, containerReg);
Assert.Equal(expectedImage, containerName);
Assert.Equal(expectedTag, containerTag);
}
[Theory]
[InlineData("dotnet/runtime", true)]
[InlineData("foo/bar", true)]
[InlineData("registry", true)]
[InlineData("-foo/bar", false)]
[InlineData(".foo/bar", false)]
[InlineData("_foo/bar", false)]
[InlineData("foo/bar-", false)]
[InlineData("foo/bar.", false)]
[InlineData("foo/bar_", false)]
public void IsValidImageName(string imageName, bool expectedReturn)
{
Assert.Equal(expectedReturn, ContainerHelpers.IsValidImageName(imageName));
}
[Theory]
[InlineData("6.0", true)] // baseline
[InlineData("5.2-asd123", true)] // with commit hash
[InlineData(".6.0", false)] // starts with .
[InlineData("-6.0", false)] // starts with -
[InlineData("---", false)] // malformed
public void IsValidImageTag(string imageTag, bool expectedReturn)
{
Assert.Equal(expectedReturn, ContainerHelpers.IsValidImageTag(imageTag));
}
[Fact]
public void IsValidImageTag_InvalidLength()
{
Assert.False(ContainerHelpers.IsValidImageTag(new string('a', 129)));
}
[Theory]
[InlineData("80/tcp", true, 80, PortType.tcp, null)]
[InlineData("80", true, 80, PortType.tcp, null)]
[InlineData("125/dup", false, 125, PortType.tcp, ContainerHelpers.ParsePortError.InvalidPortType)]
[InlineData("invalidNumber", false, null, null, ContainerHelpers.ParsePortError.InvalidPortNumber)]
[InlineData("welp/unknowntype", false, null, null, (ContainerHelpers.ParsePortError)3)]
[InlineData("a/b/c", false, null, null, ContainerHelpers.ParsePortError.UnknownPortFormat)]
[InlineData("/tcp", false, null, null, ContainerHelpers.ParsePortError.MissingPortNumber)]
public void CanParsePort(string input, bool shouldParse, int? expectedPortNumber, PortType? expectedType, ContainerHelpers.ParsePortError? expectedError) {
var parseSuccess = ContainerHelpers.TryParsePort(input, out var port, out var errors);
Assert.Equal(shouldParse, parseSuccess);
if (shouldParse) {
Assert.NotNull(port);
Assert.Equal(port.number, expectedPortNumber);
Assert.Equal(port.type, expectedType);
} else {
Assert.Null(port);
Assert.NotNull(errors);
Assert.Equal(expectedError, errors);
}
}
[Theory]
[InlineData("FOO", true)]
[InlineData("foo_bar", true)]
[InlineData("foo-bar", false)]
[InlineData("foo.bar", false)]
[InlineData("foo bar", false)]
[InlineData("1_NAME", false)]
[InlineData("ASPNETCORE_URLS", true)]
[InlineData("ASPNETCORE_URLS2", true)]
public void CanRecognizeEnvironmentVariableNames(string envVarName, bool isValid) {
var success = ContainerHelpers.IsValidEnvironmentVariable(envVarName);
Assert.Equal(isValid, success);
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Xunit;
namespace Microsoft.NET.Build.Containers.UnitTests;
public class DescriptorTests
{
[Fact]
public void BasicConstructor()
{
Descriptor d = new(
mediaType: "application/vnd.oci.image.manifest.v1+json",
digest: "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
size: 7682);
Console.WriteLine(JsonSerializer.Serialize(d, new JsonSerializerOptions { WriteIndented = true }));
Assert.Equal("application/vnd.oci.image.manifest.v1+json", d.MediaType);
Assert.Equal("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", d.Digest);
Assert.Equal(7_682, d.Size);
Assert.Null(d.Annotations);
Assert.Null(d.Data);
Assert.Null(d.Urls);
}
}

Просмотреть файл

@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit;
namespace Microsoft.NET.Build.Containers.UnitTests;
public class DockerDaemonTests
{
[Fact]
public async Task Can_detect_when_no_daemon_is_running() {
// mimic no daemon running by setting the DOCKER_HOST to a nonexistent socket
try {
System.Environment.SetEnvironmentVariable("DOCKER_HOST", "tcp://123.123.123.123:12345");
var available = await new LocalDocker(Console.WriteLine).IsAvailable().ConfigureAwait(false);
Assert.False(available, "No daemon should be listening at that port");
} finally {
System.Environment.SetEnvironmentVariable("DOCKER_HOST", null);
}
}
[Fact]
public async Task Can_detect_when_daemon_is_running() {
var available = await new LocalDocker(Console.WriteLine).IsAvailable().ConfigureAwait(false);
Assert.True(available, "Should have found a working daemon");
}
}

Просмотреть файл

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="GitHubActionsTestLogger" PrivateAssets="all" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.NET.Build.Containers\Microsoft.NET.Build.Containers.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Xunit;
namespace Microsoft.NET.Build.Containers.UnitTests
{
public class RegistryTests
{
[InlineData("public.ecr.aws", true)]
[InlineData("123412341234.dkr.ecr.us-west-2.amazonaws.com", true)]
[InlineData("123412341234.dkr.ecr-fips.us-west-2.amazonaws.com", true)]
[InlineData("notvalid.dkr.ecr.us-west-2.amazonaws.com", false)]
[InlineData("1111.dkr.ecr.us-west-2.amazonaws.com", false)]
[InlineData("mcr.microsoft.com", false)]
[InlineData("localhost", false)]
[InlineData("hub", false)]
[Theory]
public void CheckIfAmazonECR(string registryName, bool isECR)
{
Registry registry = new Registry(ContainerHelpers.TryExpandRegistryToUri(registryName));
Assert.Equal(isECR, registry.IsAmazonECRRegistry);
}
}
}

Просмотреть файл

@ -70,6 +70,6 @@
<!-- Hacky workaround for the fact that we don't publish the package yet. -->
<Target Name="CopyNupkgToCustomFolder" AfterTargets="Pack">
<Copy SourceFiles="$(OutDir)../Microsoft.NET.Build.Containers.$(Version).nupkg"
DestinationFiles="../Test.Microsoft.NET.Build.Containers.Filesystem/package/Microsoft.NET.Build.Containers.$(Version).nupkg" />
DestinationFiles="../Microsoft.NET.Build.Containers.IntegrationTests/package/Microsoft.NET.Build.Containers.$(Version).nupkg" />
</Target>
</Project>