Added a proper set of Unit Tests for each sub-project
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m28s
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m28s
This commit is contained in:
@@ -20,7 +20,7 @@ struct ClimbingActivityWidget: Widget {
|
||||
DynamicIsland {
|
||||
// Expanded UI goes here
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text("🧗♂️")
|
||||
Text("CLIMB")
|
||||
.font(.title2)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
@@ -39,12 +39,12 @@ struct ClimbingActivityWidget: Widget {
|
||||
.font(.caption)
|
||||
}
|
||||
} compactLeading: {
|
||||
Text("🧗♂️")
|
||||
Text("CLIMB")
|
||||
} compactTrailing: {
|
||||
Text("\(context.state.totalAttempts)")
|
||||
.monospacedDigit()
|
||||
} minimal: {
|
||||
Text("🧗♂️")
|
||||
Text("CLIMB")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ struct LiveActivityView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("🧗♂️ \(context.attributes.gymName)")
|
||||
Text("CLIMBING: \(context.attributes.gymName)")
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
D2F32FB12E90B26500B1BC56 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D24C19602E75002A0045894C /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D24C19672E75002A0045894C;
|
||||
remoteInfo = OpenClimb;
|
||||
};
|
||||
D2FE949E2E78FEE1008CDB25 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D24C19602E75002A0045894C /* Project object */;
|
||||
@@ -41,6 +48,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
D24C19682E75002A0045894C /* OpenClimb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenClimb.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionStatusLiveExtension.entitlements; sourceTree = "<group>"; };
|
||||
D2F32FAD2E90B26500B1BC56 /* OpenClimbTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenClimbTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2FE94802E78E958008CDB25 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
|
||||
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionStatusLiveExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D2FE948C2E78FEE0008CDB25 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
@@ -73,6 +81,11 @@
|
||||
path = OpenClimb;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2F32FAE2E90B26500B1BC56 /* OpenClimbTests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = OpenClimbTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@@ -92,6 +105,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2F32FAA2E90B26500B1BC56 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2FE94882E78FEE0008CDB25 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -111,6 +131,7 @@
|
||||
D268B79E2E83894A003AA641 /* SessionStatusLiveExtension.entitlements */,
|
||||
D24C196A2E75002A0045894C /* OpenClimb */,
|
||||
D2FE94902E78FEE0008CDB25 /* SessionStatusLive */,
|
||||
D2F32FAE2E90B26500B1BC56 /* OpenClimbTests */,
|
||||
D2FE947F2E78E958008CDB25 /* Frameworks */,
|
||||
D24C19692E75002A0045894C /* Products */,
|
||||
);
|
||||
@@ -121,6 +142,7 @@
|
||||
children = (
|
||||
D24C19682E75002A0045894C /* OpenClimb.app */,
|
||||
D2FE948B2E78FEE0008CDB25 /* SessionStatusLiveExtension.appex */,
|
||||
D2F32FAD2E90B26500B1BC56 /* OpenClimbTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -162,6 +184,29 @@
|
||||
productReference = D24C19682E75002A0045894C /* OpenClimb.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
D2F32FAC2E90B26500B1BC56 /* OpenClimbTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D2F32FB52E90B26500B1BC56 /* Build configuration list for PBXNativeTarget "OpenClimbTests" */;
|
||||
buildPhases = (
|
||||
D2F32FA92E90B26500B1BC56 /* Sources */,
|
||||
D2F32FAA2E90B26500B1BC56 /* Frameworks */,
|
||||
D2F32FAB2E90B26500B1BC56 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
D2F32FB22E90B26500B1BC56 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
D2F32FAE2E90B26500B1BC56 /* OpenClimbTests */,
|
||||
);
|
||||
name = OpenClimbTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = OpenClimbTests;
|
||||
productReference = D2F32FAD2E90B26500B1BC56 /* OpenClimbTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
D2FE948A2E78FEE0008CDB25 /* SessionStatusLiveExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D2FE94A12E78FEE1008CDB25 /* Build configuration list for PBXNativeTarget "SessionStatusLiveExtension" */;
|
||||
@@ -197,6 +242,10 @@
|
||||
D24C19672E75002A0045894C = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
};
|
||||
D2F32FAC2E90B26500B1BC56 = {
|
||||
CreatedOnToolsVersion = 26.0.1;
|
||||
TestTargetID = D24C19672E75002A0045894C;
|
||||
};
|
||||
D2FE948A2E78FEE0008CDB25 = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
};
|
||||
@@ -218,6 +267,7 @@
|
||||
targets = (
|
||||
D24C19672E75002A0045894C /* OpenClimb */,
|
||||
D2FE948A2E78FEE0008CDB25 /* SessionStatusLiveExtension */,
|
||||
D2F32FAC2E90B26500B1BC56 /* OpenClimbTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -230,6 +280,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2F32FAB2E90B26500B1BC56 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2FE94892E78FEE0008CDB25 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -247,6 +304,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2F32FA92E90B26500B1BC56 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D2FE94872E78FEE0008CDB25 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -257,6 +321,11 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
D2F32FB22E90B26500B1BC56 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D24C19672E75002A0045894C /* OpenClimb */;
|
||||
targetProxy = D2F32FB12E90B26500B1BC56 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D2FE949F2E78FEE1008CDB25 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D2FE948A2E78FEE0008CDB25 /* SessionStatusLiveExtension */;
|
||||
@@ -474,6 +543,48 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
D2F32FB32E90B26500B1BC56 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.OpenClimbTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenClimb.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenClimb";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D2F32FB42E90B26500B1BC56 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.atri.dad.OpenClimb.Watch.OpenClimbTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenClimb.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenClimb";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
D2FE94A22E78FEE1008CDB25 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -555,6 +666,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D2F32FB52E90B26500B1BC56 /* Build configuration list for PBXNativeTarget "OpenClimbTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D2F32FB32E90B26500B1BC56 /* Debug */,
|
||||
D2F32FB42E90B26500B1BC56 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D2FE94A12E78FEE1008CDB25 /* Build configuration list for PBXNativeTarget "SessionStatusLiveExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
Binary file not shown.
@@ -28,6 +28,17 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D2F32FAC2E90B26500B1BC56"
|
||||
BuildableName = "OpenClimbTests.xctest"
|
||||
BlueprintName = "OpenClimbTests"
|
||||
ReferencedContainer = "container:OpenClimb.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
|
||||
@@ -44,6 +44,19 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D2F32FAC2E90B26500B1BC56"
|
||||
BuildableName = "OpenClimbTests.xctest"
|
||||
BlueprintName = "OpenClimbTests"
|
||||
ReferencedContainer = "container:OpenClimb.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
||||
@@ -85,7 +85,7 @@ struct ContentView: View {
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
print("📱 App will enter foreground - preparing Live Activity check")
|
||||
print("App will enter foreground - preparing Live Activity check")
|
||||
Task {
|
||||
// Small delay to ensure app is fully active
|
||||
try? await Task.sleep(nanoseconds: 800_000_000) // 0.8 seconds
|
||||
@@ -99,7 +99,7 @@ struct ContentView: View {
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
print("📱 App did become active - checking Live Activity status")
|
||||
print("App did become active - checking Live Activity status")
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
|
||||
await dataManager.onAppBecomeActive()
|
||||
|
||||
@@ -230,7 +230,7 @@ class SyncService: ObservableObject {
|
||||
|
||||
if !hasLocalData && hasServerData {
|
||||
// Case 1: No local data - do full restore from server
|
||||
print("🔄 iOS SYNC: Case 1 - No local data, performing full restore from server")
|
||||
print("iOS SYNC: Case 1 - No local data, performing full restore from server")
|
||||
print("Syncing images from server first...")
|
||||
let imagePathMapping = try await syncImagesFromServer(
|
||||
backup: serverBackup, dataManager: dataManager)
|
||||
@@ -240,7 +240,7 @@ class SyncService: ObservableObject {
|
||||
print("Full restore completed")
|
||||
} else if hasLocalData && !hasServerData {
|
||||
// Case 2: No server data - upload local data to server
|
||||
print("🔄 iOS SYNC: Case 2 - No server data, uploading local data to server")
|
||||
print("iOS SYNC: Case 2 - No server data, uploading local data to server")
|
||||
let currentBackup = createBackupFromDataManager(dataManager)
|
||||
_ = try await uploadData(currentBackup)
|
||||
print("Uploading local images to server...")
|
||||
@@ -251,7 +251,7 @@ class SyncService: ObservableObject {
|
||||
let localTimestamp = parseISO8601ToMillis(timestamp: localBackup.exportedAt)
|
||||
let serverTimestamp = parseISO8601ToMillis(timestamp: serverBackup.exportedAt)
|
||||
|
||||
print("🕐 DEBUG iOS Timestamp Comparison:")
|
||||
print("DEBUG iOS Timestamp Comparison:")
|
||||
print(" Local exportedAt: '\(localBackup.exportedAt)' -> \(localTimestamp)")
|
||||
print(" Server exportedAt: '\(serverBackup.exportedAt)' -> \(serverTimestamp)")
|
||||
print(
|
||||
@@ -261,14 +261,14 @@ class SyncService: ObservableObject {
|
||||
|
||||
if localTimestamp > serverTimestamp {
|
||||
// Local is newer - replace server with local data
|
||||
print("🔄 iOS SYNC: Case 3a - Local data is newer, replacing server content")
|
||||
print("iOS SYNC: Case 3a - Local data is newer, replacing server content")
|
||||
let currentBackup = createBackupFromDataManager(dataManager)
|
||||
_ = try await uploadData(currentBackup)
|
||||
try await syncImagesToServer(dataManager: dataManager)
|
||||
print("Server replaced with local data")
|
||||
} else if serverTimestamp > localTimestamp {
|
||||
// Server is newer - replace local with server data
|
||||
print("🔄 iOS SYNC: Case 3b - Server data is newer, replacing local content")
|
||||
print("iOS SYNC: Case 3b - Server data is newer, replacing local content")
|
||||
let imagePathMapping = try await syncImagesFromServer(
|
||||
backup: serverBackup, dataManager: dataManager)
|
||||
try importBackupToDataManager(
|
||||
@@ -277,7 +277,7 @@ class SyncService: ObservableObject {
|
||||
} else {
|
||||
// Timestamps are equal - no sync needed
|
||||
print(
|
||||
"🔄 iOS SYNC: Case 3c - Data is in sync (timestamps equal), no action needed"
|
||||
"iOS SYNC: Case 3c - Data is in sync (timestamps equal), no action needed"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -36,21 +36,21 @@ class DataStateManager {
|
||||
func updateDataState() {
|
||||
let now = ISO8601DateFormatter().string(from: Date())
|
||||
userDefaults.set(now, forKey: Keys.lastModified)
|
||||
print("📝 iOS Data state updated to: \(now)")
|
||||
print("iOS Data state updated to: \(now)")
|
||||
}
|
||||
|
||||
/// Gets the current data state timestamp. This represents when any data was last modified
|
||||
/// locally.
|
||||
func getLastModified() -> String {
|
||||
if let storedTimestamp = userDefaults.string(forKey: Keys.lastModified) {
|
||||
print("📅 iOS DataStateManager returning stored timestamp: \(storedTimestamp)")
|
||||
print("iOS DataStateManager returning stored timestamp: \(storedTimestamp)")
|
||||
return storedTimestamp
|
||||
}
|
||||
|
||||
// If no timestamp is stored, return epoch time to indicate very old data
|
||||
// This ensures server data will be considered newer than uninitialized local data
|
||||
let epochTime = "1970-01-01T00:00:00.000Z"
|
||||
print("⚠️ No data state timestamp found - returning epoch time: \(epochTime)")
|
||||
print("WARNING: No data state timestamp found - returning epoch time: \(epochTime)")
|
||||
return epochTime
|
||||
}
|
||||
|
||||
|
||||
@@ -262,10 +262,10 @@ import SwiftUI
|
||||
ForEach(testResults.indices, id: \.self) { index in
|
||||
HStack {
|
||||
Image(
|
||||
systemName: testResults[index].contains("✅")
|
||||
systemName: testResults[index].contains("PASS")
|
||||
? "checkmark.circle.fill" : "exclamationmark.triangle.fill"
|
||||
)
|
||||
.foregroundColor(testResults[index].contains("✅") ? .green : .orange)
|
||||
.foregroundColor(testResults[index].contains("PASS") ? .green : .orange)
|
||||
|
||||
Text(testResults[index])
|
||||
.font(.caption)
|
||||
@@ -285,24 +285,24 @@ import SwiftUI
|
||||
|
||||
// Test 1: Check iOS version compatibility
|
||||
if iconHelper.supportsModernIconFeatures {
|
||||
testResults.append("✅ iOS 17+ features supported")
|
||||
testResults.append("PASS: iOS 17+ features supported")
|
||||
} else {
|
||||
testResults.append(
|
||||
"⚠️ Running on iOS version that doesn't support modern icon features")
|
||||
"WARNING: Running on iOS version that doesn't support modern icon features")
|
||||
}
|
||||
|
||||
// Test 2: Check dark mode detection
|
||||
let detectedDarkMode = iconHelper.isInDarkMode(for: colorScheme)
|
||||
let systemDarkMode = colorScheme == .dark
|
||||
if detectedDarkMode == systemDarkMode {
|
||||
testResults.append("✅ Dark mode detection matches system setting")
|
||||
testResults.append("PASS: Dark mode detection matches system setting")
|
||||
} else {
|
||||
testResults.append("⚠️ Dark mode detection mismatch")
|
||||
testResults.append("WARNING: Dark mode detection mismatch")
|
||||
}
|
||||
|
||||
// Test 3: Check recommended variant
|
||||
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
|
||||
testResults.append("✅ Recommended icon variant: \(variant.description)")
|
||||
testResults.append("PASS: Recommended icon variant: \(variant.description)")
|
||||
|
||||
// Test 4: Test asset availability
|
||||
validateAssetConfiguration()
|
||||
@@ -315,7 +315,7 @@ import SwiftUI
|
||||
iconHelper.updateDarkModeStatus(for: colorScheme)
|
||||
let variant = iconHelper.getRecommendedIconVariant(for: colorScheme)
|
||||
testResults.append(
|
||||
"✅ Icon appearance test completed - Current variant: \(variant.description)")
|
||||
"PASS: Icon appearance test completed - Current variant: \(variant.description)")
|
||||
}
|
||||
|
||||
private func validateAssetConfiguration() {
|
||||
@@ -326,20 +326,20 @@ import SwiftUI
|
||||
]
|
||||
|
||||
for asset in expectedAssets {
|
||||
testResults.append("✅ Asset '\(asset)' configuration found")
|
||||
testResults.append("PASS: Asset '\(asset)' configuration found")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkBundleResources() {
|
||||
// Check bundle identifier
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? "Unknown"
|
||||
testResults.append("✅ Bundle ID: \(bundleId)")
|
||||
testResults.append("PASS: Bundle ID: \(bundleId)")
|
||||
|
||||
// Check app version
|
||||
let version =
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
|
||||
testResults.append("✅ App version: \(version) (\(build))")
|
||||
testResults.append("PASS: App version: \(version) (\(build))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class ImageManager {
|
||||
|
||||
// Final integrity check
|
||||
if !validateStorageIntegrity() {
|
||||
print("🚨 CRITICAL: Storage integrity compromised - attempting emergency recovery")
|
||||
print("CRITICAL: Storage integrity compromised - attempting emergency recovery")
|
||||
emergencyImageRestore()
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ class ImageManager {
|
||||
attributes: [
|
||||
.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication
|
||||
])
|
||||
print("✅ Created directory: \(directory.path)")
|
||||
print("Created directory: \(directory.path)")
|
||||
} catch {
|
||||
print("❌ Failed to create directory \(directory.path): \(error)")
|
||||
print("ERROR: Failed to create directory \(directory.path): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,9 @@ class ImageManager {
|
||||
var backupURL = backupDirectory
|
||||
try imagesURL.setResourceValues(resourceValues)
|
||||
try backupURL.setResourceValues(resourceValues)
|
||||
print("✅ Excluded image directories from iCloud backup")
|
||||
print("Excluded image directories from iCloud backup")
|
||||
} catch {
|
||||
print("⚠️ Failed to exclude from iCloud backup: \(error)")
|
||||
print("WARNING: Failed to exclude from iCloud backup: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,11 +114,11 @@ class ImageManager {
|
||||
}
|
||||
|
||||
private func performRobustMigration() {
|
||||
print("🔄 Starting robust image migration system...")
|
||||
print("Starting robust image migration system...")
|
||||
|
||||
// Check for interrupted migration
|
||||
if let incompleteState = loadMigrationState() {
|
||||
print("🔧 Detected interrupted migration, resuming...")
|
||||
print("Detected interrupted migration, resuming...")
|
||||
resumeMigration(from: incompleteState)
|
||||
} else {
|
||||
// Start fresh migration
|
||||
@@ -135,7 +135,7 @@ class ImageManager {
|
||||
private func startNewMigration() {
|
||||
// First check for images in previous Application Support directories
|
||||
if let previousAppSupportImages = findPreviousAppSupportImages() {
|
||||
print("📁 Found images in previous Application Support directory")
|
||||
print("Found images in previous Application Support directory")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class ImageManager {
|
||||
let hasLegacyImportImages = fileManager.fileExists(atPath: legacyImportImagesDirectory.path)
|
||||
|
||||
guard hasLegacyImages || hasLegacyImportImages else {
|
||||
print("✅ No legacy images to migrate")
|
||||
print("No legacy images to migrate")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class ImageManager {
|
||||
let legacyFiles = try fileManager.contentsOfDirectory(
|
||||
atPath: legacyImagesDirectory.path)
|
||||
allLegacyFiles.append(contentsOf: legacyFiles)
|
||||
print("📦 Found \(legacyFiles.count) images in OpenClimbImages")
|
||||
print("Found \(legacyFiles.count) images in OpenClimbImages")
|
||||
}
|
||||
|
||||
// Collect files from Documents/images directory
|
||||
@@ -168,10 +168,10 @@ class ImageManager {
|
||||
let importFiles = try fileManager.contentsOfDirectory(
|
||||
atPath: legacyImportImagesDirectory.path)
|
||||
allLegacyFiles.append(contentsOf: importFiles)
|
||||
print("📦 Found \(importFiles.count) images in Documents/images")
|
||||
print("Found \(importFiles.count) images in Documents/images")
|
||||
}
|
||||
|
||||
print("📦 Total legacy images to migrate: \(allLegacyFiles.count)")
|
||||
print("Total legacy images to migrate: \(allLegacyFiles.count)")
|
||||
|
||||
let initialState = MigrationState(
|
||||
version: MigrationState.currentVersion,
|
||||
@@ -186,24 +186,24 @@ class ImageManager {
|
||||
performMigrationWithCheckpoints(files: allLegacyFiles, currentState: initialState)
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to start migration: \(error)")
|
||||
print("ERROR: Failed to start migration: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func resumeMigration(from state: MigrationState) {
|
||||
print("🔄 Resuming migration from checkpoint...")
|
||||
print("📊 Progress: \(state.completedFiles.count)/\(state.totalFiles)")
|
||||
print("Resuming migration from checkpoint...")
|
||||
print("Progress: \(state.completedFiles.count)/\(state.totalFiles)")
|
||||
|
||||
do {
|
||||
let legacyFiles = try fileManager.contentsOfDirectory(
|
||||
atPath: legacyImagesDirectory.path)
|
||||
let remainingFiles = legacyFiles.filter { !state.completedFiles.contains($0) }
|
||||
|
||||
print("📦 Resuming with \(remainingFiles.count) remaining files")
|
||||
print("Resuming with \(remainingFiles.count) remaining files")
|
||||
performMigrationWithCheckpoints(files: remainingFiles, currentState: state)
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to resume migration: \(error)")
|
||||
print("ERROR: Failed to resume migration: \(error)")
|
||||
// Fallback: start fresh
|
||||
removeMigrationState()
|
||||
startNewMigration()
|
||||
@@ -270,11 +270,11 @@ class ImageManager {
|
||||
completedFiles.append(fileName)
|
||||
migratedCount += 1
|
||||
|
||||
print("✅ Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))")
|
||||
print("Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))")
|
||||
|
||||
} catch {
|
||||
failedCount += 1
|
||||
print("❌ Failed to migrate \(fileName): \(error)")
|
||||
print("ERROR: Failed to migrate \(fileName): \(error)")
|
||||
}
|
||||
|
||||
// Save checkpoint every 5 files or if interrupted
|
||||
@@ -288,7 +288,7 @@ class ImageManager {
|
||||
lastCheckpoint: Date()
|
||||
)
|
||||
saveMigrationState(checkpointState)
|
||||
print("💾 Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)")
|
||||
print("Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ class ImageManager {
|
||||
)
|
||||
saveMigrationState(finalState)
|
||||
|
||||
print("🏁 Migration complete: \(migratedCount) migrated, \(failedCount) failed")
|
||||
print("Migration complete: \(migratedCount) migrated, \(failedCount) failed")
|
||||
|
||||
// Clean up legacy directory if no failures
|
||||
if failedCount == 0 {
|
||||
@@ -313,7 +313,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
private func verifyMigrationIntegrity() {
|
||||
print("🔍 Verifying migration integrity...")
|
||||
print("Verifying migration integrity...")
|
||||
|
||||
var allLegacyFiles = Set<String>()
|
||||
|
||||
@@ -331,12 +331,12 @@ class ImageManager {
|
||||
allLegacyFiles.formUnion(importFiles)
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to read legacy directories: \(error)")
|
||||
print("ERROR: Failed to read legacy directories: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
guard !allLegacyFiles.isEmpty else {
|
||||
print("✅ No legacy directories to verify against")
|
||||
print("No legacy directories to verify against")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -347,10 +347,10 @@ class ImageManager {
|
||||
let missingFiles = allLegacyFiles.subtracting(migratedFiles)
|
||||
|
||||
if missingFiles.isEmpty {
|
||||
print("✅ Migration integrity verified - all files present")
|
||||
print("Migration integrity verified - all files present")
|
||||
cleanupLegacyDirectory()
|
||||
} else {
|
||||
print("⚠️ Missing \(missingFiles.count) files, re-triggering migration")
|
||||
print("WARNING: Missing \(missingFiles.count) files, re-triggering migration")
|
||||
// Re-trigger migration for missing files
|
||||
performMigrationWithCheckpoints(
|
||||
files: Array(missingFiles),
|
||||
@@ -364,16 +364,16 @@ class ImageManager {
|
||||
))
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to verify migration integrity: \(error)")
|
||||
print("ERROR: Failed to verify migration integrity: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupLegacyDirectory() {
|
||||
do {
|
||||
try fileManager.removeItem(at: legacyImagesDirectory)
|
||||
print("🗑️ Cleaned up legacy directory")
|
||||
print("Cleaned up legacy directory")
|
||||
} catch {
|
||||
print("⚠️ Failed to clean up legacy directory: \(error)")
|
||||
print("WARNING: Failed to clean up legacy directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,14 +395,14 @@ class ImageManager {
|
||||
|
||||
// Check if state is too old (more than 1 hour)
|
||||
if Date().timeIntervalSince(state.lastCheckpoint) > 3600 {
|
||||
print("⚠️ Migration state is stale, starting fresh")
|
||||
print("WARNING: Migration state is stale, starting fresh")
|
||||
removeMigrationState()
|
||||
return nil
|
||||
}
|
||||
|
||||
return state.isComplete ? nil : state
|
||||
} catch {
|
||||
print("❌ Failed to load migration state: \(error)")
|
||||
print("ERROR: Failed to load migration state: \(error)")
|
||||
removeMigrationState()
|
||||
return nil
|
||||
}
|
||||
@@ -413,7 +413,7 @@ class ImageManager {
|
||||
let data = try JSONEncoder().encode(state)
|
||||
try data.write(to: migrationStateURL)
|
||||
} catch {
|
||||
print("❌ Failed to save migration state: \(error)")
|
||||
print("ERROR: Failed to save migration state: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ class ImageManager {
|
||||
private func cleanupMigrationState() {
|
||||
try? fileManager.removeItem(at: migrationStateURL)
|
||||
try? fileManager.removeItem(at: migrationLockURL)
|
||||
print("🧹 Cleaned up migration state files")
|
||||
print("Cleaned up migration state files")
|
||||
}
|
||||
|
||||
func saveImageData(_ data: Data, withName name: String? = nil) -> String? {
|
||||
@@ -444,10 +444,10 @@ class ImageManager {
|
||||
// Create backup copy
|
||||
try data.write(to: backupPath)
|
||||
|
||||
print("✅ Saved image with backup: \(fileName)")
|
||||
print("Saved image with backup: \(fileName)")
|
||||
return fileName
|
||||
} catch {
|
||||
print("❌ Failed to save image \(fileName): \(error)")
|
||||
print("ERROR: Failed to save image \(fileName): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ class ImageManager {
|
||||
if fileManager.fileExists(atPath: backupPath.path),
|
||||
let data = try? Data(contentsOf: backupPath)
|
||||
{
|
||||
print("📦 Restored image from backup: \(path)")
|
||||
print("Restored image from backup: \(path)")
|
||||
|
||||
// Restore to primary location
|
||||
try? data.write(to: URL(fileURLWithPath: primaryPath))
|
||||
@@ -497,7 +497,7 @@ class ImageManager {
|
||||
do {
|
||||
try fileManager.removeItem(atPath: primaryPath)
|
||||
} catch {
|
||||
print("❌ Failed to delete primary image at \(primaryPath): \(error)")
|
||||
print("ERROR: Failed to delete primary image at \(primaryPath): \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
@@ -507,7 +507,7 @@ class ImageManager {
|
||||
do {
|
||||
try fileManager.removeItem(at: backupPath)
|
||||
} catch {
|
||||
print("❌ Failed to delete backup image at \(backupPath.path): \(error)")
|
||||
print("ERROR: Failed to delete backup image at \(backupPath.path): \(error)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
@@ -544,7 +544,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
func performMaintenance() {
|
||||
print("🔧 Starting image maintenance...")
|
||||
print("Starting image maintenance...")
|
||||
|
||||
syncBackups()
|
||||
validateImageIntegrity()
|
||||
@@ -562,11 +562,11 @@ class ImageManager {
|
||||
let backupPath = backupDirectory.appendingPathComponent(fileName)
|
||||
|
||||
try? fileManager.copyItem(at: primaryPath, to: backupPath)
|
||||
print("🔄 Created missing backup for: \(fileName)")
|
||||
print("Created missing backup for: \(fileName)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("❌ Failed to sync backups: \(error)")
|
||||
print("ERROR: Failed to sync backups: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,15 +585,15 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Validated \(validFiles) of \(files.count) image files")
|
||||
print("Validated \(validFiles) of \(files.count) image files")
|
||||
} catch {
|
||||
print("❌ Failed to validate images: \(error)")
|
||||
print("ERROR: Failed to validate images: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupOrphanedFiles() {
|
||||
// This would need access to the data manager to check which files are actually referenced
|
||||
print("🧹 Cleanup would require coordination with data manager")
|
||||
print("Cleanup would require coordination with data manager")
|
||||
}
|
||||
|
||||
func getStorageInfo() -> (primaryCount: Int, backupCount: Int, totalSize: Int64) {
|
||||
@@ -623,7 +623,7 @@ class ImageManager {
|
||||
let previousDir = findPreviousAppSupportImages()
|
||||
print(
|
||||
"""
|
||||
📁 OpenClimb Image Storage:
|
||||
OpenClimb Image Storage:
|
||||
- App Support: \(appSupportDirectory.path)
|
||||
- Images: \(imagesDirectory.path) (\(info.primaryCount) files)
|
||||
- Backups: \(backupDirectory.path) (\(info.backupCount) files)
|
||||
@@ -635,7 +635,7 @@ class ImageManager {
|
||||
}
|
||||
|
||||
func forceRecoveryMigration() {
|
||||
print("🚨 FORCE RECOVERY: Starting manual migration recovery...")
|
||||
print("FORCE RECOVERY: Starting manual migration recovery...")
|
||||
|
||||
// Remove any stale state
|
||||
removeMigrationState()
|
||||
@@ -644,7 +644,7 @@ class ImageManager {
|
||||
// Force fresh migration
|
||||
startNewMigration()
|
||||
|
||||
print("🚨 FORCE RECOVERY: Migration recovery completed")
|
||||
print("FORCE RECOVERY: Migration recovery completed")
|
||||
}
|
||||
|
||||
func saveImportedImage(_ imageData: Data, filename: String) throws -> String {
|
||||
@@ -657,12 +657,12 @@ class ImageManager {
|
||||
// Create backup
|
||||
try? imageData.write(to: backupPath)
|
||||
|
||||
print("📥 Imported image: \(filename)")
|
||||
print("Imported image: \(filename)")
|
||||
return filename
|
||||
}
|
||||
|
||||
func emergencyImageRestore() {
|
||||
print("🆘 EMERGENCY: Attempting image restoration...")
|
||||
print("EMERGENCY: Attempting image restoration...")
|
||||
|
||||
// Try to restore from backup directory
|
||||
do {
|
||||
@@ -680,14 +680,14 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
print("🆘 EMERGENCY: Restored \(restoredCount) images from backup")
|
||||
print("EMERGENCY: Restored \(restoredCount) images from backup")
|
||||
} catch {
|
||||
print("🆘 EMERGENCY: Failed to restore from backup: \(error)")
|
||||
print("EMERGENCY: Failed to restore from backup: \(error)")
|
||||
}
|
||||
|
||||
// Try previous Application Support directories first
|
||||
if let previousAppSupportImages = findPreviousAppSupportImages() {
|
||||
print("🆘 EMERGENCY: Found previous Application Support images, migrating...")
|
||||
print("EMERGENCY: Found previous Application Support images, migrating...")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return
|
||||
}
|
||||
@@ -696,21 +696,21 @@ class ImageManager {
|
||||
if fileManager.fileExists(atPath: legacyImagesDirectory.path)
|
||||
|| fileManager.fileExists(atPath: legacyImportImagesDirectory.path)
|
||||
{
|
||||
print("🆘 EMERGENCY: Attempting legacy migration as fallback...")
|
||||
print("EMERGENCY: Attempting legacy migration as fallback...")
|
||||
forceRecoveryMigration()
|
||||
}
|
||||
}
|
||||
|
||||
func debugSafeInitialization() -> Bool {
|
||||
print("🐛 DEBUG SAFE: Performing debug-safe initialization check...")
|
||||
print("DEBUG SAFE: Performing debug-safe initialization check...")
|
||||
|
||||
// Check if we're in a debug environment
|
||||
#if DEBUG
|
||||
print("🐛 DEBUG SAFE: Debug environment detected")
|
||||
print("DEBUG SAFE: Debug environment detected")
|
||||
|
||||
// Check for interrupted migration more aggressively
|
||||
if fileManager.fileExists(atPath: migrationLockURL.path) {
|
||||
print("🐛 DEBUG SAFE: Found migration lock - likely debug interruption")
|
||||
print("DEBUG SAFE: Found migration lock - likely debug interruption")
|
||||
|
||||
// Give extra time for file system to stabilize
|
||||
Thread.sleep(forTimeInterval: 1.0)
|
||||
@@ -732,14 +732,14 @@ class ImageManager {
|
||||
((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count > 0
|
||||
|
||||
if primaryEmpty && backupHasFiles {
|
||||
print("🐛 DEBUG SAFE: Primary empty but backup exists - restoring")
|
||||
print("DEBUG SAFE: Primary empty but backup exists - restoring")
|
||||
emergencyImageRestore()
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if primary storage is empty but previous Application Support images exist
|
||||
if primaryEmpty, let previousAppSupportImages = findPreviousAppSupportImages() {
|
||||
print("🐛 DEBUG SAFE: Primary empty but found previous Application Support images")
|
||||
print("DEBUG SAFE: Primary empty but found previous Application Support images")
|
||||
migratePreviousAppSupportImages(from: previousAppSupportImages)
|
||||
return true
|
||||
}
|
||||
@@ -755,13 +755,15 @@ class ImageManager {
|
||||
|
||||
// Check if we have more backups than primary files (sign of corruption)
|
||||
if backupFiles.count > primaryFiles.count + 5 {
|
||||
print("⚠️ INTEGRITY: Backup count significantly exceeds primary - potential corruption")
|
||||
print(
|
||||
"WARNING INTEGRITY: Backup count significantly exceeds primary - potential corruption"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if primary is completely empty but we have data elsewhere
|
||||
if primaryFiles.isEmpty && !backupFiles.isEmpty {
|
||||
print("⚠️ INTEGRITY: Primary storage empty but backups exist")
|
||||
print("WARNING INTEGRITY: Primary storage empty but backups exist")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -775,7 +777,7 @@ class ImageManager {
|
||||
for: .applicationSupportDirectory, in: .userDomainMask
|
||||
).first
|
||||
else {
|
||||
print("❌ Could not access Application Support directory")
|
||||
print("ERROR: Could not access Application Support directory")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -808,13 +810,13 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("❌ Error scanning for previous Application Support directories: \(error)")
|
||||
print("ERROR: Error scanning for previous Application Support directories: \(error)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func migratePreviousAppSupportImages(from sourceDirectory: URL) {
|
||||
print("🔄 Migrating images from previous Application Support directory")
|
||||
print("Migrating images from previous Application Support directory")
|
||||
|
||||
do {
|
||||
let imageFiles = try fileManager.contentsOfDirectory(atPath: sourceDirectory.path)
|
||||
@@ -837,17 +839,17 @@ class ImageManager {
|
||||
// Create backup
|
||||
try? fileManager.copyItem(at: sourcePath, to: backupPath)
|
||||
|
||||
print("✅ Migrated: \(fileName)")
|
||||
print("Migrated: \(fileName)")
|
||||
} catch {
|
||||
print("❌ Failed to migrate \(fileName): \(error)")
|
||||
print("ERROR: Failed to migrate \(fileName): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("✅ Completed migration from previous Application Support directory")
|
||||
print("Completed migration from previous Application Support directory")
|
||||
|
||||
} catch {
|
||||
print("❌ Failed to migrate from previous Application Support: \(error)")
|
||||
print("ERROR: Failed to migrate from previous Application Support: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,20 +554,20 @@ class ClimbingDataManager: ObservableObject {
|
||||
|
||||
// Collect referenced image paths
|
||||
let referencedImagePaths = collectReferencedImagePaths()
|
||||
print("🎯 Starting export with \(referencedImagePaths.count) images")
|
||||
print("Starting export with \(referencedImagePaths.count) images")
|
||||
|
||||
let zipData = try ZipUtils.createExportZip(
|
||||
exportData: exportData,
|
||||
referencedImagePaths: referencedImagePaths
|
||||
)
|
||||
|
||||
print("✅ Export completed successfully")
|
||||
print("Export completed successfully")
|
||||
successMessage = "Export completed with \(referencedImagePaths.count) images"
|
||||
clearMessageAfterDelay()
|
||||
return zipData
|
||||
} catch {
|
||||
let errorMessage = "Export failed: \(error.localizedDescription)"
|
||||
print("❌ \(errorMessage)")
|
||||
print("ERROR: \(errorMessage)")
|
||||
setError(errorMessage)
|
||||
return nil
|
||||
}
|
||||
@@ -662,13 +662,13 @@ class ClimbingDataManager: ObservableObject {
|
||||
extension ClimbingDataManager {
|
||||
private func collectReferencedImagePaths() -> Set<String> {
|
||||
var imagePaths = Set<String>()
|
||||
print("🖼️ Starting image path collection...")
|
||||
print("📊 Total problems: \(problems.count)")
|
||||
print("Starting image path collection...")
|
||||
print("Total problems: \(problems.count)")
|
||||
|
||||
for problem in problems {
|
||||
if !problem.imagePaths.isEmpty {
|
||||
print(
|
||||
"📸 Problem '\(problem.name ?? "Unnamed")' has \(problem.imagePaths.count) images"
|
||||
"Problem '\(problem.name ?? "Unnamed")' has \(problem.imagePaths.count) images"
|
||||
)
|
||||
for imagePath in problem.imagePaths {
|
||||
print(" - Relative path: \(imagePath)")
|
||||
@@ -677,10 +677,10 @@ extension ClimbingDataManager {
|
||||
|
||||
// Check if file exists
|
||||
if FileManager.default.fileExists(atPath: fullPath) {
|
||||
print(" ✅ File exists")
|
||||
print(" File exists")
|
||||
imagePaths.insert(fullPath)
|
||||
} else {
|
||||
print(" ❌ File does NOT exist")
|
||||
print(" File does NOT exist")
|
||||
// Still add it to let ZipUtils handle the error logging
|
||||
imagePaths.insert(fullPath)
|
||||
}
|
||||
@@ -688,7 +688,7 @@ extension ClimbingDataManager {
|
||||
}
|
||||
}
|
||||
|
||||
print("🖼️ Collected \(imagePaths.count) total image paths for export")
|
||||
print("Collected \(imagePaths.count) total image paths for export")
|
||||
return imagePaths
|
||||
}
|
||||
|
||||
@@ -748,7 +748,7 @@ extension ClimbingDataManager {
|
||||
// Log storage information for debugging
|
||||
let info = await ImageManager.shared.getStorageInfo()
|
||||
print(
|
||||
"📊 Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total"
|
||||
"Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total"
|
||||
)
|
||||
}.value
|
||||
}
|
||||
@@ -786,7 +786,7 @@ extension ClimbingDataManager {
|
||||
}
|
||||
|
||||
if !orphanedFiles.isEmpty {
|
||||
print("🗑️ Cleaned up \(orphanedFiles.count) orphaned image files")
|
||||
print("Cleaned up \(orphanedFiles.count) orphaned image files")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,7 +803,7 @@ extension ClimbingDataManager {
|
||||
}
|
||||
|
||||
func forceImageRecovery() {
|
||||
print("🚨 User initiated force image recovery")
|
||||
print("User initiated force image recovery")
|
||||
ImageManager.shared.forceRecoveryMigration()
|
||||
|
||||
// Refresh the UI after recovery
|
||||
@@ -811,7 +811,7 @@ extension ClimbingDataManager {
|
||||
}
|
||||
|
||||
func emergencyImageRestore() {
|
||||
print("🆘 User initiated emergency image restore")
|
||||
print("User initiated emergency image restore")
|
||||
ImageManager.shared.emergencyImageRestore()
|
||||
|
||||
// Refresh the UI after restore
|
||||
@@ -827,7 +827,7 @@ extension ClimbingDataManager {
|
||||
let info = ImageManager.shared.getStorageInfo()
|
||||
|
||||
return """
|
||||
Image Storage Health: \(isValid ? "✅ Good" : "❌ Needs Recovery")
|
||||
Image Storage Health: \(isValid ? "Good" : "Needs Recovery")
|
||||
Primary Files: \(info.primaryCount)
|
||||
Backup Files: \(info.backupCount)
|
||||
Total Size: \(formatBytes(info.totalSize))
|
||||
@@ -845,7 +845,7 @@ extension ClimbingDataManager {
|
||||
|
||||
// Test with dummy data if we have a gym
|
||||
guard let testGym = gyms.first else {
|
||||
print("❌ No gyms available for testing")
|
||||
print("ERROR: No gyms available for testing")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -877,14 +877,14 @@ extension ClimbingDataManager {
|
||||
// Only restart if session is actually active
|
||||
guard activeSession.status == .active else {
|
||||
print(
|
||||
"⚠️ Session exists but is not active (status: \(activeSession.status)), ending Live Activity"
|
||||
"WARNING: Session exists but is not active (status: \(activeSession.status)), ending Live Activity"
|
||||
)
|
||||
await LiveActivityManager.shared.endLiveActivity()
|
||||
return
|
||||
}
|
||||
|
||||
if let gym = gym(withId: activeSession.gymId) {
|
||||
print("🔍 Checking Live Activity for active session at \(gym.name)")
|
||||
print("Checking Live Activity for active session at \(gym.name)")
|
||||
|
||||
// First cleanup any dismissed activities
|
||||
await LiveActivityManager.shared.cleanupDismissedActivities()
|
||||
@@ -894,15 +894,12 @@ extension ClimbingDataManager {
|
||||
activeSession: activeSession,
|
||||
gymName: gym.name
|
||||
)
|
||||
|
||||
// Update with current session data
|
||||
await updateLiveActivityData()
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this when app becomes active to check for Live Activity restart
|
||||
func onAppBecomeActive() {
|
||||
print("📱 App became active - checking Live Activity status")
|
||||
print("App became active - checking Live Activity status")
|
||||
Task {
|
||||
await checkAndRestartLiveActivity()
|
||||
}
|
||||
@@ -910,7 +907,7 @@ extension ClimbingDataManager {
|
||||
|
||||
/// Call this when app enters background to update Live Activity
|
||||
func onAppEnterBackground() {
|
||||
print("📱 App entering background - updating Live Activity if needed")
|
||||
print("App entering background - updating Live Activity if needed")
|
||||
Task {
|
||||
await updateLiveActivityData()
|
||||
}
|
||||
@@ -939,7 +936,7 @@ extension ClimbingDataManager {
|
||||
return
|
||||
}
|
||||
|
||||
print("🔄 Attempting to restart dismissed Live Activity for \(gym.name)")
|
||||
print("Attempting to restart dismissed Live Activity for \(gym.name)")
|
||||
|
||||
// Wait a bit before restarting to avoid frequency limits
|
||||
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
||||
@@ -979,7 +976,7 @@ extension ClimbingDataManager {
|
||||
activeSession.status == .active,
|
||||
let gym = gym(withId: activeSession.gymId)
|
||||
else {
|
||||
print("⚠️ Live Activity update skipped - no active session or gym")
|
||||
print("WARNING: Live Activity update skipped - no active session or gym")
|
||||
if let session = activeSession {
|
||||
print(" Session ID: \(session.id)")
|
||||
print(" Session Status: \(session.status)")
|
||||
@@ -1003,7 +1000,7 @@ extension ClimbingDataManager {
|
||||
elapsedInterval = 0
|
||||
}
|
||||
|
||||
print("🔄 Live Activity Update Debug:")
|
||||
print("Live Activity Update Debug:")
|
||||
print(" Session ID: \(activeSession.id)")
|
||||
print(" Gym: \(gym.name)")
|
||||
print(" Total attempts in session: \(totalAttempts)")
|
||||
|
||||
@@ -34,11 +34,11 @@ final class LiveActivityManager {
|
||||
let isStillActive = activities.contains { $0.id == currentActivity.id }
|
||||
|
||||
if isStillActive {
|
||||
print("ℹ️ Live Activity still running: \(currentActivity.id)")
|
||||
print("Live Activity still running: \(currentActivity.id)")
|
||||
return
|
||||
} else {
|
||||
print(
|
||||
"⚠️ Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference"
|
||||
"WARNING: Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference"
|
||||
)
|
||||
self.currentActivity = nil
|
||||
}
|
||||
@@ -47,18 +47,18 @@ final class LiveActivityManager {
|
||||
// Check if there are ANY active Live Activities for this session
|
||||
let existingActivities = Activity<SessionActivityAttributes>.activities
|
||||
if let existingActivity = existingActivities.first {
|
||||
print("ℹ️ Found existing Live Activity: \(existingActivity.id), using it")
|
||||
print("Found existing Live Activity: \(existingActivity.id), using it")
|
||||
self.currentActivity = existingActivity
|
||||
return
|
||||
}
|
||||
|
||||
print("🔄 No Live Activity found, restarting for existing session")
|
||||
print("No Live Activity found, restarting for existing session")
|
||||
await startLiveActivity(for: activeSession, gymName: gymName)
|
||||
}
|
||||
|
||||
/// Call this when a ClimbSession starts to begin a Live Activity
|
||||
func startLiveActivity(for session: ClimbSession, gymName: String) async {
|
||||
print("🔴 Starting Live Activity for gym: \(gymName)")
|
||||
print("Starting Live Activity for gym: \(gymName)")
|
||||
|
||||
await endLiveActivity()
|
||||
|
||||
@@ -84,9 +84,9 @@ final class LiveActivityManager {
|
||||
pushType: nil
|
||||
)
|
||||
self.currentActivity = activity
|
||||
print("✅ Live Activity started successfully: \(activity.id)")
|
||||
print("Live Activity started successfully: \(activity.id)")
|
||||
} catch {
|
||||
print("❌ Failed to start live activity: \(error)")
|
||||
print("ERROR: Failed to start live activity: \(error)")
|
||||
print("Error details: \(error.localizedDescription)")
|
||||
|
||||
// Check specific error types
|
||||
@@ -104,7 +104,7 @@ final class LiveActivityManager {
|
||||
func updateLiveActivity(elapsed: TimeInterval, totalAttempts: Int, completedProblems: Int) async
|
||||
{
|
||||
guard let currentActivity = currentActivity else {
|
||||
print("⚠️ No current activity to update")
|
||||
print("WARNING: No current activity to update")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -114,14 +114,14 @@ final class LiveActivityManager {
|
||||
|
||||
if !isStillActive {
|
||||
print(
|
||||
"⚠️ Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference"
|
||||
"WARNING: Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference"
|
||||
)
|
||||
self.currentActivity = nil
|
||||
return
|
||||
}
|
||||
|
||||
print(
|
||||
"🔄 Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)"
|
||||
"Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)"
|
||||
)
|
||||
|
||||
let updatedContentState = SessionActivityAttributes.ContentState(
|
||||
@@ -131,7 +131,7 @@ final class LiveActivityManager {
|
||||
)
|
||||
|
||||
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
|
||||
print("✅ Live Activity updated successfully")
|
||||
print("Live Activity updated successfully")
|
||||
}
|
||||
|
||||
/// Call this when a ClimbSession ends to end the Live Activity
|
||||
@@ -141,25 +141,25 @@ final class LiveActivityManager {
|
||||
|
||||
// First end the tracked activity if it exists
|
||||
if let currentActivity {
|
||||
print("🔴 Ending tracked Live Activity: \(currentActivity.id)")
|
||||
print("Ending tracked Live Activity: \(currentActivity.id)")
|
||||
await currentActivity.end(nil, dismissalPolicy: .immediate)
|
||||
self.currentActivity = nil
|
||||
print("✅ Tracked Live Activity ended successfully")
|
||||
print("Tracked Live Activity ended successfully")
|
||||
}
|
||||
|
||||
// Force end ALL active activities of our type to ensure cleanup
|
||||
print("🔍 Checking for any remaining active activities...")
|
||||
print("Checking for any remaining active activities...")
|
||||
let activities = Activity<SessionActivityAttributes>.activities
|
||||
|
||||
if activities.isEmpty {
|
||||
print("ℹ️ No additional activities found")
|
||||
print("No additional activities found")
|
||||
} else {
|
||||
print("🔴 Found \(activities.count) additional active activities, ending them...")
|
||||
print("Found \(activities.count) additional active activities, ending them...")
|
||||
for activity in activities {
|
||||
print("🔴 Force ending activity: \(activity.id)")
|
||||
print("Force ending activity: \(activity.id)")
|
||||
await activity.end(nil, dismissalPolicy: .immediate)
|
||||
}
|
||||
print("✅ All Live Activities ended successfully")
|
||||
print("All Live Activities ended successfully")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ final class LiveActivityManager {
|
||||
if let currentActivity = currentActivity {
|
||||
let isStillActive = activities.contains { $0.id == currentActivity.id }
|
||||
if !isStillActive {
|
||||
print("🧹 Cleaning up dismissed Live Activity: \(currentActivity.id)")
|
||||
print("Cleaning up dismissed Live Activity: \(currentActivity.id)")
|
||||
self.currentActivity = nil
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ final class LiveActivityManager {
|
||||
func stopHealthChecks() {
|
||||
healthCheckTimer?.invalidate()
|
||||
healthCheckTimer = nil
|
||||
print("🛑 Stopped Live Activity health checks")
|
||||
print("Stopped Live Activity health checks")
|
||||
}
|
||||
|
||||
/// Perform a health check on the current Live Activity
|
||||
@@ -231,7 +231,7 @@ final class LiveActivityManager {
|
||||
let isStillActive = activities.contains { $0.id == currentActivity.id }
|
||||
|
||||
if !isStillActive {
|
||||
print("💔 Health check failed - Live Activity was dismissed")
|
||||
print("Health check failed - Live Activity was dismissed")
|
||||
self.currentActivity = nil
|
||||
|
||||
// Notify that we need to restart
|
||||
@@ -240,7 +240,7 @@ final class LiveActivityManager {
|
||||
object: nil
|
||||
)
|
||||
} else {
|
||||
print("✅ Live Activity health check passed")
|
||||
print("Live Activity health check passed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ struct LiveActivityDebugView: View {
|
||||
.disabled(dataManager.activeSession == nil)
|
||||
|
||||
if dataManager.gyms.isEmpty {
|
||||
Text("⚠️ Add at least one gym to test Live Activities")
|
||||
Text("WARNING: Add at least one gym to test Live Activities")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
@@ -167,29 +167,31 @@ struct LiveActivityDebugView: View {
|
||||
}
|
||||
|
||||
private func checkStatus() {
|
||||
appendDebugOutput("🔍 Checking Live Activity status...")
|
||||
appendDebugOutput("Checking Live Activity status...")
|
||||
|
||||
let status = LiveActivityManager.shared.checkLiveActivityAvailability()
|
||||
appendDebugOutput("Status: \(status)")
|
||||
|
||||
// Check iOS version
|
||||
if #available(iOS 16.1, *) {
|
||||
appendDebugOutput("✅ iOS version supports Live Activities")
|
||||
appendDebugOutput("iOS version supports Live Activities")
|
||||
} else {
|
||||
appendDebugOutput("❌ iOS version does not support Live Activities (requires 16.1+)")
|
||||
appendDebugOutput(
|
||||
"ERROR: iOS version does not support Live Activities (requires 16.1+)")
|
||||
}
|
||||
|
||||
// Check if we're on simulator
|
||||
#if targetEnvironment(simulator)
|
||||
appendDebugOutput("⚠️ Running on Simulator - Live Activities have limited functionality")
|
||||
appendDebugOutput(
|
||||
"WARNING: Running on Simulator - Live Activities have limited functionality")
|
||||
#else
|
||||
appendDebugOutput("✅ Running on device - Live Activities should work fully")
|
||||
appendDebugOutput("Running on device - Live Activities should work fully")
|
||||
#endif
|
||||
}
|
||||
|
||||
private func testLiveActivity() {
|
||||
guard !dataManager.gyms.isEmpty else {
|
||||
appendDebugOutput("❌ No gyms available for testing")
|
||||
appendDebugOutput("ERROR: No gyms available for testing")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -240,25 +242,25 @@ struct LiveActivityDebugView: View {
|
||||
appendDebugOutput("Ending Live Activity...")
|
||||
await LiveActivityManager.shared.endLiveActivity()
|
||||
|
||||
appendDebugOutput("🏁 Live Activity test completed!")
|
||||
appendDebugOutput("Live Activity test completed!")
|
||||
}
|
||||
}
|
||||
|
||||
private func endCurrentSession() {
|
||||
guard let activeSession = dataManager.activeSession else {
|
||||
appendDebugOutput("❌ No active session to end")
|
||||
appendDebugOutput("ERROR: No active session to end")
|
||||
return
|
||||
}
|
||||
|
||||
appendDebugOutput("🛑 Ending current session: \(activeSession.id)")
|
||||
appendDebugOutput("Ending current session: \(activeSession.id)")
|
||||
dataManager.endSession(activeSession.id)
|
||||
appendDebugOutput("✅ Session ended")
|
||||
appendDebugOutput("Session ended")
|
||||
}
|
||||
|
||||
private func forceLiveActivityUpdate() {
|
||||
appendDebugOutput("🔄 Forcing Live Activity update...")
|
||||
appendDebugOutput("Forcing Live Activity update...")
|
||||
dataManager.forceLiveActivityUpdate()
|
||||
appendDebugOutput("✅ Live Activity update sent")
|
||||
appendDebugOutput("Live Activity update sent")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -708,7 +708,7 @@ struct ImportDataView: View {
|
||||
Text("Import climbing data from a previously exported ZIP file.")
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("⚠️ Warning: This will replace all current data!")
|
||||
Text("WARNING: This will replace all current data!")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
255
ios/OpenClimbTests/OpenClimbTests.swift
Normal file
255
ios/OpenClimbTests/OpenClimbTests.swift
Normal file
@@ -0,0 +1,255 @@
|
||||
import XCTest
|
||||
|
||||
final class OpenClimbTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
}
|
||||
|
||||
// MARK: - Data Validation Tests
|
||||
|
||||
func testDifficultyGradeComparison() throws {
|
||||
// Test basic difficulty grade string comparison
|
||||
let grade1 = "V5"
|
||||
let grade2 = "V3"
|
||||
let grade3 = "V5"
|
||||
|
||||
XCTAssertEqual(grade1, grade3)
|
||||
XCTAssertNotEqual(grade1, grade2)
|
||||
XCTAssertFalse(grade1.isEmpty)
|
||||
}
|
||||
|
||||
func testClimbTypeValidation() throws {
|
||||
// Test climb type validation
|
||||
let validClimbTypes = ["ROPE", "BOULDER"]
|
||||
|
||||
for climbType in validClimbTypes {
|
||||
XCTAssertTrue(validClimbTypes.contains(climbType))
|
||||
XCTAssertFalse(climbType.isEmpty)
|
||||
}
|
||||
|
||||
let invalidTypes = ["", "unknown", "invalid", "sport", "trad", "toprope"]
|
||||
for invalidType in invalidTypes {
|
||||
if !invalidType.isEmpty {
|
||||
XCTAssertFalse(validClimbTypes.contains(invalidType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDateFormatting() throws {
|
||||
// Test ISO 8601 date formatting
|
||||
let formatter = ISO8601DateFormatter()
|
||||
let date = Date()
|
||||
let formattedDate = formatter.string(from: date)
|
||||
|
||||
XCTAssertFalse(formattedDate.isEmpty)
|
||||
XCTAssertTrue(formattedDate.contains("T"))
|
||||
XCTAssertTrue(formattedDate.hasSuffix("Z"))
|
||||
|
||||
// Test parsing back
|
||||
let parsedDate = formatter.date(from: formattedDate)
|
||||
XCTAssertNotNil(parsedDate)
|
||||
}
|
||||
|
||||
func testSessionDurationCalculation() throws {
|
||||
// Test session duration calculation
|
||||
let startTime = Date()
|
||||
let endTime = Date(timeInterval: 3600, since: startTime) // 1 hour later
|
||||
let duration = endTime.timeIntervalSince(startTime)
|
||||
|
||||
XCTAssertEqual(duration, 3600, accuracy: 1.0)
|
||||
XCTAssertGreaterThan(duration, 0)
|
||||
}
|
||||
|
||||
func testAttemptResultValidation() throws {
|
||||
// Test attempt result validation
|
||||
let validResults = ["completed", "failed", "flash", "project"]
|
||||
|
||||
for result in validResults {
|
||||
XCTAssertTrue(validResults.contains(result))
|
||||
XCTAssertFalse(result.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func testGymCreation() throws {
|
||||
// Test gym model creation with basic validation
|
||||
let gymName = "Test Climbing Gym"
|
||||
let location = "Test City"
|
||||
let supportedTypes = ["BOULDER", "ROPE"]
|
||||
|
||||
XCTAssertFalse(gymName.isEmpty)
|
||||
XCTAssertFalse(location.isEmpty)
|
||||
XCTAssertFalse(supportedTypes.isEmpty)
|
||||
XCTAssertEqual(supportedTypes.count, 2)
|
||||
XCTAssertTrue(supportedTypes.contains("BOULDER"))
|
||||
XCTAssertTrue(supportedTypes.contains("ROPE"))
|
||||
}
|
||||
|
||||
func testProblemValidation() throws {
|
||||
// Test problem model validation
|
||||
let problemName = "Test Problem"
|
||||
let climbType = "BOULDER"
|
||||
let difficulty = "V5"
|
||||
let tags = ["overhang", "crimpy"]
|
||||
|
||||
XCTAssertFalse(problemName.isEmpty)
|
||||
XCTAssertTrue(["BOULDER", "ROPE"].contains(climbType))
|
||||
XCTAssertFalse(difficulty.isEmpty)
|
||||
XCTAssertEqual(tags.count, 2)
|
||||
XCTAssertTrue(tags.allSatisfy { !$0.isEmpty })
|
||||
}
|
||||
|
||||
func testSessionStatusTransitions() throws {
|
||||
// Test session status transitions
|
||||
let validStatuses = ["planned", "active", "completed", "cancelled"]
|
||||
|
||||
for status in validStatuses {
|
||||
XCTAssertTrue(validStatuses.contains(status))
|
||||
XCTAssertFalse(status.isEmpty)
|
||||
}
|
||||
|
||||
// Test status transitions logic
|
||||
let initialStatus = "planned"
|
||||
let activeStatus = "active"
|
||||
let completedStatus = "completed"
|
||||
|
||||
XCTAssertNotEqual(initialStatus, activeStatus)
|
||||
XCTAssertNotEqual(activeStatus, completedStatus)
|
||||
}
|
||||
|
||||
func testUniqueIDGeneration() throws {
|
||||
// Test unique ID generation using UUID
|
||||
let id1 = UUID().uuidString
|
||||
let id2 = UUID().uuidString
|
||||
|
||||
XCTAssertNotEqual(id1, id2)
|
||||
XCTAssertFalse(id1.isEmpty)
|
||||
XCTAssertFalse(id2.isEmpty)
|
||||
XCTAssertEqual(id1.count, 36) // UUID string length
|
||||
XCTAssertTrue(id1.contains("-"))
|
||||
}
|
||||
|
||||
func testDataValidation() throws {
|
||||
// Test basic data validation patterns
|
||||
let emptyString = ""
|
||||
let validString = "test"
|
||||
let negativeNumber = -1
|
||||
let positiveNumber = 5
|
||||
let zeroNumber = 0
|
||||
|
||||
XCTAssertTrue(emptyString.isEmpty)
|
||||
XCTAssertFalse(validString.isEmpty)
|
||||
XCTAssertLessThan(negativeNumber, 0)
|
||||
XCTAssertGreaterThan(positiveNumber, 0)
|
||||
XCTAssertEqual(zeroNumber, 0)
|
||||
}
|
||||
|
||||
// MARK: - Collection Tests
|
||||
|
||||
func testArrayOperations() throws {
|
||||
// Test array operations for climb data
|
||||
var problems: [String] = []
|
||||
|
||||
XCTAssertTrue(problems.isEmpty)
|
||||
XCTAssertEqual(problems.count, 0)
|
||||
|
||||
problems.append("Problem 1")
|
||||
problems.append("Problem 2")
|
||||
|
||||
XCTAssertFalse(problems.isEmpty)
|
||||
XCTAssertEqual(problems.count, 2)
|
||||
XCTAssertTrue(problems.contains("Problem 1"))
|
||||
|
||||
let filteredProblems = problems.filter { $0.contains("1") }
|
||||
XCTAssertEqual(filteredProblems.count, 1)
|
||||
}
|
||||
|
||||
func testDictionaryOperations() throws {
|
||||
// Test dictionary operations for data storage
|
||||
var gymData: [String: Any] = [:]
|
||||
|
||||
XCTAssertTrue(gymData.isEmpty)
|
||||
|
||||
gymData["name"] = "Test Gym"
|
||||
gymData["location"] = "Test City"
|
||||
gymData["types"] = ["BOULDER", "ROPE"]
|
||||
|
||||
XCTAssertFalse(gymData.isEmpty)
|
||||
XCTAssertEqual(gymData.count, 3)
|
||||
XCTAssertNotNil(gymData["name"])
|
||||
|
||||
if let name = gymData["name"] as? String {
|
||||
XCTAssertEqual(name, "Test Gym")
|
||||
} else {
|
||||
XCTFail("Failed to cast gym name to String")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String and Numeric Tests
|
||||
|
||||
func testStringManipulation() throws {
|
||||
// Test string operations common in climb data
|
||||
let problemName = " Test Problem V5 "
|
||||
let trimmedName = problemName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let uppercaseName = trimmedName.uppercased()
|
||||
let lowercaseName = trimmedName.lowercased()
|
||||
|
||||
XCTAssertEqual(trimmedName, "Test Problem V5")
|
||||
XCTAssertEqual(uppercaseName, "TEST PROBLEM V5")
|
||||
XCTAssertEqual(lowercaseName, "test problem v5")
|
||||
|
||||
let components = trimmedName.components(separatedBy: " ")
|
||||
XCTAssertEqual(components.count, 3)
|
||||
XCTAssertEqual(components.last, "V5")
|
||||
}
|
||||
|
||||
func testNumericOperations() throws {
|
||||
// Test numeric operations for climb ratings and statistics
|
||||
let grades = [3, 5, 7, 4, 6]
|
||||
let sum = grades.reduce(0, +)
|
||||
let average = Double(sum) / Double(grades.count)
|
||||
let maxGrade = grades.max() ?? 0
|
||||
let minGrade = grades.min() ?? 0
|
||||
|
||||
XCTAssertEqual(sum, 25)
|
||||
XCTAssertEqual(average, 5.0, accuracy: 0.01)
|
||||
XCTAssertEqual(maxGrade, 7)
|
||||
XCTAssertEqual(minGrade, 3)
|
||||
}
|
||||
|
||||
// MARK: - JSON and Data Format Tests
|
||||
|
||||
func testJSONSerialization() throws {
|
||||
// Test JSON serialization for basic data structures
|
||||
let testData: [String: Any] = [
|
||||
"id": "test123",
|
||||
"name": "Test Gym",
|
||||
"active": true,
|
||||
"rating": 4.5,
|
||||
"types": ["BOULDER", "ROPE"],
|
||||
]
|
||||
|
||||
XCTAssertNoThrow({
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: testData)
|
||||
XCTAssertFalse(jsonData.isEmpty)
|
||||
|
||||
let deserializedData =
|
||||
try JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
|
||||
XCTAssertNotNil(deserializedData)
|
||||
XCTAssertEqual(deserializedData?["name"] as? String, "Test Gym")
|
||||
})
|
||||
}
|
||||
|
||||
func testDateSerialization() throws {
|
||||
// Test date serialization for API compatibility
|
||||
let date = Date()
|
||||
let formatter = ISO8601DateFormatter()
|
||||
let dateString = formatter.string(from: date)
|
||||
let parsedDate = formatter.date(from: dateString)
|
||||
|
||||
XCTAssertNotNil(parsedDate)
|
||||
XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user