Add full IO support for every global ProPresenter library file plus theme folders, and extend the existing Labels/Macros readers with exporters and editable accessors so every supported document is now a round-trippable, mutable object. New library readers/writers (each: FileReader, FileWriter, Library wrapper, element wrapper where applicable, CLI tool, tests, doc/api/*.md): - Groups (ProGroupsDocument) + GroupDefinition - ClearGroups (ClearGroupsDocument) + ClearGroupDefinition - CCLI (CCLIDocument) - Messages (MessageDocument) + Message - Timers (TimersDocument + Clock) + Timer - Stage (Stage.Document) + StageLayout - Workspace (ProPresenterWorkspace) + Screen - Props (PropDocument) + Prop - TestPatterns (TestPatternDocument) - Calendar (new CalendarDocument) + CalendarEvent - KeyMappings (new KeyMappingsDocument) + KeyMapping - CommunicationDevices (JSON file) + CommunicationDevice - Theme bundles (Template.Document folder + Assets/) + ThemeBundle/Slide/Asset Extensions to existing modules: - LabelsFileWriter; Label and LabelLibrary gain setters, addLabel, removeLabel, setColor / setColorHex helpers - MacrosFileWriter; Macro/MacroCollection/MacroLibrary gain UUID, name, color, image_type, image_data, trigger_on_startup setters plus add/remove for macros and collections Two new minimal proto schemas were defined for documents that lacked upstream definitions: - proto/calendar.proto - CalendarDocument with Event entries, raw bytes for the action/macro sub-messages so the schema can evolve - proto/keyMappings.proto - KeyMappingsDocument with ApplicationInfo and a forward-looking Mapping message (sample only carries the info) The Theme file turned out to be a regular Rv\Data\Template\Document, so no new proto was required for theme content; ThemeBundle layers folder + Assets/ handling on top in the same spirit as PresentationBundle. GroupDefinition is intentionally distinct from the existing Group class (which wraps song-level CueGroup) to avoid breaking song APIs. Verified with the full PHPUnit suite: 370 tests, 9200 assertions, all green; LSP diagnostics clean across src/. The unmodified reference samples for Labels, Groups, ClearGroups, TestPatterns, Calendar and KeyMappings round-trip byte-for-byte; the others round-trip with the same byte length (PHP protobuf is not canonically deterministic but re-write-after-write stabilises). doc/INDEX.md, doc/keywords.md and AGENTS.md updated so every new module is discoverable from the top level.
149 lines
5 KiB
PHP
149 lines
5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ProPresenter\Parser\Tests;
|
|
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ProPresenter\Parser\ThemeAsset;
|
|
use ProPresenter\Parser\ThemeBundle;
|
|
use ProPresenter\Parser\ThemeFileReader;
|
|
use ProPresenter\Parser\ThemeFileWriter;
|
|
use ProPresenter\Parser\ThemeSlide;
|
|
use Rv\Data\Template\Slide as TemplateSlide;
|
|
|
|
class ThemeFileReaderTest extends TestCase
|
|
{
|
|
private const REFERENCE_PATH = __DIR__ . '/../doc/reference_samples/pp-themes/sample';
|
|
|
|
#[Test]
|
|
public function readThrowsOnMissingFolder(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
ThemeFileReader::read(__DIR__ . '/../doc/reference_samples/pp-themes/does-not-exist');
|
|
}
|
|
|
|
#[Test]
|
|
public function readReturnsBundleWithExpectedCount(): void
|
|
{
|
|
$bundle = ThemeFileReader::read(self::REFERENCE_PATH);
|
|
$this->assertInstanceOf(ThemeBundle::class, $bundle);
|
|
$this->assertCount(11, $bundle->getSlides());
|
|
$this->assertSame(11, $bundle->count());
|
|
$this->assertCount(3, $bundle->getAssets());
|
|
$this->assertSame(3, $bundle->getAssetCount());
|
|
}
|
|
|
|
#[Test]
|
|
public function slideExposesNameAndBaseSlide(): void
|
|
{
|
|
$slide = ThemeFileReader::read(self::REFERENCE_PATH)->getSlides()[0];
|
|
$this->assertInstanceOf(ThemeSlide::class, $slide);
|
|
$this->assertSame('KeyVisual', $slide->getName());
|
|
$this->assertNotNull($slide->getBaseSlide());
|
|
}
|
|
|
|
#[Test]
|
|
public function assetsExposeBytesAndMimeType(): void
|
|
{
|
|
$asset = ThemeFileReader::read(self::REFERENCE_PATH)->getAssetByName('BACKGROUND.jpg');
|
|
$this->assertInstanceOf(ThemeAsset::class, $asset);
|
|
$this->assertSame('BACKGROUND.jpg', $asset->getName());
|
|
$this->assertNotSame('', $asset->getBytes());
|
|
$this->assertGreaterThan(0, $asset->getSize());
|
|
$this->assertSame('image/jpeg', $asset->getMimeType());
|
|
}
|
|
|
|
#[Test]
|
|
public function writerProducesStableThemeDocumentRoundTrip(): void
|
|
{
|
|
$tmp = $this->makeTempDir('theme_');
|
|
$second = $this->makeTempDir('theme_');
|
|
try {
|
|
ThemeFileWriter::write(ThemeFileReader::read(self::REFERENCE_PATH), $tmp);
|
|
ThemeFileWriter::write(ThemeFileReader::read($tmp), $second);
|
|
$this->assertSame(file_get_contents($tmp . '/Theme'), file_get_contents($second . '/Theme'));
|
|
} finally {
|
|
$this->removeDirectory($tmp);
|
|
$this->removeDirectory($second);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function writerRoundTripsEntireFolder(): void
|
|
{
|
|
$source = ThemeFileReader::read(self::REFERENCE_PATH);
|
|
$tmp = $this->makeTempDir('theme_');
|
|
try {
|
|
ThemeFileWriter::write($source, $tmp);
|
|
$roundTrip = ThemeFileReader::read($tmp);
|
|
$this->assertSame($source->count(), $roundTrip->count());
|
|
$this->assertSame($source->getAssetCount(), $roundTrip->getAssetCount());
|
|
$first = $source->getAssets()[0];
|
|
$this->assertSame($first->getBytes(), $roundTrip->getAssetByName($first->getName())?->getBytes());
|
|
} finally {
|
|
$this->removeDirectory($tmp);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function addRemoveAndStaleAssetCleanupRoundTrip(): void
|
|
{
|
|
$bundle = ThemeFileReader::read(self::REFERENCE_PATH);
|
|
$slide = new ThemeSlide(new TemplateSlide());
|
|
$slide->setName('Test Theme Slide');
|
|
$bundle->addSlide($slide);
|
|
$bundle->addAsset('TEST.png', 'png-bytes');
|
|
$this->assertSame(12, $bundle->count());
|
|
$this->assertSame(4, $bundle->getAssetCount());
|
|
|
|
$this->assertTrue($bundle->removeSlide('Test Theme Slide'));
|
|
$this->assertTrue($bundle->removeAsset('TEST.png'));
|
|
$this->assertSame(11, $bundle->count());
|
|
$this->assertSame(3, $bundle->getAssetCount());
|
|
|
|
$tmp = $this->makeTempDir('theme_');
|
|
try {
|
|
mkdir($tmp . '/Assets');
|
|
file_put_contents($tmp . '/Assets/stale.jpg', 'stale');
|
|
ThemeFileWriter::write($bundle, $tmp);
|
|
$this->assertFileDoesNotExist($tmp . '/Assets/stale.jpg');
|
|
} finally {
|
|
$this->removeDirectory($tmp);
|
|
}
|
|
}
|
|
|
|
private function makeTempDir(string $prefix): string
|
|
{
|
|
$path = sys_get_temp_dir() . '/' . $prefix . uniqid('', true);
|
|
mkdir($path);
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function removeDirectory(string $path): void
|
|
{
|
|
if (!is_dir($path)) {
|
|
return;
|
|
}
|
|
$entries = scandir($path);
|
|
if ($entries === false) {
|
|
return;
|
|
}
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
$child = $path . '/' . $entry;
|
|
if (is_dir($child)) {
|
|
$this->removeDirectory($child);
|
|
} else {
|
|
@unlink($child);
|
|
}
|
|
}
|
|
@rmdir($path);
|
|
}
|
|
}
|