mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
test(embedded-mpv): smoke missing bundled libmpv
This commit is contained in:
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user