fix(wavelog): the upload count included QSOs sent days ago
Two smaller findings from the same audit.
Entries already confirmed by the server were added to the success total, so re-running an
upload reported "N uploaded" counting contacts that went up days ago. They are skipped and
no longer counted.
A bulk reply that stored nothing read as an acceptance. `{"imported":0}` has a success
status and would have cleared the queue. The count is checked now. Look4Sat posts one QSO
per request so this was latent, but it would have become real the moment that changed.
The count check deliberately looks only at `imported` and `adif_count`, never
`adif_errors`: v1 answers a successful upload with `adif_errors:0` beside `adif_count:1`,
and matching the wrong key would have rejected every stored QSO. A probe confirms the six
relevant shapes classify correctly.
This commit is contained in:
1 parent
13c5fc9584
commit
b1dce9c518
3 files changed
+49
-6
No files matched your search
+11
@@ -86,9 +86,17 @@ object WavelogResponse {
|
||||
if (lower.contains("\"status\"") && SUCCESS_MARKERS.none { lower.contains(it) }) {
|
||||
return Verdict.Unreadable(text.take(MAX_DETAIL))
|
||||
}
|
||||
// A bulk reply that stored nothing is not an acceptance, whatever its status says. Look4Sat
|
||||
// posts one QSO per request so this is latent today, but `imported:0` reading as success
|
||||
// would silently clear the queue if that ever changes.
|
||||
if (importedZero(lower)) return Verdict.Rejected("server imported nothing")
|
||||
return Verdict.Accepted(text.take(MAX_DETAIL))
|
||||
}
|
||||
|
||||
/** Whether a bulk reply reports that no record was stored. */
|
||||
private fun importedZero(lower: String): Boolean =
|
||||
IMPORT_COUNT_KEYS.any { lower.contains("\"" + it + "\":0") }
|
||||
|
||||
/**
|
||||
* The server's own explanation, when it gave one.
|
||||
*
|
||||
@@ -146,5 +154,8 @@ object WavelogResponse {
|
||||
/** Whitespace next to a colon or comma, which JSON allows and servers use inconsistently. */
|
||||
private val AROUND_SEPARATORS = Regex("""\s*(?=[:,])|(?<=[:,])\s*""")
|
||||
|
||||
/** Count fields a bulk reply uses to say how many records it stored. */
|
||||
private val IMPORT_COUNT_KEYS = listOf("imported", "adif_count")
|
||||
|
||||
private val REASON_KEYS = listOf("reason", "message", "error")
|
||||
}
|
||||
+8
-6
@@ -76,21 +76,23 @@ class WavelogUploader(
|
||||
|
||||
// 3. Upload one by one. ADIF gridsquare = counterpart grid (the QSO partner); blank until the scraper lands
|
||||
val entries = queue.all()
|
||||
var ok = 0
|
||||
var fail = 0
|
||||
var uploaded = 0
|
||||
var failed = 0
|
||||
var firstError = ""
|
||||
for (qso in entries) {
|
||||
if (qso.uploaded) { ok++; continue }
|
||||
// Entries already confirmed by the server are skipped, and NOT counted: adding them to
|
||||
// the total made a re-run report "N uploaded" for QSOs that went up days ago.
|
||||
if (qso.uploaded) continue
|
||||
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, qso.gridsquare)
|
||||
if (result is WavelogResult.Success) {
|
||||
ok++
|
||||
uploaded++
|
||||
queue.markUploaded(qso.id)
|
||||
} else {
|
||||
fail++
|
||||
failed++
|
||||
if (firstError.isBlank()) firstError = (result as? WavelogResult.Failure)?.message ?: ""
|
||||
}
|
||||
}
|
||||
return UploadOutcome.Done(ok, fail, UploadOutcome.Reason.COMPLETED, firstError)
|
||||
return UploadOutcome.Done(uploaded, failed, UploadOutcome.Reason.COMPLETED, firstError)
|
||||
}
|
||||
|
||||
private suspend fun getStationGrid(url: String, apiKey: String, stationId: String): String? {
|
||||
|
||||
+30
@@ -220,6 +220,36 @@ class WavelogResponseTest {
|
||||
assertTrue("got " + verdict, verdict is WavelogResponse.Verdict.Rejected)
|
||||
}
|
||||
|
||||
/** A bulk reply that stored nothing is not an acceptance, whatever its status says. */
|
||||
@Test
|
||||
fun `a reply that imported nothing is a rejection`() {
|
||||
for (body in listOf("""{"imported":0,"skipped":3}""",
|
||||
"""{"status":"created","adif_count":0,"adif_errors":1,"messages":["bad"]}""")) {
|
||||
assertTrue(
|
||||
"must not be accepted: " + body,
|
||||
WavelogResponse.verdict(200, body) is WavelogResponse.Verdict.Rejected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The count check must not misfire on a real success. v1 answers with `adif_errors:0` next to
|
||||
* `adif_count:1`, and matching the wrong key would reject every stored QSO.
|
||||
*/
|
||||
@Test
|
||||
fun `adif_errors zero does not look like nothing imported`() {
|
||||
for (body in listOf(
|
||||
"""{"status":"created","adif_count":1,"adif_errors":0,"messages":[""]}""",
|
||||
"""{"status":"created","adif_errors":0}""",
|
||||
"""{"imported":2,"skipped":1}"""
|
||||
)) {
|
||||
assertTrue(
|
||||
"must still be accepted: " + body,
|
||||
WavelogResponse.verdict(200, body) is WavelogResponse.Verdict.Accepted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A failure with no explanation still has to say something usable. */
|
||||
@Test
|
||||
fun `a failure without a reason still reports one`() {
|
||||
|
||||
Reference in new issue
Block a user