Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.happyseal.zenplayer.core.model

fun testTrack(
id: Long = 1L,
title: String = "Track $id",
folder: String = "",
albumArtUri: String? = null,
) = Track(
id = id,
fileUri = "content://media/$id",
title = title,
artist = "Artist",
album = "Album",
durationMs = 180_000L,
folder = folder,
albumArtUri = albumArtUri,
)
Original file line number Diff line number Diff line change
@@ -1,121 +1,111 @@
package com.happyseal.zenplayer.features.music.data

import app.cash.turbine.test
import com.happyseal.zenplayer.core.model.Track
import com.happyseal.zenplayer.core.model.testTrack
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runTest

class FallbackAwareMusicRepositoryTest :
BehaviorSpec({
coroutineTestScope = true

fun makeTrack(id: Long) = Track(
id = id,
fileUri = "content://media/$id",
title = "Track $id",
artist = "Artist",
album = "Album",
durationMs = 180_000L,
)

val deviceTracks = listOf(makeTrack(1), makeTrack(2), makeTrack(3))
val sampleTracks = listOf(makeTrack(101), makeTrack(102))
val deviceTracks = listOf(testTrack(id = 1), testTrack(id = 2), testTrack(id = 3))
val sampleTracks = listOf(testTrack(id = 101), testTrack(id = 102))

Given("기기 음원에 정상 접근 가능한 상태") {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = deviceTracks),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

When("음악 목록을 불러오면") {
Then("기기 음원 목록이 반환되고 isFallback이 false다") {
runTest {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = deviceTracks),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

val result = repo.getAllTracks()

result.getOrNull() shouldBe deviceTracks
repo.isFallback.test {
awaitItem() shouldBe false
cancelAndIgnoreRemainingEvents()
}
val result = repo.getAllTracks()

Then("기기 음원 목록이 반환된다") {
result.getOrNull() shouldBe deviceTracks
}

Then("isFallback이 false다") {
repo.isFallback.test {
awaitItem() shouldBe false
cancelAndIgnoreRemainingEvents()
}
}
}
}

Given("기기 음원이 비어 있는 상태") {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = emptyList()),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

When("음악 목록을 불러오면") {
val result = repo.getAllTracks()

Then("내장 기본 음원 목록으로 대체된다") {
runTest {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = emptyList()),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

val result = repo.getAllTracks()

result.getOrNull() shouldBe sampleTracks
repo.isFallback.test {
awaitItem() shouldBe true
cancelAndIgnoreRemainingEvents()
}
result.getOrNull() shouldBe sampleTracks
}

Then("isFallback이 true다") {
repo.isFallback.test {
awaitItem() shouldBe true
cancelAndIgnoreRemainingEvents()
}
}
}
}

Given("기기 음원 접근 중 예외가 발생한 상태") {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(throwOnGetAll = RuntimeException("권한 없음")),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

When("음악 목록을 불러오면") {
val result = repo.getAllTracks()

Then("내장 기본 음원 목록으로 대체된다") {
runTest {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(throwOnGetAll = RuntimeException("권한 없음")),
fallback = FakeMusicRepository(tracks = sampleTracks),
)

val result = repo.getAllTracks()

result.getOrNull() shouldBe sampleTracks
repo.isFallback.test {
awaitItem() shouldBe true
cancelAndIgnoreRemainingEvents()
}
result.getOrNull() shouldBe sampleTracks
}

Then("isFallback이 true다") {
repo.isFallback.test {
awaitItem() shouldBe true
cancelAndIgnoreRemainingEvents()
}
}
}
}

Given("기본 음원 모드인 상태") {
When("특정 곡을 조회하면") {
Then("내장 음원에서 해당 곡을 찾아 반환한다") {
runTest {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = emptyList()),
fallback = FakeMusicRepository(tracks = sampleTracks),
)
repo.getAllTracks() // isFallback = true 로 전환
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = emptyList()),
fallback = FakeMusicRepository(tracks = sampleTracks),
)
repo.getAllTracks() // isFallback = true 로 전환

val result = repo.getTrackById(101L)
When("특정 곡을 조회하면") {
val result = repo.getTrackById(101L)

result.getOrNull() shouldBe sampleTracks[0]
}
Then("내장 음원에서 해당 곡을 찾아 반환한다") {
result.getOrNull() shouldBe sampleTracks[0]
}
}
}

Given("기기 음원 모드인 상태") {
When("특정 곡을 조회하면") {
Then("기기 음원에서 해당 곡을 찾아 반환한다") {
runTest {
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = deviceTracks),
fallback = FakeMusicRepository(tracks = sampleTracks),
)
repo.getAllTracks() // isFallback = false 유지
val repo = FallbackAwareMusicRepository(
primary = FakeMusicRepository(tracks = deviceTracks),
fallback = FakeMusicRepository(tracks = sampleTracks),
)
repo.getAllTracks() // isFallback = false 유지

val result = repo.getTrackById(2L)
When("특정 곡을 조회하면") {
val result = repo.getTrackById(2L)

result.getOrNull() shouldBe deviceTracks[1]
}
Then("기기 음원에서 해당 곡을 찾아 반환한다") {
result.getOrNull() shouldBe deviceTracks[1]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.happyseal.zenplayer.features.player.data

import app.cash.turbine.test
import com.happyseal.zenplayer.core.model.Track
import com.happyseal.zenplayer.core.model.testTrack
import com.happyseal.zenplayer.features.player.domain.PlaylistEvent
import com.happyseal.zenplayer.features.player.domain.RepeatMode
import io.kotest.assertions.throwables.shouldThrow
Expand All @@ -15,19 +15,7 @@ class PlayQueueManagerTest :

coroutineTestScope = true

fun makeTrack(
id: Long,
title: String,
) = Track(
id = id,
fileUri = "content://media/$id",
title = title,
artist = "Artist",
album = "Album",
durationMs = 180_000L,
)

fun makeTracks(count: Int) = (1..count).map { makeTrack(it.toLong(), "Track $it") }
fun makeTracks(count: Int) = (1..count).map { testTrack(id = it.toLong()) }

fun makeManager(
audioController: FakeAudioController = FakeAudioController(),
Expand Down Expand Up @@ -113,38 +101,36 @@ class PlayQueueManagerTest :
}
}

When("셔플을 켜면(seed 기반)") {
val seed = 42L
val manager = makeManager(random = Random(seed))
When("셔플을 켠 상태에서 5번 playNext()를 호출하면") {
val manager = makeManager()
val tracks = makeTracks(5)
manager.setPlayQueue(tracks, startIndex = 0)
manager.toggleShuffle()
val visited = mutableListOf(manager.playQueue.value.shouldNotBeNull().currentIndex)
repeat(4) {
manager.playNext()
visited.add(manager.playQueue.value.shouldNotBeNull().currentIndex)
}

Then("재생 순서가 seed 기반 무작위 순서로 바뀐다") {
// 같은 seed로 예상 순서 계산
val expectedOrder = buildList {
val rest = (1..4).toMutableList()
rest.shuffle(Random(seed))
add(0)
addAll(rest)
}
Then("모든 곡이 정확히 한 번씩 방문된다") {
visited.toSet().size shouldBe 5
}
}
}

// playNext()로 순서대로 방문
val visitedIndices = mutableListOf(
manager.playQueue.value
.shouldNotBeNull()
.currentIndex,
)
repeat(4) {
manager.playNext()
visitedIndices.add(
manager.playQueue.value
.shouldNotBeNull()
.currentIndex,
)
}
Given("셔플 ON 상태에서 재생 중일 때") {
val manager = makeManager(random = Random(seed = 42L))
val tracks = makeTracks(5)
manager.setPlayQueue(tracks, startIndex = 0)
manager.toggleShuffle()
manager.playNext() // 셔플 순서로 이동

When("셔플을 OFF로 토글하면") {
val indexBeforeToggle = manager.playQueue.value.shouldNotBeNull().currentIndex
manager.toggleShuffle()

visitedIndices shouldBe expectedOrder
Then("currentIndex가 셔플 전 재생 중이던 트랙의 원래 인덱스를 유지한다") {
manager.playQueue.value?.currentIndex shouldBe indexBeforeToggle
}
}
}
Expand Down Expand Up @@ -191,26 +177,26 @@ class PlayQueueManagerTest :

Given("재생 큐가 없는 초기 상태") {
When("playNext()를 호출하면") {
val manager = makeManager()
Then("IllegalStateException이 발생한다") {
val manager = makeManager()
shouldThrow<IllegalStateException> {
manager.playNext()
}
}
}

When("playPrevious()를 호출하면") {
val manager = makeManager()
Then("IllegalStateException이 발생한다") {
val manager = makeManager()
shouldThrow<IllegalStateException> {
manager.playPrevious()
}
}
}

When("playCurrent()를 호출하면") {
val manager = makeManager()
Then("IllegalStateException이 발생한다") {
val manager = makeManager()
shouldThrow<IllegalStateException> {
manager.playCurrent()
}
Expand Down
Loading
Loading