Add captureStream(…) to observe stdout & stderr in tests.

Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
This commit is contained in:
Ross Goldberg 2024-10-02 09:49:30 -04:00
parent dccac33abb
commit 265326dede
No known key found for this signature in database
10 changed files with 85 additions and 153 deletions

View file

@ -13,63 +13,6 @@ import Foundation
/// Terminal Control Sequence Indicator
let csi = "\u{001B}["
#if DEBUG
var printObserver: ((String) -> Void)?
// Override global print for testability.
// See masTests/OutputListener.swift.
func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n"
) {
if let observer = printObserver {
let output =
items
.map { "\($0)" }
.joined(separator: separator)
.appending(terminator)
observer(output)
}
var prefix = ""
for item in items {
Swift.print(prefix, terminator: "")
Swift.print(item, terminator: "")
prefix = separator
}
Swift.print(terminator, terminator: "")
}
func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n",
to output: inout some TextOutputStream
) {
if let observer = printObserver {
let output =
items
.map { "\($0)" }
.joined(separator: separator)
.appending(terminator)
observer(output)
}
var prefix = ""
for item in items {
Swift.print(prefix, terminator: "", to: &output)
Swift.print(item, terminator: "", to: &output)
prefix = separator
}
Swift.print(terminator, terminator: "", to: &output)
}
#endif
private var standardError = FileHandle.standardError
extension FileHandle: TextOutputStream {
@ -121,3 +64,30 @@ func clearLine() {
print("\(csi)2K\(csi)0G", terminator: "")
fflush(stdout)
}
func captureStream(
_ stream: UnsafeMutablePointer<FILE>,
encoding: String.Encoding = .utf8,
_ block: @escaping () throws -> Void
) throws -> String {
let originalFd = fileno(stream)
let duplicateFd = dup(originalFd)
defer {
close(duplicateFd)
}
let pipe = Pipe()
dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd)
do {
defer {
fflush(stream)
dup2(duplicateFd, originalFd)
pipe.fileHandleForWriting.closeFile()
}
try block()
}
return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? ""
}

View file

@ -6,6 +6,7 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@ -47,12 +48,12 @@ public class InfoSpec: QuickSpec {
version: "1.0"
)
storeSearch.apps[mockResult.trackId] = mockResult
let output = OutputListener()
expect {
try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch)
try captureStream(stdout) {
try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch)
}
}
.toNot(throwError())
expect(output.contents) == """
== """
Awesome App 1.0 [2.0]
By: Awesome Dev
Released: 2019-01-07

View file

@ -6,6 +6,7 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@ -19,9 +20,11 @@ public class ListSpec: QuickSpec {
describe("list command") {
it("lists apps") {
expect {
try Mas.List.parse([]).run(appLibrary: AppLibraryMock())
try captureStream(stderr) {
try Mas.List.parse([]).run(appLibrary: AppLibraryMock())
}
}
.toNot(throwError())
== "Error: No installed apps found\n"
}
}
}

View file

@ -6,6 +6,7 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@ -18,10 +19,39 @@ public class OutdatedSpec: QuickSpec {
}
describe("outdated command") {
it("displays apps with pending updates") {
let mockSearchResult =
SearchResult(
bundleId: "au.id.haroldchu.mac.Bandwidth",
currentVersionReleaseDate: "2024-09-02T00:27:00Z",
fileSizeBytes: "998130",
minimumOsVersion: "10.13",
price: 0,
sellerName: "Harold Chu",
sellerUrl: "https://example.com",
trackId: 490_461_369,
trackName: "Bandwidth+",
trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4",
version: "1.28"
)
let mockStoreSearch = StoreSearchMock()
mockStoreSearch.apps[mockSearchResult.trackId] = mockSearchResult
let mockAppLibrary = AppLibraryMock()
mockAppLibrary.installedApps.append(
SoftwareProductMock(
appName: mockSearchResult.trackName,
bundleIdentifier: mockSearchResult.bundleId,
bundlePath: "/Applications/Bandwidth+.app",
bundleVersion: "1.27",
itemIdentifier: NSNumber(value: mockSearchResult.trackId)
)
)
expect {
try Mas.Outdated.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
try captureStream(stdout) {
try Mas.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch)
}
}
.toNot(throwError())
== "490461369 Bandwidth+ (1.27 -> 1.28)\n"
}
}
}

View file

@ -63,9 +63,11 @@ public class UninstallSpec: QuickSpec {
it("removes an app") {
mockLibrary.installedApps.append(app)
expect {
try uninstall.run(appLibrary: mockLibrary)
try captureStream(stdout) {
try uninstall.run(appLibrary: mockLibrary)
}
}
.toNot(throwError())
== " 1111 slack (0.0)\n==> Some App /tmp/Some.app\n==> (not removed, dry run)\n"
}
it("fails if there is a problem with the trash command") {
var brokenUninstall = app // make mutable copy

View file

@ -6,6 +6,7 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@ -17,11 +18,14 @@ public class UpgradeSpec: QuickSpec {
Mas.initialize()
}
describe("upgrade command") {
it("upgrades stuff") {
it("finds no upgrades") {
expect {
try Mas.Upgrade.parse([]).run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
try captureStream(stderr) {
try Mas.Upgrade.parse([])
.run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
}
}
.toNot(throwError())
== "Warning: Nothing found to upgrade\n"
}
}
}

View file

@ -6,6 +6,7 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@ -19,9 +20,11 @@ public class VersionSpec: QuickSpec {
describe("version command") {
it("displays the current version") {
expect {
try Mas.Version.parse([]).run()
try captureStream(stdout) {
try Mas.Version.parse([]).run()
}
}
.toNot(throwError())
== Package.version + "\n"
}
}
}

View file

@ -1,28 +0,0 @@
//
// OutputListener.swift
// masTests
//
// Created by Ben Chatelain on 1/7/19.
// Copyright © 2019 mas-cli. All rights reserved.
//
@testable import mas
/// Test helper for monitoring strings written to stdout. Modified from:
/// https://stackoverflow.com/a/53569018
class OutputListener {
/// Buffers strings written to stdout
var contents = ""
init() {
printObserver = { [weak self] text in
strongify(self) { context in
context.contents += text
}
}
}
deinit {
printObserver = nil
}
}

View file

@ -1,39 +0,0 @@
//
// OutputListenerSpec.swift
// masTests
//
// Created by Ben Chatelain on 1/8/19.
// Copyright © 2019 mas-cli. All rights reserved.
//
import Nimble
import Quick
@testable import mas
public class OutputListenerSpec: QuickSpec {
override public static func spec() {
beforeSuite {
Mas.initialize()
}
describe("output listener") {
it("can intercept a single line written stdout") {
let output = OutputListener()
print("hi there", terminator: "")
expect(output.contents) == "hi there"
}
it("can intercept multiple lines written stdout") {
let output = OutputListener()
print("hi there")
expect(output.contents) == """
hi there
"""
}
}
}
}

View file

@ -1,14 +0,0 @@
//
// Strongify.swift
// masTests
//
// Created by Ben Chatelain on 1/8/19.
// Copyright © 2019 mas-cli. All rights reserved.
//
// https://medium.com/@merowing_/stop-weak-strong-dance-in-swift-3aec6d3563d4
func strongify<Context: AnyObject>(_ context: Context?, closure: (Context) -> Void) {
guard let strongContext = context else { return }
closure(strongContext)
}