Skip to content

Commit bcebc0e

Browse files
committed
Ensure no duplicate mutants are given
1 parent 17c6727 commit bcebc0e

File tree

6 files changed

+75
-36
lines changed

6 files changed

+75
-36
lines changed

Package.resolved

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let package = Package(
1111
.executable(name: "swift-mutation-testing", targets: ["MutationTesting"]),
1212
],
1313
dependencies: [
14+
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.0"),
1415
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.6.0"),
1516
.package(url: "https://github.com/swiftlang/swift-subprocess.git", branch: "main"),
1617
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"),
@@ -31,6 +32,7 @@ let package = Package(
3132
name: "CoreMutation",
3233
dependencies: [
3334
"PackageKit",
35+
.product(name: "Algorithms", package: "swift-algorithms"),
3436
],
3537
),
3638

Sources/CoreMutation/Mutant.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,8 @@ import Foundation
22
import PackageKit
33

44
public struct Mutant {
5-
65
public let mutation: Mutation.Name
76
public let location: Source.Location
87
public let original: Source.Code
9-
private let mutate: () -> Source.Code
10-
11-
public init(
12-
mutation: Mutation.Name,
13-
location: Source.Location,
14-
original: Source.Code,
15-
mutate: @escaping () -> Source.Code
16-
) {
17-
self.mutation = mutation
18-
self.location = location
19-
self.original = original
20-
self.mutate = mutate
21-
}
22-
}
23-
24-
extension Mutant {
25-
26-
public var replacement: Source.Code {
27-
mutate()
28-
}
8+
public let replacement: Source.Code
299
}

Sources/CoreMutation/Mutation.swift

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Algorithms
12
import PackageKit
23

34
public struct Mutation: Sendable {
@@ -17,19 +18,24 @@ public struct Mutation: Sendable {
1718
extension Mutation {
1819

1920
public func mutants(for file: Source.File) -> [Mutant] {
20-
changes(file).map {
21-
Mutant(
22-
mutation: name,
23-
location: Source.Location(
24-
name: file.name,
25-
path: file.path,
26-
start: $0.start,
27-
end: $0.end,
28-
),
29-
original: file.code,
30-
mutate: $0.mutate
31-
)
32-
}
21+
22+
let mutants = changes(file)
23+
.map {
24+
Mutant(
25+
mutation: name,
26+
location: Source.Location(
27+
name: file.name,
28+
path: file.path,
29+
start: $0.start,
30+
end: $0.end,
31+
),
32+
original: file.code,
33+
replacement: $0.mutate()
34+
)
35+
}
36+
.uniqued(on: \.replacement)
37+
38+
return Array(mutants)
3339
}
3440
}
3541

Sources/PackageKit/Source.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extension Source.Path: ExpressibleByStringLiteral {
5555
// MARK: - Source.Code
5656

5757
extension Source {
58-
public struct Code: Equatable, Sendable {
58+
public struct Code: Equatable, Hashable, Sendable {
5959
fileprivate let data: Data
6060
public init(data: Data) {
6161
self.data = data

Tests/SyntaxMutationTests/SyntaxMutationTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,37 @@ struct SyntaxMutationTests {
3737
let name = "Daniel"
3838
""")
3939
}
40+
41+
@Test("duplicate mutants are removed")
42+
func duplicates() throws {
43+
44+
final class Visitor: MutationVisitor {
45+
46+
override func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
47+
record(before: node, after: ExprSyntax("""
48+
let name = "Daniel"
49+
"""))
50+
record(before: node, after: ExprSyntax("""
51+
let name = "Daniel"
52+
"""))
53+
return super.visit(node)
54+
}
55+
}
56+
57+
let mutation = Mutation(name: "Replace File", visitor: Visitor.self)
58+
let file = Source.File(name: "name", path: "path", code: "")
59+
60+
let mutants = mutation.mutants(for: file)
61+
62+
try #require(mutants.count == 1)
63+
#expect(mutants[0].original == file.code)
64+
#expect(mutants[0].mutation == mutation.name)
65+
#expect(mutants[0].location.name == file.name)
66+
#expect(mutants[0].location.path == file.path)
67+
#expect(mutants[0].location.start == Source.Position(line: 1, column: 1, offset: 0))
68+
#expect(mutants[0].location.end == Source.Position(line: 1, column: 1, offset: 0))
69+
#expect(mutants[0].replacement == """
70+
let name = "Daniel"
71+
""")
72+
}
4073
}

0 commit comments

Comments
 (0)