test(embedded-mpv): smoke missing bundled libmpv

This commit is contained in:
4gray committed 2026-07-17 22:42:37 +02:00
1 parent 99cb0a4dd6
commit 0be95b002c
5 files changed
+418 -197

No files matched your search

@@ -0,0 +1,288 @@
import assert = require('node:assert/strict');
import {
chmodSync,
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, it } from 'node:test';
import {
createDisposablePackagedLinuxApp,
createPackagedEntryGuard,
readPackagedRuntimeIdentity,
} from './embedded-mpv-frame-copy-packaged-filesystem';
const temporaryDirectories = new Set<string>();
type PackageFixture = {
executablePath: string;
libmpvAliasPath: string;
libmpvSonamePath: string;
nativeDir: string;
packageRoot: string;
payloadPath: string;
runtimeManifestPath: string;
};
function createPackageFixture(): PackageFixture {
const packageRoot = mkdtempSync(
join(tmpdir(), 'iptvnator-packaged-fixture-source-')
);
temporaryDirectories.add(packageRoot);
const executablePath = join(packageRoot, 'IPTVnator');
const nativeDir = join(
packageRoot,
'resources',
'app.asar.unpacked',
'electron-backend',
'native'
);
const payloadPath = join(packageRoot, 'resources', 'payload.bin');
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const runtimeLibraryDir = join(nativeDir, 'lib');
const libmpvSonamePath = join(runtimeLibraryDir, 'libmpv.so.2');
const libmpvAliasPath = join(runtimeLibraryDir, 'libmpv.so');
mkdirSync(runtimeLibraryDir, { recursive: true });
writeFileSync(executablePath, '#!/bin/sh\nexit 0\n');
chmodSync(executablePath, 0o755);
writeFileSync(payloadPath, 'packaged payload');
chmodSync(payloadPath, 0o640);
writeFileSync(libmpvSonamePath, 'packaged libmpv');
symlinkSync('libmpv.so.2', libmpvAliasPath);
writeFileSync(
runtimeManifestPath,
JSON.stringify({
arch: 'x64',
libmpvSoname: 'libmpv.so.2',
platform: 'linux',
profile: 'portable',
runtimeMode: 'bundled',
})
);
if (process.platform !== 'win32') {
symlinkSync(
'payload.bin',
join(packageRoot, 'resources', 'payload-link')
);
}
return {
executablePath,
libmpvAliasPath,
libmpvSonamePath,
nativeDir,
packageRoot,
payloadPath,
runtimeManifestPath,
};
}
function entryExists(entryPath: string): boolean {
try {
lstatSync(entryPath);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return false;
}
throw error;
}
}
afterEach(() => {
for (const directory of temporaryDirectories) {
rmSync(directory, { force: true, recursive: true });
}
temporaryDirectories.clear();
});
describe('disposable unpacked package clone', () => {
it('requires a safe versioned libmpv target in the packaged manifest', () => {
const source = createPackageFixture();
assert.equal(
readPackagedRuntimeIdentity(source.nativeDir).libmpvSoname,
'libmpv.so.2'
);
writeFileSync(
source.runtimeManifestPath,
JSON.stringify({
arch: 'x64',
libmpvSoname: '../libmpv.so.2',
platform: 'linux',
profile: 'portable',
runtimeMode: 'bundled',
})
);
assert.throws(
() => readPackagedRuntimeIdentity(source.nativeDir),
/libmpvSoname/
);
});
it('hides and restores a cloned regular dependency without changing the source package', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath);
temporaryDirectories.add(clone.temporaryRoot);
const clonedLibmpvPath = join(clone.nativeDir, 'lib', 'libmpv.so.2');
const guard = createPackagedEntryGuard(clonedLibmpvPath, {
expectedKind: 'regular-file',
hiddenDirectory: clone.temporaryRoot,
});
try {
assert.equal(lstatSync(clonedLibmpvPath).isFile(), true);
assert.equal(
statSync(clonedLibmpvPath).ino,
statSync(source.libmpvSonamePath).ino
);
guard.hide();
assert.equal(entryExists(clonedLibmpvPath), false);
assert.equal(lstatSync(source.libmpvSonamePath).isFile(), true);
assert.equal(
readFileSync(source.libmpvSonamePath, 'utf8'),
'packaged libmpv'
);
guard.restore();
assert.equal(lstatSync(clonedLibmpvPath).isFile(), true);
assert.equal(
statSync(clonedLibmpvPath).ino,
statSync(source.libmpvSonamePath).ino
);
} finally {
guard.restore();
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
assert.equal(entryExists(source.libmpvSonamePath), true);
});
it('hides and restores a cloned symbolic link without dereferencing it', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath);
temporaryDirectories.add(clone.temporaryRoot);
const clonedAliasPath = join(clone.nativeDir, 'lib', 'libmpv.so');
const guard = createPackagedEntryGuard(clonedAliasPath, {
expectedKind: 'symbolic-link',
hiddenDirectory: clone.temporaryRoot,
});
try {
guard.hide();
assert.equal(entryExists(clonedAliasPath), false);
assert.equal(
lstatSync(source.libmpvAliasPath).isSymbolicLink(),
true
);
assert.equal(readlinkSync(source.libmpvAliasPath), 'libmpv.so.2');
guard.restore();
assert.equal(lstatSync(clonedAliasPath).isSymbolicLink(), true);
assert.equal(readlinkSync(clonedAliasPath), 'libmpv.so.2');
} finally {
guard.restore();
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
});
it('hardlinks regular files and preserves modes, symlinks, and the source manifest', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath);
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.notEqual(clone.packageRoot, source.packageRoot);
assert.equal(
statSync(clone.executablePath).mode & 0o777,
statSync(source.executablePath).mode & 0o777
);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
if (process.platform !== 'win32') {
const clonedLinkPath = join(
clone.packageRoot,
'resources',
'payload-link'
);
assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true);
assert.equal(readlinkSync(clonedLinkPath), 'payload.bin');
}
const clonedRuntimeManifestPath = join(
clone.nativeDir,
'embedded-mpv-runtime.json'
);
assert.equal(existsSync(clonedRuntimeManifestPath), true);
assert.equal(existsSync(source.runtimeManifestPath), true);
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
assert.equal(existsSync(clone.temporaryRoot), false);
assert.equal(existsSync(source.runtimeManifestPath), true);
});
it('copies a regular file when hardlinking is unavailable', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath, {
linkFile() {
throw Object.assign(new Error('cross-device hardlink'), {
code: 'EXDEV',
});
},
});
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
readFileSync(clonedPayloadPath, 'utf8'),
readFileSync(source.payloadPath, 'utf8')
);
assert.notEqual(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
});
});
@@ -1,7 +1,6 @@
import {
chmodSync,
copyFileSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
@@ -11,8 +10,8 @@ import {
readlinkSync,
renameSync,
rmSync,
statSync,
symlinkSync,
type Stats,
} from 'fs';
import { tmpdir } from 'os';
import { basename, dirname, join, resolve } from 'path';
@@ -32,16 +31,24 @@ export type DisposablePackagedLinuxAppOptions = {
export type PackagedRuntimeIdentity = {
arch: string;
libmpvSoname: string;
platform: string;
profile: string;
runtimeMode: string;
};
export type RuntimeManifestGuard = {
export type PackagedEntryKind = 'regular-file' | 'symbolic-link';
export type PackagedEntryGuard = {
hide: () => void;
restore: () => void;
};
export type PackagedEntryGuardOptions = {
expectedKind: PackagedEntryKind;
hiddenDirectory: string;
};
const HARDLINK_COPY_FALLBACK_CODES = new Set([
'EACCES',
'EMLINK',
@@ -50,6 +57,28 @@ const HARDLINK_COPY_FALLBACK_CODES = new Set([
'EPERM',
'EXDEV',
]);
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
function entryKindMatches(
stats: Stats,
expectedKind: PackagedEntryKind
): boolean {
return expectedKind === 'regular-file'
? stats.isFile() && !stats.isSymbolicLink()
: stats.isSymbolicLink();
}
function entryExistsByLstat(entryPath: string): boolean {
try {
lstatSync(entryPath);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return false;
}
throw error;
}
}
function clonePackagedEntry(
sourcePath: string,
@@ -136,43 +165,58 @@ export function createDisposablePackagedLinuxApp(
};
}
export function createRuntimeManifestGuard(
nativeDir: string
): RuntimeManifestGuard {
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
const hiddenRuntimeManifestPath = `${runtimeManifestPath}.e2e-hidden-${process.pid}`;
export function createPackagedEntryGuard(
entryPath: string,
options: PackagedEntryGuardOptions
): PackagedEntryGuard {
const guardedEntryPath = resolve(entryPath);
const hiddenDirectory = resolve(options.hiddenDirectory);
const hiddenEntryPath = join(
hiddenDirectory,
`.${basename(guardedEntryPath)}.e2e-hidden-${process.pid}`
);
let hidden = false;
return {
hide() {
if (!statSync(runtimeManifestPath).isFile()) {
const entryStats = lstatSync(guardedEntryPath);
if (!entryKindMatches(entryStats, options.expectedKind)) {
throw new Error(
`Packaged runtime manifest is not a regular file: ${runtimeManifestPath}`
`Packaged entry is not a ${options.expectedKind}: ${guardedEntryPath}`
);
}
if (existsSync(hiddenRuntimeManifestPath)) {
const hiddenDirectoryStats = lstatSync(hiddenDirectory);
if (
hiddenDirectoryStats.isSymbolicLink() ||
!hiddenDirectoryStats.isDirectory()
) {
throw new Error(
`Stale hidden runtime manifest exists: ${hiddenRuntimeManifestPath}`
`Packaged entry stash is not a regular directory: ${hiddenDirectory}`
);
}
renameSync(runtimeManifestPath, hiddenRuntimeManifestPath);
if (entryExistsByLstat(hiddenEntryPath)) {
throw new Error(
`Stale hidden packaged entry exists: ${hiddenEntryPath}`
);
}
renameSync(guardedEntryPath, hiddenEntryPath);
hidden = true;
},
restore() {
if (!hidden) {
return;
}
if (!existsSync(hiddenRuntimeManifestPath)) {
if (!entryExistsByLstat(hiddenEntryPath)) {
throw new Error(
`Hidden runtime manifest disappeared before restore: ${hiddenRuntimeManifestPath}`
`Hidden packaged entry disappeared before restore: ${hiddenEntryPath}`
);
}
if (existsSync(runtimeManifestPath)) {
if (entryExistsByLstat(guardedEntryPath)) {
throw new Error(
`Refusing to overwrite runtime manifest during restore: ${runtimeManifestPath}`
`Refusing to overwrite packaged entry during restore: ${guardedEntryPath}`
);
}
renameSync(hiddenRuntimeManifestPath, runtimeManifestPath);
renameSync(hiddenEntryPath, guardedEntryPath);
hidden = false;
},
};
@@ -198,6 +242,14 @@ export function readPackagedRuntimeIdentity(
);
}
}
if (
typeof parsed.libmpvSoname !== 'string' ||
!VERSIONED_LIBMPV_PATTERN.test(parsed.libmpvSoname)
) {
throw new Error(
`Packaged runtime manifest libmpvSoname is invalid at ${runtimeManifestPath}.`
);
}
return parsed as PackagedRuntimeIdentity;
}
@@ -1,92 +1,13 @@
import assert = require('node:assert/strict');
import {
chmodSync,
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readlinkSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { afterEach, describe, it } from 'node:test';
import { describe, it } from 'node:test';
import { isMeaningfulNativePlaybackSnapshot } from './embedded-mpv-frame-copy-packaged-fixtures';
import {
createDisposablePackagedLinuxApp,
createRuntimeManifestGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
import './embedded-mpv-frame-copy-packaged-filesystem.tests';
import { resolvePackagedElectronLaunchArgs } from './electron-test-fixtures';
import packagedPlaywrightConfig from '../playwright.packaged.config';
const projectRoot = resolve(__dirname, '..');
const temporaryDirectories = new Set<string>();
type PackageFixture = {
executablePath: string;
nativeDir: string;
packageRoot: string;
payloadPath: string;
runtimeManifestPath: string;
};
function createPackageFixture(): PackageFixture {
const packageRoot = mkdtempSync(
join(tmpdir(), 'iptvnator-packaged-fixture-source-')
);
temporaryDirectories.add(packageRoot);
const executablePath = join(packageRoot, 'IPTVnator');
const nativeDir = join(
packageRoot,
'resources',
'app.asar.unpacked',
'electron-backend',
'native'
);
const payloadPath = join(packageRoot, 'resources', 'payload.bin');
const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json');
mkdirSync(nativeDir, { recursive: true });
writeFileSync(executablePath, '#!/bin/sh\nexit 0\n');
chmodSync(executablePath, 0o755);
writeFileSync(payloadPath, 'packaged payload');
chmodSync(payloadPath, 0o640);
writeFileSync(
runtimeManifestPath,
JSON.stringify({
arch: 'x64',
platform: 'linux',
profile: 'portable',
runtimeMode: 'bundled',
})
);
if (process.platform !== 'win32') {
symlinkSync(
'payload.bin',
join(packageRoot, 'resources', 'payload-link')
);
}
return {
executablePath,
nativeDir,
packageRoot,
payloadPath,
runtimeManifestPath,
};
}
afterEach(() => {
for (const directory of temporaryDirectories) {
rmSync(directory, { force: true, recursive: true });
}
temporaryDirectories.clear();
});
describe('packaged Electron launch arguments', () => {
it('disables the Chromium sandbox only when running as root', () => {
@@ -113,92 +34,6 @@ describe('packaged Electron launch arguments', () => {
});
});
describe('disposable unpacked package clone', () => {
it('hardlinks regular files, preserves modes and symlinks, and never hides the source manifest', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath);
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.notEqual(clone.packageRoot, source.packageRoot);
assert.equal(
statSync(clone.executablePath).mode & 0o777,
statSync(source.executablePath).mode & 0o777
);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
if (process.platform !== 'win32') {
const clonedLinkPath = join(
clone.packageRoot,
'resources',
'payload-link'
);
assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true);
assert.equal(readlinkSync(clonedLinkPath), 'payload.bin');
}
const clonedRuntimeManifestPath = join(
clone.nativeDir,
'embedded-mpv-runtime.json'
);
const guard = createRuntimeManifestGuard(clone.nativeDir);
guard.hide();
assert.equal(existsSync(clonedRuntimeManifestPath), false);
assert.equal(existsSync(source.runtimeManifestPath), true);
guard.restore();
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
assert.equal(existsSync(clone.temporaryRoot), false);
assert.equal(existsSync(source.runtimeManifestPath), true);
});
it('copies a regular file when hardlinking is unavailable', () => {
const source = createPackageFixture();
const clone = createDisposablePackagedLinuxApp(source.executablePath, {
linkFile() {
throw Object.assign(new Error('cross-device hardlink'), {
code: 'EXDEV',
});
},
});
temporaryDirectories.add(clone.temporaryRoot);
try {
assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755);
const clonedPayloadPath = join(
clone.packageRoot,
'resources',
'payload.bin'
);
assert.equal(
readFileSync(clonedPayloadPath, 'utf8'),
readFileSync(source.payloadPath, 'utf8')
);
assert.notEqual(
statSync(clonedPayloadPath).ino,
statSync(source.payloadPath).ino
);
assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640);
} finally {
clone.cleanup();
temporaryDirectories.delete(clone.temporaryRoot);
}
});
});
describe('native-view playback proof', () => {
it('requires the loaded URL, a playing or paused state, and positive duration', () => {
const expectedUrl = 'http://127.0.0.1:3210/fixture.y4m';
@@ -276,6 +111,42 @@ describe('native-view playback proof', () => {
assert.ok(exitCodeIndex > proofIndex);
assert.ok(disposeIndex > exitCodeIndex);
});
it('removes the manifest-declared libmpv target and expects the stable missing-library reason', () => {
const source = readFileSync(
join(projectRoot, 'src', 'embedded-mpv-frame-copy-packaged.e2e.ts'),
'utf8'
);
const manifestIdentityIndex = source.indexOf(
'const runtimeIdentity = readPackagedRuntimeIdentity'
);
const guardIndex = source.indexOf(
'createPackagedEntryGuard(',
manifestIdentityIndex
);
const sonameIndex = source.indexOf(
'runtimeIdentity.libmpvSoname',
guardIndex
);
const hideIndex = source.indexOf('.hide()', sonameIndex);
const fallbackStart = source.indexOf(
'const launchedFallbackApp',
hideIndex
);
const missingReasonIndex = source.indexOf(
"frameCopyUnavailableReason: 'runtime-library-missing'",
fallbackStart
);
assert.ok(manifestIdentityIndex >= 0);
assert.ok(guardIndex > manifestIdentityIndex);
assert.ok(sonameIndex > guardIndex);
assert.ok(hideIndex > sonameIndex);
assert.ok(fallbackStart > hideIndex);
assert.ok(missingReasonIndex > fallbackStart);
assert.doesNotMatch(source, /createRuntimeManifestGuard/);
assert.doesNotMatch(source, /runtimeManifest\.hide/);
});
});
describe('dedicated packaged smoke target', () => {
@@ -12,7 +12,7 @@ import {
} from './electron-test-fixtures';
import type {
DisposablePackagedLinuxApp,
RuntimeManifestGuard,
PackagedEntryGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
declare global {
@@ -291,9 +291,9 @@ export async function closeAndWaitForExit(
export async function cleanupPackagedFrameCopySmoke(options: {
apps: Array<LaunchedElectronApp | undefined>;
hiddenRuntimeEntry?: PackagedEntryGuard;
media?: LocalMediaServer;
packageClone?: DisposablePackagedLinuxApp;
runtimeManifest?: RuntimeManifestGuard;
}): Promise<void> {
const errors: unknown[] = [];
@@ -308,9 +308,9 @@ export async function cleanupPackagedFrameCopySmoke(options: {
}
}
if (options.runtimeManifest) {
if (options.hiddenRuntimeEntry) {
try {
options.runtimeManifest.restore();
options.hiddenRuntimeEntry.restore();
} catch (error) {
errors.push(error);
}
@@ -5,6 +5,7 @@ import {
test,
type LaunchedElectronApp,
} from './electron-test-fixtures';
import { join } from 'path';
import {
assertNativeFallbackPrerequisites,
cleanupPackagedFrameCopySmoke,
@@ -20,10 +21,10 @@ import {
} from './embedded-mpv-frame-copy-packaged-fixtures';
import {
createDisposablePackagedLinuxApp,
createRuntimeManifestGuard,
createPackagedEntryGuard,
readPackagedRuntimeIdentity,
type DisposablePackagedLinuxApp,
type RuntimeManifestGuard,
type PackagedEntryGuard,
} from './embedded-mpv-frame-copy-packaged-filesystem';
const PACKAGED_FRAME_COPY_REQUIRED_ENV =
@@ -68,7 +69,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
const sourceExecutablePath = packagedExecutable as string;
assertNativeFallbackPrerequisites();
let packageClone: DisposablePackagedLinuxApp | undefined;
let runtimeManifest: RuntimeManifestGuard | undefined;
let hiddenRuntimeEntry: PackagedEntryGuard | undefined;
let media: LocalMediaServer | undefined;
let frameCopyApp: LaunchedElectronApp | undefined;
let fallbackApp: LaunchedElectronApp | undefined;
@@ -79,7 +80,13 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
const executablePath = packageClone.executablePath;
const nativeDir = packageClone.nativeDir;
const runtimeIdentity = readPackagedRuntimeIdentity(nativeDir);
runtimeManifest = createRuntimeManifestGuard(nativeDir);
hiddenRuntimeEntry = createPackagedEntryGuard(
join(nativeDir, 'lib', runtimeIdentity.libmpvSoname),
{
expectedKind: 'regular-file',
hiddenDirectory: packageClone.temporaryRoot,
}
);
const mediaServer = await createLocalMediaServer();
media = mediaServer;
@@ -219,7 +226,10 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
await closeAndWaitForExit(launchedFrameCopyApp);
frameCopyApp = undefined;
runtimeManifest.hide();
hiddenRuntimeEntry.hide();
expect(readPackagedRuntimeIdentity(nativeDir)).toEqual(
runtimeIdentity
);
const launchedFallbackApp = await launchPackagedElectronApp(
executablePath,
@@ -237,7 +247,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
expect(fallbackSupport).toMatchObject({
engine: 'native',
frameCopyAvailable: false,
frameCopyUnavailableReason: 'runtime-manifest-missing',
frameCopyUnavailableReason: 'runtime-library-missing',
platform: 'linux',
supported: true,
});
@@ -292,9 +302,9 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => {
} finally {
await cleanupPackagedFrameCopySmoke({
apps: [frameCopyApp, fallbackApp],
hiddenRuntimeEntry,
media,
packageClone,
runtimeManifest,
});
}
});