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);
}