mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update namefixing
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use PDO;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FixReleaseNameQueryIndexesMigrationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var array<string, string|false>
|
||||
*/
|
||||
private array $originalEnvironment = [];
|
||||
|
||||
private string $databasePath;
|
||||
|
||||
public function createApplication()
|
||||
{
|
||||
$this->databasePath = sys_get_temp_dir().'/nntmux-name-fixing-indexes-test.sqlite';
|
||||
$this->originalEnvironment = [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
'DB_CONNECTION' => getenv('DB_CONNECTION'),
|
||||
'DB_DATABASE' => getenv('DB_DATABASE'),
|
||||
];
|
||||
|
||||
if (file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
$pdo = new PDO('sqlite:'.$this->databasePath);
|
||||
$pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
|
||||
$pdo->exec("INSERT INTO settings (name, value) VALUES ('categorizeforeign', '0'), ('catwebdl', '1')");
|
||||
|
||||
$this->setEnvironmentValue('APP_ENV', 'testing');
|
||||
$this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
|
||||
$this->setEnvironmentValue('DB_DATABASE', $this->databasePath);
|
||||
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.default' => 'sqlite',
|
||||
'database.connections.sqlite.database' => $this->databasePath,
|
||||
]);
|
||||
|
||||
$this->createSchema();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->databasePath !== '' && file_exists($this->databasePath)) {
|
||||
unlink($this->databasePath);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
|
||||
foreach ($this->originalEnvironment as $key => $value) {
|
||||
$this->setEnvironmentValue($key, $value === false ? null : $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_it_adds_only_missing_reverse_lookup_and_queue_indexes_idempotently(): void
|
||||
{
|
||||
$migration = require database_path('migrations/2026_08_04_082439_add_fix_release_name_query_indexes.php');
|
||||
|
||||
$migration->up();
|
||||
$migration->up();
|
||||
|
||||
$this->assertTrue(Schema::hasIndex('par_hashes', 'ix_par_hashes_hash_releases_id'));
|
||||
$this->assertTrue(Schema::hasIndex('release_files', 'ix_release_files_crc32_releases_id'));
|
||||
$this->assertTrue(Schema::hasIndex('predb', 'ix_predb_searched_predate_id'));
|
||||
|
||||
$migration->down();
|
||||
|
||||
$this->assertFalse(Schema::hasIndex('par_hashes', 'ix_par_hashes_hash_releases_id'));
|
||||
$this->assertFalse(Schema::hasIndex('release_files', 'ix_release_files_crc32_releases_id'));
|
||||
$this->assertFalse(Schema::hasIndex('predb', 'ix_predb_searched_predate_id'));
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
Schema::create('par_hashes', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('releases_id');
|
||||
$table->string('hash', 32);
|
||||
$table->primary(['releases_id', 'hash']);
|
||||
});
|
||||
Schema::create('release_files', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('releases_id');
|
||||
$table->string('name');
|
||||
$table->string('crc32')->default('');
|
||||
$table->primary(['releases_id', 'name']);
|
||||
});
|
||||
Schema::create('predb', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->tinyInteger('searched')->default(0)->index();
|
||||
$table->dateTime('predate')->nullable()->index();
|
||||
});
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
putenv($key);
|
||||
unset($_ENV[$key], $_SERVER[$key]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Facades\Search;
|
||||
use App\Services\NameFixing\NameFixingQueryService;
|
||||
use App\Services\NameFixing\NameFixingService;
|
||||
use App\Services\NameFixing\ReleaseUpdateService;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PredbFulltextNameFixingTest extends TestCase
|
||||
{
|
||||
public function test_it_attaches_predb_id_when_the_confirmed_title_is_already_correct(): void
|
||||
{
|
||||
$title = 'Valid.Scene.Release.2026-GROUP';
|
||||
Search::shouldReceive('isAvailable')->once()->andReturnTrue();
|
||||
Search::shouldReceive('searchReleases')
|
||||
->once()
|
||||
->with(['name' => $title, 'searchname' => $title], 21)
|
||||
->andReturn([42]);
|
||||
|
||||
$database = $this->createMock(ConnectionInterface::class);
|
||||
$database->expects($this->once())
|
||||
->method('select')
|
||||
->willReturn([
|
||||
(object) [
|
||||
'releases_id' => 42,
|
||||
'name' => $title,
|
||||
'searchname' => $title,
|
||||
'fromname' => 'poster',
|
||||
'groups_id' => 1,
|
||||
'categories_id' => 7010,
|
||||
],
|
||||
]);
|
||||
|
||||
$updates = $this->createMock(ReleaseUpdateService::class);
|
||||
$updates->expects($this->once())->method('attachPredbId')->with(42, 7);
|
||||
$service = new NameFixingService(
|
||||
updateService: $updates,
|
||||
queries: new NameFixingQueryService($database)
|
||||
);
|
||||
|
||||
$matched = $service->matchPredbFulltext((object) [
|
||||
'predb_id' => 7,
|
||||
'title' => $title,
|
||||
'source' => 'srrdb',
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $matched);
|
||||
}
|
||||
|
||||
public function test_it_marks_sixteen_confirmed_candidates_as_a_flood(): void
|
||||
{
|
||||
$title = 'Valid.Scene.Release.2026-GROUP';
|
||||
Search::shouldReceive('isAvailable')->once()->andReturnTrue();
|
||||
Search::shouldReceive('searchReleases')->once()->andReturn(range(1, 21));
|
||||
|
||||
$database = $this->createStub(ConnectionInterface::class);
|
||||
$database->method('select')->willReturn(array_map(
|
||||
static fn (int $id): object => (object) [
|
||||
'releases_id' => $id,
|
||||
'name' => $title,
|
||||
'searchname' => 'Old.Name-'.$id,
|
||||
'fromname' => 'poster',
|
||||
'groups_id' => 1,
|
||||
'categories_id' => 7010,
|
||||
],
|
||||
range(1, 16)
|
||||
));
|
||||
|
||||
$service = new NameFixingService(
|
||||
updateService: $this->createStub(ReleaseUpdateService::class),
|
||||
queries: new NameFixingQueryService($database)
|
||||
);
|
||||
|
||||
$matched = $service->matchPredbFulltext((object) [
|
||||
'predb_id' => 7,
|
||||
'title' => $title,
|
||||
'source' => 'srrdb',
|
||||
]);
|
||||
|
||||
$this->assertSame(-1, $matched);
|
||||
}
|
||||
|
||||
public function test_backend_unavailability_is_not_recorded_as_a_no_match(): void
|
||||
{
|
||||
Search::shouldReceive('isAvailable')->once()->andReturnFalse();
|
||||
Search::shouldReceive('searchReleases')->never();
|
||||
$database = $this->createMock(ConnectionInterface::class);
|
||||
$database->expects($this->never())->method('select');
|
||||
$service = new NameFixingService(
|
||||
updateService: $this->createStub(ReleaseUpdateService::class),
|
||||
queries: new NameFixingQueryService($database)
|
||||
);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$service->matchPredbFulltext((object) [
|
||||
'predb_id' => 7,
|
||||
'title' => 'Valid.Scene.Release.2026-GROUP',
|
||||
'source' => 'srrdb',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -222,6 +222,29 @@ class ReleaseNameFixedRecategorizationTest extends TestCase
|
||||
$this->assertSame(1, (int) $release->isrenamed);
|
||||
}
|
||||
|
||||
public function test_internal_processing_status_updates_do_not_refresh_the_search_index(): void
|
||||
{
|
||||
Search::shouldReceive('updateRelease')->once();
|
||||
|
||||
$group = UsenetGroup::query()->create([
|
||||
'name' => 'alt.binaries.test',
|
||||
'active' => 1,
|
||||
'backfill' => 0,
|
||||
]);
|
||||
$release = Release::factory()->create([
|
||||
'groups_id' => $group->id,
|
||||
'guid' => str_repeat('d', 40),
|
||||
'leftguid' => 'd',
|
||||
'proc_nfo' => 0,
|
||||
]);
|
||||
|
||||
app(ReleaseUpdateService::class)->updateSingleColumn('proc_nfo', 1, $release->id);
|
||||
app(ReleaseUpdateService::class)->attachPredbId($release->id, 123);
|
||||
|
||||
$this->assertSame(1, (int) $release->fresh()->proc_nfo);
|
||||
$this->assertSame(123, (int) $release->fresh()->predb_id);
|
||||
}
|
||||
|
||||
private function setEnvironmentValue(string $key, ?string $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Services\NameFixing;
|
||||
|
||||
use App\Services\NameFixing\DonorMatchSelector;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DonorMatchSelectorTest extends TestCase
|
||||
{
|
||||
public function test_it_selects_the_closest_donor_with_a_stable_id_tie_breaker(): void
|
||||
{
|
||||
$selector = new DonorMatchSelector;
|
||||
|
||||
$selected = $selector->select([
|
||||
(object) ['releases_id' => 20, 'relsize' => 950],
|
||||
(object) ['releases_id' => 10, 'relsize' => 1050],
|
||||
(object) ['releases_id' => 30, 'relsize' => 900],
|
||||
], 1000, 10);
|
||||
|
||||
$this->assertNotNull($selected);
|
||||
$this->assertSame(10, $selected->releases_id);
|
||||
}
|
||||
|
||||
public function test_it_rejects_zero_sized_and_out_of_tolerance_donors(): void
|
||||
{
|
||||
$selector = new DonorMatchSelector;
|
||||
|
||||
$selected = $selector->select([
|
||||
(object) ['releases_id' => 10, 'relsize' => 0],
|
||||
(object) ['releases_id' => 20, 'relsize' => 500],
|
||||
], 1000, 10);
|
||||
|
||||
$this->assertNull($selected);
|
||||
$this->assertNull($selector->select([(object) ['releases_id' => 30, 'relsize' => 1000]], 0, 10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Services\NameFixing;
|
||||
|
||||
use App\Services\NameFixing\NameFixingQueryService;
|
||||
use App\Services\NameFixing\NameFixingService;
|
||||
use App\Services\NameFixing\ReleaseUpdateService;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
class NameFixingBatchTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function grouped_processing_fetches_sources_once_for_the_whole_batch(): void
|
||||
{
|
||||
$database = $this->createMock(ConnectionInterface::class);
|
||||
$database->expects($this->exactly(5))
|
||||
->method('select')
|
||||
->willReturnCallback(function (string $sql): array {
|
||||
$this->assertStringNotContainsString('GROUP_CONCAT', $sql);
|
||||
$this->assertStringNotContainsString('GROUP BY r.id', $sql);
|
||||
|
||||
if (str_contains($sql, 'r.proc_nfo')) {
|
||||
$this->assertStringContainsString('r.nfostatus > -1', $sql);
|
||||
|
||||
return [$this->processedRelease(10), $this->processedRelease(20)];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
$serviceReflection = new ReflectionClass(NameFixingService::class);
|
||||
$service = $serviceReflection->newInstanceWithoutConstructor();
|
||||
$serviceReflection->getProperty('queries')
|
||||
->setValue($service, new NameFixingQueryService($database));
|
||||
$serviceReflection->getProperty('updateService')
|
||||
->setValue(
|
||||
$service,
|
||||
(new ReflectionClass(ReleaseUpdateService::class))->newInstanceWithoutConstructor()
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
['checked' => 2, 'fixed' => 0],
|
||||
$service->processStandardBatch('a', 100, false)
|
||||
);
|
||||
}
|
||||
|
||||
private function processedRelease(int $id): object
|
||||
{
|
||||
return (object) [
|
||||
'releases_id' => $id,
|
||||
'guid' => "guid-{$id}",
|
||||
'name' => "release-{$id}",
|
||||
'searchname' => "release-{$id}",
|
||||
'size' => 1000,
|
||||
'groupName' => 'alt.binaries.test',
|
||||
'categoryid' => 8010,
|
||||
'nfostatus' => 0,
|
||||
'proc_nfo' => NameFixingService::PROC_NFO_DONE,
|
||||
'proc_files' => NameFixingService::PROC_FILES_DONE,
|
||||
'proc_par2' => NameFixingService::PROC_PAR2_DONE,
|
||||
'proc_uid' => NameFixingService::PROC_UID_DONE,
|
||||
'proc_hash16k' => NameFixingService::PROC_HASH16K_DONE,
|
||||
'proc_srr' => NameFixingService::PROC_SRR_DONE,
|
||||
'proc_crc32' => NameFixingService::PROC_CRC_DONE,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Services\NameFixing;
|
||||
|
||||
use App\Services\NameFixing\NameFixingQueryService;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NameFixingQueryServiceTest extends TestCase
|
||||
{
|
||||
public function test_it_preserves_every_source_row_when_grouping_by_release(): void
|
||||
{
|
||||
$database = $this->createStub(ConnectionInterface::class);
|
||||
$service = new NameFixingQueryService($database);
|
||||
$first = (object) ['releases_id' => 10, 'textstring' => 'first.rar'];
|
||||
$second = (object) ['releases_id' => 10, 'textstring' => 'second.rar'];
|
||||
$third = (object) ['releases_id' => 20, 'textstring' => 'third.rar'];
|
||||
|
||||
$grouped = $service->groupByReleaseId([$first, $second, $third]);
|
||||
|
||||
$this->assertSame([$first, $second], $grouped[10]);
|
||||
$this->assertSame([$third], $grouped[20]);
|
||||
}
|
||||
|
||||
public function test_file_candidates_use_exists_without_lossy_grouping(): void
|
||||
{
|
||||
$database = $this->createMock(ConnectionInterface::class);
|
||||
$database->expects($this->once())
|
||||
->method('select')
|
||||
->with(
|
||||
$this->callback(static fn (string $sql): bool => str_contains($sql, 'EXISTS (SELECT 1 FROM release_files')
|
||||
&& ! str_contains($sql, 'GROUP BY')),
|
||||
$this->callback(static fn (array $bindings): bool => $bindings[array_key_last($bindings)] === 100)
|
||||
)
|
||||
->willReturn([]);
|
||||
|
||||
$service = new NameFixingQueryService($database);
|
||||
$service->candidateBatch(NameFixingQueryService::SOURCE_FILES, 2, 2, 0, 100);
|
||||
}
|
||||
|
||||
public function test_predb_workers_use_disjoint_modulo_partitions_without_offsets(): void
|
||||
{
|
||||
$database = $this->createMock(ConnectionInterface::class);
|
||||
$database->expects($this->once())
|
||||
->method('select')
|
||||
->with(
|
||||
$this->callback(static fn (string $sql): bool => str_contains($sql, 'MOD(p.id, ?) = ?')
|
||||
&& ! str_contains($sql, 'OFFSET')),
|
||||
$this->callback(static fn (array $bindings): bool => $bindings[1] === 4
|
||||
&& $bindings[2] === 2
|
||||
&& $bindings[3] === 250)
|
||||
)
|
||||
->willReturn([]);
|
||||
|
||||
$service = new NameFixingQueryService($database);
|
||||
$service->predbBatch(3, 4, 250);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user