[XHarness] Add tests for all the logging classes. (#8032)

Move to use interfaces, that will let us later add tests that will
verify that all the correct logging is performed. As an example, added a
test for XmlResultParser that ensures that the failures are correctly
generated. The test uses Moq to pass the different paths to be used and
later be able to verify the wirtten xml.


Co-authored-by: Přemek Vysoký <premek.vysoky@microsoft.com>
This commit is contained in:
Manuel de la Pena 2020-03-04 15:27:52 -05:00 коммит произвёл GitHub
Родитель 004b50f590
Коммит 0c085cd719
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 739 добавлений и 10 удалений

Просмотреть файл

@ -648,9 +648,8 @@ namespace Xharness
main_log.WriteLine ("System log for the '{1}' simulator is: {0}", sim.SystemLog, sim.Name);
bool isCompanion = sim != simulator;
var log = new CaptureLog (Logs, sim.SystemLog, entire_file: Harness.Action != HarnessAction.Jenkins)
var log = new CaptureLog (Logs, Path.Combine (LogDirectory, sim.Name + ".log"), sim.SystemLog, entire_file: Harness.Action != HarnessAction.Jenkins)
{
Path = Path.Combine (LogDirectory, sim.Name + ".log"),
Description = isCompanion ? LogType.CompanionSystemLog.ToString () : LogType.SystemLog.ToString (),
};
log.StartCapture ();

Просмотреть файл

@ -0,0 +1,20 @@
using System;
namespace Xharness.Logging {
// A log that forwards all written data to a callback
public class CallbackLog : Log {
readonly Action<string> OnWrite;
public CallbackLog (Action<string> onWrite)
: base (null)
{
OnWrite = onWrite;
}
public override string FullPath => throw new NotSupportedException ();
public override void WriteImpl (string value)
{
OnWrite (value);
}
}
}

Просмотреть файл

@ -0,0 +1,139 @@
using System;
using System.IO;
namespace Xharness.Logging {
// A log that captures data written to a separate file between two moments in time
// (between StartCapture and StopCapture).
public class CaptureLog : Log
{
public readonly string CapturePath;
public readonly string Path;
long startPosition;
long endPosition;
bool entireFile;
bool started;
public CaptureLog (ILogs logs, string path, string capture_path, bool entire_file = false)
: base (logs)
{
Path = path ?? throw new ArgumentNullException (nameof (path));
CapturePath = capture_path ?? throw new ArgumentNullException (nameof (path));
this.entireFile = entire_file;
}
public void StartCapture ()
{
if (entireFile)
return;
if (File.Exists (CapturePath))
startPosition = new FileInfo (CapturePath).Length;
started = true;
}
public void StopCapture ()
{
if (!started && !entireFile)
throw new InvalidOperationException ("StartCapture most be called before StopCature on when the entire file will be captured.");
if (!File.Exists (CapturePath)) {
File.WriteAllText (Path, $"Could not capture the file '{CapturePath}' because it doesn't exist.");
return;
}
if (entireFile) {
File.Copy (CapturePath, Path, true);
return;
}
endPosition = new FileInfo (CapturePath).Length;
Capture ();
}
void Capture ()
{
if (startPosition == 0 || entireFile)
return;
if (!File.Exists (CapturePath)) {
File.WriteAllText (Path, $"Could not capture the file '{CapturePath}' because it does not exist.");
return;
}
var currentEndPosition = endPosition;
if (currentEndPosition == 0)
currentEndPosition = new FileInfo (CapturePath).Length;
var length = (int) (currentEndPosition - startPosition);
var currentLength = new FileInfo (CapturePath).Length;
var capturedLength = 0L;
if (length < 0) {
// The file shrank? lets copy the entire file in this case, which is better than nothing
File.Copy (CapturePath, Path, true);
return;
}
if (File.Exists (Path))
capturedLength = new FileInfo (Path).Length;
// capture 1k more data than when we stopped, since the system log
// is cached in memory and flushed once in a while (so when the app
// requests the system log to be captured, it's usually not complete).
var availableLength = currentLength - startPosition;
if (availableLength <= capturedLength)
return; // We've captured before, and nothing new as added since last time.
// Capture at most 1k more
availableLength = Math.Min (availableLength, length + 1024);
using (var reader = new FileStream (CapturePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
using (var writer = new FileStream (Path, FileMode.Create, FileAccess.Write, FileShare.Read)) {
var buffer = new byte [4096];
reader.Position = startPosition;
while (availableLength > 0) {
int read = reader.Read (buffer, 0, Math.Min (buffer.Length, length));
if (read > 0) {
writer.Write (buffer, 0, read);
availableLength -= read;
} else {
// There's nothing more to read.
// I can't see how we get here, since we calculate the amount to read based on what's available, but it does happen randomly.
break;
}
}
}
}
}
public override StreamReader GetReader ()
{
if (File.Exists (CapturePath)) {
return new StreamReader (new FileStream (CapturePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
} else {
return new StreamReader (new MemoryStream ());
}
}
public override void Flush ()
{
base.Flush ();
Capture ();
}
public override void WriteImpl (string value)
{
throw new InvalidOperationException ();
}
public override string FullPath {
get {
return Path;
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
using System;
using System.IO;
using System.Text;
namespace Xharness.Logging {
// A log that writes to standard output
public class ConsoleLog : Log {
StringBuilder captured = new StringBuilder ();
public ConsoleLog ()
: base (null)
{
}
public override void WriteImpl (string value)
{
captured.Append (value);
Console.Write (value);
}
public override string FullPath {
get {
throw new NotSupportedException ();
}
}
public override StreamReader GetReader ()
{
var str = new MemoryStream (Encoding.GetBytes (captured.ToString ()));
return new StreamReader (str, Encoding, false);
}
}
}

Просмотреть файл

@ -79,7 +79,7 @@ namespace Xharness.Logging {
public override Encoding Encoding {
get {
return System.Text.Encoding.UTF8;
return Encoding.UTF8;
}
}

Просмотреть файл

@ -0,0 +1,71 @@
using System;
using System.IO;
namespace Xharness.Logging {
public class LogFile : Log, ILogFile {
object lock_obj = new object ();
public string Path { get; private set; }
FileStream writer;
bool disposed;
public LogFile (ILogs logs, string description, string path, bool append = true)
: base (logs, description)
{
Path = path ?? throw new ArgumentNullException (nameof (path));
if (!append)
File.WriteAllText (path, string.Empty);
}
public override void Write (byte [] buffer, int offset, int count)
{
try {
// We don't want to open the file every time someone writes to the log, so we keep it as an instance
// variable until we're disposed. Due to the async nature of how we run tests, writes may still
// happen after we're disposed, in which case we create a temporary stream we close after writing
lock (lock_obj) {
var fs = writer;
if (fs == null) {
fs = new FileStream (Path, FileMode.Append, FileAccess.Write, FileShare.Read);
}
fs.Write (buffer, offset, count);
if (disposed) {
fs.Dispose ();
} else {
writer = fs;
}
}
} catch (Exception e) {
Console.Error.WriteLine ($"Failed to write to the file {Path}: {e}.");
return;
}
}
public override void Flush ()
{
base.Flush ();
if (writer != null && !disposed)
writer.Flush ();
}
public override string FullPath => Path;
public override StreamReader GetReader ()
{
return new StreamReader (new FileStream (Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
}
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
lock (lock_obj) {
writer?.Dispose ();
writer = null;
}
disposed = true;
}
}
}

Просмотреть файл

@ -8,7 +8,7 @@ namespace Xharness.Logging {
public Logs (string directory)
{
Directory = directory;
Directory = directory ?? throw new ArgumentNullException (nameof (directory));
}
// Create a new log backed with a file
@ -40,6 +40,9 @@ namespace Xharness.Logging {
// 'path' must be a full path to the file.
public ILogFile AddFile (string path, string name)
{
if (path == null)
throw new ArgumentNullException (nameof (path));
if (!path.StartsWith (Directory, StringComparison.Ordinal)) {
var newPath = Path.Combine (Directory, Path.GetFileNameWithoutExtension (path) + "-" + Harness.Timestamp + Path.GetExtension (path));
File.Copy (path, newPath, true);
@ -54,6 +57,8 @@ namespace Xharness.Logging {
// Create an empty file in the log directory and return the full path to the file
public string CreateFile (string path, string description)
{
if (path == null)
throw new ArgumentNullException (nameof (path));
using (var rv = new LogFile (this, description, Path.Combine (Directory, path), false)) {
Add (rv);
return rv.FullPath;

Просмотреть файл

@ -0,0 +1,29 @@
using System;
using NUnit.Framework;
using Xharness.Logging;
namespace Xharness.Tests.Logging.Tests {
[TestFixture]
public class CallbackLogTest {
[Test]
public void OnWriteTest ()
{
var message = "This is a log message";
bool called = false;
string data = null;
Action<string> cb = (d) => {
called = true;
data = d;
};
var log = new CallbackLog (cb);
log.Write (message);
Assert.IsTrue (called, "Callback was not called");
Assert.IsNotNull (data, "data");
StringAssert.EndsWith (message, data, "message"); // TODO: take time stamp into account
}
}
}

Просмотреть файл

@ -0,0 +1,105 @@
using System;
using System.IO;
using Moq;
using NUnit.Framework;
using Xharness.Logging;
namespace Xharness.Tests.Logging.Tests {
[TestFixture]
public class CaptureLogTest {
string filePath;
string capturePath;
Mock<ILogs> logs;
[SetUp]
public void SetUp ()
{
filePath = Path.GetTempFileName ();
capturePath = Path.GetTempFileName ();
logs = new Mock<ILogs> ();
File.Delete (filePath);
File.Delete (capturePath);
}
[TearDown]
public void TearDown ()
{
if (File.Exists (filePath))
File.Delete (filePath);
}
[Test]
public void ConstructorNullFilePath ()
{
Assert.Throws<ArgumentNullException> (() => {
var captureLog = new CaptureLog (logs.Object, null, filePath, false);
});
}
[Test]
public void CaptureRightOrder ()
{
var ignoredLine = "This lines should not be captured";
var logLines = new [] { "first line", "second line", "thrid line" };
using (var stream = File.Create (filePath))
using (var writer = new StreamWriter (stream)) {
writer.WriteLine (ignoredLine);
}
using (var captureLog = new CaptureLog (logs.Object, capturePath, filePath, false)) {
captureLog.StartCapture ();
using (var writer = new StreamWriter (filePath)) {
foreach (var line in logLines)
writer.WriteLine (line);
}
captureLog.StopCapture ();
// get the stream and assert we do have the correct lines
using (var captureStream = captureLog.GetReader ()) {
string line;
while ((line = captureStream.ReadLine ()) != null) {
Assert.Contains (line, logLines, "Lines not captured");
}
}
}
}
[Test]
public void CaptureMissingFileTest ()
{
using (var captureLog = new CaptureLog (logs.Object, capturePath, filePath, false)) {
Assert.AreEqual (capturePath, captureLog.Path, "capture path");
captureLog.StartCapture ();
captureLog.StopCapture ();
}
// read the data that was added to the capture path and ensure that we do have the name of the missing file
using (var reader = new StreamReader (capturePath)) {
var line = reader.ReadLine ();
StringAssert.Contains (filePath, line, "file path missing");
}
}
[Test]
public void CaptureWrongOrder ()
{
Assert.Throws<InvalidOperationException> (() => {
using (var captureLog = new CaptureLog (logs.Object, capturePath, filePath, false)) {
captureLog.StopCapture ();
}
});
}
[Test]
public void CaptureWrongOrderEntirePath ()
{
Assert.DoesNotThrow (() => {
using (var captureLog = new CaptureLog (logs.Object, capturePath, filePath, true)) {
captureLog.StopCapture ();
}
});
}
}
}

Просмотреть файл

@ -0,0 +1,51 @@
using System;
using System.IO;
using NUnit.Framework;
using Xharness.Logging;
namespace Xharness.Tests.Logging.Tests {
[TestFixture]
public class ConsoleLogTest {
string testFile;
TextWriter sdoutWriter;
ConsoleLog log;
[SetUp]
public void SetUp ()
{
testFile = Path.GetTempFileName ();
log = new ConsoleLog ();
sdoutWriter = Console.Out;
}
[Test]
public void FullPathTest () => Assert.Throws<NotSupportedException> (() => { string path = log.FullPath; });
[Test]
public void TestWrite ()
{
var message = "This is a log message";
using (FileStream testStream = new FileStream (testFile, FileMode.OpenOrCreate, FileAccess.Write))
using (StreamWriter writer = new StreamWriter (testStream)) {
Console.SetOut (writer);
// simply test that we do write in the file. We need to close the stream to be able to read it
log.WriteLine (message);
}
using (FileStream testStream = new FileStream (testFile, FileMode.OpenOrCreate, FileAccess.Read))
using (var reader = new StreamReader (testStream)) {
var line = reader.ReadLine ();
StringAssert.EndsWith (message, line, "output"); // consider the time stamp
}
}
[TearDown]
public void TearDown ()
{
Console.SetOut (sdoutWriter); // get back to write to the console
File.Delete (testFile);
}
}
}

Просмотреть файл

@ -0,0 +1,130 @@
using System;
using System.IO;
using Moq;
using NUnit.Framework;
using Xharness.Logging;
namespace Xharness.Tests.Logging.Tests {
[TestFixture]
public class LogFileTest {
string path;
string description;
Mock<ILogs> logs;
[SetUp]
public void SetUp ()
{
description = "My log";
path = Path.GetTempFileName ();
logs = new Mock<ILogs> ();
File.Delete (path); // delete the empty file
}
[TearDown]
public void TearDown ()
{
if (File.Exists (path))
File.Delete (path);
path = null;
description = null;
logs = null;
}
[Test]
public void ConstructorTest ()
{
using (var log = new LogFile (logs.Object, description, path)) {
Assert.AreEqual (description, log.Description, "description");
Assert.AreEqual (path, log.FullPath, "path");
Assert.AreSame (logs.Object, log.Logs, "logs");
}
}
[Test]
public void ConstructorNullILogsTest ()
{
Assert.DoesNotThrow (() => {
using (var log = new LogFile (null, description, path)) ;
});
}
[Test]
public void ConstructorNullPathTest ()
{
Assert.Throws<ArgumentNullException> (() => { var log = new LogFile (logs.Object, description, null); });
}
[Test]
public void ConstructorNullDescriptionTest ()
{
Assert.DoesNotThrow (() => {
using (var log = new LogFile (logs.Object, null, path)) ;
});
}
[Test]
public void WriteTest ()
{
string oldLine = "Hello world!";
string newLine = "Hola mundo!";
// create a log, write to it and assert that we have the expected data
using (var stream = File.Create (path))
using (var writer = new StreamWriter (stream)) {
writer.WriteLine (oldLine);
}
using (var log = new LogFile (logs.Object, description, path)) {
log.WriteLine (newLine);
log.Flush ();
}
bool oldLineFound = false;
bool newLineFound = false;
using (var reader = new StreamReader (path)) {
string line;
while ((line = reader.ReadLine()) != null) {
if (line == oldLine)
oldLineFound = true;
if (line.EndsWith (newLine)) // consider time stamp
newLineFound = true;
}
}
Assert.IsTrue (oldLineFound, "old line");
Assert.IsTrue (newLineFound, "new line");
}
[Test]
public void WriteNotAppendTest ()
{
string oldLine = "Hello world!";
string newLine = "Hola mundo!";
// create a log, write to it and assert that we have the expected data
using (var stream = File.Create (path))
using (var writer = new StreamWriter (stream)) {
writer.WriteLine (oldLine);
}
using (var log = new LogFile (logs.Object, description, path, false)) {
log.WriteLine (newLine);
log.Flush ();
}
bool oldLineFound = false;
bool newLineFound = false;
using (var reader = new StreamReader (path)) {
string line;
while ((line = reader.ReadLine ()) != null) {
if (line == oldLine)
oldLineFound = true;
if (line.EndsWith (newLine)) // consider timestamp
newLineFound = true;
}
}
Assert.IsFalse (oldLineFound, "old line");
Assert.IsTrue (newLineFound, "new line");
}
}
}

Просмотреть файл

@ -0,0 +1,129 @@
using System;
using System.IO;
using System.Reflection;
using NUnit.Framework;
using Xharness.Logging;
namespace Xharness.Tests.Logging.Tests {
[TestFixture]
public class LogsTest {
string directory;
string fileName;
string description;
[SetUp]
public void SetUp ()
{
directory = Path.GetTempFileName ();
fileName = "test-log.txt";
description = "My description";
File.Delete (directory);
Directory.CreateDirectory (directory);
}
[TearDown]
public void TeatDown ()
{
if (Directory.Exists (directory))
Directory.Delete (directory, true);
}
[Test]
public void ConstructorTest ()
{
using (var logs = new Logs (directory)) {
Assert.AreEqual (directory, logs.Directory);
}
}
[Test]
public void ConstructorNullDirTest ()
{
Assert.Throws<ArgumentNullException> (() => new Logs (null));
}
[Test]
public void CreateFileTest ()
{
using (var logs = new Logs (directory)) {
var file = logs.CreateFile (fileName, description);
Assert.IsTrue (File.Exists (file), "exists");
Assert.AreEqual (fileName, Path.GetFileName (file), "file name");
Assert.AreEqual (1, logs.Count, "count");
}
}
[Test]
public void CreateFileNullPathTest ()
{
using (var logs = new Logs (directory)) {
fileName = null;
var description = "My description";
Assert.Throws<ArgumentNullException> (() => logs.CreateFile (fileName, description));
}
}
[Test]
public void CreateFileNullDescriptionTest ()
{
using (var logs = new Logs (directory)) {
string description = null;
Assert.DoesNotThrow (() => logs.CreateFile (fileName, description), "null description");
Assert.AreEqual (1, logs.Count, "count");
}
}
[Test]
public void AddFileTest ()
{
var fullPath = Path.Combine (directory, fileName);
File.Create (fullPath);
using (var logs = new Logs (directory)) {
var fileLog = logs.AddFile (fullPath, description);
Assert.AreEqual (fullPath, fileLog.Path, "path"); // path && fullPath are the same
Assert.AreEqual (Path.Combine (directory, fileName), fileLog.FullPath, "full path");
Assert.AreEqual (description, fileLog.Description, "description");
}
}
[Test]
public void AddFileNotInDirTest ()
{
var fullPath = Path.Combine (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location), fileName);
using (var stream = File.Create (fullPath))
using (var writer = new StreamWriter (stream)){
writer.WriteLine ("Hello world!");
}
using (var logs = new Logs (directory)) {
var newPath = Path.Combine (directory, Path.GetFileNameWithoutExtension (fileName));
var fileLog = logs.AddFile (fileName, description);
StringAssert.StartsWith (newPath, fileLog.Path, "path"); // assert new path
StringAssert.StartsWith (newPath, fileLog.FullPath, "full path"); // assert new path is used
Assert.IsTrue (File.Exists (fileLog.FullPath), "copy");
}
}
[Test]
public void AddFilePathNullTest ()
{
using (var logs = new Logs (directory)) {
Assert.Throws<ArgumentNullException> (() => logs.AddFile (null, description));
}
}
[Test]
public void AddFileDescriptionNull ()
{
var fullPath = Path.Combine (directory, fileName);
File.Create (fullPath);
using (var logs = new Logs (directory)) {
Assert.DoesNotThrow (() => logs.Create (fullPath, null), "throws");
Assert.AreEqual (1, logs.Count, "count");
}
}
}
}

Просмотреть файл

@ -264,7 +264,7 @@ namespace Xharness.Tests {
// expect the creation of the two diff xml file logs
_ = logs.Setup (l => l.Create (It.IsAny<string> (), "Failure Log tmp", null)).Returns (tmpLogMock.Object);
_ = logs.Setup (l => l.Create (It.IsAny<string> (), Log.XML_LOG, null)).Returns (xmlLogMock.Object);
_ = logs.Setup (l => l.Create (It.IsAny<string> (), LogType.XmlLog.ToString (), null)).Returns (xmlLogMock.Object);
if (jargon == XmlResultJargon.NUnitV3) {
_ = logs.Setup (l => l.Directory).Returns (logsDir);
_ = tmpLogMock.Setup (tmpLog => tmpLog.FullPath).Returns (tmpPath);

Просмотреть файл

@ -70,9 +70,6 @@
<Compile Include="..\Jenkins.cs">
<Link>Jenkins.cs</Link>
</Compile>
<Compile Include="..\Log.cs">
<Link>Log.cs</Link>
</Compile>
<Compile Include="..\MacTarget.cs">
<Link>MacTarget.cs</Link>
</Compile>
@ -180,6 +177,23 @@
<Compile Include="..\Logging\ILogFile.cs">
<Link>Logging\ILogFile.cs</Link>
</Compile>
<Compile Include="Logging\Tests\LogsTest.cs" />
<Compile Include="..\Logging\LogFile.cs">
<Link>Logging\LogFile.cs</Link>
</Compile>
<Compile Include="Logging\Tests\LogFileTest.cs" />
<Compile Include="Logging\Tests\ConsoleLogTest.cs" />
<Compile Include="..\Logging\ConsoleLog.cs">
<Link>Logging\ConsoleLog.cs</Link>
</Compile>
<Compile Include="..\Logging\CallbackLog.cs">
<Link>Logging\CallbackLog.cs</Link>
</Compile>
<Compile Include="Logging\Tests\CallbackLogTest.cs" />
<Compile Include="..\Logging\CaptureLog.cs">
<Link>Logging\CaptureLog.cs</Link>
</Compile>
<Compile Include="Logging\Tests\CaptureLogTest.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@ -190,6 +204,7 @@
<Folder Include="Tests\" />
<Folder Include="Samples\" />
<Folder Include="Logging\" />
<Folder Include="Logging\Tests\" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Samples\NUnitV2Sample.xml" />
@ -198,4 +213,4 @@
<EmbeddedResource Include="Samples\xUnitSample.xml" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

Просмотреть файл

@ -96,7 +96,6 @@
<Compile Include="Process_Extensions.cs" />
<Compile Include="Simulators.cs" />
<Compile Include="TestProject.cs" />
<Compile Include="Log.cs" />
<Compile Include="GitHub.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="..\mtouch\Cache.cs">
@ -122,6 +121,10 @@
<Compile Include="Logging\Log.cs" />
<Compile Include="Logging\ILog.cs" />
<Compile Include="Logging\ILogFile.cs" />
<Compile Include="Logging\LogFile.cs" />
<Compile Include="Logging\ConsoleLog.cs" />
<Compile Include="Logging\CallbackLog.cs" />
<Compile Include="Logging\CaptureLog.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="BCLTestImporter\" />