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
44 changes: 43 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Build debug APK
run: ./gradlew assembleDebug

# No emulator in CI, but compiling keeps the instrumented suite from rotting.
# Fast compile check; the `instrumented` job below runs them on an emulator.
- name: Compile instrumented tests
run: ./gradlew assembleDebugAndroidTest

Expand All @@ -47,3 +47,45 @@ jobs:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
retention-days: 7

instrumented:
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew

# Hardware acceleration for the emulator on GitHub's Linux runners.
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run instrumented tests on emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
disable-animations: true
script: ./gradlew connectedDebugAndroidTest

- name: Upload instrumented test report
if: always()
uses: actions/upload-artifact@v4
with:
name: instrumented-test-report
path: app/build/reports/androidTests/connected/
retention-days: 7

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun completeWorkflow_saveQuizAndProgress() = runBlocking {
fun completeWorkflow_saveQuizAndProgress(): Unit = runBlocking {
val topicId = "ohms-law"

// Create initial progress
Expand Down Expand Up @@ -87,7 +87,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun multipleTopics_isolation() = runBlocking {
fun multipleTopics_isolation(): Unit = runBlocking {
// Insert data for two topics
database.progressDao().upsertProgress(TopicProgress("topic1", xpEarned = 100))
database.progressDao().upsertProgress(TopicProgress("topic2", xpEarned = 200))
Expand All @@ -110,7 +110,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun totalXp_aggregation_acrossTopics() = runBlocking {
fun totalXp_aggregation_acrossTopics(): Unit = runBlocking {
database.progressDao().upsertProgress(TopicProgress("topic1", xpEarned = 100))
database.progressDao().upsertProgress(TopicProgress("topic2", xpEarned = 150))
database.progressDao().upsertProgress(TopicProgress("topic3", xpEarned = 250))
Expand All @@ -121,7 +121,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun progressionFlow_unlocking() = runBlocking {
fun progressionFlow_unlocking(): Unit = runBlocking {
val topic1 = "ohms-law"
val topic2 = "series-parallel"

Expand Down Expand Up @@ -152,7 +152,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun bestScoreTracking_workflow() = runBlocking {
fun bestScoreTracking_workflow(): Unit = runBlocking {
val topicId = "test-topic"

// First attempt
Expand All @@ -175,7 +175,7 @@ class AppDatabaseIntegrationTest {
}

@Test
fun reactiveUpdates_progressFlow() = runBlocking {
fun reactiveUpdates_progressFlow(): Unit = runBlocking {
val topicId = "reactive-test"

// Insert initial
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun insertAndRetrieve_success() = runBlocking {
fun insertAndRetrieve_success(): Unit = runBlocking {
val progress = TopicProgress(
topicId = "ohms-law",
lessonCompleted = true,
Expand All @@ -55,7 +55,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun upsert_updates_existingProgress() = runBlocking {
fun upsert_updates_existingProgress(): Unit = runBlocking {
val initial = TopicProgress(
topicId = "series-parallel",
lessonCompleted = false,
Expand All @@ -81,7 +81,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun getAllProgress_returnsAll() = runBlocking {
fun getAllProgress_returnsAll(): Unit = runBlocking {
progressDao.upsertProgress(TopicProgress("topic1", lessonCompleted = true))
progressDao.upsertProgress(TopicProgress("topic2", lessonCompleted = false))
progressDao.upsertProgress(TopicProgress("topic3", lessonCompleted = true))
Expand All @@ -95,7 +95,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun getTotalXp_aggregatesCorrectly() = runBlocking {
fun getTotalXp_aggregatesCorrectly(): Unit = runBlocking {
progressDao.upsertProgress(TopicProgress("topic1", xpEarned = 100))
progressDao.upsertProgress(TopicProgress("topic2", xpEarned = 150))
progressDao.upsertProgress(TopicProgress("topic3", xpEarned = 250))
Expand All @@ -106,14 +106,14 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun getTotalXp_emptyDatabase_returnsZero() = runBlocking {
fun getTotalXp_emptyDatabase_returnsZero(): Unit = runBlocking {
val total = progressDao.getTotalXp().first()

assertEquals(0, total)
}

@Test
fun flowReactivity_updatesOnChange() = runBlocking {
fun flowReactivity_updatesOnChange(): Unit = runBlocking {
val topicId = "kirchhoffs"

progressDao.upsertProgress(TopicProgress(topicId, xpEarned = 100))
Expand All @@ -127,7 +127,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun multipleUpdates_bestScoreTracking() = runBlocking {
fun multipleUpdates_bestScoreTracking(): Unit = runBlocking {
val topicId = "capacitors"

progressDao.upsertProgress(TopicProgress(topicId, bestScore = 6, xpEarned = 100))
Expand All @@ -141,7 +141,7 @@ class ProgressDaoInstrumentedTest {
}

@Test
fun nonExistent_returnsNull() = runBlocking {
fun nonExistent_returnsNull(): Unit = runBlocking {
val result = progressDao.getProgressOnce("non-existent-topic")

assertNull(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun insertResult_success() = runBlocking {
fun insertResult_success(): Unit = runBlocking {
val result = QuizResult(
topicId = "ohms-law",
score = 9,
Expand All @@ -53,21 +53,20 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun multipleInserts_allRetrieved() = runBlocking {
fun multipleInserts_allRetrieved(): Unit = runBlocking {
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 8, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 9, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 7, totalQuestions = 10))

val results = quizResultDao.getResultsForTopic("topic1").first()

// Inserts in the same millisecond share a timestamp, so order is unspecified.
assertEquals(3, results.size)
assertEquals(8, results[0].score)
assertEquals(9, results[1].score)
assertEquals(7, results[2].score)
assertEquals(listOf(7, 8, 9), results.map { it.score }.sorted())
}

@Test
fun getResultsForTopic_orderedByTimestampDesc() = runBlocking {
fun getResultsForTopic_orderedByTimestampDesc(): Unit = runBlocking {
val timestamp1 = System.currentTimeMillis()
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 8, totalQuestions = 10, timestamp = timestamp1))

Expand All @@ -87,7 +86,7 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun getBestScore_returnsMax() = runBlocking {
fun getBestScore_returnsMax(): Unit = runBlocking {
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 6, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 9, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 8, totalQuestions = 10))
Expand All @@ -98,14 +97,14 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun getBestScore_noResults_returnsNull() = runBlocking {
fun getBestScore_noResults_returnsNull(): Unit = runBlocking {
val best = quizResultDao.getBestScore("non-existent").first()

assertNull(best)
}

@Test
fun getResultsForTopic_differentTopics_isolated() = runBlocking {
fun getResultsForTopic_differentTopics_isolated(): Unit = runBlocking {
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 8, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic1", score = 9, totalQuestions = 10))
quizResultDao.insertResult(QuizResult(topicId = "topic2", score = 7, totalQuestions = 10))
Expand All @@ -121,7 +120,7 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun autoIncrement_id_generated() = runBlocking {
fun autoIncrement_id_generated(): Unit = runBlocking {
val result1 = QuizResult(topicId = "topic1", score = 8, totalQuestions = 10)
val result2 = QuizResult(topicId = "topic1", score = 9, totalQuestions = 10)

Expand All @@ -137,14 +136,14 @@ class QuizResultDaoInstrumentedTest {
}

@Test
fun getResults_emptyTopic_returnsEmpty() = runBlocking {
fun getResults_emptyTopic_returnsEmpty(): Unit = runBlocking {
val results = quizResultDao.getResultsForTopic("empty-topic").first()

assertEquals(0, results.size)
}

@Test
fun insertResult_preservesData_integrity() = runBlocking {
fun insertResult_preservesData_integrity(): Unit = runBlocking {
val result = QuizResult(
topicId = "test-topic",
score = 95,
Expand Down
Loading
Loading