import Testing @testable import Kanban struct RanksTests { // MARK: - Append @Test func appendOnEmptyLaneReturnsBoardConvention() { #expect(Ranks.append(toVisible: [Double]()) == 1024) } @Test func appendReturnsMaxPlusGap() { #expect(Ranks.append(toVisible: [1024, 2048, 512]) == 3072) } // MARK: - Insert at head @Test func insertAtHeadOnEmptyLaneReturnsBoardConvention() { #expect(Ranks.insertAtHead(ofVisible: [Double]()) == 1024) } @Test func insertAtHeadReturnsMinMinusGap() { #expect(Ranks.insertAtHead(ofVisible: [1024, 2048, 512]) == -512) } // MARK: - Midpoint @Test func midpointBetweenDistinctValuesIsStrictlyBetween() throws { let mid = try #require(Ranks.midpoint(between: 1024, and: 2048)) #expect(mid == 1536) #expect(mid > 1024 && mid < 2048) } @Test func midpointIsOrderIndependent() { let forward = Ranks.midpoint(between: 1024, and: 2048) let reversed = Ranks.midpoint(between: 2048, and: 1024) #expect(forward == reversed) } @Test func midpointBetweenEqualValuesReturnsNil() { #expect(Ranks.midpoint(between: 42, and: 42) == nil) } @Test func midpointNeverEscapesTheOpenInterval() { // Values close enough that a naive (a+b)/2 could round to one of the // endpoints; the result (if any) must stay strictly inside. let a = 1.0 let b = 1.0.nextUp.nextUp.nextUp if let mid = Ranks.midpoint(between: a, and: b) { #expect(mid > a && mid < b) } } // MARK: - Renumbered @Test func renumberedProducesWholeMultiplesOf1024() { #expect(Ranks.renumbered(count: 4) == [1024, 2048, 3072, 4096]) } @Test func renumberedWithZeroCountIsEmpty() { #expect(Ranks.renumbered(count: 0) == []) } // MARK: - Display order / tie-break @Test func sortedForDisplayOrdersAscendingByRank() { let items: [(order: Double, name: String)] = [ (order: 3072, name: "c-card"), (order: 1024, name: "a-card"), (order: 2048, name: "b-card"), ] let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name }) #expect(sorted.map { $0.name } == ["a-card", "b-card", "c-card"]) } @Test func sortedForDisplayBreaksTiesByFolderName() { let items: [(order: Double, name: String)] = [ (order: 1024, name: "zzz-uuid"), (order: 1024, name: "aaa-uuid"), (order: 1024, name: "mmm-uuid"), ] let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name }) #expect(sorted.map { $0.name } == ["aaa-uuid", "mmm-uuid", "zzz-uuid"]) } @Test func isOrderedForDisplayMatchesSortedForDisplay() { let a: (order: Double, name: String) = (order: 1024, name: "aaa") let b: (order: Double, name: String) = (order: 1024, name: "bbb") #expect(Ranks.isOrderedForDisplay(a, before: b, order: { $0.order }, name: { $0.name })) #expect(!Ranks.isOrderedForDisplay(b, before: a, order: { $0.order }, name: { $0.name })) } // MARK: - Tombstone exclusion @Test func appendIgnoresTombstonedSiblings() { let items: [(order: Double, isDeleted: Bool)] = [ (order: 1024, isDeleted: false), (order: 9999, isDeleted: true), (order: 2048, isDeleted: false), ] #expect(Ranks.append(toVisible: items) == 3072) } @Test func insertAtHeadIgnoresTombstonedSiblings() { let items: [(order: Double, isDeleted: Bool)] = [ (order: 1024, isDeleted: false), (order: -9999, isDeleted: true), (order: 2048, isDeleted: false), ] #expect(Ranks.insertAtHead(ofVisible: items) == 0) } @Test func appendWithAllSiblingsTombstonedReturnsBoardConvention() { let items: [(order: Double, isDeleted: Bool)] = [ (order: 1024, isDeleted: true), (order: 2048, isDeleted: true), ] #expect(Ranks.append(toVisible: items) == 1024) } @Test func insertAtHeadWithAllSiblingsTombstonedReturnsBoardConvention() { let items: [(order: Double, isDeleted: Bool)] = [ (order: 1024, isDeleted: true), (order: 2048, isDeleted: true), ] #expect(Ranks.insertAtHead(ofVisible: items) == 1024) } // MARK: - Insertion at a display position @Test func insertionRankDispatchesOnPosition() throws { let orders = [1024.0, 2048.0, 3072.0] // The three cases every insertion gesture has, behind one call. #expect(Ranks.insertionRank(amongVisible: orders, at: 0) == 0, "head: min − 1024") #expect(Ranks.insertionRank(amongVisible: orders, at: 1) == 1536, "between: the midpoint") #expect(Ranks.insertionRank(amongVisible: orders, at: 2) == 2560) #expect(Ranks.insertionRank(amongVisible: orders, at: 3) == 4096, "end: max + 1024") // The result is always strictly inside the gap it names, which is what makes the display // order the caller asked for the one it gets. let placed = try #require(Ranks.insertionRank(amongVisible: orders, at: 1)) #expect(placed > orders[0] && placed < orders[1]) } @Test func insertionRankIsTotalOnEdgeInputs() { // An empty parent's first child lands at the board convention, whatever index is asked for. #expect(Ranks.insertionRank(amongVisible: [], at: 0) == 1024) #expect(Ranks.insertionRank(amongVisible: [], at: 7) == 1024) // Out-of-range indices clamp to the two ends rather than trapping: an index arrives from a // drag's geometry, and geometry can outrun a snapshot. #expect(Ranks.insertionRank(amongVisible: [1024], at: -3) == 0) #expect(Ranks.insertionRank(amongVisible: [1024], at: 99) == 2048) } @Test func insertionRankReportsAnExhaustedGapRatherThanInventingOne() { // `nil` is the renumber trigger, not a refusal — and it must fire for the duplicate-order // tie as well as for adjacent Doubles, since neither admits a rank between. #expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 1) == nil) #expect(Ranks.insertionRank(amongVisible: [1024, 1024.0000000000002], at: 1) == nil) // The ends never exhaust: append and head-insert always have room. #expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 0) == 0) #expect(Ranks.insertionRank(amongVisible: [1024, 1024], at: 2) == 2048) } // MARK: - Precision exhaustion → renumber, deterministically @Test func precisionExhaustionThenRenumberIsDeterministic() { func runScenario() -> (iterations: Int, renumbered: [Double]) { let lower = 1024.0 var upper = 2048.0 var iterations = 0 let iterationCap = 4000 while iterations < iterationCap, let mid = Ranks.midpoint(between: lower, and: upper) { upper = mid iterations += 1 } return (iterations, Ranks.renumbered(count: 5)) } let first = runScenario() let second = runScenario() // Precision must actually have been exhausted, not just hit the cap. #expect(first.iterations > 0) #expect(first.iterations < 4000) // Same scenario, run twice, must produce bit-identical results. #expect(first.iterations == second.iterations) #expect(first.renumbered == second.renumbered) // Renumbering yields clean whole multiples of 1024. #expect(first.renumbered == [1024, 2048, 3072, 4096, 5120]) } }