Merge pull request #5660 from krwq/2017-02-10-tamisol
TestAssets Isolation
This commit is contained in:
commit
3806eeb151
2 changed files with 155 additions and 315 deletions
|
@ -6,41 +6,20 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.PlatformAbstractions;
|
||||
using NuGet.Common;
|
||||
|
||||
namespace Microsoft.DotNet.TestFramework
|
||||
{
|
||||
public class TestAssetInfo
|
||||
{
|
||||
private const string DataDirectoryName = ".tam";
|
||||
|
||||
private readonly string [] FilesToExclude = { ".DS_Store", ".noautobuild" };
|
||||
|
||||
private readonly DirectoryInfo [] _directoriesToExclude;
|
||||
public string AssetName { get; private set; }
|
||||
|
||||
private readonly string _assetName;
|
||||
public FileInfo DotnetExeFile { get; private set; }
|
||||
|
||||
private readonly DirectoryInfo _dataDirectory;
|
||||
public string ProjectFilePattern { get; private set; }
|
||||
|
||||
private readonly DirectoryInfo _root;
|
||||
|
||||
private readonly TestAssetInventoryFiles _inventoryFiles;
|
||||
|
||||
private readonly FileInfo _dotnetExeFile;
|
||||
|
||||
private readonly string _projectFilePattern;
|
||||
|
||||
internal DirectoryInfo Root
|
||||
{
|
||||
get
|
||||
{
|
||||
return _root;
|
||||
}
|
||||
}
|
||||
public DirectoryInfo Root { get; private set; }
|
||||
|
||||
internal TestAssetInfo(DirectoryInfo root, string assetName, FileInfo dotnetExeFile, string projectFilePattern)
|
||||
{
|
||||
|
@ -64,22 +43,13 @@ namespace Microsoft.DotNet.TestFramework
|
|||
throw new ArgumentException("Argument cannot be null or whitespace", nameof(projectFilePattern));
|
||||
}
|
||||
|
||||
_root = root;
|
||||
Root = root;
|
||||
|
||||
_assetName = assetName;
|
||||
AssetName = assetName;
|
||||
|
||||
_dotnetExeFile = dotnetExeFile;
|
||||
DotnetExeFile = dotnetExeFile;
|
||||
|
||||
_projectFilePattern = projectFilePattern;
|
||||
|
||||
_dataDirectory = _root.GetDirectory(DataDirectoryName);
|
||||
|
||||
_inventoryFiles = new TestAssetInventoryFiles(_dataDirectory);
|
||||
|
||||
_directoriesToExclude = new []
|
||||
{
|
||||
_dataDirectory
|
||||
};
|
||||
ProjectFilePattern = projectFilePattern;
|
||||
}
|
||||
|
||||
public TestAssetInstance CreateInstance([CallerMemberName] string callingMethod = "", string identifier = "")
|
||||
|
@ -95,37 +65,8 @@ namespace Microsoft.DotNet.TestFramework
|
|||
{
|
||||
ThrowIfTestAssetDoesNotExist();
|
||||
|
||||
ThrowIfAssetSourcesHaveChanged();
|
||||
|
||||
return GetInventory(
|
||||
_inventoryFiles.Source,
|
||||
null,
|
||||
() => {});
|
||||
}
|
||||
|
||||
internal IEnumerable<FileInfo> GetRestoreFiles()
|
||||
{
|
||||
ThrowIfTestAssetDoesNotExist();
|
||||
|
||||
ThrowIfAssetSourcesHaveChanged();
|
||||
|
||||
return GetInventory(
|
||||
_inventoryFiles.Restore,
|
||||
GetSourceFiles,
|
||||
DoRestore);
|
||||
}
|
||||
|
||||
internal IEnumerable<FileInfo> GetBuildFiles()
|
||||
{
|
||||
ThrowIfTestAssetDoesNotExist();
|
||||
|
||||
ThrowIfAssetSourcesHaveChanged();
|
||||
|
||||
return GetInventory(
|
||||
_inventoryFiles.Build,
|
||||
() => GetRestoreFiles()
|
||||
.Concat(GetSourceFiles()),
|
||||
DoBuild);
|
||||
return Root.GetFiles("*.*", SearchOption.AllDirectories)
|
||||
.Where(f => !FilesToExclude.Contains(f.Name));
|
||||
}
|
||||
|
||||
private DirectoryInfo GetTestDestinationDirectory(string callingMethod, string identifier)
|
||||
|
@ -135,239 +76,15 @@ namespace Microsoft.DotNet.TestFramework
|
|||
#else
|
||||
string baseDirectory = AppContext.BaseDirectory;
|
||||
#endif
|
||||
return new DirectoryInfo(Path.Combine(baseDirectory, callingMethod + identifier, _assetName));
|
||||
}
|
||||
|
||||
private IEnumerable<FileInfo> GetFileList()
|
||||
{
|
||||
return _root.GetFiles("*.*", SearchOption.AllDirectories)
|
||||
.Where(f => !_directoriesToExclude.Any(d => d.Contains(f)))
|
||||
.Where(f => !FilesToExclude.Contains(f.Name));
|
||||
}
|
||||
|
||||
private IEnumerable<FileInfo> GetInventory(
|
||||
FileInfo file,
|
||||
Func<IEnumerable<FileInfo>> beforeAction,
|
||||
Action action)
|
||||
{
|
||||
var inventory = Enumerable.Empty<FileInfo>();
|
||||
|
||||
IEnumerable<FileInfo> preInventory;
|
||||
|
||||
if (beforeAction == null)
|
||||
{
|
||||
preInventory = new List<FileInfo>();
|
||||
}
|
||||
else
|
||||
{
|
||||
preInventory = beforeAction();
|
||||
}
|
||||
|
||||
ExclusiveFolderAccess.Do(_dataDirectory, (folder) => {
|
||||
file.Refresh();
|
||||
if (file.Exists)
|
||||
{
|
||||
inventory = folder.LoadInventory(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
|
||||
inventory = GetFileList().Where(i => !preInventory.Select(p => p.FullName).Contains(i.FullName));
|
||||
|
||||
folder.SaveInventory(file, inventory);
|
||||
}
|
||||
});
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private void DoRestore()
|
||||
{
|
||||
Console.WriteLine($"TestAsset Restore '{_assetName}'");
|
||||
|
||||
var projFiles = _root.GetFiles(_projectFilePattern, SearchOption.AllDirectories);
|
||||
|
||||
foreach (var projFile in projFiles)
|
||||
{
|
||||
var restoreArgs = new string[] { "restore", projFile.FullName };
|
||||
|
||||
var commandResult = Command.Create(_dotnetExeFile.FullName, restoreArgs)
|
||||
.CaptureStdOut()
|
||||
.CaptureStdErr()
|
||||
.Execute();
|
||||
|
||||
int exitCode = commandResult.ExitCode;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
Console.WriteLine(commandResult.StdOut);
|
||||
|
||||
Console.WriteLine(commandResult.StdErr);
|
||||
|
||||
string message = string.Format($"TestAsset Restore '{_assetName}'@'{projFile.FullName}' Failed with {exitCode}");
|
||||
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DoBuild()
|
||||
{
|
||||
string[] args = new string[] { "build" };
|
||||
|
||||
Console.WriteLine($"TestAsset Build '{_assetName}'");
|
||||
|
||||
var commandResult = Command.Create(_dotnetExeFile.FullName, args)
|
||||
.WorkingDirectory(_root.FullName)
|
||||
.CaptureStdOut()
|
||||
.CaptureStdErr()
|
||||
.Execute();
|
||||
|
||||
int exitCode = commandResult.ExitCode;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
Console.WriteLine(commandResult.StdOut);
|
||||
|
||||
Console.WriteLine(commandResult.StdErr);
|
||||
|
||||
string message = string.Format($"TestAsset Build '{_assetName}' Failed with {exitCode}");
|
||||
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfAssetSourcesHaveChanged()
|
||||
{
|
||||
if (!_dataDirectory.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dataDirectoryFiles = _dataDirectory.GetFiles("*", SearchOption.AllDirectories);
|
||||
|
||||
if (!dataDirectoryFiles.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<FileInfo> trackedFiles = null;
|
||||
ExclusiveFolderAccess.Do(_dataDirectory, (folder) => {
|
||||
trackedFiles = _inventoryFiles.AllInventoryFiles.SelectMany(f => folder.LoadInventory(f));
|
||||
});
|
||||
|
||||
var assetFiles = GetFileList();
|
||||
|
||||
var untrackedFiles = assetFiles.Where(a => !trackedFiles.Any(t => t.FullName.Equals(a.FullName)));
|
||||
|
||||
if (untrackedFiles.Any())
|
||||
{
|
||||
var message = $"TestAsset {_assetName} has untracked files. " +
|
||||
"Consider cleaning the asset and deleting its `.tam` directory to " +
|
||||
"recreate tracking files.\n\n" +
|
||||
$".tam directory: {_dataDirectory.FullName}\n" +
|
||||
"Untracked Files: \n";
|
||||
|
||||
message += String.Join("\n", untrackedFiles.Select(f => $" - {f.FullName}\n"));
|
||||
|
||||
throw new Exception(message);
|
||||
}
|
||||
|
||||
var earliestDataDirectoryTimestamp =
|
||||
dataDirectoryFiles
|
||||
.OrderBy(f => f.LastWriteTime)
|
||||
.First()
|
||||
.LastWriteTime;
|
||||
|
||||
if (earliestDataDirectoryTimestamp == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var updatedSourceFiles = ExclusiveFolderAccess.Read(_inventoryFiles.Source)
|
||||
.Where(f => f.LastWriteTime > earliestDataDirectoryTimestamp);
|
||||
|
||||
if (updatedSourceFiles.Any())
|
||||
{
|
||||
var message = $"TestAsset {_assetName} has updated files. " +
|
||||
"Consider cleaning the asset and deleting its `.tam` directory to " +
|
||||
"recreate tracking files.\n\n" +
|
||||
$".tam directory: {_dataDirectory.FullName}\n" +
|
||||
"Updated Files: \n";
|
||||
|
||||
message += String.Join("\n", updatedSourceFiles.Select(f => $" - {f.FullName}\n"));
|
||||
|
||||
throw new GracefulException(message);
|
||||
}
|
||||
return new DirectoryInfo(Path.Combine(baseDirectory, callingMethod + identifier, AssetName));
|
||||
}
|
||||
|
||||
private void ThrowIfTestAssetDoesNotExist()
|
||||
{
|
||||
if (!_root.Exists)
|
||||
if (!Root.Exists)
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Directory not found at '{_root.FullName}'");
|
||||
throw new DirectoryNotFoundException($"Directory not found at '{Root.FullName}'");
|
||||
}
|
||||
}
|
||||
|
||||
private class ExclusiveFolderAccess
|
||||
{
|
||||
private DirectoryInfo _directory;
|
||||
|
||||
private ExclusiveFolderAccess(DirectoryInfo directory)
|
||||
{
|
||||
_directory = directory;
|
||||
}
|
||||
|
||||
public static void Do(DirectoryInfo directory, Action<ExclusiveFolderAccess> action)
|
||||
{
|
||||
Task.Run(async () => await ConcurrencyUtilities.ExecuteWithFileLockedAsync<object>(
|
||||
directory.FullName,
|
||||
lockedToken =>
|
||||
{
|
||||
action(new ExclusiveFolderAccess(directory));
|
||||
return Task.FromResult(new Object());
|
||||
},
|
||||
CancellationToken.None)).Wait();
|
||||
}
|
||||
|
||||
public static IEnumerable<FileInfo> Read(FileInfo file)
|
||||
{
|
||||
IEnumerable<FileInfo> ret = null;
|
||||
Do(file.Directory, (folder) => {
|
||||
ret = folder.LoadInventory(file);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IEnumerable<FileInfo> LoadInventory(FileInfo file)
|
||||
{
|
||||
file.Refresh();
|
||||
if (!file.Exists)
|
||||
{
|
||||
return Enumerable.Empty<FileInfo>();
|
||||
}
|
||||
|
||||
var inventory = new List<FileInfo>();
|
||||
foreach (var p in File.ReadAllLines(file.FullName))
|
||||
{
|
||||
inventory.Add(new FileInfo(p));
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
public void SaveInventory(FileInfo file, IEnumerable<FileInfo> inventory)
|
||||
{
|
||||
_directory.Refresh();
|
||||
if (!_directory.Exists)
|
||||
{
|
||||
_directory.Create();
|
||||
}
|
||||
|
||||
File.WriteAllLines(file.FullName, inventory.Select((fi) => fi.FullName).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,25 @@ namespace Microsoft.DotNet.TestFramework
|
|||
{
|
||||
public class TestAssetInstance
|
||||
{
|
||||
public DirectoryInfo MigrationBackupRoot { get; }
|
||||
|
||||
public DirectoryInfo Root { get; }
|
||||
|
||||
public TestAssetInfo TestAssetInfo { get; }
|
||||
|
||||
private bool _filesCopied = false;
|
||||
|
||||
private bool _restored = false;
|
||||
|
||||
private bool _built = false;
|
||||
|
||||
public TestAssetInstance(TestAssetInfo testAssetInfo, DirectoryInfo root)
|
||||
{
|
||||
if (testAssetInfo == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(testAssetInfo));
|
||||
}
|
||||
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
throw new ArgumentException(nameof(root));
|
||||
|
@ -48,36 +60,42 @@ namespace Microsoft.DotNet.TestFramework
|
|||
}
|
||||
}
|
||||
|
||||
public DirectoryInfo MigrationBackupRoot { get; }
|
||||
|
||||
public DirectoryInfo Root { get; }
|
||||
|
||||
public TestAssetInfo TestAssetInfo { get; }
|
||||
|
||||
|
||||
public TestAssetInstance WithSourceFiles()
|
||||
{
|
||||
var filesToCopy = TestAssetInfo.GetSourceFiles();
|
||||
if (!_filesCopied)
|
||||
{
|
||||
CopySourceFiles();
|
||||
|
||||
CopyFiles(filesToCopy);
|
||||
_filesCopied = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestAssetInstance WithRestoreFiles()
|
||||
{
|
||||
var filesToCopy = TestAssetInfo.GetRestoreFiles();
|
||||
if (!_restored)
|
||||
{
|
||||
WithSourceFiles();
|
||||
|
||||
CopyFiles(filesToCopy);
|
||||
RestoreAllProjects();
|
||||
|
||||
_restored = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestAssetInstance WithBuildFiles()
|
||||
{
|
||||
var filesToCopy = TestAssetInfo.GetBuildFiles();
|
||||
if (!_built)
|
||||
{
|
||||
WithRestoreFiles();
|
||||
|
||||
CopyFiles(filesToCopy);
|
||||
BuildRootProjectOrSolution();
|
||||
|
||||
_built = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -156,19 +174,124 @@ namespace Microsoft.DotNet.TestFramework
|
|||
});
|
||||
}
|
||||
|
||||
private void CopyFiles(IEnumerable<FileInfo> filesToCopy)
|
||||
private static string RebasePath(string path, string oldBaseDirectory, string newBaseDirectory)
|
||||
{
|
||||
path = Path.IsPathRooted(path) ? PathUtility.GetRelativePath(PathUtility.EnsureTrailingSlash(oldBaseDirectory), path) : path;
|
||||
return Path.Combine(newBaseDirectory, path);
|
||||
}
|
||||
|
||||
private void CopySourceFiles()
|
||||
{
|
||||
var filesToCopy = TestAssetInfo.GetSourceFiles();
|
||||
foreach (var file in filesToCopy)
|
||||
{
|
||||
var relativePath = file.FullName.Substring(TestAssetInfo.Root.FullName.Length + 1);
|
||||
|
||||
var newPath = Path.Combine(Root.FullName, relativePath);
|
||||
var newPath = RebasePath(file.FullName, TestAssetInfo.Root.FullName, Root.FullName);
|
||||
|
||||
var newFile = new FileInfo(newPath);
|
||||
|
||||
PathUtility.EnsureDirectoryExists(newFile.Directory.FullName);
|
||||
|
||||
file.CopyTo(newPath);
|
||||
CopyFileAdjustingPaths(file, newFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyFileAdjustingPaths(FileInfo source, FileInfo destination)
|
||||
{
|
||||
if (string.Equals(source.Name, "nuget.config", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
CopyNugetConfigAdjustingPath(source, destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
source.CopyTo(destination.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyNugetConfigAdjustingPath(FileInfo source, FileInfo destination)
|
||||
{
|
||||
var doc = XDocument.Load(source.FullName, LoadOptions.PreserveWhitespace);
|
||||
foreach (var packageSource in doc.Root.Element("packageSources").Elements("add").Attributes("value"))
|
||||
{
|
||||
if (!Path.IsPathRooted(packageSource.Value))
|
||||
{
|
||||
string fullPathAtSource = Path.GetFullPath(Path.Combine(source.Directory.FullName, packageSource.Value));
|
||||
if (!PathUtility.IsChildOfDirectory(TestAssetInfo.Root.FullName, fullPathAtSource))
|
||||
{
|
||||
packageSource.Value = fullPathAtSource;
|
||||
}
|
||||
}
|
||||
|
||||
using (var file = new FileStream(
|
||||
destination.FullName,
|
||||
FileMode.CreateNew,
|
||||
FileAccess.ReadWrite))
|
||||
{
|
||||
doc.Save(file, SaveOptions.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildRootProjectOrSolution()
|
||||
{
|
||||
string[] args = new string[] { "build" };
|
||||
|
||||
Console.WriteLine($"TestAsset Build '{TestAssetInfo.AssetName}'");
|
||||
|
||||
var commandResult = Command.Create(TestAssetInfo.DotnetExeFile.FullName, args)
|
||||
.WorkingDirectory(Root.FullName)
|
||||
.CaptureStdOut()
|
||||
.CaptureStdErr()
|
||||
.Execute();
|
||||
|
||||
int exitCode = commandResult.ExitCode;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
Console.WriteLine(commandResult.StdOut);
|
||||
|
||||
Console.WriteLine(commandResult.StdErr);
|
||||
|
||||
string message = string.Format($"TestAsset Build '{TestAssetInfo.AssetName}' Failed with {exitCode}");
|
||||
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<FileInfo> GetProjectFiles()
|
||||
{
|
||||
return Root.GetFiles(TestAssetInfo.ProjectFilePattern, SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
private void Restore(FileInfo projectFile)
|
||||
{
|
||||
var restoreArgs = new string[] { "restore", projectFile.FullName };
|
||||
|
||||
var commandResult = Command.Create(TestAssetInfo.DotnetExeFile.FullName, restoreArgs)
|
||||
.CaptureStdOut()
|
||||
.CaptureStdErr()
|
||||
.Execute();
|
||||
|
||||
int exitCode = commandResult.ExitCode;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
Console.WriteLine(commandResult.StdOut);
|
||||
|
||||
Console.WriteLine(commandResult.StdErr);
|
||||
|
||||
string message = string.Format($"TestAsset Restore '{TestAssetInfo.AssetName}'@'{projectFile.FullName}' Failed with {exitCode}");
|
||||
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreAllProjects()
|
||||
{
|
||||
Console.WriteLine($"TestAsset Restore '{TestAssetInfo.AssetName}'");
|
||||
|
||||
foreach (var projFile in GetProjectFiles())
|
||||
{
|
||||
Restore(projFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue