refactor(domain): make core:domain multiplatform so iOS can reuse it
The orbital maths, the satellite models and the repository contracts sat in a Kotlin/JVM module, so an iOS target could not share a single line of them: java.lang.String.format, InputStream, System.currentTimeMillis, java.util.Locale and org.json are all JVM-only, and the tests that covered them used JUnit4. core:domain now declares jvm, iosArm64 and iosSimulatorArm64 targets, its sources moved to commonMain/commonTest, and the JVM-only pieces were replaced with multiplatform equivalents: java.lang.String.format by a shared printf implementation, System.currentTimeMillis by kotlin.time.Clock, InputStream by ByteArray, org.json by kotlinx-serialization, Locale by nothing at all. Tests that read classpath resources (javaClass.classLoader) moved to jvmTest, because that is JVM-only behaviour rather than a JVM-only API. Auditing the migration against the old module turned up four things that were wrong rather than merely ported: - java.lang.String.format rounds the shortest decimal representation of a double half-up, not the binary value: "%.3f" of 0.5005 is "0.501", because the stored double is 0.50049999999999994493. The shared implementation scaled in binary first and printed "0.500", which would have changed APRS position packets and the Wavelog frequency fields against the released Android app. It now takes the digits from the decimal representation and rounds them with integer arithmetic, and jvmTest compares it against String.format(Locale.ROOT, ...) over 40 000 sampled doubles plus the boundary cases, while commonTest pins literals so the iOS run checks the same digits. - The queue mutators lost the kotlin.jvm.Synchronized monitor each of them had. It is not a JVM-only annotation - it is an optional expectation, so it still compiles in common code - but the stdlib deprecated it for common use in 1.8 and made it an error in 2.1. The monitor is a platform actual now: the JVM keeps the real monitor, since Compose and the upload coroutine both reach the queue there, and iOS carries a documented placeholder until the iOS side has a second thread to protect against. - 107 assertions in DataParserTest and QthConverterTest were bare kotlin.assert calls, which a build without -ea skips silently: they are assertTrue now, so the iOS run cannot pass vacuously. The three Locale.setDefault cases (ar-EG, bn-BD, fa-IR) that used to guard APRS output against Eastern Arabic digits moved to jvmTest instead of being deleted with the Locale dependency - APRS-IS is an ASCII protocol, and Locale.setDefault does not exist on iOS. - @Volatile on the LoTW name cache would not have compiled for iOS either: kotlin.jvm's variant is an error in common code since 2.1. kotlin.concurrent.Volatile is the multiplatform annotation, and it is the stronger form: it takes effect on Kotlin/Native rather than being ignored. A second audit pass over the files the first one could not reach - the HTTP client, the parsers, the queue and the injection - found three more: - OkHttpHttpClient built its Request outside the try, so a URL OkHttp refuses to parse left postQso/testToken/getStation as an exception, and neither caller catches one. The client it replaced reported HTTP -1 and let the caller treat it as a failure; building the request inside the try restores that, and a transport failure reports -1 again rather than 0. - WavelogQueue's readers were stricter than the org.json ones they replaced. A timestamp stored as 1234.0 (or "1234.0") read back as 0L instead of 1234 - a QSO uploaded as 1970 - and a field holding an object or array threw the whole list away instead of falling back. The readers coerce decimals, keep the old defaults and no longer throw, matching optLong, optInt, optString and optBoolean. - The ADIF dates went through the JVM default locale before, so a device set to Arabic wrote Eastern Arabic digits into the QSO date. The shared formatter only ever produces ASCII, which the locale cases in AprsPacketDefaultLocaleTest pin down. Verified locally with ./gradlew jvmTest (343 tests, 0 failures) and the multiplatform gate in check-multiplatform.sh, which now also refuses JVM-only stdlib APIs that resolve in common code but fail to compile for iOS: @Synchronized, kotlin.jvm.Volatile, synchronized(), toUpperCase/toLowerCase/capitalize, BigDecimal. The iOS targets themselves need the macOS runner in .github/workflows/ios-kmp.yml.
This commit is contained in:
1 parent
abb73ffef7
commit
1a3c94f1e0
120 files changed
+1794
-925
No files matched your search
@@ -36,6 +36,7 @@ import com.rtbishop.look4sat.core.data.repository.SelectionRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SensorsRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SettingsRepo
|
||||
import com.rtbishop.look4sat.core.data.source.LocalSource
|
||||
import com.rtbishop.look4sat.core.data.source.OkHttpHttpClient
|
||||
import com.rtbishop.look4sat.core.data.source.RemoteSource
|
||||
import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
|
||||
import com.rtbishop.look4sat.core.data.usecase.AudioCapture
|
||||
@@ -73,11 +74,21 @@ import com.rtbishop.look4sat.core.data.qrz.QrzGridLookup
|
||||
import com.rtbishop.look4sat.core.domain.qrz.IQrzGridLookup
|
||||
import okhttp3.OkHttpClient
|
||||
import com.rtbishop.look4sat.core.data.wavelog.LotwSatellitesRepo
|
||||
import com.rtbishop.look4sat.core.domain.wavelog.WaveLogApi
|
||||
|
||||
class MainContainer(private val context: Context) : IMainContainer {
|
||||
|
||||
private val localSource = provideLocalSource()
|
||||
private val remoteSource by lazy { provideRemoteSource() }
|
||||
|
||||
/**
|
||||
* WaveLogApi is a plain object in core:domain, and shared code has no socket API of its own on
|
||||
* iOS, so the container hands it the platform client. Its requests used to be made by an
|
||||
* HttpURLConnection built inside WaveLogApi, which only ever existed on the JVM.
|
||||
*/
|
||||
init {
|
||||
WaveLogApi.installHttpClient(OkHttpHttpClient(OkHttpClient.Builder().build()))
|
||||
}
|
||||
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
|
||||
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
|
||||
override val settingsRepo = provideSettingsRepo()
|
||||
|
||||
+24
-25
@@ -29,7 +29,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.InputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
class DatabaseRepo(
|
||||
@@ -55,8 +55,8 @@ class DatabaseRepo(
|
||||
|
||||
override suspend fun updateTLEFromFile(uri: String): Int = withContext(dispatcher) {
|
||||
var importedCount = 0
|
||||
remoteSource.getFileStream(uri)?.let { stream ->
|
||||
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
|
||||
remoteSource.getFileBytes(uri)?.let { data ->
|
||||
val entries = parseSatelliteData(uri, unwrapIfZipped(uri, data))
|
||||
localSource.insertEntries(entries)
|
||||
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
|
||||
importedCount = entries.size
|
||||
@@ -67,8 +67,8 @@ class DatabaseRepo(
|
||||
|
||||
override suspend fun updateTransceiversFromFile(uri: String): Int = withContext(dispatcher) {
|
||||
var importedCount = 0
|
||||
remoteSource.getFileStream(uri)?.let { stream ->
|
||||
val transceivers = dataParser.parseJSONStream(unwrapIfZipped(uri, stream))
|
||||
remoteSource.getFileBytes(uri)?.let { data ->
|
||||
val transceivers = dataParser.parseJSON(unwrapIfZipped(uri, data).decodeToString())
|
||||
localSource.insertRadios(transceivers)
|
||||
importedCount = transceivers.size
|
||||
}
|
||||
@@ -105,8 +105,8 @@ class DatabaseRepo(
|
||||
Sources.transceiversDataUrls.filterValues { it.isNotBlank() }
|
||||
}
|
||||
// launch all network requests concurrently
|
||||
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkBytes(url) } }
|
||||
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkBytes(url) } }
|
||||
val tleResults = tleJobs.awaitAll()
|
||||
val radioResults = radioJobs.awaitAll()
|
||||
// Orbital elements are counted on their own. A combined count let a successful transceivers
|
||||
@@ -120,14 +120,14 @@ class DatabaseRepo(
|
||||
throw java.io.IOException("No orbital data source could be downloaded")
|
||||
}
|
||||
// parse fetched data concurrently and associate with types
|
||||
val importedEntries = tleResults.flatMap { (url, stream) ->
|
||||
val importedEntries = tleResults.flatMap { (url, data) ->
|
||||
val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType
|
||||
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
|
||||
data?.let { parseSatelliteData(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
|
||||
settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum })
|
||||
}
|
||||
}
|
||||
val importedRadios = radioResults.flatMap { (url, stream) ->
|
||||
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
|
||||
val importedRadios = radioResults.flatMap { (url, data) ->
|
||||
data?.let { dataParser.parseJSON(unwrapIfZipped(url, it).decodeToString()) }.orEmpty()
|
||||
}
|
||||
// insert parsed data into the database
|
||||
localSource.insertEntries(importedEntries)
|
||||
@@ -141,11 +141,11 @@ class DatabaseRepo(
|
||||
setUpdateSuccessful(0L)
|
||||
}
|
||||
|
||||
private suspend fun parseSatelliteStream(url: String, stream: InputStream): List<OrbitalData> {
|
||||
val bufferedStream = stream.buffered()
|
||||
private suspend fun parseSatelliteData(url: String, data: ByteArray): List<OrbitalData> {
|
||||
val text = data.decodeToString()
|
||||
return when {
|
||||
hasCsvHint(url) || looksLikeCsv(bufferedStream) -> dataParser.parseCSVStream(bufferedStream)
|
||||
else -> dataParser.parseTLEStream(bufferedStream)
|
||||
hasCsvHint(url) || looksLikeCsv(text) -> dataParser.parseCSV(text)
|
||||
else -> dataParser.parseTLE(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,14 +155,9 @@ class DatabaseRepo(
|
||||
url.endsWith(".csv.zip", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun looksLikeCsv(stream: InputStream): Boolean {
|
||||
if (!stream.markSupported()) return false
|
||||
stream.mark(4096)
|
||||
val preview = ByteArray(4096)
|
||||
val length = stream.read(preview)
|
||||
stream.reset()
|
||||
if (length <= 0) return false
|
||||
val line = preview.decodeToString(0, length).lineSequence().firstOrNull()?.trim().orEmpty()
|
||||
private fun looksLikeCsv(text: String): Boolean {
|
||||
val line = text.lineSequence().firstOrNull()?.trim().orEmpty()
|
||||
if (line.isEmpty()) return false
|
||||
return line.contains("OBJECT_NAME", ignoreCase = true) ||
|
||||
line.contains("NORAD_CAT_ID", ignoreCase = true) ||
|
||||
line.count { it == ',' } >= 4
|
||||
@@ -174,6 +169,10 @@ class DatabaseRepo(
|
||||
)
|
||||
}
|
||||
|
||||
private fun unwrapIfZipped(url: String, stream: InputStream): InputStream =
|
||||
if (url.endsWith(".zip", ignoreCase = true)) ZipInputStream(stream).apply { nextEntry } else stream
|
||||
private fun unwrapIfZipped(url: String, data: ByteArray): ByteArray =
|
||||
if (url.endsWith(".zip", ignoreCase = true)) {
|
||||
ZipInputStream(ByteArrayInputStream(data)).apply { nextEntry }.readBytes()
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.data.source
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.source.HttpResult
|
||||
import com.rtbishop.look4sat.core.domain.source.IHttpClient
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Android [IHttpClient] for the shared Wavelog/QRZ code, which cannot reach java.net on iOS.
|
||||
*
|
||||
* Connect and read timeouts are the 15 s the previous HttpURLConnection client used, applied to
|
||||
* the passed client so the caller keeps one connection pool. The response body is returned for
|
||||
* error codes as well, which is what reading errorStream did.
|
||||
*/
|
||||
class OkHttpHttpClient(
|
||||
baseClient: OkHttpClient,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : IHttpClient {
|
||||
|
||||
private val dispatcher = dispatcher
|
||||
|
||||
private val client = baseClient.newBuilder()
|
||||
.connectTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
override suspend fun post(url: String, headers: Map<String, String>, body: String): HttpResult =
|
||||
execute {
|
||||
Request.Builder().url(url).post(body.toRequestBody(JSON_MEDIA_TYPE)).withHeaders(headers).build()
|
||||
}
|
||||
|
||||
override suspend fun get(url: String, headers: Map<String, String>): HttpResult =
|
||||
execute { Request.Builder().url(url).withHeaders(headers).build() }
|
||||
|
||||
private suspend fun execute(buildRequest: () -> Request): HttpResult = withContext(dispatcher) {
|
||||
try {
|
||||
// Built in here, not by the caller: a URL OkHttp refuses to parse has to come back as
|
||||
// the HTTP -1 the old client reported, not as an exception thrown at the caller.
|
||||
val request = buildRequest()
|
||||
client.newCall(request).execute().use { response ->
|
||||
HttpResult(response.code, response.body.string())
|
||||
}
|
||||
} catch (exception: CancellationException) {
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
HttpResult(NO_RESPONSE, "", exception.message ?: exception.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Request.Builder.withHeaders(headers: Map<String, String>): Request.Builder {
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
return this
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Matches HttpURLConnection's connect/read timeout in the original WaveLog client. */
|
||||
const val TIMEOUT_MS = 15_000L
|
||||
|
||||
/** What HttpURLConnection's responseCode() reported when a request never got a response. */
|
||||
const val NO_RESPONSE = -1
|
||||
|
||||
/** WaveLog's v2 and v1 endpoints take application/json in both directions. */
|
||||
val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteSource(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
@@ -33,10 +32,10 @@ class RemoteSource(
|
||||
private val httpClient: OkHttpClient
|
||||
) : IRemoteSource {
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? = withContext(dispatcher) {
|
||||
override suspend fun getFileBytes(uri: String): ByteArray? = withContext(dispatcher) {
|
||||
try {
|
||||
val fileUri = uri.toUri()
|
||||
contentResolver.openInputStream(fileUri)?.buffered()
|
||||
contentResolver.openInputStream(fileUri)?.use { it.readBytes() }
|
||||
} catch (exception: CancellationException) {
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
@@ -45,17 +44,14 @@ class RemoteSource(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||
override suspend fun getNetworkBytes(url: String): ByteArray? = withContext(dispatcher) {
|
||||
try {
|
||||
val networkRequest = Request.Builder().url(url).build()
|
||||
val response = httpClient.newCall(networkRequest).execute()
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
return@withContext null
|
||||
// The whole body is read here, which also returns the connection to OkHttp's pool
|
||||
httpClient.newCall(networkRequest).execute().use { response ->
|
||||
if (!response.isSuccessful) return@use null
|
||||
response.body.bytes()
|
||||
}
|
||||
// Return the body stream directly as the caller is responsible for closing it
|
||||
// That returns the connection to OkHttp's pool
|
||||
response.body.byteStream().buffered()
|
||||
} catch (exception: CancellationException) {
|
||||
throw exception
|
||||
} catch (exception: Exception) {
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.InputStream
|
||||
import java.util.Calendar
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
@@ -18,8 +17,8 @@ import java.util.TimeZone
|
||||
class AmSatAuditTest {
|
||||
|
||||
private object UnusedSource : IRemoteSource {
|
||||
override suspend fun getFileStream(uri: String): InputStream? = null
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = null
|
||||
override suspend fun getFileBytes(uri: String): ByteArray? = null
|
||||
override suspend fun getNetworkBytes(url: String): ByteArray? = null
|
||||
override suspend fun getAmSatCatalog(): String? = null
|
||||
override suspend fun getAmSatReports(hours: Int, limit: Int): String? = null
|
||||
override suspend fun getAmSatSummary(hours: Int): String? = null
|
||||
|
||||
+2
-3
@@ -4,7 +4,6 @@ import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.InputStream
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
@@ -24,8 +23,8 @@ import java.util.TimeZone
|
||||
class AmSatSlotBuildTest {
|
||||
|
||||
private object UnusedSource : IRemoteSource {
|
||||
override suspend fun getFileStream(uri: String): InputStream? = null
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = null
|
||||
override suspend fun getFileBytes(uri: String): ByteArray? = null
|
||||
override suspend fun getNetworkBytes(url: String): ByteArray? = null
|
||||
override suspend fun getAmSatCatalog(): String? = null
|
||||
override suspend fun getAmSatReports(hours: Int, limit: Int): String? = null
|
||||
override suspend fun getAmSatSummary(hours: Int): String? = null
|
||||
|
||||
+14
-15
@@ -41,7 +41,6 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.InputStream
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseRepoTest {
|
||||
@@ -54,7 +53,7 @@ class DatabaseRepoTest {
|
||||
val uri = "content://look4sat/import/satellites"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
fileStreams[uri] = { validCsvStream() }
|
||||
fileData[uri] = { validCsvBytes() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo()
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
@@ -72,7 +71,7 @@ class DatabaseRepoTest {
|
||||
val uri = "content://look4sat/import/legacy"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
fileStreams[uri] = { validTleStream() }
|
||||
fileData[uri] = { validTleBytes() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo()
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
@@ -88,7 +87,7 @@ class DatabaseRepoTest {
|
||||
val customCsvUrl = "https://example.com/custom-omm.csv"
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[customCsvUrl] = { validCsvStream() }
|
||||
networkData[customCsvUrl] = { validCsvBytes() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
@@ -134,7 +133,7 @@ class DatabaseRepoTest {
|
||||
// unanswered on purpose: org.json is compileOnly in core:domain, so DataParser cannot
|
||||
// parse a radio payload on the JVM anyway.
|
||||
Sources.satelliteDataUrls.values.filter { it.isNotBlank() }
|
||||
.forEach { networkStreams[it] = { validCsvStream() } }
|
||||
.forEach { networkData[it] = { validCsvBytes() } }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
@@ -172,7 +171,7 @@ class DatabaseRepoTest {
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
Sources.transceiversDataUrls.values.filter { it.isNotBlank() }
|
||||
.forEach { networkStreams[it] = { "[]".byteInputStream() } }
|
||||
.forEach { networkData[it] = { "[]".encodeToByteArray() } }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
@@ -195,30 +194,30 @@ class DatabaseRepoTest {
|
||||
assertTrue("no entries may be inserted", localSource.insertedEntries.isEmpty())
|
||||
}
|
||||
|
||||
private fun validCsvStream(): InputStream = """
|
||||
private fun validCsvBytes(): InputStream = """
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
""".trimIndent().byteInputStream()
|
||||
""".trimIndent().encodeToByteArray()
|
||||
|
||||
private fun validTleStream(): InputStream = """
|
||||
private fun validTleBytes(): InputStream = """
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
|
||||
""".trimIndent().byteInputStream()
|
||||
""".trimIndent().encodeToByteArray()
|
||||
}
|
||||
|
||||
private class FakeRemoteSource : IRemoteSource {
|
||||
val fileStreams: MutableMap<String, () -> InputStream> = mutableMapOf()
|
||||
val networkStreams: MutableMap<String, () -> InputStream> = mutableMapOf()
|
||||
val fileData: MutableMap<String, () -> ByteArray> = mutableMapOf()
|
||||
val networkData: MutableMap<String, () -> ByteArray> = mutableMapOf()
|
||||
|
||||
/** Every URL asked for, so a test can assert WHICH sources were fetched, not just the result. */
|
||||
val requestedUrls = mutableListOf<String>()
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? = fileStreams[uri]?.invoke()
|
||||
override suspend fun getFileBytes(uri: String): ByteArray? = fileData[uri]?.invoke()
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? {
|
||||
override suspend fun getNetworkBytes(url: String): ByteArray? {
|
||||
requestedUrls += url
|
||||
return networkStreams[url]?.invoke()
|
||||
return networkData[url]?.invoke()
|
||||
}
|
||||
|
||||
override suspend fun getAmSatCatalog(): String? = null
|
||||
|
||||
Reference in new issue
Block a user