Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demonstrator for issue #1167 #7

Merged
merged 9 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Issue1167/.runsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>

<NUnit>
<ConsoleOut>1</ConsoleOut>
<Verbosity>0</Verbosity>
<UseTestNameInConsoleOutput>false</UseTestNameInConsoleOutput>
</NUnit>

<MSTest>
</MSTest>

<xUnit>
</xUnit>

</RunSettings>
99 changes: 99 additions & 0 deletions Issue1167/ConsoleRecorder.PoC/ConsoleOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using ConsoleRecorder.PoC.Utilities;

namespace ConsoleRecorder.PoC.Environment
{
/// <summary>
/// Provides services for interacting with the console output from the test host.
/// This class cannot be inherited.
/// </summary>
internal static class ConsoleOutput
{
/// <summary>
/// Gets a value indicating whether console output from the test host is enabled.
/// </summary>
public static bool IsEnabled { get; private set; }

/// <summary>
/// Returns a reader for the recorded standard output stream.
/// </summary>
public static TextReader Out => new StringReader(
_recorder.GetRecording(ConsoleRecorder.ChannelSelection.Out));

/// <summary>
/// Returns a reader for the recorded standard error stream.
/// </summary>
public static TextReader Error => new StringReader(
_recorder.GetRecording(ConsoleRecorder.ChannelSelection.Error));

/// <summary>
/// Returns a reader for the recorded console output.
/// </summary>
public static TextReader All => new StringReader(
_recorder.GetRecording(
ConsoleRecorder.ChannelSelection.Out | ConsoleRecorder.ChannelSelection.Error));

/// <summary>
/// Gets the interface to the console recorder.
/// </summary>
public static IRecorder Recorder => _recorder;

/// <summary>
/// A recorder for capturing console output.
/// </summary>
private static readonly ConsoleRecorder _recorder;

/// <summary>
/// The original standard output stream of the test host.
/// </summary>
private static readonly TextWriter _testHostStdOut;

/// <summary>
/// The original standard error stream of the test host.
/// </summary>
private static readonly TextWriter _testHostStdErr;

/// <summary>
/// Initializes static members of the <see cref="ConsoleOutput"/> class.
/// </summary>
static ConsoleOutput()
{
_recorder = new ConsoleRecorder();

_testHostStdOut = Console.Out;
_testHostStdErr = Console.Error;

// Intercept the console output and split it between console and recorder.
Console.SetOut(new DualWriter(_testHostStdOut, _recorder.Channels.Out));
Console.SetError(new DualWriter(_testHostStdErr, _recorder.Channels.Error));
IsEnabled = true;
}

/// <summary>
/// Enables console output from the test host.
/// </summary>
public static void Enable()
{
if (IsEnabled)
return;

Console.SetOut(new DualWriter(_testHostStdOut, _recorder.Channels.Out));
Console.SetError(new DualWriter(_testHostStdErr, _recorder.Channels.Error));

IsEnabled = true;
}

/// <summary>
/// Disables console output from the test host.
/// </summary>
public static void Disable()
{
if (!IsEnabled)
return;

Console.SetOut(new DualWriter(TextWriter.Null, _recorder.Channels.Out));
Console.SetError(new DualWriter(TextWriter.Null, _recorder.Channels.Error));

IsEnabled = false;
}
}
}
21 changes: 21 additions & 0 deletions Issue1167/ConsoleRecorder.PoC/ConsoleRecorder.PoC.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MyApp\MyApp.csproj" />
</ItemGroup>

</Project>
125 changes: 125 additions & 0 deletions Issue1167/ConsoleRecorder.PoC/ConsoleRecorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
namespace ConsoleRecorder.PoC
{
using global::ConsoleRecorder.PoC.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;

/// <summary>
/// Provides recording capabilities for console output.
/// </summary>
public class ConsoleRecorder : IRecorder
{
/// <summary>
/// Selects the recorded channels.
/// </summary>
[Flags]
public enum ChannelSelection
{
/// <summary>
/// The recording of the standard output channel.
/// </summary>
Out = 0x1,

/// <summary>
/// The recording of the standard error channel.
/// </summary>
Error = 0x2
}

/// <summary>
/// Gets a value indicating whether a recording is running.
/// </summary>
public bool IsRecording { get; private set; }

internal RecorderChannels Channels { get; private set; }

/// <summary>
/// The list of recordings.
/// </summary>
/// <remarks>
/// Retains individual recordings for each channel plus one combined recording.
/// </remarks>
private readonly StringWriter[] _recordings;

/// <summary>
/// Initializes a new instance of the <see cref="ConsoleRecorder"/> class.
/// </summary>
public ConsoleRecorder()
{
_recordings = Enumerable.Range(0, 3).Select(sw => new StringWriter()).ToArray();

var outRecordWriter = new RecordWriter(
recorder: this, record: new DualWriter(_recordings[0], _recordings[2]));
var errorRecordWriter = new RecordWriter(
recorder: this, record: new DualWriter(_recordings[1], _recordings[2]));

Channels = new RecorderChannels()
{
Out = outRecordWriter,
Error = errorRecordWriter
};

IsRecording = false;
}

/// <inheritdoc/>
public void Start()
{
if (IsRecording)
{
throw new InvalidOperationException(
"Cannot start recording. A recording is already in progress. " +
"Stop the current recording before starting a new one.");
}

IsRecording = true;
}

/// <inheritdoc/>
public void Stop()
{
if (!IsRecording)
{
throw new InvalidOperationException(
"Cannot stop recording. No recording is currently in progress. " +
"Start a recording before attempting to stop it.");
}

IsRecording = false;
}

/// <inheritdoc/>
public void Reset()
{
if (IsRecording)
{
throw new InvalidOperationException(
"Cannot reset the recorder while a recording is in progress. " +
"Stop the current recording before resetting.");
}

foreach (var recording in _recordings)
recording.GetStringBuilder().Clear();
}

/// <summary>
/// Returns the recorded console output.
/// </summary>
/// <param name="recording">The type of recording.</param>
/// <returns>A string containing the recorded console output.</returns>
public string GetRecording(ChannelSelection channels)
{
if (channels.HasFlag(ChannelSelection.Out) && channels.HasFlag(ChannelSelection.Error))
return _recordings[2].ToString();
else if (channels.HasFlag(ChannelSelection.Out))
return _recordings[0].ToString();
else if (channels.HasFlag(ChannelSelection.Error))
return _recordings[1].ToString();
else
return string.Empty;
}
}
}
78 changes: 78 additions & 0 deletions Issue1167/ConsoleRecorder.PoC/DualWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text;

namespace ConsoleRecorder.PoC.Utilities
{
/// <summary>
/// Implements a <see cref="TextWriter"/> for writing text to two writers in turn.
/// </summary>
/// <remarks>
/// This writer does not implement a backing store.
/// </remarks>
internal class DualWriter : TextWriter
{
private readonly TextWriter _primaryWriter;
private readonly TextWriter _secondaryWriter;

/// <inheritdoc/>
public override Encoding Encoding => Encoding.Default;

/// <summary>
/// Initializes a new instance of the <see cref="DualWriter"/> class.
/// </summary>
/// <param name="primaryWriter">The primary writer to write a copy to.</param>
/// <param name="secondaryWriter">The secondary writer to write a copy to.</param>
/// <exception cref="ArgumentNullException">Thrown if any of the arguments is null.</exception>
public DualWriter(TextWriter primaryWriter, TextWriter secondaryWriter)
{
ArgumentNullException.ThrowIfNull(primaryWriter);
ArgumentNullException.ThrowIfNull(secondaryWriter);

_primaryWriter = primaryWriter;
_secondaryWriter = secondaryWriter;
}

/// <inheritdoc/>
public override void Write(char value)
{
_primaryWriter.Write(value);
_secondaryWriter.Write(value);
}

/// <inheritdoc/>
/// <remarks>
/// This override exists as a workaround for the erroneous writing
/// of single characters to the standard error stream when using NUnit.
/// For more information, refer to <see href="https://github.com/nunit/nunit/issues/4414">issue #4414</see>.
/// </remarks>
public override void Write(string? value)
{
if (value is not null)
{
_primaryWriter.Write(value);
_secondaryWriter.Write(value);
}
}

/// <inheritdoc/>
/// <remarks>
/// This override exists as a workaround for the erroneous writing
/// of single characters to the standard error stream when using NUnit.
/// For more information, refer to <see href="https://github.com/nunit/nunit/issues/4414">issue #4414</see>.
/// </remarks>
public override void WriteLine(string? value)
{
if (value is not null)
{
_primaryWriter.WriteLine(value);
_secondaryWriter.WriteLine(value);
}
}

/// <inheritdoc/>
public override void Flush()
{
_primaryWriter.Flush();
_secondaryWriter.Flush();
}
}
}
28 changes: 28 additions & 0 deletions Issue1167/ConsoleRecorder.PoC/IRecorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace ConsoleRecorder.PoC
{
/// <summary>
/// Provides mechanisms for recording data.
/// </summary>
public interface IRecorder
{
/// <summary>
/// Gets a value indicating whether a recording is running.
/// </summary>
bool IsRecording { get; }

/// <summary>
/// Starts the recording.
/// </summary>
void Start();

/// <summary>
/// Stops the recording.
/// </summary>
void Stop();

/// <summary>
/// Erases the recorded data and prepares the recorder for a new recording.
/// </summary>
void Reset();
}
}
Loading