update UserBlockTracker API

This commit is contained in:
nossr50
2024-05-19 12:44:34 -07:00
parent c2054a5d45
commit 8b82163e3d
26 changed files with 708 additions and 468 deletions

View File

@@ -13,9 +13,8 @@ import org.mockito.MockedStatic;
import static com.gmail.nossr50.util.PotionEffectUtil.getNauseaPotionEffectType;
import static java.util.logging.Logger.getLogger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.when;
class PotionEffectUtilTest {
private MockedStatic<mcMMO> mockedStaticMcMMO;

View File

@@ -0,0 +1,121 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
import static org.bukkit.Bukkit.getWorld;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
class BitSetChunkStoreTest {
private static File tempDir;
@BeforeAll
public static void setUpClass() {
tempDir = Files.createTempDir();
}
@AfterAll
public static void tearDownClass() {
recursiveDelete(tempDir);
}
private World mockWorld;
private MockedStatic<Bukkit> bukkitMock;
private MockedStatic<mcMMO> mcMMOMock;
@BeforeEach
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = mockStatic(mcMMO.class);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void testSetValue() {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
original.setTrue(0, 0, 0);
assertTrue(original.isTrue(0, 0, 0));
original.setFalse(0, 0, 0);
assertFalse(original.isTrue(0, 0, 0));
}
@Test
void testIsEmpty() {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
assertTrue(original.isEmpty());
original.setTrue(0, 0, 0);
original.setFalse(0, 0, 0);
assertTrue(original.isEmpty());
}
@Test
void testRoundTrip() throws IOException {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkStore(original);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertChunkStoreEquals(original, deserialized);
}
@Test
void testNegativeWorldMin() throws IOException {
when(mockWorld.getMinHeight()).thenReturn(-64);
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, -32, 12);
original.setTrue(14, -64, 12);
original.setTrue(13, -63, 12);
byte[] serializedBytes = serializeChunkStore(original);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertChunkStoreEquals(original, deserialized);
}
@Test
void testNegativeWorldMinUpgrade() throws IOException {
final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 1, 12);
original.setTrue(14, 2, 12);
original.setTrue(13, 3, 12);
byte[] serializedBytes = serializeChunkStore(original);
when(mockWorld.getMinHeight()).thenReturn(-64);
final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqualIgnoreMinMax(original, deserialized);
}
}

View File

@@ -0,0 +1,49 @@
package com.gmail.nossr50.util.blockmeta;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BlockStoreTestUtils {
public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
/**
* Asserts that the two ChunkStores are equal.
* @param expected The expected ChunkStore
* @param actual The actual ChunkStore
*/
static void assertChunkStoreEquals(ChunkStore expected, ChunkStore actual) {
assertEquals(expected.getChunkMin(), actual.getChunkMin());
assertEquals(expected.getChunkMax(), actual.getChunkMax());
assertEqualIgnoreMinMax(expected, actual);
}
static byte[] serializeChunkStore(@NotNull ChunkStore chunkStore) throws IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (chunkStore instanceof BitSetChunkStore)
BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
else
new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
// it were the old
// PrimitiveChunkStore
return byteArrayOutputStream.toByteArray();
}
static void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
assertEquals(expected.getChunkX(), actual.getChunkX());
assertEquals(expected.getChunkZ(), actual.getChunkZ());
assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
continue; // Ignore
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
}
}
}

View File

@@ -1,13 +1,9 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
@@ -15,14 +11,13 @@ import org.mockito.Mockito;
import java.io.*;
import java.util.UUID;
/**
* Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
* that the serialization isn't completely broken.
*/
class ChunkStoreTest {
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
import static org.bukkit.Bukkit.getWorld;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
class ChunkStoreTest {
private static File tempDir;
@BeforeAll
@@ -44,151 +39,39 @@ class ChunkStoreTest {
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = Mockito.mockStatic(Bukkit.class);
bukkitMock.when(() -> Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = Mockito.mockStatic(mcMMO.class);
mcMMOMock = mockStatic(mcMMO.class);
Mockito.when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
Mockito.when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void testIndexOutOfBounds() {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
HashChunkManager hashChunkManager = new HashChunkManager();
// Top Block
Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(illegalHeightBlock));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setTrue(illegalHeightBlock));
}
@Test
void testSetTrue() {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
HashChunkManager hashChunkManager = new HashChunkManager();
int radius = 2; // Could be anything but drastically changes test time
for (int x = -radius; x <= radius; x++) {
for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
for (int z = -radius; z <= radius; z++) {
Block testBlock = initMockBlock(x, y, z);
hashChunkManager.setTrue(testBlock);
Assertions.assertTrue(hashChunkManager.isTrue(testBlock));
hashChunkManager.setFalse(testBlock);
Assertions.assertFalse(hashChunkManager.isTrue(testBlock));
}
}
}
// Bot Block
Block bottomBlock = initMockBlock(1337, 0, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(bottomBlock));
Assertions.assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
hashChunkManager.setTrue(bottomBlock);
Assertions.assertTrue(hashChunkManager.isTrue(bottomBlock));
// Top Block
Block topBlock = initMockBlock(1337, 255, -1337);
Assertions.assertFalse(hashChunkManager.isTrue(topBlock));
Assertions.assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
hashChunkManager.setTrue(topBlock);
Assertions.assertTrue(hashChunkManager.isTrue(topBlock));
}
@Test
void testSetValue() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
original.setTrue(0, 0, 0);
Assertions.assertTrue(original.isTrue(0, 0, 0));
original.setFalse(0, 0, 0);
Assertions.assertFalse(original.isTrue(0, 0, 0));
}
@Test
void testIsEmpty() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertTrue(original.isEmpty());
original.setTrue(0, 0, 0);
original.setFalse(0, 0, 0);
Assertions.assertTrue(original.isEmpty());
}
@Test
void testRoundTrip() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
void testNegativeWorldMin() throws IOException {
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, -32, 12);
original.setTrue(14, -64, 12);
original.setTrue(13, -63, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
void testNegativeWorldMinUpgrade() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 1, 12);
original.setTrue(14, 2, 12);
original.setTrue(13, 3, 12);
byte[] serializedBytes = serializeChunkstore(original);
Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqualIgnoreMinMax(original, deserialized);
}
@Test
void testChunkCoords() throws IOException {
for (int x = -96; x < 0; x++) {
int cx = x >> 4;
int ix = Math.abs(x) % 16;
//System.out.print(cx + ":" + ix + " ");
}
}
@Test
void testUpgrade() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 32);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
byte[] serializedBytes = serializeChunkStore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assert deserialized != null;
assertEqual(original, deserialized);
assertChunkStoreEquals(original, deserialized);
}
@Test
void testSimpleRegionRoundtrip() throws IOException {
void testSimpleRegionRoundTrip() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
@@ -196,7 +79,7 @@ class ChunkStoreTest {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
try (DataOutputStream outputStream = region.getOutputStream(12, 12)) {
outputStream.write(serializeChunkstore(original));
outputStream.write(serializeChunkStore(original));
}
region.close();
region = new McMMOSimpleRegionFile(file, 0, 0);
@@ -204,229 +87,10 @@ class ChunkStoreTest {
Assertions.assertNotNull(is);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
assert deserialized != null;
assertEqual(original, deserialized);
assertChunkStoreEquals(original, deserialized);
}
region.close();
file.delete();
}
@Test
void testSimpleRegionRejectsOutOfBounds() {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
region.close();
}
@Test
void testChunkStoreRejectsOutOfBounds() {
ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
}
@Test
void testRegressionChunkMirrorBug() {
ChunkManager chunkManager = new HashChunkManager();
Block mockBlockA = Mockito.mock(Block.class);
Mockito.when(mockBlockA.getX()).thenReturn(15);
Mockito.when(mockBlockA.getZ()).thenReturn(15);
Mockito.when(mockBlockA.getY()).thenReturn(0);
Mockito.when(mockBlockA.getWorld()).thenReturn(mockWorld);
Block mockBlockB = Mockito.mock(Block.class);
Mockito.when(mockBlockB.getX()).thenReturn(-15);
Mockito.when(mockBlockB.getZ()).thenReturn(-15);
Mockito.when(mockBlockB.getY()).thenReturn(0);
Mockito.when(mockBlockB.getWorld()).thenReturn(mockWorld);
chunkManager.setTrue(mockBlockA);
chunkManager.setFalse(mockBlockB);
Assertions.assertTrue(chunkManager.isTrue(mockBlockA));
}
private void assertEqual(ChunkStore expected, ChunkStore actual) {
Assertions.assertEquals(expected.getChunkMin(), actual.getChunkMin());
Assertions.assertEquals(expected.getChunkMax(), actual.getChunkMax());
assertEqualIgnoreMinMax(expected, actual);
}
private void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
Assertions.assertEquals(expected.getChunkX(), actual.getChunkX());
Assertions.assertEquals(expected.getChunkZ(), actual.getChunkZ());
Assertions.assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
continue; // Ignore
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
Assertions.assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
}
}
private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (chunkStore instanceof BitSetChunkStore)
BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
else
new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
// it were the old
// PrimitiveChunkStore
return byteArrayOutputStream.toByteArray();
}
public static class LegacyChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private final int cx;
private final int cz;
private final @NotNull UUID worldUid;
public LegacyChunkStore(@NotNull World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public int getChunkMin() {
return 0;
}
@Override
public int getChunkMax() {
return store[0][0].length;
}
@Override
public @NotNull UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public void set(int x, int y, int z, boolean value) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = value;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
}
private static class UnitTestObjectOutputStream extends ObjectOutputStream {
public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
super(outputStream);
}
@Override
public void writeUTF(@NotNull String str) throws IOException {
// Pretend to be the old class
if (str.equals(LegacyChunkStore.class.getName()))
str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
super.writeUTF(str);
}
}
@NotNull
private Block initMockBlock(int x, int y, int z) {
Block mockBlock = Mockito.mock(Block.class);
Mockito.when(mockBlock.getX()).thenReturn(x);
Mockito.when(mockBlock.getY()).thenReturn(y);
Mockito.when(mockBlock.getZ()).thenReturn(z);
Mockito.when(mockBlock.getWorld()).thenReturn(mockWorld);
return mockBlock;
}
public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
if (directoryToBeDeleted.isDirectory()) {
for (File file : directoryToBeDeleted.listFiles()) {
recursiveDelete(file);
}
}
directoryToBeDeleted.delete();
}
}
}

View File

@@ -0,0 +1,127 @@
package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.UUID;
/**
* Used for unit testing upgrades from the old ChunkStore class.
*/
class LegacyChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private final int cx;
private final int cz;
private final @NotNull UUID worldUid;
public LegacyChunkStore(@NotNull World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public int getChunkMin() {
return 0;
}
@Override
public int getChunkMax() {
return store[0][0].length;
}
@Override
public @NotNull UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public void set(int x, int y, int z, boolean value) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = value;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,23 @@
package com.gmail.nossr50.util.blockmeta;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
class UnitTestObjectOutputStream extends ObjectOutputStream {
public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
super(outputStream);
}
@Override
public void writeUTF(@NotNull String str) throws IOException {
// Pretend to be the old class
if (str.equals(LegacyChunkStore.class.getName()))
str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
super.writeUTF(str);
}
}

View File

@@ -0,0 +1,192 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.BlockUtils;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MAX;
import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MIN;
import static org.bukkit.Bukkit.getWorld;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
/**
* Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
* that the serialization isn't completely broken.
*/
class UserBlockTrackerTest {
private static File tempDir;
@BeforeAll
public static void setUpClass() {
tempDir = Files.createTempDir();
}
@AfterAll
public static void tearDownClass() {
recursiveDelete(tempDir);
}
private World mockWorld;
private MockedStatic<Bukkit> bukkitMock;
private MockedStatic<mcMMO> mcMMOMock;
@BeforeEach
void setUpMock() {
UUID worldUUID = UUID.randomUUID();
mockWorld = Mockito.mock(World.class);
when(mockWorld.getUID()).thenReturn(worldUUID);
when(mockWorld.getMaxHeight()).thenReturn(256);
when(mockWorld.getWorldFolder()).thenReturn(tempDir);
bukkitMock = mockStatic(Bukkit.class);
bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
mcMMOMock = mockStatic(mcMMO.class);
when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
}
@AfterEach
void teardownMock() {
bukkitMock.close();
mcMMOMock.close();
}
@Test
void setIneligibleShouldThrowIndexOutOfBoundsException() {
when(mockWorld.getMinHeight()).thenReturn(-64);
final HashChunkManager hashChunkManager = new HashChunkManager();
// Top Block
final Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
assertFalse(hashChunkManager.isIneligible(illegalHeightBlock));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setIneligible(illegalHeightBlock));
}
@Test
void testSetEligibility() {
when(mockWorld.getMinHeight()).thenReturn(-64);
final HashChunkManager hashChunkManager = new HashChunkManager();
int radius = 2; // Could be anything but drastically changes test time
for (int x = -radius; x <= radius; x++) {
for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
for (int z = -radius; z <= radius; z++) {
final Block testBlock = initMockBlock(x, y, z);
// mark ineligible
hashChunkManager.setIneligible(testBlock);
assertTrue(hashChunkManager.isIneligible(testBlock));
// mark eligible
hashChunkManager.setEligible(testBlock);
// Might as well test both isIneligible and isEligible while we are here
assertFalse(hashChunkManager.isIneligible(testBlock));
assertTrue(hashChunkManager.isEligible(testBlock));
}
}
}
// TODO: Whatever is going on down here should be in its own test
// Bot Block
final Block bottomBlock = initMockBlock(1337, 0, -1337);
assertFalse(hashChunkManager.isIneligible(bottomBlock));
assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
hashChunkManager.setIneligible(bottomBlock);
assertTrue(hashChunkManager.isIneligible(bottomBlock));
// Top Block
final Block topBlock = initMockBlock(1337, 255, -1337);
assertFalse(hashChunkManager.isIneligible(topBlock));
assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
hashChunkManager.setIneligible(topBlock);
assertTrue(hashChunkManager.isIneligible(topBlock));
}
@Test
void testChunkCoords() throws IOException {
// TODO: Unfinished test?
for (int x = -96; x < 0; x++) {
int cx = x >> 4;
int ix = Math.abs(x) % 16;
//System.out.print(cx + ":" + ix + " ");
}
}
@Test
void testSimpleRegionRejectsOutOfBounds() {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
region.close();
}
@Test
void testChunkStoreRejectsOutOfBounds() {
ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
}
@Test
void testRegressionChunkMirrorBug() {
final UserBlockTracker chunkManager = new HashChunkManager();
Block mockBlockA = Mockito.mock(Block.class);
when(mockBlockA.getX()).thenReturn(15);
when(mockBlockA.getZ()).thenReturn(15);
when(mockBlockA.getY()).thenReturn(0);
when(mockBlockA.getWorld()).thenReturn(mockWorld);
Block mockBlockB = Mockito.mock(Block.class);
when(mockBlockB.getX()).thenReturn(-15);
when(mockBlockB.getZ()).thenReturn(-15);
when(mockBlockB.getY()).thenReturn(0);
when(mockBlockB.getWorld()).thenReturn(mockWorld);
chunkManager.setIneligible(mockBlockA);
chunkManager.setEligible(mockBlockB);
assertTrue(chunkManager.isIneligible(mockBlockA));
}
@NotNull
private Block initMockBlock(int x, int y, int z) {
final Block mockBlock = Mockito.mock(Block.class);
when(mockBlock.getX()).thenReturn(x);
when(mockBlock.getY()).thenReturn(y);
when(mockBlock.getZ()).thenReturn(z);
when(mockBlock.getWorld()).thenReturn(mockWorld);
return mockBlock;
}
public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
if (directoryToBeDeleted.isDirectory()) {
for (File file : directoryToBeDeleted.listFiles()) {
recursiveDelete(file);
}
}
directoryToBeDeleted.delete();
}
}