field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraComposerSubmissionCaptureTests.swift (6688B)


      1 import Foundation
      2 @testable import TeraApp
      3 import XCTest
      4 
      5 @MainActor
      6 final class TeraComposerSubmissionCaptureTests: XCTestCase {
      7   private let scope = TeraComposerScope(authorPublicKey: String(repeating: "a", count: 64), localNetworkID: "default")
      8 
      9   func testTapCapturesOneRevisionWhileTenThousandLaterEditsStayUnsaved() async throws {
     10     let storage = ComposerTestStorage()
     11     let first = await storage.pauseNext()
     12     let composer = make(storage)
     13     composer.change(form("before the tap"))
     14     await first.entered.wait()
     15     let second = await storage.pauseNext()
     16     let capture = try composer.beginSubmissionCapture(form("captured at the tap"))
     17     let save = Task { try await composer.saveSubmissionCapture(capture) }
     18     for index in 1 ... 10000 {
     19       composer.change(form("later \(index)"))
     20     }
     21     await first.resume.open()
     22     await second.entered.wait()
     23     XCTAssertEqual(composer.acknowledged?.form.content, "before the tap")
     24     await second.resume.open()
     25     let captured = try await save.value
     26     XCTAssertEqual(captured.form.content, "captured at the tap")
     27     XCTAssertEqual(captured.editSequence, 2)
     28     XCTAssertEqual(captured.revision, 2)
     29     XCTAssertTrue(composer.isDirty)
     30     XCTAssertEqual(composer.state, .unsaved)
     31     let writes = await storage.requests
     32     XCTAssertEqual(writes.map(\.form.content), ["before the tap", "captured at the tap"])
     33     XCTAssertThrowsError(try composer.beginSubmissionCapture(form("another tap")))
     34     let replay = try await composer.saveSubmissionCapture(capture)
     35     XCTAssertEqual(replay, captured)
     36     let repeated = await storage.requests
     37     XCTAssertEqual(repeated, writes)
     38     composer.releaseSubmissionCapture(capture)
     39     let latest = try await composer.save(form("later 10000"))
     40     XCTAssertEqual(latest.form.content, "later 10000")
     41     XCTAssertEqual(latest.revision, 3)
     42     XCTAssertEqual(latest.id, captured.id)
     43     XCTAssertEqual(latest.editSequence, 10002)
     44   }
     45 
     46   func testCaptureCancelsBatchingWithoutLettingTheNewestEditReplaceItsSource() async throws {
     47     let storage = ComposerTestStorage()
     48     let delay = ComposerCoalescingGate()
     49     let composer = TeraComposerAutosave(persistence: storage.port, delay: { try await delay.wait() })
     50     composer.reset(scope: scope)
     51     composer.change(form("batched"))
     52     await delay.entered.wait()
     53     let capture = try composer.beginSubmissionCapture(form("tap"))
     54     composer.change(form("typed after tap"))
     55     let saved = try await composer.saveSubmissionCapture(capture)
     56     XCTAssertEqual(saved.form.content, "tap")
     57     XCTAssertEqual(saved.editSequence, 2)
     58     let writes = await storage.requests
     59     let cancelled = await delay.cancelled
     60     XCTAssertEqual(writes.count, 1)
     61     XCTAssertTrue(cancelled)
     62     XCTAssertTrue(composer.isDirty)
     63     composer.releaseSubmissionCapture(capture)
     64     let latest = try await composer.save(form("typed after tap"))
     65     XCTAssertEqual(latest.revision, 2)
     66   }
     67 
     68   func testUnknownCaptureWriteReconcilesTheOriginalBeforeSavingNewerEditing() async throws {
     69     let storage = ComposerTestStorage()
     70     await storage.failNext(.afterCommit)
     71     let composer = make(storage)
     72     let capture = try composer.beginSubmissionCapture(form("original"))
     73     do {
     74       _ = try await composer.saveSubmissionCapture(capture)
     75       XCTFail("A lost receipt cannot claim the capture was saved.")
     76     } catch { XCTAssertNil(composer.acknowledged) }
     77     composer.change(form("newest"))
     78     do {
     79       _ = try await composer.save(form("newest"))
     80       XCTFail("Explicit Save cannot overwrite the source while submission is unresolved.")
     81     } catch {
     82       XCTAssertEqual(TeraAddPresentation.failure(for: error)?.code, "submission_capture_pending")
     83     }
     84     let recovered = try await composer.saveSubmissionCapture(capture)
     85     XCTAssertEqual(recovered.form.content, "original")
     86     XCTAssertEqual(recovered.revision, 1)
     87     let writes = await storage.requests
     88     let reads = await storage.readCount
     89     XCTAssertEqual(writes.count, 1)
     90     XCTAssertEqual(reads, 1)
     91     composer.releaseSubmissionCapture(capture)
     92     let saved = try await composer.save(form("newest"))
     93     XCTAssertEqual(saved.revision, 2)
     94     XCTAssertEqual(saved.form.content, "newest")
     95   }
     96 
     97   func testStopRetainsTheBarrierAndResumeReconcilesTheLateWrite() async throws {
     98     let storage = ComposerTestStorage()
     99     let pause = await storage.pauseNext()
    100     let composer = make(storage)
    101     let capture = try composer.beginSubmissionCapture(form("captured"))
    102     let save = Task { try await composer.saveSubmissionCapture(capture) }
    103     await pause.entered.wait()
    104     composer.change(form("newer"))
    105     composer.stop()
    106     await pause.resume.open()
    107     do { _ = try await save.value; XCTFail("A stale waiter cannot acknowledge the write.") } catch {}
    108     XCTAssertTrue(composer.hasSubmissionCapture)
    109     XCTAssertNil(composer.acknowledged)
    110     composer.resume()
    111     let recovered = try await composer.saveSubmissionCapture(capture)
    112     XCTAssertEqual(recovered.form.content, "captured")
    113     XCTAssertEqual(recovered.revision, 1)
    114     let writes = await storage.requests
    115     XCTAssertEqual(writes.count, 1)
    116     composer.releaseSubmissionCapture(capture)
    117     _ = try await composer.save(form("newer"))
    118   }
    119 
    120   func testOldCaptureCannotReleaseAReplacementScopeBarrier() async throws {
    121     let storage = ComposerTestStorage()
    122     let pause = await storage.pauseNext()
    123     let composer = make(storage)
    124     let original = try composer.beginSubmissionCapture(form("old"))
    125     let save = Task { try await composer.saveSubmissionCapture(original) }
    126     await pause.entered.wait()
    127     let nextScope = TeraComposerScope(authorPublicKey: scope.authorPublicKey, localNetworkID: "next")
    128     composer.reset(scope: nextScope)
    129     let replacement = try composer.beginSubmissionCapture(form("new scope"))
    130     composer.releaseSubmissionCapture(original)
    131     XCTAssertTrue(composer.hasSubmissionCapture)
    132     await pause.resume.open()
    133     do { _ = try await save.value; XCTFail("Replaced scope cannot accept an old capture.") } catch {}
    134     let current = try await composer.saveSubmissionCapture(replacement)
    135     XCTAssertEqual(current.scope, nextScope)
    136     XCTAssertEqual(current.form.content, "new scope")
    137     composer.releaseSubmissionCapture(replacement)
    138     XCTAssertFalse(composer.hasSubmissionCapture)
    139   }
    140 
    141   private func make(_ storage: ComposerTestStorage) -> TeraComposerAutosave {
    142     let composer = TeraComposerAutosave(persistence: storage.port, delay: {})
    143     composer.reset(scope: scope)
    144     return composer
    145   }
    146 
    147   private func form(_ text: String) -> TeraComposerForm {
    148     var value = TeraComposerForm(commandType: .createUpdate)
    149     value.content = text
    150     return value
    151   }
    152 }