|
| 1 | +import Testing |
| 2 | + |
| 3 | +@testable import GraphQL |
| 4 | + |
| 5 | +// Holds a weak reference to a Token so we can observe when ARC frees it. |
| 6 | +private final class WeakTokenBox { |
| 7 | + weak var token: Token? |
| 8 | +} |
| 9 | + |
| 10 | +@Suite struct GraphQLMemoryRetentionTests { |
| 11 | + private func makeSchema() throws -> GraphQLSchema { |
| 12 | + try GraphQLSchema( |
| 13 | + query: GraphQLObjectType( |
| 14 | + name: "Query", |
| 15 | + fields: ["hello": GraphQLField(type: GraphQLString)] |
| 16 | + ) |
| 17 | + ) |
| 18 | + } |
| 19 | + |
| 20 | + // Regression test for Token.prev being a strong reference. |
| 21 | + // |
| 22 | + // Adjacent tokens in the linked list formed a mutual retain cycle: |
| 23 | + // SOF.next → T1 and T1.prev → SOF. When the Document went out of scope, |
| 24 | + // neither token could be freed. Making prev `weak` breaks the cycle. |
| 25 | + @Test func tokenChainIsReleasedAfterDocumentGoesOutOfScope() throws { |
| 26 | + let box = WeakTokenBox() |
| 27 | + |
| 28 | + func parseAndCapture() throws { |
| 29 | + let document = try parse(source: "{ hello }") |
| 30 | + // startToken is SOF (no prev). Capture the next token, which |
| 31 | + // has a .prev back to SOF — the exact edge the fix targets. |
| 32 | + box.token = document.loc?.startToken.next |
| 33 | + #expect(box.token != nil) |
| 34 | + } |
| 35 | + |
| 36 | + try parseAndCapture() |
| 37 | + #expect(box.token == nil) |
| 38 | + } |
| 39 | + |
| 40 | + // Regression test for ValidationContext.init capturing `self` in a closure. |
| 41 | + // |
| 42 | + // The onError closure stored on ASTValidationContext captured ValidationContext |
| 43 | + // strongly, creating a cycle that prevented the context (and the Document it |
| 44 | + // holds) from ever being freed. Overriding report(error:) instead eliminates |
| 45 | + // the closure and the cycle. |
| 46 | + // |
| 47 | + // SOF (startToken) has no .prev, so it has no token-chain cycle. If it is |
| 48 | + // retained after this scope it must be because ValidationContext leaked and |
| 49 | + // is still holding a copy of the Document. |
| 50 | + @Test func validationContextReleasesDocumentAfterValidation() throws { |
| 51 | + let schema = try makeSchema() |
| 52 | + let box = WeakTokenBox() |
| 53 | + |
| 54 | + func validateAndCapture() throws { |
| 55 | + let document = try parse(source: "{ hello }") |
| 56 | + box.token = document.loc?.startToken // SOF — no prev, no token cycle |
| 57 | + let errors = validate(schema: schema, ast: document) |
| 58 | + #expect(errors.isEmpty) |
| 59 | + #expect(box.token != nil) |
| 60 | + } |
| 61 | + |
| 62 | + try validateAndCapture() |
| 63 | + #expect(box.token == nil) |
| 64 | + } |
| 65 | +} |
0 commit comments