aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-04 21:26:26 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-04 21:26:26 +0200
commit57c0fcd674c659c658369f0aebfd5d9d6787a9d4 (patch)
tree7aff23d6f54e913a6a34cb5a2568a07298582444 /tests
parent68ab58589444091925c15ad20d36f935b7bc2e21 (diff)
parentec04313317bed62728b059108cd232e9744f6354 (diff)
Merge remote-tracking branch 'upstream/master' into epg-fixes
Diffstat (limited to 'tests')
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs15
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs11
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs4
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs21
-rw-r--r--tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs110
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs392
8 files changed, 544 insertions, 13 deletions
diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
index 6171f12e47..5c187da413 100644
--- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
+++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
@@ -23,11 +24,6 @@ public class BaseItemTests
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
{
- var mediaSourceManager = new Mock<IMediaSourceManager>();
- mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
- .Returns((string x) => MediaProtocol.File);
- BaseItem.MediaSourceManager = mediaSourceManager.Object;
-
var video = new Video()
{
Path = primaryPath
@@ -38,7 +34,14 @@ public class BaseItemTests
Path = altPath,
};
- video.LocalAlternateVersions = [videoAlt.Path];
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
+ .Returns((string x) => MediaProtocol.File);
+ var libraryManager = new Mock<ILibraryManager>();
+ libraryManager.Setup(x => x.GetLocalAlternateVersionIds(It.IsAny<Video>()))
+ .Returns([Guid.Empty]);
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+ BaseItem.LibraryManager = libraryManager.Object;
Assert.Equal(name, video.GetMediaSourceName(video));
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
index fc969527e8..1406c8ee91 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
@@ -15,10 +15,17 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
}
[Fact]
- public void ComputeSegments_InvalidDuration_ThrowsArgumentException()
+ public void ComputeSegments_ZeroDurationOvershoot_ClampsToDuration()
{
var keyframeData = new KeyframeData(0, new[] { MsToTicks(10000) });
- Assert.Throws<ArgumentException>(() => DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
+ Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
+ }
+
+ [Fact]
+ public void ComputeSegments_MinorDurationOvershoot_ClampsToDuration()
+ {
+ var keyframeData = new KeyframeData(MsToTicks(9900), new[] { 0L, MsToTicks(5000), MsToTicks(10000) });
+ Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
}
[Theory]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
index 7bfab570b7..abdade8f6d 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -80,7 +80,9 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("[VCB-Studio] Re Zero kara Hajimeru Isekai Seikatsu [21][Ma10p_1080p][x265_flac].mkv", 21)]
[InlineData("[CASO&Sumisora][Oda_Nobuna_no_Yabou][04][BDRIP][1920x1080][x264_AAC][7620E503].mp4", 4)]
- // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
+ [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
+ [InlineData("Season 2/Hunter X Hunter - 101.mkv", 101)] // triple digit episode number without brackets
+ [InlineData("Season 1/Show Name - 1234 [720p].mkv", 1234)] // four digit episode number with quality tag
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
// TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
index 7f2188a3eb..9fef6c517a 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData(2, "The Simpsons/The Simpsons - 02.avi")]
[InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")]
[InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")]
- [InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
+ [InlineData(317, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")]
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")]
// TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")]
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
index ab825c9fa7..09bae2adab 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)]
[InlineData("Series/1-12 - The Woman.mp4", 1)]
[InlineData("Running Man/Running Man S2017E368.mkv", 2017)]
- [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
+ [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", null)]
// TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)]
public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
{
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
index 76922af8d5..a7491f42e9 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
@@ -45,8 +45,9 @@ public class FFProbeVideoInfoTests
[Theory]
[InlineData(null, 0)]
[InlineData(0L, 0)]
- [InlineData(1L, 0)]
- [InlineData(TimeSpan.TicksPerMinute * 5, 0)]
+ [InlineData(1L, 1)]
+ [InlineData(TimeSpan.TicksPerMinute * 3, 1)]
+ [InlineData(TimeSpan.TicksPerMinute * 5, 1)]
[InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)]
[InlineData(TimeSpan.TicksPerMinute * 50, 10)]
public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount)
@@ -58,4 +59,20 @@ public class FFProbeVideoInfoTests
Assert.Equal(chaptersCount, chapters.Length);
}
+
+ [Theory]
+ [InlineData(1L)]
+ [InlineData(TimeSpan.TicksPerMinute * 3)]
+ [InlineData(TimeSpan.TicksPerMinute * 5)]
+ [InlineData((TimeSpan.TicksPerMinute * 5) + 1)]
+ [InlineData((TimeSpan.TicksPerMinute * 50) + 1)]
+ public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime)
+ {
+ var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video()
+ {
+ RunTimeTicks = runtime
+ });
+
+ Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime));
+ }
}
diff --git a/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs
new file mode 100644
index 0000000000..8f5b1b3c48
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs
@@ -0,0 +1,110 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.TV;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.TV;
+
+public class EpisodeMetadataServiceTests
+{
+ private readonly TestEpisodeMetadataService _service = new();
+
+ [Fact]
+ public void MergeData_ProviderSeasonOverridesPathDerivedSeason()
+ {
+ var source = new MetadataResult<Episode>
+ {
+ Item = new Episode
+ {
+ ParentIndexNumber = 2
+ }
+ };
+
+ var target = new MetadataResult<Episode>
+ {
+ Item = new Episode
+ {
+ ParentIndexNumber = 1
+ }
+ };
+
+ _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true);
+
+ Assert.Equal(2, target.Item.ParentIndexNumber);
+ }
+
+ [Fact]
+ public void MergeData_BackfillExistingMetadata_DoesNotOverrideProviderSeason()
+ {
+ var existingMetadata = new MetadataResult<Episode>
+ {
+ Item = new Episode
+ {
+ ParentIndexNumber = 1
+ }
+ };
+
+ var temp = new MetadataResult<Episode>
+ {
+ Item = new Episode
+ {
+ ParentIndexNumber = 2
+ }
+ };
+
+ _service.Merge(existingMetadata, temp, replaceData: false, mergeMetadataSettings: false);
+
+ Assert.Equal(2, temp.Item.ParentIndexNumber);
+ }
+
+ [Fact]
+ public void MergeData_MissingProviderSeasonKeepsExistingSeason()
+ {
+ var source = new MetadataResult<Episode>
+ {
+ Item = new Episode()
+ };
+
+ var target = new MetadataResult<Episode>
+ {
+ Item = new Episode
+ {
+ ParentIndexNumber = 1
+ }
+ };
+
+ _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true);
+
+ Assert.Equal(1, target.Item.ParentIndexNumber);
+ }
+
+ private sealed class TestEpisodeMetadataService : EpisodeMetadataService
+ {
+ public TestEpisodeMetadataService()
+ : base(
+ Mock.Of<IServerConfigurationManager>(),
+ NullLogger<EpisodeMetadataService>.Instance,
+ Mock.Of<IProviderManager>(),
+ Mock.Of<IFileSystem>(),
+ Mock.Of<ILibraryManager>(),
+ Mock.Of<IExternalDataManager>(),
+ Mock.Of<IItemRepository>())
+ {
+ }
+
+ public void Merge(MetadataResult<Episode> source, MetadataResult<Episode> target, bool replaceData, bool mergeMetadataSettings)
+ {
+ MergeData(source, target, Array.Empty<MetadataField>(), replaceData, mergeMetadataSettings);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
index a7bbef7ed4..03c0b4af39 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
@@ -1,4 +1,9 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
+using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
@@ -78,4 +83,391 @@ public class DotIgnoreIgnoreRuleTest
// Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes
Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false));
}
+
+ [Fact]
+ public void CacheHit_RepeatedCallsDoNotRereadFiles()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var subDir = Path.Combine(tempDir, "subdir");
+ Directory.CreateDirectory(subDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(subDir, "test.tmp"),
+ IsDirectory = false
+ };
+
+ // First call - should cache
+ var result1 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result1);
+
+ // Second call - should use cache
+ var result2 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result2);
+
+ // Third call with different file in same directory - should use cache
+ var fileInfo2 = new FileSystemMetadata
+ {
+ FullName = Path.Combine(subDir, "other.tmp"),
+ IsDirectory = false
+ };
+ var result3 = rule.ShouldIgnore(fileInfo2, null);
+ Assert.True(result3);
+
+ // Call with file that doesn't match pattern
+ var fileInfo3 = new FileSystemMetadata
+ {
+ FullName = Path.Combine(subDir, "other.txt"),
+ IsDirectory = false
+ };
+ var result4 = rule.ShouldIgnore(fileInfo3, null);
+ Assert.False(result4);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void CacheInvalidation_ModifyIgnoreFile_Reparses()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "test.tmp"),
+ IsDirectory = false
+ };
+
+ // First call - should ignore .tmp files
+ var result1 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result1);
+
+ // Modify the .ignore file to ignore .txt instead
+ // Wait a bit to ensure the file modification time changes
+ Thread.Sleep(50);
+ File.WriteAllText(ignoreFilePath, "*.txt");
+
+ // Now .tmp files should NOT be ignored
+ var result2 = rule.ShouldIgnore(fileInfo, null);
+ Assert.False(result2);
+
+ // And .txt files SHOULD be ignored
+ var txtFileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "test.txt"),
+ IsDirectory = false
+ };
+ var result3 = rule.ShouldIgnore(txtFileInfo, null);
+ Assert.True(result3);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void EmptyIgnoreFile_IgnoresEverything()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, string.Empty);
+
+ var rule = new DotIgnoreIgnoreRule();
+
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "anyfile.mkv"),
+ IsDirectory = false
+ };
+
+ // Empty .ignore file should ignore everything
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void WhitespaceOnlyIgnoreFile_IgnoresEverything()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, " \n\t\n ");
+
+ var rule = new DotIgnoreIgnoreRule();
+
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "anyfile.mkv"),
+ IsDirectory = false
+ };
+
+ // Whitespace-only .ignore file should ignore everything
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void NoIgnoreFile_DoesNotIgnore()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var rule = new DotIgnoreIgnoreRule();
+
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "anyfile.mkv"),
+ IsDirectory = false
+ };
+
+ // No .ignore file means don't ignore
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.False(result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void ConcurrentAccess_ThreadSafe()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+
+ // Run multiple parallel checks
+ Parallel.For(0, 100, i =>
+ {
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, $"test{i}.tmp"),
+ IsDirectory = false
+ };
+
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result);
+ });
+
+ // Also test with non-matching files
+ Parallel.For(0, 100, i =>
+ {
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, $"test{i}.txt"),
+ IsDirectory = false
+ };
+
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.False(result);
+ });
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void ClearCache_ClearsAllCachedData()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "test.tmp"),
+ IsDirectory = false
+ };
+
+ // First call to populate cache
+ var result1 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result1);
+
+ // Clear cache
+ rule.ClearDirectoryCache();
+
+ // Should still work (will re-populate cache)
+ var result2 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result2);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void IgnoreFileDeleted_HandlesGracefully()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "test.tmp"),
+ IsDirectory = false
+ };
+
+ // First call - should ignore
+ var result1 = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result1);
+
+ // Delete the .ignore file
+ File.Delete(ignoreFilePath);
+
+ // Should not ignore anymore (file deleted)
+ var result2 = rule.ShouldIgnore(fileInfo, null);
+ Assert.False(result2);
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public void ParentDirectoryIgnoreFile_AppliesToSubdirectories()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var subDir1 = Path.Combine(tempDir, "sub1");
+ var subDir2 = Path.Combine(tempDir, "sub1", "sub2");
+ Directory.CreateDirectory(subDir1);
+ Directory.CreateDirectory(subDir2);
+
+ try
+ {
+ // Put .ignore in root
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "*.tmp");
+
+ var rule = new DotIgnoreIgnoreRule();
+
+ // Check file in sub2 - should find .ignore in parent
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(subDir2, "test.tmp"),
+ IsDirectory = false
+ };
+
+ var result = rule.ShouldIgnore(fileInfo, null);
+ Assert.True(result);
+
+ // Check file in sub1
+ var fileInfo2 = new FileSystemMetadata
+ {
+ FullName = Path.Combine(subDir1, "test.tmp"),
+ IsDirectory = false
+ };
+
+ var result2 = rule.ShouldIgnore(fileInfo2, null);
+ Assert.True(result2);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void DirectoryMatching_TrailingSlashPattern()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var subDir = Path.Combine(tempDir, "videos");
+ Directory.CreateDirectory(subDir);
+
+ try
+ {
+ var ignoreFilePath = Path.Combine(tempDir, ".ignore");
+ File.WriteAllText(ignoreFilePath, "videos/");
+
+ var rule = new DotIgnoreIgnoreRule();
+
+ // Directory should be ignored
+ var dirInfo = new FileSystemMetadata
+ {
+ FullName = subDir,
+ IsDirectory = true
+ };
+
+ var result = rule.ShouldIgnore(dirInfo, null);
+ Assert.True(result);
+
+ // File named "videos" should NOT be ignored (pattern has trailing slash)
+ var fileInfo = new FileSystemMetadata
+ {
+ FullName = Path.Combine(tempDir, "videos"),
+ IsDirectory = false
+ };
+
+ // Note: The Ignore library behavior may vary here, this tests the actual behavior
+ var resultFile = rule.ShouldIgnore(fileInfo, null);
+ // The file named "videos" without trailing slash might or might not match depending on the library
+ // This test documents the actual behavior
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
}