Skip to content

Commit ce7126b

Browse files
committed
IOS-6729: Fix loadPhotos data race crash by protecting mutable states
1 parent 86bc6ba commit ce7126b

File tree

6 files changed

+93
-209
lines changed

6 files changed

+93
-209
lines changed

MEGAUnitTests/Mocks/Sdk/MockNode.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,22 @@ final class MockNode: MEGANode {
99
private let nodeHandle: HandleEntity
1010
private let changeType: MEGANodeChangeType
1111
private var nodeModificationTime: Date?
12+
private let _hasThumbnail: Bool
1213

1314
init(handle: HandleEntity,
1415
name: String = "",
1516
nodeType: MEGANodeType = .file,
1617
parentHandle: HandleEntity = .invalid,
1718
changeType: MEGANodeChangeType = .new,
18-
modificationTime: Date? = nil ) {
19+
modificationTime: Date? = nil,
20+
hasThumbnail: Bool = false) {
1921
nodeHandle = handle
2022
nodeName = name
2123
self.nodeType = nodeType
2224
nodeParentHandle = parentHandle
2325
self.changeType = changeType
2426
nodeModificationTime = modificationTime
27+
_hasThumbnail = hasThumbnail
2528
super.init()
2629
}
2730

@@ -44,4 +47,6 @@ final class MockNode: MEGANode {
4447
override var parentHandle: HandleEntity { nodeParentHandle }
4548

4649
override var modificationTime: Date? { nodeModificationTime }
50+
51+
override func hasThumbnail() -> Bool { _hasThumbnail }
4752
}

MEGAUnitTests/Photos/PhotosViewModelTests.swift

+55-156
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import MEGADomainMock
33
@testable import MEGA
44
@testable import MEGADomain
55

6+
@MainActor
67
final class PhotosViewModelTests: XCTestCase {
7-
var photosViewModel: PhotosViewModel?
8+
var sut: PhotosViewModel!
89

910
override func setUp() {
1011
let publisher = PhotoUpdatePublisher(photosViewController: PhotosViewController())
@@ -15,34 +16,10 @@ final class PhotosViewModelTests: XCTestCase {
1516
allPhotosFromCloudDriveOnly: allPhotosForCloudDrive,
1617
allPhotosFromCameraUpload: allPhotosForCameraUploads)
1718

18-
photosViewModel = PhotosViewModel(photoUpdatePublisher: publisher, photoLibraryUseCase: usecase, mediaUseCase: MockMediaUseCase())
19-
}
20-
21-
private func mediaNodesSorted() -> [MEGANode] {
22-
let today = Date()
23-
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)
24-
25-
return [
26-
MockNode(handle: 1, name: "TestImage.png", nodeType: .file, parentHandle:0, modificationTime: today),
27-
MockNode(handle: 2, name: "TestVideo1.mp4", nodeType: .file, parentHandle:0),
28-
MockNode(handle: 2, name: "TestImage2.jpg", nodeType: .file, parentHandle:1, modificationTime: yesterday)
29-
]
30-
}
31-
32-
private func mediaNodesReverseSorted() -> [MEGANode] {
33-
let today = Date()
34-
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: today)
35-
36-
return [
37-
MockNode(handle: 2, name: "TestImage2.jpg", nodeType: .file, parentHandle:1, modificationTime: yesterday),
38-
MockNode(handle: 1, name: "TestImage.png", nodeType: .file, parentHandle:0, modificationTime: today),
39-
MockNode(handle: 2, name: "TestVideo1.mp4", nodeType: .file, parentHandle:0)
40-
]
19+
sut = PhotosViewModel(photoUpdatePublisher: publisher, photoLibraryUseCase: usecase, mediaUseCase: MockMediaUseCase())
4120
}
4221

4322
func testLoadSortOrderType() throws {
44-
guard let sut = photosViewModel else { return }
45-
4623
Helper.save(.defaultAsc, for: PhotosViewModel.SortingKeys.cameraUploadExplorerFeed.rawValue)
4724
let sortTypeUnknown = sut.sortOrderType(forKey: .cameraUploadExplorerFeed)
4825
XCTAssert(sortTypeUnknown == .newest)
@@ -56,189 +33,111 @@ final class PhotosViewModelTests: XCTestCase {
5633
XCTAssert(sortTypeNewest == .newest)
5734
}
5835

59-
func testReorderPhotos() throws {
60-
guard let sut = photosViewModel else { return }
61-
62-
sut.mediaNodesArray = mediaNodesSorted()
63-
64-
sut.cameraUploadExplorerSortOrderType = .nameAscending
65-
XCTAssert(sut.mediaNodesArray == mediaNodesSorted())
66-
67-
sut.cameraUploadExplorerSortOrderType = .newest
68-
XCTAssert(sut.mediaNodesArray == mediaNodesSorted())
69-
70-
sut.cameraUploadExplorerSortOrderType = .oldest
71-
XCTAssert(sut.mediaNodesArray == mediaNodesReverseSorted())
72-
}
73-
7436
// MARK: - All locations test cases
7537

7638
func testLoadingPhotos_withAllMediaAllLocations_shouldReturnTrue() async throws {
77-
guard let photoVM = photosViewModel else { return }
78-
7939
let expectedPhotos = sampleNodesForAllLocations()
80-
photoVM.filterType = .allMedia
81-
photoVM.filterLocation = . allLocations
82-
83-
do {
84-
let photos = try await photoVM.loadFilteredPhotos()
85-
XCTAssertTrue(photos.count == expectedPhotos.count)
86-
} catch {
87-
assertionFailure("Unexpected exception!")
88-
}
40+
sut.filterType = .allMedia
41+
sut.filterLocation = . allLocations
42+
await sut.loadPhotos()
43+
XCTAssertEqual(sut.mediaNodesArray, expectedPhotos)
8944
}
9045

9146
func testLoadingPhotos_withImagesAllLocations_shouldReturnTrue() async throws {
92-
guard let photoVM = photosViewModel else { return }
93-
9447
let expectedImages = sampleNodesForAllLocations().filter({ $0.name?.mnz_isImagePathExtension == true })
95-
photoVM.filterType = .images
96-
photoVM.filterLocation = . allLocations
97-
98-
do {
99-
let photos = try await photoVM.loadFilteredPhotos()
100-
XCTAssertTrue(photos.count == expectedImages.count)
101-
} catch {
102-
assertionFailure("Unexpected exception!")
103-
}
48+
sut.filterType = .images
49+
sut.filterLocation = . allLocations
50+
await sut.loadPhotos()
51+
XCTAssertEqual(sut.mediaNodesArray, expectedImages)
10452
}
10553

10654
func testLoadingVideos_withImagesAllLocations_shouldReturnTrue() async throws {
107-
guard let photoVM = photosViewModel else { return }
108-
10955
let expectedVideos = sampleNodesForAllLocations().filter({ $0.name?.mnz_isVideoPathExtension == true })
110-
photoVM.filterType = .videos
111-
photoVM.filterLocation = . allLocations
112-
113-
do {
114-
let photos = try await photoVM.loadFilteredPhotos()
115-
XCTAssertTrue(photos.count == expectedVideos.count)
116-
} catch {
117-
assertionFailure("Unexpected exception!")
118-
}
56+
sut.filterType = .videos
57+
sut.filterLocation = . allLocations
58+
await sut.loadPhotos()
59+
XCTAssertEqual(sut.mediaNodesArray, expectedVideos)
11960
}
12061

12162
// MARK: - Cloud Drive only test cases
12263

12364
func testLoadingPhotos_withAllMediaFromCloudDrive_shouldReturnTrue() async throws {
124-
guard let photoVM = photosViewModel else { return }
125-
12665
let expectedPhotos = sampleNodesForCloudDriveOnly()
127-
photoVM.filterType = .allMedia
128-
photoVM.filterLocation = .cloudDrive
129-
130-
do {
131-
let photos = try await photoVM.loadFilteredPhotos()
132-
XCTAssertTrue(photos.count == expectedPhotos.count)
133-
} catch {
134-
assertionFailure("Unexpected exception!")
135-
}
66+
sut.filterType = .allMedia
67+
sut.filterLocation = .cloudDrive
68+
await sut.loadPhotos()
69+
XCTAssertEqual(sut.mediaNodesArray, expectedPhotos)
13670
}
13771

13872
func testLoadingPhotos_withImagesFromCloudDrive_shouldReturnTrue() async throws {
139-
guard let photoVM = photosViewModel else { return }
140-
14173
let expectedImages = sampleNodesForCloudDriveOnly().filter({ $0.name?.mnz_isImagePathExtension == true })
142-
photoVM.filterType = .images
143-
photoVM.filterLocation = .cloudDrive
144-
145-
do {
146-
let photos = try await photoVM.loadFilteredPhotos()
147-
XCTAssertTrue(photos.count == expectedImages.count)
148-
} catch {
149-
assertionFailure("Unexpected exception!")
150-
}
74+
sut.filterType = .images
75+
sut.filterLocation = .cloudDrive
76+
await sut.loadPhotos()
77+
XCTAssertEqual(sut.mediaNodesArray, expectedImages)
15178
}
15279

15380
func testLoadingPhotos_withVideosFromCloudDrive_shouldReturnTrue() async throws {
154-
guard let photoVM = photosViewModel else { return }
155-
15681
let expectedVideos = sampleNodesForCloudDriveOnly().filter({ $0.name?.mnz_isVideoPathExtension == true })
157-
photoVM.filterType = .videos
158-
photoVM.filterLocation = .cloudDrive
159-
160-
do {
161-
let photos = try await photoVM.loadFilteredPhotos()
162-
XCTAssertTrue(photos.count == expectedVideos.count)
163-
} catch {
164-
assertionFailure("Unexpected exception!")
165-
}
82+
sut.filterType = .videos
83+
sut.filterLocation = .cloudDrive
84+
await sut.loadPhotos()
85+
XCTAssertEqual(sut.mediaNodesArray, expectedVideos)
16686
}
16787

16888
// MARK: - Camera Uploads test cases
16989

17090
func testLoadingPhotos_withAllMediaFromCameraUploads_shouldReturnTrue() async throws {
171-
guard let photoVM = photosViewModel else { return }
172-
17391
let expectedPhotos = sampleNodesForCameraUploads()
174-
photoVM.filterType = .allMedia
175-
photoVM.filterLocation = .cameraUploads
176-
177-
do {
178-
let photos = try await photoVM.loadFilteredPhotos()
179-
XCTAssertTrue(photos.count == expectedPhotos.count)
180-
} catch {
181-
assertionFailure("Unexpected exception!")
182-
}
92+
sut.filterType = .allMedia
93+
sut.filterLocation = .cameraUploads
94+
await sut.loadPhotos()
95+
XCTAssertEqual(sut.mediaNodesArray, expectedPhotos)
18396
}
18497

18598
func testLoadingPhotos_withImagesFromCameraUploads_shouldReturnTrue() async throws {
186-
guard let photoVM = photosViewModel else { return }
187-
18899
let expectedImages = sampleNodesForCameraUploads().filter({ $0.name?.mnz_isImagePathExtension == true })
189-
photoVM.filterType = .images
190-
photoVM.filterLocation = .cameraUploads
191-
192-
do {
193-
let photos = try await photoVM.loadFilteredPhotos()
194-
XCTAssertTrue(photos.count == expectedImages.count)
195-
} catch {
196-
assertionFailure("Unexpected exception!")
197-
}
100+
sut.filterType = .images
101+
sut.filterLocation = .cameraUploads
102+
await sut.loadPhotos()
103+
XCTAssertEqual(sut.mediaNodesArray, expectedImages)
198104
}
199105

200106
func testLoadingPhotos_withVideosFromCameraUploads_shouldReturnTrue() async throws {
201-
guard let photoVM = photosViewModel else { return }
202-
203107
let expectedVideos = sampleNodesForCameraUploads().filter({ $0.name?.mnz_isVideoPathExtension == true })
204-
photoVM.filterType = .videos
205-
photoVM.filterLocation = .cameraUploads
206-
207-
do {
208-
let photos = try await photoVM.loadFilteredPhotos()
209-
XCTAssertTrue(photos.count == expectedVideos.count)
210-
} catch {
211-
assertionFailure("Unexpected exception!")
212-
}
108+
sut.filterType = .videos
109+
sut.filterLocation = .cameraUploads
110+
await sut.loadPhotos()
111+
XCTAssertEqual(sut.mediaNodesArray, expectedVideos)
213112
}
214113

215114
private func sampleNodesForAllLocations() ->[MEGANode] {
216-
let node1 = MockNode(handle: 1, name: "TestImage1.png", nodeType: .file, parentHandle: 0)
217-
let node2 = MockNode(handle: 2, name: "TestImage2.png", nodeType: .file, parentHandle: 1)
218-
let node3 = MockNode(handle: 3, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 2)
219-
let node4 = MockNode(handle: 4, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 3)
220-
let node5 = MockNode(handle: 5, name: "TestImage1.png", nodeType: .file, parentHandle: 4)
221-
let node6 = MockNode(handle: 6, name: "TestImage2.png", nodeType: .file, parentHandle: 5)
222-
let node7 = MockNode(handle: 7, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 6)
223-
let node8 = MockNode(handle: 8, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 7)
115+
let node1 = MockNode(handle: 1, name: "TestImage1.png", nodeType: .file, parentHandle: 0, hasThumbnail: true)
116+
let node2 = MockNode(handle: 2, name: "TestImage2.png", nodeType: .file, parentHandle: 1, hasThumbnail: true)
117+
let node3 = MockNode(handle: 3, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 2, hasThumbnail: true)
118+
let node4 = MockNode(handle: 4, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 3, hasThumbnail: true)
119+
let node5 = MockNode(handle: 5, name: "TestImage1.png", nodeType: .file, parentHandle: 4, hasThumbnail: true)
120+
let node6 = MockNode(handle: 6, name: "TestImage2.png", nodeType: .file, parentHandle: 5, hasThumbnail: true)
121+
let node7 = MockNode(handle: 7, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 6, hasThumbnail: true)
122+
let node8 = MockNode(handle: 8, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 7, hasThumbnail: true)
224123

225124
return [node1, node2, node3, node4, node5, node6, node7, node8]
226125
}
227126

228127
private func sampleNodesForCloudDriveOnly() ->[MEGANode] {
229-
let node1 = MockNode(handle: 1, name: "TestImage1.png", nodeType: .file, parentHandle: 0)
230-
let node2 = MockNode(handle: 2, name: "TestImage2.png", nodeType: .file, parentHandle: 1)
231-
let node3 = MockNode(handle: 3, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 2)
232-
let node4 = MockNode(handle: 4, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 3)
128+
let node1 = MockNode(handle: 1, name: "TestImage1.png", nodeType: .file, parentHandle: 0, hasThumbnail: true)
129+
let node2 = MockNode(handle: 2, name: "TestImage2.png", nodeType: .file, parentHandle: 1, hasThumbnail: true)
130+
let node3 = MockNode(handle: 3, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 2, hasThumbnail: true)
131+
let node4 = MockNode(handle: 4, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 3, hasThumbnail: true)
233132

234133
return [node1, node2, node3, node4]
235134
}
236135

237136
private func sampleNodesForCameraUploads() ->[MEGANode] {
238-
let node5 = MockNode(handle: 5, name: "TestImage1.png", nodeType: .file, parentHandle: 4)
239-
let node6 = MockNode(handle: 6, name: "TestImage2.png", nodeType: .file, parentHandle: 5)
240-
let node7 = MockNode(handle: 7, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 6)
241-
let node8 = MockNode(handle: 8, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 7)
137+
let node5 = MockNode(handle: 5, name: "TestImage1.png", nodeType: .file, parentHandle: 4, hasThumbnail: true)
138+
let node6 = MockNode(handle: 6, name: "TestImage2.png", nodeType: .file, parentHandle: 5, hasThumbnail: true)
139+
let node7 = MockNode(handle: 7, name: "TestVideo1.mp4", nodeType: .file, parentHandle: 6, hasThumbnail: true)
140+
let node8 = MockNode(handle: 8, name: "TestVideo2.mp4", nodeType: .file, parentHandle: 7, hasThumbnail: true)
242141

243142
return [node5, node6, node7, node8]
244143
}

iMEGA/Camera uploads/Extension/PhotosViewController+PhotoLibrary.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ extension PhotosViewController: PhotoLibraryProvider {
1010
}
1111
}
1212

13-
@objc func objcWrapper_updatePhotoLibrary(by nodes: [MEGANode]) {
14-
let sortType = viewModel.cameraUploadExplorerSortOrderType ?? .newest
15-
updatePhotoLibrary(by: nodes, withSortType: sortType)
13+
@objc func objcWrapper_updatePhotoLibrary() {
14+
updatePhotoLibrary(by: viewModel.mediaNodesArray, withSortType: viewModel.cameraUploadExplorerSortOrderType)
1615
}
1716

1817
@objc func createPhotoLibraryContentViewModel() -> PhotoLibraryContentViewModel {

iMEGA/Camera uploads/PhotosViewController.m

+1-2
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,7 @@ - (void)reloadPhotos {
347347
[self reloadHeader];
348348
[self setupNavigationBarButtons];
349349
dispatch_async(dispatch_get_main_queue(), ^{
350-
[self objcWrapper_updatePhotoLibraryBy: self.viewModel.mediaNodesArray];
351-
350+
[self objcWrapper_updatePhotoLibrary];
352351
if (self.viewModel.mediaNodesArray.count == 0) {
353352
[self.photosCollectionView reloadEmptyDataSet];
354353
}

iMEGA/Camera uploads/ViewModel/PhotosViewModel+FilterAndSort.swift

+2-14
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,15 @@ extension PhotosViewModel {
99
return sortType != .newest && sortType != .oldest ? .newest : sortType
1010
}
1111

12-
func reorderPhotos(_ sortType: SortOrderType?, mediaNodes: [MEGANode]) -> [MEGANode] {
13-
guard let sortType = sortType,
14-
sortType == .newest || sortType == .oldest else { return mediaNodes }
15-
16-
return mediaNodes.sorted { node1, node2 in
17-
guard let date1 = node1.modificationTime,
18-
let date2 = node2.modificationTime else { return node1.name ?? "" < node2.name ?? "" }
19-
20-
return sortType == .newest ? date1 > date2 : date1 < date2
21-
}
22-
}
23-
2412
// MARK: - Filter
2513

2614
func filter(nodes: inout [MEGANode], with type: PhotosFilterOptions) {
2715
guard type != .allMedia else { return }
2816

2917
if type == .images {
30-
nodes = nodes.filter({ $0.name?.mnz_isImagePathExtension == true })
18+
nodes = nodes.filter({ $0.name?.mnz_isImagePathExtension == true && $0.hasThumbnail() })
3119
} else {
32-
nodes = nodes.filter({ $0.name?.mnz_isVideoPathExtension == true })
20+
nodes = nodes.filter({ $0.name?.mnz_isVideoPathExtension == true && $0.hasThumbnail() })
3321
}
3422
}
3523
}

0 commit comments

Comments
 (0)