// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; 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; private readonly string _assetName; private readonly DirectoryInfo _dataDirectory; private readonly DirectoryInfo _root; private readonly TestAssetInventoryFiles _inventoryFiles; private readonly FileInfo _dotnetExeFile; private readonly string _projectFilePattern; internal DirectoryInfo Root { get { return _root; } } internal TestAssetInfo(DirectoryInfo root, string assetName, FileInfo dotnetExeFile, string projectFilePattern) { if (root == null) { throw new ArgumentNullException(nameof(root)); } if (string.IsNullOrWhiteSpace(assetName)) { throw new ArgumentException("Argument cannot be null or whitespace", nameof(assetName)); } if (dotnetExeFile == null) { throw new ArgumentNullException(nameof(dotnetExeFile)); } if (string.IsNullOrWhiteSpace(projectFilePattern)) { throw new ArgumentException("Argument cannot be null or whitespace", nameof(projectFilePattern)); } _root = root; _assetName = assetName; _dotnetExeFile = dotnetExeFile; _projectFilePattern = projectFilePattern; _dataDirectory = _root.GetDirectory(DataDirectoryName); _inventoryFiles = new TestAssetInventoryFiles(_dataDirectory); _directoriesToExclude = new [] { _dataDirectory }; } public TestAssetInstance CreateInstance([CallerMemberName] string callingMethod = "", string identifier = "") { var instancePath = GetTestDestinationDirectory(callingMethod, identifier); var testInstance = new TestAssetInstance(this, instancePath); return testInstance; } internal IEnumerable GetSourceFiles() { ThrowIfTestAssetDoesNotExist(); ThrowIfAssetSourcesHaveChanged(); return GetInventory( _inventoryFiles.Source, null, () => {}); } internal IEnumerable GetRestoreFiles() { ThrowIfTestAssetDoesNotExist(); ThrowIfAssetSourcesHaveChanged(); return GetInventory( _inventoryFiles.Restore, GetSourceFiles, DoRestore); } internal IEnumerable GetBuildFiles() { ThrowIfTestAssetDoesNotExist(); ThrowIfAssetSourcesHaveChanged(); return GetInventory( _inventoryFiles.Build, () => GetRestoreFiles() .Concat(GetSourceFiles()), DoBuild); } private DirectoryInfo GetTestDestinationDirectory(string callingMethod, string identifier) { #if NET451 string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; #else string baseDirectory = AppContext.BaseDirectory; #endif return new DirectoryInfo(Path.Combine(baseDirectory, callingMethod + identifier, _assetName)); } private IEnumerable LoadInventory(FileInfo file) { if (!file.Exists) { return Enumerable.Empty(); } var inventory = new List(); var lines = file.OpenText(); while (lines.Peek() > 0) { inventory.Add(new FileInfo(lines.ReadLine())); } return inventory; } private void SaveInventory(FileInfo file, IEnumerable inventory) { FileUtility.ReplaceWithLock( filePath => { if (!_dataDirectory.Exists) { _dataDirectory.Create(); } using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) { using (var writer = new StreamWriter(stream)) { foreach (var path in inventory.Select(i => i.FullName)) { writer.WriteLine(path); } } } }, file.FullName); } private IEnumerable GetFileList() { return _root.GetFiles("*.*", SearchOption.AllDirectories) .Where(f => !_directoriesToExclude.Any(d => d.Contains(f))) .Where(f => !FilesToExclude.Contains(f.Name)); } private IEnumerable GetInventory( FileInfo file, Func> beforeAction, Action action) { var inventory = Enumerable.Empty(); if (file.Exists) { inventory = LoadInventory(file); } if(inventory.Any()) { return inventory; } IEnumerable preInventory; if (beforeAction == null) { preInventory = new List(); } else { preInventory = beforeAction(); } Task.Run(async () => await ConcurrencyUtilities.ExecuteWithFileLockedAsync( _dataDirectory.FullName, lockedToken => { if (file.Exists) { inventory = LoadInventory(file); } else { action(); inventory = GetFileList().Where(i => !preInventory.Select(p => p.FullName).Contains(i.FullName)); SaveInventory(file, inventory); } return Task.FromResult(new Object()); }, CancellationToken.None)).Wait(); 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; } var trackedFiles = _inventoryFiles.AllInventoryFiles.SelectMany(f => 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 = LoadInventory(_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); } } private void ThrowIfTestAssetDoesNotExist() { if (!_root.Exists) { throw new DirectoryNotFoundException($"Directory not found at '{_root.FullName}'"); } } } }