|
| 1 | +"""Unit tests for aom.onboarding's test-video stop guard. |
| 2 | +
|
| 3 | +The 5s stop in ``play_test_video`` fires on wall time, not on a playback |
| 4 | +handle, so it must verify the bundled test video is still the playing item |
| 5 | +before stopping (a blind ``Player().stop()`` could kill playback the user |
| 6 | +started inside the window — the legacy behavior). |
| 7 | +
|
| 8 | +Kodi is faked via Kodistubs; ``xbmc.Player`` is monkeypatched with a |
| 9 | +scriptable fake so the guard's decision is pinned without real playback. |
| 10 | +""" |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +import resources.lib.aom.onboarding as onboarding |
| 15 | + |
| 16 | + |
| 17 | +class FakePlayer: |
| 18 | + """Scriptable xbmc.Player: what is playing (or a raise), and the stops.""" |
| 19 | + |
| 20 | + def __init__(self): |
| 21 | + self.playing_file = None # None = nothing playing |
| 22 | + self.raises = False # getPlayingFile raises (stop race) |
| 23 | + self.played = [] |
| 24 | + self.stopped = 0 |
| 25 | + |
| 26 | + def play(self, path): |
| 27 | + self.played.append(path) |
| 28 | + self.playing_file = path |
| 29 | + |
| 30 | + def isPlaying(self): |
| 31 | + return self.raises or self.playing_file is not None |
| 32 | + |
| 33 | + def getPlayingFile(self): |
| 34 | + if self.raises: |
| 35 | + raise RuntimeError('XBMCAddon: no file playing') |
| 36 | + return self.playing_file |
| 37 | + |
| 38 | + def stop(self): |
| 39 | + self.stopped += 1 |
| 40 | + self.playing_file = None |
| 41 | + |
| 42 | + |
| 43 | +TEST_PATH = 'addons/script.audiooffsetmanager/resources/media/test-video.mp4' |
| 44 | + |
| 45 | + |
| 46 | +@pytest.fixture |
| 47 | +def rig(monkeypatch): |
| 48 | + """(onboarding instance, fake player): one shared player instance so the |
| 49 | + guard sees the same state play() produced.""" |
| 50 | + player = FakePlayer() |
| 51 | + monkeypatch.setattr(onboarding.xbmc, 'Player', lambda: player) |
| 52 | + onb = onboarding._Onboarding() |
| 53 | + onb._test_video_path = TEST_PATH |
| 54 | + return onb, player |
| 55 | + |
| 56 | + |
| 57 | +class TestStopGuard: |
| 58 | + |
| 59 | + def test_stops_while_test_video_still_playing(self, rig): |
| 60 | + onb, player = rig |
| 61 | + player.playing_file = TEST_PATH |
| 62 | + onb._stop_if_still_test_video() |
| 63 | + assert player.stopped == 1 |
| 64 | + |
| 65 | + def test_path_comparison_is_separator_normalized(self, rig): |
| 66 | + # Kodi may report the path with redundant or mixed separators; the |
| 67 | + # comparison is normpath/normcase-based, not string equality. |
| 68 | + onb, player = rig |
| 69 | + player.playing_file = TEST_PATH.replace('/resources/', |
| 70 | + '//resources/./') |
| 71 | + onb._stop_if_still_test_video() |
| 72 | + assert player.stopped == 1 |
| 73 | + |
| 74 | + def test_leaves_other_playback_alone(self, rig): |
| 75 | + # The user started something else inside the 5s window: never stop it. |
| 76 | + onb, player = rig |
| 77 | + player.playing_file = 'videodb://movies/titles/42' |
| 78 | + onb._stop_if_still_test_video() |
| 79 | + assert player.stopped == 0 |
| 80 | + |
| 81 | + def test_no_stop_when_nothing_is_playing(self, rig): |
| 82 | + # The test video failed to open (or already ended): nothing to stop. |
| 83 | + onb, player = rig |
| 84 | + player.playing_file = None |
| 85 | + onb._stop_if_still_test_video() |
| 86 | + assert player.stopped == 0 |
| 87 | + |
| 88 | + def test_raise_reads_as_not_ours(self, rig): |
| 89 | + # isPlaying/getPlayingFile can race a natural stop; a raise must be |
| 90 | + # treated as "not our video", never propagate out of the script. |
| 91 | + onb, player = rig |
| 92 | + player.raises = True |
| 93 | + onb._stop_if_still_test_video() |
| 94 | + assert player.stopped == 0 |
| 95 | + |
| 96 | + |
| 97 | +class TestPlayFlowWiring: |
| 98 | + |
| 99 | + def test_happy_path_plays_then_stops_the_test_video(self, rig, monkeypatch): |
| 100 | + # The full flow still stops the test video when it is (still) the |
| 101 | + # playing item — the guard replaced the blind stop, not the stop. |
| 102 | + onb, player = rig |
| 103 | + monkeypatch.setattr(onboarding.xbmcvfs, 'exists', lambda path: True) |
| 104 | + monkeypatch.setattr(onboarding.xbmc, 'sleep', lambda ms: None) |
| 105 | + monkeypatch.setattr(onboarding.xbmc, 'executebuiltin', lambda cmd: None) |
| 106 | + |
| 107 | + onb.play_test_video() |
| 108 | + |
| 109 | + assert player.played == [onb._test_video_path] |
| 110 | + assert player.stopped == 1 |
| 111 | + |
| 112 | + def test_flow_spares_playback_started_inside_the_window(self, rig, |
| 113 | + monkeypatch): |
| 114 | + # Regression pin for the review finding: the user starts a different |
| 115 | + # item during the 5s wait; the flow must not stop it. |
| 116 | + onb, player = rig |
| 117 | + monkeypatch.setattr(onboarding.xbmcvfs, 'exists', lambda path: True) |
| 118 | + monkeypatch.setattr( |
| 119 | + onboarding.xbmc, 'sleep', |
| 120 | + lambda ms: player.play('videodb://movies/titles/42')) |
| 121 | + monkeypatch.setattr(onboarding.xbmc, 'executebuiltin', lambda cmd: None) |
| 122 | + |
| 123 | + onb.play_test_video() |
| 124 | + |
| 125 | + assert player.stopped == 0 |
| 126 | + assert player.playing_file == 'videodb://movies/titles/42' |
0 commit comments