diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 49adbfa0..accb2efe 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -59,12 +59,18 @@ A80D60D32D3DD85100CEF65D /* ReleaseTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */; }; A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; }; A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; }; + A81A8CB52D68B2180086C96F /* TreeMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB42D68B2180086C96F /* TreeMapTests.swift */; }; + A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; }; + A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; }; + A81A8CBB2D68B3230086C96F /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CB02D68B0320086C96F /* TreeMap.swift */; }; + A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; }; + A81A8CBE2D68B43F0086C96F /* LinkedHashMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */; }; + A81A8CC02D68B4520086C96F /* LinkedHashMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */; }; A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; }; A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; }; - A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; }; A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; }; A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; }; A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; }; @@ -650,6 +656,10 @@ A80D60D12D3D705F00CEF65D /* ReleaseTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseTrack.swift; sourceTree = ""; }; A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = ""; }; A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = ""; }; + A81A8CB02D68B0320086C96F /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = ""; }; + A81A8CB42D68B2180086C96F /* TreeMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMapTests.swift; sourceTree = ""; }; + A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMap.swift; sourceTree = ""; }; + A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedHashMapTests.swift; sourceTree = ""; }; A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = ""; }; A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = ""; }; A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = ""; }; @@ -1227,6 +1237,23 @@ path = Intents; sourceTree = ""; }; + A81A8CB22D68B2030086C96F /* UnitTests */ = { + isa = PBXGroup; + children = ( + A81A8CB32D68B20F0086C96F /* datastructures */, + ); + path = UnitTests; + sourceTree = ""; + }; + A81A8CB32D68B20F0086C96F /* datastructures */ = { + isa = PBXGroup; + children = ( + A81A8CBF2D68B4520086C96F /* LinkedHashMapTests.swift */, + A81A8CB42D68B2180086C96F /* TreeMapTests.swift */, + ); + path = datastructures; + sourceTree = ""; + }; A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = { isa = PBXGroup; children = ( @@ -1289,6 +1316,8 @@ A8AD35572D31BEB2003A28B4 /* datastructures */ = { isa = PBXGroup; children = ( + A81A8CBC2D68B43F0086C96F /* LinkedHashMap.swift */, + A81A8CB02D68B0320086C96F /* TreeMap.swift */, A868CFE32D319988002F1201 /* SingletonGenericMap.swift */, ); path = datastructures; @@ -1357,6 +1386,7 @@ A8E2DB352D6850A9009E5D31 /* Tests */ = { isa = PBXGroup; children = ( + A81A8CB22D68B2030086C96F /* UnitTests */, A8E2DB302D684E2A009E5D31 /* UITests */, A8E2DB332D68507F009E5D31 /* SideStoreTests.xctestplan */, ); @@ -2895,7 +2925,11 @@ buildActionMask = 2147483647; files = ( A8E2DB312D684E2A009E5D31 /* UITests.swift in Sources */, + A81A8CB52D68B2180086C96F /* TreeMapTests.swift in Sources */, + A81A8CBB2D68B3230086C96F /* TreeMap.swift in Sources */, A8E2DB322D684E2A009E5D31 /* UITestsLaunchTests.swift in Sources */, + A81A8CC02D68B4520086C96F /* LinkedHashMapTests.swift in Sources */, + A81A8CBE2D68B43F0086C96F /* LinkedHashMap.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3100,6 +3134,7 @@ files = ( D55467C52A8D72C300F4CE90 /* ActiveAppsWidget.swift in Sources */, D577AB7B2A967DF5007FE952 /* AppsTimelineProvider.swift in Sources */, + A81A8CB92D68B30B0086C96F /* SingletonGenericMap.swift in Sources */, D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */, BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */, D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */, @@ -3108,7 +3143,6 @@ D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */, A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */, D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */, - A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */, A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */, @@ -3142,6 +3176,7 @@ D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */, + A81A8CBD2D68B43F0086C96F /* LinkedHashMap.swift in Sources */, D5390C3C2AC3A43900D17E62 /* AddSourceViewController.swift in Sources */, D5CD805F29CA755E00E591B0 /* SourceDetailViewController.swift in Sources */, D57968CB29CB99EF00539069 /* VibrantButton.swift in Sources */, @@ -3224,6 +3259,7 @@ BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */, + A81A8CBA2D68B3110086C96F /* TreeMap.swift in Sources */, BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, diff --git a/SideStore/Tests/SideStoreTests.xctestplan b/SideStore/Tests/SideStoreTests.xctestplan index d0177c64..1b37c370 100644 --- a/SideStore/Tests/SideStoreTests.xctestplan +++ b/SideStore/Tests/SideStoreTests.xctestplan @@ -18,6 +18,8 @@ "testTargets" : [ { "skippedTests" : [ + "UITests", + "UITests\/testBulkAddRecommendedSources()", "UITests\/testLaunchPerformance()", "UITestsLaunchTests", "UITestsLaunchTests\/testLaunch()" diff --git a/SideStore/Tests/UnitTests/datastructures/LinkedHashMapTests.swift b/SideStore/Tests/UnitTests/datastructures/LinkedHashMapTests.swift new file mode 100644 index 00000000..fa2c326b --- /dev/null +++ b/SideStore/Tests/UnitTests/datastructures/LinkedHashMapTests.swift @@ -0,0 +1,206 @@ +// +// LinkedHashMapTests.swift +// SideStore +// +// Created by Magesh K on 21/02/25. +// Copyright © 2025 SideStore. All rights reserved. +// + +import XCTest + +// A helper class that signals when it is deallocated. +class LeakTester { + let id: Int + var onDeinit: (() -> Void)? + init(id: Int, onDeinit: (() -> Void)? = nil) { + self.id = id + self.onDeinit = onDeinit + } + deinit { + onDeinit?() + } +} + +final class LinkedHashMapTests: XCTestCase { + + // Test that insertion preserves order and that iteration returns items in insertion order. + func testInsertionAndOrder() { + let map = LinkedHashMap() + map.put(key: "one", value: 1) + map.put(key: "two", value: 2) + map.put(key: "three", value: 3) + + XCTAssertEqual(map.count, 3) + XCTAssertEqual(map.keys, ["one", "two", "three"], "Insertion order should be preserved") + + var iteratedKeys = [String]() + for (key, _) in map { + iteratedKeys.append(key) + } + XCTAssertEqual(iteratedKeys, ["one", "two", "three"], "Iterator should follow insertion order") + } + + // Test that updating a key does not change its order. + func testUpdateDoesNotChangeOrder() { + let map = LinkedHashMap() + map.put(key: "a", value: 1) + map.put(key: "b", value: 2) + map.put(key: "c", value: 3) + // Update key "b" + map.put(key: "b", value: 20) + XCTAssertEqual(map.get(key: "b"), 20) + + XCTAssertEqual(map.keys, ["a", "b", "c"], "Order should not change on update") + } + + // Test removal functionality and behavior when removing a non-existent key. + func testRemoval() { + let map = LinkedHashMap() + map.put(key: 1, value: "one") + map.put(key: 2, value: "two") + map.put(key: 3, value: "three") + + let removed = map.remove(key: 2) + XCTAssertEqual(removed, "two") + XCTAssertEqual(map.count, 2) + XCTAssertEqual(map.keys, [1, 3]) + + // Removing a key that doesn't exist should return nil. + let removedNil = map.remove(key: 4) + XCTAssertNil(removedNil) + } + + // Test clearing the map. + func testClear() { + let map = LinkedHashMap() + map.put(key: "x", value: 100) + map.put(key: "y", value: 200) + XCTAssertEqual(map.count, 2) + + map.clear() + XCTAssertEqual(map.count, 0) + XCTAssertTrue(map.isEmpty) + XCTAssertEqual(map.keys, []) + XCTAssertEqual(map.values, []) + } + + // Test subscript access for getting, updating, and removal. + func testSubscript() { + let map = LinkedHashMap() + map["alpha"] = 10 + XCTAssertEqual(map["alpha"], 10) + + map["alpha"] = 20 + XCTAssertEqual(map["alpha"], 20) + + // Setting a key to nil should remove the mapping. + map["alpha"] = nil + XCTAssertNil(map["alpha"]) + } + + // Test containsKey and containsValue. + func testContains() { + let map = LinkedHashMap() + map.put(key: "key1", value: 1) + map.put(key: "key2", value: 2) + + XCTAssertTrue(map.containsKey("key1")) + XCTAssertFalse(map.containsKey("key3")) + + XCTAssertTrue(map.containsValue(1)) + XCTAssertFalse(map.containsValue(99)) + } + + // Test initialization from a dictionary. + func testInitializationFromDictionary() { + // Note: Swift dictionaries preserve insertion order for literals. + let dictionary: [String: Int] = ["a": 1, "b": 2, "c": 3] + let map = LinkedHashMap(dictionary) + XCTAssertEqual(map.count, 3) + // Order may differ since Dictionary order is not strictly defined – here we verify membership. + XCTAssertEqual(Set(map.keys), Set(["a", "b", "c"])) + } + + // Revised test that iterates over the map and compares key-value pairs element by element. + func testIteration() { + let map = LinkedHashMap() + let pairs = [(1, "one"), (2, "two"), (3, "three")] + for (key, value) in pairs { + map.put(key: key, value: value) + } + + var iteratedPairs = [(Int, String)]() + for (key, value) in map { + iteratedPairs.append((key, value)) + } + + XCTAssertEqual(iteratedPairs.count, pairs.count, "Iterated count should match inserted count") + for (iter, expected) in zip(iteratedPairs, pairs) { + XCTAssertEqual(iter.0, expected.0, "Keys should match in order") + XCTAssertEqual(iter.1, expected.1, "Values should match in order") + } + } + + // Test that the values stored in the map are deallocated when the map is deallocated. + func testMemoryLeak() { + weak var weakMap: LinkedHashMap? + var deinitCalled = false + + do { + let map = LinkedHashMap() + let tester = LeakTester(id: 1) { deinitCalled = true } + map.put(key: 1, value: tester) + weakMap = map + XCTAssertNotNil(map.get(key: 1)) + } + // At this point the map (and its stored objects) should be deallocated. + XCTAssertNil(weakMap, "LinkedHashMap should be deallocated when out of scope") + XCTAssertTrue(deinitCalled, "LeakTester should be deallocated, indicating no memory leak") + } + + // Test that removal from the map correctly frees stored objects. + func testMemoryLeakOnRemoval() { + var deinitCalledForTester1 = false + var deinitCalledForTester2 = false + + let map = LinkedHashMap() + autoreleasepool { + let tester1 = LeakTester(id: 1) { deinitCalledForTester1 = true } + let tester2 = LeakTester(id: 2) { deinitCalledForTester2 = true } + map.put(key: 1, value: tester1) + map.put(key: 2, value: tester2) + + XCTAssertNotNil(map.get(key: 1)) + XCTAssertNotNil(map.get(key: 2)) + + // Remove tester1; it should be deallocated if no retain cycle exists. + _ = map.remove(key: 1) + } + // tester1 should be deallocated immediately after removal. + XCTAssertTrue(deinitCalledForTester1, "Tester1 should be deallocated after removal") + // tester2 is still in the map. + XCTAssertNotNil(map.get(key: 2)) + + // Clear the map and tester2 should be deallocated. + map.clear() + XCTAssertTrue(deinitCalledForTester2, "Tester2 should be deallocated after clearing the map") + } + + func testDefaultSubscriptExtension() { + // Create an instance of LinkedHashMap with String keys and Bool values. + let map = LinkedHashMap() + + // Verify that accessing a non-existent key returns the default value (false). + XCTAssertEqual(map["testKey", default: false], false) + + // Use the default subscript setter to assign 'true' for the key. + map["testKey", default: false] = true + XCTAssertEqual(map["testKey", default: false], true) + + // Simulate in-place toggle: read the value, toggle it, then write it back. + var current = map["testKey", default: false] + current.toggle() // now false + map["testKey", default: false] = current + XCTAssertEqual(map["testKey", default: false], false) + } +} diff --git a/SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift b/SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift new file mode 100644 index 00000000..11881d08 --- /dev/null +++ b/SideStore/Tests/UnitTests/datastructures/TreeMapTests.swift @@ -0,0 +1,154 @@ +// +// TreeMapTests.swift +// AltStore +// +// Created by Magesh K on 21/02/25. +// Copyright © 2025 SideStore. All rights reserved. +// + + +import XCTest + +class TreeMapTests: XCTestCase { + + func testInsertionAndRetrieval() { + let map = TreeMap() + XCTAssertNil(map[10]) + map[10] = "ten" + XCTAssertEqual(map[10], "ten") + + map[5] = "five" + map[15] = "fifteen" + XCTAssertEqual(map.count, 3) + XCTAssertEqual(map[5], "five") + XCTAssertEqual(map[15], "fifteen") + } + + func testUpdateValue() { + let map = TreeMap() + map[10] = "ten" + let oldValue = map.insert(key: 10, value: "TEN") + XCTAssertEqual(oldValue, "ten") + XCTAssertEqual(map[10], "TEN") + XCTAssertEqual(map.count, 1) + } + + func testDeletion() { + let map = TreeMap() + // Setup: Inserting three nodes. + map[20] = "twenty" + map[10] = "ten" + map[30] = "thirty" + + // Remove a leaf node. + let removedLeaf = map.remove(key: 10) + XCTAssertEqual(removedLeaf, "ten") + XCTAssertNil(map[10]) + XCTAssertEqual(map.count, 2) + + // Setup additional nodes to create a one-child scenario. + map[25] = "twenty-five" + map[27] = "twenty-seven" // Right child for 25. + // Remove a node with one child. + let removedOneChild = map.remove(key: 25) + XCTAssertEqual(removedOneChild, "twenty-five") + XCTAssertNil(map[25]) + XCTAssertEqual(map.count, 3) + + // Setup for a node with two children. + map[40] = "forty" + map[35] = "thirty-five" + map[45] = "forty-five" + // Remove a node with two children. + let removedTwoChildren = map.remove(key: 40) + XCTAssertEqual(removedTwoChildren, "forty") + XCTAssertNil(map[40]) + XCTAssertEqual(map.count, 5) + } + + func testDeletionOfRoot() { + let map = TreeMap() + map[50] = "fifty" + map[30] = "thirty" + map[70] = "seventy" + + // Delete the root node. + let removedRoot = map.remove(key: 50) + XCTAssertEqual(removedRoot, "fifty") + XCTAssertNil(map[50]) + // After deletion, remaining keys should be in sorted order. + XCTAssertEqual(map.keys, [30, 70]) + } + + func testSortedIteration() { + let map = TreeMap() + let keys = [20, 10, 30, 5, 15, 25, 35] + for key in keys { + map[key] = "\(key)" + } + let sortedKeys = map.keys + XCTAssertEqual(sortedKeys, keys.sorted()) + + // Verify in-order traversal. + var previous: Int? = nil + for (key, value) in map { + if let prev = previous { + XCTAssertLessThanOrEqual(prev, key) + } + previous = key + XCTAssertEqual(value, "\(key)") + } + } + + func testRemoveAll() { + let map = TreeMap() + for i in 0..<100 { + map[i] = "\(i)" + } + XCTAssertEqual(map.count, 100) + map.removeAll() + XCTAssertEqual(map.count, 0) + XCTAssertTrue(map.isEmpty) + } + + func testBalancing() { + let map = TreeMap() + // Insert elements in ascending order to challenge the balancing. + for i in 1...1000 { + map[i] = i + } + // Verify in-order traversal produces sorted order. + var expected = 1 + for (key, value) in map { + XCTAssertEqual(key, expected) + XCTAssertEqual(value, expected) + expected += 1 + } + XCTAssertEqual(expected - 1, 1000) + + // Remove odd keys to force rebalancing. + for i in stride(from: 1, through: 1000, by: 2) { + _ = map.remove(key: i) + } + let expectedEvenKeys = (1...1000).filter { $0 % 2 == 0 } + XCTAssertEqual(map.keys, expectedEvenKeys) + } + + func testNonExistentDeletion() { + let map = TreeMap() + map[10] = "ten" + let removed = map.remove(key: 20) + XCTAssertNil(removed) + XCTAssertEqual(map.count, 1) + } + + func testDuplicateInsertion() { + let map = TreeMap() + map["a"] = "first" + XCTAssertEqual(map["a"], "first") + let oldValue = map.insert(key: "a", value: "second") + XCTAssertEqual(oldValue, "first") + XCTAssertEqual(map["a"], "second") + XCTAssertEqual(map.count, 1) + } +}