Makefile, linting, and formatting

This commit is contained in:
2025-12-15 16:32:59 -07:00
parent 0cc576bb12
commit c0d9702e54
91 changed files with 6342 additions and 5079 deletions

View File

@@ -2,10 +2,10 @@ package com.atridad.ascently
import com.atridad.ascently.data.format.*
import com.atridad.ascently.data.model.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import org.junit.Assert.*
import org.junit.Test
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class BusinessLogicTests {
@@ -21,11 +21,11 @@ class BusinessLogicTests {
assertNull(session.duration)
val completedSession =
session.copy(
status = SessionStatus.COMPLETED,
endTime = getCurrentTimestamp(),
duration = 7200L
)
session.copy(
status = SessionStatus.COMPLETED,
endTime = getCurrentTimestamp(),
duration = 7200L,
)
assertEquals(SessionStatus.COMPLETED, completedSession.status)
assertNotNull(completedSession.endTime)
assertNotNull(completedSession.duration)
@@ -38,12 +38,12 @@ class BusinessLogicTests {
val session = ClimbSession.create(gym.id)
val attempt =
Attempt.create(
sessionId = session.id,
problemId = problem.id,
result = AttemptResult.SUCCESS,
notes = "Clean send!"
)
Attempt.create(
sessionId = session.id,
problemId = problem.id,
result = AttemptResult.SUCCESS,
notes = "Clean send!",
)
assertEquals(session.id, attempt.sessionId)
assertEquals(problem.id, attempt.problemId)
@@ -76,12 +76,12 @@ class BusinessLogicTests {
val problem2 = createTestProblem(gym.id)
val attempts =
listOf(
Attempt.create(session.id, problem1.id, AttemptResult.SUCCESS),
Attempt.create(session.id, problem1.id, AttemptResult.FALL),
Attempt.create(session.id, problem2.id, AttemptResult.FLASH),
Attempt.create(session.id, problem2.id, AttemptResult.SUCCESS)
)
listOf(
Attempt.create(session.id, problem1.id, AttemptResult.SUCCESS),
Attempt.create(session.id, problem1.id, AttemptResult.FALL),
Attempt.create(session.id, problem2.id, AttemptResult.FLASH),
Attempt.create(session.id, problem2.id, AttemptResult.SUCCESS),
)
val sessionStats = calculateSessionStatistics(session, attempts)
@@ -97,17 +97,17 @@ class BusinessLogicTests {
val session = ClimbSession.create(gym.id)
val problems =
listOf(
createTestProblemWithGrade(gym.id, "V3"),
createTestProblemWithGrade(gym.id, "V4"),
createTestProblemWithGrade(gym.id, "V5"),
createTestProblemWithGrade(gym.id, "V6")
)
listOf(
createTestProblemWithGrade(gym.id, "V3"),
createTestProblemWithGrade(gym.id, "V4"),
createTestProblemWithGrade(gym.id, "V5"),
createTestProblemWithGrade(gym.id, "V6"),
)
val attempts =
problems.map { problem ->
Attempt.create(session.id, problem.id, AttemptResult.SUCCESS)
}
problems.map { problem ->
Attempt.create(session.id, problem.id, AttemptResult.SUCCESS)
}
val progression = calculateDifficultyProgression(attempts, problems)
@@ -123,17 +123,17 @@ class BusinessLogicTests {
val problems = listOf(createTestProblem(gym.id), createTestProblem(gym.id))
val session = ClimbSession.create(gym.id)
val attempts =
problems.map { problem ->
Attempt.create(session.id, problem.id, AttemptResult.SUCCESS)
}
problems.map { problem ->
Attempt.create(session.id, problem.id, AttemptResult.SUCCESS)
}
val backup =
createBackupData(
gyms = listOf(gym),
problems = problems,
sessions = listOf(session),
attempts = attempts
)
createBackupData(
gyms = listOf(gym),
problems = problems,
sessions = listOf(session),
attempts = attempts,
)
validateBackupIntegrity(backup)
@@ -146,30 +146,30 @@ class BusinessLogicTests {
@Test
fun testClimbTypeCompatibilityRules() {
val boulderGym =
Gym(
id = "boulder_gym",
name = "Boulder Gym",
location = "Boulder City",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.FONT),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp()
)
Gym(
id = "boulder_gym",
name = "Boulder Gym",
location = "Boulder City",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.FONT),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp(),
)
val ropeGym =
Gym(
id = "rope_gym",
name = "Rope Gym",
location = "Rope City",
supportedClimbTypes = listOf(ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp()
)
Gym(
id = "rope_gym",
name = "Rope Gym",
location = "Rope City",
supportedClimbTypes = listOf(ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp(),
)
// Boulder gym should support boulder problems with V-Scale
assertTrue(isCompatibleClimbType(boulderGym, ClimbType.BOULDER, DifficultySystem.V_SCALE))
@@ -197,26 +197,26 @@ class BusinessLogicTests {
val session = ClimbSession.create(gym.id)
val attempts =
listOf(
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:00:00Z",
AttemptResult.FALL
),
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:05:00Z",
AttemptResult.FALL
),
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:10:00Z",
AttemptResult.SUCCESS
)
)
listOf(
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:00:00Z",
AttemptResult.FALL,
),
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:05:00Z",
AttemptResult.FALL,
),
createAttemptWithTimestamp(
session.id,
problem.id,
"2024-01-01T10:10:00Z",
AttemptResult.SUCCESS,
),
)
val sequence = AttemptSequence(attempts)
@@ -230,32 +230,32 @@ class BusinessLogicTests {
@Test
fun testGradeConsistencyValidation() {
val validCombinations =
listOf(
Pair(ClimbType.BOULDER, DifficultySystem.V_SCALE),
Pair(ClimbType.BOULDER, DifficultySystem.FONT),
Pair(ClimbType.ROPE, DifficultySystem.YDS),
Pair(ClimbType.BOULDER, DifficultySystem.CUSTOM),
Pair(ClimbType.ROPE, DifficultySystem.CUSTOM)
)
listOf(
Pair(ClimbType.BOULDER, DifficultySystem.V_SCALE),
Pair(ClimbType.BOULDER, DifficultySystem.FONT),
Pair(ClimbType.ROPE, DifficultySystem.YDS),
Pair(ClimbType.BOULDER, DifficultySystem.CUSTOM),
Pair(ClimbType.ROPE, DifficultySystem.CUSTOM),
)
val invalidCombinations =
listOf(
Pair(ClimbType.BOULDER, DifficultySystem.YDS),
Pair(ClimbType.ROPE, DifficultySystem.V_SCALE),
Pair(ClimbType.ROPE, DifficultySystem.FONT)
)
listOf(
Pair(ClimbType.BOULDER, DifficultySystem.YDS),
Pair(ClimbType.ROPE, DifficultySystem.V_SCALE),
Pair(ClimbType.ROPE, DifficultySystem.FONT),
)
validCombinations.forEach { (climbType, difficultySystem) ->
assertTrue(
"$climbType should be compatible with $difficultySystem",
isValidGradeCombination(climbType, difficultySystem)
"$climbType should be compatible with $difficultySystem",
isValidGradeCombination(climbType, difficultySystem),
)
}
invalidCombinations.forEach { (climbType, difficultySystem) ->
assertFalse(
"$climbType should not be compatible with $difficultySystem",
isValidGradeCombination(climbType, difficultySystem)
"$climbType should not be compatible with $difficultySystem",
isValidGradeCombination(climbType, difficultySystem),
)
}
}
@@ -276,11 +276,11 @@ class BusinessLogicTests {
@Test
fun testImagePathHandling() {
val originalPaths =
listOf(
"/storage/images/problem1.jpg",
"/data/cache/problem2.png",
"relative/path/problem3.jpeg"
)
listOf(
"/storage/images/problem1.jpg",
"/data/cache/problem2.png",
"relative/path/problem3.jpeg",
)
val relativePaths = convertToRelativePaths(originalPaths)
@@ -295,76 +295,76 @@ class BusinessLogicTests {
private fun createTestGym(): Gym {
return Gym(
id = "test_gym_1",
name = "Test Climbing Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Test gym for unit testing",
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp()
id = "test_gym_1",
name = "Test Climbing Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Test gym for unit testing",
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp(),
)
}
private fun createTestProblem(
gymId: String,
climbType: ClimbType = ClimbType.BOULDER
gymId: String,
climbType: ClimbType = ClimbType.BOULDER,
): Problem {
val difficulty =
when (climbType) {
ClimbType.BOULDER -> DifficultyGrade(DifficultySystem.V_SCALE, "V5")
ClimbType.ROPE -> DifficultyGrade(DifficultySystem.YDS, "5.10a")
}
when (climbType) {
ClimbType.BOULDER -> DifficultyGrade(DifficultySystem.V_SCALE, "V5")
ClimbType.ROPE -> DifficultyGrade(DifficultySystem.YDS, "5.10a")
}
return Problem(
id = "test_problem_${java.util.UUID.randomUUID()}",
gymId = gymId,
name = "Test Problem",
description = "A test climbing problem",
climbType = climbType,
difficulty = difficulty,
tags = listOf("test", "overhang"),
location = "Wall A",
imagePaths = emptyList(),
isActive = true,
dateSet = "2024-01-01",
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp()
id = "test_problem_${java.util.UUID.randomUUID()}",
gymId = gymId,
name = "Test Problem",
description = "A test climbing problem",
climbType = climbType,
difficulty = difficulty,
tags = listOf("test", "overhang"),
location = "Wall A",
imagePaths = emptyList(),
isActive = true,
dateSet = "2024-01-01",
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp(),
)
}
private fun createTestProblemWithGrade(gymId: String, grade: String): Problem {
return Problem(
id = "test_problem_${java.util.UUID.randomUUID()}",
gymId = gymId,
name = "Test Problem $grade",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, grade),
tags = emptyList(),
location = null,
imagePaths = emptyList(),
isActive = true,
dateSet = null,
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp()
id = "test_problem_${java.util.UUID.randomUUID()}",
gymId = gymId,
name = "Test Problem $grade",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, grade),
tags = emptyList(),
location = null,
imagePaths = emptyList(),
isActive = true,
dateSet = null,
notes = null,
createdAt = getCurrentTimestamp(),
updatedAt = getCurrentTimestamp(),
)
}
private fun createAttemptWithTimestamp(
sessionId: String,
problemId: String,
timestamp: String,
result: AttemptResult
sessionId: String,
problemId: String,
timestamp: String,
result: AttemptResult,
): Attempt {
return Attempt.create(
sessionId = sessionId,
problemId = problemId,
result = result,
timestamp = timestamp
sessionId = sessionId,
problemId = problemId,
result = result,
timestamp = timestamp,
)
}
@@ -373,126 +373,126 @@ class BusinessLogicTests {
}
private fun calculateSessionStatistics(
session: ClimbSession,
attempts: List<Attempt>
session: ClimbSession,
attempts: List<Attempt>,
): SessionStatistics {
val successful =
attempts.count {
it.result == AttemptResult.SUCCESS || it.result == AttemptResult.FLASH
}
attempts.count {
it.result == AttemptResult.SUCCESS || it.result == AttemptResult.FLASH
}
val uniqueProblems = attempts.map { it.problemId }.toSet().size
val successRate = (successful.toDouble() / attempts.size) * 100
return SessionStatistics(
totalAttempts = attempts.size,
successfulAttempts = successful,
uniqueProblems = uniqueProblems,
successRate = successRate
totalAttempts = attempts.size,
successfulAttempts = successful,
uniqueProblems = uniqueProblems,
successRate = successRate,
)
}
private fun calculateDifficultyProgression(
attempts: List<Attempt>,
problems: List<Problem>
attempts: List<Attempt>,
problems: List<Problem>,
): DifficultyProgression {
val problemMap = problems.associateBy { it.id }
val grades =
attempts
.mapNotNull { attempt -> problemMap[attempt.problemId]?.difficulty?.grade }
.filter { it.startsWith("V") }
attempts
.mapNotNull { attempt -> problemMap[attempt.problemId]?.difficulty?.grade }
.filter { it.startsWith("V") }
val numericGrades =
grades.mapNotNull { grade ->
when (grade) {
"VB" -> 0
else -> grade.removePrefix("V").toIntOrNull()
}
grades.mapNotNull { grade ->
when (grade) {
"VB" -> 0
else -> grade.removePrefix("V").toIntOrNull()
}
}
val minGrade = "V${numericGrades.minOrNull() ?: 0}".replace("V0", "VB")
val maxGrade = "V${numericGrades.maxOrNull() ?: 0}".replace("V0", "VB")
val avgGrade = numericGrades.average()
val showsProgression =
numericGrades.size > 1 &&
(numericGrades.maxOrNull() ?: 0) > (numericGrades.minOrNull() ?: 0)
numericGrades.size > 1 &&
(numericGrades.maxOrNull() ?: 0) > (numericGrades.minOrNull() ?: 0)
return DifficultyProgression(minGrade, maxGrade, avgGrade, showsProgression)
}
private fun createBackupData(
gyms: List<Gym>,
problems: List<Problem>,
sessions: List<ClimbSession>,
attempts: List<Attempt>
gyms: List<Gym>,
problems: List<Problem>,
sessions: List<ClimbSession>,
attempts: List<Attempt>,
): ClimbDataBackup {
return ClimbDataBackup(
exportedAt = getCurrentTimestamp(),
version = "2.0",
formatVersion = "2.0",
gyms =
gyms.map { gym ->
BackupGym(
id = gym.id,
name = gym.name,
location = gym.location,
supportedClimbTypes = gym.supportedClimbTypes,
difficultySystems = gym.difficultySystems,
customDifficultyGrades = gym.customDifficultyGrades,
notes = gym.notes,
createdAt = gym.createdAt,
updatedAt = gym.updatedAt
)
},
problems =
problems.map { problem ->
BackupProblem(
id = problem.id,
gymId = problem.gymId,
name = problem.name,
description = problem.description,
climbType = problem.climbType,
difficulty = problem.difficulty,
tags = problem.tags,
location = problem.location,
imagePaths = problem.imagePaths,
isActive = problem.isActive,
dateSet = problem.dateSet,
notes = problem.notes,
createdAt = problem.createdAt,
updatedAt = problem.updatedAt
)
},
sessions =
sessions.map { session ->
BackupClimbSession(
id = session.id,
gymId = session.gymId,
date = session.date,
startTime = session.startTime,
endTime = session.endTime,
duration = session.duration,
status = session.status,
notes = session.notes,
createdAt = session.createdAt,
updatedAt = session.updatedAt
)
},
attempts =
attempts.map { attempt ->
BackupAttempt(
id = attempt.id,
sessionId = attempt.sessionId,
problemId = attempt.problemId,
result = attempt.result,
highestHold = attempt.highestHold,
notes = attempt.notes,
duration = attempt.duration,
restTime = attempt.restTime,
timestamp = attempt.timestamp,
createdAt = attempt.createdAt,
updatedAt = attempt.updatedAt,
)
}
exportedAt = getCurrentTimestamp(),
version = "2.0",
formatVersion = "2.0",
gyms =
gyms.map { gym ->
BackupGym(
id = gym.id,
name = gym.name,
location = gym.location,
supportedClimbTypes = gym.supportedClimbTypes,
difficultySystems = gym.difficultySystems,
customDifficultyGrades = gym.customDifficultyGrades,
notes = gym.notes,
createdAt = gym.createdAt,
updatedAt = gym.updatedAt,
)
},
problems =
problems.map { problem ->
BackupProblem(
id = problem.id,
gymId = problem.gymId,
name = problem.name,
description = problem.description,
climbType = problem.climbType,
difficulty = problem.difficulty,
tags = problem.tags,
location = problem.location,
imagePaths = problem.imagePaths,
isActive = problem.isActive,
dateSet = problem.dateSet,
notes = problem.notes,
createdAt = problem.createdAt,
updatedAt = problem.updatedAt,
)
},
sessions =
sessions.map { session ->
BackupClimbSession(
id = session.id,
gymId = session.gymId,
date = session.date,
startTime = session.startTime,
endTime = session.endTime,
duration = session.duration,
status = session.status,
notes = session.notes,
createdAt = session.createdAt,
updatedAt = session.updatedAt,
)
},
attempts =
attempts.map { attempt ->
BackupAttempt(
id = attempt.id,
sessionId = attempt.sessionId,
problemId = attempt.problemId,
result = attempt.result,
highestHold = attempt.highestHold,
notes = attempt.notes,
duration = attempt.duration,
restTime = attempt.restTime,
timestamp = attempt.timestamp,
createdAt = attempt.createdAt,
updatedAt = attempt.updatedAt,
)
},
)
}
@@ -501,8 +501,8 @@ class BusinessLogicTests {
val gymIds = backup.gyms.map { it.id }.toSet()
backup.problems.forEach { problem ->
assertTrue(
"Problem ${problem.id} references non-existent gym ${problem.gymId}",
gymIds.contains(problem.gymId)
"Problem ${problem.id} references non-existent gym ${problem.gymId}",
gymIds.contains(problem.gymId),
)
}
@@ -510,8 +510,8 @@ class BusinessLogicTests {
val sessionIds = backup.sessions.map { it.id }.toSet()
backup.attempts.forEach { attempt ->
assertTrue(
"Attempt ${attempt.id} references non-existent session ${attempt.sessionId}",
sessionIds.contains(attempt.sessionId)
"Attempt ${attempt.id} references non-existent session ${attempt.sessionId}",
sessionIds.contains(attempt.sessionId),
)
}
@@ -519,19 +519,19 @@ class BusinessLogicTests {
val problemIds = backup.problems.map { it.id }.toSet()
backup.attempts.forEach { attempt ->
assertTrue(
"Attempt ${attempt.id} references non-existent problem ${attempt.problemId}",
problemIds.contains(attempt.problemId)
"Attempt ${attempt.id} references non-existent problem ${attempt.problemId}",
problemIds.contains(attempt.problemId),
)
}
}
private fun isCompatibleClimbType(
gym: Gym,
climbType: ClimbType,
difficultySystem: DifficultySystem
gym: Gym,
climbType: ClimbType,
difficultySystem: DifficultySystem,
): Boolean {
return gym.supportedClimbTypes.contains(climbType) &&
gym.difficultySystems.contains(difficultySystem)
gym.difficultySystems.contains(difficultySystem)
}
private fun calculateSessionDuration(startTime: String, endTime: String): Long {
@@ -541,19 +541,19 @@ class BusinessLogicTests {
}
private fun isValidGradeCombination(
climbType: ClimbType,
difficultySystem: DifficultySystem
climbType: ClimbType,
difficultySystem: DifficultySystem,
): Boolean {
return when (climbType) {
ClimbType.BOULDER ->
difficultySystem in
listOf(
DifficultySystem.V_SCALE,
DifficultySystem.FONT,
DifficultySystem.CUSTOM
)
difficultySystem in
listOf(
DifficultySystem.V_SCALE,
DifficultySystem.FONT,
DifficultySystem.CUSTOM,
)
ClimbType.ROPE ->
difficultySystem in listOf(DifficultySystem.YDS, DifficultySystem.CUSTOM)
difficultySystem in listOf(DifficultySystem.YDS, DifficultySystem.CUSTOM)
}
}
@@ -568,29 +568,29 @@ class BusinessLogicTests {
// Data classes for testing
data class SessionStatistics(
val totalAttempts: Int,
val successfulAttempts: Int,
val uniqueProblems: Int,
val successRate: Double
val totalAttempts: Int,
val successfulAttempts: Int,
val uniqueProblems: Int,
val successRate: Double,
)
data class DifficultyProgression(
val minGrade: String,
val maxGrade: String,
val averageGrade: Double,
val showsProgression: Boolean
val minGrade: String,
val maxGrade: String,
val averageGrade: Double,
val showsProgression: Boolean,
)
data class AttemptSequence(val attempts: List<Attempt>) {
val totalAttempts = attempts.size
val failedAttempts =
attempts.count {
it.result == AttemptResult.FALL || it.result == AttemptResult.NO_PROGRESS
}
attempts.count {
it.result == AttemptResult.FALL || it.result == AttemptResult.NO_PROGRESS
}
val successfulAttempts =
attempts.count {
it.result == AttemptResult.SUCCESS || it.result == AttemptResult.FLASH
}
attempts.count {
it.result == AttemptResult.SUCCESS || it.result == AttemptResult.FLASH
}
val finalResult = attempts.lastOrNull()?.result
fun isValidSequence(): Boolean {

View File

@@ -2,10 +2,10 @@ package com.atridad.ascently
import com.atridad.ascently.data.format.*
import com.atridad.ascently.data.model.*
import java.time.Instant
import java.time.format.DateTimeFormatter
import org.junit.Assert.*
import org.junit.Test
import java.time.Instant
import java.time.format.DateTimeFormatter
class DataModelTests {
@@ -141,17 +141,17 @@ class DataModelTests {
@Test
fun testBackupGymCreationAndValidation() {
val gym =
BackupGym(
id = "gym123",
name = "Test Climbing Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Great gym for beginners",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupGym(
id = "gym123",
name = "Test Climbing Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Great gym for beginners",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertEquals("gym123", gym.id)
assertEquals("Test Climbing Gym", gym.name)
@@ -167,22 +167,22 @@ class DataModelTests {
@Test
fun testBackupProblemCreationAndValidation() {
val problem =
BackupProblem(
id = "problem123",
gymId = "gym123",
name = "Test Problem",
description = "A challenging boulder problem",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("overhang", "crimpy"),
location = "Wall A",
imagePaths = listOf("image1.jpg", "image2.jpg"),
isActive = true,
dateSet = "2024-01-01",
notes = "Watch the start holds",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupProblem(
id = "problem123",
gymId = "gym123",
name = "Test Problem",
description = "A challenging boulder problem",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("overhang", "crimpy"),
location = "Wall A",
imagePaths = listOf("image1.jpg", "image2.jpg"),
isActive = true,
dateSet = "2024-01-01",
notes = "Watch the start holds",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertEquals("problem123", problem.id)
assertEquals("gym123", problem.gymId)
@@ -190,25 +190,25 @@ class DataModelTests {
assertEquals(ClimbType.BOULDER, problem.climbType)
assertEquals("V5", problem.difficulty.grade)
assertTrue(problem.isActive)
assertEquals(2, problem.tags.size)
assertEquals(2, problem.tags?.size ?: 0)
assertEquals(2, problem.imagePaths?.size ?: 0)
}
@Test
fun testBackupClimbSessionCreationAndValidation() {
val session =
BackupClimbSession(
id = "session123",
gymId = "gym123",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T12:00:00Z",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = "Great session today",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T12:00:00Z"
)
BackupClimbSession(
id = "session123",
gymId = "gym123",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T12:00:00Z",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = "Great session today",
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T12:00:00Z",
)
assertEquals("session123", session.id)
assertEquals("gym123", session.gymId)
@@ -220,19 +220,19 @@ class DataModelTests {
@Test
fun testBackupAttemptCreationAndValidation() {
val attempt =
BackupAttempt(
id = "attempt123",
sessionId = "session123",
problemId = "problem123",
result = AttemptResult.SUCCESS,
highestHold = "Top",
notes = "Stuck it on second try",
duration = 300,
restTime = 120,
timestamp = "2024-01-01T10:30:00Z",
createdAt = "2024-01-01T10:30:00Z",
updatedAt = "2024-01-01T10:30:00Z"
)
BackupAttempt(
id = "attempt123",
sessionId = "session123",
problemId = "problem123",
result = AttemptResult.SUCCESS,
highestHold = "Top",
notes = "Stuck it on second try",
duration = 300,
restTime = 120,
timestamp = "2024-01-01T10:30:00Z",
createdAt = "2024-01-01T10:30:00Z",
updatedAt = "2024-01-01T10:30:00Z",
)
assertEquals("attempt123", attempt.id)
assertEquals("session123", attempt.sessionId)
@@ -246,15 +246,15 @@ class DataModelTests {
@Test
fun testClimbDataBackupCreationAndValidation() {
val backup =
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00Z",
version = "2.0",
formatVersion = "2.0",
gyms = emptyList(),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList()
)
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00Z",
version = "2.0",
formatVersion = "2.0",
gyms = emptyList(),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList(),
)
assertEquals("2.0", backup.version)
assertEquals("2.0", backup.formatVersion)
@@ -280,18 +280,18 @@ class DataModelTests {
@Test
fun testSessionDurationCalculation() {
val session =
BackupClimbSession(
id = "test",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T12:00:00Z",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T12:00:00Z"
)
BackupClimbSession(
id = "test",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T12:00:00Z",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T12:00:00Z",
)
assertEquals(7200L, session.duration)
val hours = session.duration!! / 3600
@@ -301,21 +301,21 @@ class DataModelTests {
@Test
fun testEmptyCollectionsHandling() {
val gym =
BackupGym(
id = "gym1",
name = "Test Gym",
location = null,
supportedClimbTypes = emptyList(),
difficultySystems = emptyList(),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupGym(
id = "gym1",
name = "Test Gym",
location = null,
supportedClimbTypes = emptyList(),
difficultySystems = emptyList(),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertTrue(gym.supportedClimbTypes.isEmpty())
assertTrue(gym.difficultySystems.isEmpty())
assertTrue(gym.customDifficultyGrades.isEmpty())
assertTrue(gym.supportedClimbTypes?.isEmpty() ?: true)
assertTrue(gym.difficultySystems?.isEmpty() ?: true)
assertTrue(gym.customDifficultyGrades?.isEmpty() ?: true)
assertNull(gym.location)
assertNull(gym.notes)
}
@@ -323,29 +323,29 @@ class DataModelTests {
@Test
fun testNullableFieldsHandling() {
val problem =
BackupProblem(
id = "problem1",
gymId = "gym1",
name = null,
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V1"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupProblem(
id = "problem1",
gymId = "gym1",
name = null,
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V1"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertNull(problem.name)
assertNull(problem.description)
assertNull(problem.location)
assertNull(problem.dateSet)
assertNull(problem.notes)
assertTrue(problem.tags.isEmpty())
assertTrue(problem.tags?.isEmpty() ?: true)
assertNull(problem.imagePaths)
}
@@ -362,7 +362,7 @@ class DataModelTests {
@Test
fun testBackupDataFormatValidation() {
val testJson =
"""
"""
{
"exportedAt": "2024-01-01T10:00:00Z",
"version": "2.0",
@@ -372,7 +372,7 @@ class DataModelTests {
"sessions": [],
"attempts": []
}
""".trimIndent()
""".trimIndent()
assertTrue(testJson.contains("exportedAt"))
assertTrue(testJson.contains("version"))
@@ -397,44 +397,44 @@ class DataModelTests {
fun testClimbTypeAndDifficultySystemCompatibility() {
// Test that V_SCALE works with BOULDER
val boulderProblem =
BackupProblem(
id = "boulder1",
gymId = "gym1",
name = "Boulder Problem",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V3"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupProblem(
id = "boulder1",
gymId = "gym1",
name = "Boulder Problem",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V3"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertEquals(ClimbType.BOULDER, boulderProblem.climbType)
assertEquals(DifficultySystem.V_SCALE, boulderProblem.difficulty.system)
// Test that YDS works with ROPE
val ropeProblem =
BackupProblem(
id = "rope1",
gymId = "gym1",
name = "Rope Problem",
description = null,
climbType = ClimbType.ROPE,
difficulty = DifficultyGrade(DifficultySystem.YDS, "5.10a"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupProblem(
id = "rope1",
gymId = "gym1",
name = "Rope Problem",
description = null,
climbType = ClimbType.ROPE,
difficulty = DifficultyGrade(DifficultySystem.YDS, "5.10a"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
assertEquals(ClimbType.ROPE, ropeProblem.climbType)
assertEquals(DifficultySystem.YDS, ropeProblem.difficulty.system)
@@ -473,12 +473,12 @@ class DataModelTests {
@Test
fun testAttemptResultValidation() {
val validResults =
listOf(
AttemptResult.SUCCESS,
AttemptResult.FALL,
AttemptResult.NO_PROGRESS,
AttemptResult.FLASH
)
listOf(
AttemptResult.SUCCESS,
AttemptResult.FALL,
AttemptResult.NO_PROGRESS,
AttemptResult.FLASH,
)
assertEquals(4, validResults.size)
assertTrue(validResults.contains(AttemptResult.SUCCESS))
@@ -490,7 +490,7 @@ class DataModelTests {
@Test
fun testSessionStatusValidation() {
val validStatuses =
listOf(SessionStatus.ACTIVE, SessionStatus.COMPLETED, SessionStatus.PAUSED)
listOf(SessionStatus.ACTIVE, SessionStatus.COMPLETED, SessionStatus.PAUSED)
assertEquals(3, validStatuses.size)
assertTrue(validStatuses.contains(SessionStatus.ACTIVE))
@@ -501,64 +501,64 @@ class DataModelTests {
@Test
fun testClimbDataIntegrity() {
val gym =
BackupGym(
id = "gym1",
name = "Test Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupGym(
id = "gym1",
name = "Test Gym",
location = "Test City",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
val problem =
BackupProblem(
id = "problem1",
gymId = gym.id,
name = "Test Problem",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V3"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z"
)
BackupProblem(
id = "problem1",
gymId = gym.id,
name = "Test Problem",
description = null,
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V3"),
tags = emptyList(),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T10:00:00Z",
)
val session =
BackupClimbSession(
id = "session1",
gymId = gym.id,
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T11:00:00Z",
duration = 3600,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T11:00:00Z"
)
BackupClimbSession(
id = "session1",
gymId = gym.id,
date = "2024-01-01",
startTime = "2024-01-01T10:00:00Z",
endTime = "2024-01-01T11:00:00Z",
duration = 3600,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00Z",
updatedAt = "2024-01-01T11:00:00Z",
)
val attempt =
BackupAttempt(
id = "attempt1",
sessionId = session.id,
problemId = problem.id,
result = AttemptResult.SUCCESS,
highestHold = null,
notes = null,
duration = 120,
restTime = null,
timestamp = "2024-01-01T10:30:00Z",
createdAt = "2024-01-01T10:30:00Z",
updatedAt = "2024-01-01T10:30:00Z"
)
BackupAttempt(
id = "attempt1",
sessionId = session.id,
problemId = problem.id,
result = AttemptResult.SUCCESS,
highestHold = null,
notes = null,
duration = 120,
restTime = null,
timestamp = "2024-01-01T10:30:00Z",
createdAt = "2024-01-01T10:30:00Z",
updatedAt = "2024-01-01T10:30:00Z",
)
// Verify referential integrity
assertEquals(gym.id, problem.gymId)

View File

@@ -11,197 +11,197 @@ class SyncMergeLogicTest {
fun `test intelligent merge preserves all data`() {
// Create local data
val localGyms =
listOf(
BackupGym(
id = "gym1",
name = "Local Gym 1",
location = "Local Location",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
)
)
listOf(
BackupGym(
id = "gym1",
name = "Local Gym 1",
location = "Local Location",
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems = listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00",
),
)
val localProblems =
listOf(
BackupProblem(
id = "problem1",
gymId = "gym1",
name = "Local Problem",
description = "Local description",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("local"),
location = null,
imagePaths = listOf("local_image.jpg"),
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
)
)
listOf(
BackupProblem(
id = "problem1",
gymId = "gym1",
name = "Local Problem",
description = "Local description",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("local"),
location = null,
imagePaths = listOf("local_image.jpg"),
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00",
),
)
val localSessions =
listOf(
BackupClimbSession(
id = "session1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = "2024-01-01T12:00:00",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
)
)
listOf(
BackupClimbSession(
id = "session1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = "2024-01-01T12:00:00",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00",
),
)
val localAttempts =
listOf(
BackupAttempt(
id = "attempt1",
sessionId = "session1",
problemId = "problem1",
result = AttemptResult.SUCCESS,
highestHold = null,
notes = null,
duration = 300,
restTime = null,
timestamp = "2024-01-01T10:30:00",
createdAt = "2024-01-01T10:30:00",
updatedAt = "2024-01-01T10:30:00"
)
)
listOf(
BackupAttempt(
id = "attempt1",
sessionId = "session1",
problemId = "problem1",
result = AttemptResult.SUCCESS,
highestHold = null,
notes = null,
duration = 300,
restTime = null,
timestamp = "2024-01-01T10:30:00",
createdAt = "2024-01-01T10:30:00",
updatedAt = "2024-01-01T10:30:00",
),
)
val localBackup =
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = localGyms,
problems = localProblems,
sessions = localSessions,
attempts = localAttempts
)
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = localGyms,
problems = localProblems,
sessions = localSessions,
attempts = localAttempts,
)
// Create server data with some overlapping and some unique data
val serverGyms =
listOf(
// Same gym but with newer update
BackupGym(
id = "gym1",
name = "Updated Gym 1",
location = "Updated Location",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems =
listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Updated notes",
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T12:00:00" // Newer update
),
// Unique server gym
BackupGym(
id = "gym2",
name = "Server Gym 2",
location = "Server Location",
supportedClimbTypes = listOf(ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T11:00:00",
updatedAt = "2024-01-01T11:00:00"
)
)
listOf(
// Same gym but with newer update
BackupGym(
id = "gym1",
name = "Updated Gym 1",
location = "Updated Location",
supportedClimbTypes = listOf(ClimbType.BOULDER, ClimbType.ROPE),
difficultySystems =
listOf(DifficultySystem.V_SCALE, DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = "Updated notes",
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T12:00:00", // Newer update
),
// Unique server gym
BackupGym(
id = "gym2",
name = "Server Gym 2",
location = "Server Location",
supportedClimbTypes = listOf(ClimbType.ROPE),
difficultySystems = listOf(DifficultySystem.YDS),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T11:00:00",
updatedAt = "2024-01-01T11:00:00",
),
)
val serverProblems =
listOf(
// Same problem but with newer update and different images
BackupProblem(
id = "problem1",
gymId = "gym1",
name = "Updated Problem",
description = "Updated description",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("updated", "server"),
location = "Updated location",
imagePaths = listOf("server_image.jpg"),
isActive = true,
dateSet = "2024-01-01",
notes = "Updated notes",
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T11:00:00" // Newer update
),
// Unique server problem
BackupProblem(
id = "problem2",
gymId = "gym2",
name = "Server Problem",
description = "Server description",
climbType = ClimbType.ROPE,
difficulty = DifficultyGrade(DifficultySystem.YDS, "5.10a"),
tags = listOf("server"),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T11:00:00",
updatedAt = "2024-01-01T11:00:00"
)
)
listOf(
// Same problem but with newer update and different images
BackupProblem(
id = "problem1",
gymId = "gym1",
name = "Updated Problem",
description = "Updated description",
climbType = ClimbType.BOULDER,
difficulty = DifficultyGrade(DifficultySystem.V_SCALE, "V5"),
tags = listOf("updated", "server"),
location = "Updated location",
imagePaths = listOf("server_image.jpg"),
isActive = true,
dateSet = "2024-01-01",
notes = "Updated notes",
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T11:00:00", // Newer update
),
// Unique server problem
BackupProblem(
id = "problem2",
gymId = "gym2",
name = "Server Problem",
description = "Server description",
climbType = ClimbType.ROPE,
difficulty = DifficultyGrade(DifficultySystem.YDS, "5.10a"),
tags = listOf("server"),
location = null,
imagePaths = null,
isActive = true,
dateSet = null,
notes = null,
createdAt = "2024-01-01T11:00:00",
updatedAt = "2024-01-01T11:00:00",
),
)
val serverSessions =
listOf(
// Unique server session
BackupClimbSession(
id = "session2",
gymId = "gym2",
date = "2024-01-02",
startTime = "2024-01-02T14:00:00",
endTime = "2024-01-02T16:00:00",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = "Server session",
createdAt = "2024-01-02T14:00:00",
updatedAt = "2024-01-02T14:00:00"
)
)
listOf(
// Unique server session
BackupClimbSession(
id = "session2",
gymId = "gym2",
date = "2024-01-02",
startTime = "2024-01-02T14:00:00",
endTime = "2024-01-02T16:00:00",
duration = 7200,
status = SessionStatus.COMPLETED,
notes = "Server session",
createdAt = "2024-01-02T14:00:00",
updatedAt = "2024-01-02T14:00:00",
),
)
val serverAttempts =
listOf(
// Unique server attempt
BackupAttempt(
id = "attempt2",
sessionId = "session2",
problemId = "problem2",
result = AttemptResult.FALL,
highestHold = "Last move",
notes = "Almost had it",
duration = 180,
restTime = 60,
timestamp = "2024-01-02T14:30:00",
createdAt = "2024-01-02T14:30:00",
updatedAt = "2024-01-02T14:30:00"
)
)
listOf(
// Unique server attempt
BackupAttempt(
id = "attempt2",
sessionId = "session2",
problemId = "problem2",
result = AttemptResult.FALL,
highestHold = "Last move",
notes = "Almost had it",
duration = 180,
restTime = 60,
timestamp = "2024-01-02T14:30:00",
createdAt = "2024-01-02T14:30:00",
updatedAt = "2024-01-02T14:30:00",
),
)
val serverBackup =
ClimbDataBackup(
exportedAt = "2024-01-01T12:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = serverGyms,
problems = serverProblems,
sessions = serverSessions,
attempts = serverAttempts
)
ClimbDataBackup(
exportedAt = "2024-01-01T12:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = serverGyms,
problems = serverProblems,
sessions = serverSessions,
attempts = serverAttempts,
)
// Simulate merge logic
val mergedBackup = performIntelligentMerge(localBackup, serverBackup)
@@ -231,12 +231,12 @@ class SyncMergeLogicTest {
// Images should be merged (both local and server images preserved)
assertTrue(
"Should contain local image",
mergedProblem1.imagePaths!!.contains("local_image.jpg")
"Should contain local image",
mergedProblem1.imagePaths!!.contains("local_image.jpg"),
)
assertTrue(
"Should contain server image",
mergedProblem1.imagePaths!!.contains("server_image.jpg")
"Should contain server image",
mergedProblem1.imagePaths!!.contains("server_image.jpg"),
)
assertEquals("Should have 2 images total", 2, mergedProblem1.imagePaths!!.size)
@@ -246,78 +246,78 @@ class SyncMergeLogicTest {
// Verify all sessions are preserved
assertTrue(
"Should contain local session",
mergedBackup.sessions.any { it.id == "session1" }
"Should contain local session",
mergedBackup.sessions.any { it.id == "session1" },
)
assertTrue(
"Should contain server session",
mergedBackup.sessions.any { it.id == "session2" }
"Should contain server session",
mergedBackup.sessions.any { it.id == "session2" },
)
// Verify all attempts are preserved
assertTrue(
"Should contain local attempt",
mergedBackup.attempts.any { it.id == "attempt1" }
"Should contain local attempt",
mergedBackup.attempts.any { it.id == "attempt1" },
)
assertTrue(
"Should contain server attempt",
mergedBackup.attempts.any { it.id == "attempt2" }
"Should contain server attempt",
mergedBackup.attempts.any { it.id == "attempt2" },
)
}
@Test
fun `test date comparison logic`() {
assertTrue(
"ISO instant should be newer",
isNewerThan("2024-01-01T12:00:00Z", "2024-01-01T10:00:00Z")
"ISO instant should be newer",
isNewerThan("2024-01-01T12:00:00Z", "2024-01-01T10:00:00Z"),
)
assertFalse(
"ISO instant should be older",
isNewerThan("2024-01-01T10:00:00Z", "2024-01-01T12:00:00Z")
"ISO instant should be older",
isNewerThan("2024-01-01T10:00:00Z", "2024-01-01T12:00:00Z"),
)
assertTrue(
"String comparison should work as fallback",
isNewerThan("2024-01-02T10:00:00", "2024-01-01T10:00:00")
"String comparison should work as fallback",
isNewerThan("2024-01-02T10:00:00", "2024-01-01T10:00:00"),
)
}
@Test
fun `test empty data scenarios`() {
val emptyBackup =
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = emptyList(),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList()
)
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = emptyList(),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList(),
)
val dataBackup =
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms =
listOf(
BackupGym(
id = "gym1",
name = "Test Gym",
location = null,
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems =
listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
)
),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList()
)
ClimbDataBackup(
exportedAt = "2024-01-01T10:00:00",
version = "2.0",
formatVersion = "2.0",
gyms =
listOf(
BackupGym(
id = "gym1",
name = "Test Gym",
location = null,
supportedClimbTypes = listOf(ClimbType.BOULDER),
difficultySystems =
listOf(DifficultySystem.V_SCALE),
customDifficultyGrades = emptyList(),
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00",
),
),
problems = emptyList(),
sessions = emptyList(),
attempts = emptyList(),
)
// Test merging empty with data
val merged1 = performIntelligentMerge(emptyBackup, dataBackup)
@@ -334,8 +334,8 @@ class SyncMergeLogicTest {
// Helper methods that simulate the merge logic from SyncService
private fun performIntelligentMerge(
local: ClimbDataBackup,
server: ClimbDataBackup
local: ClimbDataBackup,
server: ClimbDataBackup,
): ClimbDataBackup {
val mergedGyms = mergeGyms(local.gyms, server.gyms)
val mergedProblems = mergeProblems(local.problems, server.problems)
@@ -343,13 +343,13 @@ class SyncMergeLogicTest {
val mergedAttempts = mergeAttempts(local.attempts, server.attempts)
return ClimbDataBackup(
exportedAt = "2024-01-01T12:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = mergedGyms,
problems = mergedProblems,
sessions = mergedSessions,
attempts = mergedAttempts
exportedAt = "2024-01-01T12:00:00",
version = "2.0",
formatVersion = "2.0",
gyms = mergedGyms,
problems = mergedProblems,
sessions = mergedSessions,
attempts = mergedAttempts,
)
}
@@ -371,8 +371,8 @@ class SyncMergeLogicTest {
}
private fun mergeProblems(
local: List<BackupProblem>,
server: List<BackupProblem>
local: List<BackupProblem>,
server: List<BackupProblem>,
): List<BackupProblem> {
val merged = mutableMapOf<String, BackupProblem>()
@@ -390,7 +390,7 @@ class SyncMergeLogicTest {
serverProblem.imagePaths?.let { allImagePaths.addAll(it) }
merged[serverProblem.id] =
serverProblem.withUpdatedImagePaths(allImagePaths.toList())
serverProblem.withUpdatedImagePaths(allImagePaths.toList())
}
}
@@ -398,8 +398,8 @@ class SyncMergeLogicTest {
}
private fun mergeSessions(
local: List<BackupClimbSession>,
server: List<BackupClimbSession>
local: List<BackupClimbSession>,
server: List<BackupClimbSession>,
): List<BackupClimbSession> {
val merged = mutableMapOf<String, BackupClimbSession>()
@@ -419,8 +419,8 @@ class SyncMergeLogicTest {
}
private fun mergeAttempts(
local: List<BackupAttempt>,
server: List<BackupAttempt>
local: List<BackupAttempt>,
server: List<BackupAttempt>,
): List<BackupAttempt> {
val merged = mutableMapOf<String, BackupAttempt>()
@@ -431,10 +431,10 @@ class SyncMergeLogicTest {
server.forEach { serverAttempt ->
val localAttempt = merged[serverAttempt.id]
if (localAttempt == null ||
isNewerThan(
serverAttempt.updatedAt ?: serverAttempt.createdAt,
localAttempt.updatedAt ?: localAttempt.createdAt
)
isNewerThan(
serverAttempt.updatedAt ?: serverAttempt.createdAt,
localAttempt.updatedAt ?: localAttempt.createdAt,
)
) {
merged[serverAttempt.id] = serverAttempt
}
@@ -458,32 +458,32 @@ class SyncMergeLogicTest {
@Test
fun `test active sessions excluded from sync`() {
val allLocalSessions =
listOf(
BackupClimbSession(
id = "active_session_1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = null,
duration = null,
status = SessionStatus.ACTIVE,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
),
BackupClimbSession(
id = "completed_session_1",
gymId = "gym1",
date = "2023-12-31",
startTime = "2023-12-31T15:00:00",
endTime = "2023-12-31T17:00:00",
duration = 7200000,
status = SessionStatus.COMPLETED,
notes = "Previous session",
createdAt = "2023-12-31T15:00:00",
updatedAt = "2023-12-31T17:00:00"
)
)
listOf(
BackupClimbSession(
id = "active_session_1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = null,
duration = null,
status = SessionStatus.ACTIVE,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00",
),
BackupClimbSession(
id = "completed_session_1",
gymId = "gym1",
date = "2023-12-31",
startTime = "2023-12-31T15:00:00",
endTime = "2023-12-31T17:00:00",
duration = 7200000,
status = SessionStatus.COMPLETED,
notes = "Previous session",
createdAt = "2023-12-31T15:00:00",
updatedAt = "2023-12-31T17:00:00",
),
)
// Simulate filtering that would happen in createBackupFromRepository
val sessionsForSync = allLocalSessions.filter { it.status != SessionStatus.ACTIVE }
@@ -493,18 +493,18 @@ class SyncMergeLogicTest {
// Active session should be excluded
assertFalse(
"Should not contain active session in sync",
sessionsForSync.any {
it.id == "active_session_1" && it.status == SessionStatus.ACTIVE
}
"Should not contain active session in sync",
sessionsForSync.any {
it.id == "active_session_1" && it.status == SessionStatus.ACTIVE
},
)
// Completed session should be included
assertTrue(
"Should contain completed session in sync",
sessionsForSync.any {
it.id == "completed_session_1" && it.status == SessionStatus.COMPLETED
}
"Should contain completed session in sync",
sessionsForSync.any {
it.id == "completed_session_1" && it.status == SessionStatus.COMPLETED
},
)
}
}

View File

@@ -1,10 +1,10 @@
package com.atridad.ascently
import org.junit.Assert.*
import org.junit.Test
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import org.junit.Assert.*
import org.junit.Test
class UtilityTests {
@@ -75,13 +75,13 @@ class UtilityTests {
@Test
fun testClimbingStatistics() {
val attempts =
listOf(
AttemptData("SUCCESS", 120),
AttemptData("FALL", 90),
AttemptData("SUCCESS", 150),
AttemptData("FLASH", 60),
AttemptData("FALL", 110)
)
listOf(
AttemptData("SUCCESS", 120),
AttemptData("FALL", 90),
AttemptData("SUCCESS", 150),
AttemptData("FLASH", 60),
AttemptData("FALL", 110),
)
val stats = calculateAttemptStatistics(attempts)
@@ -163,23 +163,23 @@ class UtilityTests {
@Test
fun testSearchFiltering() {
val problems =
listOf(
ProblemData(
"id1",
"Crimpy Problem",
"BOULDER",
"V5",
listOf("crimpy", "overhang")
),
ProblemData("id2", "Easy Route", "ROPE", "5.6", listOf("beginner", "slab")),
ProblemData(
"id3",
"Hard Boulder",
"BOULDER",
"V10",
listOf("powerful", "roof")
)
)
listOf(
ProblemData(
"id1",
"Crimpy Problem",
"BOULDER",
"V5",
listOf("crimpy", "overhang"),
),
ProblemData("id2", "Easy Route", "ROPE", "5.6", listOf("beginner", "slab")),
ProblemData(
"id3",
"Hard Boulder",
"BOULDER",
"V10",
listOf("powerful", "roof"),
),
)
val boulderProblems = filterByClimbType(problems, "BOULDER")
assertEquals(2, boulderProblems.size)
@@ -207,20 +207,20 @@ class UtilityTests {
@Test
fun testBackupValidation() {
val validBackup =
BackupData(
version = "2.0",
formatVersion = "2.0",
exportedAt = "2024-01-01T10:00:00Z",
dataCount = 5
)
BackupData(
version = "2.0",
formatVersion = "2.0",
exportedAt = "2024-01-01T10:00:00Z",
dataCount = 5,
)
val invalidBackup =
BackupData(
version = "1.0",
formatVersion = "2.0",
exportedAt = "invalid-date",
dataCount = -1
)
BackupData(
version = "1.0",
formatVersion = "2.0",
exportedAt = "invalid-date",
dataCount = -1,
)
assertTrue(isValidBackup(validBackup))
assertFalse(isValidBackup(invalidBackup))
@@ -258,10 +258,10 @@ class UtilityTests {
val successRate = (successful.toDouble() / attempts.size) * 100
return AttemptStatistics(
totalAttempts = attempts.size,
successfulAttempts = successful,
successRate = successRate,
averageDuration = avgDuration
totalAttempts = attempts.size,
successfulAttempts = successful,
successRate = successRate,
averageDuration = avgDuration,
)
}
@@ -301,8 +301,8 @@ class UtilityTests {
}
private fun filterByClimbType(
problems: List<ProblemData>,
climbType: String
problems: List<ProblemData>,
climbType: String,
): List<ProblemData> {
return problems.filter { it.climbType == climbType }
}
@@ -312,9 +312,9 @@ class UtilityTests {
}
private fun filterByDifficultyRange(
problems: List<ProblemData>,
minGrade: String,
maxGrade: String
problems: List<ProblemData>,
minGrade: String,
maxGrade: String,
): List<ProblemData> {
return problems.filter { problem ->
if (problem.climbType == "BOULDER" && problem.difficulty.startsWith("V")) {
@@ -329,17 +329,17 @@ class UtilityTests {
}
private fun mergeData(
local: Map<String, String>,
server: Map<String, String>
local: Map<String, String>,
server: Map<String, String>,
): Map<String, String> {
return (local.keys + server.keys).associateWith { key -> server[key] ?: local[key]!! }
}
private fun isValidBackup(backup: BackupData): Boolean {
return backup.version == "2.0" &&
backup.formatVersion == "2.0" &&
backup.exportedAt.matches(Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")) &&
backup.dataCount >= 0
backup.formatVersion == "2.0" &&
backup.exportedAt.matches(Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")) &&
backup.dataCount >= 0
}
// Data classes for testing
@@ -347,24 +347,24 @@ class UtilityTests {
data class AttemptData(val result: String, val duration: Int)
data class AttemptStatistics(
val totalAttempts: Int,
val successfulAttempts: Int,
val successRate: Double,
val averageDuration: Double
val totalAttempts: Int,
val successfulAttempts: Int,
val successRate: Double,
val averageDuration: Double,
)
data class ProblemData(
val id: String,
val name: String,
val climbType: String,
val difficulty: String,
val tags: List<String>
val id: String,
val name: String,
val climbType: String,
val difficulty: String,
val tags: List<String>,
)
data class BackupData(
val version: String,
val formatVersion: String,
val exportedAt: String,
val dataCount: Int
val version: String,
val formatVersion: String,
val exportedAt: String,
val dataCount: Int,
)
}