|
|
|
@ -24,15 +24,28 @@ struct SyncDataAccessTests { |
|
|
|
|
|
|
|
|
|
|
|
let conf = Config.server |
|
|
|
let conf = Config.server |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let dir = "storage" |
|
|
|
let dirA = "storageA" |
|
|
|
let dirA = "storageA" |
|
|
|
let dirB = "storageB" |
|
|
|
let dirB = "storageB" |
|
|
|
|
|
|
|
|
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: dirA) |
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: dir) |
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: dirB) |
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: dirB) |
|
|
|
|
|
|
|
|
|
|
|
self.storeCenterA = StoreCenter(directoryName: dirA) |
|
|
|
self.storeCenterA = StoreCenter(directoryName: dirA) |
|
|
|
self.storeCenterB = StoreCenter(directoryName: dirB) |
|
|
|
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 |
|
|
|
// StoreCenter A |
|
|
|
self.storeCenterA.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) |
|
|
|
self.storeCenterA.configureURLs(secureScheme: conf.secure, domain: conf.domain, webSockets: false, useSynchronization: true) |
|
|
|
self.storeCenterA.tokenKeychain = MockKeychainStore(fileName: "\(dirA)/token.json") |
|
|
|
self.storeCenterA.tokenKeychain = MockKeychainStore(fileName: "\(dirA)/token.json") |
|
|
|
@ -40,8 +53,8 @@ struct SyncDataAccessTests { |
|
|
|
try self.storeCenterA.deviceKeychain.add(value: UUID().uuidString) |
|
|
|
try self.storeCenterA.deviceKeychain.add(value: UUID().uuidString) |
|
|
|
self.storeCenterA.classProject = "PadelClubData" |
|
|
|
self.storeCenterA.classProject = "PadelClubData" |
|
|
|
|
|
|
|
|
|
|
|
let token = try? self.storeCenterA.rawTokenShouldNotBeUsed() |
|
|
|
let tokenA = try? self.storeCenterA.rawTokenShouldNotBeUsed() |
|
|
|
if token == nil { |
|
|
|
if tokenA == nil { |
|
|
|
try await self.login(storeCenter: self.storeCenterA, username: self.username1, password: self.password1) |
|
|
|
try await self.login(storeCenter: self.storeCenterA, username: self.username1, password: self.password1) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -53,8 +66,8 @@ struct SyncDataAccessTests { |
|
|
|
|
|
|
|
|
|
|
|
self.storeCenterB.classProject = "PadelClubData" |
|
|
|
self.storeCenterB.classProject = "PadelClubData" |
|
|
|
|
|
|
|
|
|
|
|
let token2 = try? self.storeCenterB.rawTokenShouldNotBeUsed() |
|
|
|
let tokenB = try? self.storeCenterB.rawTokenShouldNotBeUsed() |
|
|
|
if token2 == nil { |
|
|
|
if tokenB == nil { |
|
|
|
try await self.login(storeCenter: self.storeCenterB, username: self.username2, password: self.password2) |
|
|
|
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<Tournament> = await StoreCenter.main.mainStore.asyncLoadingSynchronizedCollection() |
|
|
|
|
|
|
|
let tournamentColB: SyncedCollection<Tournament> = 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<GroupStage> = try tourStoreA.syncedCollection() |
|
|
|
|
|
|
|
let roundColA: SyncedCollection<Round> = try tourStoreA.syncedCollection() |
|
|
|
|
|
|
|
let matchesColA: SyncedCollection<Match> = 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<GroupStage> = try tourStoreB.syncedCollection() |
|
|
|
|
|
|
|
let roundColB: SyncedCollection<Round> = try tourStoreB.syncedCollection() |
|
|
|
|
|
|
|
let matchesColB: SyncedCollection<Match> = 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..<teamsPerGroupStage { |
|
|
|
|
|
|
|
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep) |
|
|
|
|
|
|
|
try await self.tournamentStore?.groupStages.addOrUpdateAsync(instance: gs) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
groupStages(atStep: 1).forEach { $0.buildMatches() } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func deleteStructureAsync() async throws { |
|
|
|
|
|
|
|
try await self.tournamentStore?.rounds.deleteAsync(contentOfs: rounds()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func deleteGroupStagesAsync() async throws { |
|
|
|
|
|
|
|
try await self.tournamentStore?.groupStages.deleteAsync(contentOfs: allGroupStages()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func buildGroupStagesAsync() async throws { |
|
|
|
|
|
|
|
guard groupStages().isEmpty, let tournamentStore = self.tournamentStore else { |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var _groupStages = [GroupStage]() |
|
|
|
|
|
|
|
for index in 0..<groupStageCount { |
|
|
|
|
|
|
|
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageFormat) |
|
|
|
|
|
|
|
_groupStages.append(groupStage) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try await self.tournamentStore?.groupStages.addOrUpdateAsync(contentOfs: _groupStages) |
|
|
|
|
|
|
|
try await refreshGroupStagesAsync() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func refreshGroupStagesAsync(keepExistingMatches: Bool = false) async throws { |
|
|
|
|
|
|
|
unsortedTeams().forEach { team in |
|
|
|
|
|
|
|
team.groupStage = nil |
|
|
|
|
|
|
|
team.groupStagePosition = nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if groupStageCount > 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..<roundCount).map { //index 0 is the final |
|
|
|
|
|
|
|
return Round(tournament: id, index: $0, matchFormat: matchFormat, loserBracketMode: loserBracketMode) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if rounds.isEmpty { |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try await self.tournamentStore?.rounds.addOrUpdateAsync(contentOfs: rounds) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let matches = (0..<matchCount).map { //0 is final match |
|
|
|
|
|
|
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0) |
|
|
|
|
|
|
|
let round = rounds[roundIndex] |
|
|
|
|
|
|
|
return Match(round: round.id, index: $0, format: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0))) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try await self.tournamentStore?.matches.addOrUpdateAsync(contentOfs: matches) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for round in rounds { |
|
|
|
|
|
|
|
try await round.buildLoserBracketAsync() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension GroupStage { |
|
|
|
|
|
|
|
public func buildMatchesAsync(keepExistingMatches: Bool = false) async throws { |
|
|
|
|
|
|
|
var teamScores = [TeamScore]() |
|
|
|
|
|
|
|
var matches = [Match]() |
|
|
|
|
|
|
|
clearScoreCache() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if keepExistingMatches == false { |
|
|
|
|
|
|
|
_removeMatches() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i in 0..<_numberOfMatchesToBuild() { |
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
|
|
match.resetTeamScores(outsideOf: []) |
|
|
|
|
|
|
|
teamScores.append(contentsOf: match.createTeamScores()) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try await self.tournamentStore?.matches.addOrUpdateAsync(contentOfs: matches) |
|
|
|
|
|
|
|
try await self.tournamentStore?.teamScores.addOrUpdateAsync(contentOfs: teamScores) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension Round { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func buildLoserBracketAsync() async throws { |
|
|
|
|
|
|
|
guard loserRounds().isEmpty else { return } |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
|
|
|
|
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) |
|
|
|
|
|
|
|
guard currentRoundMatchCount > 1 else { return } |
|
|
|
|
|
|
|
guard let tournamentStore else { return } |
|
|
|
|
|
|
|
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) |
|
|
|
|
|
|
|
let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let rounds = (0..<roundCount).map { //index 0 is the final |
|
|
|
|
|
|
|
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat) |
|
|
|
|
|
|
|
round.parent = id //parent |
|
|
|
|
|
|
|
//titles[round.id] = round.roundTitle(initialMode: true) |
|
|
|
|
|
|
|
return round |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
try await tournamentStore.rounds.addOrUpdateAsync(contentOfs: rounds) |
|
|
|
|
|
|
|
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let matches = (0..<matchCount).map { //0 is final match |
|
|
|
|
|
|
|
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0) |
|
|
|
|
|
|
|
let round = rounds[roundIndex] |
|
|
|
|
|
|
|
//let title = titles[round.id] |
|
|
|
|
|
|
|
return Match(round: round.id, index: $0, format: loserBracketMatchFormat) |
|
|
|
|
|
|
|
//initial mode let the roundTitle give a name without considering the playable match |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try await tournamentStore.matches.addOrUpdateAsync(contentOfs: matches) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for round in rounds { |
|
|
|
|
|
|
|
try await round.buildLoserBracketAsync() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|