NUnit プロジェクトのアセンブリのディレクトリ区切り文字を変更する


NUnit のプロジェクトファイル (*.nunit) は Windows の GUI で作成するとアセンブリのパスを指定するディレクトリ区切り文字が Windows 環境のもの,すなわち \ になります。このプロジェクトファイルを Linux や Mac OS 環境の NUnit で読み込もうとすると,アセンブリが見つからないという問題が起こります。

区切り文字を Unix 環境の / にしても Windows 環境では正常に読めるので,ファイルを編集して / にしてしまえば良いのですが,いつもついうっかり忘れてしまうので,自動化しましょう。

まず使う型を用意します。

// Choice なにそれおいしいの?
type Either<'a, 'b> =
   | Success of 'a
   | Failure of 'b

type Error =
   | None = 0
   | IO = 1
   | InvalidArgument = 2
   | InvalidInput = 3

フレームワークにあらかじめ用意されている Choice を使わずに独自に Either 型を定義していますが,本当は Choice を使った方が良いみたいです

Error はコマンドの終了コードです。次の関数の引数として使います。

// exit : Error -> unit
let exit = EnumToValue >> Environment.Exit

次にプロジェクトファイルを編集する関数です。編集する関数の実装とユーザー入力を受け取って,編集した結果を返します。ユーザー入力はここでは NUnit のプロジェクトを想定していますが,汎化しています。

// doEdit : ('a -> 'b) -> Either<'a, exn * Error> -> Either<'b, exn * Error>
let doEdit edit =
   function
      | Success input ->
         try
            Success (edit input)
         with
            | ex -> Failure (ex, Error.InvalidInput)
      | Failure (ex, error) ->
         Failure (ex, error)

そしてそのユーザーの入力を受け取る関数です。

// getUserInput : unit -> Either<string, exn * Error>
let getUserInput () =
   match Environment.GetCommandLineArgs () with
      | [| _ |] ->
         try
            Success <| Console.In.ReadToEnd ()
         with
            | ex -> Failure (ex, Error.IO)
      | [| _; path |] ->
         try
            Success <| File.ReadAllText path
         with
            | ex -> Failure (ex, Error.IO)
      | args ->
         let filename = Path.GetFileName (args.[0])
         let message = sprintf "Usage: %s [project.nunit]" filename
         let ex = ApplicationException (message) :> exn
         Failure (ex, Error.InvalidArgument)

Environment.GetCommandLineArgs を使っているので最初の引数はプログラムの実行パスです。コンパイルしての利用を想定しているので上のようになっていますが, F# スクリプトとして利用する場合は fsi.CommandLineArgs を使えば良いです。スクリプト版は https://gist.github.com/3564010 に。 F# スクリプトとして作成した方が編集関数が簡単に差し替えられるので便利かもしれません。

ユーティリティーっぽく標準入力でも引数でファイルを指定してもよしなに取り計らってくれるようにしています。すなわち標準入力ならその入力はプロジェクトファイルの中身であると想定し,引数が与えられればプロジェクトファイルのパスであると解釈します。

実際の流れは次のような感じになります。

getUserInput ()
|> doEdit editProject
|> function
   | Success result ->
      Console.WriteLine (result)
      Error.None
   | Failure (ex, error) ->
      Console.Error.WriteLine (ex.Message)
      error
|> exit

ユーティリティーっぽいので標準出力に変換後の XML を出力しています。インデント幅を 3 にしているのでパイプラインの途中の function で f の下に | が来て美しいですね。インデント幅 3 は美しい。みんな採用すれば良いのに。

肝心の実際の編集関数 editProject は次の通りです。

let editProject rawProjectXml =
   let project = XDocument.Parse (rawProjectXml)
   query {
      for assembly in project.Descendants (XName.Get ("assembly")) do
      select (assembly.Attribute (XName.Get ("path")))
   }
   // UNIX だとディレクトリの区切り文字は '\' だとうまくいかないが Windows だと '/' でも OK。
   |> Seq.iter (fun attribute -> attribute.Value <- attribute.Value.Replace ('\\', '/'))
   project

プロジェクトファイルは XML ファイルなので LINQ to XML で簡単に編集できてしまいます。設計上 XML 文字列を受け取って XDocument を返すような関数になっています。 XDocumentToString で XML 文字列になるので Console.WriteLine にそのまま渡せて便利ですね。