Skip to content

Commit fdd5052

Browse files
Fix retain cycles in AnnouncementsDataStoreTests and GutenbergVideoUploadProcessor (#25548)
* Fix retain cycle in AnnouncementsDataStoreTests Each of the four tests stores `subscription = store.onChange { … store.x … }` in an instance property. The closure captures the local `store` strongly, which is then retained by `CachedAnnouncementsStore.changeDispatcher`'s observers dictionary, forming the cycle: store → changeDispatcher → observers[token] → closure → store Without a `tearDown`, the `Receipt` only deinits when XCTest tears down the test instance, which it defers when more tests are queued in the class — so the cycle is observable by the end-of-test leak snapshot. Adding `tearDown` to nil out `subscription` triggers the `Receipt.deinit` unsubscribe path immediately and breaks the cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix retain cycle in GutenbergVideoUploadProcessor The three `lazy var` properties (`videoHtmlProcessor`, `videoBlockProcessor`, `mediaTextVideoBlockProcessor`) each construct a child processor whose `replacer:` closure references `self` without a capture list. The child processor is owned strongly by `self`, and the closure retained inside it captures `self` strongly — producing three independent 2-node cycles: GutenbergVideoUploadProcessor → videoHtmlProcessor (HTMLProcessor) → replacer.context → captures self → GutenbergVideoUploadProcessor (cycle) Switching each closure to `[weak self]` (with a `guard let self` early-out) breaks the back-edge. By ownership, the closure is only invoked while `self` is still alive, so the guard never trips in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 92fd2b4 commit fdd5052

2 files changed

Lines changed: 13 additions & 5 deletions

File tree

Tests/KeystoneTests/Tests/Features/Misc/WhatsNew/AnnouncementsDataStoreTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ class AnnouncementsDataStoreTests: XCTestCase {
7676

7777
private var subscription: Receipt?
7878

79+
override func tearDown() {
80+
subscription = nil
81+
super.tearDown()
82+
}
83+
7984
/// local cache contains valid announcements
8085
func testLocalAnnouncementsRetrieved() {
8186
// Given

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergVideoUploadProcessor.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class GutenbergVideoUploadProcessor: Processor {
1313
self.remoteURLString = remoteURLString
1414
}
1515

16-
lazy var videoHtmlProcessor = HTMLProcessor(for: "video", replacer: { (video) in
16+
lazy var videoHtmlProcessor = HTMLProcessor(for: "video", replacer: { [weak self] (video) in
17+
guard let self else { return "" }
1718
var attributes = video.attributes
1819

1920
attributes.set(.string(self.remoteURLString), forKey: "src")
@@ -30,8 +31,9 @@ class GutenbergVideoUploadProcessor: Processor {
3031
static var id = "id"
3132
}
3233

33-
lazy var videoBlockProcessor = GutenbergBlockProcessor(for: VideoBlockKeys.name, replacer: { videoBlock in
34-
guard let mediaID = videoBlock.attributes[VideoBlockKeys.id] as? Int,
34+
lazy var videoBlockProcessor = GutenbergBlockProcessor(for: VideoBlockKeys.name, replacer: { [weak self] videoBlock in
35+
guard let self,
36+
let mediaID = videoBlock.attributes[VideoBlockKeys.id] as? Int,
3537
mediaID == self.mediaUploadID else {
3638
return nil
3739
}
@@ -53,8 +55,9 @@ class GutenbergVideoUploadProcessor: Processor {
5355
static var id = "mediaId"
5456
}
5557

56-
lazy var mediaTextVideoBlockProcessor = GutenbergBlockProcessor(for: MediaTextBlockKeys.name, replacer: { videoBlock in
57-
guard let mediaID = videoBlock.attributes[MediaTextBlockKeys.id] as? Int,
58+
lazy var mediaTextVideoBlockProcessor = GutenbergBlockProcessor(for: MediaTextBlockKeys.name, replacer: { [weak self] videoBlock in
59+
guard let self,
60+
let mediaID = videoBlock.attributes[MediaTextBlockKeys.id] as? Int,
5861
mediaID == self.mediaUploadID else {
5962
return nil
6063
}

0 commit comments

Comments
 (0)