diff --git a/Source/Testably.Abstractions.Testing/FileSystemInitializerExtensions.cs b/Source/Testably.Abstractions.Testing/FileSystemInitializerExtensions.cs index d190e6d43..cf17da070 100644 --- a/Source/Testably.Abstractions.Testing/FileSystemInitializerExtensions.cs +++ b/Source/Testably.Abstractions.Testing/FileSystemInitializerExtensions.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using System.Reflection; using Testably.Abstractions.Testing.FileSystemInitializer; +using Testably.Abstractions.Testing.Helpers; namespace Testably.Abstractions.Testing; @@ -56,4 +58,113 @@ public static IDirectoryCleaner SetCurrentDirectoryToEmptyTemporaryDirectory( { return new DirectoryCleaner(fileSystem, prefix, logger); } + + /// + /// + /// The file system. + /// The assembly in which the embedded resource files are located. + /// The directory path in which the found resource files are created. + /// The relative path of the embedded resources in the . + /// + /// The search string to match against the names of embedded resources in the under + /// .
+ /// This parameter can contain a combination of valid literal path and wildcard (* and ?) characters, but it doesn't + /// support regular expressions. + /// + /// + /// One of the enumeration values that specifies whether the search operation should include only the + /// or should include all subdirectories.
+ /// The default value is . + /// + public static void InitializeEmbeddedResourcesFromAssembly(this IFileSystem fileSystem, + string directoryPath, + Assembly assembly, + string? relativePath = null, + string searchPattern = "*", + SearchOption searchOption = SearchOption.AllDirectories) + { + EnumerationOptions enumerationOptions = + EnumerationOptionsHelper.FromSearchOption(searchOption); + + string[] resourcePaths = assembly.GetManifestResourceNames(); + string assemblyNamePrefix = $"{assembly.GetName().Name ?? ""}."; + + if (relativePath != null) + { + relativePath = relativePath.Replace( + Path.AltDirectorySeparatorChar, + Path.DirectorySeparatorChar); + relativePath = relativePath.TrimEnd(Path.DirectorySeparatorChar); + relativePath += Path.DirectorySeparatorChar; + } + + foreach (string resourcePath in resourcePaths) + { + string fileName = resourcePath; + if (fileName.StartsWith(assemblyNamePrefix)) + { + fileName = fileName.Substring(assemblyNamePrefix.Length); + } + + fileName = fileName.Replace('.', Path.DirectorySeparatorChar); + int lastSeparator = fileName.LastIndexOf(Path.DirectorySeparatorChar); + if (lastSeparator > 0) + { + fileName = fileName.Substring(0, lastSeparator) + "." + + fileName.Substring(lastSeparator + 1); + } + + if (relativePath != null) + { + if (!fileName.StartsWith(relativePath)) + { + continue; + } + + fileName = fileName.Substring(relativePath.Length); + } + + if (!enumerationOptions.RecurseSubdirectories && + fileName.IndexOf(Path.DirectorySeparatorChar) >= 0) + { + continue; + } + + if (EnumerationOptionsHelper.MatchesPattern(enumerationOptions, + fileName, searchPattern)) + { + string filePath = fileSystem.Path.Combine(directoryPath, fileName); + fileSystem.InitializeFileFromEmbeddedResource(filePath, assembly, resourcePath); + } + } + } + + private static void InitializeFileFromEmbeddedResource(this IFileSystem fileSystem, + string path, + Assembly assembly, + string embeddedResourcePath) + { + using (Stream? embeddedResourceStream = assembly + .GetManifestResourceStream(embeddedResourcePath)) + { + if (embeddedResourceStream == null) + { + throw new ArgumentException( + $"Resource '{embeddedResourcePath}' not found in assembly '{assembly.FullName}'", + nameof(embeddedResourcePath)); + } + + using (BinaryReader streamReader = new(embeddedResourceStream)) + { + byte[] fileData = streamReader.ReadBytes((int)embeddedResourceStream.Length); + string? directoryPath = fileSystem.Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + fileSystem.Directory.CreateDirectory(directoryPath); + } + + fileSystem.File.WriteAllBytes(path, fileData); + } + } + } } diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystemInitializerExtensionsTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystemInitializerExtensionsTests.cs index a36f69855..0d4d06aae 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystemInitializerExtensionsTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystemInitializerExtensionsTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using Testably.Abstractions.Testing.FileSystemInitializer; using Testably.Abstractions.Testing.Tests.TestHelpers; @@ -149,6 +150,73 @@ public void Initialize_WithSubdirectory_ShouldExist(string directoryName) result.Directory.Exists.Should().BeTrue(); } + [Theory] + [AutoData] + public void + InitializeEmbeddedResourcesFromAssembly_ShouldCopyAllMatchingResourceFilesInDirectory( + string path) + { + MockFileSystem fileSystem = new(); + fileSystem.InitializeIn("foo"); + + fileSystem.InitializeEmbeddedResourcesFromAssembly( + path, + Assembly.GetExecutingAssembly(), + searchPattern: "*.txt"); + + string[] result = fileSystem.Directory.GetFiles(Path.Combine(path, "TestResources")); + string[] result2 = + fileSystem.Directory.GetFiles(Path.Combine(path, "TestResources", "SubResource")); + result.Length.Should().Be(2); + result.Should().Contain(x => x.EndsWith("TestFile1.txt")); + result.Should().Contain(x => x.EndsWith("TestFile2.txt")); + result2.Length.Should().Be(1); + result2.Should().Contain(x => x.EndsWith("SubResourceFile1.txt")); + } + + [Theory] + [AutoData] + public void + InitializeEmbeddedResourcesFromAssembly_WithoutRecurseSubdirectories_ShouldOnlyCopyTopmostFilesInRelativePath( + string path) + { + MockFileSystem fileSystem = new(); + fileSystem.InitializeIn("foo"); + + fileSystem.InitializeEmbeddedResourcesFromAssembly( + path, + Assembly.GetExecutingAssembly(), + "TestResources", + searchPattern: "*.txt", + SearchOption.TopDirectoryOnly); + + string[] result = fileSystem.Directory.GetFiles(path); + result.Length.Should().Be(2); + result.Should().Contain(x => x.EndsWith("TestFile1.txt")); + result.Should().Contain(x => x.EndsWith("TestFile2.txt")); + fileSystem.Directory.Exists(Path.Combine(path, "SubResource")).Should().BeFalse(); + } + + [Theory] + [AutoData] + public void + InitializeEmbeddedResourcesFromAssembly_WithRelativePath_ShouldCopyAllResourceInMatchingPathInDirectory( + string path) + { + MockFileSystem fileSystem = new(); + fileSystem.InitializeIn("foo"); + + fileSystem.InitializeEmbeddedResourcesFromAssembly( + path, + Assembly.GetExecutingAssembly(), + "TestResources/SubResource", + searchPattern: "*.txt"); + + string[] result = fileSystem.Directory.GetFiles(path); + result.Length.Should().Be(1); + result.Should().Contain(x => x.EndsWith("SubResourceFile1.txt")); + } + [SkippableTheory] [AutoData] public void InitializeIn_MissingDrive_ShouldCreateDrive(string directoryName) diff --git a/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.Designer.cs b/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.Designer.cs new file mode 100644 index 000000000..1f94c3efb --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.Designer.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Testably.Abstractions.Testing.Tests.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Testably.Abstractions.Testing.Tests.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to SubResourceFile1 content + ///. + /// + internal static string SubResourceFile1 { + get { + return ResourceManager.GetString("SubResourceFile1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TestFile1 content + ///. + /// + internal static string TestFile1 { + get { + return ResourceManager.GetString("TestFile1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TestFile2 content + ///. + /// + internal static string TestFile2 { + get { + return ResourceManager.GetString("TestFile2", resourceCulture); + } + } + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.resx b/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.resx new file mode 100644 index 000000000..06cb7107b --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/Properties/Resources.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\TestResources\SubResource\SubResourceFile1.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\TestResources\TestFile1.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\TestResources\TestFile2.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + diff --git a/Tests/Testably.Abstractions.Testing.Tests/TestResources/SubResource/SubResourceFile1.txt b/Tests/Testably.Abstractions.Testing.Tests/TestResources/SubResource/SubResourceFile1.txt new file mode 100644 index 000000000..dbf42fd79 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TestResources/SubResource/SubResourceFile1.txt @@ -0,0 +1 @@ +SubResourceFile1 content diff --git a/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile1.txt b/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile1.txt new file mode 100644 index 000000000..70a0af585 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile1.txt @@ -0,0 +1 @@ +TestFile1 content diff --git a/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile2.txt b/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile2.txt new file mode 100644 index 000000000..912884e35 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TestResources/TestFile2.txt @@ -0,0 +1 @@ +TestFile2 content diff --git a/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj b/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj index a130ed029..a978f1e65 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj +++ b/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj @@ -4,9 +4,36 @@ net6.0 + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + +