merge main + tests

sync3
Laurent 5 months ago
parent 5c7f74a384
commit 5958662655
  1. 36
      PadelClubData/Data/GroupStage.swift
  2. 4
      PadelClubData/Data/MatchScheduler.swift
  3. 2
      PadelClubData/Data/Tournament.swift
  4. 110
      PadelClubDataTests/SyncDataAccessTests.swift

@ -34,7 +34,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
// MARK: - Computed dependencies // MARK: - Computed dependencies
public func matches() -> [Match] { public func _matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] } guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index)
// Store.main.filter { $0.groupStage == self.id } // Store.main.filter { $0.groupStage == self.id }
@ -76,15 +76,15 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
public func isRunning() -> Bool { // at least a match has started public func isRunning() -> Bool { // at least a match has started
matches().anySatisfy({ $0.isRunning() }) _matches().anySatisfy({ $0.isRunning() })
} }
public func hasStarted() -> Bool { // meaning at least one match is over public func hasStarted() -> Bool { // meaning at least one match is over
matches().filter { $0.hasEnded() }.isEmpty == false _matches().filter { $0.hasEnded() }.isEmpty == false
} }
public func hasEnded() -> Bool { public func hasEnded() -> Bool {
let _matches = matches() let _matches = _matches()
if _matches.isEmpty { return false } if _matches.isEmpty { return false }
//guard teams().count == size else { return false } //guard teams().count == size else { return false }
return _matches.anySatisfy { $0.hasEnded() == false } == false return _matches.anySatisfy { $0.hasEnded() == false } == false
@ -102,7 +102,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
public func removeReturnMatches(onlyLast: Bool = false) { public func removeReturnMatches(onlyLast: Bool = false) {
var returnMatches = matches().filter({ $0.index >= matchCount }) var returnMatches = _matches().filter({ $0.index >= matchCount })
if onlyLast { if onlyLast {
let matchPhaseCount = matchPhaseCount - 1 let matchPhaseCount = matchPhaseCount - 1
returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount })
@ -115,7 +115,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
public var matchPhaseCount: Int { public var matchPhaseCount: Int {
let count = matches().count let count = _matches().count
if matchCount > 0 { if matchCount > 0 {
return count / matchCount return count / matchCount
} else { } else {
@ -153,7 +153,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
matches.append(newMatch) matches.append(newMatch)
} }
} else { } else {
for match in self.matches() { for match in self._matches() {
match.resetTeamScores(outsideOf: []) match.resetTeamScores(outsideOf: [])
teamScores.append(contentsOf: match.createTeamScores()) teamScores.append(contentsOf: match.createTeamScores())
} }
@ -168,7 +168,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
public func playedMatches() -> [Match] { public func playedMatches() -> [Match] {
let ordered = matches() let ordered = _matches()
let order = _matchOrder() let order = _matchOrder()
let matchCount = max(1, matchCount) let matchCount = max(1, matchCount)
let count = ordered.count / matchCount let count = ordered.count / matchCount
@ -256,7 +256,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
matchIndexes.append(index) matchIndexes.append(index)
} }
} }
return matches().filter { matchIndexes.contains($0.index%matchCount) } return _matches().filter { matchIndexes.contains($0.index%matchCount) }
} }
public func initialStartDate(forTeam team: TeamRegistration) -> Date? { public func initialStartDate(forTeam team: TeamRegistration) -> Date? {
@ -273,7 +273,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
matchIndexes.append(index) matchIndexes.append(index)
} }
} }
return matches().first(where: { matchIndexes.contains($0.index) }) return _matches().first(where: { matchIndexes.contains($0.index) })
} }
public func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { public func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
@ -321,7 +321,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
public func isReturnMatchEnabled() -> Bool { public func isReturnMatchEnabled() -> Bool {
matches().count > matchCount _matches().count > matchCount
} }
private func _matchOrder() -> [Int] { private func _matchOrder() -> [Int] {
@ -359,7 +359,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
public func returnMatchesSuffix(for matchIndex: Int) -> String { public func returnMatchesSuffix(for matchIndex: Int) -> String {
if matchCount > 0 { if matchCount > 0 {
let count = matches().count let count = _matches().count
if count > matchCount * 2 { if count > matchCount * 2 {
return " - vague \((matchIndex / matchCount) + 1)" return " - vague \((matchIndex / matchCount) + 1)"
} }
@ -401,7 +401,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
func _removeMatches() { func _removeMatches() {
self.tournamentStore?.matches.delete(contentOfs: matches()) self.tournamentStore?.matches.delete(contentOfs: _matches())
} }
func _numberOfMatchesToBuild() -> Int { func _numberOfMatchesToBuild() -> Int {
@ -420,7 +420,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2)) let combos = Array((0..<size).combinations(ofCount: 2))
let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil } let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil }
let matches = matches().filter { matchIndexes.contains($0.index) } let matches = _matches().filter { matchIndexes.contains($0.index) }
if matches.count > 1 { if matches.count > 1 {
let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!)
let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!)
@ -447,7 +447,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
return teamsSorted.first == teamPosition return teamsSorted.first == teamPosition
} else { } else {
if let matchIndex = combos.firstIndex(of: indexes), let match = self.matches().first(where: { $0.index == matchIndex }) { if let matchIndex = combos.firstIndex(of: indexes), let match = self._matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId return teamPosition.id == match.losingTeamId
} else { } else {
return false return false
@ -614,7 +614,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
} }
public func computedStartDate() -> Date? { public func computedStartDate() -> Date? {
return self.matches().sorted(by: \.computedStartDateForSorting).first?.startDate return self._matches().sorted(by: \.computedStartDateForSorting).first?.startDate
} }
public override func deleteDependencies(store: Store, actionOption: ActionOption) { public override func deleteDependencies(store: Store, actionOption: ActionOption) {
@ -627,7 +627,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
func insertOnServer() { func insertOnServer() {
self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self) self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self)
for match in self.matches() { for match in self._matches() {
match.insertOnServer() match.insertOnServer()
} }
} }
@ -655,7 +655,7 @@ extension GroupStage: Selectable {
} }
public func badgeValue() -> Int? { public func badgeValue() -> Int? {
return runningMatches(playedMatches: matches()).count return runningMatches(playedMatches: _matches()).count
} }
public func badgeValueColor() -> Color? { public func badgeValueColor() -> Color? {

@ -46,7 +46,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
groupStages = [specificGroupStage] groupStages = [specificGroupStage]
} }
let matches = groupStages.flatMap { $0.matches() } let matches = groupStages.flatMap { $0._matches() }
matches.forEach({ matches.forEach({
$0.removeCourt() $0.removeCourt()
$0.startDate = nil $0.startDate = nil
@ -142,7 +142,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable {
let _groupStages = groupStages let _groupStages = groupStages
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0.matches().count }.max() ?? 0 let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
var flattenedMatches = [Match]() var flattenedMatches = [Match]()
if simultaneousStart { if simultaneousStart {
// Flatten matches in a round-robin order by cycling through each group // Flatten matches in a round-robin order by cycling through each group

@ -825,7 +825,7 @@ defer {
} }
public func groupStagesMatches(atStep step: Int = 0) -> [Match] { public func groupStagesMatches(atStep step: Int = 0) -> [Match] {
return groupStages(atStep: step).flatMap({ $0.matches() }) return groupStages(atStep: step).flatMap({ $0._matches() })
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
} }

@ -607,7 +607,7 @@ struct SyncDataAccessTests {
} }
@Test func testMatchSharingThenTournamentDelete() async throws { @Test func testMatchSharingThenRevoking() async throws {
guard let userId1 = StoreCenter.main.userId else { guard let userId1 = StoreCenter.main.userId else {
throw TestError.notAuthenticated throw TestError.notAuthenticated
} }
@ -618,12 +618,12 @@ struct SyncDataAccessTests {
// Setup tournament // Setup tournament
let tournamentColA: SyncedCollection<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let tournamentColA: SyncedCollection<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let eventColA: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() let eventColA: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await tournamentColA.deleteAsync(contentOfs: tournamentColA)
let event = Event(creator: userId1) let event = Event(creator: userId1)
try await eventColA.addOrUpdateAsync(instance: event) try await eventColA.addOrUpdateAsync(instance: event)
let tournament = Tournament(event: event.id, name: "test_data_access_children") let tournament = Tournament(event: event.id, name: "testMatchSharingThenTournamentDelete")
tournament.relatedUser = userId1
try await tournamentColA.addOrUpdateAsync(instance: tournament) try await tournamentColA.addOrUpdateAsync(instance: tournament)
let tourStoreA = try StoreCenter.main.store(identifier: tournament.id) let tourStoreA = try StoreCenter.main.store(identifier: tournament.id)
@ -644,12 +644,91 @@ struct SyncDataAccessTests {
try await playerRegColA.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22]) try await playerRegColA.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22])
let round = Round(tournament: tournament.id) let round = Round(tournament: tournament.id)
try await roundColA.addOrUpdateAsync(instance: round)
let match = Match(round: round.id) let match = Match(round: round.id)
try await matchColA.addOrUpdateAsync(instance: match)
let ts1 = TeamScore(match: match.id, team: tr1) let ts1 = TeamScore(match: match.id, team: tr1)
let ts2 = TeamScore(match: match.id, team: tr2) let ts2 = TeamScore(match: match.id, team: tr2)
try await teamScoreColA.addOrUpdateAsync(contentOfs: [ts1, ts2])
try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [userId2])
let data = try await self.storeCenterB.testSynchronizeOnceAsync()
let syncData = try SyncData(data: data, storeCenter: self.storeCenterB)
let tournamentColB: SyncedCollection<Tournament> = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection()
#expect(syncData.shared.count == 1)
#expect(tournamentColB.count == 1)
let tourStoreB = try self.storeCenterB.store(identifier: tournament.id)
let matchColB: SyncedCollection<Match> = await tourStoreB.asyncLoadingSynchronizedCollection()
let playerRegColB: SyncedCollection<PlayerRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection()
let teamRegColB: SyncedCollection<TeamRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection()
#expect(matchColB.count == 1)
#expect(playerRegColB.count == 4)
#expect(teamRegColB.count == 2)
try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [])
let data2 = try await self.storeCenterB.testSynchronizeOnceAsync()
let syncData2 = try SyncData(data: data2, storeCenter: self.storeCenterB)
#expect(syncData2.revocations.count > 0)
#expect(matchColB.count == 0)
#expect(playerRegColB.count == 0)
#expect(teamRegColB.count == 0)
}
@Test func testMatchSharingThenTournamentDelete() async throws {
guard let userId1 = StoreCenter.main.userId else {
throw TestError.notAuthenticated
}
guard let userId2 = self.storeCenterB.userId else {
throw TestError.notAuthenticated
}
// Setup tournament
let tournamentColA: SyncedCollection<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
let eventColA: SyncedCollection<Event> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection()
try await tournamentColA.deleteAsync(contentOfs: tournamentColA)
let event = Event(creator: userId1)
try await eventColA.addOrUpdateAsync(instance: event)
let tournament = Tournament(event: event.id, name: "testMatchSharingThenTournamentDelete")
try await tournamentColA.addOrUpdateAsync(instance: tournament)
let tourStoreA = try StoreCenter.main.store(identifier: tournament.id)
let teamRegColA: SyncedCollection<TeamRegistration> = await tourStoreA.asyncLoadingSynchronizedCollection()
let playerRegColA: SyncedCollection<PlayerRegistration> = await tourStoreA.asyncLoadingSynchronizedCollection()
let roundColA: SyncedCollection<Round> = await tourStoreA.asyncLoadingSynchronizedCollection()
let matchColA: SyncedCollection<Match> = await tourStoreA.asyncLoadingSynchronizedCollection()
let teamScoreColA: SyncedCollection<TeamScore> = await tourStoreA.asyncLoadingSynchronizedCollection()
let tr1 = TeamRegistration(tournament: tournament.id)
let pr11 = PlayerRegistration(teamRegistration: tr1.id, firstName: "f1", lastName: "l1")
let pr12 = PlayerRegistration(teamRegistration: tr1.id, firstName: "f2", lastName: "l2")
let tr2 = TeamRegistration(tournament: tournament.id)
let pr21 = PlayerRegistration(teamRegistration: tr2.id, firstName: "f21", lastName: "l21")
let pr22 = PlayerRegistration(teamRegistration: tr2.id, firstName: "f22", lastName: "l22")
try await teamRegColA.addOrUpdateAsync(contentOfs: [tr1, tr2])
try await playerRegColA.addOrUpdateAsync(contentOfs: [pr11, pr12, pr21, pr22])
let round = Round(tournament: tournament.id)
try await roundColA.addOrUpdateAsync(instance: round) try await roundColA.addOrUpdateAsync(instance: round)
let match = Match(round: round.id)
try await matchColA.addOrUpdateAsync(instance: match) try await matchColA.addOrUpdateAsync(instance: match)
let ts1 = TeamScore(match: match.id, team: tr1)
let ts2 = TeamScore(match: match.id, team: tr2)
try await teamScoreColA.addOrUpdateAsync(contentOfs: [ts1, ts2]) try await teamScoreColA.addOrUpdateAsync(contentOfs: [ts1, ts2])
try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [userId2]) try await StoreCenter.main.setAuthorizedUsersAsync(for: match, users: [userId2])
@ -657,35 +736,44 @@ struct SyncDataAccessTests {
let data = try await self.storeCenterB.testSynchronizeOnceAsync() let data = try await self.storeCenterB.testSynchronizeOnceAsync()
let syncData = try SyncData(data: data, storeCenter: self.storeCenterB) let syncData = try SyncData(data: data, storeCenter: self.storeCenterB)
#expect(syncData.shared.count == 1)
let tournamentColB: SyncedCollection<Tournament> = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection() let tournamentColB: SyncedCollection<Tournament> = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection()
#expect(syncData.shared.count == 1)
#expect(tournamentColB.count == 1) #expect(tournamentColB.count == 1)
let tourStoreB = try self.storeCenterB.store(identifier: tournament.id) let tourStoreB = try self.storeCenterB.store(identifier: tournament.id)
let matchColB: SyncedCollection<Match> = await tourStoreB.asyncLoadingSynchronizedCollection() let matchColB: SyncedCollection<Match> = await tourStoreB.asyncLoadingSynchronizedCollection()
let playerRegColB: SyncedCollection<PlayerRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection() let playerRegColB: SyncedCollection<PlayerRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection()
let teamRegColB: SyncedCollection<TeamRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection() let teamRegColB: SyncedCollection<TeamRegistration> = await tourStoreB.asyncLoadingSynchronizedCollection()
let teamScoreColB: SyncedCollection<TeamScore> = await tourStoreB.asyncLoadingSynchronizedCollection()
#expect(matchColB.count == 1) #expect(matchColB.count == 1)
#expect(playerRegColB.count == 4) #expect(playerRegColB.count == 4)
#expect(teamRegColB.count == 2) #expect(teamRegColB.count == 2)
#expect(teamScoreColB.count == 2)
try await tournamentColA.deleteAsync(instance: tournament) try await roundColA.deleteAsync(instance: round)
let tournaments: [Tournament] = try await StoreCenter.main.service().get() #expect(roundColA.count == 0)
#expect(tournaments.count == 0)
#expect(tournamentColA.count == 0) try await Task.sleep(nanoseconds: 1_000_000_000) // wait for cascading deletes to be finished
let data2 = try await self.storeCenterB.testSynchronizeOnceAsync() let data2 = try await self.storeCenterB.testSynchronizeOnceAsync()
let syncData2 = try SyncData(data: data2, storeCenter: self.storeCenterB) let syncData2 = try SyncData(data: data2, storeCenter: self.storeCenterB)
#expect(tournamentColB.count == 0) for deletion in syncData2.deletions {
print(">>> deletion type = \(deletion.type), count = \(deletion.items.count)")
}
for revocation in syncData2.revocations {
print(">>> revocation type = \(revocation.type), count = \(revocation.items.count)")
}
#expect(syncData2.deletions.count > 0) #expect(syncData2.deletions.count > 0)
#expect(syncData2.revocations.count > 0)
#expect(matchColB.count == 0) #expect(matchColB.count == 0)
#expect(teamScoreColB.count == 0)
// the delete of round deletes the match, which should revoke granted objects like player/teams
#expect(playerRegColB.count == 0) #expect(playerRegColB.count == 0)
#expect(teamRegColB.count == 0) #expect(teamRegColB.count == 0)
@ -894,7 +982,7 @@ extension GroupStage {
matches.append(newMatch) matches.append(newMatch)
} }
} else { } else {
for match in self.matches() { for match in self._matches() {
match.resetTeamScores(outsideOf: []) match.resetTeamScores(outsideOf: [])
teamScores.append(contentsOf: match.createTeamScores()) teamScores.append(contentsOf: match.createTeamScores())
} }

Loading…
Cancel
Save