Skip to content

Commit 3a28715

Browse files
committed
Show the latest patch of each minor version in the chart
1 parent 21e035c commit 3a28715

5 files changed

Lines changed: 76 additions & 38 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,11 @@ Manifest order controls project, filter, baseline, and table row order. The main
187187
page shows a grouped bar chart with a fixed 0–100% axis. Each group is a
188188
candidate version, and each bar in a group is an SDK baseline. Numeric release
189189
labels, including prereleases, sort from oldest to newest. Custom version labels
190-
keep their manifest order. Select a bar to open its report, or select a legend
190+
keep their manifest order. The chart and the Markdown/SVG tables show the
191+
highest patch version of each minor version, for example 0.23.1 and not 0.23.0.
192+
A selected earlier patch version, for example from a release notes link, is
193+
shown too, and changes are measured from the previous shown version. Summaries
194+
and reports include every version. Select a bar to open its report, or select a legend
191195
item to isolate a baseline. About four versions fill the visible width, the
192196
percentage axis stays in place while the chart scrolls, and wide bars show their
193197
values. When one baseline is shown, each version shows the change in matched
@@ -208,12 +212,10 @@ python3 Tools/InterfaceCoverage/Scripts/collect_openswiftui_releases.py collect
208212
--xcode /Applications/Xcode-16.4.0.app --xcode /Applications/Xcode-26.6.0.app
209213
```
210214

211-
Use `--all-releases` in place of `--release` to compare the published releases
212-
that include both XCFrameworks. It keeps only the highest patch version of each
213-
minor version, for example 0.23.1 and not 0.23.0. A release comes after its
214-
prereleases. `--release` uses the given versions as they are. Pass `--site-url`
215-
to make summary links absolute. Add `--include-manifest` with an earlier
216-
manifest to keep its reports for other projects in the same site.
215+
Use `--all-releases` in place of `--release` to compare every published release
216+
that includes both XCFrameworks. Pass `--site-url` to make summary links
217+
absolute. Add `--include-manifest` with an earlier manifest to keep its reports
218+
for other projects in the same site.
217219

218220
The results for one release, target, and SDK form a cache entry: one report for
219221
each scope and a `cache.json` file. An entry is reused while its fingerprint

‎Scripts/collect_openswiftui_releases.py‎

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -109,30 +109,8 @@ def release_metadata(repository, version, directory, refresh=False):
109109
return json.loads(path.read_text())
110110

111111

112-
def version_key(version):
113-
"""Orders semantic versions. A release comes after its prereleases."""
114-
match = re.fullmatch(r'v?(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?', version)
115-
if match is None:
116-
return None
117-
major, minor, patch, prerelease = match.groups()
118-
parts = [(0, int(part), '') if part.isdigit() else (1, 0, part) for part in (prerelease or '').split('.') if part]
119-
return (int(major), int(minor), int(patch or 0)), not prerelease, parts
120-
121-
122-
def latest_patches(versions):
123-
"""Keeps the highest patch version of each major and minor version, in the input order.
124-
Labels that are not semantic versions are kept."""
125-
latest = {}
126-
for version in versions:
127-
key = version_key(version)
128-
if key is not None and (key[0][:2] not in latest or key > version_key(latest[key[0][:2]])):
129-
latest[key[0][:2]] = version
130-
return [version for version in versions if version_key(version) is None or latest[version_key(version)[0][:2]] == version]
131-
132-
133112
def published_releases(repository, work):
134-
"""Returns the published releases that include both XCFrameworks, oldest first. Only the
135-
highest patch version of each minor version is kept."""
113+
"""Returns the published releases that include both XCFrameworks, oldest first."""
136114
releases = json.loads(gh('release', 'list', '--repo', repository, '--limit', '1000',
137115
'--exclude-drafts', '--json', 'tagName,publishedAt'))
138116
result = []
@@ -145,7 +123,7 @@ def published_releases(repository, work):
145123
names = {asset['name'] for asset in release_metadata(repository, version, work / 'releases' / version, refresh=True)['assets']}
146124
if all(name in names for name in wanted):
147125
result.append(version)
148-
return latest_patches(result)
126+
return result
149127

150128

151129
def asset_digests(repository, version, work):
@@ -365,7 +343,7 @@ def add_site_options(command):
365343
releases = collect_command.add_mutually_exclusive_group(required=True)
366344
releases.add_argument('--release', action='append', help='Release tag. Repeat for more releases.')
367345
releases.add_argument('--all-releases', action='store_true',
368-
help='Use the highest patch version of each minor version among the published releases that include both XCFrameworks.')
346+
help='Use every published release that includes both XCFrameworks.')
369347
collect_command.add_argument('--xcode', action='append', required=True, type=pathlib.Path,
370348
help='Xcode application that supplies the baseline SDK. Repeat for more baselines.')
371349
collect_command.add_argument('--target', action='append', choices=sorted(TARGETS),

‎Sources/InterfaceCoverage/CoverageSite.swift‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,11 @@ public enum CoverageSite {
120120
files["assets/site.js"] = Data(script.utf8)
121121
for group in ordered(records.map(\.group)) {
122122
let rows = records.filter { $0.group == group }
123-
files["tables/\(group).md"] = Data(markdown(rows).utf8)
124-
files["tables/\(group).svg"] = Data(svg(rows).utf8)
123+
// Like the chart, tables show the highest patch version of each minor version.
124+
let shown = Set(latestPatches(ordered(rows.map(\.version))))
125+
let tableRows = rows.filter { shown.contains($0.version) }
126+
files["tables/\(group).md"] = Data(markdown(tableRows).utf8)
127+
files["tables/\(group).svg"] = Data(svg(tableRows).utf8)
125128
// A badge shows the latest version that has a result for each baseline.
126129
let versions = versionOrder(ordered(rows.map(\.version)))
127130
for baseline in ordered(rows.map(\.baseline)) {
@@ -174,6 +177,19 @@ public enum CoverageSite {
174177

175178
/// Sorts numeric release labels, including prereleases, from oldest to newest.
176179
/// Other labels keep their manifest order. The site script uses the same rule.
180+
/// Keeps the highest patch version of each minor version. Other labels are kept.
181+
static func latestPatches(_ versions: [String]) -> [String] {
182+
func minor(_ version: String) -> String? {
183+
guard let match = version.wholeMatch(of: /^v?(\d+)\.(\d+)(?:\.\d+)?(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/) else { return nil }
184+
return "\(match.1).\(match.2)"
185+
}
186+
let sorted = versionOrder(versions)
187+
return sorted.enumerated().filter { index, version in
188+
guard let key = minor(version) else { return true }
189+
return !sorted[(index + 1)...].contains { minor($0) == key }
190+
}.map(\.element)
191+
}
192+
177193
static func versionOrder(_ versions: [String]) -> [String] {
178194
let pattern = /^v?(\d+)\.(\d+)(?:\.(\d+))?(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/
179195
var parsed: [(version: String, numbers: [Int], prerelease: [Substring])] = []

‎Sources/InterfaceCoverage/SiteAssets/site.js‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
let groupRows = [];
4646
let rows = [];
4747
let chartWidth = 0;
48+
let chartVersions = null;
4849
let current = null;
4950
let documentData = null;
5051
let request = 0;
@@ -163,10 +164,27 @@
163164
return node;
164165
}
165166

166-
// Compares a result with the previous version that has a result for the same baseline.
167+
function minorVersion(version) {
168+
const match = /^v?(\d+)\.(\d+)(?:\.\d+)?(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i.exec(version);
169+
return match ? `${match[1]}.${match[2]}` : null;
170+
}
171+
172+
// The chart shows the highest patch version of each minor version, and the selected version.
173+
// A summary link can select an earlier patch version, which is then shown too.
174+
function displayedVersions() {
175+
const versions = versionOrder(unique(groupRows.map(row => row.version)));
176+
const selected = reports.find(row => row.id === state.report)?.version;
177+
return versions.filter((version, index) => {
178+
const minor = minorVersion(version);
179+
return version === selected || !minor || versions.slice(index + 1).every(other => minorVersion(other) !== minor);
180+
});
181+
}
182+
183+
// Compares a result with the previous shown version that has a result for the same baseline.
167184
function change(record) {
168185
if (!record.summary.reference) return null;
169-
const series = rows.filter(row => row.baseline === record.baseline && row.summary.reference > 0);
186+
const shown = displayedVersions();
187+
const series = rows.filter(row => row.baseline === record.baseline && row.summary.reference > 0 && shown.includes(row.version));
170188
const order = versionOrder(unique(series.map(row => row.version)));
171189
const index = order.indexOf(record.version);
172190
const previous = index > 0 ? series.find(row => row.version === order[index - 1]) : null;
@@ -189,6 +207,11 @@
189207
}
190208

191209
function updateChartSelection() {
210+
// Selecting another bar can hide an earlier patch version that was shown for the selection.
211+
if (!embedded && chartVersions !== null && displayedVersions().join("|") !== chartVersions) {
212+
renderChart();
213+
return;
214+
}
192215
$("chart").querySelectorAll("[data-report]").forEach(point => {
193216
point.setAttribute("aria-pressed", String(point.dataset.report === state.report));
194217
});
@@ -209,7 +232,8 @@
209232
const focused = document.activeElement?.closest("[data-report]")?.dataset.report;
210233
const scrollLeft = $("chart-scroll")?.scrollLeft || 0;
211234
const baselines = unique(groupRows.map(row => row.baseline));
212-
const versions = versionOrder(unique(groupRows.map(row => row.version)));
235+
const versions = displayedVersions();
236+
chartVersions = versions.join("|");
213237
const visible = baselines.filter(baseline => !state.baseline || baseline === state.baseline);
214238
// With one baseline, each version also shows the change from the previous version.
215239
const showChanges = visible.length === 1;
@@ -351,7 +375,7 @@
351375
head.append(th);
352376
}
353377
const body = table.createTBody();
354-
for (const version of unique(rows.map(row => row.version))) {
378+
for (const version of displayedVersions().filter(version => rows.some(row => row.version === version))) {
355379
const tr = body.insertRow();
356380
const th = element("th", version);
357381
th.scope = "row";

‎Tests/InterfaceCoverageTests/CoverageSiteTests.swift‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,24 @@ struct CoverageSiteTests {
254254
#expect(markdown.contains("| iOS 18.5 | [0.00%]("))
255255
}
256256

257+
@Test func tablesShowTheLatestPatchOfEachMinorVersion() throws {
258+
let root = try temporaryDirectory()
259+
defer { try? FileManager.default.removeItem(at: root) }
260+
let rows = try ["0.9.0", "0.10.0", "0.10.1"].map { version -> [String: Any] in
261+
var row = try fixture(root)
262+
row["version"] = version
263+
return row
264+
}
265+
let output = try generate(root, rows: rows)
266+
let tables = try FileManager.default.contentsOfDirectory(at: output.appendingPathComponent("tables"), includingPropertiesForKeys: nil)
267+
let markdown = try String(contentsOf: #require(tables.first { $0.pathExtension == "md" }), encoding: .utf8)
268+
#expect(markdown.contains("| 0.9.0 |"))
269+
#expect(markdown.contains("| 0.10.1 |"))
270+
#expect(!markdown.contains("| 0.10.0 |"))
271+
// Summaries keep every version, so release notes of earlier patches can link to them.
272+
#expect(FileManager.default.fileExists(atPath: output.appendingPathComponent("summary/example/0.10.0.json").path))
273+
}
274+
257275
@Test func rejectsInconsistentCounts() throws {
258276
let root = try temporaryDirectory()
259277
defer { try? FileManager.default.removeItem(at: root) }

0 commit comments

Comments
 (0)