Skip to content

[image_picker_ios] PR 1/5 Add injectable protocol seams and migrate leaf native tests to Swift Testing - #12897

Merged
auto-submit[bot] merged 6 commits into
flutter:mainfrom
victogomez-cs:pr1/image-picker-ios-util-seams-and-save-op-tests
Sep 30, 2026
Merged

auto-submit[bot] merged 6 commits into
flutter:mainfrom
victogomez-cs:pr1/image-picker-ios-util-seams-and-save-op-tests

Conversation

@victogomez-cs

Copy link
Copy Markdown
Contributor

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 savedPathBlock results and assert after await, so those expectations can fail CI.

Bumps image_picker_ios to 0.8.13+8.

PR 1/5 of the Obj-C → Swift migration. Part of flutter/flutter#119107

Pre-Review Checklist

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-assist bot 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

  1. 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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +20 to +27
let context = CGContext(
data: &argb,
width: 1,
height: 1,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: cgImage.bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: cgImage.bitmapInfo.rawValue)!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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:

Suggested change
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)!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +43 to +44
let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
"image_picker_test.jpg")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
"image_picker_test.jpg")
let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
UUID().uuidString + ".jpg")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Unique UUID name, and the file is removed with defer

Comment on lines +55 to +58
let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage(
withOriginalImageData: dataJPG, image: imageJPG, maxWidth: nil, maxHeight: nil,
imageQuality: nil)
#expect(URL(string: savedPathJPG)?.pathExtension == "jpg")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for each saveImage path

Comment on lines +70 to +73
let savedPathPNG = FLTImagePickerPhotoAssetUtil.saveImage(
withOriginalImageData: dataPNG, image: imagePNG, maxWidth: nil, maxHeight: nil,
imageQuality: nil)
#expect(URL(string: savedPathPNG)?.pathExtension == "png")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created for the PNG image is not cleaned up. We should use defer to ensure it is removed.

Suggested change
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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for each saveImage path

Comment on lines +86 to +87
let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage(
withPickerInfo: nil, image: imageJPG, imageQuality: nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created by saveImage is not cleaned up. We should use defer to ensure it is removed.

    let savedPathJPG = FLTImagePickerPhotoAssetUtil.saveImage(
      withPickerInfo: nil, image: imageJPG, imageQuality: nil)
    defer { try? FileManager.default.removeItem(atPath: savedPathJPG) }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for each saveImage path

Comment on lines +119 to +121
let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage(
withOriginalImageData: dataGIF, image: imageGIF, maxWidth: nil, maxHeight: nil,
imageQuality: nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created by saveImage is not cleaned up. We should use defer to ensure it is removed.

Suggested change
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) }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for each saveImage path

Comment on lines +136 to +137
let savedPathGIF = FLTImagePickerPhotoAssetUtil.saveImage(
withOriginalImageData: dataGIF, image: imageGIF, maxWidth: 3, maxHeight: 2, imageQuality: nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created by saveImage is not cleaned up. We should use defer to ensure it is removed.

Suggested change
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) }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for each saveImage path

Comment on lines +80 to +82
let path = try #require(savedPath)
#expect(FileManager.default.fileExists(atPath: path))
#expect(URL(fileURLWithPath: path).pathExtension == expectedExtension)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in verifySavingImage, the GIF test, and the orientation test

Comment on lines +113 to +115
let path = try #require(savedPath)
#expect(FileManager.default.fileExists(atPath: path))
#expect(URL(fileURLWithPath: path).pathExtension == "gif")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created for the GIF image is not cleaned up. We should use defer to ensure it is removed.

Suggested change
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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in verifySavingImage, the GIF test, and the orientation test

Comment on lines +136 to +138
let path = try #require(savedPath)
#expect(FileManager.default.fileExists(atPath: path))
#expect(URL(fileURLWithPath: path).pathExtension == "jpg")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary file created for the oriented image is not cleaned up. We should use defer to ensure it is removed.

Suggested change
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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in verifySavingImage, the GIF test, and the orientation test

@victogomez-cs victogomez-cs added the triage-ios Should be looked at in iOS triage label Sep 17, 2026
@victogomez-cs
victogomez-cs force-pushed the pr1/image-picker-ios-util-seams-and-save-op-tests branch from d3795bf to 870face Compare September 17, 2026 16:51
@victogomez-cs
victogomez-cs force-pushed the pr1/image-picker-ios-util-seams-and-save-op-tests branch from 870face to c706fe5 Compare September 22, 2026 16:21

@LongCatIsLooong LongCatIsLooong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits.

///
/// `#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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
…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.
@victogomez-cs
victogomez-cs force-pushed the pr1/image-picker-ios-util-seams-and-save-op-tests branch from c706fe5 to 2c4c357 Compare September 29, 2026 19:10

@LongCatIsLooong LongCatIsLooong left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still LGTM (feel free to put on the autosubmit label if tests passes).

savedPath = path
savedError = error
}))
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
    }
  }
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments may need updating since confirmation is no longer used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@victogomez-cs victogomez-cs added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 30, 2026
@auto-submit
auto-submit Bot merged commit 0ba9a82 into flutter:main Sep 30, 2026
13 checks passed
Istiak-Ahmed78 pushed a commit to Istiak-Ahmed78/flutter that referenced this pull request Sep 30, 2026
…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
technobibi added a commit to technobibi/packages that referenced this pull request Oct 1, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: image_picker platform-ios triage-ios Should be looked at in iOS triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants