Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(dotnet test:*)"
]
}
}
188 changes: 179 additions & 9 deletions src/RipSharp.Tests/Core/RipOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,23 +441,23 @@ public void ParseArgs_WithSeason_ParsesSeasonCorrectly()
}

[Fact]
public void ParseArgs_WithInvalidSeason_KeepsDefault()
public void ParseArgs_WithInvalidSeason_RemainsNull()
{
var args = new[] { "--output", "/tmp/movies", "--season", "not-a-number" };

var result = RipOptions.ParseArgs(args);

result.Season.Should().Be(1);
result.Season.Should().BeNull();
}

[Fact]
public void ParseArgs_WithoutSeason_DefaultsTo1()
public void ParseArgs_WithoutSeason_DefaultsToNull()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.Season.Should().Be(1);
result.Season.Should().BeNull();
}

[Fact]
Expand All @@ -471,23 +471,23 @@ public void ParseArgs_WithEpisodeStart_ParsesCorrectly()
}

[Fact]
public void ParseArgs_WithInvalidEpisodeStart_KeepsDefault()
public void ParseArgs_WithInvalidEpisodeStart_RemainsNull()
{
var args = new[] { "--output", "/tmp/movies", "--episode-start", "invalid" };

var result = RipOptions.ParseArgs(args);

result.EpisodeStart.Should().Be(1);
result.EpisodeStart.Should().BeNull();
}

[Fact]
public void ParseArgs_WithoutEpisodeStart_DefaultsTo1()
public void ParseArgs_WithoutEpisodeStart_DefaultsToNull()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.EpisodeStart.Should().Be(1);
result.EpisodeStart.Should().BeNull();
}

[Fact]
Expand Down Expand Up @@ -632,7 +632,7 @@ public void ParseArgs_TvScenario_ParsesCorrectly()
result.Tv.Should().BeTrue();
result.Title.Should().Be("Breaking Bad");
result.Season.Should().Be(1);
result.EpisodeStart.Should().Be(1);
result.EpisodeStart.Should().BeNull();
}

[Fact]
Expand All @@ -654,4 +654,174 @@ public void ParseArgs_WithSequentialFlag_DisablesParallelProcessing()

result.EnableParallelProcessing.Should().BeFalse();
}

[Fact]
public void ParseArgs_WithPreviewFlag_SetsPreviewTrue()
{
var args = new[] { "--output", "/tmp/movies", "--preview" };

var result = RipOptions.ParseArgs(args);

result.Preview.Should().BeTrue();
}

[Fact]
public void ParseArgs_WithoutPreviewFlag_DefaultsToFalse()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.Preview.Should().BeFalse();
}

[Fact]
public void ParseArgs_WithErrorIgnore_SetsErrorModeIgnore()
{
var args = new[] { "--output", "/tmp/movies", "--error", "ignore" };

var result = RipOptions.ParseArgs(args);

result.ErrorMode.Should().Be(ErrorMode.Ignore);
}

[Fact]
public void ParseArgs_WithErrorPrompt_SetsErrorModePrompt()
{
var args = new[] { "--output", "/tmp/movies", "--error", "prompt" };

var result = RipOptions.ParseArgs(args);

result.ErrorMode.Should().Be(ErrorMode.Prompt);
}

[Fact]
public void ParseArgs_WithoutErrorFlag_DefaultsToPrompt()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.ErrorMode.Should().Be(ErrorMode.Prompt);
}

[Fact]
public void ParseArgs_WithInvalidErrorMode_ThrowsArgumentException()
{
var args = new[] { "--output", "/tmp/movies", "--error", "invalid" };

Action act = () => RipOptions.ParseArgs(args);

act.Should().Throw<ArgumentException>().WithMessage("--error must be 'prompt' or 'ignore'");
}

[Fact]
public void ParseArgs_WithConcurrency_SetsConcurrencyValue()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "4" };

var result = RipOptions.ParseArgs(args);

result.Concurrency.Should().Be(4);
}

[Fact]
public void ParseArgs_WithoutConcurrency_DefaultsTo1()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.Concurrency.Should().Be(1);
}

[Fact]
public void ParseArgs_WithConcurrencyTooHigh_ThrowsArgumentException()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "9" };

Action act = () => RipOptions.ParseArgs(args);

act.Should().Throw<ArgumentException>().WithMessage("--concurrency must be between 1 and 8");
}

[Fact]
public void ParseArgs_WithConcurrencyTooLow_ThrowsArgumentException()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "0" };

Action act = () => RipOptions.ParseArgs(args);

act.Should().Throw<ArgumentException>().WithMessage("--concurrency must be between 1 and 8");
}

[Fact]
public void ParseArgs_WithEpisodeStart_SetsValue()
{
var args = new[] { "--output", "/tmp/movies", "--episode-start", "5" };

var result = RipOptions.ParseArgs(args);

result.EpisodeStart.Should().Be(5);
}

[Fact]
public void ParseArgs_WithoutEpisodeStart_IsNull()
{
var args = new[] { "--output", "/tmp/movies" };

var result = RipOptions.ParseArgs(args);

result.EpisodeStart.Should().BeNull();
}

[Fact]
public void ParseArgs_WithConcurrencyMin_Succeeds()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "1" };

var result = RipOptions.ParseArgs(args);

result.Concurrency.Should().Be(1);
}

[Fact]
public void ParseArgs_WithConcurrencyMax_Succeeds()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "8" };

var result = RipOptions.ParseArgs(args);

result.Concurrency.Should().Be(8);
}

[Fact]
public void ParseArgs_WithConcurrencyNonNumeric_KeepsDefault()
{
var args = new[] { "--output", "/tmp/movies", "--concurrency", "abc" };

var result = RipOptions.ParseArgs(args);

result.Concurrency.Should().Be(1);
}

[Fact]
public void ParseArgs_WithErrorIgnoreUpperCase_IsCaseInsensitive()
{
var args = new[] { "--output", "/tmp/movies", "--error", "IGNORE" };

var result = RipOptions.ParseArgs(args);

result.ErrorMode.Should().Be(ErrorMode.Ignore);
}

[Fact]
public void ParseArgs_WithErrorMissingValue_ThrowsArgumentException()
{
var args = new[] { "--output", "/tmp/movies", "--error" };

Action act = () => RipOptions.ParseArgs(args);

act.Should().Throw<ArgumentException>().WithMessage("--error must be 'prompt' or 'ignore'");
}
}
61 changes: 61 additions & 0 deletions src/RipSharp.Tests/MakeMkv/MakeMkvServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace RipSharp.Tests.MakeMkv;

public class MakeMkvServiceTests
{
[Fact]
public async Task RipTitleAsync_FiltersProgressLines_FromCallbackAndErrorSummary()
{
var runner = Substitute.For<IProcessRunner>();
runner.RunAsync(
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<Action<string>?>(),
Arg.Any<Action<string>?>(),
Arg.Any<CancellationToken>())
.Returns(callInfo =>
{
var onError = callInfo.ArgAt<Action<string>?>(3);
onError?.Invoke("PRGV:100,200,300");
onError?.Invoke("PRGC:1,2,3");
onError?.Invoke("real error line");
return Task.FromResult(2);
});

var service = new MakeMkvService(runner);
var callbackLines = new List<string>();

var result = await service.RipTitleAsync("disc:0", 7, "/tmp/rips", onError: callbackLines.Add);

result.Success.Should().BeFalse();
result.ExitCode.Should().Be(2);
result.ErrorLines.Should().BeEquivalentTo(["real error line"]);
callbackLines.Should().BeEquivalentTo(["real error line"]);
}

[Fact]
public async Task RipTitleAsync_PassesExpectedCommandAndArguments_ToRunner()
{
var runner = Substitute.For<IProcessRunner>();
runner.RunAsync(
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<Action<string>?>(),
Arg.Any<Action<string>?>(),
Arg.Any<CancellationToken>())
.Returns(Task.FromResult(0));

var service = new MakeMkvService(runner);

var result = await service.RipTitleAsync("disc:1", 3, "/tmp/output");

await runner.Received(1).RunAsync(
"makemkvcon",
"-r --robot mkv disc:1 3 \"/tmp/output\"",
Arg.Any<Action<string>?>(),
Arg.Any<Action<string>?>(),
Arg.Any<CancellationToken>());

result.Success.Should().BeTrue();
result.Command.Should().Be("makemkvcon -r --robot mkv disc:1 3 \"/tmp/output\"");
}
}
57 changes: 57 additions & 0 deletions src/RipSharp.Tests/Models/ProcessResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace RipSharp.Tests.Models;

public class ProcessResultTests
{
[Fact]
public void ErrorSummary_WithErrorLines_IncludesExitCodeAndLastTenLines()
{
var lines = Enumerable.Range(1, 12).Select(i => $"Error {i}").ToList();
var result = new ProcessResult(false, 2, lines);

result.ErrorSummary.Should().Contain("exited with code 2");
result.ErrorSummary.Should().Contain("Error 3");
result.ErrorSummary.Should().Contain("Error 12");
result.ErrorSummary.Should().NotContain("Error 2" + Environment.NewLine);
}

[Fact]
public void ErrorSummary_WithoutErrorLines_UsesDefaultProcessName()
{
var result = new ProcessResult(false, 42, Array.Empty<string>());

result.ErrorSummary.Should().Contain("Process exited with code 42");
result.ErrorSummary.Should().Contain("No error details captured");
}

[Fact]
public void ErrorSummary_WithoutErrorLines_UsesFirstWordFromCommand()
{
var result = new ProcessResult(false, 5, Array.Empty<string>(), "ffmpeg -i input.mkv output.mp4");

result.ErrorSummary.Should().Contain("ffmpeg exited with code 5");
}

[Fact]
public void ErrorSummary_WithoutErrorLines_UsesQuotedFirstWordFromCommand()
{
var result = new ProcessResult(false, 7, Array.Empty<string>(), "\"/usr/local/bin/ffmpeg tool\" -i input.mkv output.mp4");

result.ErrorSummary.Should().Contain("/usr/local/bin/ffmpeg tool exited with code 7");
}

[Fact]
public void ErrorSummary_WithoutErrorLines_IgnoresLeadingWhitespace()
{
var result = new ProcessResult(false, 9, Array.Empty<string>(), " ffprobe -v error file.mkv");

result.ErrorSummary.Should().Contain("ffprobe exited with code 9");
}

[Fact]
public void ErrorSummary_IncludesLogPath_WhenPresent()
{
var result = new ProcessResult(false, 1, Array.Empty<string>(), "ffmpeg", "/tmp/ffmpeg.log");

result.ErrorSummary.Should().Contain("log: /tmp/ffmpeg.log");
}
}
28 changes: 28 additions & 0 deletions src/RipSharp.Tests/Models/TitlePlanTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace RipSharp.Tests.Models;

public class TitlePlanTests
{
[Fact]
public void TitlePlan_DurationSeconds_DefaultsToZero()
{
var plan = new TitlePlan(
TitleId: 1, Index: 0, EpisodeNum: null, EpisodeTitle: null,
TempOutputPath: "/tmp/out.mkv", FinalFileName: "Movie.mkv",
VersionSuffix: null, DisplayName: "Movie");

plan.DurationSeconds.Should().Be(0);
}

[Fact]
public void TitlePlan_WithDurationSeconds_StoresValue()
{
var plan = new TitlePlan(
TitleId: 1, Index: 0, EpisodeNum: 3, EpisodeTitle: "Pilot",
TempOutputPath: "/tmp/out.mkv", FinalFileName: "Show - S01E03.mkv",
VersionSuffix: null, DisplayName: "Show S01E03", DurationSeconds: 2700);

plan.DurationSeconds.Should().Be(2700);
plan.EpisodeNum.Should().Be(3);
plan.EpisodeTitle.Should().Be("Pilot");
}
}
Loading
Loading