Skip to content

Commit

Permalink
Fix delimiter remainder and add capability to split on other delimite…
Browse files Browse the repository at this point in the history
…rs (#26)


---------

Co-authored-by: wti <[email protected]>
  • Loading branch information
GeorgeLyon and wti authored Mar 22, 2024
1 parent edc8e76 commit f73c481
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

"mounts": [
// Use a named volume for the build products for optimal performance (https://code.visualstudio.com/remote/advancedcontainers/improve-performance?WT.mc_id=javascript-14373-yolasors#_use-a-targeted-named-volume)
"source=${localWorkspaceFolderBasename}-build,target=${containerWorkspaceFolder}/build,type=volume"
"source=${localWorkspaceFolderBasename}-build,target=${containerWorkspaceFolder}/.build,type=volume"
],
"remoteEnv": {
// Useful for disambiguating devcontainers
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
/*.xcodeproj
xcuserdata/
.vscode
/build
.swiftpm
.xcode
32 changes: 31 additions & 1 deletion Sources/Shwift/Builtins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ extension Builtin {
public struct Lines: AsyncSequence {
public typealias Element = String

public struct AsyncIterator: AsyncIteratorProtocol {
public mutating func next() async throws -> String? {
try await segments.next()
}
fileprivate var segments: Segments.AsyncIterator
}
public func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(segments: segments.makeAsyncIterator())
}

fileprivate init(byteBuffers: ByteBuffers) {
segments = Segments(byteBuffers: byteBuffers, delimiter: "\n")
}
private let segments: Segments
}

public struct Segments: AsyncSequence {
public typealias Element = String

public struct AsyncIterator: AsyncIteratorProtocol {
public mutating func next() async throws -> String? {
try await iterator.next()
Expand All @@ -105,7 +124,7 @@ extension Builtin {
at: buffer.readerIndex,
length: buffer.readableBytes)!
var substring = readString[readString.startIndex...]
while let lineBreak = substring.firstIndex(of: "\n") {
while let lineBreak = substring.firstIndex(of: delimiter) {
let line = substring[substring.startIndex..<lineBreak]
substring = substring[substring.index(after: lineBreak)...]
continuation.yield(remainder + String(line))
Expand All @@ -126,11 +145,22 @@ extension Builtin {
}

fileprivate let byteBuffers: ByteBuffers
fileprivate let delimiter: Character
}

/// Make a Lines iterator splitting at newlines
public var lines: Lines {
Lines(byteBuffers: byteBuffers)
}

/// Make a Lines iterator yielding text segments between delimiters (like split).
///
/// - Parameter delimiter: Character separating input text to yield (and not itself yielded) Defaults to newline.
/// - Returns: Lines segmented by delimiter
public func segments(separatedBy delimiter: Character) -> Segments {
Segments(byteBuffers: byteBuffers, delimiter: delimiter)
}

typealias ByteBuffers = AsyncCompactMapSequence<
AsyncPrefixWhileSequence<AsyncInboundHandler<ByteBuffer>>, ByteBuffer
>
Expand Down
12 changes: 12 additions & 0 deletions Sources/Shwift/Support/Posix Spawn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ enum PosixSpawn {
) throws -> pid_t {
var pid = pid_t()

/// I'm not aware of a way to pass a string containing `NUL` to `posix_spawn`
for string in [arguments, environment].flatMap({ $0 }) {
if string.contains("\0") {
throw InvalidParameter(parameter: string, issue: "contains NUL character")
}
}

let cArguments = arguments.map { $0.withCString(strdup)! }
defer { cArguments.forEach { $0.deallocate() } }
let cEnvironment = environment.map { $0.withCString(strdup)! }
Expand Down Expand Up @@ -158,3 +165,8 @@ private extension Errno {
}
}
}

private struct InvalidParameter: Error {
let parameter: String
let issue: String
}
163 changes: 95 additions & 68 deletions Tests/ShwiftTests/Shwift Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import SystemPackage

final class ShwiftCoreTests: XCTestCase {

func testExecutable() throws {
try XCTAssertOutput(
func testExecutable() async throws {
try await XCTAssertOutput(
of: { context, standardOutput in
try await Process.run("echo", "Echo", standardOutput: standardOutput, in: context)
},
is: """
Echo
""")

try XCTAssertOutput(
try await XCTAssertOutput(
of: { context, standardOutput in
try await Process.run(
"cat", Self.supportFilePath, standardOutput: standardOutput, in: context)
Expand All @@ -22,7 +22,7 @@ final class ShwiftCoreTests: XCTestCase {
Cat
""")

try XCTAssertOutput(
try await XCTAssertOutput(
of: { context, standardOutput in
try await Process.run("echo", "Echo", standardOutput: standardOutput, in: context)
try await Process.run(
Expand All @@ -34,16 +34,16 @@ final class ShwiftCoreTests: XCTestCase {
""")
}

func testFailure() throws {
try XCTAssertOutput(
func testFailure() async throws {
try await XCTAssertOutput(
of: { context, _ in
try await Process.run("false", in: context)
},
is: .failure)
}

func testExecutablePipe() throws {
try XCTAssertOutput(
func testExecutablePipe() async throws {
try await XCTAssertOutput(
of: { context, output in
try await Builtin.pipe(
{ output in
Expand All @@ -60,8 +60,32 @@ final class ShwiftCoreTests: XCTestCase {
""")
}

func testBuiltinOutput() throws {
try XCTAssertOutput(
func testInputSegmentation() async throws {
try await XCTAssertOutput(
of: { context, output in
try await Builtin.pipe(
{ output in
try await Process.run("echo", "Foo;Bar;Baz", standardOutput: output, in: context)
},
to: { input in
try await Builtin.withChannel(input: input, output: output, in: context) { channel in
let count = try await channel.input
.segments(separatedBy: ";")
.reduce(into: 0, { count, _ in count += 1 })
try await channel.output.withTextOutputStream { stream in
print("\(count)", to: &stream)
}
}
}
).destination
},
is: """
3
""")
}

func testBuiltinOutput() async throws {
try await XCTAssertOutput(
of: { context, output in
try await Input.nullDevice.withFileDescriptor(in: context) { input in
try await Builtin.withChannel(input: input, output: output, in: context) { channel in
Expand All @@ -76,8 +100,8 @@ final class ShwiftCoreTests: XCTestCase {
""")
}

func testReadFromFile() throws {
try XCTAssertOutput(
func testReadFromFile() async throws {
try await XCTAssertOutput(
of: { context, output in
try await Builtin.read(from: FilePath(Self.supportFilePath), to: output, in: context)
},
Expand All @@ -99,56 +123,58 @@ final class ShwiftCoreTests: XCTestCase {
is expectedOutcome: Outcome,
file: StaticString = #file, line: UInt = #line,
function: StaticString = #function
) throws {
let e1 = expectation(description: "\(function):\(line)")
let e2 = expectation(description: "\(function):\(line)")
) async throws {
let e1 = expectation(description: "\(function):\(line)-operation")
let e2 = expectation(description: "\(function):\(line)-gather")
let context = Context()
Task {
do {
let output: String = try await Builtin.pipe(
{ output in
defer { e1.fulfill() }
try await operation(context, output)
},
to: { input in
defer { e2.fulfill() }
do {
return try await Output.nullDevice.withFileDescriptor(in: context) { output in
try await Builtin.withChannel(input: input, output: output, in: context) {
channel in
return try await channel.input.lines
.reduce(into: [], { $0.append($1) })
.joined(separator: "\n")
}
do {
let output: String = try await Builtin.pipe(
{ output in
defer {
e1.fulfill()
}
try await operation(context, output)
},
to: { input in
defer {
e2.fulfill()
}
do {
return try await Output.nullDevice.withFileDescriptor(in: context) { output in
try await Builtin.withChannel(input: input, output: output, in: context) {
channel in
let x = try await channel.input.lines
.reduce(into: [], { $0.append($1) })
.joined(separator: "\n")
return x
}
} catch {
XCTFail(file: file, line: line)
throw error
}
} catch {
XCTFail(file: file, line: line)
throw error
}
)
.destination
switch expectedOutcome {
case .success(let expected):
XCTAssertEqual(
output,
expected,
file: file, line: line)
case .failure:
XCTFail("Succeeded when expecting failure", file: file, line: line)
}
} catch {
switch expectedOutcome {
case .success:
throw error
case .failure:
/// Failure was expected
break
}
)
.destination
switch expectedOutcome {
case .success(let expected):
XCTAssertEqual(
output,
expected,
file: file, line: line)
case .failure:
XCTFail("Succeeded when expecting failure", file: file, line: line)
}
} catch {
switch expectedOutcome {
case .success:
throw error
case .failure:
/// Failure was expected
break
}

}
wait(for: [e1, e2], timeout: 2)
await fulfillment(of: [e1, e2], timeout: 2)
}

private static let supportFilePath = Bundle.module.path(forResource: "Cat", ofType: "txt")!
Expand All @@ -163,19 +189,20 @@ private extension Shwift.Process {
standardOutput: FileDescriptor? = nil,
in context: Context
) async throws {
var fileDescriptorMapping = FileDescriptorMapping()
if let standardInput = standardInput {
fileDescriptorMapping.addMapping(from: standardInput, to: STDIN_FILENO)
}
if let standardOutput = standardOutput {
fileDescriptorMapping.addMapping(from: standardOutput, to: STDOUT_FILENO)
try await Input.nullDevice.withFileDescriptor(in: context) { inputNullDevice in
try await Output.nullDevice.withFileDescriptor(in: context) { outputNullDevice in
let fileDescriptorMapping: FileDescriptorMapping = [
STDIN_FILENO: standardInput ?? inputNullDevice,
STDOUT_FILENO: standardOutput ?? outputNullDevice,
]
try await run(
executablePath: environment.searchForExecutables(named: executableName).matches.first!,
arguments: arguments,
environment: [:],
workingDirectory: FilePath(FileManager.default.currentDirectoryPath),
fileDescriptorMapping: fileDescriptorMapping,
in: context)
}
}
try await run(
executablePath: environment.searchForExecutables(named: executableName).matches.first!,
arguments: arguments,
environment: [:],
workingDirectory: FilePath(FileManager.default.currentDirectoryPath),
fileDescriptorMapping: fileDescriptorMapping,
in: context)
}
}

0 comments on commit f73c481

Please sign in to comment.