diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5b6d039..970da06 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -90,7 +90,7 @@ To maintain engineering transparency, the architectural reality of the repositor | :--- | :--- | :--- | | **Gradle Modules** | Single monolithic `:app` module containing all layers | Multi-module separation (`:core`, `:flashers`, `:app`) | | **Package Namespace** | `com.ashishsinghbora.flashcore.*` across all source packages | Multi-module package organization | -| **Testing Scope** | 109 automated software tests on `BlockDevice` doubles | Automated CI + physical USB controller test matrix | +| **Testing Scope** | 225 automated software tests on `BlockDevice` doubles | Automated CI + physical USB controller test matrix | | **USB Memory Pipeline** | Direct-buffer circular ring buffer with heap staging copy | Direct ring buffer (true zero-copy is not possible via public Android APIs; requires staging copy) | | **Hardware Status** | Software tested; hardware validation pending | Physical qualification across OEM and controller matrix | diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 242e443..db33230 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -33,12 +33,13 @@ FlashCore is an open-source Android utility designed to communicate directly wit All automated test verification in FlashCore is conducted strictly in **software-only environments** using pure JVM unit tests, Robolectric Android runtime simulations, and in-memory or file-backed storage abstractions. -### Automated Test Inventory (110 Tests Total) -* **109 Unit & Robolectric Tests (`app/src/test`):** +### Automated Test Inventory (226 Tests Total) +* **225 Unit & Robolectric Tests (`app/src/test`):** + - `MemoryBlockDeviceTest.kt` (38 tests): Validates in-memory block device geometry, overflow-safe bounds, multi-sector atomic allocation preflight, strict direct-buffer validation, defensive copying, concurrency under `CyclicBarrier` contention, and property round-trip fidelity. - `BlockDeviceFrameworkTest.kt` (28 tests): Validates sector reads, writes, GPT headers, FAT32 boot records, 100 MB throughput simulation, 4 GB sparse boundaries, sector failure injection, disconnect simulation, short write simulation, timeout simulation, configurable sector sizes (512/4096B), multi-sector transfers with buffer offsets, auto-capacity detection, strict LBA/overflow bounds checking, direct buffer validation, sparse zero-fill reads, closed device lifecycle, independent MBR/GPT structural and CRC32 verification, concurrent operations and close races, and constructor descriptor leak prevention. - `AndroidProductionEngineeringTest.kt` (10 tests): Validates foreground service lifecycle, cancellation action dispatch, `SavedStateHandle` restoration across process recreation, dynamic USB detachment broadcast handling, SAF 64-bit integer arithmetic, and synthetic 50 MB benchmark scaling. - `Fat32WriterTest.kt` (9 tests): Validates volume formatting, VBR/FSInfo boot sectors, directory creation (`mkdir`), multi-cluster file writes, cluster appending, directory expansion, Long File Names (LFN), and FSInfo free cluster tracking. - - `PartitionEngineTest.kt` (9 tests): Validates MBR construction, Protective MBR generation, GPT table generation, dynamic CRC32 computation, round-trip GPT parsing, tamper detection, mixed-endian GUID conversions, and 1 MiB alignment arithmetic. + - `PartitionEngineTest.kt` (10 tests): Validates MBR construction, Protective MBR generation, GPT table generation, dynamic CRC32 computation, round-trip GPT parsing, tamper detection, mixed-endian GUID conversions, 1 MiB alignment arithmetic, and MBR device write with round-trip parsing on `MemoryBlockDevice`. - `LinuxFlashingPipelineTest.kt` (8 tests): Validates end-to-end raw streaming, target capacity verification, write-protect detection, partition wipe warnings, destructive write confirmation, source checksum pre-flight validation, read-back sector corruption detection, and cooperative cancellation. - `WindowsUefiPipelineTest.kt` (8 tests): Validates x64/ARM64/dual-arch Windows ISO capability detection, FAT32 cluster slack and capacity analysis, `WimChunker` SWM header creation, bootloader provisioning (`bootx64.efi`), PE `MZ` and registry `regf` header inspection, and end-to-end pipeline execution on in-memory storage. - `VentoyPipelineTest.kt` (9 tests): Validates MBR and GPT Ventoy geometry calculations (Partition 1 data + Partition 2 32 MiB VTOYEFI), default asset provider generation, existing media detection, ISO listing in `/ISO/`, fresh install, and non-destructive update preserving user ISO files. diff --git a/README.md b/README.md index 434c2d8..45e8286 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Platform](https://img.shields.io/badge/Platform-Android_8.0+-green.svg)](https://developer.android.com) [![JDK](https://img.shields.io/badge/JDK-21-red.svg)](https://adoptium.net) [![Kotlin](https://img.shields.io/badge/Kotlin-2.2+-purple.svg)](https://kotlinlang.org) -[![Tests](https://img.shields.io/badge/Tests-110%20Automated%20Test%20Methods-blue.svg)]() +[![Tests](https://img.shields.io/badge/Tests-226%20Automated%20Test%20Methods-blue.svg)]() [![Hardware Validation](https://img.shields.io/badge/Hardware%20Validation-Pending-yellow.svg)](LIMITATIONS.md) [![Documentation](https://img.shields.io/badge/Docs-Architecture%20%7C%20Limitations-orange.svg)](ARCHITECTURE.md) @@ -27,7 +27,7 @@ Our guiding principle is **engineering truthfulness**: ### Engineering Priorities & Baseline Reality 1. 🥇 **Correctness:** Bit-for-bit exactness in sector writing and verification logic. 2. 🥈 **Safety:** Hardened disconnect handling (`ACTION_USB_DEVICE_DETACHED`) and target drive safety checks. -3. 🥉 **Testability:** Core logic is decoupled from Android hardware APIs and covered by 187 automated tests (186 unit/Robolectric in JVM + 1 Android instrumentation test) on abstract `BlockDevice` doubles. +3. 🥉 **Testability:** Core logic is decoupled from Android hardware APIs and covered by 226 automated tests (225 unit/Robolectric in JVM + 1 Android instrumentation test) on abstract `BlockDevice` doubles. 4. **USB Reliability:** SCSI BOT stall recovery routines, clear-halt, and reset recovery (physical controller compatibility matrix pending). 5. **Block-Device Abstraction:** Zero coupling between UI/engines and Android hardware APIs. 6. **Partition Correctness:** Strict GPT/MBR alignment, CRC32 checks, and protective structures. @@ -99,7 +99,7 @@ For complete technical specifications, review [`ARCHITECTURE.md`](ARCHITECTURE.m | **Block Device Test Framework** | Implemented | 37 unit tests in `BlockDeviceFrameworkTest` | N/A (Software Test Double) | 🟢 **Implemented — software tested** | In-memory sparse, file-backed, and fault-injecting simulation; does not emulate physical controller hangs, power drops, or bus resets | [`BlockDevice.kt`](app/src/main/java/com/example/block/BlockDevice.kt), [`BlockDeviceFrameworkTest.kt`](app/src/test/java/com/example/BlockDeviceFrameworkTest.kt) | | **FAT32 Filesystem Writer** | Implemented | 9 unit tests in `Fat32WriterTest` | Not validated | 🟢 **Implemented — software tested** | Custom minimal FAT32 engine; lacks fsck/repair; cluster allocation not validated against physical OS mount drivers | [`Fat32Writer.kt`](app/src/main/java/com/example/fat32/Fat32Writer.kt), [`Fat32WriterTest.kt`](app/src/test/java/com/example/Fat32WriterTest.kt) | | **ISO Filesystem Engine** | Implemented | 6 unit tests in `IsoEngineTest` & `IsoFilesystemReaderTest` | N/A (Software Parser) | 🟢 **Implemented — software tested** | Supports ISO 9660 Level 1/2/3 and Joliet; no Rock Ridge POSIX permissions or pure UDF 2.60 support | [`IsoFilesystemReader.kt`](app/src/main/java/com/example/iso/IsoFilesystemReader.kt), [`IsoEngineTest.kt`](app/src/test/java/com/example/IsoEngineTest.kt) | -| **Partition Subsystem** | Implemented | 9 unit tests in `PartitionEngineTest` | Not validated | 🟢 **Implemented — software tested** | MBR and GPT layout generation verified in memory; partition table detection not validated on physical drives | [`PartitionEngine.kt`](app/src/main/java/com/example/partition/PartitionEngine.kt), [`PartitionEngineTest.kt`](app/src/test/java/com/example/PartitionEngineTest.kt) | +| **Partition Subsystem** | Implemented | 10 unit tests in `PartitionEngineTest` | Not validated | 🟢 **Implemented — software tested** | MBR and GPT layout generation verified in memory; partition table detection not validated on physical drives | [`PartitionEngine.kt`](app/src/main/java/com/example/partition/PartitionEngine.kt), [`PartitionEngineTest.kt`](app/src/test/java/com/example/PartitionEngineTest.kt) | | **Android Production Engineering** | Implemented | 10 Robolectric tests in `AndroidProductionEngineeringTest` | Not validated | 🟡 **Implemented — hardware validation pending** | Foreground service and wake lock tested via Robolectric; synthetic benchmark/scalability harness; physical flash-drive performance and thermal telemetry not validated | [`FlashForegroundService.kt`](app/src/main/java/com/example/service/FlashForegroundService.kt), [`AndroidProductionEngineeringTest.kt`](app/src/test/java/com/example/AndroidProductionEngineeringTest.kt) | | **SPSC Direct Ring Buffer** | Implemented | 2 unit tests in `FlashCoreUnitTest` | Not validated | 🟢 **Implemented — software tested** | Off-heap direct buffers reduce GC churn, but USB transfer path still includes a heap staging copy (not zero-copy); uses ReentrantLock | [`DirectRingBuffer.kt`](app/src/main/java/com/example/dsa/DirectRingBuffer.kt), [`FlashCoreUnitTest.kt`](app/src/test/java/com/example/FlashCoreUnitTest.kt) | | **CI & Release Infrastructure** | Workflows configured | Configured in `.github/workflows` | Not validated | 🟡 **Configured — no published releases** | GitHub Actions workflows configured for lint, test, and signing; no official release tags or published APKs exist yet | [`.github/workflows/ci.yml`](.github/workflows/ci.yml), [`.github/workflows/release.yml`](.github/workflows/release.yml) | @@ -161,7 +161,7 @@ When building in an environment configured with JDK 21 and Android SDK: # 1. Run Android Lint ./gradlew lint -# 2. Run automated test suite (186 JVM/Robolectric unit tests) +# 2. Run automated test suite (225 JVM/Robolectric unit tests) ./gradlew test # 3. Assemble Debug APK @@ -172,8 +172,8 @@ When building in an environment configured with JDK 21 and Android SDK: ``` ### Automated Test Suite Details -The repository contains **187 automated test methods** across 18 test files: -- **186 Unit & Robolectric tests** in `app/src/test` (across 17 test files): Covering block device doubles, USB bulk transfer length validation, SCSI BOT driver protocol, SCSI CDB construction, SCSI CHECK CONDITION and REQUEST SENSE sense data parsing, FAT32 formatting/allocation, ISO 9660 parsing, GPT/MBR partition engines, Linux/Windows/Ventoy strategies, and foreground service lifecycle. +The repository contains **226 automated test methods** across 19 test files: +- **225 Unit & Robolectric tests** in `app/src/test` (across 18 test files): Covering block device doubles, in-memory block devices (`MemoryBlockDevice`), USB bulk transfer length validation, SCSI BOT driver protocol, SCSI CDB construction, SCSI CHECK CONDITION and REQUEST SENSE sense data parsing, FAT32 formatting/allocation, ISO 9660 parsing, GPT/MBR partition engines, Linux/Windows/Ventoy strategies, and foreground service lifecycle. - **1 Instrumentation test** in `app/src/androidTest`: Context verification (`ExampleInstrumentedTest.kt`). - **Physical Hardware Tests:** 0. (All tests run against mock/in-memory abstractions; physical USB hardware and PC boot testing are not automated in CI). diff --git a/app/src/main/java/com/ashishsinghbora/flashcore/block/MemoryBlockDevice.kt b/app/src/main/java/com/ashishsinghbora/flashcore/block/MemoryBlockDevice.kt index 44cb67c..f36a862 100644 --- a/app/src/main/java/com/ashishsinghbora/flashcore/block/MemoryBlockDevice.kt +++ b/app/src/main/java/com/ashishsinghbora/flashcore/block/MemoryBlockDevice.kt @@ -2,34 +2,74 @@ package com.ashishsinghbora.flashcore.block import java.io.IOException import java.nio.ByteBuffer -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive /** - * In-Memory Sparse Block Device for testing, benchmarking, and simulation. + * In-Memory Sparse Block Device for automated testing, benchmarking, and simulation. * - * Emulates physical sector-addressable storage without requiring physical USB OTG hardware. - * Unwritten sectors return zeroes. Sectors are allocated dynamically on write. + * Implements [BlockDevice] using dynamic heap-allocated sector buffers. + * Unwritten sectors return deterministic zeroes. Sectors are allocated dynamically on write. + * + * Concurrency Guarantees & Thread Safety: + * - Thread-safe for concurrent read and write operations protected by an internal monitor lock. + * - Allocation accounting (`maxAllocatedSectors`) is strictly atomic across concurrent writers. + * - Multi-sector writes are preflight-validated for allocation capacity before modifying storage, + * preventing partial state mutation on allocation-limit failure. + * - Multi-sector reads and writes are atomic against concurrent lifecycle events (`close`, `clear`). + * - In-memory flush records invocations without durable persistence across process termination. + * + * Testing Purpose: + * Provides a fast, deterministic block storage medium for unit and integration testing + * (MBR, GPT, FAT32, flashing pipelines) without physical hardware or disk file dependencies. */ class MemoryBlockDevice( - val totalSectors: Long = 2097152L, // Default 1 GB (2097152 * 512 bytes) - override val sectorSizeBytes: Int = 512, - @Volatile override var isConnected: Boolean = true + val totalSectors: Long = DEFAULT_TOTAL_SECTORS, + override val sectorSizeBytes: Int = DEFAULT_SECTOR_SIZE, + @Volatile override var isConnected: Boolean = true, + val maxAllocatedSectors: Int = DEFAULT_MAX_ALLOCATED_SECTORS ) : BlockDevice { - private val sectors = ConcurrentHashMap() + companion object { + const val DEFAULT_TOTAL_SECTORS: Long = 2097152L // Default 1 GB (2097152 * 512 bytes) + const val DEFAULT_SECTOR_SIZE: Int = 512 + const val DEFAULT_MAX_ALLOCATED_SECTORS: Int = 1048576 // Up to 512 MB of 512-byte sectors + } + + init { + require(sectorSizeBytes > 0) { + "Sector size must be strictly positive: $sectorSizeBytes" + } + require(totalSectors >= 0L) { + "Total sectors cannot be negative: $totalSectors" + } + require(maxAllocatedSectors > 0) { + "maxAllocatedSectors must be strictly positive: $maxAllocatedSectors" + } + if (totalSectors > 0L && totalSectors > Long.MAX_VALUE / sectorSizeBytes.toLong()) { + throw IllegalArgumentException( + "Total capacity overflows 64-bit addressable range: $totalSectors sectors * $sectorSizeBytes bytes/sector" + ) + } + } + + private val sectors = HashMap() + private val lock = Any() + + private val _writeCount = AtomicLong(0L) + val writeCount: Long get() = _writeCount.get() + + private val _readCount = AtomicLong(0L) + val readCount: Long get() = _readCount.get() - var writeCount: Long = 0L - private set - var readCount: Long = 0L - private set - var flushCount: Long = 0L - private set + private val _flushCount = AtomicLong(0L) + val flushCount: Long get() = _flushCount.get() /** * Simulated fault injection: if set to true, read/write/flush operations fail with IOException. */ + @Volatile var simulateIoFailure: Boolean = false override suspend fun capacity(): DeviceCapacity { @@ -42,23 +82,23 @@ class MemoryBlockDevice( checkConnected() if (simulateIoFailure) throw IOException("Simulated I/O failure during read") validateBounds(lba, blockCount) + val totalBytes = calculateTotalBytes(blockCount) + validateBufferBounds(dest.size, offset, totalBytes) - val totalBytes = blockCount * sectorSizeBytes - if (offset < 0 || offset + totalBytes > dest.size) { - throw IndexOutOfBoundsException("Buffer offset $offset + $totalBytes exceeds buffer size ${dest.size}") - } - - for (i in 0 until blockCount) { - val sectorLba = lba + i - val destOffset = offset + i * sectorSizeBytes - val sectorData = sectors[sectorLba] - if (sectorData != null) { - System.arraycopy(sectorData, 0, dest, destOffset, sectorSizeBytes) - } else { - dest.fill(0, destOffset, destOffset + sectorSizeBytes) + synchronized(lock) { + checkConnected() + for (i in 0 until blockCount) { + val sectorLba = lba + i.toLong() + val destOffset = offset + i * sectorSizeBytes + val sectorData = sectors[sectorLba] + if (sectorData != null) { + System.arraycopy(sectorData, 0, dest, destOffset, sectorSizeBytes) + } else { + dest.fill(0, destOffset, destOffset + sectorSizeBytes) + } } } - readCount += blockCount + _readCount.addAndGet(blockCount.toLong()) return true } @@ -67,21 +107,10 @@ class MemoryBlockDevice( checkConnected() if (simulateIoFailure) throw IOException("Simulated I/O failure during write") validateBounds(lba, blockCount) + val totalBytes = calculateTotalBytes(blockCount) + validateBufferBounds(src.size, offset, totalBytes) - val totalBytes = blockCount * sectorSizeBytes - if (offset < 0 || offset + totalBytes > src.size) { - throw IndexOutOfBoundsException("Buffer offset $offset + $totalBytes exceeds buffer size ${src.size}") - } - - for (i in 0 until blockCount) { - val sectorLba = lba + i - val srcOffset = offset + i * sectorSizeBytes - val sectorData = ByteArray(sectorSizeBytes) - System.arraycopy(src, srcOffset, sectorData, 0, sectorSizeBytes) - sectors[sectorLba] = sectorData - } - writeCount += blockCount - return true + return commitWrite(lba, blockCount, src, offset) } override suspend fun writeDirectBuffer( @@ -95,48 +124,101 @@ class MemoryBlockDevice( checkConnected() if (simulateIoFailure) throw IOException("Simulated I/O failure during direct write") validateBounds(lba, blockCount) + val totalBytes = calculateTotalBytes(blockCount) + validateBufferBounds(directBuffer.capacity(), offset, length) - val temp = ByteArray(length) - val originalPos = directBuffer.position() - try { - directBuffer.position(offset) - directBuffer.get(temp, 0, length) - } finally { - directBuffer.position(originalPos) + if (length != totalBytes) { + throw IOException( + "Direct buffer transfer length $length does not match requested block count $blockCount ($totalBytes bytes required)" + ) } - val sectorsToWrite = minOf(blockCount, length / sectorSizeBytes) - for (i in 0 until sectorsToWrite) { - val sectorLba = lba + i - val srcOffset = i * sectorSizeBytes - val sectorData = ByteArray(sectorSizeBytes) - System.arraycopy(temp, srcOffset, sectorData, 0, sectorSizeBytes) - sectors[sectorLba] = sectorData + val temp = ByteArray(length) + val slice = directBuffer.duplicate() + slice.position(offset) + slice.get(temp, 0, length) + + return commitWrite(lba, blockCount, temp, 0) + } + + /** + * Atomically validates allocation constraints and writes all blocks to memory. + * Preflights new sector requirements to prevent partial state mutation on failure. + */ + private fun commitWrite(lba: Long, blockCount: Int, src: ByteArray, offset: Int): Boolean { + synchronized(lock) { + checkConnected() + + // 1. Preflight allocation feasibility: count how many requested LBAs are genuinely new + var newSectorsNeeded = 0 + for (i in 0 until blockCount) { + val sectorLba = lba + i.toLong() + if (!sectors.containsKey(sectorLba)) { + newSectorsNeeded++ + } + } + + if (sectors.size + newSectorsNeeded > maxAllocatedSectors) { + val remaining = maxOf(0, maxAllocatedSectors - sectors.size) + throw IOException( + "MemoryBlockDevice allocation limit exceeded: operation requires $newSectorsNeeded new sector(s), " + + "but only $remaining sector(s) remaining (limit: $maxAllocatedSectors)" + ) + } + + // 2. Prepare sector buffers before modifying storage (defensive copies) + val preparedSectors = ArrayList>(blockCount) + for (i in 0 until blockCount) { + val sectorLba = lba + i.toLong() + val srcOffset = offset + (i * sectorSizeBytes) + val sectorData = ByteArray(sectorSizeBytes) + System.arraycopy(src, srcOffset, sectorData, 0, sectorSizeBytes) + preparedSectors.add(Pair(sectorLba, sectorData)) + } + + // 3. Commit prepared sectors into storage + for ((sectorLba, sectorData) in preparedSectors) { + sectors[sectorLba] = sectorData + } + + // 4. Update metrics + _writeCount.addAndGet(blockCount.toLong()) + return true } - writeCount += sectorsToWrite - return true } + /** + * Simulation hook for storage cache flushing. + * + * In this in-memory test implementation, all sector updates are already committed to JVM heap memory. + * This method does not provide physical durability across process boundaries or establish custom + * memory barriers. It increments [flushCount] and returns true to satisfy the [BlockDevice] contract + * and allow automated tests to verify that callers invoke flush at appropriate checkpoints. + */ override suspend fun flush(): Boolean { currentCoroutineContext().ensureActive() checkConnected() if (simulateIoFailure) throw IOException("Simulated I/O failure during flush") - flushCount++ + _flushCount.incrementAndGet() return true } /** - * Inspects a written sector directly for assertions in unit tests. + * Returns a defensive copy of a written sector for assertions in unit tests. + * Returns null if the sector has never been written (representing unallocated/zeroed sector). */ - fun getSector(lba: Long): ByteArray? = sectors[lba] + fun getSector(lba: Long): ByteArray? = synchronized(lock) { sectors[lba]?.copyOf() } /** * Number of non-zero sectors currently stored in memory. */ - val allocatedSectorCount: Int get() = sectors.size + val allocatedSectorCount: Int + get() = synchronized(lock) { sectors.size } fun clear() { - sectors.clear() + synchronized(lock) { + sectors.clear() + } } private fun checkConnected() { @@ -146,19 +228,38 @@ class MemoryBlockDevice( } private fun validateBounds(lba: Long, blockCount: Int) { - if (lba < 0) { + if (lba < 0L) { throw IOException("LBA cannot be negative: $lba") } if (blockCount <= 0) { throw IOException("Block count must be positive: $blockCount") } - if (lba + blockCount > totalSectors) { - throw IOException("Requested LBA range [$lba..${lba + blockCount - 1}] exceeds total sectors $totalSectors") + // Overflow-safe bounds check: lba > totalSectors - blockCount + if (blockCount.toLong() > totalSectors || lba > totalSectors - blockCount.toLong()) { + val endLba = if (lba > Long.MAX_VALUE - blockCount.toLong()) Long.MAX_VALUE else lba + blockCount.toLong() - 1L + throw IOException("Requested LBA range [$lba..$endLba] exceeds total sectors $totalSectors") + } + } + + private fun calculateTotalBytes(blockCount: Int): Int { + val totalBytesLong = blockCount.toLong() * sectorSizeBytes.toLong() + if (totalBytesLong > Int.MAX_VALUE.toLong()) { + throw IndexOutOfBoundsException("Requested byte transfer count $totalBytesLong exceeds 32-bit integer limit") + } + return totalBytesLong.toInt() + } + + private fun validateBufferBounds(bufferCapacity: Int, offset: Int, totalBytes: Int) { + if (offset < 0 || totalBytes < 0 || offset.toLong() + totalBytes.toLong() > bufferCapacity.toLong()) { + throw IndexOutOfBoundsException("Buffer offset $offset + $totalBytes exceeds buffer size $bufferCapacity") } } override fun close() { - isConnected = false - sectors.clear() + synchronized(lock) { + if (!isConnected) return + isConnected = false + sectors.clear() + } } } diff --git a/app/src/test/java/com/ashishsinghbora/flashcore/MemoryBlockDeviceTest.kt b/app/src/test/java/com/ashishsinghbora/flashcore/MemoryBlockDeviceTest.kt new file mode 100644 index 0000000..5d99e63 --- /dev/null +++ b/app/src/test/java/com/ashishsinghbora/flashcore/MemoryBlockDeviceTest.kt @@ -0,0 +1,911 @@ +package com.ashishsinghbora.flashcore + +import com.ashishsinghbora.flashcore.block.DeviceCapacity +import com.ashishsinghbora.flashcore.block.DeviceDisconnectedException +import com.ashishsinghbora.flashcore.block.MemoryBlockDevice +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException +import java.nio.ByteBuffer +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.CyclicBarrier +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +/** + * Comprehensive Unit Test Suite for [MemoryBlockDevice]. + * + * Verifies all storage-abstraction contracts, input validation, memory safety, + * arithmetic overflow handling, neighboring sector isolation, direct-buffer semantics, + * multi-sector all-or-nothing allocation failure, and high-contention concurrency. + */ +class MemoryBlockDeviceTest { + + // ------------------------------------------------------------------------ + // 1. Constructor and Geometry Validation + // ------------------------------------------------------------------------ + + @Test + fun testConstructorAcceptsValidGeometry() { + runBlocking { + val devDefault = MemoryBlockDevice() + assertEquals(MemoryBlockDevice.DEFAULT_TOTAL_SECTORS, devDefault.totalSectors) + assertEquals(512, devDefault.sectorSizeBytes) + assertTrue(devDefault.isConnected) + assertEquals(0, devDefault.allocatedSectorCount) + + val devCustom = MemoryBlockDevice(totalSectors = 4096L, sectorSizeBytes = 2048) + assertEquals(4096L, devCustom.totalSectors) + assertEquals(2048, devCustom.sectorSizeBytes) + assertEquals(4096L * 2048L, devCustom.capacity().totalBytes) + } + } + + @Test + fun testConfigurableSectorSizes() { + runBlocking { + val sectorSizes = listOf(512, 1024, 2048, 4096) + for (size in sectorSizes) { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = size) + assertEquals(size, dev.sectorSizeBytes) + assertEquals(size, dev.capacity().sectorSizeBytes) + + val payload = ByteArray(size) { (it % 256).toByte() } + assertTrue(dev.write(lba = 5L, blockCount = 1, src = payload)) + + val readBack = ByteArray(size) + assertTrue(dev.read(lba = 5L, blockCount = 1, dest = readBack)) + assertArrayEquals("Mismatch for sector size $size", payload, readBack) + } + } + } + + @Test + fun testConfigurableSectorCount() { + runBlocking { + val smallDev = MemoryBlockDevice(totalSectors = 1L, sectorSizeBytes = 512) + assertEquals(1L, smallDev.capacity().totalSectors) + + val mediumDev = MemoryBlockDevice(totalSectors = 65536L, sectorSizeBytes = 512) + assertEquals(65536L, mediumDev.capacity().totalSectors) + + val largeDev = MemoryBlockDevice(totalSectors = 10000000L, sectorSizeBytes = 512) + assertEquals(10000000L, largeDev.capacity().totalSectors) + } + } + + @Test + fun testCapacityCalculation() { + runBlocking { + // 2,097,152 sectors * 512 = 1,073,741,824 bytes = 1.00 GB + val dev = MemoryBlockDevice(totalSectors = 2097152L, sectorSizeBytes = 512) + val cap = dev.capacity() + assertEquals(2097152L, cap.totalSectors) + assertEquals(512, cap.sectorSizeBytes) + assertEquals(1073741824L, cap.totalBytes) + assertEquals("1.00 GB", cap.formattedCapacity) + } + } + + @Test + fun testInvalidConstructorArguments() { + // sectorSizeBytes <= 0 + assertThrows(IllegalArgumentException::class.java) { + MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 0) + } + assertThrows(IllegalArgumentException::class.java) { + MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = -512) + } + + // totalSectors < 0 + assertThrows(IllegalArgumentException::class.java) { + MemoryBlockDevice(totalSectors = -1L, sectorSizeBytes = 512) + } + + // maxAllocatedSectors <= 0 + assertThrows(IllegalArgumentException::class.java) { + MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = 0) + } + + // Arithmetic overflow in total capacity + assertThrows(IllegalArgumentException::class.java) { + MemoryBlockDevice(totalSectors = Long.MAX_VALUE / 200L, sectorSizeBytes = 512) + } + } + + @Test + fun testZeroCapacityDevice() { + runBlocking { + val zeroDev = MemoryBlockDevice(totalSectors = 0L, sectorSizeBytes = 512) + assertEquals(0L, zeroDev.capacity().totalSectors) + assertEquals(0L, zeroDev.capacity().totalBytes) + + // Any read or write with blockCount > 0 should fail bounds check + assertThrows(IOException::class.java) { + runBlocking { zeroDev.read(0L, 1, ByteArray(512)) } + } + assertThrows(IOException::class.java) { + runBlocking { zeroDev.write(0L, 1, ByteArray(512)) } + } + } + } + + // ------------------------------------------------------------------------ + // 2. Read and Write Semantics + // ------------------------------------------------------------------------ + + @Test + fun testInitialContentsAreZeroes() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + val dest = ByteArray(512) { 0xFF.toByte() } // Pre-fill with non-zero + assertTrue(dev.read(lba = 10L, blockCount = 1, dest = dest)) + assertArrayEquals(ByteArray(512), dest) + assertEquals(0, dev.allocatedSectorCount) + } + } + + @Test + fun testSingleSectorWriteAndRead() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val payload = ByteArray(512) { (it * 3 % 256).toByte() } + + assertTrue(dev.write(lba = 7L, blockCount = 1, src = payload)) + assertEquals(1, dev.allocatedSectorCount) + assertEquals(1L, dev.writeCount) + + val dest = ByteArray(512) + assertTrue(dev.read(lba = 7L, blockCount = 1, dest = dest)) + assertArrayEquals(payload, dest) + assertEquals(1L, dev.readCount) + } + } + + @Test + fun testMultiSectorWriteAndRead() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + val blockCount = 5 + val payload = ByteArray(blockCount * 512) { idx -> ((idx * 7 + 13) % 256).toByte() } + + assertTrue(dev.write(lba = 20L, blockCount = blockCount, src = payload)) + assertEquals(blockCount, dev.allocatedSectorCount) + assertEquals(blockCount.toLong(), dev.writeCount) + + val dest = ByteArray(blockCount * 512) + assertTrue(dev.read(lba = 20L, blockCount = blockCount, dest = dest)) + assertArrayEquals(payload, dest) + assertEquals(blockCount.toLong(), dev.readCount) + } + } + + @Test + fun testOverwriteExistingSector() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val initialData = ByteArray(512) { 0x11 } + val updatedData = ByteArray(512) { 0x22 } + + assertTrue(dev.write(lba = 5L, blockCount = 1, src = initialData)) + assertEquals(1, dev.allocatedSectorCount) + + // Overwrite same sector + assertTrue(dev.write(lba = 5L, blockCount = 1, src = updatedData)) + assertEquals("Allocated sectors count should not increase on overwrite", 1, dev.allocatedSectorCount) + + val readBack = ByteArray(512) + assertTrue(dev.read(lba = 5L, blockCount = 1, dest = readBack)) + assertArrayEquals(updatedData, readBack) + } + } + + @Test + fun testLastValidSectorWriteAndRead() { + runBlocking { + val totalSectors = 1000L + val dev = MemoryBlockDevice(totalSectors = totalSectors, sectorSizeBytes = 512) + val lastLba = totalSectors - 1L + val payload = ByteArray(512) { 0x7E } + + assertTrue(dev.write(lba = lastLba, blockCount = 1, src = payload)) + val dest = ByteArray(512) + assertTrue(dev.read(lba = lastLba, blockCount = 1, dest = dest)) + assertArrayEquals(payload, dest) + } + } + + @Test + fun testRepeatedReadConsistency() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 20L, sectorSizeBytes = 512) + val payload = ByteArray(512) { (it * 17 % 256).toByte() } + dev.write(10L, 1, payload) + + val read1 = ByteArray(512) + val read2 = ByteArray(512) + dev.read(10L, 1, read1) + dev.read(10L, 1, read2) + + assertArrayEquals(payload, read1) + assertArrayEquals(read1, read2) + } + } + + // ------------------------------------------------------------------------ + // 3. Bounds and Error Handling + // ------------------------------------------------------------------------ + + @Test + fun testReadBeforeBeginningNegativeLbaThrows() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + val ex = assertThrows(IOException::class.java) { + runBlocking { dev.read(lba = -1L, blockCount = 1, dest = ByteArray(512)) } + } + assertTrue(ex.message!!.contains("negative")) + } + } + + @Test + fun testWriteBeforeBeginningNegativeLbaThrows() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + val ex = assertThrows(IOException::class.java) { + runBlocking { dev.write(lba = -1L, blockCount = 1, src = ByteArray(512)) } + } + assertTrue(ex.message!!.contains("negative")) + } + } + + @Test + fun testReadPastEndThrows() { + runBlocking { + val totalSectors = 100L + val dev = MemoryBlockDevice(totalSectors = totalSectors, sectorSizeBytes = 512) + val ex = assertThrows(IOException::class.java) { + runBlocking { dev.read(lba = totalSectors, blockCount = 1, dest = ByteArray(512)) } + } + assertTrue(ex.message!!.contains("exceeds total sectors")) + } + } + + @Test + fun testWritePastEndThrows() { + runBlocking { + val totalSectors = 100L + val dev = MemoryBlockDevice(totalSectors = totalSectors, sectorSizeBytes = 512) + val ex = assertThrows(IOException::class.java) { + runBlocking { dev.write(lba = totalSectors, blockCount = 1, src = ByteArray(512)) } + } + assertTrue(ex.message!!.contains("exceeds total sectors")) + } + } + + @Test + fun testRequestExtendingBeyondEndThrows() { + runBlocking { + val totalSectors = 100L + val dev = MemoryBlockDevice(totalSectors = totalSectors, sectorSizeBytes = 512) + val ex = assertThrows(IOException::class.java) { + runBlocking { dev.write(lba = totalSectors - 1L, blockCount = 2, src = ByteArray(1024)) } + } + assertTrue(ex.message!!.contains("exceeds total sectors")) + } + } + + @Test + fun testOverflowSafeRangeValidation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + + // LBA near Long.MAX_VALUE should NOT bypass bounds check via arithmetic overflow + val ex1 = assertThrows(IOException::class.java) { + runBlocking { dev.read(lba = Long.MAX_VALUE - 5L, blockCount = 10, dest = ByteArray(10 * 512)) } + } + assertTrue(ex1.message!!.contains("exceeds total sectors")) + + val ex2 = assertThrows(IOException::class.java) { + runBlocking { dev.write(lba = Long.MAX_VALUE - 5L, blockCount = 10, src = ByteArray(10 * 512)) } + } + assertTrue(ex2.message!!.contains("exceeds total sectors")) + } + } + + @Test + fun testBufferBoundsValidation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512) + + // Destination buffer too small + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { dev.read(lba = 0L, blockCount = 2, dest = ByteArray(512)) } + } + + // Source buffer too small + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { dev.write(lba = 0L, blockCount = 2, src = ByteArray(512)) } + } + + // Negative offset + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { dev.read(lba = 0L, blockCount = 1, dest = ByteArray(512), offset = -1) } + } + + // Offset + bytes exceeds buffer + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { dev.write(lba = 0L, blockCount = 1, src = ByteArray(512), offset = 1) } + } + } + } + + // ------------------------------------------------------------------------ + // 4. Flush and Persistence Behavior + // ------------------------------------------------------------------------ + + @Test + fun testFlushBehavior() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + assertEquals(0L, dev.flushCount) + + val payload = ByteArray(512) { 0x5A } + assertTrue(dev.write(lba = 0L, blockCount = 1, src = payload)) + + assertTrue(dev.flush()) + assertEquals(1L, dev.flushCount) + + val dest = ByteArray(512) + assertTrue(dev.read(lba = 0L, blockCount = 1, dest = dest)) + assertArrayEquals(payload, dest) + } + } + + // ------------------------------------------------------------------------ + // 5. Corruption and Boundary Isolation (PHASE 15) + // ------------------------------------------------------------------------ + + @Test + fun testNeighboringSectorIsolation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 10L, sectorSizeBytes = 512) + val sectorN = 5L + val payload = ByteArray(512) { 0xAB.toByte() } + + // Write only to sector N + assertTrue(dev.write(lba = sectorN, blockCount = 1, src = payload)) + + // Verify sector N is written + val readN = ByteArray(512) + dev.read(lba = sectorN, blockCount = 1, dest = readN) + assertArrayEquals(payload, readN) + + // Verify sector N - 1 remains untouched zeroes + val readPrev = ByteArray(512) + dev.read(lba = sectorN - 1L, blockCount = 1, dest = readPrev) + assertArrayEquals("Neighboring sector N-1 must be zero", ByteArray(512), readPrev) + + // Verify sector N + 1 remains untouched zeroes + val readNext = ByteArray(512) + dev.read(lba = sectorN + 1L, blockCount = 1, dest = readNext) + assertArrayEquals("Neighboring sector N+1 must be zero", ByteArray(512), readNext) + } + } + + @Test + fun testBufferMutationIsolation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 20L, sectorSizeBytes = 512) + val src = ByteArray(512) { 0x42 } + + // Write data + dev.write(lba = 3L, blockCount = 1, src = src) + + // Mutate caller's source array after write + src.fill(0x00) + + // Verify stored data inside device was NOT affected + val readBack = ByteArray(512) + dev.read(lba = 3L, blockCount = 1, dest = readBack) + assertEquals("Stored sector must not be corrupted by caller mutating source buffer", 0x42.toByte(), readBack[0]) + + // getSector returns defensive copy + val directSector = dev.getSector(3L) + assertNotNull(directSector) + directSector!![0] = 0x99.toByte() + + // Verify device storage unchanged after mutating getSector result + val readBack2 = ByteArray(512) + dev.read(lba = 3L, blockCount = 1, dest = readBack2) + assertEquals(0x42.toByte(), readBack2[0]) + } + } + + // ------------------------------------------------------------------------ + // 6. Direct Buffer Operations & Partial-Write Rejection (Points 1 & 4) + // ------------------------------------------------------------------------ + + @Test + fun testDirectBufferWriteAndReadBack() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(1024) + val pattern = ByteArray(1024) { (it % 256).toByte() } + directBuf.put(pattern) + directBuf.flip() + + val success = dev.writeDirectBuffer(lba = 10L, blockCount = 2, directBuffer = directBuf, offset = 0, length = 1024) + assertTrue(success) + + // Original buffer position should be restored + assertEquals(0, directBuf.position()) + + val readBack = ByteArray(1024) + dev.read(lba = 10L, blockCount = 2, dest = readBack) + assertArrayEquals(pattern, readBack) + } + } + + @Test + fun testDirectBufferInsufficientLengthRejected() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(1024) + directBuf.put(ByteArray(1024) { 0x44 }) + directBuf.flip() + + // Request 2 blocks (1024 bytes required), but pass length = 512 + val ex = assertThrows(IOException::class.java) { + runBlocking { + dev.writeDirectBuffer(lba = 10L, blockCount = 2, directBuffer = directBuf, offset = 0, length = 512) + } + } + assertTrue(ex.message!!.contains("does not match requested block count")) + + // Assert NO storage mutation occurred (prevent silent partial write) + assertEquals(0, dev.allocatedSectorCount) + assertNull(dev.getSector(10L)) + assertNull(dev.getSector(11L)) + + val readBack = ByteArray(1024) + dev.read(10L, 2, readBack) + assertArrayEquals(ByteArray(1024), readBack) + } + } + + @Test + fun testDirectBufferExcessLengthRejected() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(1024) + directBuf.put(ByteArray(1024) { 0x44 }) + directBuf.flip() + + // Request 1 block (512 bytes required), but pass length = 1024 + val ex = assertThrows(IOException::class.java) { + runBlocking { + dev.writeDirectBuffer(lba = 5L, blockCount = 1, directBuffer = directBuf, offset = 0, length = 1024) + } + } + assertTrue(ex.message!!.contains("does not match requested block count")) + assertEquals(0, dev.allocatedSectorCount) + assertNull(dev.getSector(5L)) + } + } + + @Test + fun testDirectBufferNonMultipleLengthRejected() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(1024) + directBuf.put(ByteArray(1024)) + directBuf.flip() + + // Request 1 block (512 bytes required), but pass length = 600 + val ex = assertThrows(IOException::class.java) { + runBlocking { + dev.writeDirectBuffer(lba = 5L, blockCount = 1, directBuffer = directBuf, offset = 0, length = 600) + } + } + assertTrue(ex.message!!.contains("does not match requested block count")) + assertEquals(0, dev.allocatedSectorCount) + } + } + + @Test + fun testDirectBufferBlockCountOverflowRejected() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = Long.MAX_VALUE / 512L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(512) + + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { + dev.writeDirectBuffer(lba = 0L, blockCount = Int.MAX_VALUE, directBuffer = directBuf, offset = 0, length = 512) + } + } + assertEquals(0, dev.allocatedSectorCount) + } + } + + @Test + fun testDirectBufferBoundsValidation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + val directBuf = ByteBuffer.allocateDirect(512) + + // Negative offset + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { + dev.writeDirectBuffer(0L, 1, directBuf, offset = -1, length = 512) + } + } + + // Offset + length exceeds capacity + assertThrows(IndexOutOfBoundsException::class.java) { + runBlocking { + dev.writeDirectBuffer(0L, 1, directBuf, offset = 100, length = 512) + } + } + } + } + + // ------------------------------------------------------------------------ + // 7. Allocation Limit & Multi-Sector Non-Partial Failure (Points 2 & 3) + // ------------------------------------------------------------------------ + + @Test + fun testAllocationLimitEnforcement() { + runBlocking { + // Configure tight limit: maximum 3 sectors + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = 3) + val dummy = ByteArray(512) + + assertTrue(dev.write(0L, 1, dummy)) + assertTrue(dev.write(1L, 1, dummy)) + assertTrue(dev.write(2L, 1, dummy)) + assertEquals(3, dev.allocatedSectorCount) + + // Writing to a 4th distinct sector must exceed limit and throw IOException + assertThrows(IOException::class.java) { + runBlocking { dev.write(3L, 1, dummy) } + } + + // Overwriting existing sector 0 is permitted (does not increase count) + assertTrue(dev.write(0L, 1, dummy)) + assertEquals(3, dev.allocatedSectorCount) + } + } + + @Test + fun testMultiSectorAllocationFailureDoesNotPartiallyMutateStorage() { + runBlocking { + // maxAllocatedSectors = 2 + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = 2) + + val initialPayload = ByteArray(1024) { 0xAA.toByte() } + assertTrue(dev.write(lba = 10L, blockCount = 2, src = initialPayload)) + assertEquals(2, dev.allocatedSectorCount) + + // Attempt to write sectors 10, 11, 12 (sectors 10 and 11 exist, 12 is new -> requires 1 new sector, exceeds limit 2) + val newPayload = ByteArray(1536) { 0xBB.toByte() } + val ex = assertThrows(IOException::class.java) { + runBlocking { + dev.write(lba = 10L, blockCount = 3, src = newPayload) + } + } + assertTrue(ex.message!!.contains("allocation limit exceeded")) + + // CRITICAL VERIFICATION: + // Sector 12 was NOT created + assertNull("Sector 12 must not be allocated", dev.getSector(12L)) + + // Sectors 10 and 11 must still contain original 0xAA data (NOT 0xBB) + val check10 = ByteArray(512) + val check11 = ByteArray(512) + dev.read(10L, 1, check10) + dev.read(11L, 1, check11) + assertTrue("Sector 10 must not be corrupted by failed write", check10.all { it == 0xAA.toByte() }) + assertTrue("Sector 11 must not be corrupted by failed write", check11.all { it == 0xAA.toByte() }) + + // Allocated sector count remains 2 + assertEquals(2, dev.allocatedSectorCount) + } + } + + @Test + fun testMultiSectorAllocationFailureOnAllNewSectorsLeavesZeroMutation() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = 2) + + // Pre-allocate 1 sector + assertTrue(dev.write(0L, 1, ByteArray(512) { 0x11 })) + assertEquals(1, dev.allocatedSectorCount) + + // Attempt write of 2 new sectors (5 and 6) when only 1 remaining capacity (1 + 2 = 3 > 2) + val ex = assertThrows(IOException::class.java) { + runBlocking { + dev.write(5L, 2, ByteArray(1024) { 0x22 }) + } + } + assertTrue(ex.message!!.contains("allocation limit exceeded")) + + // Neither sector 5 nor sector 6 was created + assertNull(dev.getSector(5L)) + assertNull(dev.getSector(6L)) + assertEquals(1, dev.allocatedSectorCount) + } + } + + @Test + fun testRewriteExistingSectorAtCapacityLimit() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = 2) + + assertTrue(dev.write(10L, 1, ByteArray(512) { 0x11 })) + assertTrue(dev.write(11L, 1, ByteArray(512) { 0x22 })) + assertEquals(2, dev.allocatedSectorCount) + + // Device is at capacity limit (2 of 2). + // Rewriting existing sector 10 must succeed without consuming new allocation + val updated = ByteArray(512) { 0x99.toByte() } + assertTrue(dev.write(10L, 1, updated)) + assertEquals(2, dev.allocatedSectorCount) + + val readBack = ByteArray(512) + dev.read(10L, 1, readBack) + assertArrayEquals(updated, readBack) + } + } + + // ------------------------------------------------------------------------ + // 8. High-Contention Concurrency Tests (Points 2 & 5) + // ------------------------------------------------------------------------ + + @Test + fun testConcurrentAllocationRespectsLimitUnderContention() { + val maxAllocated = 20 + val threadCount = 40 + val dev = MemoryBlockDevice(totalSectors = 1000L, sectorSizeBytes = 512, maxAllocatedSectors = maxAllocated) + val barrier = CyclicBarrier(threadCount) + val executor = Executors.newFixedThreadPool(threadCount) + val successCount = AtomicInteger(0) + val failureCount = AtomicInteger(0) + val unexpectedErrors = CopyOnWriteArrayList() + + for (i in 0 until threadCount) { + val sectorLba = (i + 1) * 10L + executor.submit { + try { + barrier.await(5, TimeUnit.SECONDS) + runBlocking { + val payload = ByteArray(512) { (i + 1).toByte() } + val ok = dev.write(lba = sectorLba, blockCount = 1, src = payload) + if (ok) successCount.incrementAndGet() + } + } catch (e: IOException) { + if (e.message?.contains("allocation limit exceeded") == true) { + failureCount.incrementAndGet() + } else { + unexpectedErrors.add(e) + } + } catch (t: Exception) { + unexpectedErrors.add(t) + } + } + } + + executor.shutdown() + assertTrue("Executor should terminate within 10s", executor.awaitTermination(10, TimeUnit.SECONDS)) + assertTrue("Unexpected errors: $unexpectedErrors", unexpectedErrors.isEmpty()) + + // Invariants: + assertEquals("Allocated sectors must exactly equal limit", maxAllocated, dev.allocatedSectorCount) + assertEquals("Successful writes must equal allocation limit", maxAllocated, successCount.get()) + assertEquals("Failed writes must equal total - limit", threadCount - maxAllocated, failureCount.get()) + } + + @Test + fun testConcurrentRewritesOnExistingSectors() { + runBlocking { + val sectorCount = 5 + val dev = MemoryBlockDevice(totalSectors = 100L, sectorSizeBytes = 512, maxAllocatedSectors = sectorCount) + + // Pre-allocate all 5 sectors + for (s in 0 until sectorCount) { + dev.write(lba = s.toLong(), blockCount = 1, src = ByteArray(512) { 0x00 }) + } + assertEquals(sectorCount, dev.allocatedSectorCount) + + val threadCount = 20 + val iterationsPerThread = 50 + val barrier = CyclicBarrier(threadCount) + val executor = Executors.newFixedThreadPool(threadCount) + val errors = CopyOnWriteArrayList() + + for (t in 0 until threadCount) { + val threadByte = (t + 1).toByte() + executor.submit { + try { + barrier.await(5, TimeUnit.SECONDS) + for (iter in 0 until iterationsPerThread) { + val targetSector = ((t + iter) % sectorCount).toLong() + val payload = ByteArray(512) { threadByte } + runBlocking { + val ok = dev.write(targetSector, 1, payload) + assertTrue(ok) + } + } + } catch (th: Throwable) { + errors.add(th) + } + } + } + + executor.shutdown() + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)) + assertTrue("Errors during concurrent rewrite: $errors", errors.isEmpty()) + + // Allocation count must NOT grow + assertEquals("Allocated sectors must remain unchanged on rewrites", sectorCount, dev.allocatedSectorCount) + + // Each sector must be internally consistent (no partial/torn byte updates) + for (s in 0 until sectorCount) { + val buf = ByteArray(512) + dev.read(s.toLong(), 1, buf) + val expectedByte = buf[0] + assertTrue("All bytes in sector must be identical to single writer payload", buf.all { it == expectedByte }) + } + } + } + + @Test + fun testConcurrentReadersAndWritersDataIntegrity() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 200L, sectorSizeBytes = 512, maxAllocatedSectors = 50) + val writerCount = 10 + val readerCount = 10 + val totalThreads = writerCount + readerCount + val barrier = CyclicBarrier(totalThreads) + val executor = Executors.newFixedThreadPool(totalThreads) + val errors = CopyOnWriteArrayList() + + // Writers update sectors 0..9 with uniform byte values + for (w in 0 until writerCount) { + val writerVal = (w + 1).toByte() + executor.submit { + try { + barrier.await(5, TimeUnit.SECONDS) + for (i in 0 until 50) { + val lba = (i % 10).toLong() + val payload = ByteArray(512) { writerVal } + runBlocking { + dev.write(lba, 1, payload) + } + } + } catch (t: Throwable) { + errors.add(t) + } + } + } + + // Readers read sectors 0..9 and verify they contain a uniform byte value (no torn bytes) + for (r in 0 until readerCount) { + executor.submit { + try { + barrier.await(5, TimeUnit.SECONDS) + val buf = ByteArray(512) + for (i in 0 until 50) { + val lba = (i % 10).toLong() + runBlocking { + dev.read(lba, 1, buf) + } + val b0 = buf[0] + assertTrue("Sector must not have torn bytes", buf.all { it == b0 }) + } + } catch (t: Throwable) { + errors.add(t) + } + } + } + + executor.shutdown() + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)) + assertTrue("Errors during reader/writer concurrency: $errors", errors.isEmpty()) + } + } + + // ------------------------------------------------------------------------ + // 9. Lifecycle and Fault Simulation + // ------------------------------------------------------------------------ + + @Test + fun testSimulateIoFailure() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + dev.simulateIoFailure = true + + assertThrows(IOException::class.java) { + runBlocking { dev.read(0L, 1, ByteArray(512)) } + } + assertThrows(IOException::class.java) { + runBlocking { dev.write(0L, 1, ByteArray(512)) } + } + assertThrows(IOException::class.java) { + runBlocking { dev.flush() } + } + assertThrows(IOException::class.java) { + runBlocking { + dev.writeDirectBuffer(0L, 1, ByteBuffer.allocateDirect(512), 0, 512) + } + } + } + } + + @Test + fun testDeviceDisconnectedExceptionOnClose() { + runBlocking { + val dev = MemoryBlockDevice(totalSectors = 50L, sectorSizeBytes = 512) + assertTrue(dev.isConnected) + + dev.close() + assertFalse(dev.isConnected) + assertEquals(0, dev.allocatedSectorCount) + + assertThrows(DeviceDisconnectedException::class.java) { + runBlocking { dev.capacity() } + } + assertThrows(DeviceDisconnectedException::class.java) { + runBlocking { dev.read(0L, 1, ByteArray(512)) } + } + assertThrows(DeviceDisconnectedException::class.java) { + runBlocking { dev.write(0L, 1, ByteArray(512)) } + } + assertThrows(DeviceDisconnectedException::class.java) { + runBlocking { dev.flush() } + } + } + } + + // ------------------------------------------------------------------------ + // 10. Property / Round-Trip Data-Driven Tests (PHASE 14) + // ------------------------------------------------------------------------ + + @Test + fun testPropertyRoundTripAcrossSectorSizes() { + runBlocking { + val geometries = listOf( + Pair(512, 100L), + Pair(1024, 50L), + Pair(2048, 25L), + Pair(4096, 15L) + ) + + for ((sectorSize, sectorCount) in geometries) { + val dev = MemoryBlockDevice(totalSectors = sectorCount, sectorSizeBytes = sectorSize) + + // Write pseudo-random deterministic pattern across 3 sectors + val blocksToWrite = 3 + val totalBytes = blocksToWrite * sectorSize + val pattern = ByteArray(totalBytes) { idx -> + ((idx * 31 + sectorSize * 17 + 7) % 256).toByte() + } + + val startLba = sectorCount / 2L + assertTrue(dev.write(startLba, blocksToWrite, pattern)) + assertTrue(dev.flush()) + + val readBack = ByteArray(totalBytes) + assertTrue(dev.read(startLba, blocksToWrite, readBack)) + assertArrayEquals("Round-trip failed for sectorSize=$sectorSize", pattern, readBack) + } + } + } +} diff --git a/app/src/test/java/com/ashishsinghbora/flashcore/PartitionEngineTest.kt b/app/src/test/java/com/ashishsinghbora/flashcore/PartitionEngineTest.kt index fb92faa..aa5ca0c 100644 --- a/app/src/test/java/com/ashishsinghbora/flashcore/PartitionEngineTest.kt +++ b/app/src/test/java/com/ashishsinghbora/flashcore/PartitionEngineTest.kt @@ -110,6 +110,64 @@ class PartitionEngineTest { assertFalse(parsed.partitions[1].bootable) } + @Test + fun testMbrDeviceWriteAndRoundTripParsing() { + runBlocking { + val totalSectors = 2097152L // 1 GB + val memDevice = MemoryBlockDevice(totalSectors = totalSectors, sectorSizeBytes = 512) + + val p1 = Partition( + index = 1, + firstLba = 2048L, + lastLba = 100000L, + name = "System", + mbrType = MbrBuilder.TYPE_FAT32_LBA, + bootable = true + ) + val p2 = Partition( + index = 2, + firstLba = 102400L, + lastLba = 500000L, + name = "Data", + mbrType = MbrBuilder.TYPE_NTFS_EXFAT, + bootable = false + ) + + val table = MbrPartitionTable( + partitions = listOf(p1, p2), + totalDiskSectors = totalSectors, + diskSignature = 0x55AA1234 + ) + + // Write MBR bytes to Sector 0 of MemoryBlockDevice + val mbrBytes = PartitionEngine.buildMbrBytes(table) + assertTrue(memDevice.write(lba = 0L, blockCount = 1, src = mbrBytes)) + assertTrue(memDevice.flush()) + + // Read partition table back from the BlockDevice + val parsedTable = PartitionEngine.readFromDevice(memDevice) + assertNotNull(parsedTable) + assertTrue(parsedTable is MbrPartitionTable) + + val mbr = parsedTable as MbrPartitionTable + assertEquals(0x55AA1234, mbr.diskSignature) + assertEquals(2, mbr.partitions.size) + assertFalse(mbr.isProtective) + + assertEquals(1, mbr.partitions[0].index) + assertEquals(2048L, mbr.partitions[0].firstLba) + assertEquals(100000L, mbr.partitions[0].lastLba) + assertEquals(MbrBuilder.TYPE_FAT32_LBA, mbr.partitions[0].mbrType) + assertTrue(mbr.partitions[0].bootable) + + assertEquals(2, mbr.partitions[1].index) + assertEquals(102400L, mbr.partitions[1].firstLba) + assertEquals(500000L, mbr.partitions[1].lastLba) + assertEquals(MbrBuilder.TYPE_NTFS_EXFAT, mbr.partitions[1].mbrType) + assertFalse(mbr.partitions[1].bootable) + } + } + @Test fun testProtectiveMbrGenerationAndDetection() { val totalSectors = 62914560L // 30 GB disk diff --git a/scripts/test_audit_claims.py b/scripts/test_audit_claims.py index e801fdc..021bf07 100755 --- a/scripts/test_audit_claims.py +++ b/scripts/test_audit_claims.py @@ -198,8 +198,8 @@ def test_flashcore_repo_source_audit(self): details["source_audit_status"], "PASS", f"Repository source audit failed with violations: {details['violations']}" ) - self.assertEqual(details["inventory"]["total"], 187) - self.assertEqual(details["inventory"]["unit"], 186) + self.assertEqual(details["inventory"]["total"], 226) + self.assertEqual(details["inventory"]["unit"], 225) self.assertEqual(details["inventory"]["instrumentation"], 1)