From df76a77ee23ce7dbd91e07fc0e2a27d4fda0afda Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 8 Jul 2024 07:32:35 +0200 Subject: [PATCH] Added tool to generate test data --- Tools/TestDataGenerator/.gitignore | 1 + Tools/TestDataGenerator/.vscode/launch.json | 27 +++ Tools/TestDataGenerator/.vscode/tasks.json | 17 ++ Tools/TestDataGenerator/Commands/Create.cs | 227 ++++++++++++++++++ Tools/TestDataGenerator/Commands/Shared.cs | 132 ++++++++++ Tools/TestDataGenerator/Commands/Update.cs | 195 +++++++++++++++ Tools/TestDataGenerator/Program.cs | 10 + Tools/TestDataGenerator/README.md | 7 + .../TestDataGenerator.csproj | 15 ++ Tools/TestDataGenerator/TestDataGenerator.sln | 25 ++ 10 files changed, 656 insertions(+) create mode 100644 Tools/TestDataGenerator/.gitignore create mode 100644 Tools/TestDataGenerator/.vscode/launch.json create mode 100644 Tools/TestDataGenerator/.vscode/tasks.json create mode 100644 Tools/TestDataGenerator/Commands/Create.cs create mode 100644 Tools/TestDataGenerator/Commands/Shared.cs create mode 100644 Tools/TestDataGenerator/Commands/Update.cs create mode 100644 Tools/TestDataGenerator/Program.cs create mode 100644 Tools/TestDataGenerator/README.md create mode 100644 Tools/TestDataGenerator/TestDataGenerator.csproj create mode 100644 Tools/TestDataGenerator/TestDataGenerator.sln diff --git a/Tools/TestDataGenerator/.gitignore b/Tools/TestDataGenerator/.gitignore new file mode 100644 index 000000000..1269488f7 --- /dev/null +++ b/Tools/TestDataGenerator/.gitignore @@ -0,0 +1 @@ +data diff --git a/Tools/TestDataGenerator/.vscode/launch.json b/Tools/TestDataGenerator/.vscode/launch.json new file mode 100644 index 000000000..b265cd745 --- /dev/null +++ b/Tools/TestDataGenerator/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Generate Test Data", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net8.0/TestDataGenerator.dll", + "args": ["create", "./data", "--file-count=1000", "--max-folder-count=100", "--max-fan-out=3"], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "Update Test Data", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net8.0/TestDataGenerator.dll", + "args": ["update", "./data", "--new-files=10", "--updated-files=10", "--deleted-files=10"], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + } + ] +} \ No newline at end of file diff --git a/Tools/TestDataGenerator/.vscode/tasks.json b/Tools/TestDataGenerator/.vscode/tasks.json new file mode 100644 index 000000000..f2835a246 --- /dev/null +++ b/Tools/TestDataGenerator/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/TestDataGenerator.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Tools/TestDataGenerator/Commands/Create.cs b/Tools/TestDataGenerator/Commands/Create.cs new file mode 100644 index 000000000..4eed98130 --- /dev/null +++ b/Tools/TestDataGenerator/Commands/Create.cs @@ -0,0 +1,227 @@ +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; +using static TestDataGenerator.Commands.Shared; + +namespace TestDataGenerator.Commands; + +/// +/// Create test data in a folder +/// +public static class Create +{ + /// + /// The input parameters for the command + /// + /// The folder to create files in + /// The number of files to create in the target folder + /// The maximum total size of the files to create in the target folder + /// The maximum size of each file to create in the target folder + /// The minimum size of each file to create in the target folder + /// The percentage of data that should be zeroed out in each file + /// The maximum number of subfolders to create in each folder + /// The maximum depth of subfolders to create in the target folder + /// The maximum number of folders to create in the target folder + /// The maximum length of each path segment in the target folder + record CommandInput( + DirectoryInfo TargetFolder, + int FileCount, + long MaxTotalSize, + long MaxFileSize, + long MinFileSize, + int SparseFactor, + int MaxFanOut, + int MaxDepth, + int MaxFolderCount, + int MaxPathSegmentLength); + + /// + /// Creates the command + /// + /// The command + public static Command CreateCommand() + { + var command = new Command("create", "Create test data in a folder"); + + var targetFolderOption = new Argument("target-folder", "The folder to create files in"); + targetFolderOption.SetDefaultValue(new DirectoryInfo(Directory.GetCurrentDirectory())); + command.AddArgument(targetFolderOption); + + var fileCountOption = new Option("--file-count", "The number of files to create in the target folder"); + fileCountOption.SetDefaultValue(200000); + command.AddOption(fileCountOption); + + var maxTotalSizeOption = new Option("--max-total-size", "The maximum total size of the files to create in the target folder"); + maxTotalSizeOption.SetDefaultValue(1024 * 1024 * 1024L); + command.AddOption(maxTotalSizeOption); + + var maxFileSizeOption = new Option("--max-file-size", "The maximum size of each file to create in the target folder"); + maxFileSizeOption.SetDefaultValue(1024 * 1024L); + command.AddOption(maxFileSizeOption); + + var minFileSizeOption = new Option("--min-file-size", "The minimum size of each file to create in the target folder"); + minFileSizeOption.SetDefaultValue(0L); + command.AddOption(minFileSizeOption); + + var sparseFactorOption = new Option("--sparse-factor", "The percentage of data that should be zeroed out in each file"); + sparseFactorOption.SetDefaultValue(10); + command.AddOption(sparseFactorOption); + + var maxFanOutOption = new Option("--max-fan-out", "The maximum number of subfolders to create in each folder"); + maxFanOutOption.SetDefaultValue(10); + command.AddOption(maxFanOutOption); + + var maxDepthOption = new Option("--max-depth", "The maximum depth of subfolders to create in the target folder"); + maxDepthOption.SetDefaultValue(5); + command.AddOption(maxDepthOption); + + var maxFolderCountOption = new Option("--max-folder-count", "The maximum number of folders to create in the target folder"); + maxFolderCountOption.SetDefaultValue(10000); + command.AddOption(maxFolderCountOption); + + var maxPathSegmentLengthOption = new Option("--max-path-segment-length", "The maximum length of each path segment in the target folder"); + maxPathSegmentLengthOption.SetDefaultValue(15); + command.AddOption(maxPathSegmentLengthOption); + + command.Handler = CommandHandler.Create(Execute); + + return command; + } + + /// + /// Executes the command + /// + /// The input parameters + private static void Execute(CommandInput input) + { + if (input.TargetFolder.Exists && input.TargetFolder.GetFileSystemInfos().Length > 0) + throw new Exception($"The target folder {input.TargetFolder.FullName} already exists"); + + if (input.MaxFileSize == 0) + throw new Exception("The maximum file size must be greater than zero"); + + if (input.MaxTotalSize == 0) + throw new Exception("The maximum total size must be greater than zero"); + + if (input.MinFileSize > input.MaxFileSize) + throw new Exception("The minimum file size must be less than or equal to the maximum file size"); + + if (input.FileCount == 0) + throw new Exception("The file count must be greater than zero"); + + if (input.MaxFolderCount == 0) + throw new Exception("The maximum folder count must be greater than zero"); + + if (input.MaxFanOut == 0) + throw new Exception("The maximum fan-out must be greater than zero"); + + if (input.MaxDepth == 0) + throw new Exception("The maximum depth must be greater than zero"); + + if (input.MaxPathSegmentLength == 0) + throw new Exception("The maximum path segment length must be greater than zero"); + + Console.WriteLine($"Creating test data in {input.TargetFolder.FullName}"); + var rnd = new Random(); + var folders = GeneratePathStructure(rnd, input.TargetFolder.FullName, input.MaxDepth, input.MaxFanOut, input.MaxPathSegmentLength); + while (folders.Count < input.MaxFolderCount) + { + folders.AddRange(GeneratePathStructure(rnd, input.TargetFolder.FullName, input.MaxDepth, input.MaxFanOut, input.MaxPathSegmentLength)); + folders = folders.Distinct().ToList(); + } + + while (folders.Count > input.MaxFolderCount) + folders.RemoveAt(folders.Count - 1); + + folders.Add(input.TargetFolder.FullName); + + var files = DistributeFiles(rnd, folders, GenerateFileNames(rnd, input.FileCount, input.MaxPathSegmentLength)); + + var filesWithSizes = files.Select(x => (x, rnd.NextInt64(input.MinFileSize, input.MaxFileSize))).ToList(); + var totalSize = filesWithSizes.Sum(x => x.Item2); + + while (totalSize > input.MaxTotalSize) + { + var index = rnd.Next(filesWithSizes.Count); + var newSize = rnd.NextInt64(input.MinFileSize, filesWithSizes[index].Item2); + totalSize -= filesWithSizes[index].Item2 - newSize; + filesWithSizes[index] = (filesWithSizes[index].Item1, newSize); + } + + Console.WriteLine($"Creating {folders.Count - 1} folders and {files.Count} files ({SizeToHumanReadable(totalSize)})"); + if (!input.TargetFolder.Exists) + input.TargetFolder.Create(); + + var lastUpdate = DateTime.UtcNow; + var foldersCreated = 0L; + + var updateInterval = TimeSpan.FromSeconds(10); + + foreach (var folder in folders) + { + Directory.CreateDirectory(folder); + foldersCreated++; + if ((DateTime.UtcNow - lastUpdate) > updateInterval) + { + Console.WriteLine($"Created {foldersCreated} of {folders.Count} folders"); + lastUpdate = DateTime.UtcNow; + } + } + + var filesCreated = 0L; + var fileSizeCreated = 0L; + + foreach (var (file, size) in filesWithSizes) + { + try + { + using var fs = new FileStream(file, FileMode.Create, FileAccess.Write); + if (size > 0) + { + fs.SetLength(size); + fs.Position = 0; + + WriteRandomData(rnd, fs, size, input.SparseFactor); + } + + filesCreated++; + fileSizeCreated += size; + if ((DateTime.UtcNow - lastUpdate) > updateInterval) + { + Console.WriteLine($"Created {filesCreated} of {files.Count} files ({SizeToHumanReadable(fileSizeCreated)} of {SizeToHumanReadable(totalSize)})"); + lastUpdate = DateTime.UtcNow; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error creating file {file}: {ex.Message}"); + } + } + + } + + /// + /// Generates a list of random folder paths + /// + /// The random number generator to use + /// The prefix to use for the folder paths + /// The maximum depth of the folder structure + /// The maximum number of subfolders to create in each folder + /// The maximum length of each path segment + private static List GeneratePathStructure(Random rnd, string prefix, int maxDepth, int maxFanOut, int maxSegmentLength) + { + var folders = new List(); + var folderCount = rnd.Next(1, maxFanOut + 1); + + if (maxDepth == 0) + return GeneratePathSegments(rnd, folderCount, maxSegmentLength).Select(x => Path.Combine(prefix, x)).ToList(); + + for (var i = 0; i < folderCount; i++) + { + var folderName = GetPathSegment(rnd, maxSegmentLength); + folders.Add(Path.Combine(prefix, folderName)); + folders.AddRange(GeneratePathStructure(rnd, Path.Combine(prefix, folderName), maxDepth - 1, maxFanOut, maxSegmentLength)); + } + + return folders; + } +} diff --git a/Tools/TestDataGenerator/Commands/Shared.cs b/Tools/TestDataGenerator/Commands/Shared.cs new file mode 100644 index 000000000..ac9afad60 --- /dev/null +++ b/Tools/TestDataGenerator/Commands/Shared.cs @@ -0,0 +1,132 @@ +using System.Text; + +namespace TestDataGenerator.Commands; + +/// +/// Shared utility methods for the test data generator +/// +public static class Shared +{ + /// + /// Distributes the files in the list to the folders in the list + /// + /// The random number generator to use + /// The list of folders to distribute the files to + /// The list of files to distribute + /// The list of files distributed to the folders + public static List DistributeFiles(Random rnd, List folders, List list) + { + var expanded = new List(); + foreach (var file in list) + expanded.Add(Path.Combine(folders[rnd.Next(folders.Count)], file)); + + return expanded; + } + + /// + /// Converts a size in bytes to a human-readable string + /// + /// The size in bytes + /// The human-readable string + public static string SizeToHumanReadable(long size) + { + if (size < 1024) + return $"{size} B"; + if (size < 1024 * 1000) + return $"{size / 1024m:F2} KiB"; + if (size < 1024 * 1024 * 1000) + return $"{size / 1024 / 1024m:F2} MiB"; + if (size < 1024L * 1024 * 1024 * 1000) + return $"{size / 1024 / 1024 / 1024m:F2} GiB"; + return $"{size / 1024 / 1024 / 1024 / 1024m:F2} TiB"; + } + + /// + /// The list of file extensions to use for random file names + /// + private static readonly IReadOnlyList FileExtensions = new List([".bin", ".dat", ".binary", ".data", ".test", ""]); + + /// + /// The characters to use for random path segments + /// + private const string PathSegmentChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /// + /// Generates a random path segment + /// + /// The random number generator to use + /// The maximum length of the path segment + /// The random path segment + public static string GetPathSegment(Random rnd, int maxLength) + { + var length = rnd.Next(1, maxLength + 1); + var sb = new StringBuilder(length); + for (int j = 0; j < length; j++) + sb.Append(PathSegmentChars[rnd.Next(PathSegmentChars.Length)]); + return sb.ToString(); + } + + /// + /// Generates a list of random path segments + /// + /// The random number generator to use + /// The number of path segments to generate + /// The maximum length of each path segment + /// The list of path segments + public static List GeneratePathSegments(Random rnd, int count, int maxLength) + { + var names = new List(count); + for (int i = 0; i < count; i++) + names.Add(GetPathSegment(rnd, maxLength)); + return names; + } + + /// + /// Generates a list of random file names + /// + /// The random number generator to use + /// The number of file names to generate + /// The maximum length of each file name + /// The list of file names + public static List GenerateFileNames(Random rnd, int count, int maxLength) + { + var names = new List(count); + for (int i = 0; i < count; i++) + names.Add(GetPathSegment(rnd, maxLength) + FileExtensions[rnd.Next(FileExtensions.Count)]); + return names; + } + + /// + /// Writes random data to a stream + /// + /// The random number generator to use + /// The stream to write to + /// The size of the data to write + /// The factor to use for sparse data + /// + /// The sparse factor is a percentage value that determines how many bytes are skipped in the stream. + /// For example, a sparse factor of 10 means that 10% of the data will be not be overwritten. + /// + public static void WriteRandomData(Random rnd, Stream fs, long size, int sparseFactor) + { + var buffer = new byte[4096]; + while (size > 0) + { + var chunkSize = rnd.Next(1, (int)Math.Min(size, buffer.Length)); + var sp = buffer.AsSpan(chunkSize); + if (rnd.Next(100) < sparseFactor) + { + // Skip the bytes + fs.Position += sp.Length; + } + else + { + rnd.NextBytes(sp); + fs.Write(sp); + } + + size -= sp.Length; + } + } + +} \ No newline at end of file diff --git a/Tools/TestDataGenerator/Commands/Update.cs b/Tools/TestDataGenerator/Commands/Update.cs new file mode 100644 index 000000000..5066ac50e --- /dev/null +++ b/Tools/TestDataGenerator/Commands/Update.cs @@ -0,0 +1,195 @@ +using System.CommandLine; +using System.CommandLine.NamingConventionBinder; +using static TestDataGenerator.Commands.Shared; + +namespace TestDataGenerator.Commands; + +/// +/// Update test data in a folder +/// +public static class Update +{ + /// + /// The input parameters for the command + /// + /// The folder to update files in + /// The number of new files to create in the target folder + /// The number of files to update in the target folder + /// The number of files to delete in the target folder + /// The number of files to rename in the target folder + /// The maximum size of each file to create in the target folder + /// The minimum size of each file to create in the target folder + /// The percentage of data that should be zeroed out in each file + /// The percentage of files to update in the target folder + /// The maximum length of each path segment in the target folder + record CommandInput( + DirectoryInfo TargetFolder, + int NewFiles, + int UpdatedFiles, + int DeletedFiles, + int RenameFiles, + long MaxFileSize, + long MinFileSize, + int SparseFactor, + int UpdateFactor, + int MaxPathSegmentLength); + + /// + /// Creates the command + /// + /// The command + public static Command CreateCommand() + { + var command = new Command("update", "Update test data in a folder"); + + var targetFolderOption = new Argument("target-folder", "The folder to update files in"); + targetFolderOption.SetDefaultValue(new DirectoryInfo(Directory.GetCurrentDirectory())); + command.AddArgument(targetFolderOption); + + var newFilesOption = new Option("--new-files", "The number of new files to create in the target folder"); + newFilesOption.SetDefaultValue(1000); + command.AddOption(newFilesOption); + + var updatedFilesOption = new Option("--updated-files", "The number of files to update in the target folder"); + updatedFilesOption.SetDefaultValue(1000); + command.AddOption(updatedFilesOption); + + var deletedFilesOption = new Option("--deleted-files", "The number of files to delete in the target folder"); + deletedFilesOption.SetDefaultValue(1000); + command.AddOption(deletedFilesOption); + + var renameFilesOption = new Option("--rename-files", "The number of files to rename in the target folder"); + renameFilesOption.SetDefaultValue(1000); + command.AddOption(renameFilesOption); + + var maxFileSizeOption = new Option("--max-file-size", "The maximum size of each file to create in the target folder"); + maxFileSizeOption.SetDefaultValue(1024 * 1024L); + command.AddOption(maxFileSizeOption); + + var minFileSizeOption = new Option("--min-file-size", "The minimum size of each file to create in the target folder"); + minFileSizeOption.SetDefaultValue(1024L); + command.AddOption(minFileSizeOption); + + var sparseFactorOption = new Option("--sparse-factor", "The percentage of data that should be zeroed out in each file"); + sparseFactorOption.SetDefaultValue(10); + command.AddOption(sparseFactorOption); + + var updateFactorOption = new Option("--update-factor", "The percentage of files to update in the target folder"); + updateFactorOption.SetDefaultValue(10); + command.AddOption(updateFactorOption); + + var maxPathSegmentLengthOption = new Option("--max-path-segment-length", "The maximum length of each path segment in the target folder"); + maxPathSegmentLengthOption.SetDefaultValue(15); + command.AddOption(maxPathSegmentLengthOption); + + command.Handler = CommandHandler.Create(Execute); + + return command; + } + + /// + /// Executes the command + /// + /// The input parameters + private static void Execute(CommandInput input) + { + if (!input.TargetFolder.Exists) + throw new DirectoryNotFoundException($"The target folder {input.TargetFolder.FullName} does not exist"); + + if (input.MaxFileSize == 0) + throw new Exception("The maximum file size must be greater than zero"); + + Console.WriteLine($"Updating test data in {input.TargetFolder.FullName}"); + + var folders = new List(); + var files = new List(); + + foreach (var folder in input.TargetFolder.EnumerateDirectories("*", SearchOption.AllDirectories)) + { + folders.Add(folder.FullName); + foreach (var file in folder.EnumerateFiles()) + files.Add(file.FullName); + } + + Console.WriteLine($"Found {folders.Count} folders and {files.Count} files"); + + folders.Add(input.TargetFolder.FullName); + + var rnd = new Random(); + var newFileNames = DistributeFiles(rnd, folders, GenerateFileNames(rnd, input.NewFiles, input.MaxPathSegmentLength)); + + foreach (var file in newFileNames) + { + Console.WriteLine($"Creating new file {file}"); + + var size = rnd.NextInt64((int)input.MinFileSize, (int)input.MaxFileSize); + using var fs = new FileStream(file, FileMode.Create, FileAccess.Write); + fs.SetLength(size); + if (size > 0) + { + fs.Position = 0; + WriteRandomData(rnd, fs, size, input.SparseFactor); + } + } + + var updatedFileNames = new HashSet(); + while (updatedFileNames.Count < input.UpdatedFiles) + { + var file = files[rnd.Next(files.Count)]; + if (!updatedFileNames.Contains(file)) + updatedFileNames.Add(file); + + if (updatedFileNames.Count == files.Count) + break; + } + + foreach (var file in updatedFileNames) + { + Console.WriteLine($"Updating file {file}"); + + var size = rnd.NextInt64(input.MinFileSize, input.MaxFileSize); + using var fs = new FileStream(file, FileMode.Open, FileAccess.Write); + fs.SetLength(size); + if (size > 0) + { + fs.Position = 0; + WriteRandomData(rnd, fs, size, 100 - input.UpdateFactor); + } + } + + var deletedFileNames = new HashSet(); + while (deletedFileNames.Count < input.DeletedFiles) + { + var file = files[rnd.Next(files.Count)]; + if (!deletedFileNames.Contains(file)) + deletedFileNames.Add(file); + + if (deletedFileNames.Count == files.Count) + break; + } + + foreach (var file in deletedFileNames) + { + Console.WriteLine($"Deleting file {file}"); + File.Delete(file); + } + + var renamedFileNames = new HashSet(); + while (renamedFileNames.Count < input.RenameFiles) + { + var file = files[rnd.Next(files.Count)]; + if (!renamedFileNames.Contains(file)) + renamedFileNames.Add(file); + + if (renamedFileNames.Count == files.Count) + break; + } + + foreach (var file in renamedFileNames) + { + var newName = Path.Combine(folders[rnd.Next(folders.Count)], GenerateFileNames(rnd, 1, input.MaxPathSegmentLength).First()); + Console.WriteLine($"Renaming file {file} to {newName}"); + File.Move(file, newName); + } + } +} diff --git a/Tools/TestDataGenerator/Program.cs b/Tools/TestDataGenerator/Program.cs new file mode 100644 index 000000000..6f43b554d --- /dev/null +++ b/Tools/TestDataGenerator/Program.cs @@ -0,0 +1,10 @@ +using System.CommandLine; + +var rootCommand = new RootCommand("Test Data Generator") +{ + TestDataGenerator.Commands.Create.CreateCommand(), + TestDataGenerator.Commands.Update.CreateCommand() +}; + +return rootCommand.Invoke(args); + diff --git a/Tools/TestDataGenerator/README.md b/Tools/TestDataGenerator/README.md new file mode 100644 index 000000000..f01ba7e88 --- /dev/null +++ b/Tools/TestDataGenerator/README.md @@ -0,0 +1,7 @@ +# Test data generator + +This tool is intended for use by developer who need to generate a data set for testing Duplicati. + +The tool can create a set of random data at a target folder using the `create` command, and make alterations using the `update` command. + +Use the `help` command to examine the supported properties and adjust to make the desired test dataset. diff --git a/Tools/TestDataGenerator/TestDataGenerator.csproj b/Tools/TestDataGenerator/TestDataGenerator.csproj new file mode 100644 index 000000000..04c2e9be8 --- /dev/null +++ b/Tools/TestDataGenerator/TestDataGenerator.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/Tools/TestDataGenerator/TestDataGenerator.sln b/Tools/TestDataGenerator/TestDataGenerator.sln new file mode 100644 index 000000000..6cbfc34b3 --- /dev/null +++ b/Tools/TestDataGenerator/TestDataGenerator.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDataGenerator", "TestDataGenerator.csproj", "{547C0549-040B-4F4F-A410-351FC0176F28}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {547C0549-040B-4F4F-A410-351FC0176F28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {547C0549-040B-4F4F-A410-351FC0176F28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {547C0549-040B-4F4F-A410-351FC0176F28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {547C0549-040B-4F4F-A410-351FC0176F28}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F3FB6242-FFDF-42A7-A78F-BB1FE67C12D8} + EndGlobalSection +EndGlobal