Merge pull request 'Basic implementation' (#3) from develop into master

Reviewed-on: http://code.merlin.local:3000/adan/Conf/pulls/3
This commit is contained in:
adan 2020-06-10 03:36:33 +03:00
commit a7b49b69e6
30 changed files with 855 additions and 68 deletions

View File

@ -6,20 +6,11 @@ import PackageDescription
let package = Package( let package = Package(
name: "Conf", name: "Conf",
products: [ products: [
.library(name: "Conf", targets: ["Conf"]), .library(name: "Conf", targets: ["Conf"])
// .executable(name: "Demo", targets: ["DemoApp"])
], ],
dependencies: [ dependencies: [
], ],
targets: [ targets: [
.target(
name: "DemoApp",
dependencies: [
"Conf",
]),
.testTarget(
name: "DemoAppTests",
dependencies: ["DemoApp"]),
.target( .target(
name: "Conf", name: "Conf",
dependencies: [ dependencies: [

View File

@ -0,0 +1,28 @@
import Foundation
public extension Config {
func load(env filename: String) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.donEnv))
}
func load(env data: Data) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.donEnv))
}
func load(json filename: String) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.json))
}
func load(json data: Data) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.json))
}
func load(plist filename: String) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.plist))
}
func load(plist data: Data) throws {
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.plist))
}
}

45
Sources/Conf/Config.swift Normal file
View File

@ -0,0 +1,45 @@
/// Key-value storage that can be filled from multiple sources
public final class Config {
public init(useEnvironment: Bool = false) {
isBackedByEnvironment = useEnvironment
}
let isBackedByEnvironment: Bool
private let environment = Environment()
private var data = [Key: String]()
func value(for key: Key) -> String? {
return data[key] ?? envValue(for: key)
}
func envValue(for key: Key) -> String? {
guard isBackedByEnvironment,
key.path.count == 1,
let variable = key.path.first
else { return nil }
return environment[variable]
}
func set(value: String?, for key: Key) {
data[key] = value
}
public subscript(_ key: Key) -> String? {
get { value(for: key) }
set { set(value: newValue, for: key) }
}
public subscript<Value>(_ key: Key) -> Value? where Value: LosslessStringConvertible {
get { value(for: key).flatMap(Value.init) }
set { set(value: newValue?.description, for: key) }
}
public func dump() -> [Key: String] {
data
}
public func load(from provider: ConfigurationProvider) throws {
try data.merge(provider.configuration(),
uniquingKeysWith: { _, new in new })
}
}

View File

@ -0,0 +1,5 @@
public enum ConfigurationError: Error {
case fetch(Error)
case parse(Error)
case decode(path: Key, value: Any)
}

View File

@ -0,0 +1,53 @@
import Foundation
public protocol ConfigurationProvider {
func configuration() throws -> [Key: String]
}
final class DefaultConfigurationProvider: ConfigurationProvider {
typealias Fetcher = () throws -> Data
typealias Parser = (Data) throws -> [String: Any]
init(loader: @escaping Fetcher, parser: @escaping Parser) {
self.load = loader
self.parse = parser
}
let load: Fetcher
let parse: Parser
func configuration() throws -> [Key: String] {
let rawData: Data
let parsedData: [String: Any]
do {
try rawData = load()
} catch { throw ConfigurationError.fetch(error) }
do {
parsedData = try parse(rawData)
} catch { throw ConfigurationError.parse(error) }
return try decode(currentKey: Key(), object: parsedData)
}
func decode(currentKey: Key, object: Any) throws -> [Key: String] {
switch object {
case let value as LosslessStringConvertible:
return [currentKey: value.description]
case let value as [String: LosslessStringConvertible]:
return .init(uniqueKeysWithValues:
value.map { key, value in
(currentKey.child(key: key), value.description) })
case let dictionary as [String: Any]:
return try dictionary.map { key, value in
try decode(currentKey: currentKey.child(key: key), object: value)
}.reduce(into: [Key: String]()) { result, value in
result.merge(value) { current, _ in current }
}
case let array as [LosslessStringConvertible]:
return .init(uniqueKeysWithValues:
array.enumerated()
.map { ( currentKey.child(key: String($0)), $1.description) })
default:
throw ConfigurationError.decode(path: currentKey, value: object)
}
}
}

View File

@ -0,0 +1,47 @@
import Foundation
/// Manages environment variables
@dynamicMemberLookup
public struct Environment {
public init(info: ProcessInfo = .processInfo) {
self.info = info
}
private let info: ProcessInfo
public func dump() -> [String: String] {
info.environment
}
public subscript(_ key: String) -> String? {
get { info.environment[key] }
nonmutating set (value) {
if let raw = value {
setenv(key, raw, 1)
} else {
unsetenv(key)
}
}
}
public subscript(dynamicMember member: String) -> String? {
get { self[member] }
nonmutating set { self[member] = newValue }
}
public subscript<Value>(_ key: String) -> Value? where Value: LosslessStringConvertible {
get {
let raw: String? = self[key]
return raw.flatMap(Value.init)
}
nonmutating set (value) {
self[key] = value?.description
}
}
public subscript<Value>(dynamicMember member: String) -> Value? where Value: LosslessStringConvertible {
get { self[member] }
nonmutating set (value) { self[member] = value }
}
}

View File

@ -0,0 +1,19 @@
import Foundation
/// Namespace for the predefined fetchers
enum Fetcher { }
extension Fetcher {
static let direct: (Data) -> DefaultConfigurationProvider.Fetcher = { data in
return { data }
}
static let file: (String) -> DefaultConfigurationProvider.Fetcher = { configName in
return {
let url = URL(fileURLWithPath: configName, isDirectory: false)
print(url)
return try Data(contentsOf: url)
}
}
}

View File

@ -1,3 +0,0 @@
public func foo() -> Int {
return 42
}

48
Sources/Conf/Key.swift Normal file
View File

@ -0,0 +1,48 @@
/// Configuration key that points to the value
public struct Key {
let path: [String]
public init(_ value: LosslessStringConvertible) {
path = [value.description]
}
public init(_ elements: [LosslessStringConvertible]) {
path = elements.map(\.description)
}
public init(_ elements: LosslessStringConvertible...) {
self.init(elements)
}
func child(key: String) -> Key {
Key(path + [key])
}
}
extension Key: Equatable {}
extension Key: Hashable {}
extension Key: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: LosslessStringConvertible...) {
self.init(elements)
}
}
extension Key: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
}
extension Key: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self.init(value)
}
}
extension Key: CustomStringConvertible {
public var description: String {
"Key<\(path.description)>"
}
}

65
Sources/Conf/Parser.swift Normal file
View File

@ -0,0 +1,65 @@
import Foundation
/// Namespace for the predefined parsers
enum Parser {
struct InvalidFormat: Error { let data: Data }
}
extension Parser {
static let json: DefaultConfigurationProvider.Parser = { data in
let object = try JSONSerialization.jsonObject(with: data, options: [])
guard let values = object as? [String: Any] else {
throw InvalidFormat(data: data)
}
return values
}
}
extension Parser {
static let donEnv: DefaultConfigurationProvider.Parser = { data in
func fail() throws -> Never {
throw InvalidFormat(data: data)
}
guard let contents = String(data: data, encoding: .utf8) else { try fail() }
var result = [String: String]()
let lines = contents.split(whereSeparator: { $0 == "\n" || $0 == "\r\n" }).lazy
for line in lines {
// ignore comments
if line.starts(with: "#") { continue }
// ignore lines that appear empty
if line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
continue
}
// extract key and value which are separated by an equals sign
let parts = line.split(separator: "=", maxSplits: 1)
guard parts.count == 2 else { try fail() }
let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
var value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
// remove surrounding quotes from value & convert remove escape character before any embedded quotes
if value[value.startIndex] == "\"" && value[value.index(before: value.endIndex)] == "\"" {
value.remove(at: value.startIndex)
value.remove(at: value.index(before: value.endIndex))
value = value.replacingOccurrences(of: "\\\"", with: "\"")
}
// remove surrounding single quotes from value & convert remove escape character before any embedded quotes
if value[value.startIndex] == "'" && value[value.index(before: value.endIndex)] == "'" {
value.remove(at: value.startIndex)
value.remove(at: value.index(before: value.endIndex))
value = value.replacingOccurrences(of: "'", with: "'")
}
result[key] = value
}
return result
}
}
extension Parser {
static let plist: DefaultConfigurationProvider.Parser = { data in
let object = try PropertyListSerialization.propertyList(from: data, format: nil)
guard let values = object as? [String: Any] else {
throw InvalidFormat(data: data)
}
return values
}
}

View File

@ -1,3 +0,0 @@
import Conf
print("Hello, world!")

View File

@ -0,0 +1,72 @@
import XCTest
@testable import Conf
final class ConfigTests: XCTestCase {
var config: Config!
override func setUpWithError() throws {
config = Config()
}
func testEnvironmentuse() {
XCTAssertNil(Config(useEnvironment: false)["PATH"])
XCTAssertNotNil(Config(useEnvironment: true)["PATH"])
}
func testLoadConfigThrow() {
let errorConfigProvider = MockConfigurationProvider(config: [:], error: TestError())
XCTAssertThrowsError(try config.load(from: errorConfigProvider)) { error in
XCTAssertTrue(error is TestError)
}
}
func testLoadConfigSuccess() throws {
let loader = MockConfigurationProvider(config: ["key": "value"])
try config.load(from: loader)
XCTAssertEqual(config.dump(), [Key("key"): "value"])
}
func testWrite() {
let key: Key = "key"
XCTAssertEqual(config.dump(), [:])
config.set(value: "value", for: key)
XCTAssertEqual(config.dump(), [Key("key"): "value"])
config.set(value: nil, for: key)
XCTAssertEqual(config.dump(), [:])
config.set(value: "another value", for: ["long", "path"])
XCTAssertEqual(config.dump(), [Key(["long", "path"]): "another value"])
config["subscript"] = "subscriptValue"
XCTAssertEqual(config.dump()["subscript"], "subscriptValue")
config["lossless"] = 42
XCTAssertEqual(config.dump()["lossless"], "42")
}
func testRead() throws {
let loader = MockConfigurationProvider(config: [
"string": "value",
"int": "42",
"double": "23.5"
])
try config.load(from: loader)
XCTAssertEqual(config["blabla"], nil)
XCTAssertEqual(config["string"], "value")
XCTAssertEqual(config["int"], "42")
XCTAssertEqual(config["int"], 42)
XCTAssertEqual(config["double"], "23.5")
XCTAssertEqual(config["double"], 23.5)
}
}
struct MockConfigurationProvider: ConfigurationProvider {
var config: [Key: String]
var error: Error?
func configuration() throws -> [Key: String] {
if let error = self.error { throw error }
return config
}
}

View File

@ -0,0 +1,125 @@
import XCTest
@testable import Conf
final class CommonConfigurationProviderTests: XCTestCase {
func testFetchError() {
let fetcher: DefaultConfigurationProvider.Fetcher = { throw TestError() }
let parser: DefaultConfigurationProvider.Parser = { _ in [:] }
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
XCTAssertThrowsError(try provider.configuration()) { error in
if case let ConfigurationError.fetch(detailError) = error {
XCTAssertTrue(detailError is TestError)
} else {
XCTFail("Invalid error returned \(error)")
}
}
}
func testParserError() {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in throw TestError() }
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
XCTAssertThrowsError(try provider.configuration()) { error in
if case let ConfigurationError.parse(detailError) = error {
XCTAssertTrue(detailError is TestError)
} else {
XCTFail("Invalid error returned \(error)")
}
}
}
func testDecodeError() {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let uuid = UUID()
let parser: DefaultConfigurationProvider.Parser = { data in
return [
"first": "value",
"second": uuid
]
}
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
XCTAssertThrowsError(try provider.configuration()) { error in
if case let ConfigurationError.decode(path: key, value: value) = error {
XCTAssertEqual(key, "second")
XCTAssertEqual(value as? UUID, uuid)
} else {
XCTFail("Invalid error returned \(error)")
}
}
}
func testDataFlow() throws {
let data = "string".data(using: .utf8)!
let fetcher: DefaultConfigurationProvider.Fetcher = { data }
let parser: DefaultConfigurationProvider.Parser = { parserInput in
XCTAssertEqual(parserInput, data)
return [:]
}
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
XCTAssertTrue(configuration.isEmpty)
}
func testDecodeValues() throws {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in
return [ "key": 22]
}
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
XCTAssertEqual(configuration, ["key": "22"])
}
func testDecodeArray() throws {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in
return [ "key": ["one", "two"]]
}
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
let expect: [Key: String] = [
Key(["key", "0"]): "one",
Key(["key", "1"]): "two"
]
XCTAssertEqual(configuration, expect)
}
func testDecodeNestedValues() throws {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in
return [
"key": [ "nested": "value" ],
"one": [ "more": [
"deep": "value"
]
]
]
}
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
let expect: [Key: String] = [
Key(["key", "nested"]): "value",
Key(["one", "more", "deep"]): "value"
]
XCTAssertEqual(configuration, expect)
}
func testDecodeEmptyValues() throws {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in [:] }
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
XCTAssertEqual(configuration, [:])
}
func testDecodeEmptyArray() throws {
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
let parser: DefaultConfigurationProvider.Parser = { _ in [ "key": []] }
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
let configuration = try provider.configuration()
XCTAssertEqual(configuration, [:])
}
}

View File

@ -0,0 +1,23 @@
import XCTest
@testable import Conf
final class DotEnvParserTests: XCTestCase {
func testSuccess() throws {
let content = try Resource(name: "valid", type: "env").data()
let result = try Parser.donEnv(content)
let expect = [
"TZ": "Europe/Moscow",
"PUID": "1026",
"PGID": "100",
"IP": "127.0.0.1"
]
XCTAssertEqual(result as? [String: String], expect)
}
func testError() throws {
let content = try Resource(name: "invalid", type: "env").data()
XCTAssertThrowsError(try Parser.donEnv(content)) { error in
XCTAssertTrue(error is Parser.InvalidFormat)
}
}
}

View File

@ -0,0 +1,121 @@
import XCTest
@testable import Conf
final class EnvironmentTests: XCTestCase {
static let sampleValues: [String: String] = [
"first": "string",
"integer": "1",
"float": "2.33",
"booleanTrue": "true",
"booleanFalse": "false",
"YES": "YES"
]
func testDump() throws {
let values = Self.sampleValues
let info = ProcessInfo.make(with: values)
let env = Environment(info: info)
let dumped = env.dump()
XCTAssertEqual(values, dumped)
}
func testRead() throws {
let info = ProcessInfo.make(with: Self.sampleValues)
let env = Environment(info: info)
XCTAssertNil(env.blabla)
XCTAssertNil(env["blabla"])
XCTAssertEqual(env.first, "string")
XCTAssertNil(env.first as Int?)
XCTAssertEqual(env["first"], "string")
XCTAssertNil(env["first"] as Int?)
XCTAssertEqual(env.integer as Int?, 1)
XCTAssertEqual(env.integer, "1")
XCTAssertEqual(env["integer"] as Int?, 1)
XCTAssertEqual(env["integer"], "1")
XCTAssertEqual(env.float, "2.33")
XCTAssertEqual(env.float as Double?, 2.33)
XCTAssertEqual(env.float as Float?, 2.33)
XCTAssertNil(env.float as Int?)
XCTAssertEqual(env["float"], "2.33")
XCTAssertEqual(env["float"] as Double?, 2.33)
XCTAssertEqual(env["float"] as Float?, 2.33)
XCTAssertNil(env["float"] as Int?)
XCTAssertEqual(env.booleanTrue, "true")
XCTAssertEqual(env.booleanTrue as Bool?, true)
XCTAssertNil(env.booleanTrue as Int?)
XCTAssertEqual(env["booleanTrue"], "true")
XCTAssertEqual(env["booleanTrue"] as Bool?, true)
XCTAssertNil(env["booleanTrue"] as Int?)
XCTAssertEqual(env.booleanFalse, "false")
XCTAssertEqual(env.booleanFalse as Bool?, false)
XCTAssertNil(env.booleanFalse as Int?)
XCTAssertEqual(env["booleanFalse"], "false")
XCTAssertEqual(env["booleanFalse"] as Bool?, false)
XCTAssertNil(env["booleanFalse"] as Int?)
XCTAssertEqual(env.YES, "YES")
XCTAssertNil(env.YES as Bool?)
}
func testAdd() {
let env = Environment(info: ProcessInfo.instance)
XCTAssertNil(env.key1)
env.key1 = "value1"
XCTAssertEqual(env.key1, "value1")
XCTAssertNil(env.key2)
env["key2"] = "value2"
XCTAssertEqual(env.key2, "value2")
}
func testRemove() {
let env = Environment(info: ProcessInfo.instance)
env.key1 = "value1"
XCTAssertEqual(env.key1, "value1")
env.key1 = nil
XCTAssertNil(env.key1)
env["key2"] = "value2"
XCTAssertEqual(env["key2"], "value2")
env["key2"] = nil
XCTAssertNil(env["key2"])
}
func testUpdate() {
let env = Environment(info: ProcessInfo.instance)
env.key1 = "value"
XCTAssertEqual(env.key1, "value")
env.key1 = "new value"
XCTAssertEqual(env.key1, "new value")
}
func testWrite() {
let env = Environment(info: ProcessInfo.instance)
let int = Int.random(in: Int.min...Int.max)
env.int = int
XCTAssertEqual(env.int, int)
let float = Float.random(in: 0...100)
env.float = float
XCTAssertEqual(env.float, float)
env.success = true
XCTAssertEqual(env.success, true)
env.failure = false
XCTAssertEqual(env.failure, false)
}
}

View File

@ -0,0 +1,14 @@
import XCTest
@testable import Conf
final class FileFetcherTests: XCTestCase {
func testSuccess() throws {
let load = Fetcher.file("Tests/Resources/valid.env")
_ = try load()
}
func testError() throws {
let load = Fetcher.file("file that not exist")
XCTAssertThrowsError(try load())
}
}

View File

@ -1,9 +0,0 @@
import XCTest
@testable import Conf
final class FooTests: XCTestCase {
func testFoo() throws {
}
}

View File

@ -0,0 +1,16 @@
import XCTest
@testable import Conf
final class JSONParserTests: XCTestCase {
func testSuccess() throws {
let content = try Resource(name: "valid", type: "json").data()
_ = try Parser.json(content)
}
func testError() throws {
let content = try Resource(name: "invalid", type: "json").data()
XCTAssertThrowsError(try Parser.json(content)) { error in
XCTAssertTrue(error is Parser.InvalidFormat)
}
}
}

View File

@ -0,0 +1,25 @@
import XCTest
@testable import Conf
final class KeyTests: XCTestCase {
func testCreation() {
let key1: Key = Key()
XCTAssertEqual(key1.path, [])
let key2: Key = "24"
XCTAssertEqual(key2.path, ["24"])
let key3: Key = 99
XCTAssertEqual(key3.path, ["99"])
let key4: Key = Key(23.4)
XCTAssertEqual(key4.path, ["23.4"])
let key5: Key = Key("some")
XCTAssertEqual(key5.path, ["some"])
let key6: Key = ["24", 72, 23.4, true]
XCTAssertEqual(key6.path, ["24", "72", "23.4", "true"])
let key7: Key = Key([1, 2, 3])
XCTAssertEqual(key7.path, ["1", "2", "3"])
}
func testChild() {
XCTAssertEqual(Key("hello").child(key: "world").path, ["hello", "world"])
}
}

View File

@ -0,0 +1,42 @@
import Foundation
#if os(macOS)
class MockProcessInfo: ProcessInfo {
init(_ env: [String: String]) { self.env = env }
var env: [String: String]
override var environment: [String: String] { env }
}
#endif
extension ProcessInfo {
static var instance: ProcessInfo {
#if os(macOS)
return ProcessInfo()
#else
return ProcessInfo.processInfo
#endif
}
static func make(with env: [String: String]) -> ProcessInfo {
#if os(macOS)
return MockProcessInfo(env)
#else
let info = ProcessInfo.instance
info.clearEnv()
info.set(env: env)
return info
#endif
}
func clearEnv() {
environment.forEach {key, _ in
unsetenv(key)
}
}
func set(env: [String: String]) {
env.forEach { key, value in
setenv(key, value, 1)
}
}
}

View File

@ -0,0 +1,16 @@
import XCTest
@testable import Conf
final class PlistParserTests: XCTestCase {
func testSuccess() throws {
let content = try Resource(name: "valid", type: "plist").data()
_ = try Parser.plist(content)
}
func testError() throws {
let content = try Resource(name: "invalid", type: "plist").data()
XCTAssertThrowsError(try Parser.plist(content)) { error in
XCTAssertTrue(error is Parser.InvalidFormat)
}
}
}

View File

@ -0,0 +1,37 @@
import Foundation
struct Resource {
let name: String
let type: String
let url: URL
init(name: String, type: String) {
self.name = name
self.type = type
url = Resource.resourceFolderURL.appendingPathComponent(name).appendingPathExtension(type)
}
}
// MARK: - Content -
extension Resource {
func data() throws -> Data { try Data(contentsOf: url) }
func string() throws -> String { try String(contentsOf: url, encoding: .utf8) }
}
// MARK: - Path helpers -
extension Resource {
// expected folder structure
// <Some folder>
// - <Resources>
// - <resource files>
// - <Some test source folder>
// - <test case files>
// - <this file>
static let resourceFolderURL = testsFolderURL
.deletingLastPathComponent()
.appendingPathComponent(resourceFolder, isDirectory: true)
.standardized
static let testsFolderURL = sourceFileURL.deletingLastPathComponent()
private static let resourceFolder = "Resources"
private static let sourceFileURL = URL(fileURLWithPath: #file, isDirectory: false)
}

View File

@ -0,0 +1 @@
struct TestError: Error {}

View File

@ -1,43 +0,0 @@
import XCTest
import class Foundation.Bundle
final class OutputTests: XCTestCase {
func testExample() 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("DemoApp")
let process = Process()
process.executableURL = fooBinary
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "Hello, world!\n")
}
/// Returns path to the built products directory.
var productsDirectory: URL {
#if os(macOS)
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
return bundle.bundleURL.deletingLastPathComponent()
}
fatalError("couldn't find the products directory")
#else
return Bundle.main.bundleURL
#endif
}
}

View File

@ -0,0 +1,2 @@
## Sample invalid env file
Hello

View File

@ -0,0 +1 @@
[1, 2, 3]

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>one</string>
<string>two</string>
</array>
</plist>

View File

@ -0,0 +1,8 @@
# Sample env file
TZ="Europe/Moscow"
IP='127.0.0.1'
# UNIX PUID and PGID, find with: id $USER
PUID=1026
PGID=100

View File

@ -0,0 +1,11 @@
{
"param": "value",
"nested": {
"key": "values"
},
"array": [
1,
2,
3
]
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>