Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-catalog-database-reader.spec.ts
T
4gray a2fafcfc08 test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark

* feat(xtream-mock-server): add deterministic 100k fixture

* style(xtream-mock-server): apply repository formatting

* fix(xtream-mock-server): harden performance fixture data

* feat(xtream-mock-server): add performance control plane

* docs(performance): correct Xtream capture plan

* fix(xtream-mock-server): harden performance controls

* fix(xtream-mock-server): harden control lifecycle

* feat(performance): add Xtream preload markers

* feat(performance): trace Xtream main phases

* feat(performance): mark Xtream store publications

* feat(performance): trace Xtream database phases

* feat(performance): trace Xtream delete cancellation

* feat(performance): capture Xtream phase attribution

* feat(performance): mark Sources Xtream refresh

* test(performance): define Xtream benchmark evidence contracts

* test(performance): add Xtream benchmark runner

* test(performance): surface failure evidence writes

* test(performance): align database read clock

* test(performance): preserve capture failure contracts
2026-07-28 08:08:07 +02:00

366 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import {
readXtreamCatalogDatabaseSnapshot,
type XtreamCatalogDatabaseEvaluator,
} from './xtream-catalog-database-reader';
interface TestStatement {
get(...parameters: readonly (number | string)[]): unknown;
run(...parameters: readonly (number | string)[]): unknown;
}
interface TestDatabase {
close(): void;
exec(source: string): void;
prepare(source: string): TestStatement;
}
interface TestDatabaseConstructor {
new (path: string): TestDatabase;
}
interface RuntimeProcess {
getBuiltinModule(id: string): unknown;
}
const { DatabaseSync } = (
process as unknown as RuntimeProcess
).getBuiltinModule('node:sqlite') as {
readonly DatabaseSync: TestDatabaseConstructor;
};
describe('Xtream catalog database reader', () => {
it('reads the scoped catalog and import state after capture stops', async () => {
const database = createDatabase();
seedPlaylist(database, 'playlist-1');
seedPartition(database, {
categoryId: 91_101,
categoryName: 'Performance Live 01',
categoryType: 'live',
contentBase: 1_000_000,
contentType: 'live',
playlistId: 'playlist-1',
titleLabel: 'Live',
});
seedPartition(database, {
categoryId: 92_101,
categoryName: 'Performance VOD 01',
categoryType: 'movies',
contentBase: 2_000_000,
contentType: 'movie',
playlistId: 'playlist-1',
titleLabel: 'Movie',
});
seedPartition(database, {
categoryId: 93_101,
categoryName: 'Performance Series 01',
categoryType: 'series',
contentBase: 3_000_000,
contentType: 'series',
playlistId: 'playlist-1',
titleLabel: 'Series',
});
seedStatuses(database, 'playlist-1', 'completed');
const evaluator = evaluatorFor(database, 2_001);
const snapshot = await readXtreamCatalogDatabaseSnapshot(evaluator, {
captureStoppedEpochMs: 2_000,
dataDirectory: '/tmp/iptvnator-performance-fixture',
playlistId: 'playlist-1',
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
});
assert.deepEqual(snapshot, {
captureStoppedEpochMs: 2_000,
categories: {
live: categoryPartition(1, 91_101),
movies: categoryPartition(1, 92_101),
series: categoryPartition(1, 93_101),
},
categoryCount: 3,
content: {
live: contentPartition(1_000, 1_000_000),
movie: contentPartition(1_000, 2_000_000),
series: contentPartition(1_000, 3_000_000),
},
contentCount: 3_000,
databaseReadStartedEpochMs: 2_001,
importStatuses: {
live: 'completed',
movie: 'completed',
series: 'completed',
},
matchingPlaylistCount: 1,
matchingPlaylistType: 'xtream',
playlistCount: 1,
});
database.close();
});
it('reports malformed fixture rows and incomplete category cardinality', async () => {
const database = createDatabase();
seedPlaylist(database, 'playlist-1');
database.exec(`
INSERT INTO categories (playlist_id, name, type, xtream_id)
VALUES ('playlist-1', 'wrong category', 'live', 91101);
INSERT INTO content (category_id, title, xtream_id, type)
VALUES (last_insert_rowid(), 'wrong title', 1000000, 'live');
`);
seedStatuses(database, 'playlist-1', 'cancelled');
const snapshot = await readXtreamCatalogDatabaseSnapshot(
evaluatorFor(database, 3_001),
{
captureStoppedEpochMs: 3_000,
dataDirectory: '/tmp/iptvnator-performance-fixture',
playlistId: 'playlist-1',
requireFromPath:
'/workspace/dist/apps/electron-backend/main.js',
}
);
assert.equal(snapshot.categories.live.invalidNameCount, 1);
assert.equal(snapshot.content.live.invalidTitleCount, 1);
assert.equal(snapshot.content.live.invalidCategoryCardinalityCount, 1);
assert.deepEqual(snapshot.importStatuses, {
live: 'cancelled',
movie: 'cancelled',
series: 'cancelled',
});
database.close();
});
it('returns an empty deleted state without inventing a playlist type', async () => {
const database = createDatabase();
seedStatuses(database, 'playlist-1', 'completed');
const snapshot = await readXtreamCatalogDatabaseSnapshot(
evaluatorFor(database, 4_001),
{
captureStoppedEpochMs: 4_000,
dataDirectory: '/tmp/iptvnator-performance-fixture',
playlistId: 'playlist-1',
requireFromPath:
'/workspace/dist/apps/electron-backend/main.js',
}
);
assert.equal(snapshot.categoryCount, 0);
assert.equal(snapshot.contentCount, 0);
assert.equal(snapshot.matchingPlaylistCount, 0);
assert.equal(snapshot.matchingPlaylistType, null);
assert.equal(snapshot.playlistCount, 0);
database.close();
});
it('fails closed for pre-capture reads and malformed main-process results', async () => {
const database = createDatabase();
const base = {
captureStoppedEpochMs: 5_000,
dataDirectory: '/tmp/iptvnator-performance-fixture',
playlistId: 'playlist-1',
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
};
await assert.rejects(
readXtreamCatalogDatabaseSnapshot(
evaluatorFor(database, 4_999),
base
),
/invalid Xtream catalog database read/
);
await assert.rejects(
readXtreamCatalogDatabaseSnapshot(
{
evaluate: async () => ({
databaseReadStartedEpochMs: 5_001,
rows: [],
}),
},
base
),
/invalid Xtream catalog database read/
);
await assert.rejects(
readXtreamCatalogDatabaseSnapshot(evaluatorFor(database, 5_001), {
...base,
dataDirectory: 'relative',
}),
/invalid Xtream catalog database read/
);
database.close();
});
it('accepts high-resolution fractional capture and database epochs', async () => {
const database = createDatabase();
const snapshot = await readXtreamCatalogDatabaseSnapshot(
evaluatorFor(database, 6_000.75),
{
captureStoppedEpochMs: 6_000.5,
dataDirectory: '/tmp/iptvnator-performance-fixture',
playlistId: 'playlist-1',
requireFromPath:
'/workspace/dist/apps/electron-backend/main.js',
}
);
assert.equal(snapshot.captureStoppedEpochMs, 6_000.5);
assert.equal(snapshot.databaseReadStartedEpochMs, 6_000.75);
database.close();
});
it('uses the high-resolution epoch clock inside the main-process read boundary', () => {
const source = readFileSync(
resolve(
process.cwd(),
'src/performance/xtream-catalog-database-reader.ts'
),
'utf8'
);
assert.match(source, /getBuiltinModule\(\s*'node:perf_hooks'\s*\)/u);
assert.match(
source,
/perfHooks\.performance\.timeOrigin\s*\+\s*perfHooks\.performance\.now\(\)/u
);
assert.doesNotMatch(source, /Date\.now\(\)/u);
});
});
interface QueryDescriptor {
readonly parameters: readonly (number | string)[];
readonly sql: string;
}
interface EvaluationInput {
readonly queries: readonly QueryDescriptor[];
}
function evaluatorFor(
database: TestDatabase,
databaseReadStartedEpochMs: number
): XtreamCatalogDatabaseEvaluator {
return {
evaluate: async (_pageFunction: unknown, value: unknown) => {
const input = value as unknown as EvaluationInput;
return {
databaseReadStartedEpochMs,
rows: input.queries.map((query) =>
database.prepare(query.sql).get(...query.parameters)
),
};
},
} as unknown as XtreamCatalogDatabaseEvaluator;
}
function createDatabase(): TestDatabase {
const database = new DatabaseSync(':memory:');
database.exec(`
CREATE TABLE playlists (id TEXT PRIMARY KEY, type TEXT NOT NULL);
CREATE TABLE app_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playlist_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
xtream_id INTEGER NOT NULL
);
CREATE TABLE content (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
title TEXT NOT NULL,
xtream_id INTEGER NOT NULL,
type TEXT NOT NULL
);
`);
return database;
}
function seedPlaylist(database: TestDatabase, playlistId: string): void {
database
.prepare('INSERT INTO playlists (id, type) VALUES (?, ?)')
.run(playlistId, 'xtream');
}
function seedStatuses(
database: TestDatabase,
playlistId: string,
status: string
): void {
const insert = database.prepare(
'INSERT INTO app_state (key, value) VALUES (?, ?)'
);
for (const type of ['live', 'movie', 'series']) {
insert.run(`xtream-import-status:${playlistId}:${type}`, status);
}
}
function seedPartition(
database: TestDatabase,
options: {
readonly categoryId: number;
readonly categoryName: string;
readonly categoryType: string;
readonly contentBase: number;
readonly contentType: string;
readonly playlistId: string;
readonly titleLabel: string;
}
): void {
database
.prepare(
`INSERT INTO categories (playlist_id, name, type, xtream_id)
VALUES (?, ?, ?, ?)`
)
.run(
options.playlistId,
options.categoryName,
options.categoryType,
options.categoryId
);
const categoryRow = database
.prepare('SELECT id FROM categories WHERE playlist_id = ? AND type = ?')
.get(options.playlistId, options.categoryType) as { id: number };
const insert = database.prepare(
`INSERT INTO content (category_id, title, xtream_id, type)
VALUES (?, ?, ?, ?)`
);
for (let index = 0; index < 1_000; index += 1) {
insert.run(
categoryRow.id,
`Performance ${options.titleLabel} ${String(index + 1).padStart(
6,
'0'
)}`,
options.contentBase + index,
options.contentType
);
}
}
function categoryPartition(count: number, id: number) {
return {
count,
distinctXtreamIdCount: count,
invalidNameCount: 0,
maxXtreamId: id,
minXtreamId: id,
};
}
function contentPartition(count: number, id: number) {
return {
count,
distinctXtreamIdCount: count,
invalidCategoryCardinalityCount: 0,
invalidTitleCount: 0,
maxXtreamId: id + count - 1,
minXtreamId: id,
};
}