Skip to content

Commit 0d3b411

Browse files
committed
Inject test bundle, add LCOV filter, and test updates
Add an LCOV filtering script and wire it into the CI to strip ignored lines from coverage reports. Include test resources in Package.swift and add a test JSON resource. Introduce a dataBundle property/parameter on SwiftNEW and switch local JSON loading to use the injected bundle so tests can provide .module resources. Add coverage:ignore markers around OS-specific fallbacks to avoid false negatives in Xcode 26 coverage. Refactor several view actions (search toggle, retry load, present release notes) into testable helper methods and expose a testingHistorySheetContent accessor. Add new unit tests to exercise bundle icon lookup, version presentation path, search/retry flows, and local bundle JSON loading.
1 parent 0b683bf commit 0d3b411

11 files changed

Lines changed: 206 additions & 33 deletions

File tree

.github/scripts/filter_lcov.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
input_path = ARGV.fetch(0)
4+
output_path = ARGV.fetch(1)
5+
6+
def ignored_lines_for(source_path)
7+
return [] unless File.file?(source_path)
8+
9+
ignored = []
10+
active = false
11+
12+
File.readlines(source_path).each_with_index do |line, index|
13+
line_number = index + 1
14+
15+
if line.include?("coverage:ignore-line")
16+
ignored << line_number
17+
next
18+
end
19+
20+
if line.include?("coverage:ignore-start")
21+
active = true
22+
ignored << line_number
23+
next
24+
end
25+
26+
ignored << line_number if active
27+
28+
active = false if line.include?("coverage:ignore-end")
29+
end
30+
31+
ignored
32+
end
33+
34+
def flush_record(output, record, ignored_lines)
35+
covered = 0
36+
found = 0
37+
38+
filtered = []
39+
40+
record.each do |line|
41+
if line.start_with?("DA:")
42+
line_number, hit_count = line.delete_prefix("DA:").split(",", 2)
43+
next if ignored_lines.include?(line_number.to_i)
44+
45+
found += 1
46+
covered += 1 if hit_count.to_i.positive?
47+
filtered << line
48+
elsif line.start_with?("LF:") || line.start_with?("LH:")
49+
next
50+
else
51+
filtered << line
52+
end
53+
end
54+
55+
filtered.insert(-2, "LF:#{found}\n", "LH:#{covered}\n") if filtered.last == "end_of_record\n"
56+
output.concat(filtered)
57+
end
58+
59+
source_file = nil
60+
ignored_lines = []
61+
record = []
62+
output = []
63+
64+
File.readlines(input_path).each do |line|
65+
if line.start_with?("SF:")
66+
flush_record(output, record, ignored_lines) unless record.empty?
67+
source_file = line.delete_prefix("SF:").strip
68+
ignored_lines = ignored_lines_for(source_file)
69+
record = [line]
70+
else
71+
record << line
72+
end
73+
end
74+
75+
flush_record(output, record, ignored_lines) unless record.empty?
76+
77+
File.write(output_path, output.join)

.github/workflows/swift.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ jobs:
3838
-instr-profile "$CODECOV_DIR/default.profdata" \
3939
-format=lcov \
4040
-ignore-filename-regex='/.build/|/Tests/' \
41-
| sed "s#SF:$PWD/#SF:#" > coverage.lcov
41+
| sed "s#SF:$PWD/#SF:#" > coverage.raw.lcov
42+
43+
ruby .github/scripts/filter_lcov.rb coverage.raw.lcov coverage.lcov
4244
4345
echo "path=coverage.lcov" >> "$GITHUB_OUTPUT"
4446
- name: Upload coverage to Codecov

Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ let package = Package(
3939
),
4040
.testTarget(
4141
name: "SwiftNEWTests",
42-
dependencies: ["SwiftNEW"]
42+
dependencies: ["SwiftNEW"],
43+
resources: [
44+
.process("Resources")
45+
]
4346
)
4447
]
4548
)

Sources/SwiftNEW/Extensions/SwiftNEW+Functions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ extension SwiftNEW {
6262
decoded = try JSONDecoder().decode([Vmodel].self, from: responseData)
6363
} else {
6464
// MARK: Local Data
65-
guard let url = Bundle.main.url(forResource: source, withExtension: "json") else { throw CocoaError(.fileNoSuchFile) }
65+
guard let url = dataBundle.url(forResource: source, withExtension: "json") else { throw CocoaError(.fileNoSuchFile) }
6666
decoded = try await Task.detached {
6767
let fileData = try Data(contentsOf: url)
6868
return try JSONDecoder().decode([Vmodel].self, from: fileData)

Sources/SwiftNEW/Styles/MeshView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ struct MeshView: View {
1414
#if compiler(>=6.0)
1515
if #available(iOS 18.0, macOS 15.0, visionOS 2.0, tvOS 18.0, *) {
1616
meshGradient
17-
} else {
17+
} else { // coverage:ignore-start -- Xcode 26 coverage runner cannot execute older OS mesh fallback.
1818
fallbackGradient
19-
}
19+
} // coverage:ignore-end
2020
#else
2121
fallbackGradient
2222
#endif
@@ -43,6 +43,7 @@ struct MeshView: View {
4343
}
4444
#endif
4545

46+
// coverage:ignore-start -- Covered by compatibility builds; unreachable on the Xcode 26 coverage runner.
4647
private var fallbackGradient: some View {
4748
LinearGradient(
4849
colors: [Color(.clear), color.opacity(0.6)],
@@ -51,4 +52,5 @@ struct MeshView: View {
5152
)
5253
.ignoresSafeArea(.all)
5354
}
55+
// coverage:ignore-end
5456
}

Sources/SwiftNEW/SwiftNEW.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public struct SwiftNEW: View {
6969
@Binding var showBuild: Bool
7070
@Binding var headingStyle: SwiftNEWHeadingStyle
7171
@Binding var iconStyle: SwiftNEWIconStyle
72+
var dataBundle: Bundle = .main
7273

7374
public init(
7475
show: Binding<Bool>,
@@ -173,7 +174,8 @@ extension SwiftNEW {
173174
presentation: SwiftNEWPresentation = .sheet,
174175
showBuild: Bool = true,
175176
headingStyle: SwiftNEWHeadingStyle = .version,
176-
iconStyle: SwiftNEWIconStyle = .filled
177+
iconStyle: SwiftNEWIconStyle = .filled,
178+
dataBundle: Bundle = .main
177179
) {
178180
_version = AppStorage(wrappedValue: "", "swiftnew.version")
179181
_build = AppStorage(wrappedValue: "", "swiftnew.build")
@@ -201,6 +203,7 @@ extension SwiftNEW {
201203
_showBuild = .constant(showBuild)
202204
_headingStyle = .constant(headingStyle)
203205
_iconStyle = .constant(iconStyle)
206+
self.dataBundle = dataBundle
204207
}
205208
}
206209
#endif

Sources/SwiftNEW/Views/Components/ButtonComponents.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@ extension SwiftNEW {
2323
}
2424

2525
public var searchButton: some View {
26-
capsuleSecondaryButton(action: {
27-
withAnimation { showSearch.toggle() }
28-
if !showSearch {
29-
searchText = ""
30-
debouncedSearchText = ""
31-
}
32-
}) {
26+
capsuleSecondaryButton(action: toggleSearchVisibility) {
3327
Text(String(localized: "Search", bundle: .module))
3428
Image(systemName: showSearch ? "xmark.circle" : "magnifyingglass")
3529
}
3630
}
3731

32+
func toggleSearchVisibility() {
33+
withAnimation { showSearch.toggle() }
34+
if !showSearch {
35+
searchText = ""
36+
debouncedSearchText = ""
37+
}
38+
}
39+
3840
public var closeCurrentButton: some View {
3941
primaryActionButton(
4042
titleKey: "Continue",

Sources/SwiftNEW/Views/Sheets/CurrentVersionSheet.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ extension SwiftNEW {
4343
Text(loadErrorMessage)
4444
.font(.headline)
4545
.multilineTextAlignment(.center)
46-
Button {
47-
loadedDataSource = nil
48-
loadData()
49-
} label: {
46+
Button(action: retryLoadData) {
5047
Text(String(localized: "Try Again", bundle: .module))
5148
}
5249
}
@@ -172,4 +169,9 @@ extension SwiftNEW {
172169
func matchesSearch(_ new: Model) -> Bool {
173170
SwiftNEWSearch.matches(new, query: debouncedSearchText, isEnabled: showSearch)
174171
}
172+
173+
func retryLoadData() {
174+
loadedDataSource = nil
175+
loadData()
176+
}
175177
}

Sources/SwiftNEW/Views/SwiftNEW+View.swift

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,7 @@ extension SwiftNEW {
6161
if presentation == .embed {
6262
sheetContent
6363
} else {
64-
Button(action: {
65-
#if os(iOS)
66-
if showDrop {
67-
drop()
68-
} else {
69-
show = true
70-
}
71-
#else
72-
show = true
73-
#endif
74-
}) {
64+
Button(action: presentReleaseNotes) {
7565
Label(String(localized: String.LocalizationValue(label), bundle: .module), systemImage: labelImage)
7666
.frame(
7767
width: size == "mini" ? nil : (size == "invisible" ? 0 : platformWidth),
@@ -90,6 +80,18 @@ extension SwiftNEW {
9080
}
9181
}
9282

83+
func presentReleaseNotes() {
84+
#if os(iOS)
85+
if showDrop {
86+
drop()
87+
} else {
88+
show = true
89+
}
90+
#else
91+
show = true
92+
#endif
93+
}
94+
9395
private var platformWidth: CGFloat {
9496
#if os(tvOS)
9597
400
@@ -128,6 +130,12 @@ extension SwiftNEW {
128130
sheetHistory
129131
}
130132
}
133+
134+
#if DEBUG
135+
var testingHistorySheetContent: some View {
136+
historySheetContent
137+
}
138+
#endif
131139
}
132140

133141
private struct SheetBackdropModifier: ViewModifier {
@@ -143,7 +151,7 @@ private struct SheetBackdropModifier: ViewModifier {
143151
} else {
144152
content.presentationBackground(.thinMaterial)
145153
}
146-
} else {
154+
} else { // coverage:ignore-start -- Xcode 26 coverage runner cannot execute older OS sheet fallback.
147155
// Older OS / embed: fall back to a full-bleed background.
148156
if mesh {
149157
content.background {
@@ -152,7 +160,7 @@ private struct SheetBackdropModifier: ViewModifier {
152160
} else {
153161
content.background(.ultraThinMaterial, ignoresSafeAreaEdges: .all)
154162
}
155-
}
163+
} // coverage:ignore-end
156164
}
157165
}
158166

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"version": "1.0",
4+
"subVersion": "6.3.1",
5+
"new": [
6+
{
7+
"icon": "sparkles",
8+
"title": "Local",
9+
"subtitle": "Bundle",
10+
"body": "Coverage"
11+
}
12+
]
13+
}
14+
]

0 commit comments

Comments
 (0)