Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Console
open Axial.FileSystem
open Axial.PlatformService
open Axial.Process

Run a process in an application Flow

An Axial Flow receives the application's environment record when it runs. If part of the application needs to start commands, put a process service in that record. The Flow reads that service when it reaches a process step.

In production, the record contains Axial's live process service. In a unit test, it can contain a small fake that returns the result the test needs. The application code that builds the command does not change.

Put a process service in the application record

Process.live builds the real service. It is a function because it combines the services it needs. Clock.live, FileSystem.live, and Console.live are already-created service values, so they are its arguments.

open Axial.Console
open Axial.FileSystem
open Axial.PlatformService
open Axial.Process

type AppEnvironment =
    { Processes: IProcess }
    interface IHasProcess with member this.Process = this.Processes

let appEnvironment =
    { Processes = Process.live Clock.live FileSystem.live Console.live }

The clock timestamps execution. The filesystem opens file input and output targets. The console receives configured console output. The live process builder combines them into the Processes value; it does not start a process.

Convert a command to a Flow

Process.toFlow makes the command one lazy step in the application workflow. Flow.run starts the complete application Flow with its environment.

let readRevision : Flow<AppEnvironment, ProcessError, string> =
    flow {
        let! result =
            Process.command $"git rev-parse --short HEAD"
            |> Process.capture

        return result.StdOut.Trim()
    }

let outcome = Flow.run appEnvironment readRevision

Process.capture and Process.console are convenient forms that configure output and then create a Flow. The latter preserves the output policy already carried by a ProcessSpec.

Put a fake in the record for a unit test

IProcess is the interface used by the Processes field. It has two operations: Run returns the completed result, and Stream returns output events. A focused test usually needs only Run; return a prepared ProcessResult and inspect the received specification when the test cares about command construction.

let mutable requested = None

let fakeProcess =
    { new IProcess with
        member _.Run specification =
            requested <- Some specification
            Flow.succeed expectedResult

        member _.Stream _ =
            FlowStream.singleton (ProcessEvent.Completed expectedResult) }

let testEnvironment = { Processes = fakeProcess }
let outcome = Flow.run testEnvironment readRevision

This keeps a unit test deterministic: it starts no native child process, and it can assert the executable, arguments, working directory, timeout, and output policy on requested. Use the live interpreter in integration tests when the contract with the operating system matters.

Use an existing application environment at a host boundary

Process.runWith is for a command-line host that already owns a larger environment. It runs a ProcessError workflow with that record and converts failure to a process exit code. The environment must also supply IConsole, used for error reporting. Most applications instead call Flow.run themselves and handle the resulting Exit according to their own boundary policy.