table;
20 |
21 | public SerializedPersistableFileTable() {
22 | kryo.register(SerializedPersistableFileTable.class);
23 | kryo.register(HashMap.class);
24 | kryo.register(FilePointer.class);
25 | this.table = new HashMap<>();
26 | }
27 |
28 | public static SerializedPersistableFileTable fromEmpty() {
29 | return new SerializedPersistableFileTable();
30 | }
31 |
32 | public static SerializedPersistableFileTable fromFile(String filePath) throws FileNotFoundException, KryoException {
33 | kryo.register(SerializedPersistableFileTable.class);
34 | kryo.register(HashMap.class);
35 | kryo.register(FilePointer.class);
36 | try (Input input = new Input(new FileInputStream(filePath))) {
37 | return kryo.readObject(input, SerializedPersistableFileTable.class);
38 | } catch (KryoException e) {
39 | throw new InvalidFileTableException("Failed to load FileTable from disk: " + e.getMessage());
40 | }
41 | }
42 |
43 | @Override
44 | public void put(byte[] key, FilePointer value) {
45 | if (key != null && value != null) {
46 | table.put(new String(key), value);
47 | }
48 | }
49 |
50 | @Override
51 | public FilePointer get(byte[] key) {
52 | if (key != null) {
53 | return table.get(new String(key));
54 | }
55 | return null;
56 | }
57 |
58 | @Override
59 | public void saveToDisk(String filePath) throws FileNotFoundException {
60 | Output output = new Output(new FileOutputStream(filePath));
61 | kryo.writeObject(output, this);
62 | output.close();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/sahilbondre/firefly/log/FileChannelRandomAccessLog.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.log;
2 |
3 | import com.sahilbondre.firefly.filetable.FilePointer;
4 | import com.sahilbondre.firefly.model.Segment;
5 |
6 | import java.io.IOException;
7 | import java.io.RandomAccessFile;
8 | import java.nio.ByteBuffer;
9 | import java.nio.channels.FileChannel;
10 | import java.nio.channels.FileLock;
11 | import java.nio.file.Paths;
12 |
13 | public class FileChannelRandomAccessLog implements RandomAccessLog {
14 |
15 | private final String filePath;
16 | private final RandomAccessFile randomAccessFile;
17 | private final FileChannel fileChannel;
18 | private final FileLock fileLock;
19 |
20 | public FileChannelRandomAccessLog(String filePath) throws IOException {
21 | this.filePath = filePath;
22 | this.randomAccessFile = new RandomAccessFile(filePath, "rw");
23 | this.fileChannel = randomAccessFile.getChannel();
24 | this.fileLock = fileChannel.lock();
25 | }
26 |
27 | @Override
28 | public long size() throws IOException {
29 | return fileChannel.size();
30 | }
31 |
32 | @Override
33 | public String getFilePath() {
34 | return filePath;
35 | }
36 |
37 | @Override
38 | public FilePointer append(byte[] message) throws IOException {
39 | fileChannel.position(fileChannel.size());
40 | ByteBuffer buffer = ByteBuffer.wrap(message);
41 | fileChannel.write(buffer);
42 | return new FilePointer(filePath, fileChannel.size() - message.length);
43 | }
44 |
45 | @Override
46 | public byte[] read(long offset, long length) throws IOException, InvalidRangeException {
47 | long fileSize = fileChannel.size();
48 |
49 | if (offset < 0 || offset >= fileSize || length <= 0 || offset + length > fileSize) {
50 | throw new InvalidRangeException("Invalid offset or length");
51 | }
52 |
53 | fileChannel.position(offset);
54 | ByteBuffer buffer = ByteBuffer.allocate((int) length);
55 | fileChannel.read(buffer);
56 | return buffer.array();
57 | }
58 |
59 | @Override
60 | public Segment readSegment(long offset) throws IOException, InvalidRangeException {
61 | long fileSize = fileChannel.size();
62 |
63 | if (offset < 0 || offset >= fileSize) {
64 | throw new InvalidRangeException("Invalid offset");
65 | }
66 |
67 | // Read Key Size
68 | byte[] keySizeBytes = new byte[Segment.KEY_SIZE_LENGTH];
69 | fileChannel.read(ByteBuffer.wrap(keySizeBytes), offset + Segment.KEY_SIZE_LENGTH);
70 |
71 | // Read Value Size
72 | byte[] valueSizeBytes = new byte[Segment.VALUE_SIZE_LENGTH];
73 | fileChannel.read(ByteBuffer.wrap(valueSizeBytes),
74 | offset + Segment.KEY_SIZE_LENGTH + Segment.VALUE_SIZE_LENGTH);
75 |
76 | // Total Size
77 | int totalSize = Segment.CRC_LENGTH + Segment.KEY_SIZE_LENGTH +
78 | Segment.VALUE_SIZE_LENGTH + byteArrayToInt(keySizeBytes) + byteArrayToInt(valueSizeBytes);
79 |
80 | // Read entire segment
81 | byte[] segmentBytes = new byte[totalSize];
82 | fileChannel.read(ByteBuffer.wrap(segmentBytes), offset);
83 |
84 |
85 | Segment segment = Segment.fromByteArray(segmentBytes);
86 |
87 | // Validate CRC
88 | if (!segment.isSegmentValid()) {
89 | throw new InvalidRangeException("Segment is invalid");
90 | }
91 |
92 | return segment;
93 | }
94 |
95 | @Override
96 | public Integer getLogId() {
97 | String fileNameWithoutPath = Paths.get(filePath).getFileName().toString();
98 | return Integer.parseInt(fileNameWithoutPath.substring(0, fileNameWithoutPath.length() - 4));
99 | }
100 |
101 | private int byteArrayToInt(byte[] bytes) {
102 | return (bytes[0] << 8) | (bytes[1] & 0xFF);
103 | }
104 |
105 | public void close() throws IOException {
106 | fileLock.release();
107 | fileChannel.close();
108 | randomAccessFile.close();
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/com/sahilbondre/firefly/log/InvalidRangeException.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.log;
2 |
3 | public class InvalidRangeException extends IllegalArgumentException {
4 | public InvalidRangeException(String message) {
5 | super(message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/sahilbondre/firefly/log/RandomAccessLog.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.log;
2 |
3 | import com.sahilbondre.firefly.filetable.FilePointer;
4 | import com.sahilbondre.firefly.model.Segment;
5 |
6 | import java.io.IOException;
7 |
8 | public interface RandomAccessLog {
9 | long size() throws IOException;
10 |
11 | String getFilePath();
12 |
13 | FilePointer append(byte[] message) throws IOException;
14 |
15 | byte[] read(long offset, long length) throws IOException, InvalidRangeException;
16 |
17 | Segment readSegment(long offset) throws IOException, InvalidRangeException;
18 |
19 | void close() throws IOException;
20 |
21 | Integer getLogId();
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/com/sahilbondre/firefly/model/Segment.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.model;
2 |
3 | public class Segment {
4 |
5 | public static final int CRC_LENGTH = 2;
6 | public static final int KEY_SIZE_LENGTH = 2;
7 | public static final int VALUE_SIZE_LENGTH = 4;
8 | /**
9 | * Class representing a segment of the log file.
10 | *
11 | * Two big decisions here to save on performance:
12 | * 1. We're using byte[] instead of ByteBuffer.
13 | * 2. We're trusting that the byte[] is immutable and hence avoiding copying it.
14 | *
15 | *
16 | * 2 bytes: CRC
17 | * 2 bytes: Key Size
18 | * 4 bytes: Value Size
19 | * n bytes: Key
20 | * m bytes: Value
21 | *
22 | * Note: Value size is four bytes because we're using a 32-bit integer to store the size.
23 | * Int is 32-bit signed, so we can only store 2^31 - 1 bytes in the value.
24 | * Hence, the maximum size of the value is 2,147,483,647 bytes or 2.14 GB.
25 | */
26 | private final byte[] bytes;
27 |
28 | private Segment(byte[] bytes) {
29 | this.bytes = bytes;
30 | }
31 |
32 | public static Segment fromByteArray(byte[] data) {
33 | return new Segment(data);
34 | }
35 |
36 | public static Segment fromKeyValuePair(byte[] key, byte[] value) {
37 | int keySize = key.length;
38 | int valueSize = value.length;
39 | int totalSize = CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH + keySize + valueSize;
40 |
41 | byte[] segment = new byte[totalSize];
42 |
43 | // Set key size
44 | segment[2] = (byte) ((keySize >> 8) & 0xFF);
45 | segment[3] = (byte) (keySize & 0xFF);
46 |
47 | // Set value size
48 | segment[4] = (byte) ((valueSize >> 24) & 0xFF);
49 | segment[5] = (byte) ((valueSize >> 16) & 0xFF);
50 | segment[6] = (byte) ((valueSize >> 8) & 0xFF);
51 | segment[7] = (byte) (valueSize & 0xFF);
52 |
53 | System.arraycopy(key, 0, segment, CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH, keySize);
54 |
55 | System.arraycopy(value, 0, segment, CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH + keySize, valueSize);
56 |
57 | byte[] crc = new Segment(segment).crc16();
58 | segment[0] = crc[0];
59 | segment[1] = crc[1];
60 |
61 | return new Segment(segment);
62 | }
63 |
64 | public byte[] getBytes() {
65 | return bytes;
66 | }
67 |
68 | public byte[] getKey() {
69 | int keySize = getKeySize();
70 | return extractBytes(CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH, keySize);
71 | }
72 |
73 | public byte[] getValue() {
74 | int keySize = getKeySize();
75 | int valueSize = getValueSize();
76 | return extractBytes(CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH + keySize, valueSize);
77 | }
78 |
79 | public int getKeySize() {
80 | return ((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff);
81 | }
82 |
83 | public int getValueSize() {
84 | return ((bytes[4] & 0xff) << 24) | ((bytes[5] & 0xff) << 16) |
85 | ((bytes[6] & 0xff) << 8) | (bytes[7] & 0xff);
86 | }
87 |
88 | public byte[] getCrc() {
89 | return extractBytes(0, CRC_LENGTH);
90 | }
91 |
92 | public boolean isChecksumValid() {
93 | byte[] crc = crc16();
94 | return crc[0] == bytes[0] && crc[1] == bytes[1];
95 | }
96 |
97 | public boolean isSegmentValid() {
98 | return isChecksumValid() && getKeySize() > 0 && getValueSize() >= 0
99 | && bytes.length == CRC_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH + getKeySize() + getValueSize();
100 | }
101 |
102 | private byte[] extractBytes(int offset, int length) {
103 | byte[] result = new byte[length];
104 | System.arraycopy(bytes, offset, result, 0, length);
105 | return result;
106 | }
107 |
108 | private byte[] crc16(byte[] segment) {
109 | int crc = 0xFFFF; // Initial CRC value
110 | int polynomial = 0x1021; // CRC-16 polynomial
111 |
112 | for (int index = CRC_LENGTH; index < segment.length; index++) {
113 | byte b = segment[index];
114 | crc ^= (b & 0xFF) << 8;
115 |
116 | for (int i = 0; i < 8; i++) {
117 | if ((crc & 0x8000) != 0) {
118 | crc = (crc << 1) ^ polynomial;
119 | } else {
120 | crc <<= 1;
121 | }
122 | }
123 | }
124 |
125 | return new byte[]{(byte) ((crc >> 8) & 0xFF), (byte) (crc & 0xFF)};
126 | }
127 |
128 | private byte[] crc16() {
129 | return crc16(bytes);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/CompactionTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly;
2 |
3 | import com.sahilbondre.firefly.log.FileChannelRandomAccessLog;
4 | import com.sahilbondre.firefly.log.RandomAccessLog;
5 | import com.sahilbondre.firefly.model.Segment;
6 | import org.junit.jupiter.api.AfterEach;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.io.IOException;
11 | import java.nio.file.Files;
12 | import java.nio.file.Paths;
13 |
14 | import static com.sahilbondre.firefly.TestUtils.deleteFolderContentsIfExists;
15 | import static org.junit.jupiter.api.Assertions.assertEquals;
16 | import static org.junit.jupiter.api.Assertions.assertTrue;
17 |
18 | class CompactionTest {
19 |
20 | private static final String TEST_FOLDER = "src/test/resources/test_folder_compaction";
21 | private static final String TEST_LOG_FILE_1 = "1.log";
22 | private static final String TEST_LOG_FILE_2 = "2.log";
23 | private static final String TEST_LOG_FILE_3 = "3.log";
24 |
25 | private FireflyDB fireflyDB;
26 |
27 | @BeforeEach
28 | void setUp() throws IOException {
29 | deleteFolderContentsIfExists(TEST_FOLDER);
30 | // Create a test folder and log files
31 | Files.createDirectories(Paths.get(TEST_FOLDER));
32 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_1));
33 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_2));
34 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_3));
35 |
36 | RandomAccessLog log1 = new FileChannelRandomAccessLog(TEST_FOLDER + "/" + TEST_LOG_FILE_1);
37 | RandomAccessLog log2 = new FileChannelRandomAccessLog(TEST_FOLDER + "/" + TEST_LOG_FILE_2);
38 | RandomAccessLog log3 = new FileChannelRandomAccessLog(TEST_FOLDER + "/" + TEST_LOG_FILE_3);
39 |
40 | log1.append(Segment.fromKeyValuePair("key1".getBytes(), "value1".getBytes()).getBytes());
41 | log1.append(Segment.fromKeyValuePair("key2".getBytes(), "value2".getBytes()).getBytes());
42 | log1.append(Segment.fromKeyValuePair("key3".getBytes(), "value3".getBytes()).getBytes());
43 |
44 | log2.append(Segment.fromKeyValuePair("key4".getBytes(), "value4".getBytes()).getBytes());
45 | log2.append(Segment.fromKeyValuePair("key1".getBytes(), "value5".getBytes()).getBytes());
46 | log2.append(Segment.fromKeyValuePair("key2".getBytes(), "value6".getBytes()).getBytes());
47 |
48 | log3.append(Segment.fromKeyValuePair("key7".getBytes(), "value7".getBytes()).getBytes());
49 | log3.append(Segment.fromKeyValuePair("key8".getBytes(), "value8".getBytes()).getBytes());
50 | log3.append(Segment.fromKeyValuePair("key1".getBytes(), "value9".getBytes()).getBytes());
51 |
52 | log1.close();
53 | log2.close();
54 | log3.close();
55 |
56 | fireflyDB = FireflyDB.getInstance(TEST_FOLDER);
57 | }
58 |
59 | @AfterEach
60 | void tearDown() throws IOException {
61 | fireflyDB.stop();
62 | deleteFolderContentsIfExists(TEST_FOLDER);
63 | }
64 |
65 | @Test
66 | void givenMultipleLogFiles_whenCompaction_thenAllFilesRenamedCorrectly() throws IOException {
67 | // Given
68 | // A FireflyDB instance with a folder path
69 | fireflyDB.start();
70 |
71 | // When
72 | // Compaction is triggered
73 | fireflyDB.compaction();
74 |
75 | // Then
76 | // All log files are processed correctly
77 | assertTrue(Files.exists(Paths.get(TEST_FOLDER, "_1.log")));
78 | assertTrue(Files.exists(Paths.get(TEST_FOLDER, "_2.log")));
79 | assertTrue(Files.exists(Paths.get(TEST_FOLDER, "_3.log")));
80 | assertEquals("value9", new String(fireflyDB.get("key1".getBytes())));
81 | assertEquals("value6", new String(fireflyDB.get("key2".getBytes())));
82 | assertEquals("value3", new String(fireflyDB.get("key3".getBytes())));
83 | assertEquals("value4", new String(fireflyDB.get("key4".getBytes())));
84 | assertEquals("value7", new String(fireflyDB.get("key7".getBytes())));
85 | assertEquals("value8", new String(fireflyDB.get("key8".getBytes())));
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/FireflyDBStaticTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.lang.reflect.Method;
6 | import java.lang.reflect.Modifier;
7 |
8 | import static org.junit.jupiter.api.Assertions.*;
9 |
10 | class FireflyDBStaticTest {
11 |
12 | private static final String FOLDER_A = "/path/to/folderA";
13 | private static final String FOLDER_B = "/path/to/folderB";
14 |
15 | @Test
16 | void givenSameFolder_whenGetInstance_thenSameObjectReferenced() {
17 | // Given
18 | // Two instances with the same folder should reference the same object
19 |
20 | // When
21 | FireflyDB dbA1 = FireflyDB.getInstance(FOLDER_A);
22 | FireflyDB dbA2 = FireflyDB.getInstance(FOLDER_A);
23 |
24 | // Then
25 | assertSame(dbA1, dbA2);
26 | assertEquals(FOLDER_A, dbA1.getFolderPath());
27 | assertEquals(FOLDER_A, dbA1.getFolderPath());
28 | }
29 |
30 | @Test
31 | void givenDifferentFolders_whenGetInstance_thenDifferentObjectsReferenced() {
32 | // Given
33 | // Two instances with different folders should reference different objects
34 |
35 | // When
36 | FireflyDB dbA = FireflyDB.getInstance(FOLDER_A);
37 | FireflyDB dbB = FireflyDB.getInstance(FOLDER_B);
38 |
39 | // Then
40 | assertNotSame(dbA, dbB);
41 | assertEquals(FOLDER_A, dbA.getFolderPath());
42 | assertEquals(FOLDER_B, dbB.getFolderPath());
43 | }
44 |
45 | @Test
46 | void givenGetInstanceMethod_whenCheckSynchronizedModifier_thenTrue() throws NoSuchMethodException {
47 | // Given
48 | Method getInstanceMethod = FireflyDB.class.getDeclaredMethod("getInstance", String.class);
49 |
50 | // When/Then
51 | assertTrue(Modifier.isSynchronized(getInstanceMethod.getModifiers()));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/FireflyDBTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.io.IOException;
8 | import java.nio.file.Files;
9 | import java.nio.file.Paths;
10 |
11 | import static com.sahilbondre.firefly.TestUtils.deleteFolderContentsIfExists;
12 | import static org.junit.jupiter.api.Assertions.*;
13 |
14 | class FireflyDBTest {
15 |
16 | private static final String TEST_FOLDER = "src/test/resources/test_folder_simple";
17 | private static final String TEST_LOG_FILE_1 = "1.log";
18 | private static final String TEST_LOG_FILE_2 = "2.log";
19 | private static final String TEST_LOG_FILE_3 = "3.log";
20 |
21 | private FireflyDB fireflyDB;
22 |
23 | @BeforeEach
24 | void setUp() throws IOException {
25 | deleteFolderContentsIfExists(TEST_FOLDER);
26 | // Create a test folder and log files
27 | Files.createDirectories(Paths.get(TEST_FOLDER));
28 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_1));
29 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_2));
30 | Files.createFile(Paths.get(TEST_FOLDER, TEST_LOG_FILE_3));
31 |
32 | fireflyDB = FireflyDB.getInstance(TEST_FOLDER);
33 | }
34 |
35 | @AfterEach
36 | void tearDown() throws IOException {
37 | fireflyDB.stop();
38 | deleteFolderContentsIfExists(TEST_FOLDER);
39 | }
40 |
41 | @Test
42 | void givenFolderPath_whenStarted_thenInstanceCreatedAndMarkedAsStarted() throws IOException {
43 | // Given
44 | // A FireflyDB instance with a folder path
45 |
46 | // When
47 | fireflyDB.start();
48 |
49 | // Then
50 | assertNotNull(fireflyDB);
51 | assertEquals(TEST_FOLDER, fireflyDB.getFolderPath());
52 | assertTrue(fireflyDB.isStarted());
53 | }
54 |
55 | @Test
56 | void givenStartedInstance_whenStop_thenLogsClosed() throws IOException {
57 | // Given
58 | // A started FireflyDB instance
59 | fireflyDB.start();
60 | assertTrue(fireflyDB.isStarted());
61 |
62 | // When
63 | fireflyDB.stop();
64 |
65 | // Then
66 | assertFalse(fireflyDB.isStarted());
67 | }
68 |
69 | @Test
70 | void givenStartedInstance_whenSetAndGet_thenValuesAreCorrect() throws IOException {
71 |
72 | // Given
73 | fireflyDB.start();
74 | assertTrue(fireflyDB.isStarted());
75 |
76 | // Set a value
77 | byte[] key = "testKey".getBytes();
78 | byte[] value = "testValue".getBytes();
79 | fireflyDB.set(key, value);
80 |
81 | // Get the value
82 | byte[] retrievedValue = fireflyDB.get(key);
83 | assertArrayEquals(value, retrievedValue);
84 | }
85 |
86 | @Test
87 | void givenUnstartedInstance_whenSet_thenExceptionThrown() {
88 | // Given
89 | byte[] key = "testKey".getBytes();
90 | byte[] value = "testValue".getBytes();
91 |
92 | // When/Then
93 | // Attempt to set a value without starting the instance
94 | assertThrows(IllegalStateException.class, () -> fireflyDB.set(key, value));
95 | }
96 |
97 | @Test
98 | void givenUnstartedInstance_whenGet_thenExceptionThrown() {
99 | // Given
100 | byte[] key = "testKey".getBytes();
101 |
102 | // When/Then
103 | // Attempt to get a value without starting the instance
104 | assertThrows(IllegalStateException.class, () -> fireflyDB.get(key));
105 | }
106 |
107 | @Test
108 | void givenNonexistentKey_whenGet_thenExceptionThrown() throws IOException {
109 | // Given
110 | fireflyDB.start();
111 | assertTrue(fireflyDB.isStarted());
112 | byte[] key = "nonexistentKey".getBytes();
113 |
114 | // When/Then
115 | // Attempt to get a nonexistent key
116 | assertThrows(IllegalArgumentException.class, () -> fireflyDB.get(key));
117 | }
118 |
119 | @Test
120 | void givenStartedInstance_whenSetMultipleTimes_thenValuesAreCorrect() throws IOException {
121 | // Given
122 | fireflyDB.start();
123 | assertTrue(fireflyDB.isStarted());
124 |
125 | // Set a value
126 | byte[] key = "testKey".getBytes();
127 | byte[] value = "testValue".getBytes();
128 | fireflyDB.set(key, value);
129 |
130 | // Set another value
131 | byte[] key2 = "testKey2".getBytes();
132 | byte[] value2 = "testValue2".getBytes();
133 | fireflyDB.set(key2, value2);
134 |
135 | // Get the values
136 | byte[] retrievedValue = fireflyDB.get(key);
137 | byte[] retrievedValue2 = fireflyDB.get(key2);
138 | assertArrayEquals(value, retrievedValue);
139 | assertArrayEquals(value2, retrievedValue2);
140 | }
141 |
142 | @Test
143 | void givenStartedInstance_whenSetSameKeyMultipleTimes_thenValueIsCorrect() throws IOException {
144 | // Given
145 | fireflyDB.start();
146 | assertTrue(fireflyDB.isStarted());
147 |
148 | // When
149 | // Set a value
150 | byte[] key = "testKey".getBytes();
151 | byte[] value = "testValue".getBytes();
152 | fireflyDB.set(key, value);
153 |
154 | // Set another value
155 | byte[] value2 = "testValue2".getBytes();
156 | fireflyDB.set(key, value2);
157 |
158 | // Get the values
159 | byte[] retrievedValue = fireflyDB.get(key);
160 | assertArrayEquals(value2, retrievedValue);
161 | }
162 |
163 | @Test
164 | void givenStartedInstance_whenSetAndRestart_thenValueIsCorrect() throws IOException {
165 | // Given
166 | fireflyDB.start();
167 | assertTrue(fireflyDB.isStarted());
168 | byte[] key = "testKey".getBytes();
169 | byte[] value = "testValue".getBytes();
170 | fireflyDB.set(key, value);
171 | fireflyDB.stop();
172 |
173 | // When
174 | // Restart the instance
175 | fireflyDB = FireflyDB.getInstance(TEST_FOLDER);
176 | fireflyDB.start();
177 |
178 | // Get the values
179 | byte[] retrievedValue = fireflyDB.get(key);
180 | assertArrayEquals(value, retrievedValue);
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/PerformanceTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.io.IOException;
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 | import java.nio.file.Paths;
11 | import java.util.ArrayList;
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.Random;
15 | import java.util.logging.Logger;
16 | import java.util.stream.Stream;
17 |
18 |
19 | class PerformanceTest {
20 |
21 | private static final String TEST_FOLDER = "src/test/resources/test_folder";
22 | private static final int ITERATIONS = 100000;
23 | private static final int KEY_LENGTH = 8;
24 | private static final int VALUE_LENGTH = 100;
25 |
26 | Logger logger = Logger.getLogger(PerformanceTest.class.getName());
27 |
28 | private FireflyDB fireflyDB;
29 |
30 | @BeforeEach
31 | public void setUp() throws IOException {
32 | // Create a test folder
33 | Files.createDirectories(Paths.get(TEST_FOLDER));
34 |
35 | fireflyDB = FireflyDB.getInstance(TEST_FOLDER);
36 | }
37 |
38 | @AfterEach
39 | void tearDown() throws IOException {
40 | fireflyDB.stop();
41 | // Cleanup: Delete the test folder and its contents
42 | try (Stream pathStream = Files.walk(Paths.get(TEST_FOLDER))) {
43 | pathStream
44 | .sorted((path1, path2) -> -path1.compareTo(path2))
45 | .forEach(path -> {
46 | try {
47 | Files.delete(path);
48 | } catch (IOException e) {
49 | e.printStackTrace();
50 | }
51 | });
52 | } catch (IOException e) {
53 | e.printStackTrace();
54 | }
55 | }
56 |
57 | @Test
58 | void testPerformance() throws IOException {
59 | fireflyDB.start();
60 |
61 | // Benchmark writes
62 | logger.info("Starting writes...");
63 |
64 | long[] writeTimes = new long[ITERATIONS];
65 |
66 | List availableKeys = new ArrayList<>();
67 |
68 | long startTime = System.nanoTime();
69 | for (int i = 0; i < ITERATIONS; i++) {
70 | byte[] key = getRandomBytes(KEY_LENGTH);
71 | byte[] value = getRandomBytes(VALUE_LENGTH);
72 |
73 | long writeTime = saveKeyValuePairAndGetTime(key, value);
74 | availableKeys.add(key);
75 | writeTimes[i] = writeTime;
76 | }
77 | long totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds
78 | logger.info("Total time for writes: " + totalTime + " mus");
79 |
80 | double averageWriteTime = 0;
81 | for (long writeTime : writeTimes) {
82 | averageWriteTime += writeTime;
83 | }
84 | averageWriteTime /= ITERATIONS;
85 |
86 | logger.info("Average write latency: " + averageWriteTime + " mus");
87 |
88 | // Calculate p90 write latency
89 | // Sort write times
90 | Arrays.sort(writeTimes);
91 | long p90WriteTime = writeTimes[(int) (ITERATIONS * 0.9)];
92 | logger.info("p90 write latency: " + p90WriteTime + " mus");
93 |
94 | // Benchmark reads
95 | logger.info("\nStarting reads...");
96 |
97 | long[] readTimes = new long[ITERATIONS];
98 |
99 | startTime = System.nanoTime();
100 | for (int i = 0; i < ITERATIONS; i++) {
101 | // Get a random key from the list of available keys
102 | byte[] key = availableKeys.get(new Random().nextInt(availableKeys.size()));
103 |
104 | long readTime = getKeyValuePairAndGetTime(key);
105 | readTimes[i] = readTime;
106 | }
107 | totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds
108 | logger.info("Total time for reads: " + totalTime + " mus");
109 |
110 | double averageReadTime = 0;
111 | for (long readTime : readTimes) {
112 | averageReadTime += readTime;
113 | }
114 | averageReadTime /= ITERATIONS;
115 |
116 | logger.info("Average read latency: " + averageReadTime + " mus");
117 |
118 | // Calculate p90 read latency
119 | // Sort read times
120 | Arrays.sort(readTimes);
121 | long p90ReadTime = readTimes[(int) (ITERATIONS * 0.9)];
122 | logger.info("p90 read latency: " + p90ReadTime + " mus");
123 |
124 |
125 | // Benchmark reads and writes
126 | logger.info("\nStarting reads and writes...");
127 |
128 |
129 | startTime = System.nanoTime();
130 | for (int i = 0; i < ITERATIONS; i++) {
131 | byte[] writeKey = getRandomBytes(KEY_LENGTH);
132 | byte[] writeValue = getRandomBytes(VALUE_LENGTH);
133 |
134 | long writeTime = saveKeyValuePairAndGetTime(writeKey, writeValue);
135 |
136 | availableKeys.add(writeKey);
137 |
138 | byte[] readKey = availableKeys.get(new Random().nextInt(availableKeys.size()));
139 |
140 | long readTime = getKeyValuePairAndGetTime(readKey);
141 |
142 | writeTimes[i] = writeTime;
143 | readTimes[i] = readTime;
144 | }
145 | totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds
146 | logger.info("Total time for reads and writes: " + totalTime + " mus");
147 |
148 | averageReadTime = 0;
149 | for (long readTime : readTimes) {
150 | averageReadTime += readTime;
151 | }
152 | averageReadTime /= ITERATIONS;
153 |
154 | averageWriteTime = 0;
155 | for (long writeTime : writeTimes) {
156 | averageWriteTime += writeTime;
157 | }
158 | averageWriteTime /= ITERATIONS;
159 |
160 | logger.info("Average read latency: " + averageReadTime + " mus");
161 | logger.info("Average write latency: " + averageWriteTime + " mus");
162 |
163 | // Calculate p90 read latency
164 | // Sort read times
165 | Arrays.sort(readTimes);
166 |
167 | // Calculate p90 write latency
168 | // Sort write times
169 | Arrays.sort(writeTimes);
170 |
171 | p90ReadTime = readTimes[(int) (ITERATIONS * 0.9)];
172 | logger.info("p90 read latency: " + p90ReadTime + " mus");
173 |
174 | p90WriteTime = writeTimes[(int) (ITERATIONS * 0.9)];
175 | logger.info("p90 write latency: " + p90WriteTime + " mus");
176 | }
177 |
178 | private byte[] getRandomBytes(int length) {
179 | byte[] bytes = new byte[length];
180 | new Random().nextBytes(bytes);
181 | return bytes;
182 | }
183 |
184 | private long saveKeyValuePairAndGetTime(byte[] key, byte[] value) throws IOException {
185 | long startTime = System.nanoTime();
186 | fireflyDB.set(key, value);
187 | return (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds
188 | }
189 |
190 | private long getKeyValuePairAndGetTime(byte[] key) throws IOException {
191 | long startTime = System.nanoTime();
192 | fireflyDB.get(key);
193 | return (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/TestUtils.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly;
2 |
3 | import java.io.IOException;
4 | import java.nio.file.Files;
5 | import java.nio.file.Path;
6 | import java.nio.file.Paths;
7 | import java.util.stream.Stream;
8 |
9 | public class TestUtils {
10 | public static void deleteFolderContentsIfExists(String folderPath) throws IOException {
11 | // check if folder exists
12 | Path path = Paths.get(folderPath);
13 | if (Files.exists(path)) {
14 | // Delete the all the files in the test folder
15 | Stream
16 | files = Files.walk(path);
17 | files
18 | .forEach(p -> {
19 | try {
20 | Files.delete(p);
21 | } catch (IOException ignored) {
22 | }
23 | });
24 |
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/filetable/SerializedPersistableFileTableTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.filetable;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.io.FileNotFoundException;
8 | import java.io.IOException;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.nio.file.Paths;
12 | import java.util.List;
13 |
14 | import static org.junit.jupiter.api.Assertions.*;
15 |
16 | class SerializedPersistableFileTableTest {
17 |
18 | private static final String TEST_FILE_PATH = "src/test/resources/map";
19 | private SerializedPersistableFileTable fileTable;
20 |
21 | @BeforeEach
22 | void setUp() throws IOException {
23 | Files.deleteIfExists(Paths.get(TEST_FILE_PATH));
24 | fileTable = SerializedPersistableFileTable.fromEmpty();
25 | }
26 |
27 | @AfterEach
28 | void tearDown() throws IOException {
29 | Files.deleteIfExists(Paths.get(TEST_FILE_PATH));
30 | }
31 |
32 | @Test
33 | void given_KeyValue_When_PuttingAndGet_Then_RetrievedValueMatches() {
34 | // Given
35 | byte[] key = "testKey".getBytes();
36 | FilePointer expectedValue = new FilePointer("test.txt", 42);
37 |
38 | // When
39 | fileTable.put(key, new FilePointer("test.txt", 42));
40 | FilePointer retrievedValue = fileTable.get(key);
41 |
42 | // Then
43 | assertEquals(expectedValue, retrievedValue);
44 | }
45 |
46 | @Test
47 | void given_NullKey_When_PuttingAndGet_Then_RetrievedValueIsNull() {
48 | // Given
49 | FilePointer value = new FilePointer("test.txt", 42);
50 |
51 | // When
52 | fileTable.put(null, value);
53 | FilePointer retrievedValue = fileTable.get(null);
54 |
55 | // Then
56 | assertNull(retrievedValue);
57 | }
58 |
59 | @Test
60 | void given_NullValue_When_PuttingAndGet_Then_RetrievedValueIsNull() {
61 | // Given
62 | byte[] key = "testKey".getBytes();
63 |
64 | // When
65 | fileTable.put(key, null);
66 | FilePointer retrievedValue = fileTable.get(key);
67 |
68 | // Then
69 | assertNull(retrievedValue);
70 | }
71 |
72 | @Test
73 | void given_KeyValue_When_SavingToDiskAndLoadingFromFile_Then_RetrievedValueMatches() throws FileNotFoundException {
74 | // Given
75 | byte[] key = "testKey".getBytes();
76 | FilePointer value = new FilePointer("test.txt", 42);
77 |
78 | // When
79 | fileTable.put(key, value);
80 | fileTable.saveToDisk(TEST_FILE_PATH);
81 | SerializedPersistableFileTable loadedFileTable = SerializedPersistableFileTable.fromFile(TEST_FILE_PATH);
82 | FilePointer retrievedValue = loadedFileTable.get(key);
83 |
84 | // Then
85 | assertEquals(value, retrievedValue);
86 | }
87 |
88 | @Test
89 | void given_NonexistentFile_When_LoadingFromFile_Then_FileNotFoundExceptionIsThrown() {
90 | // When
91 | // Then
92 | assertThrows(FileNotFoundException.class,
93 | () -> SerializedPersistableFileTable.fromFile(TEST_FILE_PATH));
94 | }
95 |
96 | @Test
97 | void given_CorruptedFile_When_LoadingFromFile_Then_InvalidFileTableExceptionIsThrown() throws IOException {
98 | // Given
99 | // Create a corrupted file by writing invalid data
100 | Path filePath = Paths.get(TEST_FILE_PATH);
101 | Files.write(filePath, List.of("Invalid Data"));
102 |
103 | // Then
104 | assertThrows(InvalidFileTableException.class,
105 | () -> SerializedPersistableFileTable.fromFile(TEST_FILE_PATH));
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/log/FileChannelRandomAccessLogTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.log;
2 |
3 | import com.sahilbondre.firefly.filetable.FilePointer;
4 | import com.sahilbondre.firefly.model.Segment;
5 | import org.junit.jupiter.api.AfterEach;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.io.IOException;
10 | import java.nio.channels.ClosedChannelException;
11 | import java.nio.file.Files;
12 | import java.nio.file.Path;
13 | import java.nio.file.Paths;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class FileChannelRandomAccessLogTest {
18 |
19 | private static final String TEST_FILE_NAME = "src/test/resources/test.log";
20 | private static final Path TEST_FILE_PATH = Paths.get(TEST_FILE_NAME);
21 | private FileChannelRandomAccessLog randomAccessLog;
22 |
23 | @BeforeEach
24 | void setUp() throws IOException {
25 | Files.deleteIfExists(TEST_FILE_PATH);
26 | Files.createFile(TEST_FILE_PATH);
27 | randomAccessLog = new FileChannelRandomAccessLog(TEST_FILE_NAME);
28 | }
29 |
30 | @AfterEach
31 | void tearDown() throws IOException {
32 | try {
33 | randomAccessLog.close();
34 | } catch (ClosedChannelException e) {
35 | // Ignore
36 | }
37 | Files.deleteIfExists(TEST_FILE_PATH);
38 | }
39 |
40 | @Test
41 | void givenEmptyLog_whenGetSize_thenReturnsZero() throws IOException {
42 | // Given
43 | // An empty log
44 |
45 | // When
46 | long size = randomAccessLog.size();
47 |
48 | // Then
49 | assertEquals(0, size);
50 | }
51 |
52 |
53 | @Test
54 | void givenLogWithContent_whenGetSize_thenReturnsCorrectSize() throws IOException {
55 | // Given
56 | // A log with content
57 |
58 | // When
59 | randomAccessLog.append("Hello".getBytes());
60 | randomAccessLog.append("World".getBytes());
61 |
62 | // Then
63 | assertEquals(10, randomAccessLog.size());
64 | }
65 |
66 | @Test
67 | void givenLog_whenGetFilePath_thenReturnsCorrectPath() {
68 | // Given
69 | // A log instance
70 |
71 | // When
72 | String filePath = randomAccessLog.getFilePath();
73 |
74 | // Then
75 | assertEquals(TEST_FILE_NAME, filePath);
76 | }
77 |
78 | @Test
79 | void givenLogWithContent_whenAppend_thenAppendsCorrectly() throws IOException {
80 | // Given
81 | // A log with existing content
82 |
83 | // When
84 | randomAccessLog.append("Hello".getBytes());
85 | randomAccessLog.append("World".getBytes());
86 | byte[] result = randomAccessLog.read(0, randomAccessLog.size());
87 |
88 | // Then
89 | assertArrayEquals("HelloWorld".getBytes(), result);
90 | }
91 |
92 | @Test
93 | void givenLogWithContent_whenReadSubset_thenReturnsSubset() throws IOException, InvalidRangeException {
94 | // Given
95 | // A log with existing content
96 |
97 | // When
98 | randomAccessLog.append("The quick brown fox".getBytes());
99 | byte[] result = randomAccessLog.read(4, 5);
100 |
101 | // Then
102 | assertArrayEquals("quick".getBytes(), result);
103 | }
104 |
105 | @Test
106 | void givenInvalidRange_whenRead_thenThrowsInvalidRangeException() throws IOException {
107 | // Given
108 | randomAccessLog.append("Hello".getBytes());
109 | // An invalid range for reading
110 |
111 | // When/Then
112 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.read(0, -1));
113 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.read(-1, 5));
114 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.read(15, 10));
115 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.read(2, 10));
116 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.read(0, 6));
117 | }
118 |
119 | @Test
120 | void givenLog_whenClose_thenFileIsNotAccessible() throws IOException {
121 | // Given
122 | // An open log
123 |
124 | // When
125 | randomAccessLog.close();
126 |
127 | // Then
128 | assertTrue(Files.exists(TEST_FILE_PATH));
129 | assertThrows(IOException.class, () -> randomAccessLog.append("NewContent".getBytes()));
130 | }
131 |
132 | @Test
133 | void givenLogWithContent_whenReadSegment_thenReturnsCorrectSegment() throws IOException, InvalidRangeException {
134 | // Given
135 | // A log with existing content
136 | Segment firstSegment = Segment.fromKeyValuePair("Hello".getBytes(), "World".getBytes());
137 | Segment secondSegment = Segment.fromKeyValuePair("Foo".getBytes(), "Bar".getBytes());
138 | FilePointer firstFilePointer = randomAccessLog.append(firstSegment.getBytes());
139 | FilePointer secondFilePointer = randomAccessLog.append(secondSegment.getBytes());
140 |
141 | // When
142 | Segment firstReadSegment = randomAccessLog.readSegment(firstFilePointer.getOffset());
143 | Segment secondReadSegment = randomAccessLog.readSegment(secondFilePointer.getOffset());
144 |
145 | // Then
146 | assertArrayEquals(firstSegment.getBytes(), firstReadSegment.getBytes());
147 | assertArrayEquals(secondSegment.getBytes(), secondReadSegment.getBytes());
148 | assertEquals("Hello", new String(firstReadSegment.getKey()));
149 | assertEquals("World", new String(firstReadSegment.getValue()));
150 | assertEquals("Foo", new String(secondReadSegment.getKey()));
151 | assertEquals("Bar", new String(secondReadSegment.getValue()));
152 | }
153 |
154 | @Test
155 | void givenLogWithContent_whenReadSegmentWithInvalidOffset_thenThrowsInvalidRangeException() throws IOException {
156 | // Given
157 | // A log with existing content
158 | Segment firstSegment = Segment.fromKeyValuePair("Hello".getBytes(), "World".getBytes());
159 | Segment secondSegment = Segment.fromKeyValuePair("Foo".getBytes(), "Bar".getBytes());
160 | randomAccessLog.append(firstSegment.getBytes());
161 | randomAccessLog.append(secondSegment.getBytes());
162 |
163 | // When/Then
164 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.readSegment(-1));
165 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.readSegment(100));
166 | }
167 |
168 | @Test
169 | void givenEmptyLog_whenReadSegment_thenThrowsInvalidRangeException() {
170 | // Given
171 | // An empty log
172 |
173 | // When/Then
174 | assertThrows(InvalidRangeException.class, () -> randomAccessLog.readSegment(0));
175 | }
176 |
177 | @Test
178 | void givenLogWithContent_whenAppend_thenReturnsCorrectFilePointer() throws IOException {
179 | // Given
180 | // A log with existing content
181 |
182 | // When
183 | FilePointer fp1 = randomAccessLog.append("Hello".getBytes());
184 | FilePointer fp2 = randomAccessLog.append("World".getBytes());
185 |
186 | // Then
187 | assertEquals(TEST_FILE_NAME, fp1.getFileName());
188 | assertEquals(0, fp1.getOffset());
189 | assertEquals(TEST_FILE_NAME, fp2.getFileName());
190 | assertEquals(5, fp2.getOffset());
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/test/java/com/sahilbondre/firefly/model/SegmentTest.java:
--------------------------------------------------------------------------------
1 | package com.sahilbondre.firefly.model;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.junit.jupiter.api.Assertions.*;
6 |
7 | class SegmentTest {
8 |
9 | @Test
10 | void givenByteArray_whenCreatingSegment_thenAccessorsReturnCorrectValues() {
11 | // Given
12 | byte[] testData = new byte[]{
13 | (byte) -83, (byte) 64,
14 | 0x00, 0x05, // Key Size
15 | 0x00, 0x00, 0x00, 0x05, // Value Size
16 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
17 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
18 | };
19 |
20 | // When
21 | Segment segment = Segment.fromByteArray(testData);
22 |
23 | // Then
24 | assertArrayEquals(testData, segment.getBytes());
25 | assertArrayEquals("Hello".getBytes(), segment.getKey());
26 | assertArrayEquals("World".getBytes(), segment.getValue());
27 | assertEquals(5, segment.getKeySize());
28 | assertEquals(5, segment.getValueSize());
29 | assertEquals(-83, segment.getCrc()[0]);
30 | assertEquals(64, segment.getCrc()[1]);
31 | assertTrue(segment.isSegmentValid());
32 | assertTrue(segment.isChecksumValid());
33 | }
34 |
35 | @Test
36 | void givenCorruptedKeySizeSegment_whenCheckingChecksum_thenIsChecksumValidReturnsFalse() {
37 | // Given
38 | byte[] testData = new byte[]{
39 | (byte) -83, (byte) 64,
40 | 0x01, 0x45, // Key Size (Bit Flipped)
41 | 0x00, 0x00, 0x00, 0x05, // Value Size
42 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
43 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
44 | };
45 |
46 | // When
47 | Segment corruptedSegment = Segment.fromByteArray(testData);
48 |
49 | // Then
50 | assertFalse(corruptedSegment.isChecksumValid());
51 | assertFalse(corruptedSegment.isSegmentValid());
52 | }
53 |
54 | @Test
55 | void givenCorruptedValueSizeSegment_whenCheckingChecksum_thenIsChecksumValidReturnsFalse() {
56 | // Given
57 | byte[] testData = new byte[]{
58 | (byte) -83, (byte) 64,
59 | 0x00, 0x05, // Key Size
60 | 0x00, 0x00, 0x01, 0x05, // Value Size (Bit Flipped)
61 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
62 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
63 | };
64 |
65 | // When
66 | Segment corruptedSegment = Segment.fromByteArray(testData);
67 |
68 | // Then
69 | assertFalse(corruptedSegment.isChecksumValid());
70 | assertFalse(corruptedSegment.isSegmentValid());
71 | }
72 |
73 | @Test
74 | void givenCorruptedKeySegment_whenCheckingChecksum_thenIsChecksumValidReturnsFalse() {
75 | // Given
76 | byte[] testData = new byte[]{
77 | (byte) -83, (byte) 64,
78 | 0x00, 0x05, // Key Size
79 | 0x00, 0x00, 0x00, 0x05, // Value Size
80 | 0x48, 0x65, 0x6C, 0x6C, 0x6E, // Key: "Hello" (Bit Flipped)
81 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
82 | };
83 |
84 | // When
85 | Segment corruptedSegment = Segment.fromByteArray(testData);
86 |
87 | // Then
88 | assertFalse(corruptedSegment.isChecksumValid());
89 | assertFalse(corruptedSegment.isSegmentValid());
90 | }
91 |
92 | @Test
93 | void givenCorruptedValueSegment_whenCheckingChecksum_thenIsChecksumValidReturnsFalse() {
94 | // Given
95 | byte[] testData = new byte[]{
96 | (byte) -83, (byte) 64,
97 | 0x00, 0x05, // Key Size
98 | 0x00, 0x00, 0x00, 0x05, // Value Size
99 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
100 | 0x57, 0x6F, 0x62, 0x6C, 0x65 // Value: "World" (Bit Flipped)
101 | };
102 |
103 | // When
104 | Segment corruptedSegment = Segment.fromByteArray(testData);
105 |
106 | // Then
107 | assertFalse(corruptedSegment.isChecksumValid());
108 | assertFalse(corruptedSegment.isSegmentValid());
109 | }
110 |
111 | @Test
112 | void givenIncorrectValueLengthSegment_whenCheckingSegmentValid_thenIsSegmentValidReturnsFalse() {
113 | // Given
114 | byte[] testData = new byte[]{
115 | (byte) -43, (byte) -70,
116 | 0x00, 0x05, // Key Size
117 | 0x00, 0x00, 0x00, 0x06, // Value Size (Incorrect)
118 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
119 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
120 | };
121 |
122 | // When
123 | Segment corruptedSegment = Segment.fromByteArray(testData);
124 |
125 | // Then
126 | assertTrue(corruptedSegment.isChecksumValid());
127 | assertFalse(corruptedSegment.isSegmentValid());
128 | }
129 |
130 | @Test
131 | void givenKeyValuePair_whenCreatingSegment_thenAccessorsReturnCorrectValues() {
132 | // Given
133 | byte[] key = "Hello".getBytes();
134 | byte[] value = "World".getBytes();
135 | byte[] expectedSegment = new byte[]{
136 | (byte) -83, (byte) 64,
137 | 0x00, 0x05, // Key Size
138 | 0x00, 0x00, 0x00, 0x05, // Value Size
139 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, // Key: "Hello"
140 | 0x57, 0x6F, 0x72, 0x6C, 0x64 // Value: "World"
141 | };
142 |
143 | // When
144 | Segment segment = Segment.fromKeyValuePair(key, value);
145 |
146 | // Then
147 | assertArrayEquals("Hello".getBytes(), segment.getKey());
148 | assertArrayEquals("World".getBytes(), segment.getValue());
149 | assertEquals(5, segment.getKeySize());
150 | assertEquals(5, segment.getValueSize());
151 | assertEquals(-83, segment.getCrc()[0]);
152 | assertEquals(64, segment.getCrc()[1]);
153 | assertTrue(segment.isSegmentValid());
154 | assertTrue(segment.isChecksumValid());
155 | assertArrayEquals(expectedSegment, segment.getBytes());
156 | }
157 |
158 | @Test
159 | void givenKeyAndValue_whenCreatingSegment_thenSegmentIsCreatedWithCorrectSizes() {
160 | // Given
161 | byte[] key = "Hello".getBytes();
162 | byte[] value = "World".getBytes();
163 |
164 | // When
165 | Segment segment = Segment.fromKeyValuePair(key, value);
166 |
167 | // Then
168 | assertArrayEquals(key, segment.getKey());
169 | assertArrayEquals(value, segment.getValue());
170 | assertEquals(key.length, segment.getKeySize());
171 | assertEquals(value.length, segment.getValueSize());
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/test/resources/.empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godcrampy/fireflydb/a3ddffc3d5b2722b870af6b1bf21c35bf50cfa89/src/test/resources/.empty
--------------------------------------------------------------------------------