Skip to content

Commit e57d7d6

Browse files
authored
Merge pull request #76413 from gottesmm/swift_snapshot_tool_more_fixes
[swift_snapshot_tool] Some small fixes and add a run_toolchain command for running a command against a specific toolchain.
2 parents 436e06f + 3c1bcc9 commit e57d7d6

File tree

6 files changed

+265
-44
lines changed

6 files changed

+265
-44
lines changed

utils/swift_snapshot_tool/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55

66
let package = Package(
77
name: "swift_snapshot_tool",
8-
platforms: [.macOS(.v12)],
8+
platforms: [.macOS(.v14)],
99
products: [
1010
// Products define the executables and libraries a package produces, making them visible to other packages.
1111
.executable(

utils/swift_snapshot_tool/Sources/swift_snapshot_tool/bisect_toolchains.swift

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,42 @@ struct BisectToolchains: AsyncParsableCommand {
2727
""")
2828
var script: String
2929

30-
@Option(help: "Oldest tag. Expected to pass")
31-
var goodTag: String
30+
@Option(help: "Oldest Date. Expected to Pass. We use the first snapshot produced before the given date")
31+
var oldDate: String
32+
33+
var oldDateAsDate: Date {
34+
let d = DateFormatter()
35+
d.dateFormat = "yyyy-MM-dd"
36+
guard let result = d.date(from: oldDate) else {
37+
log("Improperly formatted date: \(oldDate)! Expected format: yyyy_MM_dd.")
38+
fatalError()
39+
}
40+
return result
41+
}
3242

33-
@Option(help: "Newest tag. Expected to fail. If not set, use newest snapshot")
34-
var badTag: String?
43+
@Option(help: """
44+
Newest Date. Expected to fail. If not set, use newest snapshot. We use the
45+
first snapshot after new date
46+
""")
47+
var newDate: String?
48+
49+
var newDateAsDate: Date? {
50+
guard let newDate = self.newDate else { return nil }
51+
let d = DateFormatter()
52+
d.dateFormat = "yyyy-MM-dd"
53+
guard let result = d.date(from: newDate) else {
54+
log("Improperly formatted date: \(newDate)! Expected format: yyyy_MM_dd.")
55+
fatalError()
56+
}
57+
return result
58+
}
3559

3660
@Flag(help: "Invert the test so that we assume the newest succeeds")
3761
var invert = false
3862

63+
@Argument(help: "Extra constant arguments to pass to the test")
64+
var extraArgs: [String] = []
65+
3966
mutating func run() async throws {
4067
if !FileManager.default.fileExists(atPath: workspace) {
4168
do {
@@ -49,20 +76,25 @@ struct BisectToolchains: AsyncParsableCommand {
4976
}
5077

5178
// Load our tags from swift's github repo
52-
let tags = try! await getTagsFromSwiftRepo(branch: branch)
53-
54-
guard let goodTagIndex = tags.firstIndex(where: { $0.name == self.goodTag }) else {
55-
log("Failed to find tag: \(self.goodTag)")
79+
let tags = try! await getTagsFromSwiftRepo(branch: branch, dryRun: true)
80+
81+
// Newest is first. So 0 maps to the newest tag. We do this so someone can
82+
// just say 50 toolchains ago. To get a few weeks worth. This is easier than
83+
// writing dates a lot.
84+
let oldDateAsDate = self.oldDateAsDate
85+
guard let goodTagIndex = tags.firstIndex(where: { $0.tag.date(branch: self.branch) < oldDateAsDate }) else {
86+
log("Failed to find tag with date: \(oldDateAsDate)")
5687
fatalError()
5788
}
5889

59-
let badTagIndex: Array<Tag>.Index
60-
if let badTag = self.badTag {
61-
guard let n = tags.firstIndex(where: { $0.name == badTag }) else {
62-
log("Failed to find tag: \(badTag)")
90+
let badTagIndex: Int
91+
if let newDateAsDate = self.newDateAsDate {
92+
let b = tags.firstIndex(where: { $0.tag.date(branch: self.branch) < newDateAsDate })
93+
guard let b else {
94+
log("Failed to find tag newer than date: \(newDateAsDate)")
6395
fatalError()
6496
}
65-
badTagIndex = n
97+
badTagIndex = b
6698
} else {
6799
badTagIndex = 0
68100
}
@@ -73,30 +105,73 @@ struct BisectToolchains: AsyncParsableCommand {
73105
fatalError()
74106
}
75107

76-
log("[INFO] Testing \(totalTags) toolchains")
77-
78108
var startIndex = goodTagIndex
79109
var endIndex = badTagIndex
80-
while startIndex != endIndex && startIndex != (endIndex - 1) {
110+
111+
// First check if the newest toolchain succeeds. We assume this in our bisection.
112+
do {
113+
log("Testing that Oldest Tag Succeeds: \(tags[startIndex].tag))")
114+
let result = try! await downloadToolchainAndRunTest(
115+
platform: platform, tag: tags[startIndex].tag, branch: branch, workspace: workspace, script: script,
116+
extraArgs: extraArgs)
117+
var success = result == 0
118+
if self.invert {
119+
success = !success
120+
}
121+
if !success {
122+
log("[INFO] Oldest snapshot fails?! We assume that the oldest snapshot is known good!")
123+
} else {
124+
log("[INFO] Oldest snapshot passes test. Snapshot: \(tags[startIndex])")
125+
}
126+
}
127+
128+
do {
129+
log("Testing that Newest Tag Fails: \(tags[endIndex].tag))")
130+
let result = try! await downloadToolchainAndRunTest(
131+
platform: platform, tag: tags[endIndex].tag, branch: branch, workspace: workspace, script: script,
132+
extraArgs: extraArgs)
133+
var success = result != 0
134+
if self.invert {
135+
success = !success
136+
}
137+
if !success {
138+
log("[INFO] Newest snapshot succeceds?! We assume that the newest snapshot is known bad!")
139+
} else {
140+
log("[INFO] Newest snapshot passes test. Snapshot: \(tags[endIndex])")
141+
}
142+
}
143+
144+
log("[INFO] Testing \(totalTags) toolchains")
145+
while startIndex != endIndex && startIndex != endIndex {
81146
let mid = (startIndex + endIndex) / 2
147+
148+
let midValue = tags[mid].tag
82149
log(
83-
"[INFO] Visiting Mid: \(mid) with (Start, End) = (\(startIndex),\(endIndex)). Tag: \(tags[mid])"
150+
"[INFO] Visiting Mid: \(mid) with (Start, End) = (\(startIndex),\(endIndex)). Tag: \(midValue)"
84151
)
85152
let result = try! await downloadToolchainAndRunTest(
86-
platform: platform, tag: tags[mid], branch: branch, workspace: workspace, script: script)
153+
platform: platform, tag: midValue, branch: branch, workspace: workspace, script: script,
154+
extraArgs: extraArgs)
87155

88156
var success = result == 0
89157
if self.invert {
90158
success = !success
91159
}
92160

161+
let midIsEndIndex = mid == endIndex
162+
93163
if success {
94164
log("[INFO] PASSES! Setting start to mid!")
95165
startIndex = mid
96166
} else {
97167
log("[INFO] FAILS! Setting end to mid")
98168
endIndex = mid
99169
}
170+
171+
if midIsEndIndex {
172+
log("Last successful value: \(tags[mid+1])")
173+
break
174+
}
100175
}
101176
}
102177
}

utils/swift_snapshot_tool/Sources/swift_snapshot_tool/download_toolchain.swift

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ private func downloadFile(url: URL, localPath: URL, verboseDownload: Bool = true
5555
}
5656
}
5757

58-
private func shell(_ command: String, environment: [String: String] = [:]) -> (
58+
private func shell(_ command: String, environment: [String: String] = [:],
59+
mustSucceed: Bool = true,verbose: Bool = false,
60+
extraArgs: [String] = []) -> (
5961
stdout: String, stderr: String, exitCode: Int
6062
) {
6163
let task = Process()
@@ -64,11 +66,15 @@ private func shell(_ command: String, environment: [String: String] = [:]) -> (
6466

6567
task.standardOutput = stdout
6668
task.standardError = stderr
67-
task.arguments = ["-c", command]
69+
var newCommand = command
70+
if extraArgs.count != 0 {
71+
newCommand = newCommand.appending(" ").appending(extraArgs.joined(separator: " "))
72+
}
73+
task.arguments = ["-c", newCommand]
6874
if !environment.isEmpty {
6975
if let e = task.environment {
70-
print("Task Env: \(e)")
71-
print("Passed in Env: \(environment)")
76+
log("Task Env: \(e)")
77+
log("Passed in Env: \(environment)")
7278
task.environment = e.merging(
7379
environment,
7480
uniquingKeysWith: {
@@ -78,7 +84,9 @@ private func shell(_ command: String, environment: [String: String] = [:]) -> (
7884
task.environment = environment
7985
}
8086
}
81-
87+
if verbose {
88+
log("Command: \(command)\n")
89+
}
8290
task.launchPath = "/bin/zsh"
8391
task.standardInput = nil
8492
task.launch()
@@ -88,20 +96,22 @@ private func shell(_ command: String, environment: [String: String] = [:]) -> (
8896
decoding: stderr.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
8997
let stdoutData = String(
9098
decoding: stdout.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
91-
if task.terminationStatus != 0 {
92-
log("Command Failed: \(command)")
99+
if verbose {
93100
log("StdErr:\n\(stderrData)\n")
94101
log("StdOut:\n\(stdoutData)\n")
102+
}
103+
if mustSucceed && task.terminationStatus != 0 {
104+
log("Command Failed!")
95105
fatalError()
96106
}
97-
98107
return (stdout: stdoutData, stderr: stderrData, Int(task.terminationStatus))
99108
}
100109

101110
func downloadToolchainAndRunTest(
102111
platform: Platform, tag: Tag, branch: Branch,
103112
workspace: String, script: String,
104-
verboseDownload: Bool = true
113+
extraArgs: [String],
114+
verbose: Bool = false
105115
) async throws -> Int {
106116
let fileType = platform.fileType
107117
let toolchainType = platform.toolchainType
@@ -117,6 +127,8 @@ func downloadToolchainAndRunTest(
117127
log("[INFO] Starting Download: \(downloadURL) -> \(downloadPath)")
118128
try await downloadFile(url: downloadURL, localPath: downloadPath)
119129
log("[INFO] Finished Download: \(downloadURL) -> \(downloadPath)")
130+
} else {
131+
log("[INFO] File exists! No need to download! Path: \(downloadPath)")
120132
}
121133

122134
switch platform {
@@ -126,16 +138,21 @@ func downloadToolchainAndRunTest(
126138
_ = shell("pkgutil --expand \(downloadPath.path) \(toolchainDir)")
127139
let payloadPath = "\(toolchainDir)/\(tag.name)-osx-package.pkg/Payload"
128140
_ = shell("tar -xf \(payloadPath) -C \(toolchainDir)")
129-
let swiftcPath = "\(toolchainDir)/usr/bin/swiftc"
130-
let swiftFrontendPath = "\(toolchainDir)/usr/bin/swift-frontend"
131-
log(shell("\(swiftcPath) --version").stdout)
132-
let exitCode = shell(
133-
"\(script)", environment: ["SWIFTC": swiftcPath, "SWIFT_FRONTEND": swiftFrontendPath]
134-
).exitCode
135-
log("[INFO] Exit code: \(exitCode). Tag: \(tag.name). Script: \(script)")
136-
return exitCode
141+
} else {
142+
log("[INFO] No need to install: \(downloadPath)")
137143
}
138-
return 0
144+
145+
let swiftcPath = "\(toolchainDir)/usr/bin/swiftc"
146+
let swiftFrontendPath = "\(toolchainDir)/usr/bin/swift-frontend"
147+
log(shell("\(swiftcPath) --version").stdout)
148+
let exitCode = shell(
149+
"\(script)", environment: ["SWIFTC": swiftcPath, "SWIFT_FRONTEND": swiftFrontendPath],
150+
mustSucceed: false,
151+
verbose: verbose,
152+
extraArgs: extraArgs
153+
).exitCode
154+
log("[INFO] Exit code: \(exitCode). Tag: \(tag.name). Script: \(script)")
155+
return exitCode
139156
case .ubuntu1404, .ubuntu1604, .ubuntu1804:
140157
fatalError("Unsupported platform: \(platform)")
141158
}

utils/swift_snapshot_tool/Sources/swift_snapshot_tool/get_tags.swift

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,69 @@ struct Tag: Decodable {
1414
var name: Substring {
1515
ref.dropFirst(10)
1616
}
17+
18+
func dateString(_ branch: Branch) -> Substring {
19+
// FIXME: If we ever actually use interesting a-b builds, we should capture this information
20+
// would be better to do it sooner than later.
21+
return name.dropFirst("swift-".count + branch.rawValue.count + "-SNAPSHOT-".count).dropLast(2)
22+
}
23+
24+
func date(branch: Branch) -> Date {
25+
// TODO: I think that d might be a class... if so, we really want to memoize
26+
// this.
27+
let d = DateFormatter()
28+
d.dateFormat = "yyyy-MM-dd"
29+
return d.date(from: String(dateString(branch)))!
30+
}
1731
}
1832

33+
1934
extension Tag: CustomDebugStringConvertible {
2035
var debugDescription: String {
2136
String(name)
2237
}
2338
}
2439

25-
func getTagsFromSwiftRepo(branch: Branch) async throws -> [Tag] {
26-
let GITHUB_BASE_URL = "https://api.github.com"
27-
let GITHUB_TAG_LIST_URL = URL(string: "\(GITHUB_BASE_URL)/repos/apple/swift/git/refs/tags")!
40+
/// A pair of a branch and a tag
41+
struct BranchTag {
42+
var tag: Tag
43+
var branch: Branch
44+
}
45+
46+
extension BranchTag: CustomDebugStringConvertible {
47+
var debugDescription: String {
48+
tag.debugDescription
49+
}
50+
}
51+
52+
func getTagsFromSwiftRepo(branch: Branch, dryRun: Bool = false) async throws -> [BranchTag] {
53+
let github_tag_list_url: URL
54+
if !dryRun {
55+
github_tag_list_url = URL(string: "https://api.github.com/repos/apple/swift/git/refs/tags")!
56+
} else {
57+
github_tag_list_url = URL(string: "file:///Users/gottesmm/triage/github_data")!
58+
}
2859

2960
let decoder = JSONDecoder()
3061
decoder.keyDecodingStrategy = .convertFromSnakeCase
3162

3263
log("[INFO] Starting to download snapshot information from github.")
33-
async let data = URLSession.shared.data(from: GITHUB_TAG_LIST_URL).0
64+
async let data = URLSession.shared.data(from: github_tag_list_url).0
3465
let allTags = try! decoder.decode([Tag].self, from: await data)
3566
log("[INFO] Finished downloading snapshot information from github.")
3667

3768
let snapshotTagPrefix = "swift-\(branch.rawValue.uppercased())"
3869

39-
// Then filter the tags to just include the specific snapshot prefix.
40-
var filteredTags = allTags.filter {
70+
// Then filter the tags to just include the specific snapshot branch
71+
// prefix. Add the branch to an aggregate BranchTag.
72+
var filteredTags: [BranchTag] = allTags.filter {
4173
$0.name.starts(with: snapshotTagPrefix)
74+
}.map {
75+
BranchTag(tag: $0, branch: branch)
4276
}
43-
filteredTags.sort { $0.ref < $1.ref }
77+
78+
// Then sort so that the newest branch prefix
79+
filteredTags.sort { $0.tag.ref < $1.tag.ref }
4480
filteredTags.reverse()
4581

4682
return filteredTags

0 commit comments

Comments
 (0)