Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# 1.8.2 - In Progress

- [Feature] Add USB connection option to sample
- [Feature] Migrate app onto new BLE api
- [Feature] Replaced old android library with multiplatform
- [FIX] Fix CI for desktop sample app
Expand All @@ -10,7 +11,6 @@
- [Refactor] Update project dependencies
- [Refactor] Add BuildFetch


# 1.8.1

- [Feature] Add count subfolders for new file manager
Expand Down
23 changes: 23 additions & 0 deletions components/bridge/connection/live-tests/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("flipper.multiplatform")
id("flipper.multiplatform-dependencies")
id("flipper.anvil")
}

commonDependencies {
implementation(projects.components.core.log)

implementation(projects.components.bridge.connection.pbutils)
implementation(projects.components.bridge.connection.config.api)
implementation(projects.components.bridge.connection.orchestrator.api)
implementation(projects.components.bridge.connection.feature.common.api)
implementation(projects.components.bridge.connection.feature.provider.api)
implementation(projects.components.bridge.connection.feature.rpc.api)
implementation(projects.components.bridge.connection.feature.rpc.model)
implementation(projects.components.bridge.connection.feature.getinfo.api)
implementation(projects.components.bridge.connection.feature.storage.api)
implementation(projects.components.bridge.connection.feature.alarm.api)

implementation(libs.kotlin.coroutines)
implementation(libs.okio)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flipperdevices.bridge.connection.livetests

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.tests.ReconnectLiveTest
import com.flipperdevices.bridge.connection.livetests.tests.SoundLiveTest
import dev.zacsweers.metro.Inject

/**
* Defines which live tests run and in which order.
*
* The reconnect test goes last because it churns the connection
* the other tests rely on.
*/
class LiveTestsSuite @Inject constructor(
rpcLiveTests: RpcLiveTests,
storageLiveTests: StorageLiveTests,
soundLiveTest: SoundLiveTest,
reconnectLiveTest: ReconnectLiveTest
) {
val orderedTests: List<LiveTest> =
rpcLiveTests.orderedTests +
storageLiveTests.orderedTests +
listOf(soundLiveTest, reconnectLiveTest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.flipperdevices.bridge.connection.livetests

import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport
import com.flipperdevices.bridge.connection.livetests.runner.LiveTestSuiteRunner
import com.flipperdevices.bridge.connection.livetests.runner.LogLiveTestReporter
import com.flipperdevices.bridge.connection.orchestrator.api.FDeviceOrchestrator
import com.flipperdevices.bridge.connection.orchestrator.api.model.FDeviceConnectStatus
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.info
import kotlinx.coroutines.flow.first
import dev.zacsweers.metro.Inject
import kotlin.time.TimeSource

/**
* Entry point of the live test suite.
*
* Waits until a Flipper Zero is connected via the orchestrator,
* then runs every live test sequentially and reports results to logs.
*/
class RootLiveTest @Inject constructor(
private val orchestrator: FDeviceOrchestrator,
liveTestsSuite: LiveTestsSuite
) : LogTagProvider {
override val TAG = "RootLiveTest"

private val suiteRunner = LiveTestSuiteRunner(
tests = liveTestsSuite.orderedTests,
reporter = LogLiveTestReporter(),
timeSource = TimeSource.Monotonic
)

suspend fun awaitDeviceAndRunAll(): List<LiveTestReport> {
info { "Waiting for a connected Flipper to start live tests" }
orchestrator.getState().first { status -> status is FDeviceConnectStatus.Connected }
info { "Device connected, starting live tests" }
return suiteRunner.runAll()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.flipperdevices.bridge.connection.livetests

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.tests.DeviceInfoLiveTest
import com.flipperdevices.bridge.connection.livetests.tests.PingLiveTest
import com.flipperdevices.bridge.connection.livetests.tests.PingStressLiveTest
import dev.zacsweers.metro.Inject

class RpcLiveTests @Inject constructor(
pingLiveTest: PingLiveTest,
deviceInfoLiveTest: DeviceInfoLiveTest,
pingStressLiveTest: PingStressLiveTest
) {
val orderedTests: List<LiveTest> = listOf(
pingLiveTest,
deviceInfoLiveTest,
pingStressLiveTest
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.flipperdevices.bridge.connection.livetests

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.tests.FileTransferLiveTest
import com.flipperdevices.bridge.connection.livetests.tests.LargeFileTransferLiveTest
import dev.zacsweers.metro.Inject

class StorageLiveTests @Inject constructor(
fileTransferLiveTest: FileTransferLiveTest,
largeFileTransferLiveTest: LargeFileTransferLiveTest
) {
val orderedTests: List<LiveTest> = listOf(
fileTransferLiveTest,
largeFileTransferLiveTest
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.flipperdevices.bridge.connection.livetests.api

/**
* Single scenario executed against a real Flipper Zero device.
*
* Implementations must be safe to run sequentially inside one connection
* session and must clean up everything they create on the device.
*/
interface LiveTest {
val name: String
val description: String

/**
* @return human-readable details on success or the failure reason
*/
suspend fun execute(): Result<String>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.flipperdevices.bridge.connection.livetests.model

sealed class LiveTestError(message: String) : Exception(message)

class FeatureNotSupportedError(
featureName: String
) : LiveTestError("Feature $featureName is not supported by the connected device")

class LiveTestAssertionError(
message: String
) : LiveTestError(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flipperdevices.bridge.connection.livetests.model

import kotlin.time.Duration

data class LiveTestReport(
val testName: String,
val status: LiveTestStatus,
val duration: Duration
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.flipperdevices.bridge.connection.livetests.model

sealed interface LiveTestStatus {
data class Passed(val details: String) : LiveTestStatus
data class Failed(val error: Throwable) : LiveTestStatus
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flipperdevices.bridge.connection.livetests.runner

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport

interface LiveTestReporter {
fun onTestStarted(test: LiveTest, testNumber: Int, totalCount: Int)
fun onTestFinished(report: LiveTestReport)
fun onSuiteFinished(reports: List<LiveTestReport>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.flipperdevices.bridge.connection.livetests.runner

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport
import com.flipperdevices.bridge.connection.livetests.model.LiveTestStatus
import kotlin.time.TimeSource

/**
* Executes live tests strictly one after another, so tests never
* compete for the single serial connection to the device.
*/
class LiveTestSuiteRunner(
private val tests: List<LiveTest>,
private val reporter: LiveTestReporter,
private val timeSource: TimeSource
) {
private suspend fun runSingleTest(test: LiveTest): LiveTestReport {
val startMark = timeSource.markNow()
val executionResult = runCatching { test.execute() }
.getOrElse { unexpectedError -> Result.failure(unexpectedError) }
val status = executionResult.fold(
onSuccess = { details -> LiveTestStatus.Passed(details) },
onFailure = { testError -> LiveTestStatus.Failed(testError) }
)
return LiveTestReport(
testName = test.name,
status = status,
duration = startMark.elapsedNow()
)
}

suspend fun runAll(): List<LiveTestReport> {
val reports = tests.mapIndexed { index, test ->
reporter.onTestStarted(test, index + 1, tests.size)
val report = runSingleTest(test)
reporter.onTestFinished(report)
report
}
reporter.onSuiteFinished(reports)
return reports
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.flipperdevices.bridge.connection.livetests.runner

import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.LiveTestReport
import com.flipperdevices.bridge.connection.livetests.model.LiveTestStatus
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.core.log.info
import kotlin.time.Duration

class LogLiveTestReporter : LiveTestReporter, LogTagProvider {
override val TAG = "LiveTests"

override fun onTestStarted(test: LiveTest, testNumber: Int, totalCount: Int) {
info { "[$testNumber/$totalCount] ${test.name}: ${test.description}" }
}

override fun onTestFinished(report: LiveTestReport) {
when (val status = report.status) {
is LiveTestStatus.Passed ->
info { "PASS '${report.testName}' in ${report.duration}: ${status.details}" }

is LiveTestStatus.Failed ->
error(status.error) { "FAIL '${report.testName}' in ${report.duration}" }
}
}

override fun onSuiteFinished(reports: List<LiveTestReport>) {
val passedCount = reports.count { report -> report.status is LiveTestStatus.Passed }
val totalDuration = reports.fold(Duration.ZERO) { accumulated, report ->
accumulated + report.duration
}
info { "Live tests finished: $passedCount/${reports.size} passed in $totalDuration" }
reports
.filter { report -> report.status is LiveTestStatus.Failed }
.forEach { report -> info { "Failed test: ${report.testName}" } }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.flipperdevices.bridge.connection.livetests.tests

import com.flipperdevices.bridge.connection.feature.getinfo.api.FGetInfoFeatureApi
import com.flipperdevices.bridge.connection.feature.getinfo.model.FGetInfoApiProperty
import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider
import com.flipperdevices.bridge.connection.feature.provider.api.getSync
import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError
import dev.zacsweers.metro.Inject

class DeviceInfoLiveTest @Inject constructor(
private val featureProvider: FFeatureProvider
) : LiveTest {
override val name = "Device info"
override val description = "Read device name and firmware information over RPC"

override suspend fun execute(): Result<String> {
val getInfoApi = featureProvider.getSync<FGetInfoFeatureApi>()
?: return Result.failure(FeatureNotSupportedError("GET_INFO"))
return getInfoApi.get(FGetInfoApiProperty.DeviceInfo.DEVICE_NAME)
.mapCatching { deviceName ->
val hardwareVersion = getInfoApi
.get(FGetInfoApiProperty.DeviceInfo.HARDWARE_VERSION)
.getOrThrow()
val firmwareBranch = getInfoApi
.get(FGetInfoApiProperty.DeviceInfo.FIRMWARE_BRANCH)
.getOrThrow()
"Device '$deviceName', hardware v$hardwareVersion, firmware branch '$firmwareBranch'"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.flipperdevices.bridge.connection.livetests.tests

import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider
import com.flipperdevices.bridge.connection.feature.provider.api.getSync
import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi
import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError
import dev.zacsweers.metro.Inject

class FileTransferLiveTest @Inject constructor(
private val featureProvider: FFeatureProvider
) : LiveTest {
override val name = "File transfer"
override val description =
"Upload a small random file to the SD card, verify md5, download it back and compare"

override suspend fun execute(): Result<String> {
val storageApi = featureProvider.getSync<FStorageFeatureApi>()
?: return Result.failure(FeatureNotSupportedError("STORAGE"))
return runCatching {
storageApi.transferAndVerify(FILE_SIZE_BYTES, FLIPPER_FILE_PATH)
}
}

companion object {
private const val FILE_SIZE_BYTES = 4096
private const val FLIPPER_FILE_PATH = "/ext/flipper-live-test.bin"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.flipperdevices.bridge.connection.livetests.tests

import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider
import com.flipperdevices.bridge.connection.feature.provider.api.getSync
import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi
import com.flipperdevices.bridge.connection.livetests.api.LiveTest
import com.flipperdevices.bridge.connection.livetests.model.FeatureNotSupportedError
import dev.zacsweers.metro.Inject

class LargeFileTransferLiveTest @Inject constructor(
private val featureProvider: FFeatureProvider
) : LiveTest {
override val name = "Large file transfer"
override val description =
"Upload a 64 KiB random file to stress chunked reads/writes, verify md5 and content"

override suspend fun execute(): Result<String> {
val storageApi = featureProvider.getSync<FStorageFeatureApi>()
?: return Result.failure(FeatureNotSupportedError("STORAGE"))
return runCatching {
storageApi.transferAndVerify(FILE_SIZE_BYTES, FLIPPER_FILE_PATH)
}
}

companion object {
private const val FILE_SIZE_BYTES = 65536
private const val FLIPPER_FILE_PATH = "/ext/flipper-live-test-large.bin"
}
}
Loading
Loading