Changes to project structure to allow for better testability (#191)

* Changed sources folder to separate the CLI from the 'Kit' to allow for testability of each command

* Fixed Swift 5.2 Support

* Removed Path.swift (for now)

* Fixed OpenCombine and other version issues for 5.2 support

* Removed Path.swift from test target dependency

Co-authored-by: thecb4 <cavelle@tehcb4.io>
This commit is contained in:
thecb4 2020-12-21 05:14:24 -05:00 committed by GitHub
parent dd452fb6f1
commit 5fe8c8edac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 647 additions and 27 deletions

127
Brewfile.lock.json Normal file
View File

@ -0,0 +1,127 @@
{
"entries": {
"brew": {
"pre-commit": {
"version": "2.9.3",
"bottle": {
"cellar": ":any",
"prefix": "/usr/local",
"files": {
"big_sur": {
"url": "https://homebrew.bintray.com/bottles/pre-commit-2.9.3.big_sur.bottle.tar.gz",
"sha256": "81f18e83e858feffacc9a9441c836a2abcf5b1a880f09ee99a261f12ac8ceca7"
},
"catalina": {
"url": "https://homebrew.bintray.com/bottles/pre-commit-2.9.3.catalina.bottle.tar.gz",
"sha256": "16977b9f715b4330e7975761edddbaad4b891b25e6c8e4c4186ca4e2e99b11e7"
},
"mojave": {
"url": "https://homebrew.bintray.com/bottles/pre-commit-2.9.3.mojave.bottle.tar.gz",
"sha256": "82af5cc062aed6375aff5dfc759bac2cbc549ae0709f1a720ec805368944b46e"
}
}
}
},
"swiftformat": {
"version": "0.47.8",
"bottle": {
"cellar": ":any_skip_relocation",
"prefix": "/usr/local",
"files": {
"big_sur": {
"url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.8.big_sur.bottle.tar.gz",
"sha256": "47e3aa92792a53413eb0866d3b6709e88c7164184f406c3e49c560e82a2fe589"
},
"catalina": {
"url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.8.catalina.bottle.tar.gz",
"sha256": "c268a44da4ed4e7836d2b43f917a1411f3b8f13488ee25c66f4bd8bb2f4d5e05"
},
"mojave": {
"url": "https://homebrew.bintray.com/bottles/swiftformat-0.47.8.mojave.bottle.tar.gz",
"sha256": "38abb30c870974c65ee0a3afed11cd5f983ab05a13923061fc67c4b6566df6f8"
}
}
}
},
"swiftlint": {
"version": "0.42.0",
"bottle": {
"cellar": ":any_skip_relocation",
"prefix": "/usr/local",
"files": {
"big_sur": {
"url": "https://homebrew.bintray.com/bottles/swiftlint-0.42.0.big_sur.bottle.tar.gz",
"sha256": "1a0540f0ff6cac2da0a51672db7963aef71cc58954c34791e9b01dafd63c5898"
},
"catalina": {
"url": "https://homebrew.bintray.com/bottles/swiftlint-0.42.0.catalina.bottle.tar.gz",
"sha256": "e9023ed754eb8cb78a9f2b469a90875ca42a7afffd3e96f8142252e81d889793"
}
}
}
},
"binaryen": {
"version": "98",
"bottle": {
"cellar": ":any",
"prefix": "/usr/local",
"files": {
"big_sur": {
"url": "https://homebrew.bintray.com/bottles/binaryen-98.big_sur.bottle.tar.gz",
"sha256": "5d5ac79ec3aabebd8e62433c84bd0d61b0f4ee44d56e4c4f47475276a6428bd8"
},
"catalina": {
"url": "https://homebrew.bintray.com/bottles/binaryen-98.catalina.bottle.tar.gz",
"sha256": "260a8deac0c78a9fead982cf6d05fd818330bc4e28cdc73ecd5de6952ded6e8b"
},
"mojave": {
"url": "https://homebrew.bintray.com/bottles/binaryen-98.mojave.bottle.tar.gz",
"sha256": "7a68230a62307fbd61638803e942e4d556f99b8e4d6fbbdd3dbaf6997b3b2843"
},
"high_sierra": {
"url": "https://homebrew.bintray.com/bottles/binaryen-98.high_sierra.bottle.tar.gz",
"sha256": "4656a9b10a7db143e3619126ea1e0c169d05b06b27c7e75d5f641c86135c7b1f"
}
}
}
},
"wasmer": {
"version": "0.16.2",
"bottle": {
"cellar": ":any_skip_relocation",
"prefix": "/usr/local",
"files": {
"big_sur": {
"url": "https://homebrew.bintray.com/bottles/wasmer-0.16.2.big_sur.bottle.tar.gz",
"sha256": "63d91bbfece68628e7bb464cd8d3a90a6dc89344c4889ada157f174db62f05da"
},
"catalina": {
"url": "https://homebrew.bintray.com/bottles/wasmer-0.16.2.catalina.bottle.tar.gz",
"sha256": "751b4b059036dbca254eef935bc03240e1fd559465a376a0cff8f5a41dcd3980"
},
"mojave": {
"url": "https://homebrew.bintray.com/bottles/wasmer-0.16.2.mojave.bottle.tar.gz",
"sha256": "725d2b857e0954b1e2fd8a01021847e168d5daec33cd76c32f90a0ae12fdf422"
},
"high_sierra": {
"url": "https://homebrew.bintray.com/bottles/wasmer-0.16.2.high_sierra.bottle.tar.gz",
"sha256": "42ea898c1ebd9c0ac58bf21117c05df6a4726123590444c19173a01586c80c63"
}
}
}
}
}
},
"system": {
"macos": {
"big_sur": {
"HOMEBREW_VERSION": "2.6.2",
"HOMEBREW_PREFIX": "/usr/local",
"Homebrew/homebrew-core": "386111a04c8f09f72c60548e519a3e27fade9c34",
"CLT": "",
"Xcode": "12.2",
"macOS": "11.1"
}
}
}
}

View File

@ -37,6 +37,15 @@
"version": "0.11.0" "version": "0.11.0"
} }
}, },
{
"package": "Path.swift",
"repositoryURL": "https://github.com/mxcl/Path.swift.git",
"state": {
"branch": null,
"revision": "f062ed9ce3729fb3ba3f43573136d2a0e0c6d209",
"version": "1.0.0"
}
},
{ {
"package": "routing-kit", "package": "routing-kit",
"repositoryURL": "https://github.com/vapor/routing-kit.git", "repositoryURL": "https://github.com/vapor/routing-kit.git",

View File

@ -12,6 +12,14 @@ let openCombineProduct = Target.Dependency.product(
let package = Package( let package = Package(
name: "carton", name: "carton",
platforms: [.macOS(.v10_15)], platforms: [.macOS(.v10_15)],
products: [
.library(name: "SwiftToolchain", targets: ["SwiftToolchain"]),
.library(name: "CartonHelpers", targets: ["CartonHelpers"]),
.library(name: "CartonKit", targets: ["CartonKit"]),
.library(name: "CartonCLI", targets: ["CartonCLI"]),
.executable(name: "carton", targets: ["Carton"]),
.executable(name: "carton-release", targets: ["carton-release"]),
],
dependencies: [ dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.2.2"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.2.2"),
.package( .package(
@ -27,13 +35,33 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.0"), .package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.0"),
.package(url: "https://github.com/JohnSundell/Splash.git", from: "0.14.0"), .package(url: "https://github.com/JohnSundell/Splash.git", from: "0.14.0"),
.package(url: "https://github.com/swiftwasm/WasmTransformer", .upToNextMinor(from: "0.0.2")), .package(url: "https://github.com/swiftwasm/WasmTransformer", .upToNextMinor(from: "0.0.2")),
.package(url: "https://github.com/mxcl/Path.swift.git", .exact("1.0.0")),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module // Targets are the basic building blocks of a package. A target can define a module
// or a test suite. Targets can depend on other targets in this package, and on // or a test suite. Targets can depend on other targets in this package, and on
// products in packages which this package depends on. // products in packages which this package depends on.
.target( .target(
name: "carton", name: "Carton",
dependencies: [
"CartonCLI",
// commented out for now. Will remove once confirmed working
// .product(name: "ArgumentParser", package: "swift-argument-parser"),
// .product(name: "AsyncHTTPClient", package: "async-http-client"),
// .product(name: "Crypto", package: "swift-crypto"),
// .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
// .product(name: "Vapor", package: "vapor"),
// "CartonHelpers",
// openCombineProduct,
// "SwiftToolchain",
]
),
.target(
name: "CartonCLI",
dependencies: ["CartonKit"]
),
.target(
name: "CartonKit",
dependencies: [ dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncHTTPClient", package: "async-http-client"), .product(name: "AsyncHTTPClient", package: "async-http-client"),
@ -78,9 +106,17 @@ let package = Package(
.testTarget( .testTarget(
name: "CartonTests", name: "CartonTests",
dependencies: [ dependencies: [
"carton", "Carton",
"CartonHelpers", "CartonHelpers",
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Path", package: "Path.swift"),
]
),
.testTarget(
name: "CartonCLITests",
dependencies: [
"CartonCLI",
] ]
), ),
] ]

View File

@ -6,10 +6,18 @@ import PackageDescription
let package = Package( let package = Package(
name: "carton", name: "carton",
platforms: [.macOS(.v10_15)], platforms: [.macOS(.v10_15)],
products: [
.library(name: "SwiftToolchain", targets: ["SwiftToolchain"]),
.library(name: "CartonHelpers", targets: ["CartonHelpers"]),
.library(name: "CartonKit", targets: ["CartonKit"]),
.library(name: "CartonCLI", targets: ["CartonCLI"]),
.executable(name: "carton", targets: ["Carton"]),
.executable(name: "carton-release", targets: ["carton-release"]),
],
dependencies: [ dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.1.1"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.1.1"),
.package( .package(
url: "https://github.com/apple/swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git",
.upToNextMinor(from: "0.3.0") .upToNextMinor(from: "0.3.0")
), ),
.package( .package(
@ -27,7 +35,26 @@ let package = Package(
// or a test suite. Targets can depend on other targets in this package, and on // or a test suite. Targets can depend on other targets in this package, and on
// products in packages which this package depends on. // products in packages which this package depends on.
.target( .target(
name: "carton", name: "Carton",
dependencies: [
"CartonCLI",
// commented out for now. Will remove once confirmed working
// .product(name: "ArgumentParser", package: "swift-argument-parser"),
// .product(name: "AsyncHTTPClient", package: "async-http-client"),
// .product(name: "Crypto", package: "swift-crypto"),
// .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
// .product(name: "Vapor", package: "vapor"),
// "CartonHelpers",
// openCombineProduct,
// "SwiftToolchain",
]
),
.target(
name: "CartonCLI",
dependencies: ["CartonKit"]
),
.target(
name: "CartonKit",
dependencies: [ dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncHTTPClient", package: "async-http-client"), .product(name: "AsyncHTTPClient", package: "async-http-client"),
@ -71,7 +98,18 @@ let package = Package(
), ),
.testTarget( .testTarget(
name: "CartonTests", name: "CartonTests",
dependencies: ["carton"] dependencies: [
"Carton",
"CartonHelpers",
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(
name: "CartonCLITests",
dependencies: [
"CartonCLI",
]
), ),
] ]
) )

View File

@ -12,4 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import CartonCLI
Carton.main() Carton.main()

View File

@ -15,10 +15,12 @@
import ArgumentParser import ArgumentParser
import CartonHelpers import CartonHelpers
struct Carton: ParsableCommand { public struct Carton: ParsableCommand {
static let configuration = CommandConfiguration( public static let configuration = CommandConfiguration(
abstract: "📦 Watcher, bundler, and test runner for your SwiftWasm apps.", abstract: "📦 Watcher, bundler, and test runner for your SwiftWasm apps.",
version: cartonVersion, version: cartonVersion,
subcommands: [Bundle.self, Dev.self, Init.self, SDK.self, Test.self, Package.self] subcommands: [Bundle.self, Dev.self, Init.self, SDK.self, Test.self, Package.self]
) )
public init() {}
} }

View File

@ -14,6 +14,7 @@
import ArgumentParser import ArgumentParser
import CartonHelpers import CartonHelpers
import CartonKit
import Crypto import Crypto
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic

View File

@ -20,6 +20,7 @@ import Combine
#else #else
import OpenCombine import OpenCombine
#endif #endif
import CartonKit
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic

View File

@ -14,6 +14,7 @@
import ArgumentParser import ArgumentParser
import CartonHelpers import CartonHelpers
import CartonKit
import Foundation import Foundation
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic

View File

@ -14,6 +14,7 @@
import ArgumentParser import ArgumentParser
import CartonHelpers import CartonHelpers
import CartonKit
import TSCBasic import TSCBasic
struct ListTemplates: ParsableCommand { struct ListTemplates: ParsableCommand {

View File

@ -14,6 +14,7 @@
import ArgumentParser import ArgumentParser
import CartonHelpers import CartonHelpers
import CartonKit
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic

View File

@ -19,6 +19,7 @@ import Combine
import OpenCombine import OpenCombine
#endif #endif
import CartonHelpers import CartonHelpers
import CartonKit
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic

View File

@ -15,7 +15,7 @@
import Crypto import Crypto
import TSCBasic import TSCBasic
extension ByteString { public extension ByteString {
var hexSHA256: String { var hexSHA256: String {
ByteString(SHA256.hash(data: contents)).hexadecimalRepresentation ByteString(SHA256.hash(data: contents)).hexadecimalRepresentation
} }

View File

@ -33,15 +33,20 @@ private let verifyHash = Equality<ByteString, String> {
""" """
} }
enum EntrypointError: Error { public enum EntrypointError: Error {
case downloadFailed(url: String) case downloadFailed(url: String)
} }
struct Entrypoint { public struct Entrypoint {
let fileName: String let fileName: String
let sha256: ByteString let sha256: ByteString
func paths( public init(fileName: String, sha256: ByteString) {
self.fileName = fileName
self.sha256 = sha256
}
public func paths(
on fileSystem: FileSystem on fileSystem: FileSystem
// swiftlint:disable:next large_tuple // swiftlint:disable:next large_tuple
) -> (cartonDir: AbsolutePath, staticDir: AbsolutePath, filePath: AbsolutePath) { ) -> (cartonDir: AbsolutePath, staticDir: AbsolutePath, filePath: AbsolutePath) {
@ -50,7 +55,7 @@ struct Entrypoint {
return (cartonDir, staticDir, staticDir.appending(component: fileName)) return (cartonDir, staticDir, staticDir.appending(component: fileName))
} }
func check(on fileSystem: FileSystem, _ terminal: InteractiveWriter) throws { public func check(on fileSystem: FileSystem, _ terminal: InteractiveWriter) throws {
let (cartonDir, staticDir, filePath) = paths(on: fileSystem) let (cartonDir, staticDir, filePath) = paths(on: fileSystem)
// If hash check fails, download the `static.zip` archive and unpack it // If hash check fails, download the `static.zip` archive and unpack it

View File

@ -14,8 +14,14 @@
import TSCBasic import TSCBasic
struct Project { public struct Project {
let name: String let name: String
let path: AbsolutePath let path: AbsolutePath
let inPlace: Bool let inPlace: Bool
public init(name: String, path: AbsolutePath, inPlace: Bool) {
self.name = name
self.path = path
self.inPlace = inPlace
}
} }

View File

@ -16,11 +16,11 @@ import CartonHelpers
import SwiftToolchain import SwiftToolchain
import TSCBasic import TSCBasic
enum Templates: String, CaseIterable { public enum Templates: String, CaseIterable {
case basic case basic
case tokamak case tokamak
var template: Template.Type { public var template: Template.Type {
switch self { switch self {
case .basic: return Basic.self case .basic: return Basic.self
case .tokamak: return Tokamak.self case .tokamak: return Tokamak.self
@ -28,7 +28,7 @@ enum Templates: String, CaseIterable {
} }
} }
protocol Template { public protocol Template {
static var description: String { get } static var description: String { get }
static func create( static func create(
on fileSystem: FileSystem, on fileSystem: FileSystem,

View File

@ -21,7 +21,7 @@ enum HTMLError: String, Error {
""" """
} }
struct HTML { public struct HTML {
let value: String let value: String
} }
@ -34,7 +34,9 @@ extension HTML: ResponseEncodable {
)) ))
} }
static func readCustomIndexPage(at path: String?, on fileSystem: FileSystem) throws -> String? { public static func readCustomIndexPage(at path: String?,
on fileSystem: FileSystem) throws -> String?
{
if let customIndexPage = path { if let customIndexPage = path {
let content = try localFileSystem.readFileContents(customIndexPage.isAbsolutePath ? let content = try localFileSystem.readFileContents(customIndexPage.isAbsolutePath ?
AbsolutePath(customIndexPage) : AbsolutePath(customIndexPage) :
@ -50,7 +52,7 @@ extension HTML: ResponseEncodable {
} }
} }
static func indexPage(customContent: String?, entrypointName: String) -> String { public static func indexPage(customContent: String?, entrypointName: String) -> String {
let scriptTag = #"<script type="text/javascript" src="\#(entrypointName)"></script>"# let scriptTag = #"<script type="text/javascript" src="\#(entrypointName)"></script>"#
if let customContent = customContent { if let customContent = customContent {
return customContent.replacingOccurrences( return customContent.replacingOccurrences(

View File

@ -66,7 +66,7 @@ extension WebSocket: Hashable {
} }
} }
final class Server { public final class Server {
private let decoder = JSONDecoder() private let decoder = JSONDecoder()
private var connections = Set<WebSocket>() private var connections = Set<WebSocket>()
private var subscriptions = [AnyCancellable]() private var subscriptions = [AnyCancellable]()
@ -75,7 +75,7 @@ final class Server {
private let localURL: String private let localURL: String
private let skipAutoOpen: Bool private let skipAutoOpen: Bool
struct Configuration { public struct Configuration {
let builder: Builder? let builder: Builder?
let mainWasmPath: AbsolutePath let mainWasmPath: AbsolutePath
let verbose: Bool let verbose: Bool
@ -85,9 +85,31 @@ final class Server {
let package: SwiftToolchain.Package let package: SwiftToolchain.Package
let product: Product? let product: Product?
let entrypoint: Entrypoint let entrypoint: Entrypoint
public init(
builder: Builder?,
mainWasmPath: AbsolutePath,
verbose: Bool,
skipAutoOpen: Bool,
port: Int,
customIndexContent: String?,
package: SwiftToolchain.Package,
product: Product?,
entrypoint: Entrypoint
) {
self.builder = builder
self.mainWasmPath = mainWasmPath
self.verbose = verbose
self.skipAutoOpen = skipAutoOpen
self.port = port
self.customIndexContent = customIndexContent
self.package = package
self.product = product
self.entrypoint = entrypoint
}
} }
init( public init(
with configuration: Configuration, with configuration: Configuration,
_ terminal: InteractiveWriter _ terminal: InteractiveWriter
) throws { ) throws {
@ -151,7 +173,7 @@ final class Server {
} }
/// Blocking function that starts the HTTP server /// Blocking function that starts the HTTP server
func run() throws { public func run() throws {
defer { app.shutdown() } defer { app.shutdown() }
try app.run() try app.run()
for conn in connections { for conn in connections {

View File

@ -1,21 +1,21 @@
import TSCBasic import TSCBasic
let devEntrypointSHA256 = ByteString([ public let devEntrypointSHA256 = ByteString([
0xBF, 0x7E, 0x84, 0x1D, 0x61, 0x40, 0x88, 0x83, 0xBC, 0x4C, 0x80, 0x24, 0x35, 0xEB, 0xED, 0x0D, 0xBF, 0x7E, 0x84, 0x1D, 0x61, 0x40, 0x88, 0x83, 0xBC, 0x4C, 0x80, 0x24, 0x35, 0xEB, 0xED, 0x0D,
0xF1, 0x5D, 0x22, 0x80, 0xC8, 0x70, 0x3D, 0xF0, 0xC6, 0x0C, 0x8A, 0x6F, 0x55, 0xD4, 0x8A, 0xF8, 0xF1, 0x5D, 0x22, 0x80, 0xC8, 0x70, 0x3D, 0xF0, 0xC6, 0x0C, 0x8A, 0x6F, 0x55, 0xD4, 0x8A, 0xF8,
]) ])
let bundleEntrypointSHA256 = ByteString([ public let bundleEntrypointSHA256 = ByteString([
0x2B, 0xD2, 0xB5, 0x0C, 0x08, 0xFB, 0x5A, 0x8D, 0x55, 0xC4, 0x4B, 0x3A, 0x51, 0x63, 0x80, 0x1F, 0x2B, 0xD2, 0xB5, 0x0C, 0x08, 0xFB, 0x5A, 0x8D, 0x55, 0xC4, 0x4B, 0x3A, 0x51, 0x63, 0x80, 0x1F,
0xB9, 0x15, 0x50, 0xD2, 0xB6, 0x12, 0xC3, 0xDE, 0x3A, 0x7D, 0xEB, 0xB0, 0x1F, 0x40, 0x06, 0x3F, 0xB9, 0x15, 0x50, 0xD2, 0xB6, 0x12, 0xC3, 0xDE, 0x3A, 0x7D, 0xEB, 0xB0, 0x1F, 0x40, 0x06, 0x3F,
]) ])
let testEntrypointSHA256 = ByteString([ public let testEntrypointSHA256 = ByteString([
0x51, 0x8C, 0xAF, 0x03, 0x7C, 0x12, 0x0A, 0xF4, 0xBC, 0xD1, 0xA0, 0x1B, 0xA2, 0xA8, 0x2B, 0x22, 0x51, 0x8C, 0xAF, 0x03, 0x7C, 0x12, 0x0A, 0xF4, 0xBC, 0xD1, 0xA0, 0x1B, 0xA2, 0xA8, 0x2B, 0x22,
0xDE, 0x93, 0x4D, 0x68, 0x6C, 0x09, 0xAC, 0x20, 0x58, 0x24, 0x31, 0x0F, 0xD6, 0xAA, 0x99, 0x8A, 0xDE, 0x93, 0x4D, 0x68, 0x6C, 0x09, 0xAC, 0x20, 0x58, 0x24, 0x31, 0x0F, 0xD6, 0xAA, 0x99, 0x8A,
]) ])
let staticArchiveHash = ByteString([ public let staticArchiveHash = ByteString([
0x49, 0x84, 0x3D, 0x18, 0xC5, 0x43, 0x32, 0xFE, 0x80, 0x0B, 0x5D, 0xBE, 0xF3, 0x6A, 0x52, 0x9F, 0x49, 0x84, 0x3D, 0x18, 0xC5, 0x43, 0x32, 0xFE, 0x80, 0x0B, 0x5D, 0xBE, 0xF3, 0x6A, 0x52, 0x9F,
0xD8, 0x27, 0x8C, 0xB6, 0x39, 0xAB, 0xAB, 0x6E, 0xE6, 0xCB, 0x82, 0x36, 0x21, 0x09, 0x9B, 0x50, 0xD8, 0x27, 0x8C, 0xB6, 0x39, 0xAB, 0xAB, 0x6E, 0xE6, 0xCB, 0x82, 0x36, 0x21, 0x09, 0x9B, 0x50,
]) ])

View File

@ -0,0 +1,247 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParser
import XCTest
// extensions to the ParsableArguments protocol to facilitate XCTestExpectation support
public protocol TestableParsableArguments: ParsableArguments {
var didValidateExpectation: XCTestExpectation { get }
}
public extension TestableParsableArguments {
mutating func validate() throws {
didValidateExpectation.fulfill()
}
}
// extensions to the ParsableCommand protocol to facilitate XCTestExpectation support
public protocol TestableParsableCommand: ParsableCommand, TestableParsableArguments {
var didRunExpectation: XCTestExpectation { get }
}
public extension TestableParsableCommand {
mutating func run() throws {
didRunExpectation.fulfill()
}
}
public extension XCTestExpectation {
convenience init(singleExpectation description: String) {
self.init(description: description)
expectedFulfillmentCount = 1
assertForOverFulfill = true
}
}
public func AssertResultFailure<T, U: Error>(
_ expression: @autoclosure () -> Result<T, U>,
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) {
switch expression() {
case .success:
let msg = message()
XCTFail(msg.isEmpty ? "Incorrectly succeeded" : msg, file: file, line: line)
case .failure:
break
}
}
public func AssertErrorMessage<A>(
_ type: A.Type,
_ arguments: [String],
_ errorMessage: String,
file: StaticString = #file,
line: UInt = #line
) where A: ParsableArguments {
do {
_ = try A.parse(arguments)
XCTFail("Parsing should have failed.", file: file, line: line)
} catch {
// We expect to hit this path, i.e. getting an error:
XCTAssertEqual(A.message(for: error), errorMessage, file: file, line: line)
}
}
public func AssertFullErrorMessage<A>(
_ type: A.Type,
_ arguments: [String],
_ errorMessage: String,
file: StaticString = #file,
line: UInt = #line
) where A: ParsableArguments {
do {
_ = try A.parse(arguments)
XCTFail("Parsing should have failed.", file: file, line: line)
} catch {
// We expect to hit this path, i.e. getting an error:
XCTAssertEqual(A.fullMessage(for: error), errorMessage, file: file, line: line)
}
}
public func AssertParse<A>(
_ type: A.Type,
_ arguments: [String],
file: StaticString = #file,
line: UInt = #line,
closure: (A) throws -> ()
) where A: ParsableArguments {
do {
let parsed = try type.parse(arguments)
try closure(parsed)
} catch {
let message = type.message(for: error)
XCTFail("\"\(message)\"\(error)", file: file, line: line)
}
}
public func AssertParseCommand<A: ParsableCommand>(
_ rootCommand: ParsableCommand.Type,
_ type: A.Type,
_ arguments: [String],
file: StaticString = #file,
line: UInt = #line,
closure: (A) throws -> ()
) {
do {
let command = try rootCommand.parseAsRoot(arguments)
guard let aCommand = command as? A else {
XCTFail("Command is of unexpected type: \(command)", file: file, line: line)
return
}
try closure(aCommand)
} catch {
let message = rootCommand.message(for: error)
XCTFail("\"\(message)\"\(error)", file: file, line: line)
}
}
public func AssertEqualStringsIgnoringTrailingWhitespace(
_ string1: String,
_ string2: String,
file: StaticString = #file,
line: UInt = #line
) {
let lines1 = string1.split(separator: "\n", omittingEmptySubsequences: false)
let lines2 = string2.split(separator: "\n", omittingEmptySubsequences: false)
XCTAssertEqual(
lines1.count,
lines2.count,
"Strings have different numbers of lines.",
file: file,
line: line
)
for (line1, line2) in zip(lines1, lines2) {
XCTAssertEqual(line1.trimmed(), line2.trimmed(), file: file, line: line)
}
}
public func AssertHelp<T: ParsableArguments>(
for _: T.Type, equals expected: String,
file: StaticString = #file, line: UInt = #line
) {
do {
_ = try T.parse(["-h"])
XCTFail(file: file, line: line)
} catch {
let helpString = T.fullMessage(for: error)
AssertEqualStringsIgnoringTrailingWhitespace(
helpString, expected, file: file, line: line
)
}
let helpString = T.helpMessage()
AssertEqualStringsIgnoringTrailingWhitespace(
helpString, expected, file: file, line: line
)
}
public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
for _: T.Type, root _: U.Type, equals expected: String,
file: StaticString = #file, line: UInt = #line
) {
let helpString = U.helpMessage(for: T.self)
AssertEqualStringsIgnoringTrailingWhitespace(
helpString, expected, file: file, line: line
)
}
public extension XCTest {
var debugURL: URL {
let bundleURL = Bundle(for: type(of: self)).bundleURL
return bundleURL.lastPathComponent.hasSuffix("xctest")
? bundleURL.deletingLastPathComponent()
: bundleURL
}
func AssertExecuteCommand(
command: String,
expected: String? = nil,
exitCode: ExitCode = .success,
file: StaticString = #file, line: UInt = #line
) {
let splitCommand = command.split(separator: " ")
let arguments = splitCommand.dropFirst().map(String.init)
let commandName = String(splitCommand.first!)
let commandURL = debugURL.appendingPathComponent(commandName)
guard (try? commandURL.checkResourceIsReachable()) ?? false else {
XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.",
file: file, line: line)
return
}
let process = Process()
if #available(macOS 10.13, *) {
process.executableURL = commandURL
} else {
process.launchPath = commandURL.path
}
process.arguments = arguments
let output = Pipe()
process.standardOutput = output
let error = Pipe()
process.standardError = error
if #available(macOS 10.13, *) {
guard (try? process.run()) != nil else {
XCTFail("Couldn't run command process.", file: file, line: line)
return
}
} else {
process.launch()
}
process.waitUntilExit()
let outputData = output.fileHandleForReading.readDataToEndOfFile()
let outputActual = String(data: outputData, encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)
let errorData = error.fileHandleForReading.readDataToEndOfFile()
let errorActual = String(data: errorData, encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)
if let expected = expected {
AssertEqualStringsIgnoringTrailingWhitespace(
expected,
errorActual + outputActual,
file: file,
line: line
)
}
XCTAssertEqual(process.terminationStatus, exitCode.rawValue, file: file, line: line)
}
}

View File

@ -0,0 +1,63 @@
// Copyright 2020 Carton contributors
//
// 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
//
// http://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.
//
// Created by Cavelle Benjamin on Dec/20/20.
//
@testable import CartonCLI
import XCTest
final class DevCommandTests: XCTestCase {
func testDefaultArgumentParsing() throws {
// given
let arguments: [String] = []
// when
AssertParse(Dev.self, arguments) { command in
// then
XCTAssertNotNil(command)
}
}
func testHelpString() throws {
// given
let expectation =
"""
OVERVIEW: Watch the current directory, host the app, rebuild on change.
USAGE: carton dev [--product <product>] [--destination <destination>] [--custom-index-page <custom-index-page>] [--release] [--verbose] [--port <port>] [--skip-auto-open]
OPTIONS:
--product <product> Specify name of an executable product in development.
--destination <destination>
This option has no effect and will be removed in a
future version of `carton`
--custom-index-page <custom-index-page>
Specify a path to a custom `index.html` file to be
used for your app.
--release When specified, build in the release mode.
-v, --verbose Don't clear terminal window after files change.
-p, --port <port> Set the HTTP port the development server will run on.
(default: 8080)
--skip-auto-open Skip automatically opening app in system browser.
--version Show the version.
-h, --help Show help information.
"""
// when
// then
AssertExecuteCommand(command: "carton dev -h", expected: expectation)
}
}

View File

@ -0,0 +1,27 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
extension Substring {
func trimmed() -> Substring {
guard let i = lastIndex(where: { $0 != " " }) else {
return ""
}
return self[...i]
}
}
public extension String {
func trimmingLines() -> String {
split(separator: "\n", omittingEmptySubsequences: false)
.map { $0.trimmed() }
.joined(separator: "\n")
}
}

View File

@ -58,6 +58,34 @@ final class CartonTests: XCTestCase {
XCTAssertEqual(output?.trimmingCharacters(in: .whitespacesAndNewlines), "0.9.1") XCTAssertEqual(output?.trimmingCharacters(in: .whitespacesAndNewlines), "0.9.1")
} }
// func testDev() throws {
// // This is an example of a functional test case.
// // Use XCTAssert and related functions to verify your tests produce the correct
// // results.
//
// // Some of the APIs that we use below are available in macOS 10.13 and above.
// guard #available(macOS 10.13, *) else {
// return
// }
//
// let fooBinary = productsDirectory.appendingPathComponent("carton")
//
// let process = Process()
// process.executableURL = fooBinary
//
// let pipe = Pipe()
// process.standardOutput = pipe
//
// process.arguments = ["dev"]
// try process.run()
// process.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// let output = String(data: data, encoding: .utf8)
//
// XCTAssertEqual(output?.trimmingCharacters(in: .whitespacesAndNewlines), "0.9.1")
// }
final class TestOutputStream: OutputByteStream { final class TestOutputStream: OutputByteStream {
var bytes: [UInt8] = [] var bytes: [UInt8] = []
var currentOutput: String { var currentOutput: String {