Repository navigation
[image_picker_ios] PR 1/5 Add injectable protocol seams and migrate leaf native tests to Swift Testing - #12897
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces injectable protocol seams for camera, photo library, and PHPicker APIs in the iOS image picker implementation, and migrates several test suites from Objective-C to Swift using the Swift Testing framework. Feedback on the changes identifies a potential memory corruption risk in ImageUtilTests.swift due to an incorrect bytesPerRow configuration in a 1x1 CGContext. Additionally, the review highlights several instances where temporary files created during tests are not properly cleaned up, as well as a hardcoded temporary file path that could cause test flakiness during parallel execution.
| let context = CGContext( | ||
| data: &argb, | ||
| width: 1, | ||
| height: 1, | ||
| bitsPerComponent: cgImage.bitsPerComponent, | ||
| bytesPerRow: cgImage.bytesPerRow, | ||
| space: CGColorSpaceCreateDeviceRGB(), | ||
| bitmapInfo: cgImage.bitmapInfo.rawValue)! |
There was a problem hiding this comment.
Using cgImage.bytesPerRow for a 1x1 destination context is dangerous because cgImage.bytesPerRow is the row width of the original image (which is much larger than 4 bytes). Since the destination buffer argb is only a single UInt32 (4 bytes), specifying a large bytesPerRow can lead to memory corruption, undefined behavior, or AddressSanitizer (ASan) failures.
Additionally, using cgImage.bitsPerComponent and cgImage.bitmapInfo directly might fail if the source image is in a different format (e.g., grayscale or 16-bit).
We should initialize a standard 32-bit ARGB 1x1 context instead:
| let context = CGContext( | |
| data: &argb, | |
| width: 1, | |
| height: 1, | |
| bitsPerComponent: cgImage.bitsPerComponent, | |
| bytesPerRow: cgImage.bytesPerRow, | |
| space: CGColorSpaceCreateDeviceRGB(), | |
| bitmapInfo: cgImage.bitmapInfo.rawValue)! | |
| let context = CGContext( | |
| data: &argb, | |
| width: 1, | |
| height: 1, | |
| bitsPerComponent: 8, | |
| bytesPerRow: 4, | |
| space: CGColorSpaceCreateDeviceRGB(), | |
| bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)! |
There was a problem hiding this comment.
Good catch. The 1×1 context now uses MemoryLayout.size(ofValue: argb) so it doesn’t write past the UInt32. Left bitsPerComponent and bitmapInfo as the source image’s so the color assertions stay the same
| let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent( | ||
| "image_picker_test.jpg") |
There was a problem hiding this comment.
Using a hardcoded filename like "image_picker_test.jpg" in the shared temporary directory can lead to test flakiness or conflicts when tests are run in parallel. It is safer to use a unique filename using UUID().uuidString.
| let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent( | |
| "image_picker_test.jpg") | |
| let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent( | |
| UUID().uuidString + ".jpg") |
There was a problem hiding this comment.
Done! Unique UUID name, and the file is removed with defer
| let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage( | ||
| withOriginalImageData: dataJPG, image: imageJPG, maxWidth: nil, maxHeight: nil, | ||
| imageQuality: nil) | ||
| #expect(URL(string: savedPathJPG)?.pathExtension == "jpg") |
There was a problem hiding this comment.
The temporary files created by saveImage are not cleaned up at the end of this test. We should use defer to ensure they are removed from the disk.
| let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataJPG, image: imageJPG, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| #expect(URL(string: savedPathJPG)?.pathExtension == "jpg") | |
| let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataJPG, image: imageJPG, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| defer { try? FileManager.default.removeItem(atPath: savedPathJPG) } | |
| #expect(URL(string: savedPathJPG)?.pathExtension == "jpg") |
There was a problem hiding this comment.
Done for each saveImage path
| let savedPathPNG = FLTImagePickerPhotoAssetUtil.saveImage( | ||
| withOriginalImageData: dataPNG, image: imagePNG, maxWidth: nil, maxHeight: nil, | ||
| imageQuality: nil) | ||
| #expect(URL(string: savedPathPNG)?.pathExtension == "png") |
There was a problem hiding this comment.
The temporary file created for the PNG image is not cleaned up. We should use defer to ensure it is removed.
| let savedPathPNG = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataPNG, image: imagePNG, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| #expect(URL(string: savedPathPNG)?.pathExtension == "png") | |
| let savedPathPNG = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataPNG, image: imagePNG, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| defer { try? FileManager.default.removeItem(atPath: savedPathPNG) } | |
| #expect(URL(string: savedPathPNG)?.pathExtension == "png") |
There was a problem hiding this comment.
Done for each saveImage path
| let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage( | ||
| withPickerInfo: nil, image: imageJPG, imageQuality: nil) |
There was a problem hiding this comment.
There was a problem hiding this comment.
Done for each saveImage path
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | ||
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: nil, maxHeight: nil, | ||
| imageQuality: nil) |
There was a problem hiding this comment.
The temporary file created by saveImage is not cleaned up. We should use defer to ensure it is removed.
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: nil, maxHeight: nil, | |
| imageQuality: nil) | |
| defer { try? FileManager.default.removeItem(atPath: savedPathGIF) } |
There was a problem hiding this comment.
Done for each saveImage path
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | ||
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: 3, maxHeight: 2, imageQuality: nil) |
There was a problem hiding this comment.
The temporary file created by saveImage is not cleaned up. We should use defer to ensure it is removed.
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: 3, maxHeight: 2, imageQuality: nil) | |
| let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage( | |
| withOriginalImageData: dataGIF, image: imageGIF, maxWidth: 3, maxHeight: 2, imageQuality: nil) | |
| defer { try? FileManager.default.removeItem(atPath: savedPathGIF) } |
There was a problem hiding this comment.
Done for each saveImage path
| let path = try #require(savedPath) | ||
| #expect(FileManager.default.fileExists(atPath: path)) | ||
| #expect(URL(fileURLWithPath: path).pathExtension == expectedExtension) |
There was a problem hiding this comment.
The temporary files created during verifySavingImage are not cleaned up. We should use defer to ensure they are removed from the disk.
let path = try #require(savedPath)
defer { try? FileManager.default.removeItem(atPath: path) }
#expect(FileManager.default.fileExists(atPath: path))
#expect(URL(fileURLWithPath: path).pathExtension == expectedExtension)There was a problem hiding this comment.
Done in verifySavingImage, the GIF test, and the orientation test
| let path = try #require(savedPath) | ||
| #expect(FileManager.default.fileExists(atPath: path)) | ||
| #expect(URL(fileURLWithPath: path).pathExtension == "gif") |
There was a problem hiding this comment.
The temporary file created for the GIF image is not cleaned up. We should use defer to ensure it is removed.
| let path = try #require(savedPath) | |
| #expect(FileManager.default.fileExists(atPath: path)) | |
| #expect(URL(fileURLWithPath: path).pathExtension == "gif") | |
| let path = try #require(savedPath) | |
| defer { try? FileManager.default.removeItem(atPath: path) } | |
| #expect(FileManager.default.fileExists(atPath: path)) | |
| #expect(URL(fileURLWithPath: path).pathExtension == "gif") |
There was a problem hiding this comment.
Done in verifySavingImage, the GIF test, and the orientation test
| let path = try #require(savedPath) | ||
| #expect(FileManager.default.fileExists(atPath: path)) | ||
| #expect(URL(fileURLWithPath: path).pathExtension == "jpg") |
There was a problem hiding this comment.
The temporary file created for the oriented image is not cleaned up. We should use defer to ensure it is removed.
| let path = try #require(savedPath) | |
| #expect(FileManager.default.fileExists(atPath: path)) | |
| #expect(URL(fileURLWithPath: path).pathExtension == "jpg") | |
| let path = try #require(savedPath) | |
| defer { try? FileManager.default.removeItem(atPath: path) } | |
| #expect(FileManager.default.fileExists(atPath: path)) | |
| #expect(URL(fileURLWithPath: path).pathExtension == "jpg") |
There was a problem hiding this comment.
Done in verifySavingImage, the GIF test, and the orientation test
d3795bf to
870face
Compare
870face to
c706fe5
Compare
| /// | ||
| /// `#expect` inside `savedPathBlock` is not attributed to the test case: the | ||
| /// operation queue has no Swift Testing task-local context, so those | ||
| /// expectations cannot fail `xcodebuild`. Capture here and assert after |
There was a problem hiding this comment.
Not sure I understand the part about failing xcodebuild? You mean they are going to fail at compile time when you run xcodebuild to build the tests or at test time when you run xcodebuild test?
There was a problem hiding this comment.
Test time, not compile time. #expect inside savedPathBlock is not recorded on the test, so xcodebuild test can exit 0 even when that expectation fails. The comment now says that. Assertions stay after the operation finishes
…ive tests to Swift Testing. Seams exist so tests can fake camera and PHPicker APIs without OCMock; ImageUtil, MetaDataUtil, PhotoAssetUtil, and PHPicker save-operation tests move to Swift Testing.
…e a helper method for save operations This change introduces a new private method, `runSaveOperation`, to streamline the process of saving images and handling errors in the `PickerSaveImageToPathOperationTests`. The refactor enhances code readability and reduces duplication across multiple test cases, while maintaining existing functionality and assertions.
…cessPickerItems. NSArray generics are invariant, so xcode analyze failed with -Wincompatible-pointer-types.
…, unique temp names, and file cleanup.
…ling Refactor several test methods in `ImageUtilTests`, `MetaDataUtilTests`, `PhotoAssetUtilTests`, and `PickerSaveImageToPathOperationTests` to include `throws` for improved error handling. This change enhances the robustness of the tests by ensuring that errors are properly propagated and handled. Additionally, removed unused header from the module map.
c706fe5 to
2c4c357
Compare
| savedPath = path | ||
| savedError = error | ||
| })) | ||
| await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in |
There was a problem hiding this comment.
Nonblocking, but is it possible to do this to avoid using nonisolated(unsafe)?
let result: Result<String, FlutterError> = try #require(
await withCheckedContinuation { continuation in
let operation = FLTPHPickerSaveImageToPathOperation(
result: pickerResult,
maxHeight: maxHeight,
maxWidth: maxWidth,
desiredImageQuality: desiredImageQuality,
fullMetadata: fullMetadata,
savedPathBlock: { path, error in
if let path {
continuation.resume(returning: .success(path))
} else {
continuation.resume(returning: .failure(error!))
}
}
)
if let operation {
operation.start()
} else {
continuation.resume(returning: nil)
}
}
)There was a problem hiding this comment.
Done! savedPathBlock resumes the continuation with (path, error), so the helper no longer stores those values. I kept both optionals instead of Result: both can be nil, and error! would crash there. A nil operation resumes with nil, and #require fails the test. The block runs before setFinished:, so this also drops the completionBlock wait
|
|
||
| /// Runs a save operation and returns the `savedPathBlock` arguments. | ||
| /// | ||
| /// Swift Testing `confirmation` does not wait after its body returns, so the |
There was a problem hiding this comment.
The comments may need updating since confirmation is no longer used.
There was a problem hiding this comment.
Updated. The comment no longer mentions confirmation
…handling Updated the `runSaveOperation` method in `PickerSaveImageToPathOperationTests` to improve clarity and streamline error handling. The method now utilizes a continuation to return the saved path and error, enhancing the readability and maintainability of the test code.
…er#193578) flutter/packages@3c6ce59...0ba9a82 2026-09-30 [email protected] [image_picker_ios] PR 1/5 Add injectable protocol seams and migrate leaf native tests to Swift Testing (flutter/packages#12897) 2026-09-30 [email protected] [material_ui] Migrate M3 Dialog template to use new gen_defaults (flutter/packages#12917) 2026-09-30 [email protected] [material_ui] Migrate M3 FAB template to use new gen_defaults (flutter/packages#12921) 2026-09-30 [email protected] [material_ui] Migrate M3 DatePicker template to use new gen_defaults (flutter/packages#12914) 2026-09-29 [email protected] [material_ui] Document that TextField.decoration is merged with InputDecorationTheme (flutter/packages#12493) 2026-09-29 [email protected] [camera_android_camerax] Move misplaced Javadoc above annotations (flutter/packages#13040) 2026-09-29 [email protected] [vector_graphics] Remove unnecessary Material imports (flutter/packages#12798) 2026-09-29 [email protected] Roll Flutter from c5a061b to 55b8f88 (8 revisions) (flutter/packages#13066) 2026-09-29 [email protected] [webview_flutter_wkwebview] Regenerate with pigeon 29.0.4 (flutter/packages#12923) 2026-09-29 [email protected] [url_launcher] Convert unit tests to Kotlin (flutter/packages#13018) 2026-09-29 [email protected] [material_ui, cupertino_ui] Fix unresolved doc comment references (flutter/packages#13044) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Resolves conflicts with flutter#12897, which added injectable seams and migrated the leaf native tests to Swift Testing: - Moves this change to 0.8.13+10, above main's 0.8.13+9 entry. - Combines the nullability changes in FLTImagePickerImageUtil.h and FLTImagePickerPhotoAssetUtil.h with main's. - Ports the tests added by this change from the deleted Objective-C test files to the new Swift Testing files, and unwraps the save and convert results in main's Swift tests now that they are nullable.
Adds injectable protocol seams for camera, photo library, and PHPicker so later PRs can test without OCMock. Migrates the ImageUtil, MetaDataUtil, PhotoAssetUtil, and PHPicker save-operation tests to Swift Testing.
Plugin behavior is unchanged. Native tests capture async
savedPathBlockresults and assert afterawait, so those expectations can fail CI.Bumps
image_picker_iosto 0.8.13+8.PR 1/5 of the Obj-C → Swift migration. Part of flutter/flutter#119107
Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2