Signal-iOS/SignalServiceKit/tests/Util/OWSProgressTest.swift
2025-12-30 11:34:05 -08:00

1533 lines
58 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
@testable import SignalServiceKit
class OWSProgressTest: XCTestCase {
func testSimpleSourceSink() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source = await sink.addSource(withLabel: "1", unitCount: 100)
for _ in 0..<10 {
await Task.yield()
source.incrementCompletedUnitCount(by: 10)
}
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
if progress.totalUnitCount == 0 {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertNil(progress.progressForChild(label: "1"))
} else if progress.totalUnitCount == 100 {
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 100)
XCTAssertEqual(
progress.progressForChild(label: "1")?.completedUnitCount,
progress.completedUnitCount,
)
} else {
XCTFail("Unexpected unit count")
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
XCTAssertLessThanOrEqual(outputs.count, 11)
XCTAssertEqual(outputs.last, 100)
}
func testTwoSources() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source1 = await sink.addSource(withLabel: "1", unitCount: 50)
let source2 = await sink.addSource(withLabel: "2", unitCount: 50)
let inputTask1 = Task {
for _ in 0..<5 {
await Task.yield()
source1.incrementCompletedUnitCount(by: 10)
}
}
let inputTask2 = Task {
for _ in 0..<5 {
await Task.yield()
source2.incrementCompletedUnitCount(by: 10)
}
}
await inputTask1.await()
await inputTask2.await()
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
if progress.totalUnitCount == 0 {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertNil(progress.progressForChild(label: "1"))
XCTAssertNil(progress.progressForChild(label: "2"))
} else if progress.totalUnitCount == 50 {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "1")?.completedUnitCount, 0)
XCTAssertNil(progress.progressForChild(label: "2"))
} else if progress.totalUnitCount == 100 {
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "2")?.totalUnitCount, 50)
XCTAssertEqual(
progress.progressForChild(label: "1")!.completedUnitCount + progress.progressForChild(label: "2")!.completedUnitCount,
progress.completedUnitCount,
)
} else {
XCTFail("Unexpected unit count")
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
// initial 0 emission, plus two updates to total unit count, plus 10 updates.
XCTAssertLessThanOrEqual(outputs.count, 13)
XCTAssertEqual(outputs.last, 100)
}
func testTwoLayers() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source1 = await sink.addSource(withLabel: "1", unitCount: 50)
let child = await sink.addChild(withLabel: "a", unitCount: 50)
let source2 = await child.addSource(withLabel: "2", unitCount: 100)
source1.incrementCompletedUnitCount(by: 50)
let inputTask = Task {
for _ in 0..<10 {
await Task.yield()
source2.incrementCompletedUnitCount(by: 10)
}
}
await inputTask.await()
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
if progress.totalUnitCount == 0 {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertNil(progress.progressForChild(label: "1"))
XCTAssertNil(progress.progressForChild(label: "a"))
XCTAssertNil(progress.progressForChild(label: "2"))
} else if progress.totalUnitCount == 50 {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "1")?.completedUnitCount, 0)
XCTAssertNil(progress.progressForChild(label: "a"))
XCTAssertNil(progress.progressForChild(label: "2"))
} else if progress.totalUnitCount == 100 {
if progress.progressForChild(label: "2") == nil {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "a")?.totalUnitCount, 50)
} else {
XCTAssertEqual(
progress.completedUnitCount,
progress.progressForChild(label: "1")!.completedUnitCount
+ progress.progressForChild(label: "a")!.completedUnitCount,
)
XCTAssertEqual(progress.progressForChild(label: "1")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "a")?.totalUnitCount, 50)
XCTAssertEqual(progress.progressForChild(label: "2")?.totalUnitCount, 100)
XCTAssertEqual(
progress.progressForChild(label: "a")?.completedUnitCount,
(progress.progressForChild(label: "2")?.completedUnitCount ?? 0) / 2,
)
}
} else {
XCTFail("Unexpected unit count")
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
// three child node additions, plus 11 updates.
// some emissions may get debounced.
XCTAssertLessThanOrEqual(outputs.count, 14)
XCTAssertEqual(outputs.last, 100)
}
func testMultipleLayers() async {
// A1 -> A -> root
let multiplierA1: Float = (1 / 200) * 100
let unitCountA1: UInt64 = UInt64(ceil(multiplierA1 * 100))
// B1 -> B -> root
let multiplierB1: Float = (1 / 200) * 100
let unitCountB1: UInt64 = UInt64(ceil(multiplierB1 * 50))
// BZ1 -> BZ -> B -> root
let multiplierBZ1: Float = (1 / 30) * (50 / 200) * 100
let unitCountBZ1: UInt64 = UInt64(ceil(multiplierBZ1 * 10))
// BZ2 -> BZ -> B -> root
let multiplierBZ2: Float = (1 / 30) * (50 / 200) * 100
let unitCountBZ2: UInt64 = UInt64(ceil(multiplierBZ2 * 1))
// C2 -> C -> root
let multiplierC2: Float = (1 / 11) * 200
let unitCountC2: UInt64 = UInt64(ceil(multiplierC2 * 5))
let expectedCompletedUnitCount: UInt64 =
unitCountA1
+ unitCountB1
+ unitCountBZ1
+ unitCountBZ2
+ unitCountC2
let (sink, stream) = OWSProgress.createSink()
Task {
let childA = await sink.addChild(withLabel: "A", unitCount: 100)
let sourceA1 = await childA.addSource(withLabel: "A1", unitCount: 100)
_ = await childA.addSource(withLabel: "A2", unitCount: 100)
let childB = await sink.addChild(withLabel: "B", unitCount: 100)
let sourceB1 = await childB.addSource(withLabel: "B1", unitCount: 50)
_ = await childB.addSource(withLabel: "B2", unitCount: 100)
let childBZ = await childB.addChild(withLabel: "BZ", unitCount: 50)
let sourceBZ1 = await childBZ.addSource(withLabel: "BZ1", unitCount: 10)
let sourceBZ2 = await childBZ.addSource(withLabel: "BZ2", unitCount: 10)
_ = await childBZ.addSource(withLabel: "BZ3", unitCount: 10)
let childC = await sink.addChild(withLabel: "C", unitCount: 200)
_ = await childC.addSource(withLabel: "C1", unitCount: 1)
let sourceC2 = await childC.addSource(withLabel: "C2", unitCount: 10)
// Make partial progress on every layer of the subtree.
sourceA1.incrementCompletedUnitCount(by: 100)
sourceB1.incrementCompletedUnitCount(by: 50)
sourceBZ1.incrementCompletedUnitCount(by: 10)
sourceBZ2.incrementCompletedUnitCount(by: 1)
sourceC2.incrementCompletedUnitCount(by: 5)
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
if progress.completedUnitCount >= expectedCompletedUnitCount {
break
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
// initial 0 emission, plus 13 updates to nodes, plus 5 updates.
XCTAssertLessThanOrEqual(outputs.count, 19)
XCTAssertEqual(outputs.last!, expectedCompletedUnitCount)
}
func testAddToOtherBranchAfterEmitting() async {
// Say you have your root sink and you add children
// A and B. You add source A1 to A and B1 to B.
// You are allowed to add a second source B2 to B
// after A1 emits but not after B1 emits.
let (sink, stream) = OWSProgress.createSink()
let childA = await sink.addChild(withLabel: "A", unitCount: 100)
let sourceA1 = await childA.addSource(withLabel: "A1", unitCount: 100)
let childB = await sink.addChild(withLabel: "B", unitCount: 100)
_ = await childB.addSource(withLabel: "B1", unitCount: 50)
Task {
sourceA1.incrementCompletedUnitCount(by: 50)
}
var outputs = [Float]()
for await progress in stream {
outputs.append(progress.percentComplete)
if progress.progressForChild(label: "A") == nil {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 0)
} else if progress.progressForChild(label: "A1") == nil {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
} else if progress.progressForChild(label: "B") == nil {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
} else if progress.progressForChild(label: "B1") == nil {
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 200)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
} else if progress.completedUnitCount == 0 {
XCTAssertEqual(progress.totalUnitCount, 200)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "B1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 50,
label: "B1",
parentLabel: "B",
),
)
} else if progress.completedUnitCount == 50 {
XCTAssertEqual(progress.totalUnitCount, 200)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "B1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 50,
label: "B1",
parentLabel: "B",
),
)
break
} else {
XCTFail("Unexpected progress update")
}
}
XCTAssertGreaterThanOrEqual(outputs.count, 1)
Task {
let sourceB2 = await childB.addSource(withLabel: "B2", unitCount: 50)
sourceB2.incrementCompletedUnitCount(by: 50)
}
for await progress in stream {
outputs.append(progress.percentComplete)
if progress.completedUnitCount == 50 {
XCTAssertEqual(progress.totalUnitCount, 200)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "B1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 50,
label: "B1",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B2"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 50,
label: "B2",
parentLabel: "B",
),
)
} else if progress.completedUnitCount == 100 {
XCTAssertEqual(progress.totalUnitCount, 200)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A1"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A1",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "B1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 50,
label: "B1",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 50,
label: "B2",
parentLabel: "B",
),
)
break
} else {
XCTFail("Unexpected progress update")
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
XCTAssertEqual(outputs.last!, 0.5)
}
func testRestartLabel() async {
// Add 1, A, B, C to the root,
// then 2 to A,
// 3, 4, D to B,
// 5 to C
// 6, E to D.
// Make some progres on all of them, then...
// * "reset" 1 by re-adding it to the root
// * "reset" 3 by re-adding it to B.
// * "reset" C and all its children by re-adding it to the root
// * "reset" D and all its children by re-adding it to C
let (sink, stream) = OWSProgress.createSink()
var source1 = await sink.addSource(withLabel: "1", unitCount: 100)
let childA = await sink.addChild(withLabel: "A", unitCount: 100)
let childB = await sink.addChild(withLabel: "B", unitCount: 100)
var childC = await sink.addChild(withLabel: "C", unitCount: 100)
let source2 = await childA.addSource(withLabel: "2", unitCount: 100)
var source3 = await childB.addSource(withLabel: "3", unitCount: 100)
let source4 = await childB.addSource(withLabel: "4", unitCount: 100)
var childD = await childB.addChild(withLabel: "D", unitCount: 100)
let source5 = await childC.addSource(withLabel: "5", unitCount: 100)
let source6 = await childD.addSource(withLabel: "6", unitCount: 100)
_ = await childD.addChild(withLabel: "E", unitCount: 0)
// Complete everything by 50%
source1.incrementCompletedUnitCount(by: 50)
source2.incrementCompletedUnitCount(by: 50)
source3.incrementCompletedUnitCount(by: 50)
source4.incrementCompletedUnitCount(by: 50)
source5.incrementCompletedUnitCount(by: 50)
source6.incrementCompletedUnitCount(by: 50)
for await progress in stream {
// Wait to get all the updates from the setup we did.
if progress.completedUnitCount == 200 {
XCTAssertEqual(progress.totalUnitCount, 400)
XCTAssertEqual(
progress.progressForChild(label: "1"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "1",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "C",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "5"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "5",
parentLabel: "C",
),
)
XCTAssertEqual(
progress.progressForChild(label: "6"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "6",
parentLabel: "D",
),
)
XCTAssertEqual(
progress.progressForChild(label: "E"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 0,
label: "E",
parentLabel: "D",
),
)
break
}
}
// Now "reset" source1, and use a new unit count to boot
let oldSource1 = source1
source1 = await sink.addSource(withLabel: "1", unitCount: 200)
for await progress in stream {
XCTAssertEqual(progress.completedUnitCount, 150)
XCTAssertEqual(progress.totalUnitCount, 500)
XCTAssertEqual(
progress.progressForChild(label: "1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "1",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "C",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "5"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "5",
parentLabel: "C",
),
)
XCTAssertEqual(
progress.progressForChild(label: "6"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "6",
parentLabel: "D",
),
)
XCTAssertEqual(
progress.progressForChild(label: "E"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 0,
label: "E",
parentLabel: "D",
),
)
break
}
// Updating old source 1 should do nothing (we're expecting exactly one
// update below)
oldSource1.incrementCompletedUnitCount(by: 50)
// Now reset source 3.
let oldSource3 = source3
source3 = await childB.addSource(withLabel: "3", unitCount: 100)
for await progress in stream {
XCTAssertEqual(progress.completedUnitCount, 134)
XCTAssertEqual(progress.totalUnitCount, 500)
XCTAssertEqual(
progress.progressForChild(label: "1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "1",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 34,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "C",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "5"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "5",
parentLabel: "C",
),
)
XCTAssertEqual(
progress.progressForChild(label: "6"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "6",
parentLabel: "D",
),
)
XCTAssertEqual(
progress.progressForChild(label: "E"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 0,
label: "E",
parentLabel: "D",
),
)
break
}
// Updating old source 3 should do nothing (we're expecting exactly one
// update below)
oldSource3.incrementCompletedUnitCount(by: 50)
// Now reset child C
let oldSource5 = source5
childC = await sink.addChild(withLabel: "C", unitCount: 200)
for await progress in stream {
XCTAssertEqual(progress.completedUnitCount, 84)
XCTAssertEqual(progress.totalUnitCount, 600)
XCTAssertEqual(
progress.progressForChild(label: "1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "1",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 34,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "C",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "6"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "6",
parentLabel: "D",
),
)
XCTAssertEqual(
progress.progressForChild(label: "E"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 0,
label: "E",
parentLabel: "D",
),
)
break
}
// Updating old source 5 (child of C) should do nothing (we're expecting exactly one
// update below)
oldSource5.incrementCompletedUnitCount(by: 50)
// Now reset D
childD = await childB.addChild(withLabel: "D", unitCount: 100)
for await progress in stream {
XCTAssertEqual(progress.completedUnitCount, 67)
XCTAssertEqual(progress.totalUnitCount, 600)
XCTAssertEqual(
progress.progressForChild(label: "1"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "1",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 17,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "C",
parentLabel: nil,
),
)
break
}
// We should be allowed to add a new source with the same
// label (1) at another layer of the tree (to D)
await childD.addSource(withLabel: "1", unitCount: 100)
.incrementCompletedUnitCount(by: 100)
for await progress in stream {
if progress.completedUnitCount == 67 {
// skip the first update
continue
}
XCTAssertEqual(progress.completedUnitCount, 100)
XCTAssertEqual(progress.totalUnitCount, 600)
let progressesFor1 = progress.progressesForAllChildren(withLabel: "1")
XCTAssertEqual(progressesFor1.count, 2)
XCTAssert(progressesFor1.contains(
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "1",
parentLabel: nil,
),
))
XCTAssert(progressesFor1.contains(
OWSProgress.ChildProgress(
completedUnitCount: 100,
totalUnitCount: 100,
label: "1",
parentLabel: "D",
),
))
XCTAssertEqual(
progress.progressForChild(label: "A"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "A",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "2"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "2",
parentLabel: "A",
),
)
XCTAssertEqual(
progress.progressForChild(label: "B"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "B",
parentLabel: nil,
),
)
XCTAssertEqual(
progress.progressForChild(label: "3"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 100,
label: "3",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "4"),
OWSProgress.ChildProgress(
completedUnitCount: 50,
totalUnitCount: 100,
label: "4",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "D"),
OWSProgress.ChildProgress(
completedUnitCount: 100,
totalUnitCount: 100,
label: "D",
parentLabel: "B",
),
)
XCTAssertEqual(
progress.progressForChild(label: "C"),
OWSProgress.ChildProgress(
completedUnitCount: 0,
totalUnitCount: 200,
label: "C",
parentLabel: nil,
),
)
break
}
}
func testChildProgresses() async {
let (rootProgress, stream) = OWSProgress.createSink()
func wait(label: String, percent: Float) async {
for await progress in stream {
if progress.progressForChild(label: label)?.percentComplete == percent {
break
}
}
}
let source1 = await rootProgress.addSource(withLabel: "source1", unitCount: 10)
let sinkA = await rootProgress.addChild(withLabel: "sinkA", unitCount: 10)
let sourceA1 = await sinkA.addSource(withLabel: "sourceA1", unitCount: 100)
source1.incrementCompletedUnitCount(by: 5)
await wait(label: "source1", percent: 0.5)
source1.incrementCompletedUnitCount(by: 5)
await wait(label: "source1", percent: 1)
sourceA1.incrementCompletedUnitCount(by: 50)
await wait(label: "sourceA1", percent: 0.5)
let sourceA2 = await sinkA.addSource(withLabel: "sourceA2", unitCount: 200)
await wait(label: "sourceA2", percent: 0)
sourceA2.incrementCompletedUnitCount(by: 200)
await wait(label: "sourceA2", percent: 1)
sourceA1.incrementCompletedUnitCount(by: 50)
await wait(label: "sourceA1", percent: 1)
let source2 = await rootProgress.addSource(withLabel: "source2", unitCount: 20)
await wait(label: "source2", percent: 0)
source2.incrementCompletedUnitCount(by: 20)
await wait(label: "source2", percent: 1)
}
func testUpdatePeriodically_estimatedTimeFinishesFirst() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source = await sink.addSource(withLabel: "1", unitCount: 100)
let inputTask = Task {
try await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
}
try await source.updatePeriodically(
timeInterval: 0.001,
estimatedTimeToCompletion: 50,
work: { try await inputTask.value },
)
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
}
XCTAssertLessThanOrEqual(outputs.count, 52)
XCTAssertEqual(outputs.last, 100)
}
func testUpdatePeriodically_WorkFinishesFirst() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source = await sink.addSource(withLabel: "1", unitCount: 100)
let inputTask = Task {
try await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
}
try await source.updatePeriodically(
timeInterval: 0.001,
estimatedTimeToCompletion: 200,
work: { try await inputTask.value },
)
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
}
XCTAssertLessThanOrEqual(outputs.count, 102)
XCTAssertEqual(outputs.last, 100)
}
func testUpdatePeriodically_NonThrowing() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source = await sink.addSource(withLabel: "1", unitCount: 100)
// If the task doesn't throw the updatePeriodically call shouldn't throw either.
let inputTask = Task {
try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
return "Hello, World!"
}
let stringResult = await source.updatePeriodically(
timeInterval: 0.001,
estimatedTimeToCompletion: 200,
work: { await inputTask.value },
)
XCTAssertEqual(stringResult, "Hello, World!")
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
}
XCTAssertLessThanOrEqual(outputs.count, 102)
XCTAssertEqual(outputs.last, 100)
}
func testUpdatePeriodically_OptionalResult() async {
let (sink, stream) = OWSProgress.createSink()
Task {
let source = await sink.addSource(withLabel: "1", unitCount: 100)
let inputTask: Task<String?, Never> = Task {
try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
return nil
}
let stringResult = await source.updatePeriodically(
timeInterval: 0.001,
estimatedTimeToCompletion: 200,
work: { await inputTask.value },
)
XCTAssertNil(stringResult)
}
var outputs = [UInt64]()
for await progress in stream {
outputs.append(progress.completedUnitCount)
}
XCTAssertLessThanOrEqual(outputs.count, 102)
XCTAssertEqual(outputs.last, 100)
}
func testSimpleSourceSink_callback() async {
var sinkRef: OWSProgressSink?
let outputs: [UInt64] = await withCheckedContinuation { outputsContinuation in
Task {
var outputs = [UInt64]()
let sink = OWSProgress.createSink { progress in
outputs.append(progress.completedUnitCount)
if progress.isFinished {
outputsContinuation.resume(returning: outputs)
}
}
sinkRef = sink
let source = await sink.addSource(withLabel: "1", unitCount: 100)
let inputTask = Task {
for _ in 0..<10 {
await Task.yield()
source.incrementCompletedUnitCount(by: 10)
}
}
await inputTask.await()
}
}
// Emissions can get debounced, so we can't guarantee
// anything about them except that theres a first and last.
XCTAssertGreaterThanOrEqual(outputs.count, 1)
XCTAssertLessThanOrEqual(outputs.count, 11)
XCTAssertEqual(outputs.last, 100)
XCTAssertNotNil(sinkRef)
}
// MARK: - Zero Unit Counts
func testZeroUnitCount_sourceOnRoot() async {
let (sink, stream) = OWSProgress.createSink()
let source = await sink.addSource(withLabel: "source", unitCount: 0)
// Incrementing is irrelevant.
source.incrementCompletedUnitCount(by: 0)
source.incrementCompletedUnitCount(by: 100)
// Should complete after a single progress update.
var numUpdates = 0
for await progress in stream {
numUpdates += 1
switch numUpdates {
case 1:
XCTAssertEqual(progress.percentComplete, 1)
XCTAssertEqual(progress.totalUnitCount, 0)
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssert(progress.isFinished)
default:
XCTFail("Unexpected update")
}
}
}
func testZeroUnitCount_childOnRoot() async {
let (sink, stream) = OWSProgress.createSink()
let child = await sink.addChild(withLabel: "child", unitCount: 0)
let source = await child.addSource(withLabel: "source", unitCount: 100)
// Incrementing is irrelevant.
source.incrementCompletedUnitCount(by: 0)
source.incrementCompletedUnitCount(by: 50)
// Should complete after a single progress update.
var numUpdates = 0
for await progress in stream {
numUpdates += 1
switch numUpdates {
case 1:
XCTAssertEqual(progress.percentComplete, 1)
XCTAssertEqual(progress.totalUnitCount, 0)
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssert(progress.isFinished)
default:
XCTFail("Unexpected update")
}
}
}
func testZeroUnitCount_nestedSource() async {
let (sink, stream) = OWSProgress.createSink()
let child = await sink.addChild(withLabel: "child", unitCount: 100)
let source = await child.addSource(withLabel: "source", unitCount: 0)
// Incrementing is irrelevant.
source.incrementCompletedUnitCount(by: 0)
source.incrementCompletedUnitCount(by: 100)
// Should complete after 2 progress updates.
var numUpdates = 0
for await progress in stream {
numUpdates += 1
switch numUpdates {
case 1:
XCTAssertEqual(progress.percentComplete, 0)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertFalse(progress.isFinished)
XCTAssertEqual(numUpdates, 1)
case 2:
XCTAssertEqual(progress.percentComplete, 1)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertEqual(progress.completedUnitCount, 100)
XCTAssert(progress.isFinished)
default:
XCTFail("Unexpected update")
}
}
}
func testZeroUnitCount_manyChildrenOnRoot() async {
let (sink, stream) = OWSProgress.createSink()
let source1 = await sink.addSource(withLabel: "1", unitCount: 100)
_ = await sink.addSource(withLabel: "2", unitCount: 0)
// Should get 2 progress updates for the setup.
var numUpdates = 0
loop: for await progress in stream {
numUpdates += 1
XCTAssertEqual(progress.percentComplete, 0)
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertFalse(progress.isFinished)
switch numUpdates {
case 1:
break
case 2:
break loop
default:
XCTFail("Unexpected update")
}
}
// Complete the only source with nonzero units, which
// should complete the whole progress.
source1.incrementCompletedUnitCount(by: 100)
for await progress in stream {
numUpdates += 1
switch numUpdates {
case 3:
XCTAssertEqual(progress.percentComplete, 1)
XCTAssertEqual(progress.completedUnitCount, 100)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssert(progress.isFinished)
default:
XCTFail("Unexpected update")
}
}
}
func testZeroUnitCount_manyChildrenOnChild() async {
let (sink, stream) = OWSProgress.createSink()
let child = await sink.addChild(withLabel: "A", unitCount: 100)
let source1 = await child.addSource(withLabel: "1", unitCount: 100)
_ = await child.addSource(withLabel: "2", unitCount: 0)
// Should get 3 progress updates for the setup.
var numUpdates = 0
loop: for await progress in stream {
numUpdates += 1
XCTAssertEqual(progress.percentComplete, 0)
XCTAssertEqual(progress.completedUnitCount, 0)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssertFalse(progress.isFinished)
switch numUpdates {
case 1, 2:
break
case 3:
break loop
default:
XCTFail("Unexpected update")
}
}
// Complete the only source with nonzero units, which
// should complete the whole progress.
source1.incrementCompletedUnitCount(by: 100)
for await progress in stream {
numUpdates += 1
switch numUpdates {
case 4:
XCTAssertEqual(progress.percentComplete, 1)
XCTAssertEqual(progress.completedUnitCount, 100)
XCTAssertEqual(progress.totalUnitCount, 100)
XCTAssert(progress.isFinished)
default:
XCTFail("Unexpected update")
}
}
}
// MARK: - Sequential progress
func testSequentialProgress() async {
enum Step: String, OWSSequentialProgressStep {
case first
case second
case third
var progressUnitCount: UInt64 {
switch self {
case .first: 1
case .second: 2
case .third: 1
}
}
}
let (root, stream) = await OWSSequentialProgress<Step>.createSink()
let source1a = await root.child(for: .first).addSource(withLabel: "a", unitCount: 1)
let source2b = await root.child(for: .second).addSource(withLabel: "b", unitCount: 1)
let source2c = await root.child(for: .second).addSource(withLabel: "c", unitCount: 1)
let source3d = await root.child(for: .third).addSource(withLabel: "d", unitCount: 1)
// Skip over the updates from the first 6 setup steps.
var numUpdates = 0
for await _ in stream {
numUpdates += 1
if numUpdates == 6 {
break
}
}
var outputs = [OWSSequentialProgress<Step>]()
func awaitOneUpdate() async {
for await progress in stream {
outputs.append(progress)
break
}
}
await awaitOneUpdate()
source1a.incrementCompletedUnitCount(by: 1)
await awaitOneUpdate()
source2b.incrementCompletedUnitCount(by: 1)
await awaitOneUpdate()
source2c.incrementCompletedUnitCount(by: 1)
await awaitOneUpdate()
source3d.incrementCompletedUnitCount(by: 1)
await awaitOneUpdate()
let allowedOutputs: [(UInt64, Step)] = [
(0, .first),
(1, .second),
(2, .second),
(3, .third),
(4, .third),
]
XCTAssertEqual(allowedOutputs.count, outputs.count)
for i in 0..<allowedOutputs.count {
XCTAssertEqual(allowedOutputs[i].0, outputs[i].completedUnitCount)
XCTAssertEqual(allowedOutputs[i].1, outputs[i].currentStep)
}
XCTAssert(outputs.last?.isFinished == true)
}
}