Kavita/API.Tests/Parser/DefaultParserTests.cs
Joe Milazzo 5d1dd7b3f0
.NET 7 + Spring Cleaning (#1677)
* Updated to net7.0

* Updated GA to .net 7

* Updated System.IO.Abstractions to use New factory.

* Converted Regex into SourceGenerator in Parser.

* Updated more regex to source generators.

* Enabled Nullability and more regex changes throughout codebase.

* Parser is 100% GeneratedRegexified

* Lots of nullability code

* Enabled nullability for all repositories.

* Fixed another unit test

* Refactored some code around and took care of some todos.

* Updating code for nullability and cleaning up methods that aren't used anymore. Refctored all uses of Parser.Normalize() to use new extension

* More nullability exercises. 500 warnings to go.

* Fixed a bug where custom file uploads for entities wouldn't save in webP.

* Nullability is done for all DTOs

* Fixed all unit tests and nullability for the project. Only OPDS is left which will be done with an upcoming OPDS enhancement.

* Use localization in book service after validating

* Code smells

* Switched to preview build of swashbuckle for .net7 support

* Fixed up merge issues

* Disable emulate comic book when on single page reader

* Fixed a regression where double page renderer wouldn't layout the images correctly

* Updated to swashbuckle which support .net 7

* Fixed a bad GA action

* Some code cleanup

* More code smells

* Took care of most of nullable issues

* Fixed a broken test due to having more than one test run in parallel

* I'm really not sure why the unit tests are failing or are so extremely slow on .net 7

* Updated all dependencies

* Fixed up build and removed hardcoded framework from build scripts. (this merge removes Regex Source generators). Unit tests are completely busted.

* Unit tests and code cleanup. Needs shakeout now.

* Adjusted Series model since a few fields are not-nullable. Removed dead imports on the project.

* Refactored to use Builder pattern for all unit tests.

* Switched nullability down to warnings. It wasn't possible to switch due to constraint issues in DB Migration.
2023-03-05 12:55:13 -08:00

443 lines
21 KiB
C#

using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Parser;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Parser;
public class DefaultParserTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly DefaultParser _defaultParser;
public DefaultParserTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_defaultParser = new DefaultParser(directoryService);
}
#region ParseFromFallbackFolders
[Theory]
[InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")]
[InlineData("C:/", "C:/Love Hina/Specials/Ani-Hina Art Collection.cbz", "Love Hina")]
[InlineData("C:/", "C:/Mujaki no Rakuen Something/Mujaki no Rakuen Vol12 ch76.cbz", "Mujaki no Rakuen")]
[InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")]
public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries)
{
var actual = _defaultParser.Parse(inputPath, rootDir);
if (actual == null)
{
Assert.NotNull(actual);
return;
}
Assert.Equal(expectedSeries, actual.Series);
}
[Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!~1~2")]
[InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster~0~1")]
[InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", "Hajime no Ippo~0~0")]
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string expectedParseInfo)
{
const string rootDirectory = "/manga/";
var tokens = expectedParseInfo.Split("~");
var actual = new ParserInfo {Series = "", Chapters = "0", Volumes = "0"};
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(tokens[0], actual.Series);
Assert.Equal(tokens[1], actual.Volumes);
Assert.Equal(tokens[2], actual.Chapters);
}
[Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")]
[InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")]
[InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")]
[InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")]
[InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")]
[InlineData("/manga/Btooom!/Specials/Just a special SP01.cbz", "Btooom!")]
public void ParseFromFallbackFolders_ShouldUseExistingSeriesName(string inputFile, string expectedParseInfo)
{
const string rootDirectory = "/manga/";
var fs = new MockFileSystem();
fs.AddDirectory(rootDirectory);
fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new DefaultParser(ds);
var actual = parser.Parse(inputFile, rootDirectory);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series);
}
[Theory]
[InlineData("/manga/Btooom!/Specials/Art Book.cbz", "Btooom!")]
[InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", "Hajime no Ippo")]
public void ParseFromFallbackFolders_ShouldUseExistingSeriesName_NewScanLoop(string inputFile, string expectedParseInfo)
{
const string rootDirectory = "/manga/";
var fs = new MockFileSystem();
fs.AddDirectory(rootDirectory);
fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new DefaultParser(ds);
var actual = parser.Parse(inputFile, rootDirectory);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series);
}
#endregion
#region Parse
[Fact]
public void Parse_ParseInfo_Manga()
{
const string rootPath = @"E:/Manga/";
var expected = new Dictionary<string, ParserInfo>();
var filepath = @"E:/Manga/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Mujaki no Rakuen", Volumes = "12",
Chapters = "76", Filename = "Mujaki no Rakuen Vol12 ch76.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:/Manga/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
Chapters = "0", Filename = "Vol 1.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Beelzebub\Beelzebub_01_[Noodles].zip";
expected.Add(filepath, new ParserInfo
{
Series = "Beelzebub", Volumes = "0",
Chapters = "1", Filename = "Beelzebub_01_[Noodles].zip", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Ichinensei ni Nacchattara\Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip";
expected.Add(filepath, new ParserInfo
{
Series = "Ichinensei ni Nacchattara", Volumes = "1",
Chapters = "1", Filename = "Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "",
Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)\Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Akame ga KILL! ZERO", Volumes = "1", Edition = "",
Chapters = "0", Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Dorohedoro\Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Dorohedoro", Volumes = "1", Edition = "",
Chapters = "0", Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\APOSIMZ\APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "APOSIMZ", Volumes = "0", Edition = "",
Chapters = "40", Filename = "APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Corpse Party Musume\Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Kedouin Makoto - Corpse Party Musume", Volumes = "0", Edition = "",
Chapters = "9", Filename = "Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Goblin Slayer\Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Goblin Slayer - Brand New Day", Volumes = "0", Edition = "",
Chapters = "6.5", Filename = "Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
filepath = @"E:\Manga\Summer Time Rendering\Specials\Record 014 (between chapter 083 and ch084) SP11.cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Summer Time Rendering", Volumes = "0", Edition = "",
Chapters = "0", Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = true
});
filepath = @"E:\Manga\Seraph of the End\Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Seraph of the End - Vampire Reign", Volumes = "0", Edition = "",
Chapters = "93", Filename = "Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Kono Subarashii Sekai ni Bakuen wo!\Vol. 00 Ch. 000.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Kono Subarashii Sekai ni Bakuen wo!", Volumes = "0", Edition = "",
Chapters = "0", Filename = "Vol. 00 Ch. 000.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Toukyou Akazukin\Vol. 01 Ch. 001.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Toukyou Akazukin", Volumes = "1", Edition = "",
Chapters = "1", Filename = "Vol. 01 Ch. 001.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
// If an image is cover exclusively, ignore it
filepath = @"E:\Manga\Seraph of the End\cover.png";
expected.Add(filepath, null);
filepath = @"E:\Manga\The Beginning After the End\Chapter 001.cbz";
expected.Add(filepath, new ParserInfo
{
Series = "The Beginning After the End", Volumes = "0", Edition = "",
Chapters = "1", Filename = "Chapter 001.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
// Note: Fallback to folder will parse Monster #8 and get Monster
filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg";
expected.Add(filepath, new ParserInfo
{
Series = "Monster", Volumes = "0", Edition = "",
Chapters = "1", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Air Gear\Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz";
expected.Add(filepath, new ParserInfo
{
Series = "Air Gear", Volumes = "1", Edition = "Omnibus",
Chapters = "0", Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch186\Vol. 19 p106.gif";
expected.Add(filepath, new ParserInfo
{
Series = "Just Images the second", Volumes = "19", Edition = "",
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch186\Vol. 19 p106.gif";
expected.Add(filepath, new ParserInfo
{
Series = "Just Images the second", Volumes = "19", Edition = "",
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Manga\Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub";
expected.Add(filepath, new ParserInfo
{
Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
Chapters = "0", Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
FullFilePath = filepath, IsSpecial = false
});
foreach (var file in expected.Keys)
{
var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath);
if (expectedInfo == null)
{
Assert.Null(actual);
continue;
}
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {file}");
Assert.Equal(expectedInfo.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
}
}
[Fact]
public void Parse_ParseInfo_Manga_WithSpecialsFolder()
{
const string rootPath = @"E:/Manga/";
var filesystem = new MockFileSystem();
filesystem.AddDirectory("E:/Manga");
filesystem.AddDirectory("E:/Foo 50");
filesystem.AddDirectory("E:/Foo 50/Specials");
filesystem.AddFile(@"E:/Manga/Foo 50/Foo 50 v1.cbz", new MockFileData(""));
filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var parser = new DefaultParser(ds);
var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz";
// There is a bad parse for series like "Foo 50", so we have parsed chapter as 50
var expected = new ParserInfo
{
Series = "Foo 50", Volumes = "1",
Chapters = "50", Filename = "Foo 50 v1.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};
var actual = parser.Parse(filepath, rootPath);
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expected.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expected.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expected.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expected.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expected.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
_testOutputHelper.WriteLine("IsSpecial ✓");
filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
expected = new ParserInfo
{
Series = "Foo 50", Volumes = "0", IsSpecial = true,
Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};
actual = parser.Parse(filepath, rootPath);
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expected.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expected.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expected.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expected.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expected.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expected.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
Assert.Equal(expected.IsSpecial, actual.IsSpecial);
_testOutputHelper.WriteLine("IsSpecial ✓");
}
[Fact]
public void Parse_ParseInfo_Comic()
{
const string rootPath = @"E:/Comics/";
var expected = new Dictionary<string, ParserInfo>();
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Teen Titans", Volumes = "0",
Chapters = "0", Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath
});
// Fallback test with bad naming
filepath = @"E:\Comics\Comics\Babe\Babe Vol.1 #1-4\Babe 01.cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Babe", Volumes = "0", Edition = "",
Chapters = "1", Filename = "Babe 01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Comics\Comics\Publisher\Batman the Detective (2021)\Batman the Detective - v6 - 11 - (2021).cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Batman the Detective", Volumes = "6", Edition = "",
Chapters = "11", Filename = "Batman the Detective - v6 - 11 - (2021).cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
filepath = @"E:\Comics\Comics\Batman - The Man Who Laughs #1 (2005)\Batman - The Man Who Laughs #1 (2005).cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Batman - The Man Who Laughs", Volumes = "0", Edition = "",
Chapters = "1", Filename = "Batman - The Man Who Laughs #1 (2005).cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false
});
foreach (var file in expected.Keys)
{
var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath, LibraryType.Comic);
if (expectedInfo == null)
{
Assert.Null(actual);
continue;
}
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {file}");
Assert.Equal(expectedInfo.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
}
}
#endregion
}