From 98f694537cb53605fb60b49cb0ae6e5133907411 Mon Sep 17 00:00:00 2001 From: Taka Date: Mon, 31 Jan 2022 10:21:03 +1100 Subject: [PATCH] Added code to verify what is being retrieved from a repo. --- cli/Program.fs | 32 +++++- git-guts/GitRepo.fs | 24 +++++ git-guts/Logs.fs | 4 +- git-guts/PackIndex.fs | 87 ++++++++++------- git-guts/Utils.fs | 66 ++++++++++++- git-guts/Verify.fs | 183 +++++++++++++++++++++++++++++++++++ git-guts/git-guts.fsproj | 1 + git-guts/tests/TestIndex.fs | 2 +- git-guts/tests/TestLogs.fs | 5 +- git-guts/tests/TestPacks.fs | 6 +- git-guts/tests/TestRefs.fs | 6 +- git-guts/tests/TestVerify.fs | 49 ++++++++++ git-guts/tests/Utils.fs | 53 ++-------- git-guts/tests/tests.fsproj | 1 + 14 files changed, 424 insertions(+), 95 deletions(-) create mode 100644 git-guts/Verify.fs create mode 100644 git-guts/tests/TestVerify.fs diff --git a/cli/Program.fs b/cli/Program.fs index 7d37516..052a16c 100644 --- a/cli/Program.fs +++ b/cli/Program.fs @@ -120,6 +120,30 @@ type DumpLogsCommand() = dumpLogs settings.RepoDir 0 +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +type VerifyObjectsSettings() = + inherit AppSettings() + [] + member val Progress = false with get, set + +type VerifyObjectsCommand() = + inherit Command() + override this.Execute( ctx, settings ) = + verifyObjects settings.RepoDir settings.Progress + 0 + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +type VerifyLogsSettings() = + inherit AppSettings() + +type VerifyLogsCommand() = + inherit Command() + override this.Execute( ctx, settings ) = + verifyLogs settings.RepoDir + 0 + // -------------------------------------------------------------------- [] @@ -129,7 +153,7 @@ let main argv = // colors (see ColorSystemDetector.Detect()), instead of checking if stdout is connected to a terminal, // so we do that here. if Console.IsOutputRedirected then - disableSpectreCapabilities + disableSpectreCapabilities () // parse the command-line arguments let app = CommandApp() @@ -153,5 +177,11 @@ let main argv = cfg.AddCommand( "dump-logs" ).WithDescription( "Dump logs." ) |> ignore + cfg.AddCommand( "verify-objects" ).WithDescription( + "Verify retrieval of objects from a git repo." + ) |> ignore + cfg.AddCommand( "verify-logs" ).WithDescription( + "Verify logs in a git repo." + ) |> ignore ) app.Run( argv ) diff --git a/git-guts/GitRepo.fs b/git-guts/GitRepo.fs index 22306c4..f28d1b5 100644 --- a/git-guts/GitRepo.fs +++ b/git-guts/GitRepo.fs @@ -2,6 +2,7 @@ namespace git_guts open System.IO open System.Text +open System.Text.RegularExpressions open System.Collections.Generic // -------------------------------------------------------------------- @@ -60,6 +61,29 @@ module GitRepo = failwith "Object not found." obj.Value.dumpObj() + let getObjNames repoDir = seq { + + // return all loose objects in the repo + let dname = Path.Join( repoDir, ".git/objects" ) + let regex = Regex( @"/([0-9a-f]{2})/([0-9a-f]{38}$)" ) + for fname in Directory.GetFiles( dname, "*", SearchOption.AllDirectories ) do + let fname2 = fname.Replace( Path.DirectorySeparatorChar, '/' ) + let matches = regex.Matches( fname2 ) + if matches.Count > 0 then + let groups = matches.[0].Groups + let objName = groups.[1].Value + groups.[2].Value + yield objName, fname2 + + // return all objects in each pack + for fname in findRepoPacks repoDir do + let mutable objs = [] // FUDGE! Can't yield from inside the onObject callback :-/ + _readPackIndexFile fname ( fun fanout -> () ) ( fun objNo nObjs objName crc offset -> + objs <- objs @ [ ( objName, fname ) ] + ) + yield! objs + + } + let dumpPackFile fname = // FUDGE! This is a wrapper function that passes in the recursively-called _findRepoObjRec function :-/ _dumpPackFile fname _findRepoObjRec diff --git a/git-guts/Logs.fs b/git-guts/Logs.fs index 19bd9f5..a2526bd 100644 --- a/git-guts/Logs.fs +++ b/git-guts/Logs.fs @@ -40,7 +40,7 @@ type LogEntry = [] module Logs = - let private _findLogFiles repoDir = seq { + let internal _findLogFiles repoDir = seq { // find log files in the specified repo let dname = Path.Join( repoDir, "/.git/logs" ) if Directory.Exists( dname ) then @@ -52,7 +52,7 @@ module Logs = yield ref, fname } - let private _readLogFile fname = seq { + let internal _readLogFile fname = seq { let regex = Regex( @"^([0-9a-f]{40}) ([0-9a-f]{40}) (.+?) \<(.+?)\> (\d+) ([+-]\d{4})(\s+[^:]+)?" ) for line in File.ReadLines( fname ) do let line2 = line.Trim() diff --git a/git-guts/PackIndex.fs b/git-guts/PackIndex.fs index 0094b8f..98d2e3e 100644 --- a/git-guts/PackIndex.fs +++ b/git-guts/PackIndex.fs @@ -99,10 +99,11 @@ module PackIndex = ( objName, crc, offset ) - let internal _dumpPackIndexFile fname = + let internal _readPackIndexFile fname onFanoutTable onObject = // initialize - use inp = new FileStream( fname, FileMode.Open, FileAccess.Read, FileShare.Read ) + let fname2 = changeExtn fname ".idx" + use inp = new FileStream( fname2, FileMode.Open, FileAccess.Read, FileShare.Read ) // read the header let version = _readPackIndexHeader inp @@ -110,44 +111,60 @@ module PackIndex = // read the fanout table let getFanoutVals = Seq.initInfinite (fun n -> readNboInt4 inp) let fanout = Seq.take 256 getFanoutVals |> Seq.toArray + onFanoutTable fanout let nObjs = fanout.[255] - // dump the fanout table header - AnsiConsole.MarkupLine( "{0}", makeHeader "FANOUT" "" ) - printfn "" - let fieldWidth = Math.Max( String.Format( "{0}", nObjs ).Length, 2 ) - let fmt = String.Format( "{{0,{0}}}", fieldWidth ) - printf " " - for col = 0 to 15 do - let iVal = String.Format( "{0:x2}", col ) - printf " %s" ( String.Format( fmt, iVal ) ) - printfn "" - printf " " - for col = 0 to 15 do - let ruler = String( '-', fieldWidth ) - printf " %s" ruler - printfn "" - - // dump the fanout table - for row = 0 to 15 do - printf "%02x:" (16 * row) - for col = 0 to 15 do - let fanoutVal = String.Format( fmt, fanout.[16*row+col] ) - printf " %s" fanoutVal - printfn "" - // dump the objects - let fieldWidth2 = String.Format( "{0}", nObjs ).Length - printfn "" - let hdr = sprintf "OBJECTS (%d)" nObjs - AnsiConsole.MarkupLine( "{0}", makeHeader hdr "" ) - printfn "" - let prefix = String( ' ', fieldWidth2 ) - printfn "%s name crc offset" prefix - printfn "%s ---------------------------------------- -------- --------" prefix - let fmt = sprintf "{0,%d}: {1} {2:x8} 0x{3:x}" fieldWidth2 for objNo = 0 to nObjs-1 do let objName, crc, offset = readPackIndexObject inp objNo nObjs + onObject objNo nObjs objName crc offset + + let internal _dumpPackIndexFile fname = + + _readPackIndexFile fname ( fun fanout -> + + // dump the fanout table header + AnsiConsole.MarkupLine( "{0}", makeHeader "FANOUT" "" ) + printfn "" + let nObjs = fanout.[255] + let fieldWidth = Math.Max( String.Format( "{0}", nObjs ).Length, 2 ) + let fmt = String.Format( "{{0,{0}}}", fieldWidth ) + printf " " + for col = 0 to 15 do + let iVal = String.Format( "{0:x2}", col ) + printf " %s" ( String.Format( fmt, iVal ) ) + printfn "" + printf " " + for col = 0 to 15 do + let ruler = String( '-', fieldWidth ) + printf " %s" ruler + printfn "" + + // dump the fanout table + for row = 0 to 15 do + printf "%02x:" (16 * row) + for col = 0 to 15 do + let fanoutVal = String.Format( fmt, fanout.[16*row+col] ) + printf " %s" fanoutVal + printfn "" + + ) ( fun objNo nObjs objName crc offset -> + + let fieldWidth = String.Format( "{0}", nObjs ).Length + + if objNo = 0 then + // output the header + printfn "" + let hdr = sprintf "OBJECTS (%d)" nObjs + AnsiConsole.MarkupLine( "{0}", makeHeader hdr "" ) + printfn "" + let prefix = String( ' ', fieldWidth ) + printfn "%s name crc offset" prefix + printfn "%s ---------------------------------------- -------- --------" prefix + + // dump the next object + let fmt = sprintf "{0,%d}: {1} {2:x8} 0x{3:x}" fieldWidth AnsiConsole.MarkupLine( fmt, objNo, objNameStr objName, crc, offset ) + ) diff --git a/git-guts/Utils.fs b/git-guts/Utils.fs index 860165b..e6ed83c 100644 --- a/git-guts/Utils.fs +++ b/git-guts/Utils.fs @@ -3,11 +3,36 @@ namespace git_guts open System open System.Text open System.IO +open System.Diagnostics open Spectre.Console // -------------------------------------------------------------------- +type CaptureStdout () = + // Temporarily capture output sent to stdout. + + // set up a buffer to capture stdout + let _writer = new StringWriter() + let _prevOut = System.Console.Out + let _prevAnsiConsole = AnsiConsole.Console.Profile.Out + do + System.Console.SetOut( _writer ) + AnsiConsole.Console.Profile.Out <- new AnsiConsoleOutput( _writer ) + + interface IDisposable with + member this.Dispose() = + // clean up + System.Console.SetOut( _prevOut ) + AnsiConsole.Console.Profile.Out <- _prevAnsiConsole + _writer.Dispose() + + member this.getOutput = + // return the captured output + _writer.ToString() + +// -------------------------------------------------------------------- + type StringBuilderBuf () = // Allow messages to be built up in a StringBuilder. @@ -31,7 +56,31 @@ type StringBuilderBuf () = [] module Utils = - let disableSpectreCapabilities = + let runGit repoDir cmd args = + // run git and capture the output + let gitPath = "git" // nb: we assume this is on the PATH + let gitDir = Path.Combine( repoDir, ".git" ) + let startInfo = ProcessStartInfo( FileName=gitPath, RedirectStandardOutput=true, UseShellExecute=false ) + let addArg arg = startInfo.ArgumentList.Add( arg ) + Seq.iter addArg [| "--git-dir"; gitDir; cmd |] + Seq.iter addArg args + let proc = Process.Start( startInfo ) + let getBytes = Seq.initInfinite ( fun _ -> proc.StandardOutput.BaseStream.ReadByte() ) + let output = getBytes |> Seq.takeWhile ( fun b -> b <> -1 ) |> Seq.map ( fun b -> byte(b) ) |> Seq.toArray + proc.WaitForExit() + if proc.ExitCode <> 0 then + failwithf "git failure: rc=%d" proc.ExitCode + output + + let runGitText repoDir cmd args = + // run git and capture the output as text + Encoding.UTF8.GetString( runGit repoDir cmd args ) + + let runGitGc repoDir = + // run git garbage collection + runGit repoDir "gc" [] |> ignore + + let disableSpectreCapabilities () = // disable colors (and other capabilities) in Spectre.Console AnsiConsole.Profile.Capabilities.ColorSystem <- ColorSystem.NoColors AnsiConsole.Profile.Capabilities.Ansi <- false @@ -163,3 +212,18 @@ module Utils = // parse a timestamp let epoch = DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ) epoch.AddSeconds( float( tstamp ) ) + + let plural n val1 val2 = + // return a pluralized string + sprintf "%d %s" n ( if n = 1 then val1 else val2 ) + + let friendlyByteCount nBytes = + // return a friendly byte-count string + if nBytes < 1024L then + plural (int nBytes) "byte" "bytes" + else if nBytes < 1024L * 1024L then + sprintf "%.1f KB" ( float( nBytes ) / 1024.0 ) + else if nBytes < 1024L * 1024L * 1024L then + sprintf "%.1f MB" ( float( nBytes ) / 1024.0 / 1024.0 ) + else + sprintf "%.1f GB" ( float( nBytes ) / 1024.0 / 1024.0 / 1024.0 ) diff --git a/git-guts/Verify.fs b/git-guts/Verify.fs new file mode 100644 index 0000000..c7e3b92 --- /dev/null +++ b/git-guts/Verify.fs @@ -0,0 +1,183 @@ +namespace git_guts + +open System +open System.IO +open System.Text +open System.Text.RegularExpressions + +// -------------------------------------------------------------------- + +[] +module VerifyObjects = + + let private _adjustGitTreeOutput (objDump: string) = + // adjust git's output for TREE objects so that it matches what we output + let regex = Regex( @"^(\d+) [a-z]+ ([0-9a-f]{40})\s+(.+)$" ) + let buf = new StringBuilder() + let reader = new StreamReader( new MemoryStream( Encoding.UTF8.GetBytes( objDump ) ) ) + while not reader.EndOfStream do + let line = reader.ReadLine() + let matches = regex.Matches( line ) + let groups = matches.[0].Groups + let perms, objName = groups.[1].Value, groups.[2].Value + let mutable path = groups.[3].Value + // FUDGE! git outputs UTF-8 bytes as octal-encoded strings, not the bytes themselves?! + if path.[0] = '"' && path.[path.Length-1] = '"' then + let rec updatePath (partialPath: string) = seq { + let pos = partialPath.IndexOf( '\\' ) + if pos < 0 then + yield! Encoding.UTF8.GetBytes( partialPath ) + else + yield! Encoding.UTF8.GetBytes( partialPath.Substring( 0, pos ) ) + let n = Convert.ToByte( partialPath.Substring( pos+1, 3 ), 8 ) + yield byte( n ) + yield! updatePath ( partialPath.Substring( pos+4 ) ) + } + path <- Encoding.UTF8.GetString( + updatePath ( path.Substring( 1, path.Length-2 ) ) |> Seq.toArray + ) + buf.AppendFormat( "{0} {1} {2}\n", perms, objName, path ) |> ignore + buf.ToString().TrimEnd() + + let verifyObjects repoDir progress = + + // NOTE: This will iterate over every object in a repo, and compare what we retrieve with what + // "git cat-file" returns. In particular, this will include *every* revision of *every* file, + // so for large repo's, it will take some time... + + // initialize + disableSpectreCapabilities () + let mutable currPackFname = "" + let mutable packObjCounts = Map[ ( "", 0 ) ] + + let onEndPackFile () = + let nObjs = packObjCounts.[ currPackFname ] + printfn "- Checked %s." ( plural nObjs "object" "objects" ) + + // check each object in the repo + printfn "Checking loose objects..." // nb: because getObjNames returns loose objects first + for objName, fname in getObjNames repoDir do + + // check if we have a loose object or an object in a pack + let packFname = + if Path.GetExtension( fname ) = ".pack" then Path.GetFileName( fname ) else "" + + // check if we've started a new pack + if packFname <> currPackFname then + // yup - log the end of the current one + onEndPackFile () + // prepare to start processing the new pack file + currPackFname <- packFname + packObjCounts <- packObjCounts.Add ( currPackFname, 0 ) + let fsize = FileInfo( fname ).Length + printfn "" + printfn "Checking pack file (%s): %s" (friendlyByteCount fsize) currPackFname + + // find the next object + if progress then + eprintfn "- Checking object: %s" objName + let objRec = _findRepoObjRec repoDir objName + if objRec.IsNone then + failwithf "Can't find object: %s" objName + let mutable objData = objRec.Value.objData + let obj = makeGitObject objRec.Value + packObjCounts <- packObjCounts.Add ( currPackFname, packObjCounts.[currPackFname]+1 ) + if progress then + eprintfn " - Got %s: #bytes=%d" obj.objType objData.Length + + // check the object type + let expectedObjType = ( runGitText repoDir "cat-file" [ "-t"; objName ] ).TrimEnd() + if obj.objType <> expectedObjType then + failwithf "Object type mismatch for %s: got \"%s\", expected \"%s\"." objName obj.objType expectedObjType + + // check the object data + let mutable expectedObjData = ( runGit repoDir "cat-file" [ "-p"; objName ] ) + if obj.objType = "tree" then + objData <- Encoding.UTF8.GetBytes( + using ( new CaptureStdout() ) ( fun cap -> + obj.dumpObj() + cap.getOutput.TrimEnd() + ) + ) + expectedObjData <- Encoding.UTF8.GetBytes( + _adjustGitTreeOutput ( Encoding.UTF8.GetString expectedObjData ) + ) + if objData <> expectedObjData then + let dname = Path.GetTempPath() + File.WriteAllBytes( Path.Join( dname, "git-content.expected" ), expectedObjData ) + File.WriteAllBytes( Path.Join( dname, "git-content.actual" ), objData ) + failwithf "Object data mismatch for %s." objName + + onEndPackFile () + + // NOTE: These functions generate object names that are invalid (they contain a non-hex character), + // but they will always compare greater/less than a valid name, based on the first byte, which will + // help test how we use the fanout table, and the binary search through the table of object names. + let makeObjName1 byte0 = + sprintf "%02x%s!" byte0 (String('0',37)) + let makeObjName2 byte0 = + sprintf "%02x%sz" byte0 (String('f',37)) + // NOTE: Also test with an object name that appears in the middle of the range, for a given first byte. + let makeObjName3 byte0 = + sprintf "%02x%s" byte0 "80808080808080808080808080808080808080" + + // verify looking up unknown objects + printfn "" + printfn "Checking unknown objects..." + let mutable nObjs = 0 + [| makeObjName1; makeObjName2; makeObjName3 |] |> Seq.iter ( fun makeObjName -> + for byte0 = 0 to 255 do + let objName = makeObjName byte0 + let obj = findRepoObject repoDir objName + if obj.IsSome then + failwithf "Unexpectedly found object: %s" objName + nObjs <- nObjs + 1 + ) + printf "- Checked %s." ( plural nObjs "unknown object" "unknown objects." ) + +// -------------------------------------------------------------------- + +[] +module VerifyLogs = + + let verifyLogs repoDir = + + // verify reading each log file + for ref, fname in _findLogFiles repoDir do + printfn "Processing log file: %s" fname + + // run git to get the log entries for the current ref + let ref2 = + let ref2 = ref.Replace( Path.DirectorySeparatorChar, '/' ) + if ref2.Length >= 12 && ref2.Substring( 0, 11 ) = "refs/heads/" then + ref2.Substring( 11 ) + else + ref2 + let expected = ( runGitText repoDir "reflog" [| "show"; ref2 |] ).TrimEnd() + + // NOTE: git shows just enough of the object names for them to be unique, so we need + // to figure out how much that is, so that we can generate the same output :-/ + // We assume the first line is a log entry, that starts with an abbreviated object name. + let objNamePrefixLen = expected.IndexOf( ' ' ) + + // extract the log entries for the current ref + let buf = new StringBuilder() + let mutable nLogEntries = 0 + _readLogFile fname |> Seq.rev |> Seq.iteri ( fun logEntryNo logEntry -> + if logEntry.nextRef.IsSome then + let objNamePrefix = logEntry.nextRef.Value.Substring( 0, objNamePrefixLen ) + buf.AppendFormat( "{0} {1}@{{{2}}}: {3}", objNamePrefix, ref2, logEntryNo, logEntry.entryType.Value ) |> ignore + if logEntry.msg.IsSome then + buf.AppendFormat( ": {0}", logEntry.msg.Value ) |> ignore + buf.AppendLine( "" ) |> ignore + nLogEntries <- nLogEntries + 1 + ) + let output = buf.ToString().TrimEnd() + + // compare what we extracted with the git output + if output <> expected then + let dname = Path.GetTempPath() + File.WriteAllText( Path.Join( dname, "git-log.expected" ), expected ) + File.WriteAllText( Path.Join( dname, "git-log.actual" ), output ) + failwithf "Mismatched output for ref: %s" ref2 + printfn "- Checked %s." ( plural nLogEntries "log entry" "log entries" ) diff --git a/git-guts/git-guts.fsproj b/git-guts/git-guts.fsproj index f1dd695..e126dec 100644 --- a/git-guts/git-guts.fsproj +++ b/git-guts/git-guts.fsproj @@ -17,6 +17,7 @@ + diff --git a/git-guts/tests/TestIndex.fs b/git-guts/tests/TestIndex.fs index 5743484..4092312 100644 --- a/git-guts/tests/TestIndex.fs +++ b/git-guts/tests/TestIndex.fs @@ -14,7 +14,7 @@ type TestStagingIndex () = [] member this.init () = // prepare to run a test - disableSpectreCapabilities + disableSpectreCapabilities () [] member this.TestDumpStagingIndex () = diff --git a/git-guts/tests/TestLogs.fs b/git-guts/tests/TestLogs.fs index ac09cb1..02466f9 100644 --- a/git-guts/tests/TestLogs.fs +++ b/git-guts/tests/TestLogs.fs @@ -14,7 +14,7 @@ type TestLogs () = [] member this.init () = // prepare to run a test - disableSpectreCapabilities + disableSpectreCapabilities () [] member this.TestDumpLogs () = @@ -30,8 +30,9 @@ type TestLogs () = let expectedFname = let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".logs.txt" Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) - cap.checkOutput expectedFname + checkCapturedOutput cap expectedFname ) + // run the tests doTest "empty.zip" doTest "simple.zip" diff --git a/git-guts/tests/TestPacks.fs b/git-guts/tests/TestPacks.fs index 066bcaa..a038d6d 100644 --- a/git-guts/tests/TestPacks.fs +++ b/git-guts/tests/TestPacks.fs @@ -13,7 +13,7 @@ type TestPacks () = [] member this.init () = // prepare to run a test - disableSpectreCapabilities + disableSpectreCapabilities () [] member this.TestDumpPack () = @@ -35,7 +35,7 @@ type TestPacks () = let expectedFname = let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".pack-data.txt" Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) - cap.checkOutput expectedFname + checkCapturedOutput cap expectedFname ) // dump the pack index file @@ -45,7 +45,7 @@ type TestPacks () = let expectedFname = let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".pack-index.txt" Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) - cap.checkOutput expectedFname + checkCapturedOutput cap expectedFname ) // check that we can find each object name correctly diff --git a/git-guts/tests/TestRefs.fs b/git-guts/tests/TestRefs.fs index 041911e..ff3b532 100644 --- a/git-guts/tests/TestRefs.fs +++ b/git-guts/tests/TestRefs.fs @@ -14,7 +14,7 @@ type TestRefs () = [] member this.init () = // prepare to run a test - disableSpectreCapabilities + disableSpectreCapabilities () [] member this.TestDumpRefs () = @@ -30,7 +30,7 @@ type TestRefs () = let expectedFname = let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".refs.txt" Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) - cap.checkOutput expectedFname + checkCapturedOutput cap expectedFname ) // move the loose objects to a pack, and check again @@ -40,7 +40,7 @@ type TestRefs () = let expectedFname = let fname = Path.GetFileNameWithoutExtension( zipFname ) + ".refs-packed.txt" Path.Combine( __SOURCE_DIRECTORY__, "fixtures", fname ) - cap.checkOutput expectedFname + checkCapturedOutput cap expectedFname ) // run the tests diff --git a/git-guts/tests/TestVerify.fs b/git-guts/tests/TestVerify.fs new file mode 100644 index 0000000..4b5daf3 --- /dev/null +++ b/git-guts/tests/TestVerify.fs @@ -0,0 +1,49 @@ +namespace tests + +open System +open System.IO +open Microsoft.VisualStudio.TestTools.UnitTesting + +open git_guts + +// -------------------------------------------------------------------- + +[] +type TestVerify () = + + [] + member this.init () = + // prepare to run a test + disableSpectreCapabilities () + + [] + member this.TestVerify () = + + let doChecks repoDir = + + // verify retrieving objects from the repo + using ( new CaptureStdout() ) ( fun cap -> + verifyObjects repoDir false + ) + + // verify retrieving logs from the repo + using ( new CaptureStdout() ) ( fun cap -> + verifyLogs repoDir + ) + + let doTest zipFname = + + // set up the test repo + use gitTestRepo = new GitTestRepo( zipFname ) + + // do the checks + doChecks gitTestRepo.repoDir + + // run garbage collection, and verify again + runGitGc gitTestRepo.repoDir + doChecks gitTestRepo.repoDir + + // run the tests + let dname = Path.Combine( __SOURCE_DIRECTORY__, "fixtures" ) + for fname in Directory.GetFiles( dname, "*.zip", SearchOption.AllDirectories ) do + doTest ( Path.GetFileName fname ) diff --git a/git-guts/tests/Utils.fs b/git-guts/tests/Utils.fs index b6e9b36..d0f66d6 100644 --- a/git-guts/tests/Utils.fs +++ b/git-guts/tests/Utils.fs @@ -4,11 +4,12 @@ open System open System.Text open System.IO open System.IO.Compression -open System.Diagnostics open Microsoft.VisualStudio.TestTools.UnitTesting open Spectre.Console +open git_guts + // -------------------------------------------------------------------- type TempDir () = @@ -49,58 +50,16 @@ type GitTestRepo ( zipFname ) = // -------------------------------------------------------------------- -type CaptureStdout () = - // Temporarily capture output sent to stdout. - - // set up a buffer to capture stdout - let _writer = new StringWriter() - let _prevOut = System.Console.Out - let _prevAnsiConsole = AnsiConsole.Console.Profile.Out - do - System.Console.SetOut( _writer ) - AnsiConsole.Console.Profile.Out <- new AnsiConsoleOutput( _writer ) - - interface IDisposable with - member this.Dispose() = - // clean up - System.Console.SetOut( _prevOut ) - AnsiConsole.Console.Profile.Out <- _prevAnsiConsole - _writer.Dispose() +[] +module Utils = - member this.checkOutput fname = + let checkCapturedOutput (cap: CaptureStdout) fname = // compare the captured output with what's expected let expected = File.ReadAllText( fname, Encoding.UTF8 ) - let output = this.getOutput + let output = cap.getOutput if output <> expected then let fname2 = Path.Combine( Path.GetTempPath(), "captured-output.txt" ) File.WriteAllText( fname2, output, Encoding.UTF8 ) Assert.Fail( sprintf "Captured output`mismatch: %s" ( Path.GetFileName( fname ) ) ) - - member this.getOutput = - // return the captured output - _writer.ToString() - -// -------------------------------------------------------------------- - -[] -module Utils = - - let runGit repoDir cmd args = - // run git and capture the output - let gitPath = "git" // nb: we assume this is on the PATH - let gitDir = Path.Combine( repoDir, ".git" ) - let startInfo = ProcessStartInfo( FileName=gitPath, RedirectStandardOutput=true, UseShellExecute=false ) - let addArg arg = startInfo.ArgumentList.Add( arg ) - Seq.iter addArg [| "--git-dir"; gitDir; cmd |] - Seq.iter addArg args - let proc = Process.Start( startInfo ) - let output = proc.StandardOutput.ReadToEnd() - proc.WaitForExit() - Assert.AreEqual( 0, proc.ExitCode ) - output - - let runGitGc repoDir = - // run git garbage collection - runGit repoDir "gc" [] |> ignore diff --git a/git-guts/tests/tests.fsproj b/git-guts/tests/tests.fsproj index d90da79..f9e6e82 100644 --- a/git-guts/tests/tests.fsproj +++ b/git-guts/tests/tests.fsproj @@ -12,6 +12,7 @@ +