Files
XC_VM/tests/Unit/MonitorCommandTest.php
T
Divarion_D dc7fa83b0a refactor: apply Rector safe sets + fix the transform's fallout (reviewed)
Ran `make rector-fix` (deadCode + codeQuality prepared sets) over the PSR-4
class trees (Core/Domain/Cli/Infrastructure). Net -600 lines: dead-code removal
and the empty-if/else collapse (the feedback_simplify_empty_else pattern this
adoption targeted). 99 files mechanically transformed.

The output was reviewed against the full test suite + PHPStan + a targeted scan;
this commit is the CORRECTED pass (no broken state in history):

- FIX (Rector bug): its boolean inversion dropped the parens around an
  assignment-in-condition — `if (($rKey = array_search(...)) === false)` became
  `if ($rKey = array_search(...) !== false)`, assigning the bool to $rKey and
  unsetting the wrong array offset. Restored parens in 7 sites (BouquetService,
  ChannelService, CategoryService). PHPStan caught only 1 of the 7; the rest
  were silent. Added BouquetServiceTest as a regression (proven to fail on the
  broken form).
- FIX (pre-existing, same class): GroupService::removeGroupFromUsers had the
  identical dropped-parens bug already in the tree — corrected here too.
- FIX: ServersCronJob::pingServer returns floor() (float) under an `: int`
  return type — added an (int) cast.
- ACCEPT: LocallyCalledStaticMethodToNonStaticRector converted 25 locally-called
  private static helpers to instance methods (behaviour-preserving; call sites
  rewritten). Updated MonitorCommandTest's reflection helper to invoke on a
  constructor-less instance.

Verified: PHPStan level 5 clean (0), suite 691 tests / 0 errors.

NOTE: do not re-run `make rector-fix` on this tree without first skipping the
inversion rule — the dropped-parens bug is deterministic and would return.
2026-09-13 17:27:42 +03:00

157 lines
7.4 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\Commands\MonitorCommand;
/**
* Unit tests for pure helpers extracted from MonitorCommand's obfuscated
* goto-based execute(). Each helper replaces a self-contained goto cluster; the
* tests lock its behaviour so the control-flow untangling cannot silently drift.
*/
final class MonitorCommandTest extends TestCase {
public static function setUpBeforeClass(): void {
if (!defined('STREAMS_PATH')) {
define('STREAMS_PATH', sys_get_temp_dir() . '/xcvm-mon-test/');
}
if (!is_dir(STREAMS_PATH)) {
mkdir(STREAMS_PATH, 0777, true);
}
}
/**
* Invoke a private MonitorCommand method via reflection on a constructor-less
* instance. Works for both instance and (still) static helpers — a static
* method ignores the object argument. The tested helpers are pure (no $this),
* so a bare instance is sufficient.
*/
private function call(string $method, ...$args) {
$m = new ReflectionMethod(MonitorCommand::class, $method);
$m->setAccessible(true);
$instance = (new \ReflectionClass(MonitorCommand::class))->newInstanceWithoutConstructor();
return $m->invoke($instance, ...$args);
}
// ── parseFrameRate (label768/780/1047/1052/1057) ───────────
public function testPlainInteger(): void {
$this->assertSame(30.0, $this->call('parseFrameRate', '30'));
$this->assertSame(25.0, $this->call('parseFrameRate', 25));
}
public function testRationalFrameRate(): void {
$this->assertSame(25.0, $this->call('parseFrameRate', '25/1'));
$this->assertEqualsWithDelta(29.97, $this->call('parseFrameRate', '30000/1001'), 0.001);
}
public function testZeroAndMalformedAreZero(): void {
$this->assertSame(0.0, $this->call('parseFrameRate', ''));
$this->assertSame(0.0, $this->call('parseFrameRate', '0'));
$this->assertSame(0.0, $this->call('parseFrameRate', '0/0'), 'division-by-zero guarded');
$this->assertSame(0.0, $this->call('parseFrameRate', '30/0'), 'zero denominator guarded');
}
// ── isAutoRestartDue (label195 schedule chain) ─────────────
public function testAutoRestartDueWhenDayHourMinuteMatch(): void {
$now = mktime(14, 30, 0, 8, 8, 2026);
// derive the expected components from the same $now -> timezone-agnostic.
$day = date('l', $now);
$at = date('H', $now) . ':' . date('i', $now);
$this->assertTrue($this->call('isAutoRestartDue', ['days' => [$day], 'at' => $at], $now));
}
public function testAutoRestartNotDueOnMismatch(): void {
$now = mktime(14, 30, 0, 8, 8, 2026);
$day = date('l', $now);
$at = date('H', $now) . ':' . date('i', $now);
$this->assertFalse($this->call('isAutoRestartDue', ['days' => ['Nonesuch'], 'at' => $at], $now), 'wrong day');
$this->assertFalse($this->call('isAutoRestartDue', ['days' => [$day], 'at' => ((intval(date('H', $now)) + 1) % 24) . ':' . date('i', $now)], $now), 'wrong hour');
$this->assertFalse($this->call('isAutoRestartDue', ['days' => [$day], 'at' => date('H', $now) . ':' . (((intval(date('i', $now)) + 1) % 60)) ], $now), 'wrong minute');
}
public function testAutoRestartNotDueWhenUnconfigured(): void {
$now = mktime(14, 30, 0, 8, 8, 2026);
$this->assertFalse($this->call('isAutoRestartDue', [], $now));
$this->assertFalse($this->call('isAutoRestartDue', ['days' => [], 'at' => '14:30'], $now), 'empty days');
$this->assertFalse($this->call('isAutoRestartDue', ['days' => ['Saturday']], $now), 'no time');
}
// ── resolveStreamCodecMeta (label562 codec block) ──────────
public function testCodecMetaEmptyOrNonArrayJson(): void {
$this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', '', false));
$this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', null, false));
$this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', '"not-an-array"', false));
}
public function testCodecMetaCompatibleH264Aac(): void {
$json = json_encode(['codecs' => [
'video' => ['codec_name' => 'h264', 'height' => 1080],
'audio' => ['codec_name' => 'aac'],
]]);
[$compat, $audio, $video, $res] = $this->call('resolveStreamCodecMeta', $json, false);
$this->assertSame(1, $compat);
$this->assertSame('aac', $audio);
$this->assertSame('h264', $video);
$this->assertSame(1080, $res);
}
public function testCodecMetaResolutionSnapsAndHevcGated(): void {
$json = json_encode(['codecs' => [
'video' => ['codec_name' => 'h264', 'height' => 700],
'audio' => ['codec_name' => 'aac'],
]]);
$this->assertSame(720, $this->call('resolveStreamCodecMeta', $json, false)[3], '700 -> nearest 720');
$hevc = json_encode(['codecs' => ['video' => ['codec_name' => 'hevc', 'height' => 2160], 'audio' => ['codec_name' => 'ac3']]]);
$this->assertSame(0, $this->call('resolveStreamCodecMeta', $hevc, false)[0], 'hevc not allowed');
$this->assertSame(1, $this->call('resolveStreamCodecMeta', $hevc, true)[0], 'hevc allowed');
}
// ── persistSegmentDuration (label195/label562 probe core) ──
public function testPersistSegmentDurationClampsAndBumps(): void {
[$probe, $seg] = $this->call('persistSegmentDuration', ['of_duration' => 15], 4242, 4);
$this->assertSame(10, $probe['of_duration'], 'clamped to 10');
$this->assertSame(10, $seg, 'segTime bumped to clamped duration');
$this->assertSame('10', file_get_contents(STREAMS_PATH . '4242_.dur'));
}
public function testPersistSegmentDurationShortSegmentBumpsOrKeeps(): void {
[$probe, $seg] = $this->call('persistSegmentDuration', ['of_duration' => 6], 4243, 4);
$this->assertSame(6, $probe['of_duration'], 'not clamped');
$this->assertSame(6, $seg, 'segTime bumped 4 -> 6');
[, $seg2] = $this->call('persistSegmentDuration', ['of_duration' => 6], 4243, 8);
$this->assertSame(8, $seg2, 'segTime kept (already larger)');
}
// ── FPS-drop restart ───────────────────────────────────────
public function testFpsThresholdIsAPercentageOfTheBaseline(): void {
// 90% of 25 fps = 22.5: 22 is a drop, 23 is not.
$this->assertTrue(MonitorCommand::isFpsBelowThreshold(22.0, 25.0, 90));
$this->assertFalse(MonitorCommand::isFpsBelowThreshold(23.0, 25.0, 90));
// The old formula (fps * 90 < baseline) only fired below ~0.28 fps here.
$this->assertTrue(MonitorCommand::isFpsBelowThreshold(12.5, 25.0, 90));
}
public function testFpsThresholdDefaultsTo90AndIsClamped(): void {
$this->assertTrue(MonitorCommand::isFpsBelowThreshold(22.0, 25.0, 0), 'unset → 90%');
$this->assertFalse(MonitorCommand::isFpsBelowThreshold(23.0, 25.0, ''), 'unset → 90%');
$this->assertFalse(MonitorCommand::isFpsBelowThreshold(25.0, 25.0, 250), 'clamped to 100%');
$this->assertFalse(MonitorCommand::isFpsBelowThreshold(10.0, 0.0, 90), 'no baseline, no restart');
}
// ── priority backup ────────────────────────────────────────
public function testPriorityBackupOnlyConsidersHigherRankedSources(): void {
$sources = ['A', 'B', 'C'];
$this->assertSame(['A'], MonitorCommand::higherPrioritySources($sources, 'B'), 'from B only A, never C');
$this->assertSame(['A', 'B'], MonitorCommand::higherPrioritySources($sources, 'C'));
$this->assertSame([], MonitorCommand::higherPrioritySources($sources, 'A'), 'on the primary: nothing to switch back to');
$this->assertSame($sources, MonitorCommand::higherPrioritySources($sources, 'X'), 'unknown current: all are candidates');
}
}