From 2b082252abf0aa673f23a4e61efb63b57bc67140 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 26 May 2025 12:31:04 +0200 Subject: [PATCH] Update sync tests and refactoring --- PadelClubData/Data/GroupStage.swift | 44 +-- PadelClubData/Data/MatchScheduler.swift | 4 +- PadelClubData/Data/Tournament.swift | 2 +- PadelClubDataTests/SyncDataAccessTests.swift | 271 +++++++++++++++++- PadelClubDataTests/SynchronizationTests.swift | 55 ++++ 5 files changed, 346 insertions(+), 30 deletions(-) diff --git a/PadelClubData/Data/GroupStage.swift b/PadelClubData/Data/GroupStage.swift index 3c951ad..40c9fed 100644 --- a/PadelClubData/Data/GroupStage.swift +++ b/PadelClubData/Data/GroupStage.swift @@ -36,7 +36,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { // MARK: - Computed dependencies - public func _matches() -> [Match] { + public func matches() -> [Match] { guard let tournamentStore = self.tournamentStore else { return [] } return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) // Store.main.filter { $0.groupStage == self.id } @@ -78,21 +78,21 @@ final public class GroupStage: BaseGroupStage, SideStorable { } 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 - _matches().filter { $0.hasEnded() }.isEmpty == false + matches().filter { $0.hasEnded() }.isEmpty == false } public func hasEnded() -> Bool { - let _matches = _matches() + let _matches = matches() if _matches.isEmpty { return false } //guard teams().count == size else { return false } return _matches.anySatisfy { $0.hasEnded() == false } == false } - fileprivate func _createMatch(index: Int) -> Match { + func createMatch(index: Int) -> Match { let match: Match = Match(groupStage: self.id, index: index, format: self.matchFormat, @@ -104,7 +104,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func removeReturnMatches(onlyLast: Bool = false) { - var returnMatches = _matches().filter({ $0.index >= matchCount }) + var returnMatches = matches().filter({ $0.index >= matchCount }) if onlyLast { let matchPhaseCount = matchPhaseCount - 1 returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) @@ -117,7 +117,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public var matchPhaseCount: Int { - let count = _matches().count + let count = matches().count if matchCount > 0 { return count / matchCount } else { @@ -130,7 +130,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { var matches = [Match]() let matchPhaseCount = matchPhaseCount for i in 0..<_numberOfMatchesToBuild() { - let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount) + let newMatch = self.createMatch(index: i + matchCount * matchPhaseCount) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) teamScores.append(contentsOf: newMatch.createTeamScores()) matches.append(newMatch) @@ -149,13 +149,13 @@ final public class GroupStage: BaseGroupStage, SideStorable { _removeMatches() for i in 0..<_numberOfMatchesToBuild() { - let newMatch = self._createMatch(index: i) + let newMatch = self.createMatch(index: i) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) teamScores.append(contentsOf: newMatch.createTeamScores()) matches.append(newMatch) } } else { - for match in _matches() { + for match in matches() { match.resetTeamScores(outsideOf: []) teamScores.append(contentsOf: match.createTeamScores()) } @@ -170,7 +170,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func playedMatches() -> [Match] { - let ordered = _matches() + let ordered = matches() let order = _matchOrder() let matchCount = max(1, matchCount) let count = ordered.count / matchCount @@ -258,7 +258,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { 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? { @@ -275,7 +275,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { 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] { @@ -323,7 +323,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { } public func isReturnMatchEnabled() -> Bool { - _matches().count > matchCount + matches().count > matchCount } private func _matchOrder() -> [Int] { @@ -357,7 +357,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { public func returnMatchesSuffix(for matchIndex: Int) -> String { if matchCount > 0 { - let count = _matches().count + let count = matches().count if count > matchCount * 2 { return " - vague \((matchIndex / matchCount) + 1)" } @@ -398,11 +398,11 @@ final public class GroupStage: BaseGroupStage, SideStorable { return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? [] } - private func _removeMatches() { - self.tournamentStore?.matches.delete(contentOfs: _matches()) + func _removeMatches() { + self.tournamentStore?.matches.delete(contentOfs: matches()) } - private func _numberOfMatchesToBuild() -> Int { + func _numberOfMatchesToBuild() -> Int { (size * (size - 1)) / 2 } @@ -418,7 +418,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. 1 { let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) @@ -445,7 +445,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { return teamsSorted.first == teamPosition } else { - if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { + if let matchIndex = combos.firstIndex(of: indexes), let match = matches().first(where: { $0.index == matchIndex }) { return teamPosition.id == match.losingTeamId } else { return false @@ -617,7 +617,7 @@ final public class GroupStage: BaseGroupStage, SideStorable { func insertOnServer() { self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self) - for match in self._matches() { + for match in self.matches() { match.insertOnServer() } } @@ -645,7 +645,7 @@ extension GroupStage: Selectable { } public func badgeValue() -> Int? { - return runningMatches(playedMatches: _matches()).count + return runningMatches(playedMatches: matches()).count } public func badgeValueColor() -> Color? { diff --git a/PadelClubData/Data/MatchScheduler.swift b/PadelClubData/Data/MatchScheduler.swift index e2b2466..59c2ae8 100644 --- a/PadelClubData/Data/MatchScheduler.swift +++ b/PadelClubData/Data/MatchScheduler.swift @@ -75,7 +75,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { groupStages = [specificGroupStage] } - let matches = groupStages.flatMap { $0._matches() } + let matches = groupStages.flatMap { $0.matches() } matches.forEach({ $0.removeCourt() $0.startDate = nil @@ -153,7 +153,7 @@ final public class MatchScheduler: BaseMatchScheduler, SideStorable { let _groupStages = groupStages // 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]() if simultaneousStart { // Flatten matches in a round-robin order by cycling through each group diff --git a/PadelClubData/Data/Tournament.swift b/PadelClubData/Data/Tournament.swift index eccf5d0..5d352d6 100644 --- a/PadelClubData/Data/Tournament.swift +++ b/PadelClubData/Data/Tournament.swift @@ -818,7 +818,7 @@ defer { } 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!) }) } diff --git a/PadelClubDataTests/SyncDataAccessTests.swift b/PadelClubDataTests/SyncDataAccessTests.swift index 03d1ec1..366c86f 100644 --- a/PadelClubDataTests/SyncDataAccessTests.swift +++ b/PadelClubDataTests/SyncDataAccessTests.swift @@ -24,15 +24,28 @@ struct SyncDataAccessTests { let conf = Config.server + let dir = "storage" let dirA = "storageA" let dirB = "storageB" - FileManager.default.deleteDirectoryInDocuments(directoryName: dirA) + FileManager.default.deleteDirectoryInDocuments(directoryName: dir) FileManager.default.deleteDirectoryInDocuments(directoryName: dirB) self.storeCenterA = StoreCenter(directoryName: dirA) self.storeCenterB = StoreCenter(directoryName: dirB) + // StoreCenter.main + StoreCenter.main.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) + StoreCenter.main.tokenKeychain = MockKeychainStore(fileName: "\(dir)/token.json") + StoreCenter.main.deviceKeychain = MockKeychainStore(fileName: "\(dir)/device.json") + try StoreCenter.main.deviceKeychain.add(value: UUID().uuidString) + StoreCenter.main.classProject = "PadelClubData" + + let token = try? StoreCenter.main.rawTokenShouldNotBeUsed() + if token == nil { + try await self.login(storeCenter: StoreCenter.main, username: self.username1, password: self.password1) + } + // StoreCenter A self.storeCenterA.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) self.storeCenterA.tokenKeychain = MockKeychainStore(fileName: "\(dirA)/token.json") @@ -40,8 +53,8 @@ struct SyncDataAccessTests { try self.storeCenterA.deviceKeychain.add(value: UUID().uuidString) self.storeCenterA.classProject = "PadelClubData" - let token = try? self.storeCenterA.rawTokenShouldNotBeUsed() - if token == nil { + let tokenA = try? self.storeCenterA.rawTokenShouldNotBeUsed() + if tokenA == nil { try await self.login(storeCenter: self.storeCenterA, username: self.username1, password: self.password1) } @@ -53,8 +66,8 @@ struct SyncDataAccessTests { self.storeCenterB.classProject = "PadelClubData" - let token2 = try? self.storeCenterB.rawTokenShouldNotBeUsed() - if token2 == nil { + let tokenB = try? self.storeCenterB.rawTokenShouldNotBeUsed() + if tokenB == nil { try await self.login(storeCenter: self.storeCenterB, username: self.username2, password: self.password2) } @@ -346,4 +359,252 @@ struct SyncDataAccessTests { } + @Test func testBuildEverything() async throws { + + guard let _ = StoreCenter.main.userId else { + throw TestError.notAuthenticated + } + guard let userId2 = self.storeCenterB.userId else { + throw TestError.notAuthenticated + } + + // Cleanup + + let tournamentColA: SyncedCollection = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColB: SyncedCollection = await self.storeCenterB.mainStore.asyncLoadingSynchronizedCollection() + + let tournamentsToDelete: [Tournament] = try await StoreCenter.main.service().get() + try await tournamentColA.deleteAsync(contentOfs: tournamentsToDelete) + + // Setup tournament + build everything + let tournament = Tournament() + + try await tournamentColA.addOrUpdateAsync(instance: tournament) + try await tournament.deleteAndBuildEverythingAsync() + let tourStoreA = try StoreCenter.main.store(identifier: tournament.id) + + let gsColA: SyncedCollection = try tourStoreA.syncedCollection() + let roundColA: SyncedCollection = try tourStoreA.syncedCollection() + let matchesColA: SyncedCollection = try tourStoreA.syncedCollection() + #expect(gsColA.count == 4) + #expect(roundColA.count == 15) + #expect(matchesColA.count == 56) + + // todo add sharing + try await StoreCenter.main.setAuthorizedUsersAsync(for: tournament, users: [userId2]) + + // Sync with 2nd store + try await self.storeCenterB.testSynchronizeOnceAsync() + #expect(tournamentColB.count == 1) + + let tourStoreB = try self.storeCenterB.store(identifier: tournament.id) + + let gsColB: SyncedCollection = try tourStoreB.syncedCollection() + let roundColB: SyncedCollection = try tourStoreB.syncedCollection() + let matchesColB: SyncedCollection = try tourStoreB.syncedCollection() + #expect(gsColB.count == 4) + #expect(roundColB.count == 15) + #expect(matchesColB.count == 56) + + // change setup + build everything + tournament.groupStageCount = 2 + tournament.teamCount = 20 + try await tournamentColA.addOrUpdateAsync(instance: tournament) + try await tournament.deleteAndBuildEverythingAsync() + + #expect(gsColA.count == 2) + #expect(roundColA.count == 15) + #expect(matchesColA.count == 44) + + // Sync with 2nd store + let data = try await self.storeCenterB.testSynchronizeOnceAsync() + let syncData = try SyncData(data: data, storeCenter: self.storeCenterB) + + #expect(gsColB.count == 2) + #expect(roundColB.count == 15) + #expect(matchesColB.count == 44) + + } + +} + + +extension Tournament { + + @MainActor + public func deleteAndBuildEverythingAsync(preset: PadelTournamentStructurePreset = .manual) async throws { + resetBracketPosition() + try await deleteStructureAsync() + try await deleteGroupStagesAsync() + + switch preset { + case .doubleGroupStage: + try await buildGroupStagesAsync() + try await addNewGroupStageStepAsync() + qualifiedPerGroupStage = 0 + groupStageAdditionalQualified = 0 + default: + try await buildGroupStagesAsync() + try await buildBracketAsync() + } + } + + public func addNewGroupStageStepAsync() async throws { + let lastStep = lastStep() + 1 + for i in 0.. 0 { + switch groupStageOrderingMode { + case .random: + try await setGroupStageAsync(randomize: true, keepExistingMatches: keepExistingMatches) + case .snake: + try await setGroupStageAsync(randomize: false, keepExistingMatches: keepExistingMatches) + case .swiss: + try await setGroupStageAsync(randomize: true, keepExistingMatches: keepExistingMatches) + } + } + } + + public func setGroupStageAsync(randomize: Bool, keepExistingMatches: Bool = false) async throws { + let groupStages = groupStages() + let numberOfBracketsAsInt = groupStages.count +// let teamsPerBracket = teamsPerBracket + if groupStageCount != numberOfBracketsAsInt { + try await deleteGroupStagesAsync() + try await buildGroupStagesAsync() + } else { +// setGroupStageTeams(randomize: randomize) + for groupStage in groupStages { + try await groupStage.buildMatchesAsync(keepExistingMatches: keepExistingMatches) + } + } + } + + public func buildBracketAsync(minimalBracketTeamCount: Int? = nil) async throws { + guard rounds().isEmpty else { return } + let roundCount = RoundRule.numberOfRounds(forTeams: minimalBracketTeamCount ?? bracketTeamCount()) + let matchCount = RoundRule.numberOfMatches(forTeams: minimalBracketTeamCount ?? bracketTeamCount()) + + let rounds = (0.. 1 else { return } + guard let tournamentStore else { return } + let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) + let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat + + let rounds = (0.. = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() + let tournamentColB: SyncedCollection = await self.secondStoreCenter.mainStore.asyncLoadingSynchronizedCollection() + + let tournamentsToDelete: [Tournament] = try await StoreCenter.main.service().get() + tournamentColA.delete(contentOfs: tournamentsToDelete) + + // Setup tournament + build everything + let tournament = Tournament() + + try await tournamentColA.addOrUpdateAsync(instance: tournament) + try await tournament.deleteAndBuildEverything() + let tourStore = try StoreCenter.main.store(identifier: tournament.id) + + let gsColA: SyncedCollection = try tourStore.syncedCollection() + let roundColA: SyncedCollection = try tourStore.syncedCollection() + let matchesColA: SyncedCollection = try tourStore.syncedCollection() + #expect(gsColA.count == 4) + #expect(roundColA.count == 15) + #expect(matchesColA.count == 56) + + // Sync with 2nd store + try await secondStoreCenter.testSynchronizeOnceAsync() + #expect(tournamentColB.count == 1) + + let tourStoreB = try secondStoreCenter.store(identifier: tournament.id) + + let gsColB: SyncedCollection = try tourStoreB.syncedCollection() + let roundColB: SyncedCollection = try tourStoreB.syncedCollection() + let matchesColB: SyncedCollection = try tourStoreB.syncedCollection() + #expect(gsColB.count == 4) + #expect(roundColB.count == 15) + #expect(matchesColB.count == 56) + + // change setup + build everything + tournament.groupStageCount = 2 + tournament.teamCount = 20 + try await tournamentColA.addOrUpdateAsync(instance: tournament) + try await tournament.deleteAndBuildEverything() + + #expect(gsColA.count == 2) + #expect(roundColA.count == 15) + #expect(matchesColA.count == 44) + + // Sync with 2nd store + try await secondStoreCenter.testSynchronizeOnceAsync() + + #expect(gsColB.count == 2) + #expect(roundColB.count == 15) + #expect(matchesColB.count == 44) + + } }