propresenter7-php-lib/tests/BinaryFidelityTest.php
Thorsten Buss 4d10db4f17 Initial public release
PHP 8.4 library and CLI tools to read, modify, and generate ProPresenter 7
files: songs (.pro), playlists (.proplaylist), bundles (.probundle), themes,
and the global library files (Macros, Labels, Groups, ClearGroups, CCLI,
Messages, Timers, Stage, Workspace, Props, TestPatterns, Calendar,
KeyMappings, CommunicationDevices).

Built on the MIT-licensed ProPresenter7-Proto schema by greyshirtguy
(v7.16.2). Includes 18 CLI parsers, comprehensive API docs in doc/,
binary-format specs, and a synthetic 369-test PHPUnit suite covering
RTF extraction, translation slides, ZIP64 repair, round-trip fidelity,
and end-to-end generation.
2026-05-03 22:21:01 +02:00

110 lines
3.6 KiB
PHP

<?php
namespace ProPresenter\Parser\Tests;
use PHPUnit\Framework\TestCase;
use Rv\Data\Presentation;
class BinaryFidelityTest extends TestCase
{
private const ALL_SONGS_DIR = __DIR__ . '/../doc/reference_samples/all-songs';
private const TEST_PRO_PATH = __DIR__ . '/../doc/reference_samples/Test.pro';
private const EMPTY_SKIP_FILE = __DIR__ . '/../doc/reference_samples/all-songs/_empty.pro';
private const EXPECTED_NON_EMPTY_ALL_SONGS = 9;
public function testDecodeEncodeRoundTripAcrossReferenceFiles(): void
{
$referenceFiles = glob(self::ALL_SONGS_DIR . '/*.pro');
$this->assertIsArray($referenceFiles, 'Unable to list .pro files in all-songs directory.');
sort($referenceFiles, SORT_STRING);
$paths = [];
foreach ($referenceFiles as $path) {
if ($path === self::EMPTY_SKIP_FILE) {
continue;
}
$bytes = @file_get_contents($path);
$this->assertIsString($bytes, 'Unable to read file: ' . $path);
if ($bytes === '') {
$this->fail('Unexpected empty file not in skip list: ' . $path);
}
$paths[] = $path;
}
$paths[] = self::TEST_PRO_PATH;
$this->assertCount(
self::EXPECTED_NON_EMPTY_ALL_SONGS + 1,
$paths,
sprintf('Expected %d non-empty all-songs files plus Test.pro.', self::EXPECTED_NON_EMPTY_ALL_SONGS),
);
$failures = [];
$byteIdenticalCount = 0;
foreach ($paths as $path) {
$original = @file_get_contents($path);
$this->assertIsString($original, 'Unable to read file: ' . $path);
$presentation = new Presentation();
try {
$presentation->mergeFromString($original);
} catch (\Throwable $throwable) {
$failures[] = [
'path' => $path,
'reason' => 'decode_error',
'message' => $throwable->getMessage(),
];
continue;
}
$reencoded = $presentation->serializeToString();
if ($original === $reencoded) {
$byteIdenticalCount++;
}
$roundTrip = new Presentation();
try {
$roundTrip->mergeFromString($reencoded);
} catch (\Throwable $throwable) {
$failures[] = [
'path' => $path,
'reason' => 'reencode_decode_error',
'message' => $throwable->getMessage(),
];
continue;
}
$originalJson = json_decode($presentation->serializeToJsonString(), true);
$roundTripJson = json_decode($roundTrip->serializeToJsonString(), true);
if ($originalJson !== $roundTripJson) {
$failures[] = [
'path' => $path,
'reason' => 'semantic_mismatch',
];
}
}
$total = count($paths);
$mismatchCount = count($failures);
$testProIdentical = !in_array(self::TEST_PRO_PATH, array_column($failures, 'path'), true);
$message = "Round-trip results: {$byteIdenticalCount}/{$total} byte-identical, {$mismatchCount} semantic/decode failures. Test.pro byte-identical: "
. ($testProIdentical ? 'yes' : 'no') . '.';
if ($mismatchCount > 0) {
$message .= "\nDetails:\n" . json_encode($failures, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
$this->assertSame([], $failures, $message);
}
}