import { test } from '@japa/runner' import db from '@adonisjs/lucid/services/db' import testUtils from '@adonisjs/core/services/test_utils' import ReleaseCompleteness from '#services/ReleaseCompleteness' /** A release with `n` files (each with a binary of size 100000 + i), named f.bin. */ async function release(n: number) { const now = new Date() const [row] = await db .table('releases') .insert({ release_key: `complete-${n}`.padEnd(40, '0'), name: `Complete.Test.${n}`, source: 'stem', groups: '[]', first_seen_at: now, last_seen_at: now }) .returning('id') const releaseId = Number(row.id) await db.rawQuery( `WITH f AS ( INSERT INTO files (filename, poster, date, parts, groups, segments, release_id, file_key, created_at, updated_at) SELECT 'f' || i || '.bin', 'x', 0, 1, '[]', '\\x'::bytea, ?::bigint, 'complete-' || ?::int || '-' || i, now(), now() FROM generate_series(1, ?::int) i RETURNING id, filename) INSERT INTO binaries (file_key, poster, subject, name, total_parts, groups, first_seen_at, last_seen_at, size, file_id, release_id) SELECT 'complete-b-' || ?::int || '-' || f.id, 'x', 'x', f.filename, 1, '[]', now(), now(), 100000 + substr(f.filename, 2, length(f.filename) - 5)::int, f.id, ?::bigint FROM f`, [releaseId, n, n, n, releaseId] ) return releaseId } /** The par2 listing of `count` files f1..f (sizes as in release()), in one INSERT, as Par2Service.attach does. */ async function listing(releaseId: number, count: number, offset = 0) { await db.rawQuery( `INSERT INTO release_files (release_id, par2_file_id, name, size, md5_16k, set_id) SELECT ?::bigint, md5(i::text), 'f' || i || '.bin', 100000 + i, md5(i::text), 'set' FROM generate_series(1 + ?::int, ?::int + ?::int) i`, [releaseId, offset, count, offset] ) // Triggers only note the release; gc:scheduler's sweep recomputes it. await ReleaseCompleteness.sweep() return Boolean((await db.from('releases').where('id', releaseId).first()).complete) } test.group('release completeness', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('file inserts keep counts, size, extensions and search text current, once per statement', async ({ assert }) => { const id = await release(3) const row = await db.from('releases').where('id', id).first() assert.equal(Number(row.file_count), 3) assert.deepEqual(row.extensions, ['bin']) // No par2 listing, no file total, not complete: its completeness can't change, so it isn't noted. const [{ count }] = await db.from('release_dirty').where('release_id', id).count('* as count') assert.equal(Number(count), 0) }) test('a release with a par2 listing is noted once per insert, and complete by file names alone', async ({ assert }) => { const now = new Date() const [row] = await db .table('releases') .insert({ release_key: 'names-only'.padEnd(40, '0'), name: 'Names.Only', source: 'stem', groups: '[]', first_seen_at: now, last_seen_at: now }) .returning('id') const id = Number(row.id) await db.rawQuery( `INSERT INTO release_files (release_id, par2_file_id, name, size, md5_16k, set_id) SELECT ?::bigint, md5(i::text), 'n' || i || '.rar', 100000 + i, md5(i::text), 'set' FROM generate_series(1, 3) i`, [id] ) await ReleaseCompleteness.sweep() // Seeded files: no binaries, matched by name only. await db.rawQuery( `INSERT INTO files (filename, poster, date, parts, groups, segments, release_id, file_key, created_at, updated_at) SELECT 'n' || i || '.rar', 'x', 0, 1, '[]', '\\x'::bytea, ?::bigint, 'names-only-' || i, now(), now() FROM generate_series(1, 3) i`, [id] ) const [{ count }] = await db.from('release_dirty').where('release_id', id).count('* as count') assert.equal(Number(count), 1) await ReleaseCompleteness.sweep() assert.isTrue(Boolean((await db.from('releases').where('id', id).first()).complete)) }) test('complete when every listed file of 64 KiB or more is there, by size or name', async ({ assert }) => { const id = await release(3) assert.isTrue(await listing(id, 3)) const other = await release(2) // f3 is listed but missing. assert.isFalse(await listing(other, 3)) }) test('a par2 listing of thousands of files is checked in one go', async ({ assert }) => { const id = await release(3000) await ReleaseCompleteness.sweep() const started = Date.now() assert.isTrue(await listing(id, 3000)) // Was cubic: minutes for a few thousand rows. assert.isBelow(Date.now() - started, 5000) }).timeout(60_000) })