diff --git a/AmiReel.Tests/Services/ShortsPipelineTests.cs b/AmiReel.Tests/Services/ShortsPipelineTests.cs new file mode 100644 index 0000000..941f950 --- /dev/null +++ b/AmiReel.Tests/Services/ShortsPipelineTests.cs @@ -0,0 +1,223 @@ +using AmiReel.Models; +using AmiReel.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AmiReel.Tests.Services; + +[TestClass] +public class ShortsPipelineTests +{ + private readonly List _tempDirectories = []; + + [TestCleanup] + public void Cleanup() + { + foreach (string directory in _tempDirectories) + { + try { Directory.Delete(directory, true); } catch { } + } + } + + private string CreateTempDirectory() + { + string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + _tempDirectories.Add(directory); + return directory; + } + + private static ShortSettings ValidSettings(string input, string font, ShortStyle style = ShortStyle.Crop, string background = "") => new() + { + InputFile = input, + OutputPath = Path.Combine(Path.GetDirectoryName(input) ?? "", "out.mp4"), + Title = "Test Short", + Hook = "THIS RAN ON AN AMIGA", + Website = "AMIGADB.NET", + Start = "00:00:00", + DurationSeconds = 30, + Style = style, + BackgroundImage = background, + FontFile = font, + FfmpegPath = "", + FfprobePath = "", + }; + + [TestMethod] + public void EscapeFfmpegPath_WithBackslashesColonsAndQuotes_EscapesAllThree() + { + // Act + string result = ShortsPipeline.EscapeFfmpegPath(@"C:\Fonts\font's.ttf"); + + // Assert + Assert.AreEqual(@"C\:\\Fonts\\font\'s.ttf", result); + } + + [TestMethod] + public void EscapeFfmpegPath_EscapesBackslashesBeforeIntroducingNewOnes() + { + // Act: if colon/quote escaping ran before backslash escaping, the backslashes + // they introduce would themselves get doubled, which must not happen. + string result = ShortsPipeline.EscapeFfmpegPath("C:\\a"); + + // Assert + Assert.AreEqual("C\\:\\\\a", result); + } + + [TestMethod] + public void BuildMeta_WithAllFieldsPopulated_JoinsWithMiddleDot() + { + // Arrange + ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "1991", Type = "Cracktro" }; + + // Act + string meta = ShortsPipeline.BuildMeta(settings); + + // Assert + Assert.AreEqual("Pirates · 1991 · Cracktro", meta); + } + + [TestMethod] + public void BuildMeta_WithSomeFieldsBlank_SkipsBlankFields() + { + // Arrange + ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "", Type = " " }; + + // Act + string meta = ShortsPipeline.BuildMeta(settings); + + // Assert + Assert.AreEqual("Pirates", meta); + } + + [TestMethod] + public void BuildMeta_WithAllFieldsBlank_ReturnsEmptyString() + { + // Arrange + ShortSettings settings = ValidSettings("in.mp4", "font.ttf"); + + // Act + string meta = ShortsPipeline.BuildMeta(settings); + + // Assert + Assert.AreEqual("", meta); + } + + [TestMethod] + public void Validate_WithMissingInputFile_ThrowsFileNotFoundException() + { + // Arrange + string directory = CreateTempDirectory(); + ShortSettings settings = ValidSettings(Path.Combine(directory, "missing.mp4"), "font.ttf"); + + // Act & Assert + Assert.ThrowsExactly(() => ShortsPipeline.Validate(settings)); + } + + [TestMethod] + public void Validate_WithBlankTitle_ThrowsArgumentException() + { + // Arrange + string directory = CreateTempDirectory(); + string input = Path.Combine(directory, "clip.mp4"); + File.WriteAllText(input, "data"); + string font = Path.Combine(directory, "font.ttf"); + File.WriteAllText(font, "data"); + ShortSettings settings = ValidSettings(input, font) with { Title = "" }; + + // Act & Assert + Assert.ThrowsExactly(() => ShortsPipeline.Validate(settings)); + } + + [TestMethod] + public void Validate_WithZeroDuration_ThrowsArgumentException() + { + // Arrange + string directory = CreateTempDirectory(); + string input = Path.Combine(directory, "clip.mp4"); + File.WriteAllText(input, "data"); + string font = Path.Combine(directory, "font.ttf"); + File.WriteAllText(font, "data"); + ShortSettings settings = ValidSettings(input, font) with { DurationSeconds = 0 }; + + // Act & Assert + Assert.ThrowsExactly(() => ShortsPipeline.Validate(settings)); + } + + [TestMethod] + public void Validate_WithMissingFontFile_ThrowsFileNotFoundException() + { + // Arrange + string directory = CreateTempDirectory(); + string input = Path.Combine(directory, "clip.mp4"); + File.WriteAllText(input, "data"); + ShortSettings settings = ValidSettings(input, Path.Combine(directory, "missing-font.ttf")); + + // Act & Assert + Assert.ThrowsExactly(() => ShortsPipeline.Validate(settings)); + } + + [TestMethod] + public void Validate_BrandStyleWithoutBackgroundImage_ThrowsFileNotFoundException() + { + // Arrange + string directory = CreateTempDirectory(); + string input = Path.Combine(directory, "clip.mp4"); + File.WriteAllText(input, "data"); + string font = Path.Combine(directory, "font.ttf"); + File.WriteAllText(font, "data"); + ShortSettings settings = ValidSettings(input, font, ShortStyle.Brand); + + // Act & Assert + Assert.ThrowsExactly(() => ShortsPipeline.Validate(settings)); + } + + [TestMethod] + public void Validate_NonBrandStyleWithoutBackgroundImage_DoesNotThrow() + { + // Arrange + string directory = CreateTempDirectory(); + string input = Path.Combine(directory, "clip.mp4"); + File.WriteAllText(input, "data"); + string font = Path.Combine(directory, "font.ttf"); + File.WriteAllText(font, "data"); + ShortSettings settings = ValidSettings(input, font, ShortStyle.Crop); + + // Act & Assert (no exception) + ShortsPipeline.Validate(settings); + } + + [TestMethod] + public void GetBaseFilter_ForEveryStyle_ReturnsAFilterEndingInBaseTag() + { + foreach (ShortStyle style in Enum.GetValues()) + { + // Act + string filter = ShortsPipeline.GetBaseFilter(style); + + // Assert + Assert.IsFalse(string.IsNullOrWhiteSpace(filter), $"{style} produced an empty filter."); + StringAssert.Contains(filter, "[base]", $"{style} filter does not label an output as [base]."); + } + } + + [TestMethod] + public void BuildFilterComplex_IncludesAllFourTextOverlaysAndTheirTimingGates() + { + // Act + string result = ShortsPipeline.BuildFilterComplex( + "[0:v]copy[base]", + fontEsc: "C\\:\\\\font.ttf", + hookEsc: "hook.txt", titleEsc: "title.txt", metaEsc: "meta.txt", websiteEsc: "website.txt", + hookEnd: 2.7, infoStart: 23, ctaStart: 26.5); + + // Assert + StringAssert.Contains(result, "textfile='hook.txt'"); + StringAssert.Contains(result, "textfile='title.txt'"); + StringAssert.Contains(result, "textfile='meta.txt'"); + StringAssert.Contains(result, "textfile='website.txt'"); + StringAssert.Contains(result, "enable='between(t,0,2.7)'"); + StringAssert.Contains(result, "enable='gte(t,23)'"); + StringAssert.Contains(result, "enable='gte(t,26.5)'"); + StringAssert.Contains(result, "[vout]"); + } +} diff --git a/MainPage.xaml b/MainPage.xaml index b477e05..bf598f9 100644 --- a/MainPage.xaml +++ b/MainPage.xaml @@ -9,13 +9,19 @@ + - + +