Skip to content

Commit 7dd3d27

Browse files
authored
Simplify the thunk names we generate for test functions. (#750)
Parameterized test functions with the same name but different argument types currently get the same generated names from swift-syntax, so we do some additional decorating to ensure uniqueness. This results in very long symbol names that `swift-demangle` has trouble with. This PR changes the decorating formula. Previously, we would include a copy of the test function's signature (stripped of whitespace, Unicode, and non-identifier-friendly ASCII) and, if the identifier contained non-ASCII characters, the CRC32 of the identifier for further uniqueness. This change drops the copy of the identifier name and always includes the CRC32. Collisions are only possible for fully-qualified test function names that are _identical_, and I naïvely judge the risk of those collisions to be sufficiently low that we can make this change. For `ZipTests.allElementsEqual😀(i:j:)` we currently generate a thunk named: ``` $s12TestingTests03ZipB0V0022allElementsEqual_oxFJo4TestfMp_43funcallElementsEqual__i_Int_j_Int__5bcb7b35fMu_ ``` This change would change it to: ``` $s12TestingTests03ZipB0V0022allElementsEqual_oxFJo4TestfMp_9Z5bcb7b35fMu_ ``` Which demangles to: ``` $s12TestingTests03ZipB0V0022allElementsEqual_oxFJo4TestfMp_9Z5bcb7b35fMu_ ---> unique name #1 of Z5bcb7b35 in peer macro @test expansion #1 of allElementsEqual😀 in TestingTests.ZipTests ``` The benefit of the change is that the generated names are shorter and easier to read when expanding a macro, and play better with the demangler. There may also be some benefit on Windows where the linker has a 65KB symbol name cap, although we're not exporting these symbols so probably not. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 987fed7 commit 7dd3d27

File tree

2 files changed

+16
-50
lines changed

2 files changed

+16
-50
lines changed

Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift

+9-35
Original file line numberDiff line numberDiff line change
@@ -62,44 +62,18 @@ extension MacroExpansionContext {
6262
.tokens(viewMode: .fixedUp)
6363
.map(\.textWithoutBackticks)
6464
.joined()
65+
let crcValue = crc32(identifierCharacters.utf8)
66+
let suffix = String(crcValue, radix: 16, uppercase: false)
6567

66-
// Strip out any characters in the function's signature that won't play well
67-
// in a generated symbol name.
68-
let identifier = String(
69-
identifierCharacters.map { character in
70-
if character.isLetter || character.isWholeNumber {
71-
return character
72-
}
73-
return "_"
74-
}
75-
)
76-
77-
// If there is a non-ASCII character in the identifier, we might be
78-
// stripping it out above because we are only looking for letters and
79-
// digits. If so, add in a hash of the identifier to improve entropy and
80-
// reduce the risk of a collision.
81-
//
82-
// For example, the following function names will produce identical unique
83-
// names without this mutation:
84-
//
85-
// @Test(arguments: [0]) func A(🙃: Int) {}
86-
// @Test(arguments: [0]) func A(🙂: Int) {}
87-
//
88-
// Note the check here is not the same as the one above: punctuation like
89-
// "(" should be replaced, but should not cause a hash to be emitted since
90-
// it does not contribute any entropy to the makeUniqueName() algorithm.
91-
//
92-
// The intent here is not to produce a cryptographically strong hash, but to
93-
// disambiguate between superficially similar function names. A collision
94-
// may still occur, but we only need it to be _unlikely_. CRC-32 is good
95-
// enough for our purposes.
96-
if !identifierCharacters.allSatisfy(\.isASCII) {
97-
let crcValue = crc32(identifierCharacters.utf8)
98-
let suffix = String(crcValue, radix: 16, uppercase: false)
99-
return makeUniqueName("\(prefix)\(identifier)_\(suffix)")
68+
// If the caller did not specify a prefix and the CRC32 value starts with a
69+
// digit, include a single-character prefix to ensure that Swift's name
70+
// demangling still works correctly.
71+
var prefix = prefix
72+
if prefix.isEmpty, let firstSuffixCharacter = suffix.first, firstSuffixCharacter.isWholeNumber {
73+
prefix = "Z"
10074
}
10175

102-
return makeUniqueName("\(prefix)\(identifier)")
76+
return makeUniqueName("\(prefix)\(suffix)")
10377
}
10478
}
10579

Tests/TestingMacrosTests/UniqueIdentifierTests.swift

+7-15
Original file line numberDiff line numberDiff line change
@@ -27,50 +27,35 @@ struct UniqueIdentifierTests {
2727
return BasicMacroExpansionContext().makeUniqueName(thunking: functionDecl).text
2828
}
2929

30-
@Test("Thunk identifiers contain a function's name")
31-
func thunkNameContainsFunctionName() throws {
32-
let uniqueName = try makeUniqueName("func someDistinctFunctionName() async throws")
33-
#expect(uniqueName.contains("someDistinctFunctionName"))
34-
}
35-
3630
@Test("Thunk identifiers do not contain backticks")
3731
func noBackticks() throws {
3832
let uniqueName = try makeUniqueName("func `someDistinctFunctionName`() async throws")
39-
#expect(uniqueName.contains("someDistinctFunctionName"))
4033

4134
#expect(!uniqueName.contains("`"))
4235
}
4336

4437
@Test("Thunk identifiers do not contain arbitrary Unicode")
4538
func noArbitraryUnicode() throws {
4639
let uniqueName = try makeUniqueName("func someDistinctFunction🌮Name🐔() async throws")
47-
#expect(uniqueName.contains("someDistinctFunction"))
4840

4941
#expect(!uniqueName.contains("🌮"))
5042
#expect(!uniqueName.contains("🐔"))
51-
#expect(uniqueName.contains("Name"))
5243
}
5344

5445
@Test("Argument types influence generated identifiers")
5546
func argumentTypes() throws {
5647
let uniqueNameWithInt = try makeUniqueName("func someDistinctFunctionName(i: Int) async throws")
57-
#expect(uniqueNameWithInt.contains("someDistinctFunctionName"))
5848
let uniqueNameWithUInt = try makeUniqueName("func someDistinctFunctionName(i: UInt) async throws")
59-
#expect(uniqueNameWithUInt.contains("someDistinctFunctionName"))
6049

6150
#expect(uniqueNameWithInt != uniqueNameWithUInt)
6251
}
6352

6453
@Test("Effects influence generated identifiers")
6554
func effects() throws {
6655
let uniqueName = try makeUniqueName("func someDistinctFunctionName()")
67-
#expect(uniqueName.contains("someDistinctFunctionName"))
6856
let uniqueNameAsync = try makeUniqueName("func someDistinctFunctionName() async")
69-
#expect(uniqueNameAsync.contains("someDistinctFunctionName"))
7057
let uniqueNameThrows = try makeUniqueName("func someDistinctFunctionName() throws")
71-
#expect(uniqueNameThrows.contains("someDistinctFunctionName"))
7258
let uniqueNameAsyncThrows = try makeUniqueName("func someDistinctFunctionName() async throws")
73-
#expect(uniqueNameAsyncThrows.contains("someDistinctFunctionName"))
7459

7560
#expect(uniqueName != uniqueNameAsync)
7661
#expect(uniqueName != uniqueNameThrows)
@@ -89,4 +74,11 @@ struct UniqueIdentifierTests {
8974
#expect(uniqueName1 != uniqueName3)
9075
#expect(uniqueName2 != uniqueName3)
9176
}
77+
78+
@Test("Body does not influence generated identifiers")
79+
func body() throws {
80+
let uniqueName1 = try makeUniqueName("func f() { abc() }")
81+
let uniqueName2 = try makeUniqueName("func f() { def() }")
82+
#expect(uniqueName1 == uniqueName2)
83+
}
9284
}

0 commit comments

Comments
 (0)