Commit 604f2da53e

604f2da53efc32270918b9d94ca9fb7a4372a1cf

parent: f8856437fb

Unsigned

cmc <hello@cleberg.net> · 2026-09-20 01:33 UTC

Move the live suite's fixtures off krz/gitbay-ios (!116)

The issue and merge-request fixtures move to ios-smoke/ui-smoke-edit, the
repo the edit flow already commits to. krz/gitbay-ios stops accumulating
smoke issues and closed merge requests and no longer needs a ui-smoke
branch.

smoke-account.sh seeds ui-smoke divergent from main in the fixture repo —
the MR flows open one merge request each way, so each side needs a commit
the other lacks — and reruns now reach that step on an existing repo. The
milestone the issue flow assigns is created by the suite.

Closes #9

Layout: unified · split

gitbayUITests/LiveSmokeUITests.swift +41 −23
@@ -11,9 +11,10 @@ import XCTest
11/// -only-testing:gitbayUITests ... 11/// -only-testing:gitbayUITests ...
12/// 12///
13/// They run as the `ios-smoke` account, a krz admin that owns the 13/// They run as the `ios-smoke` account, a krz admin that owns the
14/// scratch and fixture repositories (smoke-account.sh provisions it), and 14/// scratch and fixture repositories (smoke-account.sh provisions it).
15/// assume branch `ui-smoke` exists on krz/gitbay-ios. A signed-out device 15/// Everything they write goes to those, never to krz/gitbay-ios. A
16/// is signed in at setUp from TEST_RUNNER_GITBAY_UITEST_TOKEN. 16/// signed-out device is signed in at setUp from
17/// TEST_RUNNER_GITBAY_UITEST_TOKEN.
17final class LiveSmokeUITests: XCTestCase { 18final class LiveSmokeUITests: XCTestCase {
18 19
19 private var app: XCUIApplication! 20 private var app: XCUIApplication!
@@ -32,7 +33,11 @@ final class LiveSmokeUITests: XCTestCase {
32 static let smokeUser = "ios-smoke" 33 static let smokeUser = "ios-smoke"
33 static let smokeProfileDescription = "Live UI smoke account for gitbay-ios" 34 static let smokeProfileDescription = "Live UI smoke account for gitbay-ios"
34 static let smokePGPEmail = "ios-smoke+pgp@gitbay.org" 35 static let smokePGPEmail = "ios-smoke+pgp@gitbay.org"
35 static let editRepo = "\(smokeUser)/ui-smoke-edit" 36
37 /// The repository the write flows work in: the fixture issue and
38 /// merge requests, and the file testBlameAndEditFlows edits.
39 /// smoke-account.sh seeds it, with `main` and `ui-smoke` divergent.
40 static let fixtureRepo = "\(smokeUser)/ui-smoke-edit"
36 41
37 override func setUpWithError() throws { 42 override func setUpWithError() throws {
38 try XCTSkipUnless( 43 try XCTSkipUnless(
@@ -56,6 +61,8 @@ final class LiveSmokeUITests: XCTestCase {
56 deleteScratchRepo() 61 deleteScratchRepo()
57 } 62 }
58 if name.contains("testCreationAndEditingFlows") { 63 if name.contains("testCreationAndEditingFlows") {
64 try requireFixtureRepo()
65 ensureFixtureMilestone()
59 ensureFixtureIssue() 66 ensureFixtureIssue()
60 try requireFixtureBranch() 67 try requireFixtureBranch()
61 } 68 }
@@ -69,6 +76,7 @@ final class LiveSmokeUITests: XCTestCase {
69 ensureOrgFixtures() 76 ensureOrgFixtures()
70 } 77 }
71 if name.contains("testCloseInFavourFlow") { 78 if name.contains("testCloseInFavourFlow") {
79 try requireFixtureRepo()
72 try requireFixtureBranch() 80 try requireFixtureBranch()
73 closeSmokeMergeRequests() 81 closeSmokeMergeRequests()
74 try openSupersedeFixtures() 82 try openSupersedeFixtures()
@@ -143,7 +151,7 @@ final class LiveSmokeUITests: XCTestCase {
143 "--title", Self.successorTitle]) 151 "--title", Self.successorTitle])
144 let open = openSmokeMergeRequests().map { $0.title } 152 let open = openSmokeMergeRequests().map { $0.title }
145 try XCTSkipUnless(open.contains(Self.supersededTitle) && open.contains(Self.successorTitle), """ 153 try XCTSkipUnless(open.contains(Self.supersededTitle) && open.contains(Self.successorTitle), """
146 could not open both fixture merge requests on krz/gitbay-ios \ 154 could not open both fixture merge requests on \(Self.fixtureRepo) \
147 (have \(open)); the ui-smoke branch may be level with main. 155 (have \(open)); the ui-smoke branch may be level with main.
148 """) 156 """)
149 } 157 }
@@ -175,6 +183,19 @@ final class LiveSmokeUITests: XCTestCase {
175 _ = runCommand(["repo", "delete", Self.scratchRepo + "-2", "--yes"]) 183 _ = runCommand(["repo", "delete", Self.scratchRepo + "-2", "--yes"])
176 } 184 }
177 185
186 /// The milestone the editing flow assigns. Created rather than
187 /// required: unlike a branch, the API can make one.
188 static let fixtureMilestone = "v1.0.0"
189
190 private func ensureFixtureMilestone() {
191 guard let data = readCommand(["milestone", "list", Self.fixtureRepo, "--state", "all"]),
192 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
193 else { return }
194 let rows = envelope["data"] as? [[String: Any]] ?? []
195 if rows.contains(where: { $0["title"] as? String == Self.fixtureMilestone }) { return }
196 _ = runCommand(["milestone", "create", Self.fixtureRepo, Self.fixtureMilestone])
197 }
198
178 /// The issue the editing flow works on. It is a fixture, not a real 199 /// The issue the editing flow works on. It is a fixture, not a real
179 /// issue: pinning the test to whatever issue happened to be open put 200 /// issue: pinning the test to whatever issue happened to be open put
180 /// it at the mercy of the project moving on, and it duly broke when 201 /// it at the mercy of the project moving on, and it duly broke when
@@ -200,8 +221,6 @@ final class LiveSmokeUITests: XCTestCase {
200 ) 221 )
201 } 222 }
202 223
203 static let fixtureRepo = "krz/gitbay-ios"
204
205 /// The MR flow needs a source branch carrying a commit main does not 224 /// The MR flow needs a source branch carrying a commit main does not
206 /// have. Nothing in the app or the API can create a branch — that is 225 /// have. Nothing in the app or the API can create a branch — that is
207 /// a push — so this only checks, and says so plainly rather than 226 /// a push — so this only checks, and says so plainly rather than
@@ -214,9 +233,9 @@ final class LiveSmokeUITests: XCTestCase {
214 else { return } // no token, or the instance is unreachable 233 else { return } // no token, or the instance is unreachable
215 let names = branches.compactMap { $0["name"] as? String } 234 let names = branches.compactMap { $0["name"] as? String }
216 try XCTSkipUnless(names.contains("ui-smoke"), """ 235 try XCTSkipUnless(names.contains("ui-smoke"), """
217 krz/gitbay-ios has no ui-smoke branch. The MR flow opens a \ 236 \(Self.fixtureRepo) has no ui-smoke branch. The MR flows open \
218 merge request from it; recreate it with a commit main does \ 237 one merge request each way between it and main, so each side \
219 not have (see .gitbay/ui-smoke.md on that branch). 238 needs a commit the other lacks; smoke-account.sh seeds both.
220 """) 239 """)
221 } 240 }
222 241
@@ -265,9 +284,9 @@ final class LiveSmokeUITests: XCTestCase {
265 /// MR create, MR close. One test so the flows share navigation and 284 /// MR create, MR close. One test so the flows share navigation and
266 /// the writes happen exactly once. 285 /// the writes happen exactly once.
267 func testCreationAndEditingFlows() throws { 286 func testCreationAndEditingFlows() throws {
268 openRepo("krz/gitbay-ios") 287 openRepo(Self.fixtureRepo)
269 288
270 // --- the fixture issue: milestone picker assigns v1.0.0 --- 289 // --- the fixture issue: milestone picker assigns the fixture ---
271 app.staticTexts["Issues"].firstMatch.tap() 290 app.staticTexts["Issues"].firstMatch.tap()
272 let issueRow = app.staticTexts 291 let issueRow = app.staticTexts
273 .containing(NSPredicate(format: "label CONTAINS 'ui-smoke fixture'")).firstMatch 292 .containing(NSPredicate(format: "label CONTAINS 'ui-smoke fixture'")).firstMatch
@@ -280,11 +299,11 @@ final class LiveSmokeUITests: XCTestCase {
280 XCTAssertTrue(milestoneMenu.waitForExistence(timeout: 10)) 299 XCTAssertTrue(milestoneMenu.waitForExistence(timeout: 10))
281 milestoneMenu.tap() 300 milestoneMenu.tap()
282 let milestoneChoice = app.buttons 301 let milestoneChoice = app.buttons
283 .containing(NSPredicate(format: "label BEGINSWITH 'v1.0.0'")).firstMatch 302 .containing(NSPredicate(format: "label BEGINSWITH %@", Self.fixtureMilestone)).firstMatch
284 XCTAssertTrue(milestoneChoice.waitForExistence(timeout: 5), "milestone menu did not open") 303 XCTAssertTrue(milestoneChoice.waitForExistence(timeout: 5), "milestone menu did not open")
285 milestoneChoice.tap() 304 milestoneChoice.tap()
286 // The reload renders the assigned milestone in the header row. 305 // The reload renders the assigned milestone in the header row.
287 XCTAssertTrue(app.staticTexts["v1.0.0"].firstMatch 306 XCTAssertTrue(app.staticTexts[Self.fixtureMilestone].firstMatch
288 .waitForExistence(timeout: 10), "milestone not shown after assign") 307 .waitForExistence(timeout: 10), "milestone not shown after assign")
289 308
290 // --- the fixture issue: edit sheet saves (content unchanged) --- 309 // --- the fixture issue: edit sheet saves (content unchanged) ---
@@ -415,18 +434,17 @@ final class LiveSmokeUITests: XCTestCase {
415 keys.tap() 434 keys.tap()
416 } 435 }
417 436
418 /// The edit flow saves a change to a real file. Seeding a 437 /// Seeding a repository's first commit is a push, which no test can
419 /// repository's first commit is a push, which no test can make, so 438 /// make, so this only checks — and says why, rather than failing
420 /// this only checks — and says why, rather than failing later at 439 /// later at "ios-smoke/ui-smoke-edit not in the repo list".
421 /// "ios-smoke/ui-smoke-edit not in the repo list".
422 private func requireFixtureRepo() throws { 440 private func requireFixtureRepo() throws {
423 guard let data = readCommand(["repo", "show", Self.editRepo]), 441 guard let data = readCommand(["repo", "show", Self.fixtureRepo]),
424 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any] 442 let envelope = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
425 else { return } // no token, or the instance is unreachable 443 else { return } // no token, or the instance is unreachable
426 try XCTSkipUnless(envelope["data"] != nil, """ 444 try XCTSkipUnless(envelope["data"] != nil, """
427 \(Self.editRepo) is missing. The edit flow saves a change to \ 445 \(Self.fixtureRepo) is missing. The write flows work in it; \
428 notes.txt in it; gitbayUITests/smoke-account.sh seeds it (or \ 446 gitbayUITests/smoke-account.sh seeds it (or push it by hand, \
429 push it by hand, see that script) and run again. 447 see that script) and run again.
430 """) 448 """)
431 } 449 }
432 450
@@ -1186,7 +1204,7 @@ extension LiveSmokeUITests {
1186 // previous query, and clearing it from the harness is unreliable. 1204 // previous query, and clearing it from the harness is unreliable.
1187 app.terminate() 1205 app.terminate()
1188 app.launch() 1206 app.launch()
1189 openRepo(Self.editRepo) 1207 openRepo(Self.fixtureRepo)
1190 app.staticTexts["Files"].firstMatch.tap() 1208 app.staticTexts["Files"].firstMatch.tap()
1191 let notes = app.staticTexts["notes.txt"].firstMatch 1209 let notes = app.staticTexts["notes.txt"].firstMatch
1192 XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree") 1210 XCTAssertTrue(notes.waitForExistence(timeout: 15), "notes.txt not in the tree")
gitbayUITests/smoke-account.sh +53 −16
@@ -4,7 +4,9 @@
4# what already exists, so a failed run is rerun as is. It creates the 4# what already exists, so a failed run is rerun as is. It creates the
5# account with a fresh SSH key (~/.ssh/gitbay-ios-smoke), makes it a krz 5# account with a fresh SSH key (~/.ssh/gitbay-ios-smoke), makes it a krz
6# admin, sets its profile description, registers a PGP key, and pushes 6# admin, sets its profile description, registers a PGP key, and pushes
7# the ios-smoke/ui-smoke-edit fixture that testBlameAndEditFlows edits. 7# the ios-smoke/ui-smoke-edit fixture the write flows work in: notes.txt
8# for testBlameAndEditFlows, and a ui-smoke branch divergent from main
9# for the merge-request flows.
8# LiveSmokeUITests asserts on the same values, so keep the two in step. 10# LiveSmokeUITests asserts on the same values, so keep the two in step.
9set -eu 11set -eu
10 12
@@ -39,37 +41,72 @@ if ! as_smoke pgp list --json | grep -q "$pgp_email"; then
39 rm -rf "$gnupg" 41 rm -rf "$gnupg"
40fi 42fi
41 43
42if as_smoke repo show "$user/ui-smoke-edit" --json > /dev/null 2>&1; then 44repo=$user/ui-smoke-edit
43 echo "$user/ui-smoke-edit exists" 45url=ssh://git@gitbay.org/$repo.git
44 exit 0
45fi
46as_smoke repo create "$user/ui-smoke-edit"
47work=$(mktemp -d) 46work=$(mktemp -d)
48git -C "$work" init -q -b main 47trap 'rm -rf "$work"' EXIT
49cat > "$work/README.md" <<'EOF' 48
49commit_in_work() {
50 git -C "$work" add -A
51 git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" \
52 -c commit.gpgsign=false commit -q -m "$1"
53}
54
55push_work() {
56 GIT_SSH_COMMAND="$smoke_ssh" git -C "$work" push -q "$url" "$1"
57}
58
59if as_smoke repo show "$repo" --json > /dev/null 2>&1; then
60 echo "$repo exists"
61 GIT_SSH_COMMAND="$smoke_ssh" git clone -q "$url" "$work"
62else
63 as_smoke repo create "$repo"
64 git -C "$work" init -q -b main
65 cat > "$work/README.md" <<'EOF'
50# ui-smoke-edit 66# ui-smoke-edit
51 67
52A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt` 68A fixture repository for the gitbay-ios live UI smoke suite. `notes.txt`
53is edited by `testBlameAndEditFlows` on every run. 69is edited by `testBlameAndEditFlows` on every run, and the `ui-smoke`
70branch is what the merge-request flows open merge requests from.
54 71
55Nothing in the app or the JSON API can seed a repository's first commit 72Nothing in the app or the JSON API can seed a repository's first commit
56(that is a push), so the suite only checks that this repo exists and 73(that is a push), so the suite only checks that this repo exists and
57skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios 74skips when it does not. `gitbayUITests/smoke-account.sh` in krz/gitbay-ios
58is what created it. 75is what created it.
59EOF 76EOF
60cat > "$work/notes.txt" <<'EOF' 77 cat > "$work/notes.txt" <<'EOF'
61Scratch content for the live UI smoke suite. 78Scratch content for the live UI smoke suite.
62 79
63testBlameAndEditFlows opens this file and saves an edit through the app, 80testBlameAndEditFlows opens this file and saves an edit through the app,
64which is how the file-editing path is verified end to end. The text does 81which is how the file-editing path is verified end to end. The text does
65not matter; the file existing does. 82not matter; the file existing does.
66EOF 83EOF
67git -C "$work" add -A 84 commit_in_work 'fixture repo for the live smoke suite'
68git -C "$work" -c user.name="$user" -c user.email="$user@gitbay.org" -c commit.gpgsign=false \ 85 push_work main
69 commit -q -m 'fixture repo for the live smoke suite' 86fi
70GIT_SSH_COMMAND="$smoke_ssh" \ 87
71 git -C "$work" push -q "ssh://git@gitbay.org/$user/ui-smoke-edit.git" main 88# The MR flows open one merge request each way between ui-smoke and
72rm -rf "$work" 89# main, so each side needs a commit the other lacks. Only a push can
90# make a branch, which is why this lives here and not in the suite.
91if ! as_smoke repo refs "$repo" --json | grep -q '"ui-smoke"'; then
92 git -C "$work" checkout -q -b ui-smoke
93 cat > "$work/ui-smoke.md" <<'EOF'
94This branch exists so the live UI smoke suite has a merge request to
95open. It is never merged: the suite opens merge requests both ways
96between it and main, and closes them again.
97EOF
98 commit_in_work 'ui-smoke: a commit main does not have'
99 push_work ui-smoke
100
101 git -C "$work" checkout -q main
102 cat > "$work/main.md" <<'EOF'
103The counterpart of ui-smoke.md: a commit on main that the ui-smoke
104branch does not have, so a merge request from main to ui-smoke has
105something to carry.
106EOF
107 commit_in_work 'main: a commit ui-smoke does not have'
108 push_work main
109fi
73 110
74echo "Mint the suite's token with:" 111echo "Mint the suite's token with:"
75echo " $smoke_ssh git@gitbay.org token create --name ui-smoke --scope full --ttl 30d" 112echo " $smoke_ssh git@gitbay.org token create --name ui-smoke --scope full --ttl 30d"