Skip to content

Commit

Permalink
Added basic tests for BitMask (#278)
Browse files Browse the repository at this point in the history
* Sealed `BitMask` class and changed existing comments to documentation comments

* Added checks to BitMask width and height arguments. I was undecided if 0 was or not a valid value, but I decided it is based on how Cairo does things.

See: https://cgit.freedesktop.org/cairo/tree/src/cairo-image-surface.c

in function _cairo_image_surface_is_size_valid

* Added basic tests to `BitMask`

---------

Co-authored-by: Lehonti Ramos <john@doe>
  • Loading branch information
Lehonti and Lehonti Ramos authored Aug 7, 2023
1 parent f4206fc commit 3ec8774
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 48 deletions.
100 changes: 52 additions & 48 deletions Pinta.Core/Effects/BitMask.cs
Original file line number Diff line number Diff line change
@@ -1,69 +1,73 @@
using System;
using System.Collections;

namespace Pinta.Core
namespace Pinta.Core;

/// <summary>
/// Represents a two-dimensional matrix of bits used to store a
/// true/false value for each pixel in an image.
/// </summary>
public sealed class BitMask
{
// Represent a two dimensional matrix of bits used to store a
// true/false value for each pixel in an image.
public class BitMask
private readonly BitArray array;

public BitMask (int width, int height)
{
private readonly BitArray array;
if (width < 0)
throw new ArgumentOutOfRangeException (nameof (width));
if (height < 0)
throw new ArgumentOutOfRangeException (nameof (height));

public BitMask (int width, int height)
{
Width = width;
Height = height;
Width = width;
Height = height;

array = new BitArray (width * height);
}

public bool this[PointI pt] {
get => this[pt.X, pt.Y];
set => this[pt.X, pt.Y] = value;
}
array = new BitArray (width * height);
}

public bool this[int x, int y] {
get => Get (x, y);
set => Set (x, y, value);
}
public bool this[PointI pt] {
get => this[pt.X, pt.Y];
set => this[pt.X, pt.Y] = value;
}

public int Width { get; }
public bool this[int x, int y] {
get => Get (x, y);
set => Set (x, y, value);
}

public int Height { get; }
public int Width { get; }

public bool IsEmpty => array.Length == 0;
public int Height { get; }

public void Clear (bool newValue) => array.SetAll (newValue);
public bool IsEmpty => array.Length == 0;

public bool Get (int x, int y) => array.Get (GetIndex (x, y));
public void Clear (bool newValue) => array.SetAll (newValue);

public void Invert (int x, int y)
{
var index = GetIndex (x, y);
public bool Get (int x, int y) => array.Get (GetIndex (x, y));

array[index] = !array[index];
}
public void Invert (int x, int y)
{
var index = GetIndex (x, y);

public void Invert (Scanline scan)
{
var x = scan.X;
array[index] = !array[index];
}

while (x < scan.X + scan.Length) {
Invert (x, scan.Y);
++x;
}
public void Invert (Scanline scan)
{
var x = scan.X;
while (x < scan.X + scan.Length) {
Invert (x, scan.Y);
++x;
}
}

public void Set (int x, int y, bool newValue) => array[GetIndex (x, y)] = newValue;

public void Set (RectangleI rect, bool newValue)
{
for (var y = rect.Y; y <= rect.Bottom; ++y) {
for (var x = rect.X; x <= rect.Right; ++x) {
Set (x, y, newValue);
}
}
}
public void Set (int x, int y, bool newValue) => array[GetIndex (x, y)] = newValue;

private int GetIndex (int x, int y) => (y * Width) + x;
public void Set (RectangleI rect, bool newValue)
{
for (var y = rect.Y; y <= rect.Bottom; ++y)
for (var x = rect.X; x <= rect.Right; ++x)
Set (x, y, newValue);
}

private int GetIndex (int x, int y) => (y * Width) + x;
}
125 changes: 125 additions & 0 deletions tests/Pinta.Core.Tests/BitMaskTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;

namespace Pinta.Core.Tests;

[TestFixture]
internal sealed class BitMaskTest
{
const int DEFAULT_SIZE = 1;
const int DEFAULT_OFFSET = 0;

const int DEFAULT_HEIGHT = DEFAULT_SIZE;
const int DEFAULT_HEIGHT_INDEX = DEFAULT_OFFSET;

const int DEFAULT_WIDTH = DEFAULT_SIZE;
const int DEFAULT_WIDTH_INDEX = DEFAULT_OFFSET;

[TestCase (-1)]
public void Constructor_RejectsInvalidWidth (int width)
{
Assert.Throws<ArgumentOutOfRangeException> (() => new BitMask (width, DEFAULT_HEIGHT));
}

[TestCase (-1)]
public void Constructor_RejectsInvalidHeight (int height)
{
Assert.Throws<ArgumentOutOfRangeException> (() => new BitMask (DEFAULT_WIDTH, height));
}

[TestCaseSource (nameof (out_of_bounds_access_cases))]
public void WidthAccessOutOfBoundsFails (int desiredWidth, int indexToAccess)
{
var mask = new BitMask (desiredWidth, DEFAULT_HEIGHT);
var coordinates = new PointI (indexToAccess, DEFAULT_HEIGHT_INDEX);
Assert.Throws<ArgumentOutOfRangeException> (() => _ = mask[indexToAccess, DEFAULT_HEIGHT_INDEX]);
Assert.Throws<ArgumentOutOfRangeException> (() => _ = mask[coordinates]);
}

[TestCaseSource (nameof (within_bounds_access_cases))]
public void WidthAccessWithinBoundsSucceeds (int desiredWidth, int indexToAccess)
{
var mask = new BitMask (desiredWidth, DEFAULT_HEIGHT);
var coordinates = new PointI (indexToAccess, DEFAULT_HEIGHT_INDEX);
Assert.DoesNotThrow (() => _ = mask[indexToAccess, DEFAULT_HEIGHT_INDEX]);
Assert.DoesNotThrow (() => _ = mask[coordinates]);
}

[TestCaseSource (nameof (out_of_bounds_access_cases))]
public void HeightAccessOutOfBoundsFails (int desiredHeight, int indexToAccess)
{
var mask = new BitMask (DEFAULT_WIDTH, desiredHeight);
var coordinates = new PointI (DEFAULT_WIDTH_INDEX, indexToAccess);
Assert.Throws<ArgumentOutOfRangeException> (() => _ = mask[DEFAULT_WIDTH_INDEX, indexToAccess]);
Assert.Throws<ArgumentOutOfRangeException> (() => _ = mask[coordinates]);
}

[TestCaseSource (nameof (within_bounds_access_cases))]
public void HeightAccessWithinBoundsSucceeds (int desiredHeight, int indexToAccess)
{
var mask = new BitMask (DEFAULT_WIDTH, desiredHeight);
var coordinates = new PointI (DEFAULT_WIDTH_INDEX, indexToAccess);
Assert.DoesNotThrow (() => _ = mask[DEFAULT_WIDTH_INDEX, indexToAccess]);
Assert.DoesNotThrow (() => _ = mask[coordinates]);
}

[TestCase (DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_WIDTH_INDEX, DEFAULT_HEIGHT_INDEX)]
public void BitInitializedToFalse (int maskWidth, int maskHeight, int bitToTestX, int bitToTestY)
{
var mask = new BitMask (maskWidth, maskHeight);
var bit = mask[bitToTestX, bitToTestY];
Assert.IsFalse (bit);
}

[TestCase (DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_WIDTH_INDEX, DEFAULT_HEIGHT_INDEX)]
public void BitInvertsWithXY (int maskWidth, int maskHeight, int bitToInvertX, int bitToInvertY)
{
var mask = new BitMask (maskWidth, maskHeight);
mask.Invert (bitToInvertX, bitToInvertY);
var bit = mask[bitToInvertX, bitToInvertY];
Assert.IsTrue (bit);
}

[TestCase (DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_WIDTH_INDEX, DEFAULT_HEIGHT_INDEX)]
public void BitInvertsWithScanline (int maskWidth, int maskHeight, int bitToInvertX, int bitToInvertY)
{
var mask = new BitMask (maskWidth, maskHeight);
var scanline = new Scanline (bitToInvertX, bitToInvertY, 1);
mask.Invert (scanline);
var bit = mask[bitToInvertX, bitToInvertY];
Assert.IsTrue (bit);
}

[TestCase (DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_WIDTH_INDEX, DEFAULT_HEIGHT_INDEX, new[] { true, false, true, false })]
public void BitGetsSetXY (int maskWidth, int maskHeight, int bitToSetX, int bitToSetY, bool[] valuesToSetAndTest)
{
var mask = new BitMask (maskWidth, maskHeight);
var coordinates = new PointI (bitToSetX, bitToSetY);
foreach (var value in valuesToSetAndTest) {
mask.Set (bitToSetX, bitToSetY, value);
Assert.AreEqual (value, mask[bitToSetX, bitToSetY]);
Assert.AreEqual (value, mask[coordinates]);
}
}

static readonly IReadOnlyList<TestCaseData> out_of_bounds_access_cases = new TestCaseData[]
{
new (0, 0),
new (0, 1),
new (1, 1),
new (1, 2),
new (1, -1),
new (1, int.MinValue),
new (1, int.MinValue + 1),
new (1, int.MaxValue),
new (1, int.MaxValue - 1),
new (2, 2),
};

static readonly IReadOnlyList<TestCaseData> within_bounds_access_cases = new TestCaseData[]
{
new (DEFAULT_SIZE, DEFAULT_OFFSET),
new (2, 1),
};
}

0 comments on commit 3ec8774

Please sign in to comment.