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(
|
static func run(
|
||||||
composeFile: String, service: String,
|
composeFile: String, service: String,
|
||||||
command: [String],
|
command: [String],
|
||||||
|
timeout: TimeInterval = 60,
|
||||||
) throws {
|
) throws {
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
||||||
|
|
@ -16,8 +17,21 @@ enum DockerExec {
|
||||||
process.standardError = stderrPipe
|
process.standardError = stderrPipe
|
||||||
process.standardOutput = FileHandle.nullDevice
|
process.standardOutput = FileHandle.nullDevice
|
||||||
|
|
||||||
|
let didExit = DispatchSemaphore(value: 0)
|
||||||
|
process.terminationHandler = { _ in didExit.signal() }
|
||||||
|
|
||||||
try process.run()
|
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 {
|
if process.terminationStatus != 0 {
|
||||||
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import Foundation
|
||||||
@main
|
@main
|
||||||
struct ForgejeSeed {
|
struct ForgejeSeed {
|
||||||
static func main() async throws {
|
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
|
let args = CommandLine.arguments
|
||||||
guard args.count >= 3 else {
|
guard args.count >= 3 else {
|
||||||
throw SeedError.missingArguments
|
throw SeedError.missingArguments
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import Foundation
|
||||||
enum SeedError: LocalizedError {
|
enum SeedError: LocalizedError {
|
||||||
case missingArguments
|
case missingArguments
|
||||||
case dockerExecFailed(command: String, exitCode: Int32, stderr: String)
|
case dockerExecFailed(command: String, exitCode: Int32, stderr: String)
|
||||||
|
case dockerExecTimedOut(command: String, seconds: TimeInterval)
|
||||||
case retryExhausted(operation: String, attempts: Int)
|
case retryExhausted(operation: String, attempts: Int)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
|
|
@ -11,6 +12,8 @@ enum SeedError: LocalizedError {
|
||||||
"Usage: forgejo-seed <base-url> <service-name> [compose-file]"
|
"Usage: forgejo-seed <base-url> <service-name> [compose-file]"
|
||||||
case let .dockerExecFailed(command, exitCode, stderr):
|
case let .dockerExecFailed(command, exitCode, stderr):
|
||||||
"Docker exec failed (\(exitCode)): \(command)\n\(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):
|
case let .retryExhausted(operation, attempts):
|
||||||
"\(operation) failed after \(attempts) attempts"
|
"\(operation) failed after \(attempts) attempts"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,30 @@ struct Seeder {
|
||||||
allowSelfSignedCertificates: true,
|
allowSelfSignedCertificates: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
try createAdminUser()
|
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)
|
try await createAPIUsers(adminClient: adminClient)
|
||||||
|
step(3, "Creating repositories")
|
||||||
try await createRepositories(adminClient: adminClient)
|
try await createRepositories(adminClient: adminClient)
|
||||||
|
step(4, "Creating issues, files, and metadata")
|
||||||
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
||||||
|
step(5, "Creating pull requests")
|
||||||
try await createPullRequests(testbotClient: testbotClient)
|
try await createPullRequests(testbotClient: testbotClient)
|
||||||
|
step(6, "Setting pull request metadata")
|
||||||
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
|
||||||
|
step(7, "Creating API token")
|
||||||
try await createAPIToken(adminClient: adminClient)
|
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
|
// MARK: - Phase 1: Create users
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue