Files
sbox-public/engine/Sandbox.Test.Unit/System/ByteStreamTest.cs
Lorenz Junglas 91f8fcf183 Speed up / parallelize tests (#3587)
- Added Sandbox.Test.Unit project (contains independent tests that can run in parallel) 
- Modify some slow/stress tests (e.g. instead of doing a million iterations settle for 10k).

Tests run almost twice as fast now.
2025-12-10 14:23:00 +01:00

436 lines
9.5 KiB
C#

using System;
using static Sandbox.IByteParsable;
namespace SystemTest;
public class MyDataClass : IByteParsable<MyDataClass>
{
public string DataContents;
public static object ReadObject( ref ByteStream stream, ByteParseOptions o ) => Read( ref stream, o );
public static void WriteObject( ref ByteStream stream, object value, ByteParseOptions o ) => Write( ref stream, value as MyDataClass, o );
public static MyDataClass Read( ref ByteStream stream, ByteParseOptions o )
{
if ( stream.Read<byte>() == 0 )
return default;
var value = new MyDataClass();
value.DataContents = stream.Read<string>();
return value;
}
public static void Write( ref ByteStream stream, MyDataClass value, ByteParseOptions o )
{
if ( value is null )
{
stream.Write( (byte)0 );
return;
}
stream.Write( (byte)1 );
stream.Write( value.DataContents );
}
}
public class MyDataClassChild : MyDataClass
{
}
public struct MyPlainOldDataClass : IByteParsable<MyPlainOldDataClass>
{
public Vector3 Position;
public static object ReadObject( ref ByteStream stream, ByteParseOptions o ) => Read( ref stream, o );
public static void WriteObject( ref ByteStream stream, object value, ByteParseOptions o ) => Write( ref stream, (MyPlainOldDataClass)value, o );
public static MyPlainOldDataClass Read( ref ByteStream stream, ByteParseOptions o )
{
var value = new MyPlainOldDataClass();
value.Position = stream.Read<Vector3>();
return value;
}
public static void Write( ref ByteStream stream, MyPlainOldDataClass value, ByteParseOptions o )
{
stream.Write( value.Position );
}
}
[TestClass]
public class ByteStreamTest
{
[TestMethod]
public void WriterBasic()
{
var s = ByteStream.Create( 512 );
s.Write( (int)32 );
Assert.AreEqual( 4, s.Position );
s.Write( (uint)32 );
Assert.AreEqual( 8, s.Position );
s.Write( (short)32 );
Assert.AreEqual( 10, s.Position );
s.Write( (ushort)32 );
Assert.AreEqual( 12, s.Position );
s.Write( (long)32 );
Assert.AreEqual( 20, s.Position );
s.Write( (ulong)32 );
Assert.AreEqual( 28, s.Position );
s.Write( Vector2.One );
Assert.AreEqual( 36, s.Position );
s.Write( Vector3.One );
Assert.AreEqual( 48, s.Position );
s.Write( Vector4.One );
Assert.AreEqual( 64, s.Position );
s.Write( Transform.Zero );
s.Write( "hello there this is a big string" );
Console.WriteLine( $"Written {s.Position}b" );
}
[TestMethod]
public void ReadWriteInt()
{
var s = ByteStream.Create( 512 );
s.Write( 32 );
s.Write( 64 );
s.Write( 512 );
var data = s.ToArray();
s.Dispose();
s = ByteStream.CreateReader( data );
Assert.AreEqual( 32, s.Read<int>() );
Assert.AreEqual( 64, s.Read<int>() );
Assert.AreEqual( 512, s.Read<int>() );
}
[TestMethod]
public void ReadWriteFloat()
{
var s = ByteStream.Create( 512 );
s.Write( 32.0f );
s.Write( 64.0f );
s.Write( 512.0f );
var data = s.ToArray();
s.Dispose();
s = ByteStream.CreateReader( data );
Assert.AreEqual( 32.0f, s.Read<float>() );
Assert.AreEqual( 64.0f, s.Read<float>() );
Assert.AreEqual( 512.0f, s.Read<float>() );
}
[TestMethod]
public void ReadWriteVector3()
{
var s = ByteStream.Create( 512 );
s.Write( new Vector3( 4, 5, 1 ) );
s.Write( new Vector3( 44, 1235, 5121 ) );
s.Write( new Vector3( 0, 0, 0 ) );
var data = s.ToArray();
s.Dispose();
s = ByteStream.CreateReader( data );
Assert.AreEqual( new Vector3( 4, 5, 1 ), s.Read<Vector3>() );
Assert.AreEqual( new Vector3( 44, 1235, 5121 ), s.Read<Vector3>() );
Assert.AreEqual( new Vector3( 0, 0, 0 ), s.Read<Vector3>() );
}
[TestMethod]
public void ReadWriteByteParsable()
{
var s = ByteStream.Create( 512 );
var a = new MyDataClass();
a.DataContents = "Hello There Happy Birthday Superman";
s.Write( a );
var data = s.ToArray();
Assert.AreNotEqual( 0, data.Length );
s.Dispose();
s = ByteStream.CreateReader( data );
var b = s.Read<MyDataClass>();
Assert.AreEqual( b.DataContents, a.DataContents );
}
[TestMethod]
public void ReadWriteByteParsableStruct()
{
var s = ByteStream.Create( 512 );
var a = new MyPlainOldDataClass();
a.Position = Vector3.Up * 1000.0f;
s.Write( a );
var data = s.ToArray();
Assert.AreNotEqual( 0, data.Length );
s.Dispose();
s = ByteStream.CreateReader( data );
var b = s.Read<MyPlainOldDataClass>();
Assert.AreEqual( b.Position, a.Position );
}
[TestMethod]
public void Growing()
{
var s = ByteStream.Create( 2 );
s.Write( 32 );
s.Write( 64 );
s.Write( 512 );
var data = s.ToArray();
s.Dispose();
s = ByteStream.CreateReader( data );
Assert.AreEqual( 32, s.Read<int>() );
Assert.AreEqual( 64, s.Read<int>() );
Assert.AreEqual( 512, s.Read<int>() );
}
[TestMethod]
public void WriteStreamToStream()
{
using var s = ByteStream.Create( 2 );
s.Write( 64 );
s.Write( "Hello" );
s.Write( 512 );
using var t = ByteStream.Create( 2 );
t.Write( 32 );
t.Write( s );
t.Write( 109 );
Assert.AreEqual( s.Length + 4 + 4, t.Length );
var data = t.ToArray();
var r = ByteStream.CreateReader( data );
Assert.AreEqual( 32, r.Read<int>() );
Assert.AreEqual( 64, r.Read<int>() );
Assert.AreEqual( "Hello", r.Read<string>() );
Assert.AreEqual( 512, r.Read<int>() );
Assert.AreEqual( 109, r.Read<int>() );
}
[TestMethod]
public void No_Negative_Buffers()
{
Assert.ThrowsException<ArgumentOutOfRangeException>( () => ByteStream.Create( -1 ) );
}
[TestMethod]
public void ReadBuffer()
{
var data = Enumerable.Range( 0, 256 ).Select( x => (byte)x ).ToArray();
var r = ByteStream.CreateReader( data );
var buffer = new byte[256];
r.Read<int>();
Assert.AreEqual( 128, r.Read( buffer, 128, 128 ) );
Assert.AreEqual( 4, buffer[128] );
Assert.AreEqual( 5, buffer[129] );
Assert.AreEqual( 127 + 4, buffer[128 + 127] );
}
[TestMethod]
public void ReadBufferUntilStreamEnd()
{
var data = Enumerable.Range( 0, 16 ).Select( x => (byte)x ).ToArray();
var r = ByteStream.CreateReader( data );
var buffer = new byte[256];
r.Read<int>();
Assert.AreEqual( 12, r.Read( buffer, 128, 128 ) );
Assert.AreEqual( 4, buffer[128] );
Assert.AreEqual( 5, buffer[129] );
Assert.AreEqual( 15, buffer[128 + 11] );
Assert.AreEqual( 0, r.Read( buffer, 0, 256 ) );
}
[TestMethod]
public void ReadBufferOutOfRange()
{
var buffer = new byte[256];
Assert.ThrowsException<ArgumentOutOfRangeException>( () =>
{
var r = ByteStream.CreateReader( new byte[128] );
return r.Read( buffer, -1, 16 );
} );
Assert.ThrowsException<ArgumentOutOfRangeException>( () =>
{
var r = ByteStream.CreateReader( new byte[128] );
return r.Read( buffer, 384, 16 );
} );
Assert.ThrowsException<ArgumentOutOfRangeException>( () =>
{
var r = ByteStream.CreateReader( new byte[128] );
return r.Read( buffer, 128, 256 );
} );
}
[TestMethod]
public void LeakExploit()
{
const int chunkSize = 0x50000000 - 128;
byte[] chunk1 = new byte[chunkSize];
const int kLeakSize = 8;
Assert.ThrowsException<System.OverflowException>( () =>
{
ByteStream leakStream = ByteStream.Create( 128 );
leakStream.Write( chunk1, (int)(0x80000000 - kLeakSize), kLeakSize );
leakStream.Position = 0;
ulong arrPtr = leakStream.Read<ulong>();
} );
}
/// <summary>
/// We were getting rare access violations when decompressing a <see cref="ByteStream"/>.
/// </summary>
[TestMethod]
public void DecompressStressTest()
{
var payloadBytes = CreateRandomBytes( 0x1eb78437, 1024 );
using var payload = new ByteStream( payloadBytes );
using var compressed = payload.Compress();
var compressedBytes = compressed.ToArray();
for ( var i = 0; i < 10; ++i )
{
using var temp = new ByteStream( compressedBytes );
GC.Collect();
using var decompressed = temp.Decompress();
Assert.AreEqual( payloadBytes.Length, decompressed.Length );
foreach ( var b in payloadBytes )
{
Assert.AreEqual( b, decompressed.Read<byte>() );
}
}
}
[TestMethod]
public void WriteOffset()
{
using var stream = new ByteStream( 10 );
var data = new byte[100];
data[50] = 123;
data[59] = 231;
stream.Write( data, 50, 10 );
var written = stream.ToArray();
Assert.AreEqual( 123, written[0] );
Assert.AreEqual( 231, written[9] );
}
private static byte[] CreateRandomBytes( int seed, int length )
{
var random = new Random( seed );
var bytes = new byte[length];
random.NextBytes( bytes );
return bytes;
}
// This isn't : unmanaged
public readonly record struct TypeWrapper( Type Value );
interface IConstrainetBreaker<TFrom>
{
void Write( TFrom value );
void Read();
}
public class ConstraintBreaker<TFrom> : IConstrainetBreaker<TFrom> where TFrom : unmanaged
{
public void Write( TFrom value )
{
var byteStream = ByteStream.Create( IntPtr.Size );
byteStream.Write( value );
}
public void Read()
{
var byteStream = ByteStream.Create( IntPtr.Size );
var x = byteStream.Read<TFrom>();
}
}
[TestMethod]
public void CannotWriteNonPod()
{
var breakerType = typeof( ConstraintBreaker<> ).MakeGenericType( [typeof( TypeWrapper )] );
var instance = (IConstrainetBreaker<TypeWrapper>)Activator.CreateInstance( breakerType );
Assert.ThrowsException<InvalidOperationException>(
() =>
{
instance.Write( new TypeWrapper( typeof( int ) ) );
} );
}
[TestMethod]
public void CannotReadNonPod()
{
var breakerType = typeof( ConstraintBreaker<> ).MakeGenericType( [typeof( TypeWrapper )] );
var instance = (IConstrainetBreaker<TypeWrapper>)Activator.CreateInstance( breakerType );
Assert.ThrowsException<InvalidOperationException>(
() =>
{
instance.Read();
} );
}
}