mirror of
https://codeberg.org/secana/Forji.git
synced 2026-06-16 05:13:55 -07:00
test: stream seed progress and add a docker exec timeout (#69)
The integration seeder gave no visible progress and could hang indefinitely. A `docker compose exec` for the admin-user step wedged on a transient Docker-on-macOS flake, and `DockerExec.run` waited on it with no timeout, blocking the whole UI test suite forever with no output (stdout was block-buffered, so the existing phase prints never flushed). - Line-buffer the seeder's stdout so phase progress streams live instead of being dumped all at once when stdout is a pipe. - Add numbered "[n/7] <phase> (Ns elapsed)" headers for a clear progress and timing signal. - Add a 60s timeout to `docker compose exec` and retry the admin-user step, so a hung exec fails fast and recovers instead of wedging the suite. Reviewed-on: https://codeberg.org/secana/Forji/pulls/69
This commit is contained in:
parent
b1942df58d
commit
2a679140e6
4 changed files with 40 additions and 3 deletions
|
|
@ -4,6 +4,7 @@ enum DockerExec {
|
|||
static func run(
|
||||
composeFile: String, service: String,
|
||||
command: [String],
|
||||
timeout: TimeInterval = 60,
|
||||
) throws {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||
|
|
@ -16,8 +17,21 @@ enum DockerExec {
|
|||
process.standardError = stderrPipe
|
||||
process.standardOutput = FileHandle.nullDevice
|
||||
|
||||
let didExit = DispatchSemaphore(value: 0)
|
||||
process.terminationHandler = { _ in didExit.signal() }
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// Enforce a timeout so a hung `docker compose exec` (a known Docker-on-macOS flake)
|
||||
// fails fast and can be retried, instead of blocking the whole seed indefinitely.
|
||||
if didExit.wait(timeout: .now() + timeout) == .timedOut {
|
||||
process.terminate()
|
||||
_ = didExit.wait(timeout: .now() + 5)
|
||||
throw SeedError.dockerExecTimedOut(
|
||||
command: command.joined(separator: " "),
|
||||
seconds: timeout,
|
||||
)
|
||||
}
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import Foundation
|
|||
@main
|
||||
struct ForgejeSeed {
|
||||
static func main() async throws {
|
||||
// Line-buffer stdout so seed progress streams live instead of being held in a
|
||||
// block buffer (and dumped all at once) when stdout is a pipe rather than a TTY.
|
||||
setvbuf(stdout, nil, _IOLBF, 0)
|
||||
|
||||
let args = CommandLine.arguments
|
||||
guard args.count >= 3 else {
|
||||
throw SeedError.missingArguments
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Foundation
|
|||
enum SeedError: LocalizedError {
|
||||
case missingArguments
|
||||
case dockerExecFailed(command: String, exitCode: Int32, stderr: String)
|
||||
case dockerExecTimedOut(command: String, seconds: TimeInterval)
|
||||
case retryExhausted(operation: String, attempts: Int)
|
||||
|
||||
var errorDescription: String? {
|
||||
|
|
@ -11,6 +12,8 @@ enum SeedError: LocalizedError {
|
|||
"Usage: forgejo-seed <base-url> <service-name> [compose-file]"
|
||||
case let .dockerExecFailed(command, exitCode, stderr):
|
||||
"Docker exec failed (\(exitCode)): \(command)\n\(stderr)"
|
||||
case let .dockerExecTimedOut(command, seconds):
|
||||
"Docker exec timed out after \(Int(seconds))s: \(command)"
|
||||
case let .retryExhausted(operation, attempts):
|
||||
"\(operation) failed after \(attempts) attempts"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,30 @@ struct Seeder {
|
|||
allowSelfSignedCertificates: true,
|
||||
)
|
||||
|
||||
let start = Date()
|
||||
let total = 7
|
||||
func step(_ number: Int, _ label: String) {
|
||||
let elapsed = Int(Date().timeIntervalSince(start))
|
||||
print("\n[\(number)/\(total)] \(label) (\(elapsed)s elapsed)")
|
||||
}
|
||||
|
||||
step(1, "Creating admin user")
|
||||
try await withRetry(maxAttempts: 3, delay: .seconds(2), operation: "create admin user") {
|
||||
try createAdminUser()
|
||||
}
|
||||
step(2, "Creating API users")
|
||||
try await createAPIUsers(adminClient: adminClient)
|
||||
step(3, "Creating repositories")
|
||||
try await createRepositories(adminClient: adminClient)
|
||||
step(4, "Creating issues, files, and metadata")
|
||||
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
||||
step(5, "Creating pull requests")
|
||||
try await createPullRequests(testbotClient: testbotClient)
|
||||
step(6, "Setting pull request metadata")
|
||||
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
||||
step(7, "Creating API token")
|
||||
try await createAPIToken(adminClient: adminClient)
|
||||
print("\nSeed data created successfully.")
|
||||
print("\nSeed data created successfully (\(Int(Date().timeIntervalSince(start)))s total).")
|
||||
}
|
||||
|
||||
// MARK: - Phase 1: Create users
|
||||
|
|
|
|||
Loading…
Reference in a new issue