run method

Each case shares one deadline, including creation and every operation. Cleanup has its own total budget, split between release, subscription cancellation and disposal so a hung release cannot prevent disposal. Late create results receive the same bounded cleanup. Late errors are observed without retaining or interpolating raw exception objects.

Implementation

Future<List<YlConformanceFailure>> run() async {
  if (caseTimeout <= Duration.zero || cleanupTimeout <= Duration.zero) {
    throw ArgumentError('Conformance deadlines must be positive.');
  }
  final failures = <YlConformanceFailure>[];
  final cases = <String, Future<void> Function(_Case)>{
    'lifecycle': (c) async {
      final p = c.player;
      _require(p.implementation.spiMajor == ylPlayerSpiMajor);
      validateYlPlayerCapabilities(p.capabilities);
      validateYlPlayerState(p.state);
      _require(
        p.state.sessionId == null && p.state.status == YlPlaybackStatus.idle,
      );
      _require(p.textureId.value == null || p.textureId.value! >= 0);
      final before = p.state;
      final assessment = await c.step(() => p.assess(fixture.source));
      _require(assessment.outcome != YlSourceAssessmentOutcome.incompatible);
      _require(p.state == before);
      await c.step(() => p.setVolume(0.5));
      await c.step(p.stop);
    },
    'load-identity': (c) async {
      final first = await _load(c);
      final second = await _load(c);
      _require(first.sessionId != second.sessionId);
    },
    'immediate-play': (c) async {
      final result = await _load(c);
      _require(c.player.state.status == YlPlaybackStatus.buffering);
      _require(c.events.whereType<YlFirstFrameEvent>().isEmpty);
      // No frame pump, Ready injection or stream wait between load and play.
      await c.step(() => c.player.play(result.sessionId));
    },
    'newer-load-cancellation': (c) async {
      await c.step(() => fixture.holdNextLoad(c.player));
      final pending = c.observe(() => c.player.load(fixture.source));
      final newer = await _load(c);
      await c.rejected(pending, YlFailureCodes.loadCancelled);
      await c.step(() => fixture.releaseHeldLoad(c.player));
      _require(c.player.state.sessionId == newer.sessionId);
    },
    'stale-session': (c) async {
      final old = await _load(c);
      final current = await _load(c);
      final before = c.player.state;
      for (final command in _sessionCommands(c.player, old.sessionId)) {
        await c.rejected(c.observe(command), YlFailureCodes.sessionStale);
      }
      _require(c.player.state == before);
      await c.step(() => c.player.play(current.sessionId));
    },
    'stop': (c) async {
      final result = await _load(c);
      await c.step(c.player.stop);
      // Rejection is immediate after acceptance; native idle may arrive
      // independently of the reply, within this case's existing deadline.
      await c.rejected(
        c.observe(() => c.player.play(result.sessionId)),
        YlFailureCodes.sessionStale,
      );
      await c.waitFor(
        () =>
            c.player.state.sessionId == null &&
            c.player.state.status == YlPlaybackStatus.idle,
      );
      await c.step(() => c.player.setVolume(0.5));
      final next = await _load(c);
      _require(next.sessionId != result.sessionId);
    },
    'revision-order': (c) async {
      final result = await _load(c);
      await c.step(() => fixture.emitReady(c.player, result.sessionId));
      _require(c.player.state.status == YlPlaybackStatus.ready);
      await c.step(() => c.player.play(result.sessionId));
      await c.step(() => c.player.pause(result.sessionId));
      await c.step(c.player.stop);
      await _load(c);
      _require(c.snapshots.isNotEmpty);
      var revision = c.initialRevision;
      for (final state in c.snapshots) {
        validateYlPlayerState(state);
        _require(state.revision > revision);
        revision = state.revision;
      }
      _require(c.player.state.revision == revision);
    },
    'first-frame-correlation': (c) async {
      final old = await _load(c);
      final current = await _load(c);
      await c.step(() => fixture.emitReady(c.player, current.sessionId));
      final timeline = c.player.state.timeline;
      if (timeline.isSeekable &&
          c.player.capabilities.supportedOperations.contains(
            YlPlayerOperation.seek,
          )) {
        final window = timeline.dvrWindow;
        final position = window == null
            ? Duration.zero
            : window.start + (window.end - window.start) ~/ 2;
        await c.step(() => c.player.seekTo(current.sessionId, position));
      } else {
        await c.step(() => c.player.play(current.sessionId));
      }
      final before = c.player.state;
      await c.step(() => fixture.emitFirstFrame(c.player, old.sessionId));
      _require(c.events.whereType<YlFirstFrameEvent>().isEmpty);
      await c.step(() => fixture.emitFirstFrame(c.player, current.sessionId));
      await c.waitFor(
        () => c.events.whereType<YlFirstFrameEvent>().isNotEmpty,
      );
      await c.step(() => fixture.emitFirstFrame(c.player, current.sessionId));
      final frames = c.events.whereType<YlFirstFrameEvent>().toList();
      _require(
        frames.length == 1 && frames.single.sessionId == current.sessionId,
      );
      _require(frames.single.revision <= before.revision);
      _require(
        c.player.state.revision >= before.revision &&
            c.player.state.sessionId == current.sessionId,
      );
    },
    'terminal-failure': (c) async {
      final result = await _load(c);
      await c.step(
        () => fixture.emitTerminalFailure(c.player, result.sessionId),
      );
      await c.waitFor(
        () => c.events.whereType<YlPlaybackFailedEvent>().isNotEmpty,
      );
      await c.step(
        () => fixture.emitTerminalFailure(c.player, result.sessionId),
      );
      final errors = c.events.whereType<YlPlaybackFailedEvent>().toList();
      _require(
        errors.length == 1 && errors.single.sessionId == result.sessionId,
      );
      _require(
        c.player.state.status == YlPlaybackStatus.failed &&
            c.player.state.failure == errors.single.failure,
      );
    },
    'safe-public-strings': (c) async {
      final assessment = await c.step(() => c.player.assess(fixture.source));
      final result = await _load(c);
      _checkStrings(c, result, assessment);
      await c.step(
        () => fixture.emitTerminalFailure(c.player, result.sessionId),
      );
      _checkStrings(c, result, assessment);
    },
    'stop-held-load': (c) => _invalidateHeld(c, dispose: false),
    'dispose-held-load': (c) => _invalidateHeld(c, dispose: true),
    'double-disposal': (c) async {
      await c.step(c.player.dispose);
      await c.step(c.player.dispose);
      await c.rejected(
        c.observe(() => c.player.load(fixture.source)),
        YlFailureCodes.playerDisposed,
      );
      await c.rejected(
        c.observe(() => c.player.setVolume(0.5)),
        YlFailureCodes.playerDisposed,
      );
    },
  };
  // Snapshot fixture declarations: subsequent list mutation cannot change a run.
  try {
    final policies = List<YlConformancePolicyCase>.unmodifiable(
      fixture.policyCases,
    );
    if (!policies.any(
      (p) => p.expected == YlConformancePolicyOutcome.success,
    )) {
      failures.add(_failure('policy-fixtures'));
    }
    for (var i = 0; i < policies.length; i++) {
      final policy = policies[i];
      cases['policy-$i'] = (c) => _policy(c, policy);
    }
  } on Object {
    failures.add(_failure('policy-fixtures'));
  }
  for (final entry in cases.entries) {
    final context = _Case(fixture, caseTimeout, cleanupTimeout);
    try {
      await context.create();
      await entry.value(context);
      context.check();
    } on TimeoutException {
      failures.add(_failure(entry.key, timeout: true));
    } on Object {
      failures.add(_failure(entry.key));
    } finally {
      if (!await context.cleanup()) {
        failures.add(_failure('${entry.key}.cleanup'));
      }
    }
  }
  return List.unmodifiable(failures);
}