Reputation: 51
I'm encountering memory corruption issues when running tests using XUnit (and experiencing the same behavior with NUnit), even with seemingly straightforward code. There's no multithreading or complex dependencies involved. The test project is set up for headless testing according to the Avalonia documentation.
Here's the simplified test case:
[AvaloniaFact]
public unsafe void TestWriteableBitmap()
{
var image = new WriteableBitmap(new PixelSize(1024, 1024), new Vector(96, 96), PixelFormats.Rgba8888);
using (var lockedBitmap = image.Lock())
{
//make all 0
Unsafe.InitBlock(lockedBitmap.Address.ToPointer(), 0, (uint)(lockedBitmap.RowBytes * lockedBitmap.Size.Height));
//write 0
using (var stream = File.OpenWrite($"/home/qserj1/test/000.bit"))
{
var pixels = new byte[(uint)(lockedBitmap.RowBytes * lockedBitmap.Size.Height)];
Marshal.Copy(lockedBitmap.Address, pixels, 0, pixels.Length);
stream.Write(pixels, 0, pixels.Length);
}
}
//write 1
using (var lockedBitmap = image.Lock())
{
using (var stream = File.OpenWrite($"/home/qserj1/test/001.bit"))
{
var pixels = new byte[(uint)(lockedBitmap.RowBytes * lockedBitmap.Size.Height)];
Marshal.Copy(lockedBitmap.Address, pixels, 0, pixels.Length);
stream.Write(pixels, 0, pixels.Length);
}
}
}
The test creates a WriteableBitmap, initializes its memory to zeros, and writes the data to "000.bit". Then, it locks the bitmap again and writes the data to "001.bit". I expect both files to contain only zeros, but the second file ("001.bit") contains garbage data.
There seems to be some correlation with running the test in debug mode versus release mode, but it's not clear. The most confusing part is that when this code is integrated into a running Avalonia application, the files always contain zeros, as expected. I need to optimize a procedure that uses WriteableBitmap to create images from network data, and I'm encountering unexpected garbage data in the images within the test environment.
How can I explain this discrepancy between the test environment and the running application? Are there any specific considerations for writing tests involving WriteableBitmap in Avalonia using XUnit/NUnit?
Avalonia 11.2 (11.1.4 too) xUnit 2.6.6
Test application: https://drive.google.com/file/d/1b18DcQMMcMSzQq8c2PB5XPVW52Zsh7D-/view?usp=sharing
Upvotes: 2
Views: 32
Reputation: 51
I figured out what the problem was. It’s so simple that it’s surprising how much time I spent struggling with what seemed like a terrible bug. To work with Avalonia objects in the XUnit testing environment, the Avalonia.Headless.XUnit package is used. It provides the creation of an Avalonia environment and the ability to create its objects and perform various actions with them.
So, when calling .Lock() on a WriteableBitmap while using Headless.XUnit, we end up with the following function (located in the HeadlessPlatformRenderInterface.cs file):
public ILockedFramebuffer Lock()
{
Version++;
var mem = Marshal.AllocHGlobal(PixelSize.Width * PixelSize.Height * 4);
return new LockedFramebuffer(mem, PixelSize, PixelSize.Width * 4, Dpi, PixelFormat.Rgba8888,
() => Marshal.FreeHGlobal(mem));
}
Do you understand what this does? Every time Lock() is called, it allocates a new buffer in memory and returns a new LockedFramebuffer object created over this memory block. At the end of its lifecycle, it frees this buffer. Each time, it’s a new buffer—uninitialized. Simply put, this behavior is completely incorrect and bears no resemblance to the actual functioning of WriteableBitmap, aside from returning a buffer of the size expected by the image. This is the whole problem that nearly drove me crazy :-)
I still have a question: is it possible to use something other than the Headless stub in the XUnit environment? It seems like a slightly different question. After all, you could write a test application without any testing frameworks, and it would be quite reliable.
Upvotes: 3