Skip to content

Commit 667a421

Browse files
authored
Merge pull request #1 from Michal-MK/feat/add-binary-comparison
Feat/add binary comparison
2 parents 3435d2e + 3b3754e commit 667a421

9 files changed

Lines changed: 211 additions & 19 deletions

exampleTest/BinaryTest.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Text;
5+
using Xunit;
6+
7+
namespace CheckTestOutput.Example
8+
{
9+
public class BinaryTest
10+
{
11+
private const string ImagePath = "Image.png";
12+
13+
OutputChecker check = new OutputChecker("testoutputs");
14+
15+
[Fact]
16+
public void CheckStagedBinaryData()
17+
{
18+
check.CheckBinary(File.ReadAllBytes(Path.Combine(check.CheckDirectory, ImagePath)));
19+
Assert.True(true);
20+
}
21+
22+
[Fact]
23+
public void CheckModifiedBinaryData()
24+
{
25+
var imgPath = Path.Combine(check.CheckDirectory, ImagePath);
26+
byte[] previousData = File.ReadAllBytes(imgPath);
27+
byte[] modifiedData = previousData.Append((byte)64).ToArray();
28+
29+
// File contents do not match
30+
Assert.Throws<Exception>(() => check.CheckBinary(modifiedData));
31+
32+
// Return back the original content
33+
File.WriteAllBytes(Path.Combine(check.CheckDirectory, $"{nameof(BinaryTest)}.{nameof(CheckModifiedBinaryData)}.bin"), previousData);
34+
}
35+
36+
[Fact]
37+
public void CheckNonexistentBinaryData()
38+
{
39+
var imageBytes = File.ReadAllBytes(Path.Combine(check.CheckDirectory, ImagePath));
40+
// File does not exist
41+
Assert.Throws<ArgumentNullException>(() => check.CheckBinary(imageBytes));
42+
}
43+
44+
[Fact]
45+
public void CheckUTF8TextConvertedToBinaryData()
46+
{
47+
const string TEXT = "( ͡° ͜ʖ ͡°)";
48+
49+
check.CheckBinary(Encoding.UTF8.GetBytes(TEXT));
50+
Assert.True(true);
51+
}
52+
53+
[Fact]
54+
public void CheckUTF8TextConvertedWithASCIIBinaryDataThrows()
55+
{
56+
const string TEXT = "( ͡° ͜ʖ ͡°)";
57+
58+
Assert.Throws<Exception>(() => check.CheckBinary(Encoding.ASCII.GetBytes(TEXT)));
59+
60+
// Return back the original content
61+
File.WriteAllBytes(Path.Combine(check.CheckDirectory, $"{nameof(BinaryTest)}.{nameof(CheckUTF8TextConvertedWithASCIIBinaryDataThrows)}.bin"), Encoding.UTF8.GetBytes(TEXT));
62+
}
63+
}
64+
}
130 Bytes
Binary file not shown.
130 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
( ͡° ͜ʖ ͡°)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
( ͡° ͜ʖ ͡°)

exampleTest/testoutputs/Image.png

130 Bytes
Loading

src/BasicChecks.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.IO;
4-
using System.Linq;
53

64
namespace CheckTestOutput
75
{
@@ -25,6 +23,24 @@ public static void CheckString(
2523
);
2624
}
2725

26+
/// <summary> Verifies that the provided <paramref name="output" /> equals to the `outputDirectory/TestClass.TestMethod.bin` file. </summary>
27+
/// <param name="checkName"> If not null, checkName will be appended to the calling <paramref name="memberName" />. Intended to be used when having multiple checks in one method. </param>
28+
public static void CheckBinary(
29+
this OutputChecker t,
30+
byte[] output,
31+
string checkName = null,
32+
string fileExtension = "bin",
33+
[System.Runtime.CompilerServices.CallerMemberName] string memberName = null,
34+
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = null)
35+
{
36+
t.CheckOutputBinaryCore(
37+
output,
38+
checkName,
39+
$"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}",
40+
fileExtension
41+
);
42+
}
43+
2844
/// <summary> Verifies that the provided <paramref name="output" /> equals to the `outputDirectory/TestClass.TestMethod.txt` file. File is compared line-by-line. </summary>
2945
/// <param name="checkName"> If not null, checkName will be appended to the calling <paramref name="memberName" />. Intended to be used when having multiple checks in one method. </param>
3046
public static void CheckLines(

src/CheckTestOutput.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.1;net6.0</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.1;net6.0;net7.0</TargetFrameworks>
55
<LangVersion>9</LangVersion>
66

77
<Description>Simple helper which checks that output of a test matches a file. If not matching, just git staging the new file will accept the new version.</Description>

src/OutputChecker.cs

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.ComponentModel;
@@ -83,7 +83,7 @@ public OutputChecker(
8383

8484
private readonly Lazy<bool> DoesGitWork;
8585

86-
private string[] RunGitCommand(params string[] args)
86+
private Process StartGitProcess(params string[] args)
8787
{
8888
#if DEBUG
8989
Console.WriteLine("Running git command: " + string.Join(" ", args));
@@ -104,18 +104,11 @@ private string[] RunGitCommand(params string[] args)
104104
procInfo.ArgumentList.Add(a);
105105

106106

107-
var proc = Process.Start(procInfo);
108-
109-
var outputLines = new List<string>();
110-
var outputReaderTask = Task.Run(() => {
111-
string line = null;
112-
while ((line = proc.StandardOutput.ReadLine()) != null)
113-
{
114-
if (line.Length > 0)
115-
outputLines.Add(line);
116-
}
117-
});
107+
return Process.Start(procInfo);
108+
}
118109

110+
private void HandleProcessExit(Process proc, Task outputReaderTask, params string[] args)
111+
{
119112
// Literally, a Raspberry PI with a shitty SD card has faster IO than Azure Windows VM
120113
var timeout = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 15_000 : 3_000;
121114
if (!proc.WaitForExit(timeout))
@@ -128,10 +121,52 @@ private string[] RunGitCommand(params string[] args)
128121
throw new Exception($"`git {string.Join(" ", args)}` command failed: " + proc.StandardError.ReadToEnd());
129122

130123
outputReaderTask.Wait();
124+
}
125+
126+
private string[] RunGitCommand(params string[] args)
127+
{
128+
var proc = StartGitProcess(args);
129+
130+
var outputLines = new List<string>();
131+
var outputReaderTask = Task.Run(() =>
132+
{
133+
string line;
134+
while ((line = proc.StandardOutput.ReadLine()) != null)
135+
{
136+
if (line.Length > 0)
137+
outputLines.Add(line);
138+
}
139+
});
140+
141+
HandleProcessExit(proc, outputReaderTask, args);
131142

132143
return outputLines.ToArray();
133144
}
134145

146+
private byte[] RunGitBinaryCommand(params string[] args)
147+
{
148+
const int BUFFER_SIZE = 1024;
149+
150+
var proc = StartGitProcess(args);
151+
152+
List<byte> ret = new();
153+
154+
var outputReaderTask = Task.Run(() =>
155+
{
156+
byte[] buffer = new byte[BUFFER_SIZE];
157+
158+
int charsRead = 0;
159+
while ((charsRead = proc.StandardOutput.BaseStream.Read(buffer, 0, BUFFER_SIZE)) != 0)
160+
{
161+
ret.AddRange(buffer.AsSpan(0, charsRead).ToArray());
162+
}
163+
});
164+
165+
HandleProcessExit(proc, outputReaderTask, args);
166+
167+
return ret.ToArray();
168+
}
169+
135170
static string[] ReadAllLines(StreamReader reader)
136171
{
137172
var lines = new List<string>();
@@ -160,6 +195,28 @@ private string GetOldContent(string file)
160195
}
161196
}
162197

198+
private byte[] GetOldBinaryContent(string file)
199+
{
200+
if (DoesGitWork.Value)
201+
{
202+
var lsFiles = RunGitCommand("ls-files", "-s", file);
203+
if (lsFiles.Length == 0)
204+
return null;
205+
206+
var hash = lsFiles[0].Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries).ElementAtOrDefault(1);
207+
if (String.IsNullOrEmpty(hash))
208+
return null;
209+
210+
var data = RunGitBinaryCommand("cat-file", "blob", hash);
211+
212+
return data;
213+
}
214+
else
215+
{
216+
return File.ReadAllBytes(file);
217+
}
218+
}
219+
163220
private bool IsModified(string file)
164221
{
165222
// command `git ls-files --other --modified $file` returns the file name back iff it is modified or other (untracked)
@@ -242,14 +299,67 @@ internal void CheckOutputCore(string outputString, string checkName, string meth
242299
throw new Exception(
243300
$"{Path.GetFileName(filename)} has changed, the actual output differs from the previous accepted output:\n\n" +
244301
string.Join("\n", diff) + "\n\n" +
245-
"If this change OK? To let the test pass, stage the file in git. Confused? See https://github.com/exyi/CheckTestOutput/blob/master/trouble.md#changed-file\n"
302+
"Is this change OK? To let the test pass, stage the file in git. Confused? See https://github.com/exyi/CheckTestOutput/blob/master/trouble.md#changed-file\n"
303+
304+
);
305+
}
306+
}
307+
else
308+
{
309+
throw new Exception($"{Path.GetFileName(filename)} has changed, the previous accepted output differs from the actual output:\n\n{outputString}\n\nNote that CheckTestOutput could not use git on your system, so the \"UX\" is limited.");
310+
}
311+
}
312+
313+
internal void CheckOutputBinaryCore(byte[] outputBytes, string checkName, string method, string fileExtension = "bin")
314+
{
315+
Directory.CreateDirectory(CheckDirectory);
316+
317+
var filename = Path.Combine(CheckDirectory, (checkName == null ? method : $"{method}-{checkName}") + "." + fileExtension);
246318

319+
if (GetOldBinaryContent(filename).SequenceEqual(outputBytes))
320+
{
321+
// fine! Just check that the file is not changed - if it is changed or deleted, we rewrite
322+
if (IsModified(filename))
323+
{
324+
using (var t = File.Create(filename))
325+
{
326+
t.Write(outputBytes);
327+
}
328+
}
329+
return;
330+
}
331+
332+
if (DoesGitWork.Value)
333+
{
334+
using (var t = File.Create(filename))
335+
{
336+
t.Write(outputBytes);
337+
}
338+
339+
if (IsModified(filename))
340+
{
341+
if (IsNewFile(filename))
342+
{
343+
throw new Exception($"{Path.GetFileName(filename)} is not explicitly accepted - the file is untracked in git. To let this test pass, view the file and stage it. Confused? See https://github.com/exyi/CheckTestOutput/blob/master/trouble.md#untracked-file\n");
344+
}
345+
346+
347+
var diff = RunGitCommand("diff", filename);
348+
if (diff.All(string.IsNullOrEmpty))
349+
{
350+
// I guess fine from our perspective, but it's weird...
351+
Console.WriteLine($"CheckTestOutput warning: {Path.GetFileName(filename)} is modified, but the diff is empty.");
352+
return;
353+
}
354+
throw new Exception(
355+
$"{Path.GetFileName(filename)} has changed, the actual output differs from the previous accepted output!"
356+
+ "Is the change OK? To let the test pass, stage the file in git. Confused? See https://github.com/exyi/CheckTestOutput/blob/master/trouble.md#changed-file\n"
247357
);
248358
}
249359
}
250360
else
251361
{
252-
throw new Exception($"{Path.GetFileName(filename)}has changed, the previous accepted output differs from the actual output:\n\n{outputString}\n\nNote that CheckTestOutput could not use git on your system, so the \"UX\" is limited.");
362+
throw new Exception($"{Path.GetFileName(filename)} has changed, the previous accepted output differs from the actual output.");
253363
}
254364
}
255365
}

0 commit comments

Comments
 (0)