diff --git a/CsCheck/Check.cs b/CsCheck/Check.cs index 2492179..bd2f216 100644 --- a/CsCheck/Check.cs +++ b/CsCheck/Check.cs @@ -117,7 +117,7 @@ public string ExceptionMessage(Func print) /// code to call related to writing metrics regarding generated inputs to file public static void Sample(this Gen gen, Action assert, Action? writeLine = null, string? seed = null, long iter = -1, int time = -1, int threads = -1, Func? print = null, - ILogger? logger = null) + ILogger? logger = null) { seed ??= Seed; if (iter == -1) iter = Iter; @@ -126,7 +126,7 @@ public static void Sample(this Gen gen, Action assert, Action? bool isIter = time < 0; var cde = new CountdownEvent(threads); if (logger is not null) - assert = logger.WrapAssert(assert); + assert = logger.WrapAssert(assert); var worker = new SampleActionWorker( gen, diff --git a/CsCheck/Logging.cs b/CsCheck/Logging.cs index f69d6ac..cb547ed 100644 --- a/CsCheck/Logging.cs +++ b/CsCheck/Logging.cs @@ -1,34 +1,42 @@ namespace CsCheck; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading.Channels; +using System.Xml; -public interface ILogger : IDisposable +#pragma warning disable IDE0290 // Use primary constructor +#pragma warning disable MA0004 // Use Task.ConfigureAwait +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + +public interface ILogger : IDisposable { - Action WrapAssert(Action assert); + Action WrapAssert(Action assert); } -public sealed class TycheLogger : ILogger +public sealed class TycheLogger : ILogger { private readonly Task _loggingTask; - private readonly Channel<(T Value, bool Success)> _channel; - public TycheLogger(Func loggingTask, Channel<(T Value, bool Success)> channel) + private readonly Channel<(object Value, bool Success)> _channel; + + public TycheLogger(Func loggingTask, Channel<(object Value, bool Success)> channel) { _loggingTask = Task.Run(loggingTask); _channel = channel; } - public Action WrapAssert(Action assert) + + public Action WrapAssert(Action assert) { return t => { try { assert(t); - _channel.Writer.TryWrite((t, true)); + _channel.Writer.TryWrite((t ?? (object)string.Empty, true)); } catch { - _channel.Writer.TryWrite((t, false)); + _channel.Writer.TryWrite((t ?? (object)string.Empty, false)); throw; } }; @@ -43,91 +51,66 @@ public void Dispose() public static class Logging { - public enum LogProcessor + public enum LogProcessor { Tyche } + + public static ILogger CreateLogger(LogProcessor p, [CallerMemberName] string? name = null, StreamWriter? writer = null) => p switch { - Tyche, - } + LogProcessor.Tyche => CreateTycheLogger(name, writer), + _ => throw new ArgumentOutOfRangeException(nameof(p), p, null), + }; - public static ILogger CreateLogger(LogProcessor p, string propertyUnderTest, StreamWriter? writer = null) + public static ILogger CreateTycheLogger([CallerMemberName] string? name = null, StreamWriter? writer = null) { - var channel = Channel.CreateUnbounded<(T Value, bool Success)>( - new UnboundedChannelOptions - { - SingleReader = true, - SingleWriter = false, - } - ); - switch (p) + if (name is null) throw new CsCheckException("name is null"); + var channel = Channel.CreateUnbounded<(object Value, bool Success)>(new() { SingleReader = true, SingleWriter = false }); + return new TycheLogger(async () => { - case LogProcessor.Tyche: - var t = async () => - { - var todayString = $"{DateTime.Today.Date:yyyy-M-dd}"; - var projectRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\..")); - - var runStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; - if (writer == null) - { - var infoFilePath = Path.Combine(projectRoot, ".cscheck\\observed", $"{todayString}_info.jsonl"); - Directory.CreateDirectory(Path.GetDirectoryName(infoFilePath)!); - - //Log information that Tyche uses to distinguish between different runs - using var infoWriter = new StreamWriter(infoFilePath, true); - infoWriter.AutoFlush = true; - - var infoRecord = - new - { - type = "info", - run_start = runStart, - property = propertyUnderTest, - title = "Hypothesis Statistics", - content = "" - }; - await infoWriter.WriteLineAsync(JsonSerializer.Serialize(infoRecord)).ConfigureAwait(false); - infoWriter.Close(); - } + var todayString = $"{DateTime.Today.Date:yyyy-M-dd}"; + var runStart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; + if (writer is null) + { + var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../.cscheck/observed"); + Directory.CreateDirectory(directory); + var infoFilePath = Path.Combine(directory, $"{todayString}_info.jsonl"); + var infoRecord = new TycheInfo("info", runStart, name, "Hypothesis Statistics", ""); + await using var infoWriter = new StreamWriter(infoFilePath, true); + infoWriter.AutoFlush = true; + await infoWriter.WriteLineAsync(JsonSerializer.Serialize(infoRecord)); + infoWriter.Close(); - if (writer != null) - { - writer.AutoFlush = true; - await LogTycheTestCases(propertyUnderTest, writer, channel, runStart).ConfigureAwait(false); - } - else - { - var testcasesFilePath = Path.Combine(projectRoot, $".CsCheck\\observed", $"{todayString}_testcases.jsonl"); - Directory.CreateDirectory(Path.GetDirectoryName(testcasesFilePath)!); - var fileStream = new FileStream(testcasesFilePath, FileMode.Append, FileAccess.Write, FileShare.Read); - using var w = new StreamWriter(fileStream); - w.AutoFlush = true; - await LogTycheTestCases(propertyUnderTest, w, channel, runStart).ConfigureAwait(false); - } - }; - return new TycheLogger(t, channel); - default: - throw new ArgumentOutOfRangeException(nameof(p), p, null); - } + var testcasesFilePath = Path.Combine(directory, $"{todayString}_testcases.jsonl"); + var fileStream = new FileStream(testcasesFilePath, FileMode.Append, FileAccess.Write, FileShare.Read); + await using var writer = new StreamWriter(fileStream); + writer.AutoFlush = true; + await LogTycheTestCases(name, writer, channel, runStart); + await writer.DisposeAsync(); + } + else + { + writer.AutoFlush = true; + await LogTycheTestCases(name, writer, channel, runStart); + } + }, channel); } - private static async Task LogTycheTestCases(string propertyUnderTest, StreamWriter writer, Channel<(T Value, bool Success)> channel, + private static async Task LogTycheTestCases(string propertyUnderTest, StreamWriter writer, Channel<(object Value, bool Success)> channel, double runStart) { - while (await channel.Reader.WaitToReadAsync().ConfigureAwait(false)) + while (await channel.Reader.WaitToReadAsync()) { - var d = new Dictionary(StringComparer.Ordinal); - var (value, success) = await channel.Reader.ReadAsync().ConfigureAwait(false); - var tycheData = new TycheData( - "test_case", runStart, propertyUnderTest, - success ? "passed" : "failed", JsonSerializer.Serialize(value), - "reason", d, "testing", d, null, d, d - ); + var (value, success) = await channel.Reader.ReadAsync(); + var tycheData = new TycheData("test_case", runStart, propertyUnderTest, success ? "passed" : "failed", JsonSerializer.Serialize(value) + , "reason", _emptyDictionary, "testing", _emptyDictionary, null, _emptyDictionary, _emptyDictionary); var serializedData = JsonSerializer.Serialize(tycheData); - await writer.WriteLineAsync(serializedData).ConfigureAwait(false); + await writer.WriteLineAsync(serializedData); } } + + private static readonly Dictionary _emptyDictionary = []; } #pragma warning disable IDE1006 // Naming Styles +public record TycheInfo(string type, double run_start, string property, string title, string content); public record TycheData(string type, double run_start, string property, string status, string representation, string? status_reason, Dictionary arguments, string? how_generated, Dictionary features, Dictionary? coverage, Dictionary timing, Dictionary metadata); \ No newline at end of file diff --git a/Tests/LoggingTests.cs b/Tests/LoggingTests.cs index a0fd703..c7e07f7 100644 --- a/Tests/LoggingTests.cs +++ b/Tests/LoggingTests.cs @@ -20,7 +20,7 @@ public void Bool_Distribution_WithTycheLogs(int generatedIntUponTrue) using var memoryStream = new MemoryStream(); using var writer = new StreamWriter(memoryStream); writer.AutoFlush = true; - var logger = Logging.CreateLogger(Logging.LogProcessor.Tyche, "Bool_Distribution_WithTycheLogs", writer); + var logger = Logging.CreateTycheLogger(writer: writer); // Random test logic const int frequency = 10; @@ -47,10 +47,10 @@ public void Bool_Distribution_WithTycheLogs(int generatedIntUponTrue) var tycheData = JsonSerializer.Deserialize(json); - Assert.True(tycheData != null && LogCheck(tycheData)); + Assert.True(tycheData != null && LogCheck(tycheData, generatedIntUponTrue)); Assert.Equal(20, JsonSerializer.Deserialize(tycheData.representation)?.Sum()); - bool LogCheck(TycheData td) + static bool LogCheck(TycheData td, int generatedIntUponTrue) { return td.type == "test_case" && @@ -61,16 +61,14 @@ bool LogCheck(TycheData td) } //Test below can be used to see example of output - [Theory(Skip="Only run if you want to verify Tyche output")] + [Theory(Skip = "Only run if you want to verify Tyche output")] [InlineData(1)] public void Bool_Distribution_WithTycheLogs_ToFile(int generatedIntUponTrue) { - var logger = Logging.CreateLogger(Logging.LogProcessor.Tyche, "Bool_Distribution_WithTycheLogs"); - + var logger = Logging.CreateTycheLogger(); // Random test logic const int frequency = 10; var expected = Enumerable.Repeat(frequency, 2).ToArray(); - //Try catch to suppress failing original test logic try { @@ -78,8 +76,6 @@ public void Bool_Distribution_WithTycheLogs_ToFile(int generatedIntUponTrue) .Select(sample => Tally(2, sample)) .Sample(actual => Check.ChiSquared(expected, actual, 10), iter: 1, time: -2, logger: logger); } - catch - { - } + catch {} } } \ No newline at end of file