Browse Source

test: add abstract file read tests

Akeit0 7 months ago
parent
commit
e9c4b276e5

+ 75 - 0
tests/Lua.Tests/AbstractFileTests.cs

@@ -0,0 +1,75 @@
+using Lua.IO;
+using Lua.Standard;
+using Lua.Tests.Helpers;
+
+namespace Lua.Tests;
+
+public class AbstractFileTests
+{
+    class ReadOnlyFileSystem(Dictionary<string, string> dictionary) : NotImplementedExceptionFileSystemBase
+    {
+        public override ILuaIOStream? Open(string path, LuaFileOpenMode mode, bool throwError)
+        {
+            if (!dictionary.TryGetValue(path, out var value))
+            {
+                if (!throwError) return null;
+                throw new FileNotFoundException($"File {path} not found");
+            }
+
+            if (mode != LuaFileOpenMode.Read)
+                throw new NotSupportedException($"File {path} not opened in read mode");
+            return new ReadOnlyCharMemoryLuaIOStream(value.AsMemory());
+        }
+    }
+
+    [Test]
+    public async Task ReadLinesTest()
+    {
+        var fileContent = "line1\nline2\r\nline3";
+        var fileSystem = new ReadOnlyFileSystem(new() { { "test.txt", fileContent } });
+        var state = LuaState.Create();
+        state.FileSystem = fileSystem;
+        state.OpenStandardLibraries();
+        try
+        {
+            await state.DoStringAsync(
+                """
+                local lines = {}
+                for line in io.lines("test1.txt") do
+                  table.insert(lines, line)
+                  print(line)
+                end
+                assert(#lines == 3, "Expected 3 lines")
+                assert(lines[1] == "line1", "Expected line1")
+                assert(lines[2] == "line2", "Expected line2")
+                assert(lines[3] == "line3", "Expected line3")
+                """);
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine(e);
+            throw;
+        }
+    }
+
+    [Test]
+    public async Task ReadFileTest()
+    {
+        var fileContent = "Hello, World!";
+        var fileSystem = new ReadOnlyFileSystem(new() { { "test.txt", fileContent } });
+        var state = LuaState.Create();
+        state.FileSystem = fileSystem;
+        state.OpenStandardLibraries();
+
+        await state.DoStringAsync(
+            """
+            local file = io.open("test.txt", "r")
+            assert(file, "Failed to open file")
+            local content = file:read("*a")
+            assert(content == "Hello, World!", "Expected 'Hello, World!'")
+            file:close()
+            file = io.open("test2.txt", "r")
+            assert(file == nil, "Expected file to be nil")
+            """);
+    }
+}

+ 102 - 0
tests/Lua.Tests/Helpers/CharMemoryStream.cs

@@ -0,0 +1,102 @@
+using Lua.IO;
+namespace Lua.Tests.Helpers;
+
+internal sealed class ReadOnlyCharMemoryLuaIOStream(ReadOnlyMemory<char> buffer, Action<ReadOnlyCharMemoryLuaIOStream>? onDispose  =null,object? state =null) : NotSupportedStreamBase
+{
+    public readonly ReadOnlyMemory<char> Buffer = buffer;
+    int position;
+    public readonly object? State = state;
+    Action<ReadOnlyCharMemoryLuaIOStream>? onDispose = onDispose;
+
+    public static (string Result, int AdvanceCount) ReadLine(ReadOnlySpan<char> remaining)
+    {
+        int advanceCount;
+        var line = remaining.IndexOfAny('\n', '\r');
+        if (line == -1)
+        {
+            line = remaining.Length;
+            advanceCount = line;
+        }
+        else
+        {
+            if (remaining[line] == '\r' && line + 1 < remaining.Length && remaining[line + 1] == '\n')
+            {
+                advanceCount = line + 2;
+            }
+            else
+            {
+                advanceCount = line + 1;
+            }
+        }
+
+
+        return new(remaining[..line].ToString(), advanceCount);
+    }
+    public override ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+    {
+        if (position >= Buffer.Length)
+        {
+            return new(default(string));
+        }
+
+        var remaining = Buffer[position..];
+        var (line, advanceCount) = ReadLine(remaining.Span);
+        position += advanceCount;
+        return new(line);
+    }
+
+    public override ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+    {
+        if (position >= Buffer.Length)
+        {
+            return new(string.Empty);
+        }
+
+        var remaining = Buffer[position..];
+        position = Buffer.Length;
+        return new(remaining.ToString());
+    }
+
+    public override ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
+    {
+        cancellationToken .ThrowIfCancellationRequested();
+        if (position >= Buffer.Length)
+        {
+            return new("");
+        }
+
+        var remaining = Buffer[position..];
+        if (count > remaining.Length)
+        {
+            count = remaining.Length;
+        }
+
+        var result = remaining.Slice(0, count).ToString();
+        position += count;
+        return new(result);
+    }
+
+    public override void Dispose()
+    {
+        onDispose?.Invoke(this);
+        onDispose = null;
+    }
+
+    public override long Seek(long offset, SeekOrigin origin)
+    {
+        unchecked
+        {
+            position = origin switch
+            {
+                SeekOrigin.Begin => (int)offset,
+                SeekOrigin.Current => position + (int)offset,
+                SeekOrigin.End => (int)(Buffer.Length + offset),
+                _ => (int)IOThrowHelpers.ThrowArgumentExceptionForSeekOrigin()
+            };
+        }
+
+        IOThrowHelpers.ValidatePosition(position, Buffer.Length);
+
+        return position;
+    }
+}

+ 181 - 0
tests/Lua.Tests/Helpers/IOThrowHelpers.cs

@@ -0,0 +1,181 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// CommunityToolkit.HighPerformance.Streams
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+namespace Lua.Tests.Helpers;
+
+internal static class IOThrowHelpers
+{
+    /// <summary>
+    /// Validates the <see cref="Stream.Position"/> argument (it needs to be in the [0, length]) range.
+    /// </summary>
+    /// <param name="position">The new <see cref="Stream.Position"/> value being set.</param>
+    /// <param name="length">The maximum length of the target <see cref="Stream"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidatePosition(long position, int length)
+    {
+        if ((ulong)position > (ulong)length)
+        {
+            ThrowArgumentOutOfRangeExceptionForPosition();
+        }
+    }
+
+    /// <summary>
+    /// Validates the <see cref="Stream.Position"/> argument (it needs to be in the [0, length]) range.
+    /// </summary>
+    /// <param name="position">The new <see cref="Stream.Position"/> value being set.</param>
+    /// <param name="length">The maximum length of the target <see cref="Stream"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidatePosition(long position, long length)
+    {
+        if ((ulong)position > (ulong)length)
+        {
+            ThrowArgumentOutOfRangeExceptionForPosition();
+        }
+    }
+
+    /// <summary>
+    /// Validates the <see cref="Stream.Read(byte[],int,int)"/> or <see cref="Stream.Write(byte[],int,int)"/> arguments.
+    /// </summary>
+    /// <param name="buffer">The target array.</param>
+    /// <param name="offset">The offset within <paramref name="buffer"/>.</param>
+    /// <param name="count">The number of elements to process within <paramref name="buffer"/>.</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateBuffer([NotNull] byte[]? buffer, int offset, int count)
+    {
+        if (buffer is null)
+        {
+            ThrowArgumentNullExceptionForBuffer();
+        }
+
+        if (offset < 0)
+        {
+            ThrowArgumentOutOfRangeExceptionForOffset();
+        }
+
+        if (count < 0)
+        {
+            ThrowArgumentOutOfRangeExceptionForCount();
+        }
+
+        if (offset + count > buffer!.Length)
+        {
+            ThrowArgumentExceptionForLength();
+        }
+    }
+
+    /// <summary>
+    /// Validates the CanWrite property.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateCanWrite(bool canWrite)
+    {
+        if (!canWrite)
+        {
+            ThrowNotSupportedException();
+        }
+    }
+
+    /// <summary>
+    /// Validates that a given <see cref="Stream"/> instance hasn't been disposed.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static void ValidateDisposed(bool disposed)
+    {
+        if (disposed)
+        {
+            ThrowObjectDisposedException();
+        }
+    }
+    /// <summary>
+    /// Gets a standard <see cref="NotSupportedException"/> instance for a stream.
+    /// </summary>
+    /// <returns>A <see cref="NotSupportedException"/> with the standard text.</returns>
+    public static Exception GetNotSupportedException()
+    {
+        return new NotSupportedException("The requested operation is not supported for this stream.");
+    }
+
+    /// <summary>
+    /// Throws a <see cref="NotSupportedException"/> when trying to perform a not supported operation.
+    /// </summary>
+    [DoesNotReturn]
+    public static void ThrowNotSupportedException()
+    {
+        throw GetNotSupportedException();
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target stream.
+    /// </summary>
+    [DoesNotReturn]
+    public static void ThrowArgumentExceptionForEndOfStreamOnWrite()
+    {
+        throw new ArgumentException("The current stream can't contain the requested input data.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when using an invalid seek mode.
+    /// </summary>
+    /// <returns>Nothing, as this method throws unconditionally.</returns>
+    public static long ThrowArgumentExceptionForSeekOrigin()
+    {
+        throw new ArgumentException("The input seek mode is not valid.", "origin");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when setting the <see cref="Stream.Position"/> property.
+    /// </summary>
+    private static void ThrowArgumentOutOfRangeExceptionForPosition()
+    {
+        throw new ArgumentOutOfRangeException(nameof(Stream.Position), "The value for the property was not in the valid range.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentNullException"/> when an input buffer is <see langword="null"/>.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentNullExceptionForBuffer()
+    {
+        throw new ArgumentNullException("buffer", "The buffer is null.");
+    }
+    
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentOutOfRangeExceptionForOffset()
+    {
+        throw new ArgumentOutOfRangeException("offset", "Offset can't be negative.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentOutOfRangeExceptionForCount()
+    {
+        throw new ArgumentOutOfRangeException("count", "Count can't be negative.");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ArgumentException"/> when the sum of offset and count exceeds the length of the target buffer.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowArgumentExceptionForLength()
+    {
+        throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
+    }
+
+    /// <summary>
+    /// Throws an <see cref="ObjectDisposedException"/> when using a disposed <see cref="Stream"/> instance.
+    /// </summary>
+    [DoesNotReturn]
+    private static void ThrowObjectDisposedException()
+    {
+        throw new ObjectDisposedException("source", "The current stream has already been disposed");
+    }
+}

+ 39 - 0
tests/Lua.Tests/Helpers/NotImplementedExceptionFileSystemBase.cs

@@ -0,0 +1,39 @@
+using Lua.IO;
+
+namespace Lua.Tests.Helpers
+{
+    abstract class NotImplementedExceptionFileSystemBase : ILuaFileSystem
+    {
+        public virtual bool IsReadable(string path)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual ValueTask<LuaFileContent> ReadFileContentAsync(string fileName, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual ILuaIOStream? Open(string path, LuaFileOpenMode mode, bool throwError)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual void Rename(string oldName, string newName)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual void Remove(string path)
+        {
+            throw new NotImplementedException();
+        }
+
+        public virtual string DirectorySeparator => Path.DirectorySeparatorChar.ToString();
+
+        public virtual string GetTempFileName()
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 46 - 0
tests/Lua.Tests/Helpers/NotSupportedStreamBase.cs

@@ -0,0 +1,46 @@
+using Lua.IO;
+
+namespace Lua.Tests.Helpers
+{
+    public class NotSupportedStreamBase : ILuaIOStream
+    {
+        public virtual void Dispose()
+        {
+        }
+
+        public virtual ValueTask<string?> ReadLineAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask<string> ReadToEndAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask<string?> ReadStringAsync(int count, CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual ValueTask FlushAsync(CancellationToken cancellationToken)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual void SetVBuf(LuaFileBufferingMode mode, int size)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+
+        public virtual  long Seek(long offset, SeekOrigin origin)
+        {
+            throw IOThrowHelpers.GetNotSupportedException();
+        }
+    }
+}