📄 tests/DltReaderTests.cs
using Shouldly;

namespace SharpDlt.Tests;

public class DltReaderTests
{
    private static byte[] TwoMessages() =>
        [
            .. TestDlt.VerboseMessage([TestDlt.StringArgument("first")], messageCounter: 1),
            .. TestDlt.VerboseMessage([TestDlt.StringArgument("second")], messageCounter: 2),
        ];

    [Fact]
    public void ReadMessages_FromStream_ReadsEveryMessage()
    {
        using var stream = new MemoryStream(TwoMessages());

        var messages = DltReader.ReadMessages(stream).ToList();

        messages.Count.ShouldBe(2);
        messages[0].StandardHeader.MessageCounter.ShouldBe((byte)1);
        messages[0].ToString().ShouldEndWith("first");
        messages[1].StandardHeader.MessageCounter.ShouldBe((byte)2);
        messages[1].ToString().ShouldEndWith("second");
    }

    [Fact]
    public void ReadMessages_FromEmptyStream_ReturnsNothing()
    {
        using var stream = new MemoryStream();

        DltReader.ReadMessages(stream).ShouldBeEmpty();
    }

    [Fact]
    public void ReadMessages_IsLazy()
    {
        using var stream = new MemoryStream(TwoMessages());

        var messages = DltReader.ReadMessages(stream);
        stream.Position.ShouldBe(0);

        messages.First();
        stream.Position.ShouldBeGreaterThan(0);
        stream.Position.ShouldBeLessThan(stream.Length);
    }

    [Fact]
    public void ReadMessages_TruncatedTrailingMessage_IsIgnored()
    {
        var data = TwoMessages();
        using var stream = new MemoryStream(data[..^3]);

        DltReader.ReadMessages(stream).Count().ShouldBe(1);
    }

    [Fact]
    public void ReadMessages_TrailingBytesShorterThanAHeader_AreIgnored()
    {
        var data = TwoMessages().Concat<byte>([0x44, 0x4C]).ToArray();
        using var stream = new MemoryStream(data);

        DltReader.ReadMessages(stream).Count().ShouldBe(2);
    }

    [Fact]
    public void ReadMessages_WithoutStorageHeader()
    {
        var data = TestDlt.VerboseMessage([TestDlt.StringArgument("no storage")], withStorageHeader: false);
        using var stream = new MemoryStream(data);

        var messages = DltReader.ReadMessages(stream, new DltReaderOptions { HasStorageHeader = false }).ToList();

        messages.ShouldHaveSingleItem().StorageHeader.ShouldBeNull();
    }

    [Fact]
    public void ReadMessages_InvalidLengthField_Throws()
    {
        var data = TestDlt.Message(TestDlt.StorageHeader(), TestDlt.StandardHeader());
        data[18] = 0;
        data[19] = 3;
        using var stream = new MemoryStream(data);

        Should.Throw<DltParseException>(() => DltReader.ReadMessages(stream).ToList());
    }

    [Fact]
    public void ReadMessages_InvalidStorageHeader_Throws()
    {
        var data = TwoMessages();
        data[0] = (byte)'X';
        using var stream = new MemoryStream(data);

        Should.Throw<DltParseException>(() => DltReader.ReadMessages(stream).ToList());
    }

    [Fact]
    public void ReadMessages_NullStream_Throws()
    {
        Should.Throw<ArgumentNullException>(() => DltReader.ReadMessages((Stream)null!));
    }

    [Fact]
    public void ReadMessages_EmptyPath_Throws()
    {
        Should.Throw<ArgumentException>(() => DltReader.ReadMessages(string.Empty));
    }

    [Fact]
    public void ReadMessages_FromFile_ReadsEveryMessageAndClosesTheFile()
    {
        var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");
        File.WriteAllBytes(path, TwoMessages());
        try
        {
            var messages = DltReader.ReadMessages(path).ToList();

            messages.Count.ShouldBe(2);

            // The file is closed again, so it can be deleted.
            File.Delete(path);
            File.Exists(path).ShouldBeFalse();
        }
        finally
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
    }

    [Fact]
    public void ReadMessages_MissingFile_Throws()
    {
        var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");

        Should.Throw<FileNotFoundException>(() => DltReader.ReadMessages(path).ToList());
    }

    [Fact]
    public async Task ReadMessagesAsync_FromStream_ReadsEveryMessage()
    {
        using var stream = new MemoryStream(TwoMessages());

        var messages = await DltReader.ReadMessagesAsync(stream).ToListAsync();

        messages.Count.ShouldBe(2);
        messages[1].ToString().ShouldEndWith("second");
    }

    [Fact]
    public async Task ReadMessagesAsync_FromFile_ReadsEveryMessage()
    {
        var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");
        await File.WriteAllBytesAsync(path, TwoMessages());
        try
        {
            var messages = await DltReader.ReadMessagesAsync(path).ToListAsync();

            messages.Count.ShouldBe(2);
        }
        finally
        {
            File.Delete(path);
        }
    }

    [Fact]
    public async Task ReadMessagesAsync_Cancellation_Throws()
    {
        using var stream = new MemoryStream(TwoMessages());
        using var cancellation = new CancellationTokenSource();
        await cancellation.CancelAsync();

        await Should.ThrowAsync<OperationCanceledException>(async () =>
            await DltReader.ReadMessagesAsync(stream, cancellationToken: cancellation.Token).ToListAsync()
        );
    }
}