open System open System.Diagnostics open System.IO type Result = { Code: int; Output: string; Error: string } type Fixture = { Root: string; Source: string; Mask: string } let require condition message = if not condition then invalidOp message let run root environment executable arguments = let start = ProcessStartInfo executable start.WorkingDirectory <- root start.UseShellExecute <- false start.RedirectStandardOutput <- true start.RedirectStandardError <- true for argument in arguments do start.ArgumentList.Add argument for name, value in environment do start.Environment[name] <- value use child = Process.Start start let output = child.StandardOutput.ReadToEndAsync() let error = child.StandardError.ReadToEndAsync() child.WaitForExit() { Code = child.ExitCode; Output = output.Result; Error = error.Result } let git fixture arguments = let result = run fixture.Root [] "git" arguments require (result.Code = 0) result.Error result.Output let write path (text: string) = File.WriteAllText(path, text) let stage fixture = git fixture [ "add"; "." ] |> ignore let hook = Path.Combine(__SOURCE_DIRECTORY__, "pre-commit") let runHook fixture environment = let before = git fixture [ "ls-files"; "--stage"; "-z" ] let result = run fixture.Root environment hook [] require (before = git fixture [ "ls-files"; "--stage"; "-z" ]) "hook changed the index" result let passes fixture = let result = runHook fixture [] require (result.Code = 0) result.Error let fails expected fixture = let result = runHook fixture [] require (result.Code <> 0) "hook unexpectedly succeeded" require (result.Error.Contains(expected: string)) result.Error let directory fixture path = Directory.CreateDirectory(Path.Combine(fixture.Root, path)).FullName let tests: (string * (Fixture -> unit)) list = [ "staged tree", fun fixture -> write fixture.Source "unstaged source edit\n" write fixture.Mask "unstaged invalid mask\n" passes fixture "unrelated change", fun fixture -> write fixture.Source "other=changed\nvalue=synthetic\n" stage fixture passes fixture "missing match", fun fixture -> write fixture.Source "value=changed\n" stage fixture fails "no match" fixture "multiple matches", fun fixture -> write fixture.Source "synthetic synthetic\n" stage fixture fails "multiple matches" fixture "overlapping matches", fun fixture -> write fixture.Source "aaa" write fixture.Mask "[[replace]]\nbefore = 'aa'\nafter = 'b'\n" stage fixture fails "multiple matches" fixture "empty before", fun fixture -> write fixture.Mask "[[replace]]\nbefore = ''\nafter = 'x'\n" stage fixture fails "before is empty" fixture "ordered rules", fun fixture -> write fixture.Mask ("[[replace]]\nbefore = 'synthetic'\nafter = 'intermediate'\n" + "[[replace]]\nbefore = 'intermediate'\nafter = '********'\n") stage fixture passes fixture "invalid TOML", fun fixture -> write fixture.Mask "invalid = [" stage fixture let result = runHook fixture [] require (result.Code <> 0) "invalid TOML was accepted" "invalid schema", fun fixture -> write fixture.Mask "[[replace]]\nbefore = 'synthetic'\nafter = 1\n" stage fixture fails "expected text" fixture "missing target", fun fixture -> git fixture [ "rm"; "--cached"; "config.txt" ] |> ignore fails "target is not a regular file" fixture "symlink target", fun fixture -> File.Delete fixture.Source File.CreateSymbolicLink(fixture.Source, "elsewhere") |> ignore stage fixture fails "target is not a regular file" fixture "omitted subtree", fun fixture -> let hidden = directory fixture "hidden" write (Path.Combine(hidden, ".omit")) "" write (Path.Combine(hidden, "invalid.mask.toml")) "invalid" let nested = directory fixture "hidden/nested" write (Path.Combine(nested, ".omit")) "" write (Path.Combine(nested, "legacy.mask.diff")) "invalid" stage fixture let result = runHook fixture [] require (result.Code = 0) result.Error require (result.Output.Contains "1 directories omitted") result.Output "omit prefix boundary", fun fixture -> let hidden = directory fixture "hidden" write (Path.Combine(hidden, ".omit")) "" let peer = directory fixture "hidden-peer" write (Path.Combine(peer, "invalid.mask.toml")) "invalid" stage fixture fails "target is not a regular file" fixture "unstaged omit", fun fixture -> write fixture.Source "synthetic synthetic" stage fixture write (Path.Combine(fixture.Root, ".omit")) "" fails "multiple matches" fixture "legacy format", fun fixture -> write (Path.Combine(fixture.Root, "old.mask.diff")) "invalid" stage fixture fails "legacy mask format" fixture "alternate index", fun fixture -> let index = Path.Combine(fixture.Root, "alternate") File.Copy(Path.Combine(fixture.Root, ".git/index"), index) write fixture.Source "staged drift\n" git fixture [ "add"; "config.txt" ] |> ignore let before = File.ReadAllBytes index let result = runHook fixture [ "GIT_INDEX_FILE", index ] require (result.Code = 0) result.Error require (before = File.ReadAllBytes index) "hook changed the alternate index" ] for name, test in tests do let root = Directory.CreateDirectory(Path.Combine(fsi.CommandLineArgs[1], $"hook-tests-{Guid.NewGuid():N}")).FullName let fixture = { Root = root; Source = Path.Combine(root, "config.txt"); Mask = Path.Combine(root, "config.txt.mask.toml") } try git fixture [ "init"; "-q" ] |> ignore write fixture.Source "value=synthetic\n" write fixture.Mask "[[replace]]\nbefore = 'synthetic'\nafter = '********'\n" stage fixture test fixture printfn "passed: %s" name finally Directory.Delete(root, true) printfn "%d hook tests passed" tests.Length