Skip to content

Commit

Permalink
test: Bracket creation and simulation tests (#8)
Browse files Browse the repository at this point in the history
* feat: Color styling for pending bracket view games

* test: Add bracket simulation tests

* feat: Filter by league for live game scores

* chore: Asset symbol extension setting

* feat: Add refresh button for live games
  • Loading branch information
mvirgo authored May 21, 2024
1 parent be12924 commit 94f85f8
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 51 deletions.
30 changes: 30 additions & 0 deletions foam-madness-tests/CoreDataTestStack.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// CoreDataTestStack.swift
// foam-madness-tests
//
// Created by Michael Virgo on 5/20/24.
// Copyright © 2024 mvirgo. All rights reserved.
//

import CoreData

class CoreDataTestStack {
static let shared = CoreDataTestStack()

private static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "foam-madness")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()

var context: NSManagedObjectContext {
return Self.persistentContainer.viewContext
}
}
82 changes: 82 additions & 0 deletions foam-madness-tests/foam_madness_sim_custom_bracket_test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// foam_madness_sim_custom_bracket_test.swift
// foam-madness-tests
//
// Created by Michael Virgo on 5/20/24.
// Copyright © 2024 mvirgo. All rights reserved.
//

import CoreData
import XCTest
@testable import foam_madness

final class foam_madness_sim_custom_bracket_test: XCTestCase {
var bracketCreationController: BracketCreationController!
var viewContext: NSManagedObjectContext!

override func setUpWithError() throws {
viewContext = CoreDataTestStack.shared.context
bracketCreationController = BracketCreationController(context: viewContext)
}

override func tearDownWithError() throws {
viewContext = nil
}

func createCustomTournament(numTeams: Int, fillType: CustomType) -> Tournament {
return bracketCreationController.createCustomBracket(
numTeams: numTeams,
isWomens: false,
tournamentName: "\(fillType)\(numTeams)",
isSimulated: true,
useLeft: false,
shotsPerRound: AppConstants.defaultShotsPerRound)
}

// Check all allowed custom bracket sizes with random fills
func testCreateAndSimRandomFillCustomBracket() throws {
for numTeams in numTeamsArray {
let tournament = createCustomTournament(numTeams: numTeams, fillType: CustomType.random)
bracketCreationController.fillTournamentWithRandomTeams(tournament)
let winner = bracketCreationController.simulateTournament(tournament: tournament)

XCTAssert(winner != "")
}
}

// Check all allowed custom bracket sizes with an existing bracket fill
func testCreateAndSimExistingFillCustomBracket() throws {
let brackets = BracketHelper.loadBrackets()
let fillBracket = brackets.randomElement()!

for numTeams in numTeamsArray {
if (numTeams == 4) {
// Skip existing for 4 team bracket - not allowed
continue
}
let tournament = createCustomTournament(numTeams: numTeams, fillType: CustomType.existing)
bracketCreationController.fillTournamentFromExistingCustom(tournament, fillBracket.file)
let winner = bracketCreationController.simulateTournament(tournament: tournament)

XCTAssert(winner != "")
}
}

// Check creation of custom brackets has no teams initially filled
func testCreateCustomBracketHasNoTeams() throws {
for numTeams in numTeamsArray {
let tournament = createCustomTournament(numTeams: numTeams, fillType: CustomType.selectAll)
let games = Array(tournament.games!) as! [Game]
let minRound = games.min(by: { $0.round < $1.round })?.round ?? 0
let gamesInMinRound = games.filter({ $0.round == minRound }).count

XCTAssert(gamesInMinRound == numTeams / 2)

for game in games {
if game.teams?.count ?? 0 > 0 {
XCTFail("Improperly found set teams in selectAll custom tournament of size \(numTeams)")
}
}
}
}
}
42 changes: 42 additions & 0 deletions foam-madness-tests/foam_madness_sim_existing_bracket_test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// foam_madness_sim_existing_bracket_test.swift
// foam-madness-tests
//
// Created by Michael Virgo on 5/20/24.
// Copyright © 2024 mvirgo. All rights reserved.
//

import CoreData
import XCTest
@testable import foam_madness

final class foam_madness_sim_existing_bracket_test: XCTestCase {
var bracketCreationController: BracketCreationController!
var viewContext: NSManagedObjectContext!

override func setUpWithError() throws {
viewContext = CoreDataTestStack.shared.context
bracketCreationController = BracketCreationController(context: viewContext)
}

override func tearDownWithError() throws {
viewContext = nil
}

func testCreateAndSimExistingBrackets() throws {
let brackets = BracketHelper.loadBrackets()

for bracket in brackets {
let tournament = bracketCreationController.createBracketFromFile(
bracketLocation: bracket.file,
tournamentName: bracket.name,
isSimulated: true,
useLeft: false,
shotsPerRound: AppConstants.defaultShotsPerRound
)
let winner = bracketCreationController.simulateTournament(tournament: tournament)

XCTAssert(winner != "")
}
}
}
20 changes: 19 additions & 1 deletion foam-madness.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
D15865952B575ECE009E486A /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865942B575ECE009E486A /* PrimaryButtonStyle.swift */; };
D158B32D2B9A96720002D2F6 /* GameStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158B32C2B9A96720002D2F6 /* GameStatsController.swift */; };
D16485722BE9D7AC006BB362 /* BracketWinnerLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16485712BE9D7AB006BB362 /* BracketWinnerLine.swift */; };
D168622F2BFC40F700649094 /* foam_madness_sim_existing_bracket_test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D168622E2BFC40F700649094 /* foam_madness_sim_existing_bracket_test.swift */; };
D16862312BFC41F000649094 /* CoreDataTestStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16862302BFC41F000649094 /* CoreDataTestStack.swift */; };
D16862332BFC450B00649094 /* foam_madness_sim_custom_bracket_test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16862322BFC450B00649094 /* foam_madness_sim_custom_bracket_test.swift */; };
D16862352BFC560000649094 /* LiveGamesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16862342BFC560000649094 /* LiveGamesModel.swift */; };
D16BEEB42BF7E605008D06B0 /* SelectInitialBracketShellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16BEEB32BF7E605008D06B0 /* SelectInitialBracketShellView.swift */; };
D17FF6012BA140DA00149D63 /* SearchTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17FF6002BA140DA00149D63 /* SearchTeamView.swift */; };
D17FF6052BA1454900149D63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17FF6042BA1454900149D63 /* SearchBar.swift */; };
Expand Down Expand Up @@ -162,6 +166,10 @@
D15865942B575ECE009E486A /* PrimaryButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButtonStyle.swift; sourceTree = "<group>"; };
D158B32C2B9A96720002D2F6 /* GameStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStatsController.swift; sourceTree = "<group>"; };
D16485712BE9D7AB006BB362 /* BracketWinnerLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketWinnerLine.swift; sourceTree = "<group>"; };
D168622E2BFC40F700649094 /* foam_madness_sim_existing_bracket_test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = foam_madness_sim_existing_bracket_test.swift; sourceTree = "<group>"; };
D16862302BFC41F000649094 /* CoreDataTestStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestStack.swift; sourceTree = "<group>"; };
D16862322BFC450B00649094 /* foam_madness_sim_custom_bracket_test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = foam_madness_sim_custom_bracket_test.swift; sourceTree = "<group>"; };
D16862342BFC560000649094 /* LiveGamesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveGamesModel.swift; sourceTree = "<group>"; };
D16BEEB32BF7E605008D06B0 /* SelectInitialBracketShellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectInitialBracketShellView.swift; sourceTree = "<group>"; };
D17FF6002BA140DA00149D63 /* SearchTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTeamView.swift; sourceTree = "<group>"; };
D17FF6042BA1454900149D63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -308,6 +316,7 @@
7ACBCB35250EC71400EBA5B1 /* Live Games API */ = {
isa = PBXGroup;
children = (
D16862342BFC560000649094 /* LiveGamesModel.swift */,
7ACBCB36250EC73900EBA5B1 /* APIClient.swift */,
7ACBCB38250EC7DA00EBA5B1 /* LiveGamesResponse.swift */,
7ACBCB3A250EC7FC00EBA5B1 /* ErrorResponse.swift */,
Expand Down Expand Up @@ -490,6 +499,9 @@
D1A67BEE29BD6E4D00E1D55B /* foam_madness_teams_file_test.swift */,
D1A67BEC29BD631D00E1D55B /* foam_madness_probability_file_test.swift */,
D1A67BE529BD48E200E1D55B /* foam_madness_bracket_file_tests.swift */,
D168622E2BFC40F700649094 /* foam_madness_sim_existing_bracket_test.swift */,
D16862322BFC450B00649094 /* foam_madness_sim_custom_bracket_test.swift */,
D16862302BFC41F000649094 /* CoreDataTestStack.swift */,
);
path = "foam-madness-tests";
sourceTree = "<group>";
Expand Down Expand Up @@ -548,7 +560,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1520;
LastUpgradeCheck = 1530;
ORGANIZATIONNAME = mvirgo;
TargetAttributes = {
7A2ECEE4242EDE920065E369 = {
Expand Down Expand Up @@ -641,6 +653,7 @@
D12027022BF7A00B00A10EA7 /* SelectYearView.swift in Sources */,
D1C757A12B9A62C500817E1A /* TournamentStatsController.swift in Sources */,
D15865802B5750F9009E486A /* ShootModeView.swift in Sources */,
D16862352BFC560000649094 /* LiveGamesModel.swift in Sources */,
D113300D2BE5952B00F6DF30 /* BracketGameCell.swift in Sources */,
D13EEA582BF9903B00F61B51 /* UpdateRegionsView.swift in Sources */,
D1BF49392BA805AA00A00669 /* AppConstants.swift in Sources */,
Expand Down Expand Up @@ -690,7 +703,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D16862332BFC450B00649094 /* foam_madness_sim_custom_bracket_test.swift in Sources */,
D1A67BEF29BD6E4D00E1D55B /* foam_madness_teams_file_test.swift in Sources */,
D168622F2BFC40F700649094 /* foam_madness_sim_existing_bracket_test.swift in Sources */,
D16862312BFC41F000649094 /* CoreDataTestStack.swift in Sources */,
D1A67BED29BD631D00E1D55B /* foam_madness_probability_file_test.swift in Sources */,
D1A67BE629BD48E200E1D55B /* foam_madness_bracket_file_tests.swift in Sources */,
);
Expand Down Expand Up @@ -722,6 +738,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
Expand Down Expand Up @@ -786,6 +803,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
Expand Down
1 change: 1 addition & 0 deletions foam-madness/Controller/AppConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
struct AppConstants {
static let defaultShotsPerRound = 10
static let defaultUseBracketView = false
static let refreshDebounceSeconds: Double = 20.0
}
53 changes: 53 additions & 0 deletions foam-madness/Model/Live Games API/LiveGamesModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// LiveGamesModel.swift
// foam-madness
//
// Created by Michael Virgo on 5/20/24.
// Copyright © 2024 mvirgo. All rights reserved.
//

import SwiftUI

class LiveGamesModel: ObservableObject {
@Published var liveGames: [Event] = []
@Published var loading = false
private var lastRefreshTime: Date?

init() {
refreshData()
}

func refreshData() {
let now = Date()
if let lastRefreshTime = lastRefreshTime, now.timeIntervalSince(lastRefreshTime) < AppConstants.refreshDebounceSeconds {
// Skip if within the debounce delay
return
}
lastRefreshTime = now
loadGames()
}

private func loadGames() {
loading = true
liveGames = []
APIClient.getScores(url: APIClient.Endpoints.getNCAAMScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getNCAAWScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getNBAScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getWNBAScores.url, completion: handleLiveGameScores(response:error:))
}

private func handleLiveGameScores(response: LiveGamesResponse?, error: Error?) {
if let error = error {
print(error.localizedDescription)
} else if let response = response {
// Add events to liveGames array
for (_, var game) in response.events.enumerated() {
// Note that there will only be one league in NCAA or (W)NBA APIs
// Add game
game.league = response.leagues[0].abbreviation
liveGames.append(game)
}
}
loading = false
}
}
81 changes: 38 additions & 43 deletions foam-madness/View/Live/LiveGamesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,55 @@
import SwiftUI

struct LiveGamesView: View {
@State var liveGames = [Event]()
@State var loading = false
let cellPadding = 4.0
let rowCells = 3.0
@StateObject private var liveGamesModel = LiveGamesModel()
@State var filter = ""
private let cellPadding = 4.0
private let rowCells = 3.0

var body: some View {
GeometryReader { geometry in
let maxWidth = (geometry.size.width - cellPadding * rowCells) / rowCells
let filteredGames =
filter == ""
? liveGamesModel.liveGames
: liveGamesModel.liveGames.filter({ $0.league == filter })

if (liveGames.count > 0) {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()),GridItem(.flexible()),GridItem(.flexible())], spacing: cellPadding) {
ForEach(liveGames)
{ game in
LiveGamesCell(game: game)
.frame(width: maxWidth, height: maxWidth)
.cornerRadius(5)
}
}.padding(cellPadding)
VStack {
Picker("", selection: $filter) {
Text("All Leagues").tag("")
Text("Mens's NCAA").tag("NCAAM")
Text("Women's NCAA").tag("NCAAW")
Text("NBA").tag("NBA")
Text("WNBA").tag("WNBA")
}.accentColor(commonBlue)
if (filteredGames.count > 0) {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()),GridItem(.flexible()),GridItem(.flexible())], spacing: cellPadding) {
ForEach(filteredGames)
{ game in
LiveGamesCell(game: game)
.frame(width: maxWidth, height: maxWidth)
.cornerRadius(5)
}
}.padding(cellPadding)
}
} else {
VStack {
Text(liveGamesModel.loading ? "Loading games..." : "No games found.").font(.largeTitle)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
} else {
VStack {
Text(loading ? "Loading games..." : "No games found.").font(.largeTitle)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationTitle("Live Game Scores")
.onAppear {
loadGames()
}
}

private func loadGames() {
loading = true
APIClient.getScores(url: APIClient.Endpoints.getNCAAMScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getNCAAWScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getNBAScores.url, completion: handleLiveGameScores(response:error:))
APIClient.getScores(url: APIClient.Endpoints.getWNBAScores.url, completion: handleLiveGameScores(response:error:))
}

private func handleLiveGameScores(response: LiveGamesResponse?, error: Error?) {
if let error = error {
print(error.localizedDescription)
} else if let response = response {
// Add events to liveGames array
for (_, var game) in response.events.enumerated() {
// Note that there will only be one league in NCAA or (W)NBA APIs
// Add game
game.league = response.leagues[0].abbreviation
liveGames.append(game)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(action: { liveGamesModel.refreshData() }, label: {
Label("Refresh", systemImage: "arrow.clockwise")
})
}
}
loading = false
}
}

Expand Down
Loading

0 comments on commit 94f85f8

Please sign in to comment.