mirror of
https://github.com/apple/container.git
synced 2026-07-12 04:27:05 +00:00
831a6bfe1c
- Closes #1547. - A negative progress value (e.g. from a race in progress events) would produce a negative barLength and crash inside String(repeating:count:). Wrap the computed length in max(0, ...) and add a regression test that calls set(size: -10) and exercises draw(state:detail:).
1235 lines
39 KiB
Swift
1235 lines
39 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import XCTest
|
|
|
|
@testable import TerminalProgress
|
|
|
|
final class ProgressBarTests: XCTestCase {
|
|
func testSpinner() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task"
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testSpinnerFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task"
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task [0s]")
|
|
}
|
|
|
|
func testNoSpinner() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task [0s]")
|
|
}
|
|
|
|
func testNoSpinnerFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task [0s]")
|
|
}
|
|
|
|
func testNoTasks() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testTasks() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testTasksAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(tasks: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testTasksSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(tasks: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testTotalTasks() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
totalTasks: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
|
|
}
|
|
|
|
func testTotalTasksFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
totalTasks: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ [0/2] Task [0s]")
|
|
}
|
|
|
|
func testTotalTasksAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
totalTasks: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(totalTasks: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
|
|
}
|
|
|
|
func testTotalTasksSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
totalTasks: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(totalTasks: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
|
|
}
|
|
|
|
func testTotalTasksInvalid() throws {
|
|
do {
|
|
let _ = try ProgressConfig(description: "test", totalTasks: 0)
|
|
} catch ProgressConfig.Error.invalid(_) {
|
|
return
|
|
}
|
|
XCTFail("expected ProgressConfig.Error.invalid")
|
|
}
|
|
|
|
func testDescription() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task"
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testNoDescription() async throws {
|
|
let config = try ProgressConfig()
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ [0s]")
|
|
}
|
|
|
|
func testNoPercent() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: false,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testPercentHidden() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testPercentItems() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: true,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% [0s]")
|
|
}
|
|
|
|
func testPercentItemsFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: true,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% [0s]")
|
|
}
|
|
|
|
func testPercentSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: true,
|
|
showSize: false,
|
|
showSpeed: false,
|
|
totalSize: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% [0s]")
|
|
}
|
|
|
|
func testPercentSizeFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showPercent: true,
|
|
showSize: false,
|
|
showSpeed: false,
|
|
totalSize: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% [0s]")
|
|
}
|
|
|
|
func testNoProgressBar() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: false,
|
|
totalItems: 2,
|
|
width: 57
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% [0s]")
|
|
}
|
|
|
|
func testProgressBar() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalItems: 2,
|
|
width: 57
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 50% |██ | [0s]")
|
|
}
|
|
|
|
func testProgressBarFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalItems: 2,
|
|
width: 57
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 100% |███| [0s]")
|
|
}
|
|
|
|
func testProgressBarMinWidth() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalItems: 2,
|
|
width: 13
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 50% | | [0s]")
|
|
}
|
|
|
|
func testProgressBarMinWidthFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalItems: 2,
|
|
width: 13
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 100% |█| [0s]")
|
|
}
|
|
|
|
func testNoItems() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testItemsZero() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testItemsAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task (1 it) [0s]")
|
|
}
|
|
|
|
func testItemsAddFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task [0s]")
|
|
}
|
|
|
|
func testItemsSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task (2 it) [0s]")
|
|
}
|
|
|
|
func testTotalItemsZeroItems() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 0% [0s]")
|
|
}
|
|
|
|
func testTotalItems() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
|
|
}
|
|
|
|
func testTotalItemsFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% (2 it) [0s]")
|
|
}
|
|
|
|
func testTotalItemsAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.add(totalItems: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
|
|
}
|
|
|
|
func testTotalItemsSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.set(totalItems: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
|
|
}
|
|
|
|
func testTotalItemsInvalid() throws {
|
|
do {
|
|
let _ = try ProgressConfig(description: "test", totalItems: 0)
|
|
} catch ProgressConfig.Error.invalid(_) {
|
|
return
|
|
}
|
|
XCTFail("expected ProgressConfig.Error.invalid")
|
|
}
|
|
|
|
func testNoSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testSizeZero() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testSizeAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(size: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task (1 byte) [0s]")
|
|
}
|
|
|
|
func testSizeAddFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.add(size: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task [0s]")
|
|
}
|
|
|
|
func testSizeSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task (2 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeZeroSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
totalSize: 1
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 0% [0s]")
|
|
}
|
|
|
|
func testTotalSizeDifferentUnits() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 byte/2 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeDifferentUnitsFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% (2 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeSameUnits() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeSameUnitsFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% (4 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeAdd() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 3
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
progress.add(totalSize: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeSet() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 3
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
progress.set(totalSize: 4)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testTotalSizeInvalid() throws {
|
|
do {
|
|
let _ = try ProgressConfig(description: "test", totalSize: 0)
|
|
} catch ProgressConfig.Error.invalid(_) {
|
|
return
|
|
}
|
|
XCTFail("expected ProgressConfig.Error.invalid")
|
|
}
|
|
|
|
func testItemsAndSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalItems: 2,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it, 2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testItemsAndSizeFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalItems: 2,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.set(size: 2)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "✔ Task 100% (2 it, 4 bytes) [0s]")
|
|
}
|
|
|
|
func testNoSpeed() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpeed: false,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testSpeed() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpeed: true,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertTrue(output.contains("/s"))
|
|
}
|
|
|
|
func testSpeedFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpeed: true,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertFalse(output.contains("/s"))
|
|
}
|
|
|
|
func testItemsSizeAndSpeed() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
showSize: true,
|
|
showSpeed: true,
|
|
totalItems: 2,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertTrue(output.contains("1 of 2 it, 2/4 bytes"))
|
|
XCTAssertTrue(output.contains("/s"))
|
|
}
|
|
|
|
func testItemsSizeAndSpeedFinish() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
showSize: true,
|
|
showSpeed: true,
|
|
totalItems: 2,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.set(size: 2)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertTrue(output.contains("2 it, 4 bytes"))
|
|
XCTAssertFalse(output.contains("/s"))
|
|
}
|
|
|
|
func testNoTime() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTime: false
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task")
|
|
}
|
|
|
|
func testTime() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTime: true
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
sleep(1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [1s]")
|
|
}
|
|
|
|
func testIgnoreSmallSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
ignoreSmallSize: true,
|
|
totalSize: 4
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testProgressBarSizeExceedsTotal() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalSize: 50
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 100)
|
|
let _ = progress.draw()
|
|
}
|
|
|
|
func testProgressBarNegativeValue() async throws {
|
|
// Regression test: a negative progress value (e.g. from a race in progress events)
|
|
// must not cause String(repeating:count:) to be called with a negative count.
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalSize: 50,
|
|
width: 57
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: -10)
|
|
// draw(state:detail:) should clamp barLength to [0, remainingWidth] and not crash.
|
|
let state = progress.state.withLock { $0 }
|
|
let _ = progress.draw(state: state, detail: .full)
|
|
}
|
|
|
|
func testItemsName() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
itemsName: "files",
|
|
showItems: true,
|
|
totalItems: 2
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "⠋ Task 50% (1 of 2 files) [0s]")
|
|
}
|
|
|
|
func testPlainModeConfig() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
outputMode: .plain
|
|
)
|
|
XCTAssertEqual(config.outputMode, .plain)
|
|
}
|
|
|
|
func testPlainModeDraw() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task [0s]")
|
|
}
|
|
|
|
func testPlainModeDrawWithTasks() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
showTasks: true,
|
|
totalTasks: 2,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "[0/2] Task [0s]")
|
|
}
|
|
|
|
func testPlainModeDrawWithPercent() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
showItems: true,
|
|
totalItems: 2,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 50% (1 of 2 it) [0s]")
|
|
}
|
|
|
|
func testPlainModeFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
showTasks: true,
|
|
totalTasks: 2,
|
|
clearOnFinish: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(tasks: 2)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "[2/2] Task [0s]")
|
|
}
|
|
|
|
func testPlainModeWithSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 4,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 50% (2/4 bytes) [0s]")
|
|
}
|
|
|
|
func testPlainModeNoProgressBar() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
showProgressBar: false,
|
|
totalItems: 2,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
XCTAssertEqual(output, "Task 50% [0s]")
|
|
}
|
|
|
|
func testPlainModeNoAnsiEscapes() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSpinner: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertFalse(output.contains("\u{001B}"))
|
|
}
|
|
|
|
func testPlainModeTerminalOutput() async throws {
|
|
let pipe = Pipe()
|
|
let config = try ProgressConfig(
|
|
terminal: pipe.fileHandleForWriting,
|
|
description: "Task",
|
|
showSpinner: false,
|
|
clearOnFinish: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.render(force: true)
|
|
progress.finish()
|
|
try pipe.fileHandleForWriting.close()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(decoding: data, as: UTF8.self)
|
|
let lines = output.components(separatedBy: "\n").filter { !$0.isEmpty }
|
|
// Expect exactly 2 lines: one from render, one from finish
|
|
XCTAssertEqual(lines.count, 2)
|
|
XCTAssertTrue(lines[0].contains("Task"))
|
|
XCTAssertTrue(lines[1].contains("Task"))
|
|
}
|
|
|
|
func testPlainModeTerminalOutputNoAnsiEscapes() async throws {
|
|
let pipe = Pipe()
|
|
let config = try ProgressConfig(
|
|
terminal: pipe.fileHandleForWriting,
|
|
description: "Task",
|
|
showSpinner: false,
|
|
clearOnFinish: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.render(force: true)
|
|
progress.finish()
|
|
try pipe.fileHandleForWriting.close()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(decoding: data, as: UTF8.self)
|
|
XCTAssertFalse(output.contains("\u{001B}"))
|
|
}
|
|
|
|
func testPlainModeTerminalOutputUsesNewlines() async throws {
|
|
let pipe = Pipe()
|
|
let config = try ProgressConfig(
|
|
terminal: pipe.fileHandleForWriting,
|
|
description: "Task",
|
|
showSpinner: false,
|
|
clearOnFinish: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.render(force: true)
|
|
progress.finish()
|
|
try pipe.fileHandleForWriting.close()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(decoding: data, as: UTF8.self)
|
|
// Plain mode should use newlines, not carriage returns
|
|
XCTAssertFalse(output.contains("\r"))
|
|
XCTAssertTrue(output.contains("\n"))
|
|
}
|
|
|
|
func testPlainModeDefaultClearOnFinishOmitsFinalLine() async throws {
|
|
let pipe = Pipe()
|
|
let config = try ProgressConfig(
|
|
terminal: pipe.fileHandleForWriting,
|
|
description: "Task",
|
|
showSpinner: false,
|
|
outputMode: .plain
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.render(force: true)
|
|
progress.finish()
|
|
try pipe.fileHandleForWriting.close()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(decoding: data, as: UTF8.self)
|
|
let lines = output.components(separatedBy: "\n").filter { !$0.isEmpty }
|
|
XCTAssertEqual(lines.count, 1)
|
|
XCTAssertEqual(lines.first, "Task [0s]")
|
|
}
|
|
|
|
func testOutputModeDefaultIsAnsi() async throws {
|
|
let config = try ProgressConfig(description: "Task")
|
|
XCTAssertEqual(config.outputMode, .ansi)
|
|
}
|
|
|
|
// MARK: - Color mode tests
|
|
|
|
func testColorModeConfig() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
XCTAssertEqual(config.outputMode, .color)
|
|
}
|
|
|
|
func testVisibleLengthPlainText() async throws {
|
|
let text = "hello"
|
|
XCTAssertEqual(text.visibleLength, 5)
|
|
}
|
|
|
|
func testVisibleLengthWithAnsiCodes() async throws {
|
|
let text = "\u{001B}[36mhello\u{001B}[0m"
|
|
XCTAssertEqual(text.visibleLength, 5)
|
|
}
|
|
|
|
func testVisibleLengthEmptyString() async throws {
|
|
XCTAssertEqual("".visibleLength, 0)
|
|
}
|
|
|
|
func testColorModeDraw() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "⠋ Task [0s]")
|
|
}
|
|
|
|
func testColorModeDrawContainsAnsiCodes() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertTrue(output.contains("\u{001B}["))
|
|
XCTAssertTrue(output.contains(EscapeSequence.cyan)) // spinner
|
|
XCTAssertTrue(output.contains(EscapeSequence.bold)) // description
|
|
XCTAssertTrue(output.contains(EscapeSequence.dim)) // time
|
|
XCTAssertTrue(output.contains(EscapeSequence.reset)) // reset
|
|
}
|
|
|
|
func testColorModeDrawFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "✔ Task [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.green)) // done icon
|
|
}
|
|
|
|
func testColorModeDrawWithTasks() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
totalTasks: 2,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "⠋ [0/2] Task [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.cyan)) // tasks
|
|
}
|
|
|
|
func testColorModeDrawWithPercent() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 2,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "⠋ Task 50% (1 of 2 it) [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.yellow)) // in-progress percent
|
|
}
|
|
|
|
func testColorModeDrawWithPercentFinished() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showItems: true,
|
|
totalItems: 2,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
progress.finish()
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "✔ Task 100% (2 it) [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.green)) // finished percent
|
|
}
|
|
|
|
func testColorModeDrawWithSize() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showSize: true,
|
|
showSpeed: false,
|
|
totalSize: 4,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(size: 2)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "⠋ Task 50% (2/4 bytes) [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.dim)) // parens content
|
|
}
|
|
|
|
func testColorModeDrawVisibleLengthMatchesContent() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
showItems: true,
|
|
totalTasks: 2,
|
|
totalItems: 4,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 2)
|
|
let colorOutput = progress.draw()
|
|
|
|
let plainConfig = try ProgressConfig(
|
|
description: "Task",
|
|
showTasks: true,
|
|
showItems: true,
|
|
totalTasks: 2,
|
|
totalItems: 4
|
|
)
|
|
let plainProgress = ProgressBar(config: plainConfig)
|
|
plainProgress.set(items: 2)
|
|
let plainOutput = plainProgress.draw()
|
|
|
|
XCTAssertEqual(colorOutput.visibleLength, plainOutput.count)
|
|
}
|
|
|
|
func testColorModeNoAnsiCodesInContent() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertFalse(stripped.contains("\u{001B}"))
|
|
}
|
|
|
|
func testColorModeDrawWithProgressBar() async throws {
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
showProgressBar: true,
|
|
totalItems: 2,
|
|
width: 57,
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
progress.set(items: 1)
|
|
let output = progress.draw()
|
|
let stripped = output.replacingOccurrences(
|
|
of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
|
|
)
|
|
XCTAssertEqual(stripped, "Task 50% |██ | [0s]")
|
|
XCTAssertTrue(output.contains(EscapeSequence.green))
|
|
}
|
|
|
|
func testColorModeRequiresTTY() async throws {
|
|
let pipe = Pipe()
|
|
let config = try ProgressConfig(
|
|
terminal: pipe.fileHandleForWriting,
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
// Pipe is not a TTY, so render should produce no output (same as ansi)
|
|
progress.render(force: true)
|
|
progress.finish()
|
|
try pipe.fileHandleForWriting.close()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
XCTAssertTrue(data.isEmpty)
|
|
}
|
|
|
|
func testColorModeDrawOutputContainsColorCodes() async throws {
|
|
// Verify draw() includes ANSI codes even without a TTY
|
|
// (draw is separate from terminal rendering)
|
|
let config = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
let progress = ProgressBar(config: config)
|
|
let output = progress.draw()
|
|
XCTAssertTrue(output.contains(EscapeSequence.cyan))
|
|
XCTAssertTrue(output.contains(EscapeSequence.bold))
|
|
XCTAssertTrue(output.contains(EscapeSequence.dim))
|
|
XCTAssertTrue(output.contains(EscapeSequence.reset))
|
|
}
|
|
|
|
func testColorModeRenderTerminatorIsCarriageReturn() async throws {
|
|
// Color mode is TTY-only so we cannot capture its raw terminal output via a pipe.
|
|
// Instead, verify that plain mode emits \n (newlines) while color mode shares the
|
|
// ansi code path which emits \r (carriage returns). We confirm this by checking
|
|
// that plain output uses \n and that color mode is distinct from plain.
|
|
let plainPipe = Pipe()
|
|
let plainConfig = try ProgressConfig(
|
|
terminal: plainPipe.fileHandleForWriting,
|
|
description: "Task",
|
|
showSpinner: false,
|
|
clearOnFinish: false,
|
|
outputMode: .plain
|
|
)
|
|
let plainProgress = ProgressBar(config: plainConfig)
|
|
plainProgress.render(force: true)
|
|
plainProgress.finish()
|
|
try plainPipe.fileHandleForWriting.close()
|
|
|
|
let plainData = plainPipe.fileHandleForReading.readDataToEndOfFile()
|
|
let plainOutput = String(decoding: plainData, as: UTF8.self)
|
|
// Plain mode uses \n terminators
|
|
XCTAssertTrue(plainOutput.contains("\n"))
|
|
XCTAssertFalse(plainOutput.contains("\r"))
|
|
|
|
// Color mode follows the ansi path (not plain), so it uses \r
|
|
let colorConfig = try ProgressConfig(
|
|
description: "Task",
|
|
outputMode: .color
|
|
)
|
|
XCTAssertNotEqual(colorConfig.outputMode, .plain)
|
|
}
|
|
|
|
func testOutputModeDefaultIsNotColor() async throws {
|
|
let config = try ProgressConfig(description: "Task")
|
|
XCTAssertNotEqual(config.outputMode, .color)
|
|
XCTAssertEqual(config.outputMode, .ansi)
|
|
}
|
|
}
|