Skip to content

Commit

Permalink
Go-like API
Browse files Browse the repository at this point in the history
  • Loading branch information
kevgo committed Oct 3, 2020
1 parent 8595630 commit 2bdaea6
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 85 deletions.
41 changes: 22 additions & 19 deletions src/observable-process.ts → src/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import mergeStream = require("merge-stream")
import * as util from "util"

import { Result } from "./result"
import { createSearchableStream, SearchableStream } from "./searchable-stream"
import * as searchableStream from "./searchable-stream"
const delay = util.promisify(setTimeout)

/** Signature of the function to call when the process has ended */
export type EndedCallback = (result: Result) => void

/** a long-running process whose behavior can be observed at runtime */
export class ObservableProcess {
export class Class {
/** the underlying ChildProcess instance */
process: childProcess.ChildProcess
childProcess: childProcess.ChildProcess

/** populated when the process finishes */
private result: Result | undefined
Expand All @@ -18,38 +21,38 @@ export class ObservableProcess {
stdin: NodeJS.WritableStream

/** searchable STDOUT stream of the underlying ChildProcess */
stdout: SearchableStream
stdout: searchableStream.SearchableStream

/** searchable STDERR stream of the underlying ChildProcess */
stderr: SearchableStream
stderr: searchableStream.SearchableStream

/** searchable combined STDOUT and STDERR stream */
output: SearchableStream
output: searchableStream.SearchableStream

/** functions to call when this process ends */
private endedCallbacks: Array<(result: Result) => void>
private endedCallbacks: Array<EndedCallback>

constructor(args: { cwd: string; env: NodeJS.ProcessEnv; params: string[]; runnable: string }) {
this.endedCallbacks = []
this.process = childProcess.spawn(args.runnable, args.params, {
this.childProcess = childProcess.spawn(args.runnable, args.params, {
cwd: args.cwd,
env: args.env,
})
this.process.on("close", this.onClose.bind(this))
if (this.process.stdin == null) {
this.childProcess.on("close", this.onClose.bind(this))
if (this.childProcess.stdin == null) {
throw new Error("process.stdin should not be null") // this exists only to make the typechecker shut up
}
this.stdin = this.process.stdin
if (this.process.stdout == null) {
this.stdin = this.childProcess.stdin
if (this.childProcess.stdout == null) {
throw new Error("process.stdout should not be null") // NOTE: this exists only to make the typechecker shut up
}
this.stdout = createSearchableStream(this.process.stdout)
if (this.process.stderr == null) {
this.stdout = searchableStream.createSearchableStream(this.childProcess.stdout)
if (this.childProcess.stderr == null) {
throw new Error("process.stderr should not be null") // NOTE: this exists only to make the typechecker shut up
}
this.stderr = createSearchableStream(this.process.stderr)
const outputStream = mergeStream(this.process.stdout, this.process.stderr)
this.output = createSearchableStream(outputStream)
this.stderr = searchableStream.createSearchableStream(this.childProcess.stderr)
const outputStream = mergeStream(this.childProcess.stdout, this.childProcess.stderr)
this.output = searchableStream.createSearchableStream(outputStream)
}

/** stops the currently running process */
Expand All @@ -61,14 +64,14 @@ export class ObservableProcess {
errText: this.stderr.fullText(),
combinedText: this.output.fullText(),
}
this.process.kill()
this.childProcess.kill()
await delay(1)
return this.result
}

/** returns the process ID of the underlying ChildProcess */
pid(): number {
return this.process.pid
return this.childProcess.pid
}

/** returns a promise that resolves when the underlying ChildProcess terminates */
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { ObservableProcess } from "./observable-process"
export { Class } from "./class"
export { Result } from "./result"
export { SearchableStream } from "./searchable-stream"
export { start } from "./start"
6 changes: 3 additions & 3 deletions src/start.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import stringArgv from "string-argv"

import { ObservableProcess } from "./observable-process"
import * as observableProcess from "./class"

/** The options that can be provided to Spawn */
export interface SpawnOptions {
Expand All @@ -9,7 +9,7 @@ export interface SpawnOptions {
}

/** starts a new ObservableProcess with the given options */
export function start(command: string | string[], args: SpawnOptions = {}): ObservableProcess {
export function start(command: string | string[], args: SpawnOptions = {}): observableProcess.Class {
// determine args
if (!command) {
throw new Error("start: no command to execute given")
Expand All @@ -25,7 +25,7 @@ export function start(command: string | string[], args: SpawnOptions = {}): Obse
const [runnable, ...params] = argv

// start the process
return new ObservableProcess({
return new observableProcess.Class({
cwd: args.cwd || process.cwd(),
env: args.env || process.env,
params,
Expand Down
6 changes: 0 additions & 6 deletions test/helpers/start-node-process.ts

This file was deleted.

10 changes: 6 additions & 4 deletions test/input-test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { strict as assert } from "assert"

import { startNodeProcess } from "./helpers/start-node-process"
import * as observableProcess from "../src/index"

test("ObservableProcess.stdin", async function () {
// start a process that reads from STDIN
const running = startNodeProcess(
const running = observableProcess.start([
"node",
"-e",
`process.stdin.on("data", data => { process.stdout.write(data) });\
process.stdin.on("end", () => { process.stdout.write("\\nEND") })`
)
process.stdin.on("end", () => { process.stdout.write("\\nEND") })`,
])

// write some stuff into the STDIN stream of this process
running.stdin.write("hello")
Expand Down
14 changes: 8 additions & 6 deletions test/kill-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ import { strict as assert } from "assert"
import got from "got"
import * as portFinder from "portfinder"

import { startNodeProcess } from "./helpers/start-node-process"
import * as observableProcess from "../src/index"

test("ObservableProcess.kill()", async function () {
this.timeout(8000)

// start a long-running process
const port = await portFinder.getPortPromise()
const longRunningProcess = startNodeProcess(
const process = observableProcess.start([
"node",
"-e",
`http = require('http');\
http.createServer(function(_, res) { res.end('hello') }).listen(${port}, 'localhost');\
console.log('online')`
)
await longRunningProcess.stdout.waitForText("online")
console.log('online')`,
])
await process.stdout.waitForText("online")
await assertIsRunning(port)

// kill the process
const result = await longRunningProcess.kill()
const result = await process.kill()

// verify the process is no longer running
await assertIsNotRunning(port)
Expand Down
36 changes: 24 additions & 12 deletions test/output-test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
import { strict as assert } from "assert"

import { startNodeProcess } from "./helpers/start-node-process"
import * as observableProcess from "../src/index"

suite("ObservableProcess.output")

test("reading", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
await observable.waitForEnd()
assert.equal(observable.output.fullText(), "helloworld")
const process = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
await process.waitForEnd()
assert.equal(process.output.fullText(), "helloworld")
})

test("waiting for text", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
const text = await observable.output.waitForText("helloworld")
const process = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
const text = await process.output.waitForText("helloworld")
assert.equal(text, "helloworld")
})

test("waiting for text times out", async function () {
const observable = startNodeProcess("setTimeout(function() {}, 3)")
const promise = observable.output.waitForText("hello", 1)
const process = observableProcess.start(["node", "-e", "setTimeout(function() {}, 3)"])
const promise = process.output.waitForText("hello", 1)
await assert.rejects(promise, new Error('Text "hello" not found within 1 ms. The captured text so far is:\n'))
})

test("waiting for regex", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
const text = await observable.output.waitForRegex(/h.+d/)
const process = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
const text = await process.output.waitForRegex(/h.+d/)
assert.equal(text, "helloworld")
})

test("waiting for regex times out", async function () {
const observable = startNodeProcess("setTimeout(function() {}, 3)")
const promise = observable.output.waitForRegex(/h.+d/, 1)
const process = observableProcess.start(["node", "-e", "setTimeout(function() {}, 3)"])
const promise = process.output.waitForRegex(/h.+d/, 1)
await assert.rejects(promise, new Error("Regex /h.+d/ not found within 1 ms. The captured text so far is:\n"))
})
8 changes: 4 additions & 4 deletions test/pid-test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { strict as assert } from "assert"

import { startNodeProcess } from "./helpers/start-node-process"
import * as observableProcess from "../src/index"

test("ObservableProcess.pid()", async function () {
const oProcess = startNodeProcess("setTimeout(function() {}, 1)")
const pid = oProcess.pid()
const process = observableProcess.start(["node", "-e", "setTimeout(function() {}, 1)"])
const pid = process.pid()
assert.equal(typeof pid, "number")
assert.ok(pid > 0)
await oProcess.waitForEnd()
await process.waitForEnd()
})
14 changes: 7 additions & 7 deletions test/start-test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
import { strict as assert } from "assert"

import { start } from "../src/start"
import * as observableProcess from "../src/index"

suite("ObservableProcess.spawn()")

test("starting a process via an argv array", async function () {
const observable = start(["node", "-e", "console.log('hello')"])
const observable = observableProcess.start(["node", "-e", "console.log('hello')"])
const result = await observable.waitForEnd()
assert.equal(result.exitCode, 0)
})

test("starting a process via a string", async function () {
const observable = start("node -e console.log('hello')")
const observable = observableProcess.start("node -e console.log('hello')")
const result = await observable.waitForEnd()
assert.equal(result.exitCode, 0)
})

test("starting processes in the path", async function () {
const observable = start("node -h")
const observable = observableProcess.start("node -h")
const result = await observable.waitForEnd()
assert.equal(result.exitCode, 0)
})

test("no command to run", function () {
assert.throws(function () {
// @ts-ignore
start()
observableProcess.start()
}, new Error("start: no command to execute given"))
})

test("wrong argument type", function () {
assert.throws(function () {
// @ts-ignore
start(1)
observableProcess.start(1)
}, new Error("start: you must provide the command to run as a string or string[]"))
})

test("providing environment variables", async function () {
const observable = start(["node", "-e", "console.log('foo:', process.env.foo)"], {
const observable = observableProcess.start(["node", "-e", "console.log('foo:', process.env.foo)"], {
env: { foo: "bar", PATH: process.env.PATH },
})
await observable.waitForEnd()
Expand Down
24 changes: 18 additions & 6 deletions test/stderr-test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
import { strict as assert } from "assert"

import { startNodeProcess } from "./helpers/start-node-process"
import * as observableProcess from "../src/index"

suite("ObservableProcess.stderr")

test("reading", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
const observable = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
await observable.waitForEnd()
assert.equal(observable.stderr.fullText(), "world")
})

test("waiting for text", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
const observable = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
const text = await observable.stderr.waitForText("world")
assert.equal(text, "world")
})

test("waiting for text times out", async function () {
const observable = startNodeProcess("setTimeout(function() {}, 10)")
const observable = observableProcess.start(["node", "-e", "setTimeout(function() {}, 10)"])
const promise = observable.stderr.waitForText("hello", 1)
await assert.rejects(promise, new Error('Text "hello" not found within 1 ms. The captured text so far is:\n'))
})

test("waiting for regex", async function () {
const observable = startNodeProcess('process.stdout.write("hello"); process.stderr.write("world")')
const observable = observableProcess.start([
"node",
"-e",
'process.stdout.write("hello"); process.stderr.write("world")',
])
const text = await observable.stderr.waitForRegex(/w.+d/)
assert.equal(text, "world")
})

test("waiting for regex times out", async function () {
const observable = startNodeProcess("setTimeout(function() {}, 10)")
const observable = observableProcess.start(["node", "-e", "setTimeout(function() {}, 10)"])
const promise = observable.stderr.waitForRegex(/w.+d/, 1)
await assert.rejects(promise, new Error("Regex /w.+d/ not found within 1 ms. The captured text so far is:\n"))
})
Loading

0 comments on commit 2bdaea6

Please sign in to comment.