Friday, April 11, 2008

Sample F# Test Runner (and C# too...)

While I was fooling around with F# and testing my functions, I got really annoyed with switching between applications to run my tests, Visual Studio and NUnit Gui. The source of my fustration is during the day I get to use a fantastic tool TestDriven.Net to run my unit tests in C# and Visual Studio. With F#, however, TestDriven.Net does not recognize the different syntax and no tests run. So I decided to write my own test runner, by taking advantage of .Net's FileSystemWatcher class. (Note, I used the FileSystemWatcher.Changed event.1 ) The first iteration I wrote in C# so I could have an excuse to use xUnit. I probably over engineered the C# code, but old habits die hard. In the C# implementation, there are three files. The program class sets everything up, the HarnessRunner class sets up the filesystem watcher and listens for the file changed event, and finally the ProcessStarter to run the process. The application is configurable to use the app.config or to pass any necessary commands to the program. ( Hence a little of the bloat.)
using System;
using System.IO;

namespace HarnessRunner
{
    public class program
    {
        [STAThread]
        public static void Main(string[] args)
        {
            Settings settings = Settings.Default;

            if (ValidateInputValue(settings.TestRunnerCommand))
            {
                Console.WriteLine("Enter the fully qualified command to run:\r\n");
                settings.TestRunnerCommand = Console.ReadLine();
            }

            if (ValidateInputValue(settings.TestAssembly))
            {
                Console.WriteLine("Enter the file to test:\r\n");
                settings.TestAssembly = Console.ReadLine();
            }

            if (ValidateInputValue(settings.TestRunnerSwitches))
            {
                Console.WriteLine("Enter any swtiches to the test runner:\r\n");
                settings.TestRunnerSwitches = Console.ReadLine();
            }
                     
            Console.WriteLine("Setting up the watcher to run: \r\n{0} {1} {2}", Path.GetFileName(settings.TestRunnerCommand),
                              Path.GetFileName(settings.TestAssembly),settings.TestRunnerSwitches);
            try
            {
                HarnessRunner runner = new HarnessRunner(new FileSystemWatcher(), new ProcessStarter());
                runner.InitializeFileSystemWatcher(settings.TestAssembly);
            }
            catch (Exception e)
            {
                Console.WriteLine("There was an exception during the run: {0}{1}{2}", e.Message, Environment.NewLine,
                                  e.StackTrace);
            }
            Console.ReadLine();
        }

        private static bool ValidateInputValue(string command)
        {
            return string.Compare(command, string.Empty) == 0;
        }
    }
}
using System;
using System.Diagnostics;
using System.IO;

namespace HarnessRunner
{
    public interface IStartTestHarnesses
    {
        string Start();
    }

    public interface IFileSystemWatcher
    {
        bool EnableRaisingOfEvents { get; set; }
        string Path { get; set; }
        NotifyFilters NotifyFilter { get; set; }
        event EventHandler Changed;
    }

    public class ProcessStarter : IStartTestHarnesses
    {
        public string Start()
        {
            Settings settings = Settings.Default;
            Process process = new Process();
            process.StartInfo.FileName = settings.TestRunnerCommand;
            process.StartInfo.Arguments = settings.TestAssembly + " " + settings.TestRunnerSwitches;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.RedirectStandardOutput = true;
            process.Start();

            return process.StandardOutput.ReadToEnd();
        }
    }
}
using System;
using System.IO;

namespace HarnessRunner
{
    public class HarnessRunner
    {
        private FileSystemWatcher fileSystemWatcher;
        private IStartTestHarnesses startTestHarnesses;

        public HarnessRunner(FileSystemWatcher fileSystemWatcher, IStartTestHarnesses startTestHarnesses)
        {
            this.fileSystemWatcher = fileSystemWatcher;
            this.startTestHarnesses = startTestHarnesses;
            fileSystemWatcher.Changed += fileSystemWatcher_Changed;
        }

        private void fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(startTestHarnesses.Start());
        }

        public void InitializeFileSystemWatcher(string filetowatch)
        {
            fileSystemWatcher.Path = Path.GetDirectoryName(filetowatch);
            fileSystemWatcher.Filter = Path.GetFileName(filetowatch);
            fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite;
            fileSystemWatcher.EnableRaisingEvents = true;
        }
    }
}

So, once I had that working, I decided to write the F# equivalent. There are two functions, one that starts the process and one to setup the filesystemwatcher. Other than that, there are just some config options.
#light

open System
open System.IO
open System.Diagnostics
open System.Configuration

let mutable command = ConfigurationManager.AppSettings.Item("TestRunnerCommand")
let mutable testAssembly = ConfigurationManager.AppSettings.Item("TestAssembly")
let mutable commandSwitches = ConfigurationManager.AppSettings.Item("TestRunnerSwitches")

let startProcess f =
     let p = new Process()
     p.StartInfo.FileName <- command
     p.StartInfo.Arguments <- f ^" "^commandSwitches
     p.StartInfo.UseShellExecute <- false
     p.StartInfo.RedirectStandardOutput <- true
     let started = p.Start()
     printfn "%O" (p.StandardOutput.ReadToEnd())

let SetupFileSystemWatcher f =
     let fileSystemWatcher = new FileSystemWatcher()
     fileSystemWatcher.Path <- System.Environment.CurrentDirectory
     fileSystemWatcher.Filter <- f
     fileSystemWatcher.NotifyFilter <- NotifyFilters.LastWrite
     fileSystemWatcher.EnableRaisingEvents <- true
     fileSystemWatcher.Changed.Add(fun _ -> startProcess f)
   
if command = ""  command = null then
     printfn "Enter the fully qualified command to run:\r\n"
     command <- Console.ReadLine()       
if testAssembly = ""  testAssembly = null then      
     printfn "Enter the file to test:\r\n"
     testAssembly <- Console.ReadLine()
if commandSwitches = ""  commandSwitches = null then
      printfn "Enter any swtiches to the test runner:\r\n"
      commandSwitches <- Console.ReadLine()

printfn "Setting up watcher for %A" testAssembly  
SetupFileSystemWatcher testAssembly 
read_line() 
So, now that the code is posted, I'm left with some sort of follow up point to wrap this post up. The only problem is, I can't seem to come up with any points! I put together this article to show a program that performs the same function, implemented in two separate languages. Comments, feedback and discussions are welcome! 1 For some reason, the FileSystemWatcher.Changed event fires three times when my assembly is compiled.

No comments: