From 7c05a534404abd20ba2b8c9ac21b14b48ffccc5c Mon Sep 17 00:00:00 2001 From: Matthew Asplund Date: Mon, 21 Nov 2022 12:05:38 -0600 Subject: [PATCH] Use File Time (#168) * Convert to using file time * Move force rebuild into evaluate engine * Bump minor version * Update Opal with File Times * Load Operation Results into Soup View * Update graph properties * Test for duplicate output files * Store input lookup state * Pass in Operation graph to make testing easier * Add checks for circular operations * Update to use single try call for last write time * Rename recipe build runner * Convert to System interface * Add ability to simulate IDetourCallback events * Add check for multiple operations write to single file * Add checks for new output dependency * Add checks for observed input that is an output * Add check for input output within single operation * Normalize file system state paths * Remove internal circular dependencies for now * Delay load lookup tables Co-authored-by: Matthew Asplund --- .vscode/settings.json | 4 +- Scripts/build-client.cmd | 2 +- Scripts/soup.cmd | 2 +- Scripts/soupd.cmd | 2 +- Source/Client/CLI/PackageLock.sml | 2 +- Source/Client/CLI/Recipe.sml | 2 +- .../Client/CLI/Source/Commands/BuildCommand.h | 4 +- .../CLI/Source/Commands/InitializeCommand.h | 2 +- .../CLI/Source/Commands/InstallCommand.h | 4 +- .../CLI/Source/Commands/PublishCommand.h | 4 +- .../CLI/Source/Commands/RestoreCommand.h | 4 +- .../Client/CLI/Source/Commands/RunCommand.h | 4 +- .../CLI/Source/Commands/TargetCommand.h | 4 +- .../CLI/Source/Commands/VersionCommand.h | 2 +- Source/Client/CLI/Source/Program.h | 1 + Source/Client/Core/Source/Build/BuildEngine.h | 10 +- .../Core/Source/Build/BuildEvaluateEngine.h | 257 ++++- .../Core/Source/Build/BuildHistoryChecker.h | 4 +- .../{RecipeBuildRunner.h => BuildRunner.h} | 16 +- .../Core/Source/Build/FileSystemState.h | 37 +- .../Core/Source/Build/SystemAccessTracker.h | 4 +- Source/Client/Core/Source/Module.cpp | 1 + .../Source/OperationGraph/OperationResult.h | 6 +- .../OperationGraph/OperationResultsReader.h | 21 +- .../OperationGraph/OperationResultsWriter.h | 18 +- Source/Client/Core/Source/Utils/Helpers.h | 77 -- .../Core/UnitTests/Build/BuildEngineTests.h | 19 +- .../Build/BuildEvaluateEngineTests.h | 1026 +++++++++++++++-- .../Build/BuildHistoryCheckerTests.h | 44 +- ...eBuildRunnerTests.h => BuildRunnerTests.h} | 36 +- .../UnitTests/Build/FileSystemStateTests.h | 13 +- .../Core/UnitTests/Build/MockEvaluateEngine.h | 3 +- .../OperationResultsManagerTests.h | 14 +- .../OperationResultsReaderTests.h | 32 +- .../OperationGraph/OperationResultsTests.h | 18 +- .../OperationResultsWriterTests.h | 28 +- .../gen/Build/BuildEvaluateEngineTests.gen.h | 5 + ...nnerTests.gen.h => BuildRunnerTests.gen.h} | 8 +- Source/Client/Core/UnitTests/gen/Main.cpp | 4 +- .../OperationGraphGeneratorUnitTests.cs | 848 +++++++++++--- .../Runtime/Contracts/BuildState.cs | 8 +- .../Runtime/OperationGraph/OperationGraph.cs | 28 +- .../OperationGraph/OperationGraphGenerator.cs | 310 +++-- Source/GenerateSharp/SoupView/App.xaml.cs | 2 +- Source/GenerateSharp/SoupView/SoupView.csproj | 3 +- .../View/ValueTableItemTemplateSelector.cs | 8 +- .../ViewModel/DependencyGraphPageModel.cs | 31 +- .../SoupView/ViewModel/Observable.cs | 4 +- .../ViewModel/OperationDetailsViewModel.cs | 16 +- .../ViewModel/OperationGraphPageModel.cs | 67 +- .../ViewModel/ProjectDetailsViewModel.cs | 2 +- .../ViewModel/TaskDetailsViewModel.cs | 78 +- .../SoupView/ViewModel/TaskGraphPageModel.cs | 21 +- .../ViewModel/ValueTableItemViewModel.cs | 5 + .../GenerateSharp/Utilities/BuildConstants.cs | 5 + .../OperationGraph/OperationGraphManager.cs | 19 +- .../OperationGraph/OperationGraphWriter.cs | 9 +- .../OperationGraph/OperationResult.cs | 69 ++ .../OperationGraph/OperationResults.cs | 69 ++ .../OperationGraph/OperationResultsManager.cs | 124 ++ .../OperationGraph/OperationResultsReader.cs | 138 +++ Source/Installer/SoupInstaller/Setup.cs | 2 +- Source/Monitor/Client/PackageLock.sml | 2 +- .../Monitor/Host/MockDetourProcessManager.h | 34 +- Source/Monitor/Host/Module.cpp | 1 + Source/Tools/PrintGraph/PackageLock.sml | 2 +- Source/Tools/PrintResults/PackageLock.sml | 2 +- Source/Tools/PrintValueTable/PackageLock.sml | 2 +- 68 files changed, 2943 insertions(+), 710 deletions(-) rename Source/Client/Core/Source/Build/{RecipeBuildRunner.h => BuildRunner.h} (97%) delete mode 100644 Source/Client/Core/Source/Utils/Helpers.h rename Source/Client/Core/UnitTests/Build/{RecipeBuildRunnerTests.h => BuildRunnerTests.h} (99%) rename Source/Client/Core/UnitTests/gen/Build/{RecipeBuildRunnerTests.gen.h => BuildRunnerTests.gen.h} (78%) rename Source/GenerateSharp/Runtime.UnitTests/{Utilities => OperationGraph}/OperationGraphGeneratorUnitTests.cs (52%) create mode 100644 Source/GenerateSharp/Utilities/OperationGraph/OperationResult.cs create mode 100644 Source/GenerateSharp/Utilities/OperationGraph/OperationResults.cs create mode 100644 Source/GenerateSharp/Utilities/OperationGraph/OperationResultsManager.cs create mode 100644 Source/GenerateSharp/Utilities/OperationGraph/OperationResultsReader.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 8680e574..dcf213a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -100,6 +100,8 @@ "format": "cpp", "*.tcc": "cpp", "cinttypes": "cpp", - "memory_resource": "cpp" + "memory_resource": "cpp", + "ranges": "cpp", + "span": "cpp" } } \ No newline at end of file diff --git a/Scripts/build-client.cmd b/Scripts/build-client.cmd index 54dbab24..0aeb7024 100644 --- a/Scripts/build-client.cmd +++ b/Scripts/build-client.cmd @@ -8,7 +8,7 @@ SET ClientCLIDir=%SourceDir%\Client\CLI SET MonitorClientDir=%SourceDir%\Monitor\Client if %Flavor% == release (SET OutputX64DirectorPath=txTMowfPh1V3rPmbvNBmBW9Z8Jg) else (SET OutputX64DirectorPath=J3mu4cpISw6nDaCPED8gkqZ-q84) if %Flavor% == release (SET OutputX86DirectorPath=ci_UJP5zJKyF-O0VVSVDMNi1Wwg) else (SET OutputX86DirectorPath=9fr4dmE4CrAXgS2yFzcvYJXkGDg) -SET ClientCLIOutputDirectory=%OutputDir%\Cpp\Soup\0.26.1\%OutputX64DirectorPath% +SET ClientCLIOutputDirectory=%OutputDir%\Cpp\Soup\0.27.0\%OutputX64DirectorPath% SET MonitorClientOutputX64Directory=%OutputDir%\Cpp\Monitor.Client\1.0.0\%OutputX64DirectorPath% SET MonitorClientOutputX86Directory=%OutputDir%\Cpp\Monitor.Client\1.0.0\%OutputX86DirectorPath% diff --git a/Scripts/soup.cmd b/Scripts/soup.cmd index addc3fb3..3d668ad6 100644 --- a/Scripts/soup.cmd +++ b/Scripts/soup.cmd @@ -7,7 +7,7 @@ SET RunDir=%OutDir%\run REM - Use a copy of the final binary in case we are re-buiding itself robocopy %ScriptsDir%\Install\ %RunDir%\ /MIR /NJH /NJS /NDL > NUL -robocopy %OutDir%\Cpp\Soup\0.26.1\txTMowfPh1V3rPmbvNBmBW9Z8Jg\bin\ %RunDir%\Soup\ /MIR /NJH /NJS /NDL > NUL +robocopy %OutDir%\Cpp\Soup\0.27.0\txTMowfPh1V3rPmbvNBmBW9Z8Jg\bin\ %RunDir%\Soup\ /MIR /NJH /NJS /NDL > NUL robocopy %OutDir%\msbuild\bin\Soup.Build.Generate\Release\net6.0\win-x64\publish\ %RunDir%\Soup\Generate\ /MIR /NJH /NJS /NDL > NUL robocopy C:\Users\mwasp\.soup\out\CSharp\Soup.Cpp\0.4.0\txTMowfPh1V3rPmbvNBmBW9Z8Jg\bin\ %RunDir%\Soup\Extensions\Soup.Cpp\0.4.0\ /MIR /NJH /NJS /NDL > NUL robocopy C:\Users\mwasp\.soup\out\CSharp\Soup.CSharp\0.7.0\txTMowfPh1V3rPmbvNBmBW9Z8Jg\bin\ %RunDir%\Soup\Extensions\Soup.CSharp\0.7.0\ /MIR /NJH /NJS /NDL > NUL diff --git a/Scripts/soupd.cmd b/Scripts/soupd.cmd index c317746d..a3b5ab6c 100644 --- a/Scripts/soupd.cmd +++ b/Scripts/soupd.cmd @@ -7,7 +7,7 @@ SET RunDir=%OutDir%\run REM - Use a copy of the final binary in case we are re-buiding itself robocopy %ScriptsDir%\Install\ %RunDir%\ /MIR /NJH /NJS /NDL > NUL -robocopy %OutDir%\Cpp\Soup\0.26.1\J3mu4cpISw6nDaCPED8gkqZ-q84\bin\ %RunDir%\Soup\ /MIR /NJH /NJS /NDL > NUL +robocopy %OutDir%\Cpp\Soup\0.27.0\J3mu4cpISw6nDaCPED8gkqZ-q84\bin\ %RunDir%\Soup\ /MIR /NJH /NJS /NDL > NUL robocopy %OutDir%\msbuild\bin\Soup.Build.Generate\Debug\net6.0\win-x64\publish\ %RunDir%\Soup\Generate\ /MIR /NJH /NJS /NDL > NUL robocopy C:\Users\mwasp\.soup\out\CSharp\Soup.Cpp\0.4.0\J3mu4cpISw6nDaCPED8gkqZ-q84\bin\ %RunDir%\Soup\Extensions\Soup.Cpp\0.4.0\ /MIR /NJH /NJS /NDL > NUL robocopy C:\Users\mwasp\.soup\out\CSharp\Soup.CSharp\0.7.0\J3mu4cpISw6nDaCPED8gkqZ-q84\bin\ %RunDir%\Soup\Extensions\Soup.CSharp\0.7.0\ /MIR /NJH /NJS /NDL > NUL diff --git a/Source/Client/CLI/PackageLock.sml b/Source/Client/CLI/PackageLock.sml index a1890934..faaf6df1 100644 --- a/Source/Client/CLI/PackageLock.sml +++ b/Source/Client/CLI/PackageLock.sml @@ -20,7 +20,7 @@ Closures: { { Name: "mkdir", Version: "../../Tools/Mkdir/", Build: "Build0" } { Name: "Monitor.Host", Version: "../../Monitor/Host/", Build: "Build0" } { Name: "Monitor.Shared", Version: "../../Monitor/Shared/", Build: "Build0" } - { Name: "Opal", Version: "0.5.1", Build: "Build0" } + { Name: "Opal", Version: "0.7.1", Build: "Build0" } { Name: "reflex", Version: "1.0.1", Build: "Build0" } { Name: "Soup", Version: "../CLI", Build: "Build0" } { Name: "Soup.Core", Version: "../Core/", Build: "Build1" } diff --git a/Source/Client/CLI/Recipe.sml b/Source/Client/CLI/Recipe.sml index 1d986122..ec7b1902 100644 --- a/Source/Client/CLI/Recipe.sml +++ b/Source/Client/CLI/Recipe.sml @@ -1,5 +1,5 @@ Name: "Soup" -Version: "0.26.1" +Version: "0.27.0" Language: "C++|0.1" Type: "Executable" diff --git a/Source/Client/CLI/Source/Commands/BuildCommand.h b/Source/Client/CLI/Source/Commands/BuildCommand.h index e7a05198..6c989b08 100644 --- a/Source/Client/CLI/Source/Commands/BuildCommand.h +++ b/Source/Client/CLI/Source/Commands/BuildCommand.h @@ -34,7 +34,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Build in the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -43,7 +43,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/InitializeCommand.h b/Source/Client/CLI/Source/Commands/InitializeCommand.h index 7c72ccb0..f34346dd 100644 --- a/Source/Client/CLI/Source/Commands/InitializeCommand.h +++ b/Source/Client/CLI/Source/Commands/InitializeCommand.h @@ -31,7 +31,7 @@ namespace Soup::Client Log::HighPriority("The initialize utility will walk through the creation of the most basic Console recipe.\n"); // Use the current directory as the default names - auto workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + auto workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); auto recipePath = workingDirectory + Core::BuildConstants::RecipeFileName(); diff --git a/Source/Client/CLI/Source/Commands/InstallCommand.h b/Source/Client/CLI/Source/Commands/InstallCommand.h index 0ff79104..837a941a 100644 --- a/Source/Client/CLI/Source/Commands/InstallCommand.h +++ b/Source/Client/CLI/Source/Commands/InstallCommand.h @@ -33,7 +33,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Build in the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -42,7 +42,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/PublishCommand.h b/Source/Client/CLI/Source/Commands/PublishCommand.h index e256170a..341d8f5e 100644 --- a/Source/Client/CLI/Source/Commands/PublishCommand.h +++ b/Source/Client/CLI/Source/Commands/PublishCommand.h @@ -33,7 +33,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Buildin the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -42,7 +42,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/RestoreCommand.h b/Source/Client/CLI/Source/Commands/RestoreCommand.h index 849f11e5..19d34e1e 100644 --- a/Source/Client/CLI/Source/Commands/RestoreCommand.h +++ b/Source/Client/CLI/Source/Commands/RestoreCommand.h @@ -33,7 +33,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Build in the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -42,7 +42,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/RunCommand.h b/Source/Client/CLI/Source/Commands/RunCommand.h index ad15d1a8..31ffc85d 100644 --- a/Source/Client/CLI/Source/Commands/RunCommand.h +++ b/Source/Client/CLI/Source/Commands/RunCommand.h @@ -33,7 +33,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Buildin the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -42,7 +42,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/TargetCommand.h b/Source/Client/CLI/Source/Commands/TargetCommand.h index 630d49d2..d839fc9d 100644 --- a/Source/Client/CLI/Source/Commands/TargetCommand.h +++ b/Source/Client/CLI/Source/Commands/TargetCommand.h @@ -33,7 +33,7 @@ namespace Soup::Client if (_options.Path.empty()) { // Buildin the current directory - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2(); + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory(); } else { @@ -42,7 +42,7 @@ namespace Soup::Client // Check if this is relative to current directory if (!workingDirectory.HasRoot()) { - workingDirectory = System::IFileSystem::Current().GetCurrentDirectory2() + workingDirectory; + workingDirectory = System::IFileSystem::Current().GetCurrentDirectory() + workingDirectory; } } diff --git a/Source/Client/CLI/Source/Commands/VersionCommand.h b/Source/Client/CLI/Source/Commands/VersionCommand.h index ea292a1f..7549d2f8 100644 --- a/Source/Client/CLI/Source/Commands/VersionCommand.h +++ b/Source/Client/CLI/Source/Commands/VersionCommand.h @@ -31,7 +31,7 @@ namespace Soup::Client // TODO var version = Assembly.GetExecutingAssembly().GetName().Version; // Log::Message($"{version.Major}.{version.Minor}.{version.Build}"); - Log::HighPriority("0.26.1"); + Log::HighPriority("0.27.0"); } private: diff --git a/Source/Client/CLI/Source/Program.h b/Source/Client/CLI/Source/Program.h index 4df62864..00422a86 100644 --- a/Source/Client/CLI/Source/Program.h +++ b/Source/Client/CLI/Source/Program.h @@ -50,6 +50,7 @@ namespace Soup::Client false)); // Setup the real services + System::ISystem::Register(std::make_shared()); System::IFileSystem::Register(std::make_shared()); System::IProcessManager::Register(std::make_shared()); Monitor::IDetourProcessManager::Register(std::make_shared()); diff --git a/Source/Client/Core/Source/Build/BuildEngine.h b/Source/Client/Core/Source/Build/BuildEngine.h index 93f42e6a..e64295c4 100644 --- a/Source/Client/Core/Source/Build/BuildEngine.h +++ b/Source/Client/Core/Source/Build/BuildEngine.h @@ -3,7 +3,7 @@ // #pragma once -#include "RecipeBuildRunner.h" +#include "BuildRunner.h" #include "BuildEvaluateEngine.h" #include "BuildLoadEngine.h" #include "LocalUserConfig/LocalUserConfigExtensions.h" @@ -92,14 +92,16 @@ namespace Soup::Core auto fileSystemState = FileSystemState(); // Initialize a shared Evaluate Engine - auto evaluateEngine = BuildEvaluateEngine(fileSystemState); + auto evaluateEngine = BuildEvaluateEngine( + arguments.ForceRebuild, + fileSystemState); // Initialize shared location manager auto locationManager = RecipeBuildLocationManager(builtInLanguages); // Initialize the build runner that will perform the generate and evaluate phase // for each individual package - auto buildRunner = RecipeBuildRunner( + auto buildRunner = BuildRunner( arguments, sdkParameters, sdkReadAccess, @@ -114,7 +116,7 @@ namespace Soup::Core endTime = std::chrono::high_resolution_clock::now(); duration = std::chrono::duration_cast>(endTime - startTime); - // std::cout << "RecipeBuildRunner: " << std::to_string(duration.count()) << " seconds." << std::endl; + // std::cout << "BuildRunner: " << std::to_string(duration.count()) << " seconds." << std::endl; } private: diff --git a/Source/Client/Core/Source/Build/BuildEvaluateEngine.h b/Source/Client/Core/Source/Build/BuildEvaluateEngine.h index 94513868..7f074614 100644 --- a/Source/Client/Core/Source/Build/BuildEvaluateEngine.h +++ b/Source/Client/Core/Source/Build/BuildEvaluateEngine.h @@ -12,16 +12,119 @@ namespace Soup::Core { + class BuildEvaluateState + { + public: + BuildEvaluateState( + const OperationGraph& operationGraph, + OperationResults& operationResults, + const Path& temporaryDirectory, + const std::vector& globalAllowedReadAccess, + const std::vector& globalAllowedWriteAccess) : + OperationGraph(operationGraph), + OperationResults(operationResults), + TemporaryDirectory(temporaryDirectory), + GlobalAllowedReadAccess(globalAllowedReadAccess), + GlobalAllowedWriteAccess(globalAllowedWriteAccess), + RemainingDependencyCounts(), + LookupLoaded(false), + InputFileLookup(), + OutputFileLookup() + { + } + + const OperationGraph& OperationGraph; + OperationResults& OperationResults; + + const Path& TemporaryDirectory; + + const std::vector& GlobalAllowedReadAccess; + const std::vector& GlobalAllowedWriteAccess; + + // Running State + std::unordered_map RemainingDependencyCounts; + + bool LookupLoaded; + std::unordered_map> InputFileLookup; + std::unordered_map OutputFileLookup; + + void EnsureOperationLookupLoaded() + { + if (LookupLoaded) + return; + + for (auto& operation : OperationGraph.GetOperations()) + { + auto& operationInfo = operation.second; + + for (auto fileId : operationInfo.DeclaredInput) + { + auto findResult = InputFileLookup.find(fileId); + if (findResult != InputFileLookup.end()) + { + findResult->second.insert(operationInfo.Id); + } + else + { + auto insertResult = InputFileLookup.emplace( + fileId, + std::set()); + insertResult.first->second.insert(operationInfo.Id); + } + } + + for (auto fileId : operationInfo.DeclaredOutput) + { + OutputFileLookup.emplace(fileId, operationInfo.Id); + } + } + + LookupLoaded = true; + } + + bool TryGetInputFileOperations( + FileId fileId, + const std::set*& result) + { + auto findResult = InputFileLookup.find(fileId); + if (findResult != InputFileLookup.end()) + { + result = &findResult->second; + return true; + } + else + { + return false; + } + } + + bool TryGetOutputFileOperation( + FileId fileId, + OperationId& result) + { + auto findResult = OutputFileLookup.find(fileId); + if (findResult != OutputFileLookup.end()) + { + result = findResult->second; + return true; + } + else + { + return false; + } + } + }; + /// /// The core build evaluation engine that knows how to perform a build from a provided Operation Graph. /// export class BuildEvaluateEngine : public IEvaluateEngine { private: + bool _forceRebuild; + // Shared Runtime State FileSystemState& _fileSystemState; - - std::unordered_map _remainingDependencyCounts; BuildHistoryChecker _stateChecker; public: @@ -29,9 +132,10 @@ namespace Soup::Core /// Initializes a new instance of the class. /// BuildEvaluateEngine( + bool forceRebuild, FileSystemState& fileSystemState) : + _forceRebuild(forceRebuild), _fileSystemState(fileSystemState), - _remainingDependencyCounts(), _stateChecker(fileSystemState) { } @@ -48,13 +152,15 @@ namespace Soup::Core { // Run all build operations in the correct order with incremental build checks Log::Diag("Build evaluation start"); - _remainingDependencyCounts.clear(); - auto result = CheckExecuteOperations( + auto evaluateState = BuildEvaluateState( operationGraph, operationResults, temporaryDirectory, globalAllowedReadAccess, - globalAllowedWriteAccess, + globalAllowedWriteAccess); + + auto result = CheckExecuteOperations( + evaluateState, operationGraph.GetRootOperationIds()); Log::Diag("Build evaluation end"); @@ -66,11 +172,7 @@ namespace Soup::Core /// Execute the collection of build operations /// bool CheckExecuteOperations( - const OperationGraph& operationGraph, - OperationResults& operationResults, - const Path& temporaryDirectory, - const std::vector& globalAllowedReadAccess, - const std::vector& globalAllowedWriteAccess, + BuildEvaluateState& evaluateState, const std::vector& operations) { bool didAnyEvaluate = false; @@ -78,10 +180,10 @@ namespace Soup::Core { // Check if the operation was already a child from a different path // Only run the operation when all of its dependencies have completed - auto& operationInfo = operationGraph.GetOperationInfo(operationId); - auto currentOperationSearch = _remainingDependencyCounts.find(operationId); + auto& operationInfo = evaluateState.OperationGraph.GetOperationInfo(operationId); + auto currentOperationSearch = evaluateState.RemainingDependencyCounts.find(operationId); int32_t remainingCount = -1; - if (currentOperationSearch != _remainingDependencyCounts.end()) + if (currentOperationSearch != evaluateState.RemainingDependencyCounts.end()) { remainingCount = --currentOperationSearch->second; } @@ -89,7 +191,7 @@ namespace Soup::Core { // Get the cached total count and store the active count in the lookup remainingCount = operationInfo.DependencyCount - 1; - auto insertResult = _remainingDependencyCounts.emplace(operationId, remainingCount); + auto insertResult = evaluateState.RemainingDependencyCounts.emplace(operationId, remainingCount); if (!insertResult.second) throw std::runtime_error("The operation id already existed in the remaining count lookup"); } @@ -98,19 +200,12 @@ namespace Soup::Core { // Run the single operation didAnyEvaluate |= CheckExecuteOperation( - operationResults, - temporaryDirectory, - globalAllowedReadAccess, - globalAllowedWriteAccess, + evaluateState, operationInfo); // Recursively build all of the operation children didAnyEvaluate |= CheckExecuteOperations( - operationGraph, - operationResults, - temporaryDirectory, - globalAllowedReadAccess, - globalAllowedWriteAccess, + evaluateState, operationInfo.Children); } else if (remainingCount < 0) @@ -130,10 +225,7 @@ namespace Soup::Core /// Check if an individual operation has been run and execute if required /// bool CheckExecuteOperation( - OperationResults& operationResults, - const Path& temporaryDirectory, - const std::vector& globalAllowedReadAccess, - const std::vector& globalAllowedWriteAccess, + BuildEvaluateState& evaluateState, const OperationInfo& operationInfo) { // Check if each source file is out of date and requires a rebuild @@ -142,7 +234,7 @@ namespace Soup::Core // Check if this operation was run before auto buildRequired = false; OperationResult* previousResult; - if (operationResults.TryFindResult(operationInfo.Id, previousResult) && + if (evaluateState.OperationResults.TryFindResult(operationInfo.Id, previousResult) && previousResult->WasSuccessfulRun) { // Check if the executable has changed since the last run @@ -167,7 +259,15 @@ namespace Soup::Core } else { - Log::Info("Up to date"); + if (_forceRebuild) + { + Log::HighPriority("Up to date: Force Build"); + buildRequired = true; + } + else + { + Log::Info("Up to date"); + } } } else @@ -198,14 +298,19 @@ namespace Soup::Core else { ExecuteOperation( - temporaryDirectory, - globalAllowedReadAccess, - globalAllowedWriteAccess, + evaluateState.TemporaryDirectory, + evaluateState.GlobalAllowedReadAccess, + evaluateState.GlobalAllowedWriteAccess, operationInfo, operationResult); } - operationResults.AddOrUpdateOperationResult(operationInfo.Id, std::move(operationResult)); + // Ensure there are no new dependencies + VerifyObservedState(evaluateState, operationInfo, operationResult); + + evaluateState.OperationResults.AddOrUpdateOperationResult( + operationInfo.Id, + std::move(operationResult)); } else { @@ -251,7 +356,7 @@ namespace Soup::Core // Mark this operation as successful to enable future incremental builds operationResult.WasSuccessfulRun = true; - operationResult.EvaluateTime = std::chrono::system_clock::now(); + operationResult.EvaluateTime = System::ISystem::Current().GetCurrentTime(); // Ensure the File System State is notified of any output files that have changed _fileSystemState.CheckFileWriteTimes(operationResult.ObservedOutput); @@ -350,12 +455,16 @@ namespace Soup::Core output.push_back(std::move(path)); } - operationResult.ObservedInput = _fileSystemState.ToFileIds(input, operationInfo.Command.WorkingDirectory); - operationResult.ObservedOutput = _fileSystemState.ToFileIds(output, operationInfo.Command.WorkingDirectory); + operationResult.ObservedInput = _fileSystemState.ToFileIds( + input, + operationInfo.Command.WorkingDirectory); + operationResult.ObservedOutput = _fileSystemState.ToFileIds( + output, + operationInfo.Command.WorkingDirectory); // Mark this operation as successful to enable future incremental builds operationResult.WasSuccessfulRun = true; - operationResult.EvaluateTime = std::chrono::system_clock::now(); + operationResult.EvaluateTime = System::ISystem::Current().GetCurrentTime(); // Ensure the File System State is notified of any output files that have changed _fileSystemState.CheckFileWriteTimes(operationResult.ObservedOutput); @@ -367,5 +476,77 @@ namespace Soup::Core throw BuildFailedException(); } } + + void VerifyObservedState( + BuildEvaluateState& evaluateState, + const OperationInfo& operationInfo, + OperationResult& operationResult) + { + // TODO: Should generate NEW input/output lookup to check for entirely observed dependencies + + evaluateState.EnsureOperationLookupLoaded(); + + // Verify new inputs + for (auto fileId : operationResult.ObservedInput) + { + // Check if this input was generated from another operation + OperationId matchedOutputOperationId; + if (evaluateState.TryGetOutputFileOperation(fileId, matchedOutputOperationId) && + operationInfo.Id != matchedOutputOperationId) + { + // If it is a known output file then it must be a declared input for this operation + const std::set* matchedInputOperationIds; + if (!evaluateState.TryGetInputFileOperations(fileId, matchedInputOperationIds) || + !matchedInputOperationIds->contains(operationInfo.Id)) + { + auto filePath = _fileSystemState.GetFilePath(fileId); + auto& existingOperation = evaluateState.OperationGraph.GetOperationInfo(matchedOutputOperationId); + auto message = "File \"" + filePath.ToString() + "\" observed as input for operation \"" + operationInfo.Title + "\" was written to by operation \"" + existingOperation.Title + "\" and must be declared as input"; + throw std::runtime_error(message); + } + } + } + + // Verify new outputs + for (auto fileId : operationResult.ObservedOutput) + { + // Ensure the file is not also an output + auto findObservedInput = std::find(operationResult.ObservedInput.begin(), operationResult.ObservedInput.end(), fileId); + if (findObservedInput != operationResult.ObservedInput.end()) + { + auto filePath = _fileSystemState.GetFilePath(fileId); + Log::Warning("File \"" + filePath.ToString() + "\" observed as both input and output for operation \"" + operationInfo.Title + "\""); + Log::Warning("Removing from input list for now. Will be treated as error in the future."); + operationResult.ObservedInput.erase(findObservedInput); + } + + // Ensure declared output is compatible + OperationId matchedOutputOperationId; + if (evaluateState.TryGetOutputFileOperation(fileId, matchedOutputOperationId)) + { + if (matchedOutputOperationId != operationInfo.Id) + { + auto filePath = _fileSystemState.GetFilePath(fileId); + auto& existingOperation = evaluateState.OperationGraph.GetOperationInfo(matchedOutputOperationId); + auto message = "File \"" + filePath.ToString() + "\" observed as output for operation \"" + operationInfo.Title + "\" was already written by operation \"" + existingOperation.Title + "\""; + throw std::runtime_error(message); + } + } + else + { + // Ensure new ouput does not create a dependency connection + const std::set* matchedInputOperationIds; + if (evaluateState.TryGetInputFileOperations(fileId, matchedInputOperationIds)) + { + if (!matchedInputOperationIds->contains(operationInfo.Id)) + { + auto filePath = _fileSystemState.GetFilePath(fileId); + auto message = "File \"" + filePath.ToString() + "\" observed as output from operation \"" + operationInfo.Title + "\" creates new dependency to existing declared inputs"; + throw std::runtime_error(message); + } + } + } + } + } }; } diff --git a/Source/Client/Core/Source/Build/BuildHistoryChecker.h b/Source/Client/Core/Source/Build/BuildHistoryChecker.h index 215121e4..cecc3ac6 100644 --- a/Source/Client/Core/Source/Build/BuildHistoryChecker.h +++ b/Source/Client/Core/Source/Build/BuildHistoryChecker.h @@ -23,7 +23,7 @@ namespace Soup::Core /// respect to the input files /// bool IsOutdated( - std::chrono::time_point lastEvaluateTime, + std::chrono::time_point lastEvaluateTime, FileId inputFile) { auto lastWriteTime = _fileSystemState.GetLastWriteTime(inputFile); @@ -109,7 +109,7 @@ namespace Soup::Core bool IsOutdated( FileId inputFile, FileId outputFile, - std::chrono::time_point outputFileLastWriteTime) + std::chrono::time_point outputFileLastWriteTime) { // Get the file state from the cache auto lastWriteTime = _fileSystemState.GetLastWriteTime(inputFile); diff --git a/Source/Client/Core/Source/Build/RecipeBuildRunner.h b/Source/Client/Core/Source/Build/BuildRunner.h similarity index 97% rename from Source/Client/Core/Source/Build/RecipeBuildRunner.h rename to Source/Client/Core/Source/Build/BuildRunner.h index 9ce3afda..77432a45 100644 --- a/Source/Client/Core/Source/Build/RecipeBuildRunner.h +++ b/Source/Client/Core/Source/Build/BuildRunner.h @@ -1,4 +1,4 @@ -// +// // Copyright (c) Soup. All rights reserved. // @@ -20,10 +20,10 @@ namespace Soup::Core { /// - /// The recipe build runner that knows how to perform the correct build for a recipe + /// The build runner that knows how to perform the correct build for a recipe /// and all of its development and runtime dependencies /// - export class RecipeBuildRunner + export class BuildRunner { private: // Root arguments @@ -48,9 +48,9 @@ namespace Soup::Core public: /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - RecipeBuildRunner( + BuildRunner( const RecipeBuildArguments& arguments, const ValueList& sdkParameters, const std::vector& sdkReadAccess, @@ -329,7 +329,7 @@ namespace Soup::Core auto parametersFile = soupTargetDirectory + BuildConstants::GenerateParametersFileName(); Log::Info("Check outdated parameters file: " + parametersFile.ToString()); - if (_arguments.ForceRebuild || IsOutdated(parametersTable, parametersFile)) + if (IsOutdated(parametersTable, parametersFile)) { Log::Info("Save Parameters file"); ValueTableManager::SaveState(parametersFile, parametersTable); @@ -360,7 +360,7 @@ namespace Soup::Core auto readAccessFile = soupTargetDirectory + BuildConstants::GenerateReadAccessFileName(); Log::Info("Check outdated read access file: " + readAccessFile.ToString()); - if (_arguments.ForceRebuild || IsOutdated(evaluateAllowedReadAccess, readAccessFile)) + if (IsOutdated(evaluateAllowedReadAccess, readAccessFile)) { Log::Info("Save Read Access file"); PathListManager::Save(readAccessFile, evaluateAllowedReadAccess); @@ -368,7 +368,7 @@ namespace Soup::Core auto writeAccessFile = soupTargetDirectory + BuildConstants::GenerateWriteAccessFileName(); Log::Info("Check outdated write access file: " + writeAccessFile.ToString()); - if (_arguments.ForceRebuild || IsOutdated(evaluateAllowedWriteAccess, writeAccessFile)) + if (IsOutdated(evaluateAllowedWriteAccess, writeAccessFile)) { Log::Info("Save Write Access file"); PathListManager::Save(writeAccessFile, evaluateAllowedWriteAccess); diff --git a/Source/Client/Core/Source/Build/FileSystemState.h b/Source/Client/Core/Source/Build/FileSystemState.h index 6601caad..ba8b62a3 100644 --- a/Source/Client/Core/Source/Build/FileSystemState.h +++ b/Source/Client/Core/Source/Build/FileSystemState.h @@ -41,7 +41,7 @@ namespace Soup::Core FileSystemState( FileId maxFileId, std::unordered_map files, - std::unordered_map>> writeCache) : + std::unordered_map>> writeCache) : _maxFileId(maxFileId), _files(std::move(files)), _fileLookup(), @@ -50,7 +50,9 @@ namespace Soup::Core // Build up the reverse lookup for new files for (auto& file : _files) { - auto insertResult = _fileLookup.emplace(file.second.ToString(), file.first); + auto normalizedFilePath = file.second.ToString(); + ToUpper(normalizedFilePath); + auto insertResult = _fileLookup.emplace(std::move(normalizedFilePath), file.first); if (!insertResult.second) throw std::runtime_error("The file was not unique in the provided set."); } @@ -86,7 +88,7 @@ namespace Soup::Core /// /// Find the write time for a given file id /// - std::optional> GetLastWriteTime(FileId file) + std::optional> GetLastWriteTime(FileId file) { auto findResult = _writeCache.find(file); if (findResult != _writeCache.end()) @@ -137,7 +139,9 @@ namespace Soup::Core if (!insertResult.second) throw std::runtime_error("The provided file id already exists in the file system state"); - auto insertLookupResult = _fileLookup.emplace(file.ToString(), result); + auto normalizedFilePath = file.ToString(); + ToUpper(normalizedFilePath); + auto insertLookupResult = _fileLookup.emplace(std::move(normalizedFilePath), result); if (!insertLookupResult.second) throw std::runtime_error("The file was not unique even though we just failed to find it"); } @@ -150,7 +154,9 @@ namespace Soup::Core /// bool TryFindFileId(const Path& file, FileId& fileId) const { - auto findResult = _fileLookup.find(file.ToString()); + auto normalizedFilePath = file.ToString(); + ToUpper(normalizedFilePath); + auto findResult = _fileLookup.find(normalizedFilePath); if (findResult != _fileLookup.end()) { fileId = findResult->second; @@ -196,23 +202,32 @@ namespace Soup::Core /// /// Update the write times for the provided set of files /// - std::optional> CheckFileWriteTime(FileId fileId) + std::optional> CheckFileWriteTime(FileId fileId) { auto& filePath = GetFilePath(fileId); // The file does not exist in the cache // Load the actual value and save it for later - std::optional> lastWriteTime = std::nullopt; - if (System::IFileSystem::Current().Exists(filePath)) + std::optional> lastWriteTime = std::nullopt; + std::chrono::time_point lastWriteTimeValue; + if (System::IFileSystem::Current().TryGetLastWriteTime(filePath, lastWriteTimeValue)) { - lastWriteTime = std::chrono::system_clock::from_time_t( - System::IFileSystem::Current().GetLastWriteTime(filePath)); + lastWriteTime = lastWriteTimeValue; } auto insertResult = _writeCache.insert_or_assign(fileId, lastWriteTime); return lastWriteTime; } + static void ToUpper(std::string& value) + { + std::transform( + value.begin(), + value.end(), + value.begin(), + [](unsigned char c) { return static_cast(std::toupper(c)); }); + } + private: // The maximum id that has been used for files // Used to ensure unique ids are generated across the entire system @@ -221,6 +236,6 @@ namespace Soup::Core std::unordered_map _files; std::unordered_map _fileLookup; - std::unordered_map>> _writeCache; + std::unordered_map>> _writeCache; }; } diff --git a/Source/Client/Core/Source/Build/SystemAccessTracker.h b/Source/Client/Core/Source/Build/SystemAccessTracker.h index 363c2167..1558a683 100644 --- a/Source/Client/Core/Source/Build/SystemAccessTracker.h +++ b/Source/Client/Core/Source/Build/SystemAccessTracker.h @@ -1297,7 +1297,7 @@ namespace Soup::Core else { auto value = filePath.ToString(); - ToUpper(value); + ToUpper(value); #ifdef TRACE_SYSTEM_ACCESS Log::Diag("TouchFileDelete " + value); @@ -1326,7 +1326,7 @@ namespace Soup::Core void TouchFileDeleteOnClose(const Path& filePath) { auto value = filePath.ToString(); - ToUpper(value); + ToUpper(value); #ifdef TRACE_SYSTEM_ACCESS Log::Diag("TouchFileDeleteOnClose " + value); diff --git a/Source/Client/Core/Source/Module.cpp b/Source/Client/Core/Source/Module.cpp index 63b9272f..8b8305bf 100644 --- a/Source/Client/Core/Source/Module.cpp +++ b/Source/Client/Core/Source/Module.cpp @@ -27,6 +27,7 @@ #undef max #undef min #undef CreateProcess +#undef GetCurrentTime export module Soup.Core; diff --git a/Source/Client/Core/Source/OperationGraph/OperationResult.h b/Source/Client/Core/Source/OperationGraph/OperationResult.h index d15885b9..aadfd6c5 100644 --- a/Source/Client/Core/Source/OperationGraph/OperationResult.h +++ b/Source/Client/Core/Source/OperationGraph/OperationResult.h @@ -17,14 +17,14 @@ namespace Soup::Core { public: bool WasSuccessfulRun; - std::chrono::time_point EvaluateTime; + std::chrono::time_point EvaluateTime; std::vector ObservedInput; std::vector ObservedOutput; public: OperationResult() : WasSuccessfulRun(false), - EvaluateTime(std::chrono::time_point::min()), + EvaluateTime(std::chrono::time_point::min()), ObservedInput(), ObservedOutput() { @@ -32,7 +32,7 @@ namespace Soup::Core OperationResult( bool wasSuccessfulRun, - std::chrono::time_point evaluateTime, + std::chrono::time_point evaluateTime, std::vector observedInput, std::vector observedOutput) : WasSuccessfulRun(wasSuccessfulRun), diff --git a/Source/Client/Core/Source/OperationGraph/OperationResultsReader.h b/Source/Client/Core/Source/OperationGraph/OperationResultsReader.h index 31469bb8..5ee0a7f8 100644 --- a/Source/Client/Core/Source/OperationGraph/OperationResultsReader.h +++ b/Source/Client/Core/Source/OperationGraph/OperationResultsReader.h @@ -14,10 +14,12 @@ namespace Soup::Core { private: // Binary Operation Results file format - static constexpr uint32_t FileVersion = 1; + static constexpr uint32_t FileVersion = 2; - // The offset from January 1, 1970 at 00:00:00.000 to January 1, 0001 at 00:00:00.000 in the Gregorian calendar - static constexpr long long UnixEpochOffset = 62135596800000; + // The time duration that represents how we store the values in the file using 64 bit integer with resolution of 100 nanoseconds + // Note: Unix Time, time since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, not counting leap seconds + using ContentTimePeriod = std::ratio<1, 10'000'000>; + using ContentDuration = std::chrono::duration; public: static OperationResults Deserialize(std::istream& stream, FileSystemState& fileSystemState) @@ -112,10 +114,13 @@ namespace Soup::Core // Read the value indicating if there was a successful run auto wasSuccessfulRun = ReadBoolean(content); - // Read the utc tick since January 1, 0001 at 00:00:00.000 in the Gregorian calendar - auto evaluateTimeMilliseconds = ReadInt64(content); - auto unixEvaluateTimeMilliseconds = std::chrono::milliseconds(evaluateTimeMilliseconds - UnixEpochOffset); - auto evaluateTime = std::chrono::time_point(unixEvaluateTimeMilliseconds); + // Read the tick offset of the system clock since its epoch + auto evaluateTimeTicks = ReadInt64(content); + auto evaluateTimeDuration = ContentDuration(evaluateTimeTicks); + + // Use system clock with a known epoch + auto evaluateTimeSystem = std::chrono::time_point(evaluateTimeDuration); + auto evaluateTimeFile = std::chrono::clock_cast(evaluateTimeSystem); // Read the observed input files auto observedInput = ReadFileIdList(content, activeFileIdMap); @@ -125,7 +130,7 @@ namespace Soup::Core auto result = OperationResult( wasSuccessfulRun, - evaluateTime, + evaluateTimeFile, std::move(observedInput), std::move(observedOutput)); diff --git a/Source/Client/Core/Source/OperationGraph/OperationResultsWriter.h b/Source/Client/Core/Source/OperationGraph/OperationResultsWriter.h index 10e4b0ce..142cc4c4 100644 --- a/Source/Client/Core/Source/OperationGraph/OperationResultsWriter.h +++ b/Source/Client/Core/Source/OperationGraph/OperationResultsWriter.h @@ -14,10 +14,12 @@ namespace Soup::Core { private: // Binary Operation results file format - static constexpr uint32_t FileVersion = 1; + static constexpr uint32_t FileVersion = 2; - // The offset from January 1, 1970 at 00:00:00.000 to January 1, 0001 at 00:00:00.000 in the Gregorian calendar - static constexpr long long UnixEpochOffset = 62135596800000; + // The time duration that represents how we store the values in the file using 64 bit integer with resolution of 100 nanoseconds + // Note: Unix Time, time since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, not counting leap seconds + using ContentTimePeriod = std::ratio<1, 10'000'000>; + using ContentDuration = std::chrono::duration; public: static void Serialize( @@ -59,10 +61,12 @@ namespace Soup::Core // Write out the value indicating if there was a successful run WriteValue(stream, result.WasSuccessfulRun); - // Write out the utc milliseconds since January 1, 0001 at 00:00:00.000 in the Gregorian calendar - auto unixEvaluateTimeMilliseconds = std::chrono::time_point_cast(result.EvaluateTime).time_since_epoch().count(); - auto evaluateTimeMilliseconds = unixEvaluateTimeMilliseconds + UnixEpochOffset; - WriteValue(stream, evaluateTimeMilliseconds); + // Use system clock with a known epoch + auto evaluateTimeSystem = std::chrono::clock_cast(result.EvaluateTime); + + // Write the tick offset of the system clock since its epoch + auto evaluateTimeDuration = ContentDuration(evaluateTimeSystem.time_since_epoch()); + WriteValue(stream, evaluateTimeDuration.count()); // Write out the observed input files WriteValues(stream, result.ObservedInput); diff --git a/Source/Client/Core/Source/Utils/Helpers.h b/Source/Client/Core/Source/Utils/Helpers.h deleted file mode 100644 index 00791367..00000000 --- a/Source/Client/Core/Source/Utils/Helpers.h +++ /dev/null @@ -1,77 +0,0 @@ -// -// Copyright (c) Soup. All rights reserved. -// - -#pragma once - -namespace Soup::Core -{ - export std::string EscapeString(std::string_view value) - { - std::stringstream stringBuilder; - - // TODO: THIS IS HORRIBLY WRONG! Utf8 could have multi-byte characters - for (size_t i = 0; i < value.size(); i++) - { - // Add the escape character to all known escapes - // TODO: This is only a partial set - if (value[i] == '\\' || value[i] == '"') - stringBuilder << '\\'; - - stringBuilder << value[i]; - } - - return stringBuilder.str(); - } - - export std::string ToUpper(const std::string& value) - { - std::string result = value; - std::transform( - result.begin(), - result.end(), - result.begin(), - [](unsigned char c) { return static_cast(std::toupper(c)); }); - - return result; - } - - export std::string ToString(const std::vector& values) - { - std::stringstream stringBuilder; - - bool first = true; - for (auto& value : values) - { - if (!first) - stringBuilder << " "; - - stringBuilder << value; - first = false; - } - - return stringBuilder.str(); - } - - export std::string ToString(const std::vector& values) - { - std::stringstream stringBuilder; - - bool first = true; - for (auto& value : values) - { - if (!first) - stringBuilder << " "; - - stringBuilder << value.ToString(); - first = false; - } - - return stringBuilder.str(); - } - - export std::string ToString(bool value) - { - return value ? "true" : "false"; - } -} \ No newline at end of file diff --git a/Source/Client/Core/UnitTests/Build/BuildEngineTests.h b/Source/Client/Core/UnitTests/Build/BuildEngineTests.h index 5f51d1ed..ba25f2f9 100644 --- a/Source/Client/Core/UnitTests/Build/BuildEngineTests.h +++ b/Source/Client/Core/UnitTests/Build/BuildEngineTests.h @@ -17,6 +17,10 @@ namespace Soup::Core::UnitTests auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + // Register the test file system auto fileSystem = std::make_shared(); auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); @@ -108,6 +112,14 @@ namespace Soup::Core::UnitTests testListener->GetMessages(), "Verify log messages match expected."); + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected file system requests Assert::AreEqual( std::vector({ @@ -197,17 +209,14 @@ namespace Soup::Core::UnitTests Path("C:/WorkingDirectory/MyPackage/out/J_HqSstV55vlb-x6RWC_hLRFRDU/.soup/Generate.bor")); auto myPackageGenerateResults = OperationResultsReader::Deserialize(myPackageGenerateResultsMockFile->Content, fileSystemState); - // TODO: Clear time for now until mocked - for (auto& result : myPackageGenerateResults.GetResults()) - result.second.EvaluateTime = std::chrono::time_point::min(); - Assert::AreEqual( OperationResults({ { 1, OperationResult( true, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), {}, {}) }, diff --git a/Source/Client/Core/UnitTests/Build/BuildEvaluateEngineTests.h b/Source/Client/Core/UnitTests/Build/BuildEvaluateEngineTests.h index a92fedca..24c9f368 100644 --- a/Source/Client/Core/UnitTests/Build/BuildEvaluateEngineTests.h +++ b/Source/Client/Core/UnitTests/Build/BuildEvaluateEngineTests.h @@ -9,12 +9,16 @@ namespace Soup::Core::UnitTests { class BuildEvaluateEngineTests { + static const long GENERIC_WRITE = 0x40000000L; + public: // [[Fact]] void Initialize() { auto fileSystemState = FileSystemState(); - auto uut = BuildEvaluateEngine(fileSystemState); + auto uut = BuildEvaluateEngine( + false, + fileSystemState); } // [[Fact]] @@ -34,7 +38,9 @@ namespace Soup::Core::UnitTests auto scopedProcessManager = ScopedProcessManagerRegister(processManager); // Setup the input build state - auto uut = BuildEvaluateEngine(fileSystemState); + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph(); @@ -51,6 +57,12 @@ namespace Soup::Core::UnitTests Assert::IsFalse(ranOperations, "Verify no operations ran"); + // Verify operation results + Assert::AreEqual( + std::unordered_map(), + operationResults.GetResults(), + "Verify operation results match expected."); + // Verify expected logs Assert::AreEqual( std::vector({}), @@ -77,6 +89,10 @@ namespace Soup::Core::UnitTests auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + // Register the test file system auto fileSystem = std::make_shared(); auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); @@ -91,8 +107,18 @@ namespace Soup::Core::UnitTests auto detourProcessManager = std::make_shared(); auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + callback.OnCreateFile2(L"InputFile2.in", 0, 0, 0, 0, false); + callback.OnCreateFile2(L"OutputFile2.out", GENERIC_WRITE, 0, 0, 0, false); + }); + // Setup the input build state - auto uut = BuildEvaluateEngine(fileSystemState); + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -125,6 +151,23 @@ namespace Soup::Core::UnitTests Assert::IsTrue(ranOperations, "Verify ran operations"); + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + { 4, }, + { 5, }) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + // Verify expected logs Assert::AreEqual( std::vector({ @@ -140,9 +183,19 @@ namespace Soup::Core::UnitTests testListener->GetMessages(), "Verify log messages match expected."); + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected file system requests Assert::AreEqual( - std::vector({}), + std::vector({ + "TryGetLastWriteTime: C:/TestWorkingDirectory/OUTPUTFILE2.OUT", + }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -161,34 +214,41 @@ namespace Soup::Core::UnitTests } // [[Fact]] - void Evaluate_OneOperation_Incremental_MissingFileInfo() + void Execute_OneOperation_ObservedInputAndOutput_CircularReference_RemoveInput() { // Register the test listener auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); - auto executableInputTime = std::chrono::sys_days(May/22/2015) + 9h + 9min; + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); // Register the test file system auto fileSystem = std::make_shared(); auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); auto fileSystemState = FileSystemState( - 3, + 1, std::unordered_map({ - { 1, Path("C:/TestWorkingDirectory/InputFile.in") }, - { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, - { 3, Path("C:/TestWorkingDirectory/Command.exe") }, - }), - std::unordered_map>>({ - { 3, executableInputTime }, })); // Register the test process manager auto detourProcessManager = std::make_shared(); auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + // Read and write the same file + callback.OnCreateFile2(L"File.txt", 0, 0, 0, 0, false); + callback.OnCreateFile2(L"File.txt", GENERIC_WRITE, 0, 0, 0, false); + }); + // Setup the input build state - auto uut = BuildEvaluateEngine(fileSystemState); + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -201,26 +261,152 @@ namespace Soup::Core::UnitTests Path("C:/TestWorkingDirectory/"), Path("./Command.exe"), "Arguments"), - { 1, }, - { 2, }, + { }, + { }, { }, { }, { }, 1), }); - auto operationResults = OperationResults({ + auto operationResults = OperationResults(); + auto temporaryDirectory = Path(); + auto globalAllowedReadAccess = std::vector(); + auto globalAllowedWriteAccess = std::vector(); + + auto ranOperations = uut.Evaluate( + operationGraph, + operationResults, + temporaryDirectory, + globalAllowedReadAccess, + globalAllowedWriteAccess); + + Assert::IsTrue(ranOperations, "Verify ran operations"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map( { - 1, - OperationResult( - true, - std::chrono::sys_days(May/22/2015) + 9h + 10min, + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + { }, + { 2, }) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected logs + Assert::AreEqual( + std::vector({ + "DIAG: Build evaluation start", + "DIAG: Check for previous operation invocation", + "INFO: Operation has no successful previous invocation", + "HIGH: TestCommand: 1", + "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command.exe Arguments", + "DIAG: Allowed Read Access:", + "DIAG: Allowed Write Access:", + "WARN: File \"C:/TestWorkingDirectory/FILE.TXT\" observed as both input and output for operation \"TestCommand: 1\"", + "WARN: Removing from input list for now. Will be treated as error in the future.", + "DIAG: Build evaluation end", + }), + testListener->GetMessages(), + "Verify log messages match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + + // Verify expected file system requests + Assert::AreEqual( + std::vector({ + "TryGetLastWriteTime: C:/TestWorkingDirectory/FILE.TXT", + }), + fileSystem->GetRequests(), + "Verify file system requests match expected."); + + // Verify expected process requests + Assert::AreEqual( + std::vector({ + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + "ProcessStart: 1", + "WaitForExit: 1", + "GetStandardOutput: 1", + "GetStandardError: 1", + "GetExitCode: 1", + }), + detourProcessManager->GetRequests(), + "Verify detour process manager requests match expected."); + } + + // [[Fact]] + void Execute_OneOperation_ObservedInput_CircularReference_RemoveInput() + { + // Register the test listener + auto testListener = std::make_shared(); + auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Register the test file system + auto fileSystem = std::make_shared(); + auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); + auto fileSystemState = FileSystemState( + 1, + std::unordered_map({ + { 1, Path("C:/TestWorkingDirectory/File.txt") }, + })); + + // Register the test process manager + auto detourProcessManager = std::make_shared(); + auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + // Read and write the same file + callback.OnCreateFile2(L"File.txt", 0, 0, 0, 0, false); + callback.OnCreateFile2(L"File.txt", GENERIC_WRITE, 0, 0, 0, false); + }); + + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); + + // Evaluate the build + auto operationGraph = OperationGraph( + { 1, }, + { + OperationInfo( + 1, + "TestCommand: 1", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command.exe"), + "Arguments"), + { }, { 1, }, - { 2, }) - }, - }); + { }, + { }, + { }, + 1), + }); + auto operationResults = OperationResults(); auto temporaryDirectory = Path(); auto globalAllowedReadAccess = std::vector(); auto globalAllowedWriteAccess = std::vector(); + auto ranOperations = uut.Evaluate( operationGraph, operationResults, @@ -230,25 +416,52 @@ namespace Soup::Core::UnitTests Assert::IsTrue(ranOperations, "Verify ran operations"); + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + { }, + { 1, }) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + // Verify expected logs Assert::AreEqual( std::vector({ "DIAG: Build evaluation start", "DIAG: Check for previous operation invocation", - "INFO: Output target does not exist: C:/TestWorkingDirectory/OutputFile.out", + "INFO: Operation has no successful previous invocation", "HIGH: TestCommand: 1", "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command.exe Arguments", "DIAG: Allowed Read Access:", "DIAG: Allowed Write Access:", + "WARN: File \"C:/TestWorkingDirectory/File.txt\" observed as both input and output for operation \"TestCommand: 1\"", + "WARN: Removing from input list for now. Will be treated as error in the future.", "DIAG: Build evaluation end", }), testListener->GetMessages(), "Verify log messages match expected."); + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected file system requests Assert::AreEqual( std::vector({ - "Exists: C:/TestWorkingDirectory/OutputFile.out", + "TryGetLastWriteTime: C:/TestWorkingDirectory/File.txt", }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -268,15 +481,18 @@ namespace Soup::Core::UnitTests } // [[Fact]] - void Evaluate_OneOperation_Incremental_MissingTargetFile() + void Evaluate_OneOperation_Incremental_MissingFileInfo() { // Register the test listener auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); - // Setup the input file only - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; - auto executableInputTime = std::chrono::sys_days(May/22/2015) + 9h + 9min; + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + auto executableInputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 9min); // Register the test file system auto fileSystem = std::make_shared(); @@ -288,9 +504,7 @@ namespace Soup::Core::UnitTests { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, { 3, Path("C:/TestWorkingDirectory/Command.exe") }, }), - std::unordered_map>>({ - { 1, inputTime }, - { 2, std::nullopt }, + std::unordered_map>>({ { 3, executableInputTime }, })); @@ -298,8 +512,10 @@ namespace Soup::Core::UnitTests auto detourProcessManager = std::make_shared(); auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); - // Create the build state - auto uut = BuildEvaluateEngine(fileSystemState); + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -324,7 +540,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - std::chrono::sys_days(May/22/2015) + 9h + 10min, + std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 10min), { 1, }, { 2, }) }, @@ -341,6 +558,23 @@ namespace Soup::Core::UnitTests Assert::IsTrue(ranOperations, "Verify ran operations"); + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + {}, + {}) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + // Verify expected logs Assert::AreEqual( std::vector({ @@ -356,9 +590,19 @@ namespace Soup::Core::UnitTests testListener->GetMessages(), "Verify log messages match expected."); + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected file system requests Assert::AreEqual( - std::vector({}), + std::vector({ + "TryGetLastWriteTime: C:/TestWorkingDirectory/OutputFile.out", + }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -377,16 +621,21 @@ namespace Soup::Core::UnitTests } // [[Fact]] - void Evaluate_OneOperation_Incremental_OutOfDate() + void Evaluate_OneOperation_Incremental_MissingTargetFile() { // Register the test listener auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); - // Setup the input/output files to be out of date - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 10min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; - auto executableInputTime = std::chrono::sys_days(May/22/2015) + 9h + 9min; + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Setup the input file only + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); + auto executableInputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 9min); // Register the test file system auto fileSystem = std::make_shared(); @@ -398,9 +647,9 @@ namespace Soup::Core::UnitTests { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, { 3, Path("C:/TestWorkingDirectory/Command.exe") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, inputTime }, - { 2, outputTime }, + { 2, std::nullopt }, { 3, executableInputTime }, })); @@ -408,8 +657,10 @@ namespace Soup::Core::UnitTests auto detourProcessManager = std::make_shared(); auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); - // Setup the input build state - auto uut = BuildEvaluateEngine(fileSystemState); + // Create the build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -434,7 +685,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - std::chrono::sys_days(May/22/2015) + 9h + 10min, + std::chrono::clock_cast(std::chrono::sys_days(May/22/2015) + 9h + 10min), { 1, }, { 2, }) }, @@ -451,12 +702,29 @@ namespace Soup::Core::UnitTests Assert::IsTrue(ranOperations, "Verify ran operations"); + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + {}, + {}) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + // Verify expected logs Assert::AreEqual( std::vector({ "DIAG: Build evaluation start", "DIAG: Check for previous operation invocation", - "INFO: Input altered after target [C:/TestWorkingDirectory/InputFile.in] -> [C:/TestWorkingDirectory/OutputFile.out]", + "INFO: Output target does not exist: C:/TestWorkingDirectory/OutputFile.out", "HIGH: TestCommand: 1", "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command.exe Arguments", "DIAG: Allowed Read Access:", @@ -466,6 +734,14 @@ namespace Soup::Core::UnitTests testListener->GetMessages(), "Verify log messages match expected."); + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected file system requests Assert::AreEqual( std::vector({}), @@ -487,16 +763,23 @@ namespace Soup::Core::UnitTests } // [[Fact]] - void Evaluate_OneOperation_Incremental_Executable_OutOfDate() + void Evaluate_OneOperation_Incremental_OutOfDate() { // Register the test listener auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + // Setup the input/output files to be out of date - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 10min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 9min; - auto executableInputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 10min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); + auto executableInputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 9min); // Register the test file system auto fileSystem = std::make_shared(); @@ -508,7 +791,7 @@ namespace Soup::Core::UnitTests { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, { 3, Path("C:/TestWorkingDirectory/Command.exe") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, inputTime }, { 2, outputTime }, { 3, executableInputTime }, @@ -519,7 +802,9 @@ namespace Soup::Core::UnitTests auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); // Setup the input build state - auto uut = BuildEvaluateEngine(fileSystemState); + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -544,7 +829,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - std::chrono::sys_days(May/22/2015) + 9h + 0min, + std::chrono::clock_cast(std::chrono::sys_days(May/22/2015) + 9h + 10min), { 1, }, { 2, }) }, @@ -561,12 +846,37 @@ namespace Soup::Core::UnitTests Assert::IsTrue(ranOperations, "Verify ran operations"); + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + {}, + {}) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + // Verify expected logs Assert::AreEqual( std::vector({ "DIAG: Build evaluation start", "DIAG: Check for previous operation invocation", - "INFO: Input altered after last evaluate [C:/TestWorkingDirectory/Command.exe]", + "INFO: Input altered after target [C:/TestWorkingDirectory/InputFile.in] -> [C:/TestWorkingDirectory/OutputFile.out]", "HIGH: TestCommand: 1", "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command.exe Arguments", "DIAG: Allowed Read Access:", @@ -597,16 +907,23 @@ namespace Soup::Core::UnitTests } // [[Fact]] - void Evaluate_OneOperation_Incremental_UpToDate() + void Evaluate_OneOperation_Incremental_Executable_OutOfDate() { // Register the test listener auto testListener = std::make_shared(); auto scopedTraceListener = ScopedTraceListenerRegister(testListener); - // Setup the input/output files to be up to date - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; - auto executableInputTime = std::chrono::sys_days(May/22/2015) + 9h + 10min; + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Setup the input/output files to be out of date + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 10min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 9min); + auto executableInputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); // Register the test file system auto fileSystem = std::make_shared(); @@ -618,18 +935,20 @@ namespace Soup::Core::UnitTests { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, { 3, Path("C:/TestWorkingDirectory/Command.exe") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, inputTime }, { 2, outputTime }, { 3, executableInputTime }, })); // Register the test process manager - auto processManager = std::make_shared(); - auto scopedProcessManager = ScopedProcessManagerRegister(processManager); + auto detourProcessManager = std::make_shared(); + auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); - // Create the initial build state - auto uut = BuildEvaluateEngine(fileSystemState); + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); // Evaluate the build auto operationGraph = OperationGraph( @@ -654,7 +973,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - std::chrono::sys_days(May/22/2015) + 9h + 15min, + std::chrono::clock_cast(std::chrono::sys_days(May/22/2015) + 9h + 0min), { 1, }, { 2, }) }, @@ -669,15 +988,43 @@ namespace Soup::Core::UnitTests globalAllowedReadAccess, globalAllowedWriteAccess); - Assert::IsFalse(ranOperations, "Verify did not run operations"); + Assert::IsTrue(ranOperations, "Verify ran operations"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::time_point()), + {}, + {}) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); // Verify expected logs Assert::AreEqual( std::vector({ "DIAG: Build evaluation start", "DIAG: Check for previous operation invocation", - "INFO: Up to date", - "INFO: TestCommand: 1", + "INFO: Input altered after last evaluate [C:/TestWorkingDirectory/Command.exe]", + "HIGH: TestCommand: 1", + "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command.exe Arguments", + "DIAG: Allowed Read Access:", + "DIAG: Allowed Write Access:", "DIAG: Build evaluation end", }), testListener->GetMessages(), @@ -688,6 +1035,545 @@ namespace Soup::Core::UnitTests std::vector({}), fileSystem->GetRequests(), "Verify file system requests match expected."); + + // Verify expected process requests + Assert::AreEqual( + std::vector({ + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + "ProcessStart: 1", + "WaitForExit: 1", + "GetStandardOutput: 1", + "GetStandardError: 1", + "GetExitCode: 1", + }), + detourProcessManager->GetRequests(), + "Verify detour process manager requests match expected."); + } + + // [[Fact]] + void Evaluate_OneOperation_Incremental_UpToDate() + { + // Register the test listener + auto testListener = std::make_shared(); + auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + + // Setup the input/output files to be up to date + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); + auto executableInputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 10min); + + // Register the test file system + auto fileSystem = std::make_shared(); + auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); + auto fileSystemState = FileSystemState( + 3, + std::unordered_map({ + { 1, Path("C:/TestWorkingDirectory/InputFile.in") }, + { 2, Path("C:/TestWorkingDirectory/OutputFile.out") }, + { 3, Path("C:/TestWorkingDirectory/Command.exe") }, + }), + std::unordered_map>>({ + { 1, inputTime }, + { 2, outputTime }, + { 3, executableInputTime }, + })); + + // Register the test process manager + auto processManager = std::make_shared(); + auto scopedProcessManager = ScopedProcessManagerRegister(processManager); + + // Create the initial build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); + + // Evaluate the build + auto operationGraph = OperationGraph( + { 1, }, + { + OperationInfo( + 1, + "TestCommand: 1", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command.exe"), + "Arguments"), + { 1, }, + { 2, }, + { }, + { }, + { }, + 1), + }); + auto operationResults = OperationResults({ + { + 1, + OperationResult( + true, + std::chrono::clock_cast(std::chrono::sys_days(May/22/2015) + 9h + 15min), + { 1, }, + { 2, }) + }, + }); + auto temporaryDirectory = Path(); + auto globalAllowedReadAccess = std::vector(); + auto globalAllowedWriteAccess = std::vector(); + auto ranOperations = uut.Evaluate( + operationGraph, + operationResults, + temporaryDirectory, + globalAllowedReadAccess, + globalAllowedWriteAccess); + + Assert::IsFalse(ranOperations, "Verify did not run operations"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map( + { + { + 1, + OperationResult( + true, + std::chrono::clock_cast( + std::chrono::sys_days(May / 22 / 2015) + 9h + 15min), + { 1, }, + { 2, }) + }, + }), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected logs + Assert::AreEqual( + std::vector({ + "DIAG: Build evaluation start", + "DIAG: Check for previous operation invocation", + "INFO: Up to date", + "INFO: TestCommand: 1", + "DIAG: Build evaluation end", + }), + testListener->GetMessages(), + "Verify log messages match expected."); + + // Verify expected file system requests + Assert::AreEqual( + std::vector({}), + fileSystem->GetRequests(), + "Verify file system requests match expected."); + } + + // [[Fact]] + void Execute_TwoOperations_DuplicateOutputFile_Fails() + { + // Register the test listener + auto testListener = std::make_shared(); + auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Register the test file system + auto fileSystem = std::make_shared(); + auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); + auto fileSystemState = FileSystemState( + 3, + std::unordered_map({ + { 1, Path("C:/TestWorkingDirectory/OutputFile.out") }, + })); + + // Register the test process manager + auto detourProcessManager = std::make_shared(); + auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + callback.OnCreateFile2(L"OutputFile.out", GENERIC_WRITE, 0, 0, 0, false); + }); + + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); + + // Evaluate the build + auto operationGraph = OperationGraph( + { 1, }, + { + OperationInfo( + 1, + "TestCommand: 1", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command1.exe"), + "Arguments"), + { }, + { }, + { }, + { }, + { 2 }, + 1), + OperationInfo( + 2, + "TestCommand: 2", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command2.exe"), + "Arguments"), + { }, + { 1, }, + { }, + { }, + { }, + 1), + }); + auto operationResults = OperationResults(); + auto temporaryDirectory = Path(); + auto globalAllowedReadAccess = std::vector(); + auto globalAllowedWriteAccess = std::vector(); + + auto exception = Assert::Throws([&]() + { + auto ranOperations = uut.Evaluate( + operationGraph, + operationResults, + temporaryDirectory, + globalAllowedReadAccess, + globalAllowedWriteAccess); + }); + + Assert::AreEqual( + "File \"C:/TestWorkingDirectory/OutputFile.out\" observed as output for operation \"TestCommand: 1\" was already written by operation \"TestCommand: 2\"", + exception.what(), + "Verify Exception message"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map(), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected logs + Assert::AreEqual( + std::vector({ + "DIAG: Build evaluation start", + "DIAG: Check for previous operation invocation", + "INFO: Operation has no successful previous invocation", + "HIGH: TestCommand: 1", + "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command1.exe Arguments", + "DIAG: Allowed Read Access:", + "DIAG: Allowed Write Access:", + }), + testListener->GetMessages(), + "Verify log messages match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + + // Verify expected file system requests + Assert::AreEqual( + std::vector({ + "TryGetLastWriteTime: C:/TestWorkingDirectory/OutputFile.out", + }), + fileSystem->GetRequests(), + "Verify file system requests match expected."); + + // Verify expected process requests + Assert::AreEqual( + std::vector({ + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + "ProcessStart: 1", + "WaitForExit: 1", + "GetStandardOutput: 1", + "GetStandardError: 1", + "GetExitCode: 1", + }), + detourProcessManager->GetRequests(), + "Verify detour process manager requests match expected."); + } + + // [[Fact]] + void Execute_TwoOperations_UndeclaredOutputWithDeclaredInput_Fails() + { + // Register the test listener + auto testListener = std::make_shared(); + auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Register the test file system + auto fileSystem = std::make_shared(); + auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); + auto fileSystemState = FileSystemState( + 3, + std::unordered_map({ + { 1, Path("C:/TestWorkingDirectory/File.txt") }, + })); + + // Register the test process manager + auto detourProcessManager = std::make_shared(); + auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + callback.OnCreateFile2(L"File.txt", GENERIC_WRITE, 0, 0, 0, false); + }); + + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); + + // Evaluate the build + auto operationGraph = OperationGraph( + { 1, 2, }, + { + OperationInfo( + 1, + "TestCommand: 1", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command1.exe"), + "Arguments"), + { }, + { }, + { }, + { }, + { }, + 1), + OperationInfo( + 2, + "TestCommand: 2", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command2.exe"), + "Arguments"), + { 1, }, + { }, + { }, + { }, + { }, + 1), + }); + auto operationResults = OperationResults(); + auto temporaryDirectory = Path(); + auto globalAllowedReadAccess = std::vector(); + auto globalAllowedWriteAccess = std::vector(); + + auto exception = Assert::Throws([&]() + { + auto ranOperations = uut.Evaluate( + operationGraph, + operationResults, + temporaryDirectory, + globalAllowedReadAccess, + globalAllowedWriteAccess); + }); + + Assert::AreEqual( + "File \"C:/TestWorkingDirectory/File.txt\" observed as output from operation \"TestCommand: 1\" creates new dependency to existing declared inputs", + exception.what(), + "Verify Exception message"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map(), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected logs + Assert::AreEqual( + std::vector({ + "DIAG: Build evaluation start", + "DIAG: Check for previous operation invocation", + "INFO: Operation has no successful previous invocation", + "HIGH: TestCommand: 1", + "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command1.exe Arguments", + "DIAG: Allowed Read Access:", + "DIAG: Allowed Write Access:", + }), + testListener->GetMessages(), + "Verify log messages match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + + // Verify expected file system requests + Assert::AreEqual( + std::vector({ + "TryGetLastWriteTime: C:/TestWorkingDirectory/File.txt", + }), + fileSystem->GetRequests(), + "Verify file system requests match expected."); + + // Verify expected process requests + Assert::AreEqual( + std::vector({ + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + "ProcessStart: 1", + "WaitForExit: 1", + "GetStandardOutput: 1", + "GetStandardError: 1", + "GetExitCode: 1", + }), + detourProcessManager->GetRequests(), + "Verify detour process manager requests match expected."); + } + + // [[Fact]] + void Execute_TwoOperations_UndeclaredInputWithDeclaredOutput_Fails() + { + // Register the test listener + auto testListener = std::make_shared(); + auto scopedTraceListener = ScopedTraceListenerRegister(testListener); + + // Register the test system + auto system = std::make_shared(); + auto scopedSystem = ScopedSystemRegister(system); + + // Register the test file system + auto fileSystem = std::make_shared(); + auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); + auto fileSystemState = FileSystemState( + 3, + std::unordered_map({ + { 1, Path("C:/TestWorkingDirectory/File.txt") }, + })); + + // Register the test process manager + auto detourProcessManager = std::make_shared(); + auto scopedDetourProcessManager = Monitor::ScopedDetourProcessManagerRegister(detourProcessManager); + + detourProcessManager->RegisterExecuteCallback( + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + [](Monitor::IDetourCallback& callback) + { + callback.OnCreateFile2(L"File.txt", 0, 0, 0, 0, false); + }); + + // Setup the input build state + auto uut = BuildEvaluateEngine( + false, + fileSystemState); + + // Evaluate the build + auto operationGraph = OperationGraph( + { 1, 2, }, + { + OperationInfo( + 1, + "TestCommand: 1", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command1.exe"), + "Arguments"), + { }, + { }, + { }, + { }, + { }, + 1), + OperationInfo( + 2, + "TestCommand: 2", + CommandInfo( + Path("C:/TestWorkingDirectory/"), + Path("./Command2.exe"), + "Arguments"), + { }, + { 1, }, + { }, + { }, + { }, + 1), + }); + auto operationResults = OperationResults(); + auto temporaryDirectory = Path(); + auto globalAllowedReadAccess = std::vector(); + auto globalAllowedWriteAccess = std::vector(); + + auto exception = Assert::Throws([&]() + { + auto ranOperations = uut.Evaluate( + operationGraph, + operationResults, + temporaryDirectory, + globalAllowedReadAccess, + globalAllowedWriteAccess); + }); + + Assert::AreEqual( + "File \"C:/TestWorkingDirectory/File.txt\" observed as input for operation \"TestCommand: 1\" was written to by operation \"TestCommand: 2\" and must be declared as input", + exception.what(), + "Verify Exception message"); + + // Verify operation results + Assert::AreEqual( + std::unordered_map(), + operationResults.GetResults(), + "Verify operation results match expected."); + + // Verify expected logs + Assert::AreEqual( + std::vector({ + "DIAG: Build evaluation start", + "DIAG: Check for previous operation invocation", + "INFO: Operation has no successful previous invocation", + "HIGH: TestCommand: 1", + "DIAG: Execute: [C:/TestWorkingDirectory/] ./Command1.exe Arguments", + "DIAG: Allowed Read Access:", + "DIAG: Allowed Write Access:", + }), + testListener->GetMessages(), + "Verify log messages match expected."); + + // Verify expected system requests + Assert::AreEqual( + std::vector({ + "GetCurrentTime", + }), + system->GetRequests(), + "Verify system requests match expected."); + + // Verify expected file system requests + Assert::AreEqual( + std::vector({ + }), + fileSystem->GetRequests(), + "Verify file system requests match expected."); + + // Verify expected process requests + Assert::AreEqual( + std::vector({ + "CreateDetourProcess: 1 [C:/TestWorkingDirectory/] ./Command1.exe Arguments Environment [2] 1 AllowedRead [0] AllowedWrite [0]", + "ProcessStart: 1", + "WaitForExit: 1", + "GetStandardOutput: 1", + "GetStandardError: 1", + "GetExitCode: 1", + }), + detourProcessManager->GetRequests(), + "Verify detour process manager requests match expected."); } }; } diff --git a/Source/Client/Core/UnitTests/Build/BuildHistoryCheckerTests.h b/Source/Client/Core/UnitTests/Build/BuildHistoryCheckerTests.h index b4893c99..6295493d 100644 --- a/Source/Client/Core/UnitTests/Build/BuildHistoryCheckerTests.h +++ b/Source/Client/Core/UnitTests/Build/BuildHistoryCheckerTests.h @@ -22,7 +22,7 @@ namespace Soup::Core::UnitTests std::unordered_map({ { 1, Path("C:/Root/Output.bin") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, std::nullopt }, })); @@ -64,7 +64,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 2, std::nullopt }, })); @@ -94,7 +94,7 @@ namespace Soup::Core::UnitTests // Verify expected file system requests Assert::AreEqual( std::vector({ - "Exists: C:/Root/Output.bin", + "TryGetLastWriteTime: C:/Root/Output.bin", }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -114,7 +114,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, std::nullopt }, { 2, std::nullopt }, })); @@ -155,7 +155,8 @@ namespace Soup::Core::UnitTests auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); // Create the file state - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); // Initialize the file system state auto fileSystemState = FileSystemState( @@ -164,7 +165,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, outputTime }, })); @@ -192,7 +193,7 @@ namespace Soup::Core::UnitTests // Verify expected file system requests Assert::AreEqual( std::vector({ - "Exists: C:/Root/Input.cpp", + "TryGetLastWriteTime: C:/Root/Input.cpp", }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -210,7 +211,8 @@ namespace Soup::Core::UnitTests auto scopedFileSystem = ScopedFileSystemRegister(fileSystem); // Create the file state - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); // Initialize the file system state auto fileSystemState = FileSystemState( @@ -219,7 +221,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, outputTime }, { 2, std::nullopt }, })); @@ -260,8 +262,10 @@ namespace Soup::Core::UnitTests auto scopedTraceListener = ScopedTraceListenerRegister(testListener); // Create the file state - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 13min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 13min); // Initialize the file system state auto fileSystemState = FileSystemState( @@ -270,7 +274,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, outputTime }, { 2, inputTime }, })); @@ -307,8 +311,10 @@ namespace Soup::Core::UnitTests auto scopedTraceListener = ScopedTraceListenerRegister(testListener); // Create the file state - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); // Initialize the file system state auto fileSystemState = FileSystemState( @@ -317,7 +323,7 @@ namespace Soup::Core::UnitTests { 1, Path("C:/Root/Output.bin") }, { 2, Path("C:/Root/Input.cpp") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, outputTime }, { 2, inputTime }, })); @@ -352,8 +358,10 @@ namespace Soup::Core::UnitTests auto scopedTraceListener = ScopedTraceListenerRegister(testListener); // Create the file state - auto outputTime = std::chrono::sys_days(May/22/2015) + 9h + 12min; - auto inputTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; + auto outputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 12min); + auto inputTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); // Initialize the file system state auto fileSystemState = FileSystemState( @@ -363,7 +371,7 @@ namespace Soup::Core::UnitTests { 2, Path("C:/Root/Input.cpp") }, { 3, Path("C:/Input.h") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 1, outputTime }, { 2, inputTime }, { 3, inputTime }, diff --git a/Source/Client/Core/UnitTests/Build/RecipeBuildRunnerTests.h b/Source/Client/Core/UnitTests/Build/BuildRunnerTests.h similarity index 99% rename from Source/Client/Core/UnitTests/Build/RecipeBuildRunnerTests.h rename to Source/Client/Core/UnitTests/Build/BuildRunnerTests.h index 085e162c..ff407d45 100644 --- a/Source/Client/Core/UnitTests/Build/RecipeBuildRunnerTests.h +++ b/Source/Client/Core/UnitTests/Build/BuildRunnerTests.h @@ -1,4 +1,4 @@ -// +// // Copyright (c) Soup. All rights reserved. // @@ -7,7 +7,7 @@ namespace Soup::Core::UnitTests { - class RecipeBuildRunnerTests + class BuildRunnerTests { public: // [[Fact]] @@ -23,7 +23,7 @@ namespace Soup::Core::UnitTests auto fileSystemState = FileSystemState(); auto builtInLanguages = std::map(); auto locationManager = RecipeBuildLocationManager(builtInLanguages); - auto uut = RecipeBuildRunner( + auto uut = BuildRunner( std::move(arguments), std::move(sdkParameters), std::move(sdkReadAccess), @@ -106,7 +106,7 @@ namespace Soup::Core::UnitTests auto evaluateEngine = MockEvaluateEngine(); auto builtInLanguages = std::map(); auto locationManager = RecipeBuildLocationManager(builtInLanguages); - auto uut = RecipeBuildRunner( + auto uut = BuildRunner( arguments, sdkParameters, sdkReadAccess, @@ -240,7 +240,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -380,7 +380,7 @@ namespace Soup::Core::UnitTests auto evaluateEngine = MockEvaluateEngine(); auto builtInLanguages = std::map(); auto locationManager = RecipeBuildLocationManager(builtInLanguages); - auto uut = RecipeBuildRunner( + auto uut = BuildRunner( arguments, sdkParameters, sdkReadAccess, @@ -573,7 +573,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -628,7 +628,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -793,7 +793,7 @@ namespace Soup::Core::UnitTests auto evaluateEngine = MockEvaluateEngine(); auto builtInLanguages = std::map(); auto locationManager = RecipeBuildLocationManager(builtInLanguages); - auto uut = RecipeBuildRunner( + auto uut = BuildRunner( arguments, sdkParameters, sdkReadAccess, @@ -1055,7 +1055,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -1102,7 +1102,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -1162,7 +1162,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -1330,7 +1330,7 @@ namespace Soup::Core::UnitTests auto evaluateEngine = MockEvaluateEngine(); auto builtInLanguages = std::map(); auto locationManager = RecipeBuildLocationManager(builtInLanguages); - auto uut = RecipeBuildRunner( + auto uut = BuildRunner( arguments, sdkParameters, sdkReadAccess, @@ -1523,7 +1523,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -1578,7 +1578,7 @@ namespace Soup::Core::UnitTests 1, OperationResult( true, - GetMinMillisecondTime(), + GetEpochTime(), {}, {}) }, @@ -1588,10 +1588,10 @@ namespace Soup::Core::UnitTests } private: - static std::chrono::time_point GetMinMillisecondTime() + static std::chrono::time_point GetEpochTime() { - return std::chrono::time_point_cast( - std::chrono::time_point::min()); + return std::chrono::clock_cast( + std::chrono::time_point()); } }; } diff --git a/Source/Client/Core/UnitTests/Build/FileSystemStateTests.h b/Source/Client/Core/UnitTests/Build/FileSystemStateTests.h index fb9ac0c2..7fc10f9e 100644 --- a/Source/Client/Core/UnitTests/Build/FileSystemStateTests.h +++ b/Source/Client/Core/UnitTests/Build/FileSystemStateTests.h @@ -92,19 +92,19 @@ namespace Soup::Core::UnitTests std::unordered_map({ { 2, Path("C:/Root/DoStuff.exe") }, }), - std::unordered_map>>({})); + std::unordered_map>>({})); auto lastWriteTime = uut.GetLastWriteTime(2); Assert::AreEqual( - std::optional>(std::nullopt), + std::optional>(std::nullopt), lastWriteTime, "Verify last write time matches expected."); // Verify expected file system requests Assert::AreEqual( std::vector({ - "Exists: C:/Root/DoStuff.exe", + "TryGetLastWriteTime: C:/Root/DoStuff.exe", }), fileSystem->GetRequests(), "Verify file system requests match expected."); @@ -113,20 +113,21 @@ namespace Soup::Core::UnitTests // [[Fact]] void GetLastWriteTime_Found() { - auto setLastWriteTime = std::chrono::sys_days(May/22/2015) + 9h + 11min; + auto setLastWriteTime = std::chrono::clock_cast( + std::chrono::sys_days(May/22/2015) + 9h + 11min); auto uut = FileSystemState( 10, std::unordered_map({ { 2, Path("C:/Root/DoStuff.exe") }, }), - std::unordered_map>>({ + std::unordered_map>>({ { 2, setLastWriteTime }, })); auto lastWriteTime = uut.GetLastWriteTime(2); Assert::AreEqual( - std::optional>(setLastWriteTime), + std::optional>(setLastWriteTime), lastWriteTime, "Verify last write time matches expected."); } diff --git a/Source/Client/Core/UnitTests/Build/MockEvaluateEngine.h b/Source/Client/Core/UnitTests/Build/MockEvaluateEngine.h index b19e1201..c0e261dc 100644 --- a/Source/Client/Core/UnitTests/Build/MockEvaluateEngine.h +++ b/Source/Client/Core/UnitTests/Build/MockEvaluateEngine.h @@ -50,7 +50,8 @@ namespace Soup::Core _requests.push_back(message.str()); - auto time = std::chrono::time_point::min(); + auto time = std::chrono::clock_cast( + std::chrono::time_point()); for (auto& operation : operationGraph.GetOperations()) { auto& operationInfo = operation.second; diff --git a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsManagerTests.h b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsManagerTests.h index b88e22f4..f0de8eb0 100644 --- a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsManagerTests.h +++ b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsManagerTests.h @@ -96,12 +96,12 @@ namespace Soup::Core::UnitTests auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x9b, 0x4f, 0xc9, 0xb4, 0xa6, 0xf1, 0xfc, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }); @@ -123,7 +123,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( false, - std::chrono::time_point_cast(std::chrono::time_point::min()), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -161,7 +162,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -179,12 +181,12 @@ namespace Soup::Core::UnitTests // Verify the file content auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x9b, 0x4f, 0xc9, 0xb4, 0xa6, 0xf1, 0xfc, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }); diff --git a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsReaderTests.h b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsReaderTests.h index 1c8979d9..1e818c51 100644 --- a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsReaderTests.h +++ b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsReaderTests.h @@ -50,7 +50,7 @@ namespace Soup::Core::UnitTests auto fileSystemState = FileSystemState(); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '2', }); auto content = std::stringstream(std::string(binaryFileContent.data(), binaryFileContent.size())); @@ -68,7 +68,7 @@ namespace Soup::Core::UnitTests auto fileSystemState = FileSystemState(); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '2', }); @@ -87,7 +87,7 @@ namespace Soup::Core::UnitTests auto fileSystemState = FileSystemState(); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x00, 0x00, 0x00, 0x00, }); @@ -107,12 +107,12 @@ namespace Soup::Core::UnitTests auto fileSystemState = FileSystemState(); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x9b, 0x4f, 0xc9, 0xb4, 0xa6, 0xf1, 0xfc, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }); @@ -126,7 +126,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( false, - std::chrono::time_point_cast(std::chrono::time_point::min()), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -148,7 +149,7 @@ namespace Soup::Core::UnitTests }); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 'C', ':', '/', 'F', 'i', 'l', 'e', '1', 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 'C', ':', '/', 'F', 'i', 'l', 'e', '2', @@ -157,7 +158,7 @@ namespace Soup::Core::UnitTests 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf1, 0x7d, 0xde, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x10, 0x16, 0x62, 0xbb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, }); @@ -171,7 +172,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( true, - std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms, + std::chrono::clock_cast( + std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms), { 11, 13, }, { 12, 14, }), } @@ -197,7 +199,7 @@ namespace Soup::Core::UnitTests }); auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 'C', ':', '/', 'F', 'i', 'l', 'e', '1', 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 'C', ':', '/', 'F', 'i', 'l', 'e', '2', @@ -210,12 +212,12 @@ namespace Soup::Core::UnitTests 'R', 'T', 'S', '\0', 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf1, 0x7d, 0xde, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x10, 0x16, 0x62, 0xbb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x58, 0xba, 0xdf, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x80, 0x8d, 0xa9, 0xeb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, }); @@ -229,7 +231,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( true, - std::chrono::sys_days(March / 5 / 2020) + 12h + 35min + 34s + 1ms, + std::chrono::clock_cast( + std::chrono::sys_days(March / 5 / 2020) + 12h + 35min + 34s + 1ms), { 11, 13, }, { 12, 14, }), }, @@ -237,7 +240,8 @@ namespace Soup::Core::UnitTests 6, OperationResult( true, - std::chrono::sys_days(March / 5 / 2020) + 12h + 36min + 55s, + std::chrono::clock_cast( + std::chrono::sys_days(March / 5 / 2020) + 12h + 36min + 55s), { 15, 17, }, { 16, 18, }), }, diff --git a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsTests.h b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsTests.h index 27a1ec1e..fe294854 100644 --- a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsTests.h +++ b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsTests.h @@ -28,7 +28,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -40,7 +41,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -69,7 +71,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -83,7 +86,8 @@ namespace Soup::Core::UnitTests Assert::AreEqual( OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }), *operationResult, @@ -99,7 +103,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { })); @@ -109,7 +114,8 @@ namespace Soup::Core::UnitTests 1, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }), }, diff --git a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsWriterTests.h b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsWriterTests.h index a61ce438..99cafcdf 100644 --- a/Source/Client/Core/UnitTests/OperationGraph/OperationResultsWriterTests.h +++ b/Source/Client/Core/UnitTests/OperationGraph/OperationResultsWriterTests.h @@ -22,7 +22,7 @@ namespace Soup::Core::UnitTests auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x00, 0x00, 0x00, 0x00, }); @@ -42,7 +42,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( false, - std::chrono::time_point::min(), + std::chrono::clock_cast( + std::chrono::time_point()), { }, { }) }, @@ -53,12 +54,12 @@ namespace Soup::Core::UnitTests auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x9b, 0x4f, 0xc9, 0xb4, 0xa6, 0xf1, 0xfc, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }); @@ -79,7 +80,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( true, - std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms, + std::chrono::clock_cast( + std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms), { 1, 3, }, { 2, 4, }) }, @@ -90,12 +92,12 @@ namespace Soup::Core::UnitTests auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf1, 0x7d, 0xde, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x10, 0x16, 0x62, 0xbb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, }); @@ -115,7 +117,8 @@ namespace Soup::Core::UnitTests 5, OperationResult( true, - std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms, + std::chrono::clock_cast( + std::chrono::sys_days(March/5/2020) + 12h + 35min + 34s + 1ms), { 1, 3, }, { 2, 4, }) }, @@ -123,7 +126,8 @@ namespace Soup::Core::UnitTests 6, OperationResult( true, - std::chrono::sys_days(March/5/2020) + 12h + 36min + 55s, + std::chrono::clock_cast( + std::chrono::sys_days(March/5/2020) + 12h + 36min + 55s), { 5, 7, }, { 6, 8, }) }, @@ -134,17 +138,17 @@ namespace Soup::Core::UnitTests auto binaryFileContent = std::vector( { - 'B', 'O', 'R', '\0', 0x01, 0x00, 0x00, 0x00, + 'B', 'O', 'R', '\0', 0x02, 0x00, 0x00, 0x00, 'F', 'I', 'S', '\0', 0x00, 0x00, 0x00, 0x00, 'R', 'T', 'S', '\0', 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf1, 0x7d, 0xde, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x10, 0x16, 0x62, 0xbb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x58, 0xba, 0xdf, 0xbc, 0xf3, 0x39, 0x00, 0x00, + 0x80, 0x8d, 0xa9, 0xeb, 0x0b, 0x41, 0x38, 0x00, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, }); diff --git a/Source/Client/Core/UnitTests/gen/Build/BuildEvaluateEngineTests.gen.h b/Source/Client/Core/UnitTests/gen/Build/BuildEvaluateEngineTests.gen.h index e968a4bb..604fb4b5 100644 --- a/Source/Client/Core/UnitTests/gen/Build/BuildEvaluateEngineTests.gen.h +++ b/Source/Client/Core/UnitTests/gen/Build/BuildEvaluateEngineTests.gen.h @@ -8,11 +8,16 @@ TestState RunBuildEvaluateEngineTests() TestState state = { 0, 0 }; state += Soup::Test::RunTest(className, "Initialize", [&testClass]() { testClass->Initialize(); }); state += Soup::Test::RunTest(className, "Execute_OneOperation_FirstRun", [&testClass]() { testClass->Execute_OneOperation_FirstRun(); }); + state += Soup::Test::RunTest(className, "Execute_OneOperation_ObservedInputAndOutput_CircularReference_RemoveInput", [&testClass]() { testClass->Execute_OneOperation_ObservedInputAndOutput_CircularReference_RemoveInput(); }); + state += Soup::Test::RunTest(className, "Execute_OneOperation_ObservedInput_CircularReference_RemoveInput", [&testClass]() { testClass->Execute_OneOperation_ObservedInput_CircularReference_RemoveInput(); }); state += Soup::Test::RunTest(className, "Evaluate_OneOperation_Incremental_MissingFileInfo", [&testClass]() { testClass->Evaluate_OneOperation_Incremental_MissingFileInfo(); }); state += Soup::Test::RunTest(className, "Evaluate_OneOperation_Incremental_MissingTargetFile", [&testClass]() { testClass->Evaluate_OneOperation_Incremental_MissingTargetFile(); }); state += Soup::Test::RunTest(className, "Evaluate_OneOperation_Incremental_OutOfDate", [&testClass]() { testClass->Evaluate_OneOperation_Incremental_OutOfDate(); }); state += Soup::Test::RunTest(className, "Evaluate_OneOperation_Incremental_Executable_OutOfDate", [&testClass]() { testClass->Evaluate_OneOperation_Incremental_Executable_OutOfDate(); }); state += Soup::Test::RunTest(className, "Evaluate_OneOperation_Incremental_UpToDate", [&testClass]() { testClass->Evaluate_OneOperation_Incremental_UpToDate(); }); + state += Soup::Test::RunTest(className, "Execute_TwoOperations_DuplicateOutputFile_Fails", [&testClass]() { testClass->Execute_TwoOperations_DuplicateOutputFile_Fails(); }); + state += Soup::Test::RunTest(className, "Execute_TwoOperations_UndeclaredOutputWithDeclaredInput_Fails", [&testClass]() { testClass->Execute_TwoOperations_UndeclaredOutputWithDeclaredInput_Fails(); }); + state += Soup::Test::RunTest(className, "Execute_TwoOperations_UndeclaredInputWithDeclaredOutput_Fails", [&testClass]() { testClass->Execute_TwoOperations_UndeclaredInputWithDeclaredOutput_Fails(); }); return state; } \ No newline at end of file diff --git a/Source/Client/Core/UnitTests/gen/Build/RecipeBuildRunnerTests.gen.h b/Source/Client/Core/UnitTests/gen/Build/BuildRunnerTests.gen.h similarity index 78% rename from Source/Client/Core/UnitTests/gen/Build/RecipeBuildRunnerTests.gen.h rename to Source/Client/Core/UnitTests/gen/Build/BuildRunnerTests.gen.h index 946c9e66..84783e81 100644 --- a/Source/Client/Core/UnitTests/gen/Build/RecipeBuildRunnerTests.gen.h +++ b/Source/Client/Core/UnitTests/gen/Build/BuildRunnerTests.gen.h @@ -1,10 +1,10 @@ #pragma once -#include "Build/RecipeBuildRunnerTests.h" +#include "Build/BuildRunnerTests.h" -TestState RunRecipeBuildRunnerTests() +TestState RunBuildRunnerTests() { - auto className = "RecipeBuildRunnerTests"; - auto testClass = std::make_shared(); + auto className = "BuildRunnerTests"; + auto testClass = std::make_shared(); TestState state = { 0, 0 }; state += Soup::Test::RunTest(className, "Initialize_Success", [&testClass]() { testClass->Initialize_Success(); }); state += Soup::Test::RunTest(className, "Execute_NoDependencies", [&testClass]() { testClass->Execute_NoDependencies(); }); diff --git a/Source/Client/Core/UnitTests/gen/Main.cpp b/Source/Client/Core/UnitTests/gen/Main.cpp index 79459115..26914f5a 100644 --- a/Source/Client/Core/UnitTests/gen/Main.cpp +++ b/Source/Client/Core/UnitTests/gen/Main.cpp @@ -27,10 +27,10 @@ using namespace Soup::Test; #include "Build/BuildEvaluateEngineTests.gen.h" #include "Build/BuildHistoryCheckerTests.gen.h" #include "Build/BuildLoadEngineTests.gen.h" +#include "Build/BuildRunnerTests.gen.h" #include "Build/FileSystemStateTests.gen.h" #include "Build/PackageProviderTests.gen.h" #include "Build/RecipeBuildLocationManagerTests.gen.h" -#include "Build/RecipeBuildRunnerTests.gen.h" #include "LocalUserConfig/LocalUserConfigExtensionsTests.gen.h" #include "LocalUserConfig/LocalUserConfigTests.gen.h" @@ -65,10 +65,10 @@ int main() state += RunBuildEvaluateEngineTests(); state += RunBuildHistoryCheckerTests(); state += RunBuildLoadEngineTests(); + state += RunBuildRunnerTests(); state += RunFileSystemStateTests(); state += RunPackageProviderTests(); state += RunRecipeBuildLocationManagerTests(); - state += RunRecipeBuildRunnerTests(); state += RunLocalUserConfigExtensionsTests(); state += RunLocalUserConfigTests(); diff --git a/Source/GenerateSharp/Runtime.UnitTests/Utilities/OperationGraphGeneratorUnitTests.cs b/Source/GenerateSharp/Runtime.UnitTests/OperationGraph/OperationGraphGeneratorUnitTests.cs similarity index 52% rename from Source/GenerateSharp/Runtime.UnitTests/Utilities/OperationGraphGeneratorUnitTests.cs rename to Source/GenerateSharp/Runtime.UnitTests/OperationGraph/OperationGraphGeneratorUnitTests.cs index d1a5a46a..68adde29 100644 --- a/Source/GenerateSharp/Runtime.UnitTests/Utilities/OperationGraphGeneratorUnitTests.cs +++ b/Source/GenerateSharp/Runtime.UnitTests/OperationGraph/OperationGraphGeneratorUnitTests.cs @@ -9,6 +9,7 @@ namespace Soup.Build.Runtime.UnitTests { + [Collection("Opal")] public class OperationGraphGeneratorUnitTests { [Fact] @@ -21,7 +22,12 @@ public void CreateOperation_NoInputOrOutput() var fileSystemState = new FileSystemState(); var readAccessList = new List(); var writeAccessList = new List(); - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff", @@ -40,17 +46,265 @@ public void CreateOperation_NoInputOrOutput() "DIAG: Write Access Subset:", }, testListener.GetMessages()); + + + Assert.Equal( + new Dictionary() + { + { + new OperationId(1), + new OperationInfo() + { + Id = new OperationId(1), + Title = "Do Stuff", + Command = new CommandInfo() + { + Executable = new Path("DoStuff.exe"), + Arguments = "do stuff", + WorkingDirectory = new Path("C:/WorkingDir/"), + }, + DeclaredInput = new List(), + DeclaredOutput = new List(), + ReadAccess = new List(), + WriteAccess = new List(), + DependencyCount = 0, + Children = new List(), + } + }, + }, + graph.Operations); + } + + [Fact] + public void CreateOperation_RelativeWorkingDirectory_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List(); + var writeAccessList = new List(); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff", + new Path("DoStuff.exe"), + "do stuff", + new Path("./WorkingDir/"), + new List(), + new List()); + }); + Assert.Equal("Working directory must be an absolute path.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + }, + testListener.GetMessages()); + } + + [Fact] + public void CreateOperation_DuplicateCommand_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List(); + var writeAccessList = new List(); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.CreateOperation( + "Do Stuff", + new Path("DoStuff.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List()); + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff", + new Path("DoStuff.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List()); + }); + Assert.Equal("Operation with this command already exists.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "DIAG: Read Access Subset:", + "DIAG: Write Access Subset:", + "DIAG: Create Operation: Do Stuff", + }, + testListener.GetMessages()); + } + + [Fact] + public void CreateOperation_DuplicateOutputFile_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List(); + var writeAccessList = new List() + { + new Path("C:/WorkingDir/WriteAccess/") + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.CreateOperation( + "Do Stuff 1", + new Path("DoStuff1.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List() + { + new Path("./WriteAccess/Output.txt"), + }); + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff 2", + new Path("DoStuff2.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List() + { + new Path("./WriteAccess/Output.txt"), + }); + }); + Assert.Equal("File \"C:/WorkingDir/WriteAccess/Output.txt\" already written to by operation \"Do Stuff 1\"", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff 1", + "DIAG: Read Access Subset:", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/WriteAccess/", + "DIAG: Create Operation: Do Stuff 2", + "DIAG: Read Access Subset:", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/WriteAccess/", + }, + testListener.GetMessages()); + } + + [Fact] + public void CreateOperation_DuplicateOutputDirectory_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List(); + var writeAccessList = new List() + { + new Path("C:/WorkingDir/WriteAccess/") + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.CreateOperation( + "Do Stuff 1", + new Path("DoStuff1.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List() + { + new Path("./WriteAccess/Output/"), + }); + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff 2", + new Path("DoStuff2.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List(), + new List() + { + new Path("./WriteAccess/Output/"), + }); + }); + Assert.Equal("Directory \"C:/WorkingDir/WriteAccess/Output/\" already written to by operation \"Do Stuff 1\"", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff 1", + "DIAG: Read Access Subset:", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/WriteAccess/", + "DIAG: Create Operation: Do Stuff 2", + "DIAG: Read Access Subset:", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/WriteAccess/", + }, + testListener.GetMessages()); } [Fact] - public void CreateOperation_NoAccess_ReadAccessDenied() + public void CreateOperation_NoAccess_ReadAccessDenied_Fails() { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + var fileSystemState = new FileSystemState(); var readAccessList = new List(); var writeAccessList = new List(); - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); - Assert.Throws( + var exception = Assert.Throws( () => { uut.CreateOperation( @@ -64,17 +318,36 @@ public void CreateOperation_NoAccess_ReadAccessDenied() }, new List()); }); + Assert.Equal("Operation does not have permission to read requested input.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "ERRO: File access denied: ./ReadFile.txt", + }, + testListener.GetMessages()); } [Fact] - public void CreateOperation_NoAccess_WriteAccessDenied() + public void CreateOperation_NoAccess_WriteAccessDenied_Fails() { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + var fileSystemState = new FileSystemState(); var readAccessList = new List(); var writeAccessList = new List(); - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); - Assert.Throws( + var exception = Assert.Throws( () => { uut.CreateOperation( @@ -88,11 +361,26 @@ public void CreateOperation_NoAccess_WriteAccessDenied() new Path("WriteFile.txt"), }); }); + Assert.Equal("Operation does not have permission to write requested output.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "DIAG: Read Access Subset:", + "ERRO: File access denied: ./WriteFile.txt", + }, + testListener.GetMessages()); } [Fact] public void CreateOperation_Access_ReadAccessDenied() { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + var fileSystemState = new FileSystemState(); var readAccessList = new List() { @@ -102,9 +390,14 @@ public void CreateOperation_Access_ReadAccessDenied() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); - Assert.Throws( + var exception = Assert.Throws( () => { uut.CreateOperation( @@ -118,11 +411,25 @@ public void CreateOperation_Access_ReadAccessDenied() }, new List()); }); + Assert.Equal("Operation does not have permission to read requested input.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "ERRO: File access denied: ./ReadFile.txt", + }, + testListener.GetMessages()); } [Fact] - public void CreateOperation_Access_WriteAccessDenied() + public void CreateOperation_Access_WriteAccessDenied_Fails() { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + var fileSystemState = new FileSystemState(); var readAccessList = new List() { @@ -132,9 +439,14 @@ public void CreateOperation_Access_WriteAccessDenied() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); - Assert.Throws( + var exception = Assert.Throws( () => { uut.CreateOperation( @@ -148,6 +460,17 @@ public void CreateOperation_Access_WriteAccessDenied() new Path("WriteFile.txt"), }); }); + Assert.Equal("Operation does not have permission to write requested output.", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "DIAG: Read Access Subset:", + "ERRO: File access denied: ./WriteFile.txt", + }, + testListener.GetMessages()); } [Fact] @@ -166,7 +489,12 @@ public void CreateOperation_Access_ReadAccessRelative() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff", @@ -189,6 +517,32 @@ public void CreateOperation_Access_ReadAccessRelative() "DIAG: Write Access Subset:" }, testListener.GetMessages()); + + Assert.Equal( + new Dictionary() + { + { + new OperationId(1), + new OperationInfo() + { + Id = new OperationId(1), + Title = "Do Stuff", + Command = new CommandInfo() + { + Executable = new Path("DoStuff.exe"), + Arguments = "do stuff", + WorkingDirectory = new Path("C:/WorkingDir/"), + }, + DeclaredInput = new List() { new FileId(1), }, + DeclaredOutput = new List(), + ReadAccess = new List() { new FileId(2), }, + WriteAccess = new List(), + DependencyCount = 0, + Children = new List(), + } + }, + }, + graph.Operations); } [Fact] @@ -207,7 +561,12 @@ public void CreateOperation_Access_WriteAccessRelative() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff", @@ -230,51 +589,36 @@ public void CreateOperation_Access_WriteAccessRelative() "DIAG: C:/WorkingDir/WriteAccess/", }, testListener.GetMessages()); - } - - [Fact] - public void CreateOperation_Access_ReadAccessRelative_SubFolder() - { - // Register the test listener - var testListener = new TestTraceListener(); - using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); - - var fileSystemState = new FileSystemState(); - var readAccessList = new List() - { - new Path("C:/WorkingDir/ReadAccess/") - }; - var writeAccessList = new List() - { - new Path("C:/WorkingDir/WriteAccess/") - }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); - uut.CreateOperation( - "Do Stuff", - new Path("DoStuff.exe"), - "do stuff", - new Path("C:/WorkingDir/"), - new List() - { - new Path("./ReadAccess/SubFolder/ReadFile.txt"), - }, - new List()); - - // Verify expected logs Assert.Equal( - new List() + new Dictionary() { - "DIAG: Create Operation: Do Stuff", - "DIAG: Read Access Subset:", - "DIAG: C:/WorkingDir/ReadAccess/", - "DIAG: Write Access Subset:" + { + new OperationId(1), + new OperationInfo() + { + Id = new OperationId(1), + Title = "Do Stuff", + Command = new CommandInfo() + { + Executable = new Path("DoStuff.exe"), + Arguments = "do stuff", + WorkingDirectory = new Path("C:/WorkingDir/"), + }, + DeclaredInput = new List(), + DeclaredOutput = new List() { new FileId(1), }, + ReadAccess = new List(), + WriteAccess = new List() { new FileId(2), }, + DependencyCount = 0, + Children = new List(), + } + }, }, - testListener.GetMessages()); + graph.Operations); } [Fact] - public void CreateOperation_Access_WriteAccessRelative_SubFolder() + public void CreateOperation_Access_ReadAccessRelative_SubFolder() { // Register the test listener var testListener = new TestTraceListener(); @@ -289,18 +633,23 @@ public void CreateOperation_Access_WriteAccessRelative_SubFolder() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff", new Path("DoStuff.exe"), "do stuff", new Path("C:/WorkingDir/"), - new List(), new List() { - new Path("./WriteAccess/SubFolder/WriteFile.txt"), - }); + new Path("./ReadAccess/SubFolder/ReadFile.txt"), + }, + new List()); // Verify expected logs Assert.Equal( @@ -308,35 +657,40 @@ public void CreateOperation_Access_WriteAccessRelative_SubFolder() { "DIAG: Create Operation: Do Stuff", "DIAG: Read Access Subset:", - "DIAG: Write Access Subset:", - "DIAG: C:/WorkingDir/WriteAccess/", + "DIAG: C:/WorkingDir/ReadAccess/", + "DIAG: Write Access Subset:" }, testListener.GetMessages()); - } - - [Fact] - public void BuildGraph_NoNodes() - { - var fileSystemState = new FileSystemState(); - var readAccessList = new List() - { - new Path("C:/WorkingDir/ReadAccess/") - }; - var writeAccessList = new List() - { - new Path("C:/WorkingDir/WriteAccess/") - }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); - - var graph = uut.BuildGraph(); - Assert.Equal(new Dictionary(), graph.GetOperations()); - Assert.Equal(new List(), graph.GetRootOperationIds()); - Assert.Equal(new List<(FileId, Path)>(), graph.GetReferencedFiles()); + Assert.Equal( + new Dictionary() + { + { + new OperationId(1), + new OperationInfo() + { + Id = new OperationId(1), + Title = "Do Stuff", + Command = new CommandInfo() + { + Executable = new Path("DoStuff.exe"), + Arguments = "do stuff", + WorkingDirectory = new Path("C:/WorkingDir/"), + }, + DeclaredInput = new List() { new FileId(1), }, + DeclaredOutput = new List(), + ReadAccess = new List() { new FileId(2), }, + WriteAccess = new List(), + DependencyCount = 0, + Children = new List(), + } + }, + }, + graph.Operations); } [Fact] - public void BuildGraph_SingleNodes() + public void CreateOperation_Access_WriteAccessRelative_SubFolder() { // Register the test listener var testListener = new TestTraceListener(); @@ -351,31 +705,30 @@ public void BuildGraph_SingleNodes() { new Path("C:/WorkingDir/WriteAccess/") }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff", new Path("DoStuff.exe"), "do stuff", new Path("C:/WorkingDir/"), + new List(), new List() { - new Path("./ReadAccess/ReadFile.txt"), - }, - new List() - { - new Path("./WriteAccess/WriteFile.txt"), + new Path("./WriteAccess/SubFolder/WriteFile.txt"), }); - var graph = uut.BuildGraph(); - // Verify expected logs Assert.Equal( new List() { "DIAG: Create Operation: Do Stuff", "DIAG: Read Access Subset:", - "DIAG: C:/WorkingDir/ReadAccess/", "DIAG: Write Access Subset:", "DIAG: C:/WorkingDir/WriteAccess/", }, @@ -384,7 +737,7 @@ public void BuildGraph_SingleNodes() Assert.Equal( new Dictionary() { - { + { new OperationId(1), new OperationInfo() { @@ -396,28 +749,20 @@ public void BuildGraph_SingleNodes() Arguments = "do stuff", WorkingDirectory = new Path("C:/WorkingDir/"), }, - DeclaredInput = new List() { new FileId(1), }, - DeclaredOutput = new List() { new FileId(2), }, - ReadAccess = new List() { new FileId(3), }, - WriteAccess = new List() { new FileId(4), }, - DependencyCount = 1, + DeclaredInput = new List(), + DeclaredOutput = new List() { new FileId(1), }, + ReadAccess = new List(), + WriteAccess = new List() { new FileId(2), }, + DependencyCount = 0, + Children = new List(), } }, }, - graph.GetOperations()); - Assert.Equal( - new List() - { - new OperationId(1), - }, - graph.GetRootOperationIds()); - Assert.Equal( - new List<(FileId, Path)>(), - graph.GetReferencedFiles()); + graph.Operations); } [Fact] - public void BuildGraph_DependencyNodes_DirectFileRead() + public void CreateOperation_DependencyNodes_DirectFileRead() { // Register the test listener var testListener = new TestTraceListener(); @@ -433,7 +778,12 @@ public void BuildGraph_DependencyNodes_DirectFileRead() { new Path("C:/WorkingDir/WriteAccess/"), }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff 1", @@ -463,8 +813,6 @@ public void BuildGraph_DependencyNodes_DirectFileRead() new Path("./WriteAccess/WriteFile2.txt"), }); - var graph = uut.BuildGraph(); - // Verify expected logs Assert.Equal( new List() @@ -501,7 +849,7 @@ public void BuildGraph_DependencyNodes_DirectFileRead() DeclaredOutput = new List() { new FileId(2), }, ReadAccess = new List() { new FileId(3), }, WriteAccess = new List() { new FileId(4), }, - DependencyCount = 1, + DependencyCount = 0, Children = new List() { new OperationId(2), @@ -528,20 +876,11 @@ public void BuildGraph_DependencyNodes_DirectFileRead() } }, }, - graph.GetOperations()); - Assert.Equal( - new List() - { - new OperationId(1), - }, - graph.GetRootOperationIds()); - Assert.Equal( - new List<(FileId, Path)>(), - graph.GetReferencedFiles()); + graph.Operations); } [Fact] - public void BuildGraph_DependencyNodes_SubFolder() + public void CreateOperation_DependencyNodes_SubFolder() { // Register the test listener var testListener = new TestTraceListener(); @@ -557,7 +896,12 @@ public void BuildGraph_DependencyNodes_SubFolder() { new Path("C:/WorkingDir/WriteAccess/"), }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff 1", @@ -581,8 +925,6 @@ public void BuildGraph_DependencyNodes_SubFolder() new Path("./WriteAccess/Folder/WriteFile1.txt"), }); - var graph = uut.BuildGraph(); - // Verify expected logs Assert.Equal( new List() @@ -617,7 +959,7 @@ public void BuildGraph_DependencyNodes_SubFolder() DeclaredOutput = new List() { new FileId(1), }, ReadAccess = new List() { }, WriteAccess = new List() { new FileId(2), }, - DependencyCount = 1, + DependencyCount = 0, Children = new List() { new OperationId(2), @@ -644,20 +986,255 @@ public void BuildGraph_DependencyNodes_SubFolder() } }, }, - graph.GetOperations()); + graph.Operations); + } + + [Fact] + public void CreateOperation_DependencyNodes_Circular_SingleFile_SingleOperation_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List() + { + new Path("C:/WorkingDir/"), + }; + var writeAccessList = new List() + { + new Path("C:/WorkingDir/"), + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff", + new Path("DoStuff.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List() + { + new Path("File.txt"), + }, + new List() + { + new Path("File.txt"), + }); + }); + + Assert.Equal("Operation introduced circular reference", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "DIAG: Read Access Subset:", + "DIAG: C:/WorkingDir/", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/", + }, + testListener.GetMessages()); + } + + + [Fact] + public void CreateOperation_DependencyNodes_Circular_TwoOperations_TwoFiles_Fails() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List() + { + new Path("C:/WorkingDir/"), + }; + var writeAccessList = new List() + { + new Path("C:/WorkingDir/"), + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.CreateOperation( + "Do Stuff 1", + new Path("DoStuff1.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List() + { + new Path("File1.txt"), + }, + new List() + { + new Path("File2.txt"), + }); + + var exception = Assert.Throws( + () => + { + uut.CreateOperation( + "Do Stuff 2", + new Path("DoStuff2.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List() + { + new Path("File2.txt"), + }, + new List() + { + new Path("File1.txt"), + }); + }); + + Assert.Equal("Operation introduced circular reference", exception.Message); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff 1", + "DIAG: Read Access Subset:", + "DIAG: C:/WorkingDir/", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/", + "DIAG: Create Operation: Do Stuff 2", + "DIAG: Read Access Subset:", + "DIAG: C:/WorkingDir/", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/", + }, + testListener.GetMessages()); + } + + [Fact] + public void FinalizeGraph_NoNodes() + { + var fileSystemState = new FileSystemState(); + var readAccessList = new List() + { + new Path("C:/WorkingDir/ReadAccess/") + }; + var writeAccessList = new List() + { + new Path("C:/WorkingDir/WriteAccess/") + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.FinalizeGraph(); + + Assert.Equal(new Dictionary(), graph.Operations); + Assert.Equal(new List(), graph.RootOperationIds); + Assert.Equal(new List<(FileId, Path)>(), graph.ReferencedFiles); + } + + [Fact] + public void FinalizeGraph_SingleNodes() + { + // Register the test listener + var testListener = new TestTraceListener(); + using var scopedTraceListener = new ScopedTraceListenerRegister(testListener); + + var fileSystemState = new FileSystemState(); + var readAccessList = new List() + { + new Path("C:/WorkingDir/ReadAccess/") + }; + var writeAccessList = new List() + { + new Path("C:/WorkingDir/WriteAccess/") + }; + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); + + uut.CreateOperation( + "Do Stuff", + new Path("DoStuff.exe"), + "do stuff", + new Path("C:/WorkingDir/"), + new List() + { + new Path("./ReadAccess/ReadFile.txt"), + }, + new List() + { + new Path("./WriteAccess/WriteFile.txt"), + }); + + uut.FinalizeGraph(); + + // Verify expected logs + Assert.Equal( + new List() + { + "DIAG: Create Operation: Do Stuff", + "DIAG: Read Access Subset:", + "DIAG: C:/WorkingDir/ReadAccess/", + "DIAG: Write Access Subset:", + "DIAG: C:/WorkingDir/WriteAccess/", + }, + testListener.GetMessages()); + + Assert.Equal( + new Dictionary() + { + { + new OperationId(1), + new OperationInfo() + { + Id = new OperationId(1), + Title = "Do Stuff", + Command = new CommandInfo() + { + Executable = new Path("DoStuff.exe"), + Arguments = "do stuff", + WorkingDirectory = new Path("C:/WorkingDir/"), + }, + DeclaredInput = new List() { new FileId(1), }, + DeclaredOutput = new List() { new FileId(2), }, + ReadAccess = new List() { new FileId(3), }, + WriteAccess = new List() { new FileId(4), }, + DependencyCount = 1, + } + }, + }, + graph.Operations); Assert.Equal( new List() { new OperationId(1), }, - graph.GetRootOperationIds()); + graph.RootOperationIds); Assert.Equal( new List<(FileId, Path)>(), - graph.GetReferencedFiles()); + graph.ReferencedFiles); } [Fact] - public void BuildGraph_DependencyNodes_FlattenGraph() + public void FinalizeGraph_DependencyNodes_FlattenGraph() { // Register the test listener var testListener = new TestTraceListener(); @@ -673,7 +1250,12 @@ public void BuildGraph_DependencyNodes_FlattenGraph() { new Path("C:/WorkingDir/WriteAccess/"), }; - var uut = new OperationGraphGenerator(fileSystemState, readAccessList, writeAccessList); + var graph = new OperationGraph(); + var uut = new OperationGraphGenerator( + fileSystemState, + readAccessList, + writeAccessList, + graph); uut.CreateOperation( "Do Stuff 1", @@ -721,7 +1303,7 @@ public void BuildGraph_DependencyNodes_FlattenGraph() new Path("./WriteAccess/WriteFile3.txt"), }); - var graph = uut.BuildGraph(); + uut.FinalizeGraph(); // Verify expected logs Assert.Equal( @@ -814,16 +1396,16 @@ public void BuildGraph_DependencyNodes_FlattenGraph() } }, }, - graph.GetOperations()); + graph.Operations); Assert.Equal( new List() { new OperationId(1), }, - graph.GetRootOperationIds()); + graph.RootOperationIds); Assert.Equal( new List<(FileId, Path)>(), - graph.GetReferencedFiles()); + graph.ReferencedFiles); } } } diff --git a/Source/GenerateSharp/Runtime/Contracts/BuildState.cs b/Source/GenerateSharp/Runtime/Contracts/BuildState.cs index f2cac94f..011d47b5 100644 --- a/Source/GenerateSharp/Runtime/Contracts/BuildState.cs +++ b/Source/GenerateSharp/Runtime/Contracts/BuildState.cs @@ -14,6 +14,7 @@ namespace Soup.Build.Runtime /// public class BuildState : IBuildState { + private OperationGraph graph; private OperationGraphGenerator graphGenerator; /// @@ -28,10 +29,12 @@ public BuildState( this.ActiveStateImpl = activeState; this.SharedStateImpl = new ValueTable(); + this.graph = new OperationGraph(); this.graphGenerator = new OperationGraphGenerator( fileSystemState, readAccessList, - writeAccessList); + writeAccessList, + graph); } /// @@ -98,7 +101,8 @@ public void LogTrace(TraceLevel level, string message) public OperationGraph BuildOperationGraph() { - return this.graphGenerator.BuildGraph(); + this.graphGenerator.FinalizeGraph(); + return this.graph; } } } diff --git a/Source/GenerateSharp/Runtime/OperationGraph/OperationGraph.cs b/Source/GenerateSharp/Runtime/OperationGraph/OperationGraph.cs index 1b2dbb6e..b40f9495 100644 --- a/Source/GenerateSharp/Runtime/OperationGraph/OperationGraph.cs +++ b/Source/GenerateSharp/Runtime/OperationGraph/OperationGraph.cs @@ -52,39 +52,25 @@ public OperationGraph( /// /// Get the set of referenced file ids that map to their paths /// - public IList<(FileId FileId, Path Path)> GetReferencedFiles() + public List<(FileId FileId, Path Path)> ReferencedFiles { - return _referencedFiles; - } - - public void SetReferencedFiles(List<(FileId FileId, Path Path)> files) - { - _referencedFiles = files; + get { return _referencedFiles; } + set { _referencedFiles = value; } } /// /// Get the list of root operation ids /// - public IList GetRootOperationIds() - { - return _rootOperations; - } - - /// - /// Set the list of root operation ids - /// - public void SetRootOperationIds(List value) + public List RootOperationIds { - _rootOperations = value; + get { return _rootOperations; } + set { _rootOperations = value; } } /// /// Get Operations /// - public IReadOnlyDictionary GetOperations() - { - return _operations; - } + public IReadOnlyDictionary Operations => _operations; /// /// Find an operation info diff --git a/Source/GenerateSharp/Runtime/OperationGraph/OperationGraphGenerator.cs b/Source/GenerateSharp/Runtime/OperationGraph/OperationGraphGenerator.cs index a3df85a3..eeb17b98 100644 --- a/Source/GenerateSharp/Runtime/OperationGraph/OperationGraphGenerator.cs +++ b/Source/GenerateSharp/Runtime/OperationGraph/OperationGraphGenerator.cs @@ -5,6 +5,7 @@ using Opal; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Soup.Build.Runtime @@ -20,22 +21,28 @@ internal class OperationGraphGenerator private IList writeAccessList; private OperationId uniqueId; private OperationGraph graph; - private Dictionary> outputFileLookup; - private Dictionary> outputDirectoryLookup; + + // Running state used to build graph dynamically + private Dictionary> inputFileLookup; + private Dictionary outputFileLookup; + private Dictionary outputDirectoryLookup; public OperationGraphGenerator( FileSystemState fileSystemState, IList readAccessList, - IList writeAccessList) + IList writeAccessList, + OperationGraph graph) { this.fileSystemState = fileSystemState; this.readAccessList = readAccessList; this.writeAccessList = writeAccessList; + this.graph = graph; this.uniqueId = new OperationId(0); - this.graph = new OperationGraph(); - this.outputFileLookup = new Dictionary>(); - this.outputDirectoryLookup = new Dictionary>(); + + this.inputFileLookup = new Dictionary>(); + this.outputFileLookup = new Dictionary(); + this.outputDirectoryLookup = new Dictionary(); } /// @@ -91,95 +98,32 @@ public void CreateOperation( this.uniqueId = new OperationId(this.uniqueId.value + 1); var operationId = this.uniqueId; - // Build up the declared build operation + // Resolve the requested files to unique ids var declaredInputFileIds = this.fileSystemState.ToFileIds(declaredInput, commandInfo.WorkingDirectory); var declaredOutputFileIds = this.fileSystemState.ToFileIds(declaredOutput, commandInfo.WorkingDirectory); var readAccessFileIds = this.fileSystemState.ToFileIds(readAccess, commandInfo.WorkingDirectory); var writeAccessFileIds = this.fileSystemState.ToFileIds(writeAccess, commandInfo.WorkingDirectory); - this.graph.AddOperation( - new OperationInfo( - operationId, - title, - commandInfo, - declaredInputFileIds, - declaredOutputFileIds, - readAccessFileIds, - writeAccessFileIds)); - } + // Build up the declared build operation + var operationInfo = new OperationInfo( + operationId, + title, + commandInfo, + declaredInputFileIds, + declaredOutputFileIds, + readAccessFileIds, + writeAccessFileIds); + this.graph.AddOperation(operationInfo); + + StoreLookupInfo(operationInfo); + ResolveDependencies(operationInfo); + } - public OperationGraph BuildGraph() + public void FinalizeGraph() { - // Store the operation in the required file lookups to help build up the dependency graph - foreach (var operationInfo in this.graph.GetOperations().Values) - { - foreach (var file in operationInfo.DeclaredOutput) - { - var filePath = this.fileSystemState.GetFilePath(file); - if (filePath.HasFileName) - { - AddOutputFileOperations(file).Add(operationInfo); - } - else - { - AddOutputDirectoryOperations(file).Add(operationInfo); - } - } - } - - // Build up the child dependencies based on the operations that use this operations output files - foreach (var activeOperationInfo in this.graph.GetOperations().Values) - { - // Check for inputs that match previous output files - foreach (var file in activeOperationInfo.DeclaredInput) - { - if (TryGetOutputFileOperations(file, out var matchedOperations)) - { - foreach (var matchedOperation in matchedOperations) - { - // The active operation must run after the matched output operation - if (UniqueAdd(matchedOperation.Children, activeOperationInfo.Id)) - { - activeOperationInfo.DependencyCount++; - } - } - } - } - - // Check for output files that are under previous output directories - foreach (var file in activeOperationInfo.DeclaredOutput) - { - var filePath = this.fileSystemState.GetFilePath(file); - var parentDirectory = filePath.GetParent(); - var done = false; - while (!done) - { - if (this.fileSystemState.TryFindFileId(parentDirectory, out var parentDirectoryId)) - { - if (TryGetOutputDirectoryOperations(parentDirectoryId, out var matchedOperations)) - { - foreach (OperationInfo matchedOperation in matchedOperations) - { - // The matched directory output operation must run before the active operation - if (UniqueAdd(matchedOperation.Children, activeOperationInfo.Id)) - { - activeOperationInfo.DependencyCount++; - } - } - } - } - - // Get the next parent directory - var nextParentDirectory = parentDirectory.GetParent(); - done = nextParentDirectory.ToString().Length == parentDirectory.ToString().Length; - parentDirectory = nextParentDirectory; - } - } - } - // Add any operation with zero dependencies to the root var rootOperations = new List(); - foreach (var activeOperationInfo in this.graph.GetOperations().Values) + foreach (var activeOperationInfo in this.graph.Operations.Values) { if (activeOperationInfo.DependencyCount == 0) { @@ -188,12 +132,12 @@ public OperationGraph BuildGraph() } } - this.graph.SetRootOperationIds(rootOperations); + this.graph.RootOperationIds = rootOperations; // Remove extra dependency references that are already covered by upstream references var recursiveChildren = new Dictionary>(); BuildRecursiveChildSets(recursiveChildren, rootOperations); - foreach (var operation in this.graph.GetOperations().Values) + foreach (var operation in this.graph.Operations.Values) { // Check each child to see if it is already covered by another child var removeList = new List(); @@ -225,12 +169,116 @@ public OperationGraph BuildGraph() var childOperation = this.graph.GetOperationInfo(childId); childOperation.DependencyCount--; } + } + } + + private void StoreLookupInfo(OperationInfo operationInfo) + { + // Store the operation in the required file lookups to ensure single target + // and help build up the dependency graph + foreach (var file in operationInfo.DeclaredOutput) + { + var filePath = this.fileSystemState.GetFilePath(file); + if (filePath.HasFileName) + { + CheckSetOutputFileOperation(file, operationInfo); + } + else + { + CheckSetOutputDirectoryOperation(file, operationInfo); + } + } + + foreach (var file in operationInfo.DeclaredInput) + { + var filePath = this.fileSystemState.GetFilePath(file); + if (filePath.HasFileName) + { + AddInputFileOperation(file, operationInfo); + } + } + } + + private void ResolveDependencies(OperationInfo operationInfo) + { + // Build up the child dependencies based on the operations that use this operations output files + + // Check for inputs that match previous output files + foreach (var file in operationInfo.DeclaredInput) + { + if (TryGetOutputFileOperation(file, out var matchedOperation)) + { + // The active operation must run after the matched output operation + CheckAddChildOperation(matchedOperation, operationInfo); + } + } + + // Check for outputs that match previous input files + foreach (var file in operationInfo.DeclaredOutput) + { + if (TryGetInputFileOperations(file, out var matchedOperations)) + { + foreach (var matchedOperation in matchedOperations) + { + // The active operation must run before the matched output operation + CheckAddChildOperation(operationInfo, matchedOperation); + } + } + } + + // Check for output files that are under previous output directories + foreach (var file in operationInfo.DeclaredOutput) + { + var filePath = this.fileSystemState.GetFilePath(file); + var parentDirectory = filePath.GetParent(); + var done = false; + while (!done) + { + if (this.fileSystemState.TryFindFileId(parentDirectory, out var parentDirectoryId)) + { + if (TryGetOutputDirectoryOperation(parentDirectoryId, out var matchedOperation)) + { + // The matched directory output operation must run before the active operation + CheckAddChildOperation(matchedOperation, operationInfo); + } + } + + // Get the next parent directory + var nextParentDirectory = parentDirectory.GetParent(); + done = nextParentDirectory.ToString().Length == parentDirectory.ToString().Length; + parentDirectory = nextParentDirectory; + } + } + + // Ensure there are no circular references + var closure = new HashSet(); + BuildChildClosure(operationInfo.Children, closure); + if (closure.Contains(operationInfo.Id)) + { + throw new InvalidOperationException("Operation introduced circular reference"); } + } + + private void BuildChildClosure( + IList operations, + HashSet closure) + { + foreach (var operationId in operations) + { + // Check if this node was already handled in a different branch + if (!closure.Contains(operationId)) + { + closure.Add(operationId); - return this.graph; + var operation = this.graph.GetOperationInfo(operationId); + BuildChildClosure(operation.Children, closure); + } + } } - private void BuildRecursiveChildSets(Dictionary> recursiveChildren, IList operations) + private void BuildRecursiveChildSets( + Dictionary> recursiveChildren, + IList operations) { foreach (var operationId in operations) { @@ -299,79 +347,111 @@ private bool IsAllowedAccess( return null; } - private bool UniqueAdd(IList operationList, OperationId operation) + private void CheckAddChildOperation(OperationInfo parentOperation, OperationInfo childOperation) + { + // Check if this item is already known + if (!parentOperation.Children.Contains(childOperation.Id)) + { + // Keep track of the parent child references + parentOperation.Children.Add(childOperation.Id); + childOperation.DependencyCount++; + } + } + + private bool TryGetInputFileOperations( + FileId file, + [MaybeNullWhen(false)] out IList operations) { - if (!operationList.Contains(operation)) + if (this.inputFileLookup.TryGetValue(file, out var value)) { - operationList.Add(operation); + operations = value; return true; } else { + operations = null; return false; } } - private bool TryGetOutputFileOperations( + private bool TryGetOutputFileOperation( FileId file, - out IList operations) + [MaybeNullWhen(false)] out OperationInfo operation) { if (this.outputFileLookup.TryGetValue(file, out var value)) { - operations = value; + operation = value; return true; } else { - operations = new List(); + operation = null; return false; } } - private bool TryGetOutputDirectoryOperations( + private bool TryGetOutputDirectoryOperation( FileId file, - out IList operations) + [MaybeNullWhen(false)] out OperationInfo operation) { if (this.outputDirectoryLookup.TryGetValue(file, out var value)) { - operations = value; + operation = value; return true; } else { - operations = new List(); + operation = null; return false; } } - private IList AddOutputFileOperations( - FileId file) + private void CheckSetOutputFileOperation( + FileId file, + OperationInfo operation) + { + if (this.outputFileLookup.TryGetValue(file, out var existingOperation)) + { + var filePath = this.fileSystemState.GetFilePath(file); + throw new InvalidOperationException($"File \"{filePath}\" already written to by operation \"{existingOperation.Title}\""); + } + else + { + this.outputFileLookup.Add(file, operation); + } + } + + private void CheckSetOutputDirectoryOperation( + FileId file, + OperationInfo operation) { - if (this.outputFileLookup.ContainsKey(file)) + if (this.outputDirectoryLookup.TryGetValue(file, out var existingOperation)) { var filePath = this.fileSystemState.GetFilePath(file); - throw new InvalidOperationException($"Operation output file already exists: {filePath}"); + throw new InvalidOperationException($"Directory \"{filePath}\" already written to by operation \"{existingOperation.Title}\""); } else { - var result = new List(); - this.outputFileLookup.Add(file, result); - return result; + this.outputDirectoryLookup.Add(file, operation); } } - private IList AddOutputDirectoryOperations( - FileId file) + private void AddInputFileOperation( + FileId file, + OperationInfo operation) { - if (this.outputFileLookup.ContainsKey(file)) + if (this.inputFileLookup.TryGetValue(file, out var operations)) { - throw new InvalidOperationException("Operation output directory already exists"); + operations.Add(operation); } else { - var result = new List(); - this.outputDirectoryLookup.Add(file, result); - return result; + this.inputFileLookup.Add( + file, + new List() + { + operation, + }); } } } diff --git a/Source/GenerateSharp/SoupView/App.xaml.cs b/Source/GenerateSharp/SoupView/App.xaml.cs index cbe131b7..d1e4e354 100644 --- a/Source/GenerateSharp/SoupView/App.xaml.cs +++ b/Source/GenerateSharp/SoupView/App.xaml.cs @@ -44,6 +44,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) m_window.Activate(); } - private Window m_window; + private Window? m_window; } } diff --git a/Source/GenerateSharp/SoupView/SoupView.csproj b/Source/GenerateSharp/SoupView/SoupView.csproj index c71648a0..43fb643e 100644 --- a/Source/GenerateSharp/SoupView/SoupView.csproj +++ b/Source/GenerateSharp/SoupView/SoupView.csproj @@ -1,4 +1,4 @@ - + WinExe net6.0-windows10.0.19041.0 @@ -10,6 +10,7 @@ win10-$(Platform).pubxml true true + enable diff --git a/Source/GenerateSharp/SoupView/View/ValueTableItemTemplateSelector.cs b/Source/GenerateSharp/SoupView/View/ValueTableItemTemplateSelector.cs index 171404ce..94941b18 100644 --- a/Source/GenerateSharp/SoupView/View/ValueTableItemTemplateSelector.cs +++ b/Source/GenerateSharp/SoupView/View/ValueTableItemTemplateSelector.cs @@ -11,11 +11,11 @@ namespace SoupView.View { internal class ValueTableItemTemplateSelector : DataTemplateSelector { - public DataTemplate TableTemplate { get; set; } - public DataTemplate ListTemplate { get; set; } - public DataTemplate ValueTemplate { get; set; } + public DataTemplate? TableTemplate { get; set; } + public DataTemplate? ListTemplate { get; set; } + public DataTemplate? ValueTemplate { get; set; } - protected override DataTemplate SelectTemplateCore(object item) + protected override DataTemplate? SelectTemplateCore(object item) { var explorerItem = (ValueTableItemViewModel)item; switch (explorerItem.Type) diff --git a/Source/GenerateSharp/SoupView/ViewModel/DependencyGraphPageModel.cs b/Source/GenerateSharp/SoupView/ViewModel/DependencyGraphPageModel.cs index 42a17bfe..0beb93d3 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/DependencyGraphPageModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/DependencyGraphPageModel.cs @@ -6,21 +6,21 @@ using Opal.System; using Soup.Build; using Soup.Build.Utilities; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using System.Windows.Input; namespace SoupView.ViewModel { internal class DependencyGraphPageModel : Observable { - private GraphNode selectedNode = null; - private ProjectDetailsViewModel selectedProject = null; + private GraphNode? selectedNode = null; + private ProjectDetailsViewModel? selectedProject = null; private string errorBarMessage = string.Empty; private bool isErrorBarOpen = false; - private IList> graph = null; + private IList>? graph = null; private uint uniqueId = 0; private Dictionary projectDetailsLookup = new Dictionary(); @@ -37,7 +37,7 @@ public string ErrorBarMessage } } - public IList> Graph + public IList>? Graph { get { return graph; } set @@ -50,7 +50,7 @@ public IList> Graph } } - public GraphNode SelectedNode + public GraphNode? SelectedNode { get { return selectedNode; } set @@ -59,7 +59,14 @@ public GraphNode SelectedNode { selectedNode = value; NotifyPropertyChanged(); - SelectedProject = this.projectDetailsLookup[selectedNode.Id]; + if (selectedNode != null) + { + SelectedProject = this.projectDetailsLookup[selectedNode.Id]; + } + else + { + selectedProject = null; + } } } } @@ -77,7 +84,7 @@ public bool IsErrorBarOpen } } - public ProjectDetailsViewModel SelectedProject + public ProjectDetailsViewModel? SelectedProject { get { return selectedProject; } set @@ -121,7 +128,7 @@ private async Task BuildGraphAsync(IList<(Path Path, uint Id)> recipeFiles, ILis var loadResult = await RecipeExtensions.TryLoadRecipeFromFileAsync(recipeFile.Path); var currentChildRecipes = new List<(Path Path, uint Id)>(); string title; - Recipe recipe = null; + Recipe? recipe = null; var packageFolder = recipeFile.Path.GetParent(); if (loadResult.IsSuccess) @@ -184,11 +191,15 @@ private void AddRecipeFiles( } else { + if (packageReference.Version == null) + throw new InvalidOperationException("Package reference must have version"); var packagesDirectory = LifetimeManager.Get().GetUserProfileDirectory() + new Path(".soup/packages/"); var languageRootFolder = packagesDirectory + new Path(recipeLanguage); var packageRootFolder = languageRootFolder + new Path(packageReference.Name); - var packageVersionFolder = packageRootFolder + new Path(packageReference.Version.ToString()) + new Path("/"); + var packageVersionFolder = packageRootFolder + + new Path(packageReference.Version.ToString()) + + new Path("/"); var recipeFile = packageVersionFolder + BuildConstants.RecipeFileName; recipeFiles.Add((recipeFile, this.uniqueId++)); diff --git a/Source/GenerateSharp/SoupView/ViewModel/Observable.cs b/Source/GenerateSharp/SoupView/ViewModel/Observable.cs index 44dde9bc..e9490dab 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/Observable.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/Observable.cs @@ -9,9 +9,9 @@ namespace SoupView.ViewModel { internal class Observable : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; - protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) + protected void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) { if (PropertyChanged != null) { diff --git a/Source/GenerateSharp/SoupView/ViewModel/OperationDetailsViewModel.cs b/Source/GenerateSharp/SoupView/ViewModel/OperationDetailsViewModel.cs index de56f848..f03ea340 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/OperationDetailsViewModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/OperationDetailsViewModel.cs @@ -3,6 +3,7 @@ // using Soup.Build.Runtime; +using Soup.Build.Utilities; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,7 +13,10 @@ internal class OperationDetailsViewModel : Observable { private ObservableCollection properties = new ObservableCollection(); - public OperationDetailsViewModel(FileSystemState fileSystemState, OperationInfo operation) + public OperationDetailsViewModel( + FileSystemState fileSystemState, + OperationInfo operation, + OperationResult? operationResult) { properties.Clear(); properties.Add(new PropertyValue("Title", operation.Title)); @@ -30,6 +34,16 @@ public OperationDetailsViewModel(FileSystemState fileSystemState, OperationInfo properties.Add(new PropertyValue("DeclaredOutput", string.Concat(declaredOutputFiles))); properties.Add(new PropertyValue("ReadAccess", string.Concat(readAccessFiles))); properties.Add(new PropertyValue("WriteAccess", string.Concat(writeAccessFiles))); + + if (operationResult != null) + { + properties.Add(new PropertyValue("WasSuccessfulRun", operationResult.WasSuccessfulRun.ToString())); + properties.Add(new PropertyValue("EvaluateTime", operationResult.EvaluateTime.ToString())); + var observedInputFiles = fileSystemState.GetFilePaths(operationResult.ObservedInput); + var observedOutputFiles = fileSystemState.GetFilePaths(operationResult.ObservedOutput); + properties.Add(new PropertyValue("ObservedInput", string.Concat(observedInputFiles))); + properties.Add(new PropertyValue("OvservedOutput", string.Concat(observedOutputFiles))); + } } public IList Properties => properties; diff --git a/Source/GenerateSharp/SoupView/ViewModel/OperationGraphPageModel.cs b/Source/GenerateSharp/SoupView/ViewModel/OperationGraphPageModel.cs index 5cd730dd..a494c6bf 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/OperationGraphPageModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/OperationGraphPageModel.cs @@ -15,11 +15,11 @@ namespace SoupView.ViewModel { internal class OperationGraphPageModel : Observable { - private GraphNode selectedNode = null; - private OperationDetailsViewModel selectedOperation = null; + private GraphNode? selectedNode = null; + private OperationDetailsViewModel? selectedOperation = null; private string errorBarMessage = string.Empty; private bool isErrorBarOpen = false; - private IList> graph = null; + private IList>? graph = null; private Dictionary operationDetailsLookup = new Dictionary(); public string ErrorBarMessage @@ -35,7 +35,7 @@ public string ErrorBarMessage } } - public IList> Graph + public IList>? Graph { get { return graph; } set @@ -48,7 +48,7 @@ public IList> Graph } } - public GraphNode SelectedNode + public GraphNode? SelectedNode { get { return selectedNode; } set @@ -57,12 +57,19 @@ public GraphNode SelectedNode { selectedNode = value; NotifyPropertyChanged(); - SelectedOperation = this.operationDetailsLookup[selectedNode.Id]; + if (selectedNode != null) + { + SelectedOperation = this.operationDetailsLookup[selectedNode.Id]; + } + else + { + SelectedOperation = null; + } } } } - public OperationDetailsViewModel SelectedOperation + public OperationDetailsViewModel? SelectedOperation { get { return selectedOperation; } set @@ -106,7 +113,15 @@ public async Task LoadProjectAsync(Path recipeFilePath) return; } - Graph = BuildGraph(fileSystemState, evaluateGraph); + // Check for the optional results + var evaluateResultsFile = soupTargetDirectory + BuildConstants.EvaluateResultsFileName; + OperationResults? operationResults = null; + if (OperationResultsManager.TryLoadState(evaluateResultsFile, fileSystemState, out var loadOperationResults)) + { + operationResults = loadOperationResults; + } + + Graph = BuildGraph(fileSystemState, evaluateGraph, operationResults); } else { @@ -123,13 +138,22 @@ private void NotifyError(string message) IsErrorBarOpen = true; } - private IList> BuildGraph(FileSystemState fileSystemState, OperationGraph evaluateGraph) + private IList> BuildGraph( + FileSystemState fileSystemState, + OperationGraph evaluateGraph, + OperationResults? operationResults) { this.operationDetailsLookup.Clear(); var activeIds = evaluateGraph.GetRootOperationIds(); var activeGraph = new List>(); var knownIds = new HashSet(); - BuildGraphColumn(fileSystemState, evaluateGraph, activeGraph, activeIds, knownIds); + BuildGraphColumn( + fileSystemState, + evaluateGraph, + operationResults, + activeGraph, + activeIds, + knownIds); return activeGraph; } @@ -137,6 +161,7 @@ private IList> BuildGraph(FileSystemState fileSystemState, Oper private void BuildGraphColumn( FileSystemState fileSystemState, OperationGraph evaluateGraph, + OperationResults? operationResults, IList> activeGraph, IList activeIds, HashSet knownIds) @@ -155,7 +180,13 @@ private void BuildGraphColumn( // Find the depest level first if (nextIds.Count > 0) { - BuildGraphColumn(fileSystemState, evaluateGraph, activeGraph, nextIds, knownIds); + BuildGraphColumn( + fileSystemState, + evaluateGraph, + operationResults, + activeGraph, + nextIds, + knownIds); } // Build up all the nodes at this level that have not already been added @@ -174,7 +205,19 @@ private void BuildGraphColumn( knownIds.Add(operationId); column.Add(node); - this.operationDetailsLookup.Add(operationId.value, new OperationDetailsViewModel(fileSystemState, operation)); + // Check if there is a matching result + OperationResult? operationResult = null; + if (operationResults != null) + { + if (operationResults.TryFindResult(operationId, out var operationResultValue)) + { + operationResult = operationResultValue; + } + } + + this.operationDetailsLookup.Add( + operationId.value, + new OperationDetailsViewModel(fileSystemState, operation, operationResult)); } } diff --git a/Source/GenerateSharp/SoupView/ViewModel/ProjectDetailsViewModel.cs b/Source/GenerateSharp/SoupView/ViewModel/ProjectDetailsViewModel.cs index 8fabc277..0a160a58 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/ProjectDetailsViewModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/ProjectDetailsViewModel.cs @@ -13,7 +13,7 @@ internal class ProjectDetailsViewModel : Observable { private ObservableCollection properties = new ObservableCollection(); - public ProjectDetailsViewModel(Recipe recipe, Path path) + public ProjectDetailsViewModel(Recipe? recipe, Path path) { properties.Clear(); if (recipe is not null) diff --git a/Source/GenerateSharp/SoupView/ViewModel/TaskDetailsViewModel.cs b/Source/GenerateSharp/SoupView/ViewModel/TaskDetailsViewModel.cs index 47826e4d..51d9377b 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/TaskDetailsViewModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/TaskDetailsViewModel.cs @@ -35,40 +35,47 @@ private void BuildValueTable( viewModelList.Clear(); foreach (var value in table) { - var viewModel = new ValueTableItemViewModel(); - + string title; + ValueTableItemType type; + var children = new ObservableCollection(); switch (value.Value.Type) { case ValueType.Boolean: - viewModel.Title = $"{value.Key} - {value.Value.AsBoolean()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.Key} - {value.Value.AsBoolean()}"; + type = ValueTableItemType.Value; break; case ValueType.String: - viewModel.Title = $"{value.Key} - {value.Value.AsString()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.Key} - {value.Value.AsString()}"; + type = ValueTableItemType.Value; break; case ValueType.Float: - viewModel.Title = $"{value.Key} - {value.Value.AsFloat()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.Key} - {value.Value.AsFloat()}"; + type = ValueTableItemType.Value; break; case ValueType.Integer: - viewModel.Title = $"{value.Key} - {value.Value.AsInteger()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.Key} - {value.Value.AsInteger()}"; + type = ValueTableItemType.Value; break; case ValueType.List: - viewModel.Title = $"{value.Key}"; - viewModel.Type = ValueTableItemType.List; - BuildValueList(value.Value.AsList(), viewModel.Children); + title = $"{value.Key}"; + type = ValueTableItemType.List; + BuildValueList(value.Value.AsList(), children); break; case ValueType.Table: - viewModel.Title = $"{value.Key}"; - viewModel.Type = ValueTableItemType.Table; - BuildValueTable(value.Value.AsTable(), viewModel.Children); + title = $"{value.Key}"; + type = ValueTableItemType.Table; + BuildValueTable(value.Value.AsTable(), children); break; default: throw new InvalidOperationException("Unknown Value type"); } + var viewModel = new ValueTableItemViewModel(title) + { + Type = type, + Children = children, + }; + viewModelList.Add(viewModel); } } @@ -80,40 +87,47 @@ private void BuildValueList( viewModelList.Clear(); foreach (var value in list) { - var viewModel = new ValueTableItemViewModel(); - + string title; + ValueTableItemType type; + var children = new ObservableCollection(); ; switch (value.Type) { case ValueType.Boolean: - viewModel.Title = $"{value.AsBoolean()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.AsBoolean()}"; + type = ValueTableItemType.Value; break; case ValueType.String: - viewModel.Title = $"{value.AsString()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.AsString()}"; + type = ValueTableItemType.Value; break; case ValueType.Float: - viewModel.Title = $"{value.AsFloat()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.AsFloat()}"; + type = ValueTableItemType.Value; break; case ValueType.Integer: - viewModel.Title = $"{value.AsInteger()}"; - viewModel.Type = ValueTableItemType.Value; + title = $"{value.AsInteger()}"; + type = ValueTableItemType.Value; break; case ValueType.List: - viewModel.Title = string.Empty; - viewModel.Type = ValueTableItemType.List; - BuildValueList(value.AsList(), viewModel.Children); + title = string.Empty; + type = ValueTableItemType.List; + BuildValueList(value.AsList(), children); break; case ValueType.Table: - viewModel.Title = string.Empty; - viewModel.Type = ValueTableItemType.Table; - BuildValueTable(value.AsTable(), viewModel.Children); + title = string.Empty; + type = ValueTableItemType.Table; + BuildValueTable(value.AsTable(), children); break; default: throw new InvalidOperationException("Unknown Value type"); } + var viewModel = new ValueTableItemViewModel(title) + { + Type = type, + Children = children, + }; + viewModelList.Add(viewModel); } } diff --git a/Source/GenerateSharp/SoupView/ViewModel/TaskGraphPageModel.cs b/Source/GenerateSharp/SoupView/ViewModel/TaskGraphPageModel.cs index 36398e52..b68146f1 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/TaskGraphPageModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/TaskGraphPageModel.cs @@ -14,12 +14,12 @@ namespace SoupView.ViewModel { internal class TaskGraphPageModel : Observable { - private GraphNode selectedNode = null; - private TaskDetailsViewModel selectedTask = null; + private GraphNode? selectedNode = null; + private TaskDetailsViewModel? selectedTask = null; private string errorBarMessage = string.Empty; private uint uniqueId = 0; private bool isErrorBarOpen = false; - private IList> graph = null; + private IList>? graph = null; private Dictionary taskDetailsLookup = new Dictionary(); public string ErrorBarMessage @@ -35,7 +35,7 @@ public string ErrorBarMessage } } - public IList> Graph + public IList>? Graph { get { return graph; } set @@ -48,7 +48,7 @@ public IList> Graph } } - public GraphNode SelectedNode + public GraphNode? SelectedNode { get { return selectedNode; } set @@ -57,12 +57,19 @@ public GraphNode SelectedNode { selectedNode = value; NotifyPropertyChanged(); - SelectedTask = this.taskDetailsLookup[selectedNode.Id]; + if (selectedNode!= null) + { + SelectedTask = this.taskDetailsLookup[selectedNode.Id]; + } + else + { + SelectedTask = null; + } } } } - public TaskDetailsViewModel SelectedTask + public TaskDetailsViewModel? SelectedTask { get { return selectedTask; } set diff --git a/Source/GenerateSharp/SoupView/ViewModel/ValueTableItemViewModel.cs b/Source/GenerateSharp/SoupView/ViewModel/ValueTableItemViewModel.cs index 3e0ca93a..428d0dbc 100644 --- a/Source/GenerateSharp/SoupView/ViewModel/ValueTableItemViewModel.cs +++ b/Source/GenerateSharp/SoupView/ViewModel/ValueTableItemViewModel.cs @@ -53,5 +53,10 @@ public bool IsSelected } } } + + public ValueTableItemViewModel(string title) + { + Title = title; + } } } diff --git a/Source/GenerateSharp/Utilities/BuildConstants.cs b/Source/GenerateSharp/Utilities/BuildConstants.cs index 4d9c2a33..95ae1e3f 100644 --- a/Source/GenerateSharp/Utilities/BuildConstants.cs +++ b/Source/GenerateSharp/Utilities/BuildConstants.cs @@ -46,6 +46,11 @@ public static class BuildConstants /// public static Path EvaluateGraphFileName => new Path("Evaluate.bog"); + /// + /// Gets the Evaluate Results file name + /// + public static Path EvaluateResultsFileName => new Path("Evaluate.bor"); + /// /// Gets the Generate task info Value Table file name /// diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphManager.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphManager.cs index 9d79cf85..ad0ef2c3 100644 --- a/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphManager.cs +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphManager.cs @@ -6,6 +6,7 @@ using Opal.System; using Soup.Build.Runtime; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Soup.Build.Utilities @@ -21,13 +22,13 @@ public static class OperationGraphManager public static bool TryLoadState( Path operationGraphFile, FileSystemState fileSystemState, - out OperationGraph result) + [MaybeNullWhen(false)] out OperationGraph result) { // Verify the requested file exists if (!LifetimeManager.Get().Exists(operationGraphFile)) { Log.Info("Operation graph file does not exist"); - result = new OperationGraph(); + result = null; return false; } @@ -42,9 +43,9 @@ public static bool TryLoadState( // Map up the incoming file ids to the active file system state ids var activeFileIdMap = new Dictionary(); - for (var i = 0; i < loadedResult.GetReferencedFiles().Count; i++) + for (var i = 0; i < loadedResult.ReferencedFiles.Count; i++) { - var fileReference = loadedResult.GetReferencedFiles()[i]; + var fileReference = loadedResult.ReferencedFiles[i]; var activeFileId = fileSystemState.ToFileId(fileReference.Path); activeFileIdMap.Add(fileReference.FileId, activeFileId); @@ -53,7 +54,7 @@ public static bool TryLoadState( } // Update all of the operations - foreach (var operationReference in loadedResult.GetOperations()) + foreach (var operationReference in loadedResult.Operations) { var operation = operationReference.Value; UpdateFileIds(operation.DeclaredInput, activeFileIdMap); @@ -66,7 +67,7 @@ public static bool TryLoadState( catch { Log.Error("Failed to parse operation graph"); - result = new OperationGraph(); + result = null; return false; } } @@ -79,11 +80,9 @@ public static void SaveState( OperationGraph state, FileSystemState fileSystemState) { - var targetFolder = operationGraphFile.GetParent(); - // Update the operation graph referenced files var files = new HashSet(); - foreach (var operationReference in state.GetOperations()) + foreach (var operationReference in state.Operations) { var operation = operationReference.Value; files.UnionWith(operation.DeclaredInput); @@ -98,7 +97,7 @@ public static void SaveState( referencedFiles.Add((fileId, fileSystemState.GetFilePath(fileId))); } - state.SetReferencedFiles(referencedFiles); + state.ReferencedFiles = referencedFiles; // Open the file to write to using var fileStream = System.IO.File.Open( diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphWriter.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphWriter.cs index 963db31c..07fcc99d 100644 --- a/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphWriter.cs +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationGraphWriter.cs @@ -23,7 +23,7 @@ public static void Serialize(OperationGraph state, System.IO.BinaryWriter writer writer.Write(FileVersion); // Write out the set of files - var files = state.GetReferencedFiles(); + var files = state.ReferencedFiles; writer.Write(new char[] { 'F', 'I', 'S', '\0' }); writer.Write((uint)files.Count); foreach (var file in files) @@ -35,13 +35,12 @@ public static void Serialize(OperationGraph state, System.IO.BinaryWriter writer // Write out the root operation ids writer.Write(new char[] { 'R', 'O', 'P', '\0' }); - WriteValues(writer, state.GetRootOperationIds()); + WriteValues(writer, state.RootOperationIds); // Write out the set of operations - var operations = state.GetOperations(); writer.Write(new char[] { 'O', 'P', 'S', '\0' }); - writer.Write((uint)operations.Count); - foreach (var operationValue in state.GetOperations()) + writer.Write((uint)state.Operations.Count); + foreach (var operationValue in state.Operations) { WriteOperationInfo(writer, operationValue.Value); } diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationResult.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationResult.cs new file mode 100644 index 00000000..6642febc --- /dev/null +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationResult.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Soup. All rights reserved. +// + +using Soup.Build.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Soup.Build.Utilities +{ + public class OperationResult : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + public OperationResult( + bool wasSuccessfulRun, + DateTime evaluateTime, + IList observedInput, + IList observedOutput) + { + WasSuccessfulRun = wasSuccessfulRun; + EvaluateTime = evaluateTime; + ObservedInput = observedInput; + ObservedOutput = observedOutput; + } + + public bool Equals(OperationResult? rhs) + { + if (ReferenceEquals(rhs, null)) + return false; + + var result = WasSuccessfulRun == rhs.WasSuccessfulRun && + EvaluateTime == rhs.EvaluateTime && + Enumerable.SequenceEqual(ObservedInput, rhs.ObservedInput) && + Enumerable.SequenceEqual(ObservedOutput, rhs.ObservedOutput); + + return result; + } + + public override bool Equals(object? rhs) + { + return Equals(rhs as OperationResult); + } + + public override int GetHashCode() + { + return (WasSuccessfulRun.GetHashCode() * 0x100000) + (EvaluateTime.GetHashCode() * 0x1000); + } + + public static bool operator ==(OperationResult? lhs, OperationResult? rhs) + { + if (ReferenceEquals(lhs, null)) + return ReferenceEquals(rhs, null); + return lhs.Equals(rhs); + } + + public static bool operator !=(OperationResult? lhs, OperationResult? rhs) + { + return !(lhs == rhs); + } + + public bool WasSuccessfulRun { get; init; } + public DateTime EvaluateTime { get; init; } + public IList ObservedInput { get; init; } + public IList ObservedOutput { get; init; } + } +} \ No newline at end of file diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationResults.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationResults.cs new file mode 100644 index 00000000..66a5daa4 --- /dev/null +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationResults.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Soup. All rights reserved. +// + +using Opal; +using Soup.Build.Runtime; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Soup.Build.Utilities +{ + /// + /// The cached operation results that is used to track input/output mappings for previous build + /// executions to support incremental builds + /// + public class OperationResults + { + private Dictionary _results; + + /// + /// Initializes a new instance of the class. + /// + public OperationResults() + { + ReferencedFiles = new List<(FileId FileId, Path Path)>(); + _results = new Dictionary(); + } + + /// + /// Initializes a new instance of the class. + /// + public OperationResults( + IList<(FileId FileId, Path Path)> referencedFiles, + Dictionary results) + { + ReferencedFiles = referencedFiles; + _results = results; + } + + /// + /// Get the set of referenced file ids that map to their paths + /// + public IList<(FileId FileId, Path Path)> ReferencedFiles { get; set; } + + /// + /// Get Results + /// + public IDictionary Results => _results; + + /// + /// Find an operation result + /// + public bool TryFindResult( + OperationId operationId, + [MaybeNullWhen(false)] out OperationResult result) + { + if (_results.TryGetValue(operationId, out var operationResult)) + { + result = operationResult; + return true; + } + else + { + result = null; + return false; + } + } + } +} diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsManager.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsManager.cs new file mode 100644 index 00000000..1c52ef87 --- /dev/null +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsManager.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) Soup. All rights reserved. +// + +using Opal; +using Opal.System; +using Soup.Build.Runtime; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Soup.Build.Utilities +{ + /// + /// The operation state manager + /// + public static class OperationResultsManager + { + /// + /// Load the operation state from the provided directory + /// + public static bool TryLoadState( + Path operationResultsFile, + FileSystemState fileSystemState, + [MaybeNullWhen(false)] out OperationResults result) + { + // Verify the requested file exists + if (!LifetimeManager.Get().Exists(operationResultsFile)) + { + Log.Info("Operation results file does not exist"); + result = null; + return false; + } + + // Open the file to read from + using var file = LifetimeManager.Get().OpenRead(operationResultsFile); + using var reader = new System.IO.BinaryReader(file.GetInStream(), Encoding.UTF8, true); + + // Read the contents of the build state file + try + { + var loadedResult = OperationResultsReader.Deserialize(reader); + + // Map up the incoming file ids to the active file system state ids + var activeFileIdMap = new Dictionary(); + for (var i = 0; i < loadedResult.ReferencedFiles.Count; i++) + { + var fileReference = loadedResult.ReferencedFiles[i]; + var activeFileId = fileSystemState.ToFileId(fileReference.Path); + activeFileIdMap.Add(fileReference.FileId, activeFileId); + + // Update the referenced id + fileReference.FileId = activeFileId; + } + + // Update all of the operations + foreach (var operationReference in loadedResult.Results) + { + var operation = operationReference.Value; + UpdateFileIds(operation.ObservedInput, activeFileIdMap); + UpdateFileIds(operation.ObservedOutput, activeFileIdMap); + } + + result = loadedResult; + return true; + } + catch + { + Log.Error("Failed to parse operation results"); + result = null; + return false; + } + } + + /// + /// Save the operation state for the provided directory + /// + public static void SaveState( + Path operationGraphFile, + OperationGraph state, + FileSystemState fileSystemState) + { + var targetFolder = operationGraphFile.GetParent(); + + // Update the operation graph referenced files + var files = new HashSet(); + foreach (var operationReference in state.Operations) + { + var operation = operationReference.Value; + files.UnionWith(operation.DeclaredInput); + files.UnionWith(operation.DeclaredOutput); + files.UnionWith(operation.ReadAccess); + files.UnionWith(operation.WriteAccess); + } + + var referencedFiles = new List<(FileId FileId, Path Path)>(); + foreach (var fileId in files) + { + referencedFiles.Add((fileId, fileSystemState.GetFilePath(fileId))); + } + + state.ReferencedFiles = referencedFiles; + + // Open the file to write to + using var fileStream = System.IO.File.Open( + operationGraphFile.ToString(), + System.IO.FileMode.Create, + System.IO.FileAccess.Write); + using var writer = new System.IO.BinaryWriter(fileStream); + + // Write the build state to the file stream + OperationGraphWriter.Serialize(state, writer); + } + + private static void UpdateFileIds(IList fileIds, IDictionary activeFileIdMap) + { + for (var i = 0; i < fileIds.Count; i++) + { + var findActiveFileId = activeFileIdMap[fileIds[i]]; + fileIds[i] = findActiveFileId; + } + } + } +} diff --git a/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsReader.cs b/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsReader.cs new file mode 100644 index 00000000..a492e999 --- /dev/null +++ b/Source/GenerateSharp/Utilities/OperationGraph/OperationResultsReader.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) Soup. All rights reserved. +// + +using Opal; +using Soup.Build.Runtime; +using System; +using System.Collections.Generic; + +namespace Soup.Build.Utilities +{ + /// + /// The operation results state reader + /// + internal static class OperationResultsReader + { + // Binary Operation Results file format + private static uint FileVersion => 2; + + public static OperationResults Deserialize(System.IO.BinaryReader reader) + { + // Read the File Header with version + var headerBuffer = reader.ReadBytes(4); + if (headerBuffer[0] != 'B' || + headerBuffer[1] != 'O' || + headerBuffer[2] != 'R' || + headerBuffer[3] != '\0') + { + throw new InvalidOperationException("Invalid operation results file header"); + } + + var fileVersion = reader.ReadUInt32(); + if (fileVersion != FileVersion) + { + throw new InvalidOperationException("Operation results file version does not match expected"); + } + + // Read the set of files + headerBuffer = reader.ReadBytes(4); + if (headerBuffer[0] != 'F' || + headerBuffer[1] != 'I' || + headerBuffer[2] != 'S' || + headerBuffer[3] != '\0') + { + throw new InvalidOperationException("Invalid operation results files header"); + } + + var fileCount = reader.ReadUInt32(); + var files = new List<(FileId FileId, Path Path)>(); + for (var i = 0; i < fileCount; i++) + { + // Read the command working directory + var fileId = new FileId(reader.ReadUInt32()); + var file = new Path(ReadString(reader)); + + files.Add((fileId, file)); + } + + // Read the set of operation results + headerBuffer = reader.ReadBytes(4); + if (headerBuffer[0] != 'R' || + headerBuffer[1] != 'T' || + headerBuffer[2] != 'S' || + headerBuffer[3] != '\0') + { + throw new InvalidOperationException("Invalid operation results operation results header"); + } + + var operationResultsCount = reader.ReadUInt32(); + var operationResults = new Dictionary(); + for (var i = 0; i < operationResultsCount; i++) + { + var (operationId, operationResult) = ReadOperationInfo(reader); + operationResults.Add(operationId, operationResult); + } + + if (reader.BaseStream.Position != reader.BaseStream.Length) + { + var remaining = reader.BaseStream.Length - reader.BaseStream.Position; + throw new InvalidOperationException($"Operation results file corrupted - Did not read the entire file {remaining}"); + } + + return new OperationResults( + files, + operationResults); + } + + private static (OperationId, OperationResult) ReadOperationInfo(System.IO.BinaryReader reader) + { + // Read the operation id + var id = new OperationId(reader.ReadUInt32()); + + // Read the value indicating if there was a successful run + var wasSuccessfulRun = ReadBoolean(reader); + + // Read the utc tick since January 1, 0001 at 00:00:00.000 in the Gregorian calendar + var evaluateTime = new DateTime(reader.ReadInt64(), DateTimeKind.Utc); + + // Read the observed input files + var observedInput = ReadFileIdList(reader); + + // Read the observed output files + var observedOutput = ReadFileIdList(reader); + + return (id, new OperationResult( + wasSuccessfulRun, + evaluateTime, + observedInput, + observedOutput)); + } + + private static bool ReadBoolean(System.IO.BinaryReader reader) + { + uint result = reader.ReadUInt32(); + return result != 0; + } + + private static string ReadString(System.IO.BinaryReader reader) + { + var size = reader.ReadUInt32(); + var result = reader.ReadChars((int)size); + + return new string(result); + } + + private static List ReadFileIdList(System.IO.BinaryReader reader) + { + var size = reader.ReadUInt32(); + var result = new List((int)size); + for (var i = 0; i < size; i++) + { + result.Add(new FileId(reader.ReadUInt32())); + } + + return result; + } + } +} diff --git a/Source/Installer/SoupInstaller/Setup.cs b/Source/Installer/SoupInstaller/Setup.cs index f65009e3..7bfb2093 100644 --- a/Source/Installer/SoupInstaller/Setup.cs +++ b/Source/Installer/SoupInstaller/Setup.cs @@ -55,7 +55,7 @@ static public void Main() }; // Upgrade values - project.Version = new Version(0, 26, 1); + project.Version = new Version(0, 27, 0); Compiler.BuildMsi(project); } diff --git a/Source/Monitor/Client/PackageLock.sml b/Source/Monitor/Client/PackageLock.sml index c3903a19..c86a1f17 100644 --- a/Source/Monitor/Client/PackageLock.sml +++ b/Source/Monitor/Client/PackageLock.sml @@ -5,7 +5,7 @@ Closures: { { Name: "Detours", Version: "4.0.9", Build: "Build0" } { Name: "Monitor.Client", Version: "../Client", Build: "Build0" } { Name: "Monitor.Shared", Version: "../Shared/", Build: "Build0" } - { Name: "Opal", Version: "0.5.1", Build: "Build0" } + { Name: "Opal", Version: "0.7.1", Build: "Build0" } ] } Build0: { diff --git a/Source/Monitor/Host/MockDetourProcessManager.h b/Source/Monitor/Host/MockDetourProcessManager.h index 64093deb..bd1145ca 100644 --- a/Source/Monitor/Host/MockDetourProcessManager.h +++ b/Source/Monitor/Host/MockDetourProcessManager.h @@ -13,6 +13,12 @@ namespace Monitor /// export class MockDetourProcessManager : public IDetourProcessManager { + private: + std::atomic m_uniqueId; + std::vector _requests; + std::map _executeResults; + std::map> _executeCallbacks; + public: /// /// Initializes a new instance of the class. @@ -27,12 +33,26 @@ namespace Monitor /// /// Create a result /// - void RegisterExecuteResult(std::string command, std::string output) + void RegisterExecuteResult( + std::string command, + std::string output) { _executeResults.emplace( std::move(command), std::move(output)); } + + /// + /// Create a result + /// + void RegisterExecuteCallback( + std::string command, + std::function callback) + { + _executeCallbacks.emplace( + std::move(command), + std::move(callback)); + } /// /// Get the load requests @@ -65,6 +85,13 @@ namespace Monitor _requests.push_back(message.str()); + // Check if there is a registered callback + auto findCallback = _executeCallbacks.find(message.str()); + if (findCallback != _executeCallbacks.end()) + { + findCallback->second(*callback); + } + // Check if there is a registered output auto findOutput = _executeResults.find(message.str()); if (findOutput != _executeResults.end()) @@ -86,10 +113,5 @@ namespace Monitor std::string()); } } - - private: - std::atomic m_uniqueId; - std::vector _requests; - std::map _executeResults; }; } diff --git a/Source/Monitor/Host/Module.cpp b/Source/Monitor/Host/Module.cpp index 6a139ac5..7c802cc0 100644 --- a/Source/Monitor/Host/Module.cpp +++ b/Source/Monitor/Host/Module.cpp @@ -20,6 +20,7 @@ module; #include #include #include +#include #include #include #include diff --git a/Source/Tools/PrintGraph/PackageLock.sml b/Source/Tools/PrintGraph/PackageLock.sml index 22762e73..6f0928f2 100644 --- a/Source/Tools/PrintGraph/PackageLock.sml +++ b/Source/Tools/PrintGraph/PackageLock.sml @@ -6,7 +6,7 @@ Closures: { { Name: "Detours", Version: "4.0.9", Build: "Build0" } { Name: "Monitor.Host", Version: "../../Monitor/Host/", Build: "Build0" } { Name: "Monitor.Shared", Version: "../../Monitor/Shared/", Build: "Build0" } - { Name: "Opal", Version: "0.5.1", Build: "Build0" } + { Name: "Opal", Version: "0.7.1", Build: "Build0" } { Name: "printgraph", Version: "../PrintGraph", Build: "Build0" } { Name: "reflex", Version: "1.0.1", Build: "Build0" } { Name: "Soup.Core", Version: "../../Client/Core/", Build: "Build1" } diff --git a/Source/Tools/PrintResults/PackageLock.sml b/Source/Tools/PrintResults/PackageLock.sml index 66606cec..f73768f0 100644 --- a/Source/Tools/PrintResults/PackageLock.sml +++ b/Source/Tools/PrintResults/PackageLock.sml @@ -6,7 +6,7 @@ Closures: { { Name: "Detours", Version: "4.0.9", Build: "Build0" } { Name: "Monitor.Host", Version: "../../Monitor/Host/", Build: "Build0" } { Name: "Monitor.Shared", Version: "../../Monitor/Shared/", Build: "Build0" } - { Name: "Opal", Version: "0.5.1", Build: "Build0" } + { Name: "Opal", Version: "0.7.1", Build: "Build0" } { Name: "printresults", Version: "../PrintResults", Build: "Build0" } { Name: "reflex", Version: "1.0.1", Build: "Build0" } { Name: "Soup.Core", Version: "../../Client/Core/", Build: "Build1" } diff --git a/Source/Tools/PrintValueTable/PackageLock.sml b/Source/Tools/PrintValueTable/PackageLock.sml index 52bd3cdc..cb0d7bc3 100644 --- a/Source/Tools/PrintValueTable/PackageLock.sml +++ b/Source/Tools/PrintValueTable/PackageLock.sml @@ -6,7 +6,7 @@ Closures: { { Name: "Detours", Version: "4.0.9", Build: "Build0" } { Name: "Monitor.Host", Version: "../../Monitor/Host/", Build: "Build0" } { Name: "Monitor.Shared", Version: "../../Monitor/Shared/", Build: "Build0" } - { Name: "Opal", Version: "0.5.1", Build: "Build0" } + { Name: "Opal", Version: "0.7.1", Build: "Build0" } { Name: "printvaluetable", Version: "../PrintValueTable", Build: "Build0" } { Name: "reflex", Version: "1.0.1", Build: "Build0" } { Name: "Soup.Core", Version: "../../Client/Core/", Build: "Build1" }