Skip to content

Commit

Permalink
Improve System Test performance with stubbing dotnet new (#294)
Browse files Browse the repository at this point in the history
* first attempt to improve "dotnet new classlib"

* improve "dotnet new sln"

* stub "dotnet sln add"

* try fixing linux build issue

* Fix linux build

* Ensure dotnet command is executed with the corresponding sdk version

* code cleanup

* fix Reqnroll.TestProjectGenerator.Tests

* Add  --no-restore for new classlib

---------

Co-authored-by: obligaron <[email protected]>
  • Loading branch information
gasparnagy and obligaron authored Nov 6, 2024
1 parent 7c22e2f commit 4eed410
Show file tree
Hide file tree
Showing 17 changed files with 285 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ protected BaseCommandBuilder(IOutputWriter outputWriter)
_outputWriter = outputWriter;
}

public CommandBuilder Build()
public virtual CommandBuilder Build()
{
return new CommandBuilder(_outputWriter, ExecutablePath, BuildArguments(), GetWorkingDirectory());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Reqnroll.TestProjectGenerator.FilesystemWriter;
using System;
using System.Collections.Concurrent;
using System.IO;

namespace Reqnroll.TestProjectGenerator.Dotnet;

public class CacheAndCopyCommandBuilder : CommandBuilder
{
private const string TemplateName = "TName";
private readonly NetCoreSdkInfo _sdk;
private readonly CommandBuilder _baseCommandBuilder;
private readonly string _targetPath;
private readonly string _nameToReplace;
private static readonly ConcurrentDictionary<string, object> LockObjects = new();

public CacheAndCopyCommandBuilder(IOutputWriter outputWriter, NetCoreSdkInfo sdk, CommandBuilder baseCommandBuilder, string targetPath, string nameToReplace = null)
: base(outputWriter, baseCommandBuilder.ExecutablePath, baseCommandBuilder.ArgumentsFormat, baseCommandBuilder.WorkingDirectory)
{
_sdk = sdk;
_baseCommandBuilder = baseCommandBuilder;
_targetPath = targetPath;
_nameToReplace = nameToReplace;
}

private string CalculateCacheTargetPath(string suffix = "")
{
var targetPathInfo = new DirectoryInfo(_targetPath);
var directoryName = targetPathInfo.Name;
string argsCleaned = ArgumentsFormat.Replace(_targetPath, "").Replace(" ", "").Replace("\"", "").Replace("/", "") + directoryName;
if (_nameToReplace != null)
{
argsCleaned = argsCleaned.Replace(_nameToReplace, TemplateName);
directoryName = directoryName.Replace(_nameToReplace, TemplateName);
}

var sdkSpecifier = _sdk == null ? "" : $"_{_sdk.Version}";
return Path.Combine(Path.GetTempPath(), "RRC", $"RRC{sdkSpecifier}_{argsCleaned}{suffix}", directoryName);
}

public override CommandResult Execute(Func<Exception, Exception> exceptionFunction)
{
var cachePath = CalculateCacheTargetPath();

CommandResult originalResult = null;
if (!Directory.Exists(cachePath))
{
LockObjects.TryAdd(cachePath, new object());

lock (LockObjects[cachePath])
{
if (!Directory.Exists(cachePath))
{
var tempPath = CalculateCacheTargetPath($"-tmp{Guid.NewGuid():N}");
var arguments = ArgumentsFormat.Replace(_targetPath, tempPath);
if (_nameToReplace != null) arguments = arguments.Replace(_nameToReplace, TemplateName);
var commandBuilder = new CommandBuilder(_outputWriter, ExecutablePath, arguments, WorkingDirectory);

originalResult = commandBuilder.Execute(exceptionFunction);
try
{
if (!Directory.Exists(cachePath))
Directory.Move(Path.Combine(tempPath, ".."), Path.Combine(cachePath, ".."));
}
catch (IOException ex)
{
_outputWriter.WriteLine($"Unable to move TMP to CACHE: {ex.Message}");
}
try
{
if (Directory.Exists(tempPath))
Directory.Delete(Path.Combine(tempPath, ".."), true);
}
catch (IOException ex)
{
_outputWriter.WriteLine($"Unable to delete TMP: {ex.Message}");
}
}
}
}

var copyFolderCommandBuilder = new CopyFolderCommandBuilder(_outputWriter, cachePath, _targetPath, TemplateName, _nameToReplace);
var copyFolderResult = copyFolderCommandBuilder.Execute(exceptionFunction);
return originalResult == null ? copyFolderResult :
new CommandResult(originalResult.ExitCode, originalResult.ConsoleOutput + Environment.NewLine + copyFolderResult.ConsoleOutput);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ namespace Reqnroll.TestProjectGenerator.Dotnet
{
public class CommandBuilder
{
private readonly IOutputWriter _outputWriter;
private readonly string _workingDirectory;
protected readonly IOutputWriter _outputWriter;

public CommandBuilder(IOutputWriter outputWriter, string executablePath, string argumentsFormat, string workingDirectory)
{
_outputWriter = outputWriter;
_workingDirectory = workingDirectory;
WorkingDirectory = workingDirectory;
ExecutablePath = executablePath;
ArgumentsFormat = argumentsFormat;
}

public string ArgumentsFormat { get; }
public string ExecutablePath { get; }
public string WorkingDirectory { get; }

public CommandResult ExecuteWithRetry(int times, TimeSpan interval, Func<Exception, Exception> exceptionFunction)
{
Expand All @@ -45,11 +45,11 @@ public CommandResult Execute()
return Execute(innerException => new Exception($"Error while executing {ExecutablePath} {ArgumentsFormat}", innerException));
}

public CommandResult Execute(Func<Exception, Exception> exceptionFunction)
public virtual CommandResult Execute(Func<Exception, Exception> exceptionFunction)
{
var solutionCreateProcessHelper = new ProcessHelper();

var processResult = solutionCreateProcessHelper.RunProcess(_outputWriter, _workingDirectory, ExecutablePath, ArgumentsFormat);
var processResult = solutionCreateProcessHelper.RunProcess(_outputWriter, WorkingDirectory, ExecutablePath, ArgumentsFormat);
if (processResult.ExitCode != 0)
{
var innerException = new Exception(processResult.CombinedOutput);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.IO;

namespace Reqnroll.TestProjectGenerator.Dotnet;
public class CopyFolderCommandBuilder : CommandBuilder
{
private readonly string _replaceFrom;
private readonly string _replaceTo;

public CopyFolderCommandBuilder(IOutputWriter outputWriter, string sourceFolder, string targetFolder, string replaceFrom = null, string replaceTo = null)
: base(outputWriter, "[copy folder]", sourceFolder, targetFolder)
{
_replaceFrom = replaceFrom;
_replaceTo = replaceTo;
}

private string ReplaceName(string value)
{
if (_replaceFrom == null || _replaceTo == null) return value;
return value.Replace(_replaceFrom, _replaceTo);
}

private void CopyDirectoryRecursively(DirectoryInfo source, DirectoryInfo target)
{
Directory.CreateDirectory(target.FullName);

// Copy each file into the new directory.
foreach (FileInfo fi in source.GetFiles())
{
string fiName = ReplaceName(fi.Name);
_outputWriter.WriteLine(@"Copying to {0}\{1}", target.FullName, fiName);
fi.CopyTo(Path.Combine(target.FullName, fiName), true);
}

// Copy each subdirectory using recursion.
foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
{
DirectoryInfo nextTargetSubDir =
target.CreateSubdirectory(diSourceSubDir.Name);
CopyDirectoryRecursively(diSourceSubDir, nextTargetSubDir);
}
}

public override CommandResult Execute(Func<Exception, Exception> exceptionFunction)
{
var sourceFolder = ArgumentsFormat;
var targetFolder = WorkingDirectory;

try
{
_outputWriter.WriteLine($"Copying '{sourceFolder}' to '{targetFolder}'...");

CopyDirectoryRecursively(new DirectoryInfo(sourceFolder), new DirectoryInfo(targetFolder));

_outputWriter.WriteLine("Copying done.");

return new CommandResult(0, $"Copied '{sourceFolder}' to '{targetFolder}'.");
}
catch (Exception ex)
{
throw exceptionFunction(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Reqnroll.TestProjectGenerator.FilesystemWriter;

namespace Reqnroll.TestProjectGenerator.Dotnet
{
public class DotNet
{
public static NewCommandBuilder New(IOutputWriter outputWriter) => NewCommandBuilder.Create(outputWriter);
public static NewCommandBuilder New(IOutputWriter outputWriter, NetCoreSdkInfo sdk) => NewCommandBuilder.Create(outputWriter, sdk);
public static BuildCommandBuilder Build(IOutputWriter outputWriter) => BuildCommandBuilder.Create(outputWriter);
public static SolutionCommandBuilder Sln(IOutputWriter outputWriter) => SolutionCommandBuilder.Create(outputWriter);
public static VersionCommandBuilder Version(IOutputWriter outputWriter) => VersionCommandBuilder.Create(outputWriter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ public partial class NewCommandBuilder
{
public class NewProjectCommandBuilder : BaseCommandBuilder
{
private string _templateName = "classlib";
private string _name = "ClassLib";
private string _folder;
private ProgrammingLanguage _language = ProgrammingLanguage.CSharp;
protected string _templateName = "classlib";
protected string _name = "ClassLib";
protected string _folder;
protected ProgrammingLanguage _language = ProgrammingLanguage.CSharp;


public NewProjectCommandBuilder(IOutputWriter outputWriter) : base(outputWriter)
Expand Down Expand Up @@ -47,7 +47,7 @@ protected override string GetWorkingDirectory()

protected override string BuildArguments()
{
var arguments = AddArgument($"new {_templateName} --no-update-check", "-o", "\"" + _folder + "\"");
var arguments = AddArgument($"new {_templateName} --no-update-check --no-restore", "-o", "\"" + _folder + "\"");
arguments = AddArgument(
arguments,
"-lang",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ public partial class NewCommandBuilder
{
public class NewSolutionCommandBuilder : BaseCommandBuilder
{
private string _name;
private string _rootPath;
protected string _name;
protected string _rootPath;

public NewSolutionCommandBuilder InFolder(string rootPath)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Reqnroll.TestProjectGenerator.FilesystemWriter;

namespace Reqnroll.TestProjectGenerator.Dotnet;

public partial class NewCommandBuilder
{
public class StubNewProjectCommandBuilder(IOutputWriter outputWriter, NetCoreSdkInfo _sdk) : NewProjectCommandBuilder(outputWriter)
{
public override CommandBuilder Build()
{
return new CacheAndCopyCommandBuilder(_outputWriter, _sdk, base.Build(), _folder);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Reqnroll.TestProjectGenerator.FilesystemWriter;

namespace Reqnroll.TestProjectGenerator.Dotnet;

public partial class NewCommandBuilder
{
public class StubNewSolutionCommandBuilder(IOutputWriter outputWriter, NetCoreSdkInfo _sdk) : NewSolutionCommandBuilder(outputWriter)
{
public override CommandBuilder Build()
{
return new CacheAndCopyCommandBuilder(_outputWriter, _sdk, base.Build(), _rootPath, _name);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
using Reqnroll.TestProjectGenerator.FilesystemWriter;

namespace Reqnroll.TestProjectGenerator.Dotnet
{
public partial class NewCommandBuilder
{
private readonly IOutputWriter _outputWriter;
private readonly NetCoreSdkInfo _sdk;

public NewCommandBuilder(IOutputWriter outputWriter)
public NewCommandBuilder(IOutputWriter outputWriter, NetCoreSdkInfo sdk)
{
_outputWriter = outputWriter;
_sdk = sdk;
}

internal static NewCommandBuilder Create(IOutputWriter outputWriter) => new NewCommandBuilder(outputWriter);
internal static NewCommandBuilder Create(IOutputWriter outputWriter, NetCoreSdkInfo sdk) => new NewCommandBuilder(outputWriter, sdk);

public NewSolutionCommandBuilder Solution() => new NewSolutionCommandBuilder(_outputWriter);
public NewSolutionCommandBuilder Solution() => new StubNewSolutionCommandBuilder(_outputWriter, _sdk);

public NewProjectCommandBuilder Project() => new NewProjectCommandBuilder(_outputWriter);
public NewProjectCommandBuilder Project() => new StubNewProjectCommandBuilder(_outputWriter, _sdk);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ public partial class SolutionCommandBuilder
{
public class AddProjectSolutionCommandBuilder : BaseCommandBuilder
{
private string _solutionPath;
private string _projectPath;
protected string _solutionPath;
protected string _projectPath;


public AddProjectSolutionCommandBuilder ToSolution(string solutionPath)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.IO;
using System.Text.RegularExpressions;

namespace Reqnroll.TestProjectGenerator.Dotnet;

public partial class SolutionCommandBuilder
{
public class StubAddProjectSolutionCommandBuilder(IOutputWriter outputWriter) : AddProjectSolutionCommandBuilder(outputWriter)
{
public override CommandBuilder Build()
{
return new AddProjectSolutionCommand(_outputWriter, _solutionPath, _projectPath, GetWorkingDirectory());
}

class AddProjectSolutionCommand(IOutputWriter outputWriter, string _solutionPath, string _projectPath, string workingDirectory)
: CommandBuilder(outputWriter, "[add project to sln]", $"{_projectPath} -> {_solutionPath}", workingDirectory)
{
public override CommandResult Execute(Func<Exception, Exception> exceptionFunction)
{
try
{
var projectGuid = Guid.NewGuid().ToString("B").ToUpperInvariant();
var projectTypeGuid = Path.GetExtension(_projectPath).ToLowerInvariant().Equals(".vbproj") ?
"{F184B08F-C81C-45F6-A57F-5ABD9991F28F}" :
"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
var projectName = Path.GetFileNameWithoutExtension(_projectPath);
var projectRelativePath = _projectPath!.Substring(Path.GetDirectoryName(_solutionPath)!.Length + 1);
var slnContent = File.ReadAllText(_solutionPath);
var projectReference =
$$"""
Project("{{projectTypeGuid}}") = "{{projectName}}", "{{projectRelativePath}}", "{{projectGuid}}"
EndProject
""";
var projectConfPlatforms =
"""
GlobalSection(ProjectConfigurationPlatforms) = postSolution
EndGlobalSection
""";
var projectConfPlatformContent =
$$"""
{{projectGuid}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{{projectGuid}}.Debug|Any CPU.Build.0 = Debug|Any CPU
{{projectGuid}}.Release|Any CPU.ActiveCfg = Release|Any CPU
{{projectGuid}}.Release|Any CPU.Build.0 = Release|Any CPU
""";

slnContent = Regex.Replace(slnContent, @"\r?\nGlobal\r?\n", match => Environment.NewLine + projectReference + match.Value);
if (!slnContent.Contains("GlobalSection(ProjectConfigurationPlatforms)"))
{
slnContent = Regex.Replace(slnContent, @"\r?\nEndGlobal", match => Environment.NewLine + projectConfPlatforms + match.Value);
}

slnContent = Regex.Replace(slnContent, @"GlobalSection\(ProjectConfigurationPlatforms\) = postSolution\r?\n", match => match.Value + projectConfPlatformContent + Environment.NewLine);
File.WriteAllText(_solutionPath, slnContent);
_outputWriter.WriteLine($"Solution file updated: {ArgumentsFormat}");
return new CommandResult(0, "Solution file updated.");
}
catch (Exception ex)
{
throw exceptionFunction(ex);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ public SolutionCommandBuilder(IOutputWriter outputWriter)

public static SolutionCommandBuilder Create(IOutputWriter outputWriter) => new SolutionCommandBuilder(outputWriter);

public AddProjectSolutionCommandBuilder AddProject() => new AddProjectSolutionCommandBuilder(_outputWriter);
public AddProjectSolutionCommandBuilder AddProject() => new StubAddProjectSolutionCommandBuilder(_outputWriter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Reqnroll.TestProjectGenerator.FilesystemWriter
{
public interface IProjectWriter
{
string WriteProject(Project project, string path);
string WriteProject(NetCoreSdkInfo sdk, Project project, string path);

void WriteReferences(Project project, string projectFilePath);
}
Expand Down
Loading

0 comments on commit 4eed410

Please sign in to comment.