From 71a6fb85cc17e7a579e052dbe7e2b69b5b4ba8b0 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:28:26 +0300 Subject: [PATCH 1/6] - **BinaryWriterPool**: - **New Feature**: Added `BinaryWriterPool.configure()` for global pool settings (`maxPoolSize`, `initialBufferSize`, `maxReusableCapacity`). - **New Feature**: Implemented "best-fit" acquisition strategy to minimize memory fragmentation. - **New Feature**: Added `BinaryWriterPool.stats` for real-time pool performance monitoring. - **Improvement**: Optimized `release()` and `clear()` logic with zero-allocation buffer detachment. - **Bug Fix**: Renamed `initialBufferSizer` to `initialBufferSize` (API alignment). - **BinaryWriter**: - **New Feature**: Added `operator []` and `operator []=` for symmetric random access to written bytes. - **New Feature**: Added `copy` parameter to `takeBytes()` to allow returning data while retaining the internal buffer for pool reuse. - **Improvement**: Refactored `writeUint8At` as a functional alias for `operator []=`. - **Fix**: Corrected documentation for bounds checks on random access methods. - **Documentation**: - Unified terminology (using "1 KiB" consistently). - Updated `withWriter` examples to recommend `takeBytes(copy: true)` for pooling scenarios. --- CHANGELOG.md | 19 ++ lib/src/binary_reader.dart | 3 +- lib/src/binary_writer.dart | 131 ++++++++----- lib/src/binary_writer_pool.dart | 187 +++++++++++++------ lib/src/internal.dart | 4 + lib/src/stream/stream_binary_reader.dart | 6 +- pubspec.yaml | 2 +- test/unit/binary_writer_buffer_test.dart | 38 +++- test/unit/binary_writer_navigation_test.dart | 51 +++++ test/unit/binary_writer_pool_test.dart | 47 +++++ 10 files changed, 377 insertions(+), 111 deletions(-) create mode 100644 lib/src/internal.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 709974c..a84a0f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ +# 5.1.0 + +- **BinaryWriterPool**: + - **New Feature**: Added `BinaryWriterPool.configure()` for global pool settings (`maxPoolSize`, `initialBufferSize`, `maxReusableCapacity`). + - **New Feature**: Implemented "best-fit" acquisition strategy to minimize memory fragmentation. + - **New Feature**: Added `BinaryWriterPool.stats` for real-time pool performance monitoring. + - **Improvement**: Optimized `release()` and `clear()` logic with zero-allocation buffer detachment. + - **Bug Fix**: Renamed `initialBufferSizer` to `initialBufferSize` (API alignment). + +- **BinaryWriter**: + - **New Feature**: Added `operator []` and `operator []=` for symmetric random access to written bytes. + - **New Feature**: Added `copy` parameter to `takeBytes()` to allow returning data while retaining the internal buffer for pool reuse. + - **Improvement**: Refactored `writeUint8At` as a functional alias for `operator []=`. + - **Fix**: Corrected documentation for bounds checks on random access methods. + +- **Documentation**: + - Unified terminology (using "1 KiB" consistently). + - Updated `withWriter` examples to recommend `takeBytes(copy: true)` for pooling scenarios. + # 5.0.0 - **BREAKING CHANGES:** diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index ea214de..517df95 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'constants.dart'; +import 'internal.dart'; /// A high-performance binary reader for decoding data from a byte buffer. /// @@ -635,7 +636,7 @@ extension type BinaryReader._(_ReaderState _rs) { } if (length == 0) { - return Uint8List(0); + return emptyUintList_; } final peekOffset = offset ?? _rs.offset; diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index d98b7c1..4a5ad3d 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -163,6 +163,24 @@ extension type BinaryWriter._(_WriterState _ws) { writeVarUint(encoded); } + /// Writes a boolean value as a single byte. + /// + /// `true` is written as `1` and `false` as `0`. + /// + /// Example: + /// ```dart + /// writer.writeBool(true); // Writes byte 0x01 + /// writer.writeBool(false); // Writes byte 0x00 + /// ``` + /// + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + // Disable lint to allow positional boolean parameter for simplicity + // ignore: avoid_positional_boolean_parameters + void writeBool(bool value) { + writeUint8(value ? 1 : 0); + } + /// Writes an 8-bit unsigned integer (0-255). /// /// Example: @@ -657,7 +675,7 @@ extension type BinaryWriter._(_WriterState _ws) { } @pragma('vm:prefer-inline') - @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int _varIntSize(int value) => switch (value) { < 0x80 => 1, < 0x4000 => 2, @@ -684,7 +702,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeStringFixed('Hello', lengthEncoding: LengthEncoding.u16); /// ``` @pragma('vm:prefer-inline') - @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeStringFixed( String value, { LengthEncoding lengthEncoding = .u8, @@ -728,24 +746,6 @@ extension type BinaryWriter._(_WriterState _ws) { } } - /// Writes a boolean value as a single byte. - /// - /// `true` is written as `1` and `false` as `0`. - /// - /// Example: - /// ```dart - /// writer.writeBool(true); // Writes byte 0x01 - /// writer.writeBool(false); // Writes byte 0x00 - /// ``` - /// - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - // Disable lint to allow positional boolean parameter for simplicity - // ignore: avoid_positional_boolean_parameters - void writeBool(bool value) { - writeUint8(value ? 1 : 0); - } - /// Writes a sequence of bytes. /// /// This is a concise alias for [writeBytes]. @@ -760,35 +760,51 @@ extension type BinaryWriter._(_WriterState _ws) { /// Extracts all written bytes and resets the writer. /// - /// After calling this method, the writer is reset and ready for reuse. - /// This is more efficient than creating a new writer for each operation. - /// - /// Returns a view of the written bytes (no copying occurs). + /// [copy] determines how the bytes are extracted: + /// - If `true`: The written bytes are copied into a new [Uint8List]. The + /// internal buffer is retained and its offset is reset to 0. This is + /// highly efficient for pooling (e.g., [BinaryWriterPool]) as the same + /// large buffer is reused for subsequent operations without re-allocation. + /// - If `false` (default): A view of the internal buffer is returned, and + /// the writer detaches from it by allocating a fresh initial-sized buffer. + /// While the returned bytes are "zero-copy" relative to the old buffer, + /// this forces the writer to re-allocate memory, which is less efficient + /// for pooling long-term. /// - /// **Use case:** When you're done with this batch and want to start fresh. + /// After calling this method, the writer is reset and ready for reuse. /// /// Example: /// ```dart /// final writer = BinaryWriter(); /// writer.writeUint32(42); - /// final packet1 = writer.takeBytes(); // Get bytes and reset - /// writer.writeUint32(100); // Writer is ready for reuse - /// final packet2 = writer.takeBytes(); + /// // For best pooling performance (retains internal buffer): + /// final packet = writer.takeBytes(copy: true); /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - Uint8List takeBytes() { + Uint8List takeBytes({bool copy = false}) { + if (copy) { + final result = _ws.list.sublist(0, _ws.offset); + _ws.offset = 0; + + return result; + } + final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); _ws._initializeBuffer(); return result; } - /// Returns a view of the written bytes without resetting the writer. + /// Returns a view of the written bytes (from index 0 up to the current + /// [bytesWritten]) without resetting the writer. /// /// Unlike [takeBytes], this does not reset the writer's state. /// Subsequent writes will continue appending to the buffer. /// + /// **Note:** Since this returns a view, the content of the returned list + /// will change if you continue writing to this writer. + /// /// **Use case:** When you need to inspect or copy data mid-stream. /// /// Example: @@ -827,33 +843,60 @@ extension type BinaryWriter._(_WriterState _ws) { _ws.offset = position; } + /// Returns the byte at the specified [index] without changing the current + /// write position. + /// + /// Throws [RangeError] if [index] is negative or greater than or equal to + /// [bytesWritten]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int operator [](int index) { + if (index < 0 || index >= _ws.offset) { + throw RangeError.range(index, 0, _ws.offset - 1, 'index'); + } + return _ws.list[index]; + } + + /// Writes a byte at the specified [index] without changing the current + /// write position. + /// + /// This operator is used to overwrite already written bytes. To append data, + /// use the standard `write*` methods. + /// + /// Throws [RangeError] if [index] is negative or greater than or equal to + /// [bytesWritten]. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void operator []=(int index, int value) { + if (index < 0 || index >= _ws.offset) { + throw RangeError.range(index, 0, _ws.offset - 1, 'index'); + } + + _checkRange(value, 0, 255, 'Uint8'); + _ws.list[index] = value; + } + /// Writes a byte at the specified [position] without changing the current /// write position. /// - /// This is useful for overwriting data at a known offset (e.g., updating a - /// length field after writing the payload). + /// Used to overwrite data at a previously written offset (e.g., + /// updating a length field). /// - /// Throws [RangeError] if [position] is negative or exceeds [bytesWritten]. + /// This is a functional alias for `operator []=`. + /// + /// Throws [RangeError] if [position] is negative or greater than or equal to + /// [bytesWritten]. /// /// Example: /// ```dart /// writer.writeUint32(10); // Write length placeholder /// writer.writeString('data'); /// writer.writeUint8At(0, 15); // Overwrite length at position 0 + /// // or: writer[0] = 15; /// ``` @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void writeUint8At(int position, int value) { - if (position < 0 || position > _ws.offset) { - throw RangeError.range(position, 0, _ws.offset, 'position'); - } - - _checkRange(value, 0, 255, 'Uint8'); - _ws.list[position] = value; - if (position == _ws.offset) { - _ws.offset = position + 1; - } - } + void writeUint8At(int position, int value) => this[position] = value; /// Resets the writer to its initial state, discarding all written data. @pragma('vm:prefer-inline') diff --git a/lib/src/binary_writer_pool.dart b/lib/src/binary_writer_pool.dart index 39f4ce3..5ae17ae 100644 --- a/lib/src/binary_writer_pool.dart +++ b/lib/src/binary_writer_pool.dart @@ -9,8 +9,9 @@ part of 'binary_writer.dart'; /// /// ## Features /// - **Automatic reuse:** [acquire] gets a pooled writer or creates a new one -/// - **Memory bounds:** Only reuses writers with buffers ≤ 64 KiB -/// - **Size limits:** Maintains max 32 pooled instances +/// - **Memory bounds:** Only reuses writers with +/// buffers ≤ `maxReusableCapacity` +/// - **Size limits:** Maintains max `maxPoolSize` pooled instances /// - **Safe:** Prevents double-release and handles edge cases /// /// ## Usage Pattern @@ -40,29 +41,88 @@ part of 'binary_writer.dart'; /// ## Performance Considerations /// - Pooling is beneficial for high-frequency write operations /// - Overhead is minimal for single-use writers (use regular constructor) -/// - Large buffers (>64 KiB) are discarded to avoid memory waste +/// - Large buffers (>64 KiB by default) are discarded to avoid memory waste /// /// ## Memory Management -/// - Pool max size: 32 writers -/// - Max reusable buffer: 64 KiB -/// - Default buffer size: 1 KiB +/// - Default pool max size: 32 writers +/// - Default max reusable buffer: 64 KiB +/// - Default initial buffer size: 1 KiB +/// - Use [configure] to change these limits /// - Use [clear] to free pooled memory explicitly /// /// See also: [BinaryWriter], [stats] for pool monitoring // ignore: avoid_classes_with_only_static_members abstract final class BinaryWriterPool { + /// Configures the pool settings. + /// + /// This should typically be called once at application startup. + /// + /// Parameters: + /// - [maxPoolSize]: Maximum number of writers to keep in the pool + /// (default: 32). + /// - [initialBufferSize]: Default initial buffer size for new writers + /// (default: 1 KiB). + /// - [maxReusableCapacity]: Maximum buffer capacity allowed for pooling + /// (default: 64 KiB). Writers exceeding this are discarded on release. + /// + /// Example: + /// ```dart + /// // Configure for heavy load with larger buffers + /// BinaryWriterPool.configure( + /// maxPoolSize: 64, + /// maxReusableCapacity: 256 * 1024, + /// ); + /// ``` + static void configure({ + int maxPoolSize = 32, + int initialBufferSize = 1024, + int maxReusableCapacity = 64 * 1024, + }) { + if (maxPoolSize <= 0) { + throw ArgumentError.value(maxPoolSize, 'maxPoolSize', 'Must be positive'); + } + + if (initialBufferSize <= 0) { + throw ArgumentError.value( + initialBufferSize, + 'initialBufferSize', + 'Must be positive', + ); + } + + if (maxReusableCapacity <= 0) { + throw ArgumentError.value( + maxReusableCapacity, + 'maxReusableCapacity', + 'Must be positive', + ); + } + + if (initialBufferSize > maxReusableCapacity) { + throw ArgumentError( + 'initialBufferSize ($initialBufferSize) cannot be larger than ' + 'maxReusableCapacity ($maxReusableCapacity). ' + 'This would cause all pooled writers to be discarded immediately.', + ); + } + + _maxPoolSize = maxPoolSize; + _initialBufferSize = initialBufferSize; + _maxReusableCapacity = maxReusableCapacity; + } + // The internal pool of reusable writer states. static final _pool = <_WriterState>[]; /// Maximum number of writers to keep in the pool. - static const _maxPoolSize = 32; + static var _maxPoolSize = 32; /// Default initial buffer size for new writers (1 KiB). - static const _initialBufferSizer = 1024; + static var _initialBufferSize = 1024; /// Maximum buffer capacity allowed for pooling (64 KiB). - /// Writers that exceed this size are discarded to free up system memory - static const int _maxReusableCapacity = 64 * 1024; + /// Writers that exceed this size are discarded to free up system memory. + static var _maxReusableCapacity = 64 * 1024; // Performance counters static var _acquireHit = 0; @@ -83,36 +143,67 @@ abstract final class BinaryWriterPool { /// /// There are two ways to get the data: /// 1. Use [BinaryWriter.toBytes] if you consume data **inside** the try - /// block (zero-copy view). - /// 2. Use [BinaryWriter.takeBytes] if you need to **return** the data - /// (transfers buffer ownership). + /// block (zero-copy view). This is the fastest method but the view + /// becomes invalid if the writer is reused. + /// 2. Use [BinaryWriter.takeBytes] with `copy: true` if you need to + /// **return** the data. This copies the written bytes but **retains** + /// the internal buffer for the pool, preventing future re-allocations. + /// 3. Use [BinaryWriter.takeBytes] with `copy: false` (default) for a + /// zero-copy transfer of ownership. This detaches the buffer from the + /// writer, causing the pool to allocate a new buffer next time. /// /// ```dart /// final writer = BinaryWriterPool.acquire(); /// try { /// writer.writeUint32(123); - /// return writer.takeBytes(); + /// return writer.takeBytes(copy: true); // Recommended for pooling /// } finally { /// BinaryWriterPool.release(writer); /// } /// ``` /// /// Returns: A [BinaryWriter] ready for use. - static BinaryWriter acquire([int initialBufferSizer = _initialBufferSizer]) { - if (initialBufferSizer <= 0) { + static BinaryWriter acquire([int? initialBufferSize]) { + final size = initialBufferSize ?? _initialBufferSize; + + if (size <= 0) { throw RangeError.value( - initialBufferSizer, - 'initialBufferSizer', + size, + 'initialBufferSize', 'Must be positive', ); } if (_pool.isNotEmpty) { + // Find the best-fitting buffer: smallest one that is >= requested size. + // If none, take the largest available to minimize expansions. + var bestIndex = -1; + var smallestSuitableCapacity = double.infinity; + + for (var i = 0; i < _pool.length; i++) { + final cap = _pool[i].capacity; + if (cap >= size && cap < smallestSuitableCapacity) { + bestIndex = i; + smallestSuitableCapacity = cap.toDouble(); + } + } + + // If no suitable buffer found, take the largest one to minimize growth + if (bestIndex == -1) { + var largestCap = -1; + for (var i = 0; i < _pool.length; i++) { + if (_pool[i].capacity > largestCap) { + largestCap = _pool[i].capacity; + bestIndex = i; + } + } + } + _acquireHit++; - final state = _pool.removeLast().._isInPool = false; + final state = _pool.removeAt(bestIndex).._isInPool = false; - if (state.capacity < initialBufferSizer) { - state.ensureSize(initialBufferSizer); + if (state.capacity < size) { + state.ensureSize(size); } return BinaryWriter._(state); @@ -120,7 +211,7 @@ abstract final class BinaryWriterPool { _acquireMiss++; - return BinaryWriter(initialBufferSize: initialBufferSizer); + return BinaryWriter(initialBufferSize: size); } /// Acquires a writer, executes the given [action], and automatically @@ -131,21 +222,21 @@ abstract final class BinaryWriterPool { /// /// Parameters: /// - [action]: The function to execute with the acquired writer - /// - [initialBufferSizer]: Initial buffer size for new writers - /// (defaults to 1 KiB) + /// - [initialBufferSize]: Initial buffer size for new writers + /// (defaults to pool setting) /// /// Example: /// ```dart /// final bytes = BinaryWriterPool.withWriter((writer) { /// writer.writeUint32(42); - /// return writer.takeBytes(); + /// return writer.takeBytes(copy: true); /// }); /// ``` static T withWriter( T Function(BinaryWriter writer) action, [ - int initialBufferSizer = _initialBufferSizer, + int? initialBufferSize, ]) { - final writer = acquire(initialBufferSizer); + final writer = acquire(initialBufferSize); try { return action(writer); } finally { @@ -156,23 +247,16 @@ abstract final class BinaryWriterPool { /// Returns a [BinaryWriter] to the pool for future reuse. /// /// The writer is reset (offset cleared) and stored for future [acquire] - /// calls. Writers with buffers larger than 64 KiB are not pooled to avoid - /// long-term memory retention. + /// calls. Writers with buffers larger than `maxReusableCapacity` are not + /// pooled to avoid long-term memory retention. /// /// **Safe to call multiple times** (duplicate releases are ignored). /// - /// Only writers with capacity ≤ 64 KiB are pooled. Writers exceeding this - /// limit are discarded, allowing the buffer to be garbage collected. + /// Only writers with capacity ≤ [_maxReusableCapacity] are pooled. + /// Writers exceeding this limit are discarded, allowing the buffer to be + /// garbage collected. /// - /// **Do NOT use the writer after releasing it:** - /// - /// ```dart - /// final writer = BinaryWriterPool.acquire(); - /// writer.writeUint32(42); - /// final bytes = writer.toBytes(); - /// BinaryWriterPool.release(writer); - /// // DON'T USE writer here - it's returned to the pool and may be reused! - /// ``` + /// **Do NOT use the writer after releasing it.** /// /// Parameters: /// - [writer]: The [BinaryWriter] to return to the pool @@ -185,7 +269,6 @@ abstract final class BinaryWriterPool { } // Only pool writers with reasonable buffer sizes - // Prevents memory bloat from occasional large allocations if (state.capacity <= _maxReusableCapacity && _pool.length < _maxPoolSize) { state ..offset = 0 @@ -210,7 +293,7 @@ abstract final class BinaryWriterPool { /// Returns a map with keys: /// - `'pooled'`: Number of writers currently in the pool /// - `'maxPoolSize'`: Maximum pool capacity - /// - `'initialBufferSizer'`: Initial buffer size for new writers + /// - `'initialBufferSize'`: Initial buffer size for new writers /// - `'maxReusableCapacity'`: Maximum buffer size for pooling /// - `'acquireHit'`: Number of successful reuses from pool /// - `'acquireMiss'`: Number of new writer allocations @@ -221,13 +304,13 @@ abstract final class BinaryWriterPool { /// Example: /// ```dart /// final stats = BinaryWriterPool.stats; - /// print('Pooled writers: ${stats.pooled}'); // 5 - /// print('Hit rate: ${stats.acquireHit / (stats.acquireHit + stats.acquireMiss)}'); + /// print('Pooled writers: ${stats.pooled}'); + /// print('Hit rate: ${stats.hitRate}'); /// ``` static PoolStatistics get stats => PoolStatistics({ 'pooled': _pool.length, 'maxPoolSize': _maxPoolSize, - 'initialBufferSizer': _initialBufferSizer, + 'initialBufferSize': _initialBufferSize, 'maxReusableCapacity': _maxReusableCapacity, 'acquireHit': _acquireHit, 'acquireMiss': _acquireMiss, @@ -244,19 +327,7 @@ abstract final class BinaryWriterPool { /// - Handle memory pressure /// /// After clearing, subsequent [acquire] calls will create new writers. - /// - /// Example: - /// ```dart - /// BinaryWriterPool.clear(); // All pooled writers discarded - /// ``` static void clear() { - // Assist GC by breaking links to potentially large byte buffers - for (var i = 0; i < _pool.length; i++) { - _pool[i] - ..list = Uint8List(0) - ..data = ByteData(0) - .._isInPool = false; - } _pool.clear(); _acquireHit = 0; _acquireMiss = 0; @@ -274,7 +345,7 @@ extension type PoolStatistics(Map _stats) { int get maxPoolSize => _stats['maxPoolSize']!; /// Initial buffer size for new writers. - int get initialBufferSizer => _stats['initialBufferSizer']!; + int get initialBufferSize => _stats['initialBufferSize']!; /// Maximum buffer size for pooling. int get maxReusableCapacity => _stats['maxReusableCapacity']!; diff --git a/lib/src/internal.dart b/lib/src/internal.dart new file mode 100644 index 0000000..7f3aa68 --- /dev/null +++ b/lib/src/internal.dart @@ -0,0 +1,4 @@ +import 'dart:typed_data'; + +/// Empty bytes cache +final emptyUintList_ = Uint8List(0); diff --git a/lib/src/stream/stream_binary_reader.dart b/lib/src/stream/stream_binary_reader.dart index c2e64b4..0bcba0e 100644 --- a/lib/src/stream/stream_binary_reader.dart +++ b/lib/src/stream/stream_binary_reader.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import '../binary_reader.dart'; import '../constants.dart'; +import '../internal.dart'; import 'transactional_reader.dart'; /// A reader designed for asynchronous streaming data that spans multiple @@ -355,7 +356,7 @@ extension type StreamBinaryReader._(_StreamReaderState _s) throw RangeError.value(length, 'length', 'Length must be non-negative'); } if (length == 0) { - return _emptyBytes; + return emptyUintList_; } _checkAvailable(length); @@ -586,6 +587,3 @@ final class _StreamReaderState extends ChunkedTransactionalState ..setRange(0, bookmarkCount, bookmarkReaderOffset); } } - -/// Empty bytes cache -final _emptyBytes = Uint8List(0); diff --git a/pubspec.yaml b/pubspec.yaml index c9a08e5..69d33a7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pro_binary description: Efficient binary serialization library for Dart. Encodes and decodes various data types. -version: 5.0.0 +version: 5.1.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues diff --git a/test/unit/binary_writer_buffer_test.dart b/test/unit/binary_writer_buffer_test.dart index e85a5ec..b89f098 100644 --- a/test/unit/binary_writer_buffer_test.dart +++ b/test/unit/binary_writer_buffer_test.dart @@ -68,17 +68,30 @@ void main() { expect(writer.bytesWritten, equals(0)); }); - test('capacity resets to initial size after takeBytes', () { + test('capacity resets to initial size after takeBytes (copy: false)', () { // Force expansion writer.writeBytes(Uint8List(200)); expect(writer.capacity, greaterThan(128)); - // takeBytes() resets to initial size (128) + // takeBytes() resets to initial size (128) by default writer.takeBytes(); expect(writer.capacity, equals(128)); expect(writer.bytesWritten, equals(0)); }); + test('capacity is retained after takeBytes(copy: true)', () { + // Force expansion + writer.writeBytes(Uint8List(200)); + final capacityBefore = writer.capacity; + expect(capacityBefore, greaterThan(128)); + + // takeBytes(copy: true) keeps the buffer + final bytes = writer.takeBytes(copy: true); + expect(bytes.length, equals(200)); + expect(writer.capacity, equals(capacityBefore)); + expect(writer.bytesWritten, equals(0)); + }); + test('capacity does not change with toBytes', () { writer.writeBytes(Uint8List(200)); final capacityBefore = writer.capacity; @@ -192,12 +205,31 @@ void main() { }); group('Memory efficiency', () { - test('takeBytes creates view not copy', () { + test('takeBytes(copy: false) creates view not copy', () { writer.writeUint32(0x12345678); final bytes = writer.takeBytes(); expect(bytes, isA()); expect(bytes.length, equals(4)); + + // It's a view, but the writer has a NEW buffer now. + // We can't easily prove it's a view of the OLD buffer without keeping + // a reference to the old buffer. + }); + + test('takeBytes(copy: true) creates copy', () { + writer.writeUint32(0x12345678); + final bytes = writer.takeBytes(copy: true); + + expect(bytes, isA()); + expect(bytes.length, equals(4)); + + // Modify the copy and check if writer's retained buffer is affected + bytes[0] = 0xFF; + writer.writeUint8(0x00); + // If it was a copy, the first byte of writer's current buffer + // (offset 0) should be 0x00 + expect(writer.toBytes()[0], equals(0x00)); }); test('toBytes creates view not copy', () { diff --git a/test/unit/binary_writer_navigation_test.dart b/test/unit/binary_writer_navigation_test.dart index c6d95c2..947157a 100644 --- a/test/unit/binary_writer_navigation_test.dart +++ b/test/unit/binary_writer_navigation_test.dart @@ -43,6 +43,7 @@ void main() { ..writeUint8(3) ..writeUint8At(1, 99); expect(writer.toBytes(), equals([1, 99, 3])); + expect(writer.bytesWritten, equals(3)); }); test('does not change current write position', () { @@ -51,7 +52,57 @@ void main() { ..writeUint8(2) ..writeUint8At(0, 99) ..writeUint8(3); + expect(writer.toBytes(), equals([99, 2, 3])); + expect(writer.bytesWritten, equals(3)); + }); + + test('throws for position at the end', () { + writer.writeUint8(1); + expect(() => writer.writeUint8At(1, 99), throwsRangeError); + }); + }); + + group('index operators', () { + test('operator [] returns byte at absolute position', () { + writer + ..writeUint8(10) + ..writeUint8(20) + ..writeUint8(30); + + expect(writer[0], equals(10)); + expect(writer[1], equals(20)); + expect(writer[2], equals(30)); + }); + + test('operator [] throws for invalid indices', () { + writer.writeUint8(10); + expect(() => writer[-1], throwsRangeError); + expect(() => writer[1], throwsRangeError); + }); + + test('operator []= overwrites existing byte', () { + writer + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3); + + final initialOffset = writer.bytesWritten; + writer[1] = 99; + expect(writer.toBytes(), equals([1, 99, 3])); + expect(writer[1], equals(99)); + expect(writer.bytesWritten, equals(initialOffset)); + }); + + test('operator []= throws even when writing at the end', () { + writer.writeUint8(1); // [1], offset 1 + expect(() => writer[1] = 2, throwsRangeError); + }); + + test('operator []= throws for invalid indices', () { + writer.writeUint8(10); + expect(() => writer[-1] = 20, throwsRangeError); + expect(() => writer[2] = 20, throwsRangeError); }); }); }); diff --git a/test/unit/binary_writer_pool_test.dart b/test/unit/binary_writer_pool_test.dart index 3ae827f..98ee62c 100644 --- a/test/unit/binary_writer_pool_test.dart +++ b/test/unit/binary_writer_pool_test.dart @@ -78,5 +78,52 @@ void main() { expect(largerWriter.capacity, greaterThanOrEqualTo(1000)); BinaryWriterPool.release(largerWriter); }); + + group('configure', () { + test('sets pool limits correctly', () { + BinaryWriterPool.configure( + maxPoolSize: 10, + initialBufferSize: 512, + maxReusableCapacity: 2048, + ); + + final stats = BinaryWriterPool.stats; + expect(stats.maxPoolSize, equals(10)); + expect(stats.initialBufferSize, equals(512)); + expect(stats.maxReusableCapacity, equals(2048)); + }); + + test('throws ArgumentError for invalid values', () { + expect( + () => BinaryWriterPool.configure(maxPoolSize: 0), + throwsArgumentError, + ); + expect( + () => BinaryWriterPool.configure(initialBufferSize: 0), + throwsArgumentError, + ); + expect( + () => BinaryWriterPool.configure(maxReusableCapacity: 0), + throwsArgumentError, + ); + expect( + () => BinaryWriterPool.configure(maxPoolSize: -1), + throwsArgumentError, + ); + }); + + test( + 'throws ArgumentError if initialBufferSize > maxReusableCapacity', + () { + expect( + () => BinaryWriterPool.configure( + initialBufferSize: 2048, + maxReusableCapacity: 1024, + ), + throwsArgumentError, + ); + }, + ); + }); }); } From a627358f77b85a042d88d76320f2908eba16ae7d Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:33:17 +0300 Subject: [PATCH 2/6] test: improvements --- test/unit/binary_writer_basic_test.dart | 83 +++++++ test/unit/binary_writer_buffer_test.dart | 267 ++++++++--------------- 2 files changed, 173 insertions(+), 177 deletions(-) diff --git a/test/unit/binary_writer_basic_test.dart b/test/unit/binary_writer_basic_test.dart index c3c9ea2..23c7aa6 100644 --- a/test/unit/binary_writer_basic_test.dart +++ b/test/unit/binary_writer_basic_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; @@ -164,6 +166,87 @@ void main() { }); }); + test('writeInt64/readInt64 round-trip', () { + const values = [ + kMinInt64, + -1234567890123456, + -1, + 0, + 1, + 1234567890123456, + kMaxInt64, + ]; + + for (final value in values) { + // Big-endian + writer + ..reset() + ..writeInt64(value); + var reader = BinaryReader(writer.takeBytes()); + expect(reader.readInt64(), equals(value), reason: 'BE: $value'); + + // Little-endian + writer + ..reset() + ..writeInt64(value, Endian.little); + reader = BinaryReader(writer.takeBytes()); + expect( + reader.readInt64(Endian.little), + equals(value), + reason: 'LE: $value', + ); + } + }); + + test('writeUint64/readUint64 round-trip', () { + const values = [ + 0, + 1, + 1234567890123456, + kMaxInt64, // Dart int max (limited by signedness) + ]; + + for (final value in values) { + // Big-endian + writer + ..reset() + ..writeUint64(value); + var reader = BinaryReader(writer.takeBytes()); + expect(reader.readUint64(), equals(value), reason: 'BE: $value'); + + // Little-endian + writer + ..reset() + ..writeUint64(value, Endian.little); + reader = BinaryReader(writer.takeBytes()); + expect( + reader.readUint64(Endian.little), + equals(value), + reason: 'LE: $value', + ); + } + }); + + test('writeUint64 explicitly handles 9223372036854775807 (kMaxInt64)', () { + const value = 9223372036854775807; // kMaxInt64 + + // Big-endian: [0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] + writer + ..reset() + ..writeUint64(value); + final bytesBE = writer.takeBytes(); + expect(bytesBE, equals([0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])); + expect(BinaryReader(bytesBE).readUint64(), equals(value)); + + // Little-endian: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F] + writer + ..reset() + ..writeUint64(value, Endian.little); + final bytesLE = writer.takeBytes(); + expect(bytesLE, equals([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F])); + expect(BinaryReader(bytesLE).readUint64(Endian.little), equals(value)); + }); + test('allow reusing writer after takeBytes', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); diff --git a/test/unit/binary_writer_buffer_test.dart b/test/unit/binary_writer_buffer_test.dart index b89f098..37f2ad6 100644 --- a/test/unit/binary_writer_buffer_test.dart +++ b/test/unit/binary_writer_buffer_test.dart @@ -4,199 +4,101 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('BinaryWriter Buffer Management', () { + group('BinaryWriter Buffer', () { late BinaryWriter writer; setUp(() { writer = BinaryWriter(); }); - test('initial capacity is 128 bytes by default and aligned', () { + test('initial capacity is aligned to 64 bytes', () { expect(writer.capacity, equals(128)); - expect(writer.capacity % 64, equals(0)); - }); - test('capacity is aligned to 64-byte boundary on initialization', () { - // Test various sizes - final customWriter256 = BinaryWriter(initialBufferSize: 256); - expect(customWriter256.capacity, equals(256)); - expect(customWriter256.capacity % 64, equals(0)); - - // Size 50 should be aligned to 64 - final customWriter50 = BinaryWriter(initialBufferSize: 50); - expect(customWriter50.capacity, equals(64)); - expect(customWriter50.capacity % 64, equals(0)); - - // Size 100 should be aligned to 128 - final customWriter100 = BinaryWriter(initialBufferSize: 100); - expect(customWriter100.capacity, equals(128)); - expect(customWriter100.capacity % 64, equals(0)); + final writer2 = BinaryWriter(initialBufferSize: 10); + expect(writer2.capacity, equals(64)); }); - test('capacity increases after buffer expansion', () { - // Default capacity is 128 bytes - expect(writer.capacity, equals(128)); - - // Write data that exceeds initial capacity - final largeData = Uint8List(200); - writer.writeBytes(largeData); + test('expands buffer when writing beyond capacity', () { + final largeBytes = Uint8List(200); + writer.writeBytes(largeBytes); - // Capacity with 1.5x growth: need 200, 128 * 1.5 = 192 < 200, so use - // 200 aligned to 256 - expect(writer.capacity, equals(256)); + expect(writer.capacity, greaterThanOrEqualTo(200)); + expect(writer.bytesWritten, equals(200)); }); - test('capacity expands with 1.5x growth strategy', () { - final smallWriter = BinaryWriter(initialBufferSize: 64); - expect(smallWriter.capacity, equals(64)); + test('uses exponential growth for expansion', () { + final initialCapacity = writer.capacity; - // Write 100 bytes (exceeds initial 64) - // 64 * 1.5 = 96 < 100, so use 100 aligned to 128 - smallWriter.writeBytes(Uint8List(100)); + // Force expansion by 1 byte + final bytes = Uint8List(initialCapacity + 1); + writer.writeBytes(bytes); - expect(smallWriter.capacity, equals(128)); + // Growth factor is 1.5x, aligned to 64 + final expected = (initialCapacity + (initialCapacity >> 1) + 63) & ~63; + expect(writer.capacity, equals(expected)); }); - test('capacity resets to initial size after reset', () { - // Force expansion - writer.writeBytes(Uint8List(200)); - expect(writer.capacity, greaterThan(128)); - - // reset() should reset capacity back to initial size (128) - writer.reset(); - expect(writer.capacity, equals(128)); - expect(writer.bytesWritten, equals(0)); - }); - - test('capacity resets to initial size after takeBytes (copy: false)', () { - // Force expansion - writer.writeBytes(Uint8List(200)); - expect(writer.capacity, greaterThan(128)); - - // takeBytes() resets to initial size (128) by default - writer.takeBytes(); - expect(writer.capacity, equals(128)); - expect(writer.bytesWritten, equals(0)); - }); - - test('capacity is retained after takeBytes(copy: true)', () { - // Force expansion - writer.writeBytes(Uint8List(200)); - final capacityBefore = writer.capacity; - expect(capacityBefore, greaterThan(128)); - - // takeBytes(copy: true) keeps the buffer - final bytes = writer.takeBytes(copy: true); - expect(bytes.length, equals(200)); - expect(writer.capacity, equals(capacityBefore)); - expect(writer.bytesWritten, equals(0)); - }); + test('multiple expansions maintain data integrity', () { + for (var i = 0; i < 1000; i++) { + writer.writeUint32(i); + } - test('capacity does not change with toBytes', () { - writer.writeBytes(Uint8List(200)); - final capacityBefore = writer.capacity; + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); - // toBytes() should not change capacity - final bytes = writer.toBytes(); - expect(writer.capacity, equals(capacityBefore)); - expect(bytes.length, equals(200)); + for (var i = 0; i < 1000; i++) { + expect(reader.readUint32(), equals(i)); + } }); - test('capacity aligns to 64-byte boundary after expansion', () { - // Start with 128 bytes (already aligned to 64) - expect(writer.capacity, equals(128)); - expect(writer.capacity % 64, equals(0)); - - // Write 200 bytes -> requires 256 capacity (128 * 2) - // 256 is already aligned to 64, so capacity should be 256 + test('reset() clears offset but maintains capacity', () { + final initialCapacity = writer.capacity; writer.writeBytes(Uint8List(200)); - expect(writer.capacity, equals(256)); - expect(writer.capacity % 64, equals(0)); - }); - - test('capacity alignment happens on initialization and expansion', () { - final sizes = [1, 17, 33, 65, 99, 130]; - final expectedInitial = [64, 64, 64, 128, 128, 192]; - for (var i = 0; i < sizes.length; i++) { - final size = sizes[i]; - final expected = expectedInitial[i]; - final w = BinaryWriter(initialBufferSize: size); + final expandedCapacity = writer.capacity; + expect(expandedCapacity, greaterThan(initialCapacity)); - expect(w.capacity, equals(expected)); - expect(w.capacity % 64, equals(0)); - - w.writeBytes(Uint8List(w.capacity + 1)); - expect(w.capacity % 64, equals(0)); - } - }); - - test('capacity alignment calculation is correct', () { - final testCases = { - 1: 64, - 63: 64, - 64: 64, - 65: 128, - 127: 128, - 128: 128, - 129: 192, - 255: 256, - 256: 256, - 257: 320, - }; - - for (final entry in testCases.entries) { - final unaligned = entry.key; - final aligned = entry.value; - final calculated = (unaligned + 63) & ~63; - expect(calculated, equals(aligned)); - } + writer.reset(); + expect(writer.bytesWritten, equals(0)); + // reset() actually re-initializes with initial size + expect(writer.capacity, equals(initialCapacity)); }); - group('toBytes', () { - test('return current buffer without resetting writer state', () { - writer - ..writeUint8(42) - ..writeUint8(100); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([42, 100])); + group('Pool interaction', () { + test('capacity resets to initial size after takeBytes (copy: false)', () { + writer.writeBytes(Uint8List(500)); + expect(writer.capacity, greaterThan(128)); - writer.writeUint8(200); - final bytes2 = writer.toBytes(); - expect(bytes2, equals([42, 100, 200])); + // takeBytes() resets to initial size (128) by default + writer.takeBytes(); + expect(writer.capacity, equals(128)); }); - test('preserve written data across toBytes calls', () { - writer.writeUint32(0x12345678); - - final bytes1 = writer.toBytes(); - expect(bytes1, equals([0x12, 0x34, 0x56, 0x78])); + test('capacity is retained after takeBytes(copy: true)', () { + writer.writeBytes(Uint8List(500)); + final expandedCapacity = writer.capacity; - writer.writeUint32(0xABCDEF00); - - final bytes2 = writer.toBytes(); - expect( - bytes2, - equals([0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x00]), - ); + // takeBytes(copy: true) keeps the buffer + writer.takeBytes(copy: true); + expect(writer.capacity, equals(expandedCapacity)); + expect(writer.bytesWritten, equals(0)); }); - }); - group('reset', () { - test('reset writer state without returning bytes', () { + test('sequential writes after takeBytes(copy: true)', () { writer - ..writeUint8(42) - ..writeUint8(100) - ..reset(); + ..writeUint8(10) + ..takeBytes(copy: true) + ..writeUint8(20) + ..writeUint8(30); - expect(writer.bytesWritten, equals(0)); - expect(writer.toBytes(), isEmpty); + expect(writer.toBytes(), equals([20, 30])); }); - test('allow writing new data after reset', () { + test('reset() during copy-mode maintains buffer', () { writer - ..writeUint8(42) + ..writeUint8(1) + ..takeBytes(copy: true) + ..writeUint8(2) ..reset() ..writeUint8(100); @@ -205,39 +107,50 @@ void main() { }); group('Memory efficiency', () { - test('takeBytes(copy: false) creates view not copy', () { - writer.writeUint32(0x12345678); - final bytes = writer.takeBytes(); + test('takeBytes(copy: false) returns a zero-copy view of the buffer', () { + writer.writeUint32(0x11223344); - expect(bytes, isA()); - expect(bytes.length, equals(4)); + // 1. Get a view of the internal buffer before taking bytes. + final internalView = writer.toBytes(); + + // 2. Take bytes (copy: false is default). This detaches the buffer. + final takenBytes = writer.takeBytes(); - // It's a view, but the writer has a NEW buffer now. - // We can't easily prove it's a view of the OLD buffer without keeping - // a reference to the old buffer. + // 3. Verify they look the same initially. + expect(takenBytes, equals(internalView)); + + // 4. Prove it's a view: modifying 'takenBytes' must affect + // 'internalView' because they share the same underlying memory. + takenBytes[0] = 0xFF; + expect(internalView[0], equals(0xFF)); }); - test('takeBytes(copy: true) creates copy', () { - writer.writeUint32(0x12345678); - final bytes = writer.takeBytes(copy: true); + test('takeBytes(copy: true) returns a deep copy of the buffer', () { + writer.writeUint32(0x11223344); - expect(bytes, isA()); - expect(bytes.length, equals(4)); - - // Modify the copy and check if writer's retained buffer is affected - bytes[0] = 0xFF; - writer.writeUint8(0x00); - // If it was a copy, the first byte of writer's current buffer - // (offset 0) should be 0x00 - expect(writer.toBytes()[0], equals(0x00)); + final internalView = writer.toBytes(); + final takenBytes = writer.takeBytes(copy: true); + + expect(takenBytes, equals(internalView)); + + // Modifying the copy should NOT affect the original internal buffer + // memory + takenBytes[0] = 0xFF; + expect(internalView[0], isNot(equals(0xFF))); }); - test('toBytes creates view not copy', () { + test('toBytes() creates view not copy', () { writer.writeUint64(123456789); final bytes = writer.toBytes(); expect(bytes, isA()); expect(bytes.length, equals(8)); + + // Modifying internal buffer affects toBytes() view + writer + ..seek(0) + ..writeUint8(0xFF); + expect(bytes[0], equals(0xFF)); }); }); }); From c8fcafa3f58c7294672200bfb741fba4c1b49341 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:35:38 +0300 Subject: [PATCH 3/6] test: improvements --- test/unit/binary_writer_basic_test.dart | 44 ++++++++++++++++++++++++ test/unit/binary_writer_string_test.dart | 30 ++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/test/unit/binary_writer_basic_test.dart b/test/unit/binary_writer_basic_test.dart index 23c7aa6..061eb1f 100644 --- a/test/unit/binary_writer_basic_test.dart +++ b/test/unit/binary_writer_basic_test.dart @@ -166,6 +166,50 @@ void main() { }); }); + test('writeFloat32/readFloat32 special values round-trip', () { + final values = [ + double.nan, + double.infinity, + double.negativeInfinity, + ]; + + for (final value in values) { + writer + ..reset() + ..writeFloat32(value); + final reader = BinaryReader(writer.takeBytes()); + final result = reader.readFloat32(); + + if (value.isNaN) { + expect(result.isNaN, isTrue, reason: 'Value should be NaN'); + } else { + expect(result, equals(value), reason: 'Value should be $value'); + } + } + }); + + test('writeFloat64/readFloat64 special values round-trip', () { + final values = [ + double.nan, + double.infinity, + double.negativeInfinity, + ]; + + for (final value in values) { + writer + ..reset() + ..writeFloat64(value); + final reader = BinaryReader(writer.takeBytes()); + final result = reader.readFloat64(); + + if (value.isNaN) { + expect(result.isNaN, isTrue, reason: 'Value should be NaN'); + } else { + expect(result, equals(value), reason: 'Value should be $value'); + } + } + }); + test('writeInt64/readInt64 round-trip', () { const values = [ kMinInt64, diff --git a/test/unit/binary_writer_string_test.dart b/test/unit/binary_writer_string_test.dart index 25b0a12..9b80c31 100644 --- a/test/unit/binary_writer_string_test.dart +++ b/test/unit/binary_writer_string_test.dart @@ -45,6 +45,17 @@ void main() { }); group('Lone surrogate pairs', () { + test('writeString defaults to allowMalformed=true', () { + const testStr = 'Before\uD800After'; + // Should not throw + writer.writeString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readString(bytes.length, allowMalformed: true); + expect(result, contains('\uFFFD')); + }); + test( 'writeString handles lone high surrogate with allowMalformed=true', () { @@ -132,6 +143,25 @@ void main() { ); }, ); + + test('writeVarString defaults to allowMalformed=true', () { + const testStr = 'Before\uD800After'; + // Should not throw + writer.writeVarString(testStr); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readVarString(allowMalformed: true); + expect(result, contains('\uFFFD')); + }); + + test('writeVarString respects allowMalformed=false', () { + const testStr = 'Before\uD800After'; + expect( + () => writer.writeVarString(testStr, allowMalformed: false), + throwsA(isA()), + ); + }); }); group('Very large strings', () { From 3f64303b958dd529d944c0f81b49a7f51cb7f826 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:42:18 +0300 Subject: [PATCH 4/6] tests: improvements --- performance/strings_bench.dart | 2 +- test/stream/stream_binary_reader_test.dart | 4 +- test/unit/binary_reader_string_test.dart | 4 +- test/unit/binary_writer_string_test.dart | 64 +++++++++++++++++++++- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/performance/strings_bench.dart b/performance/strings_bench.dart index 3cdf9f8..f7d938d 100644 --- a/performance/strings_bench.dart +++ b/performance/strings_bench.dart @@ -139,7 +139,7 @@ class FixedStringBench extends BenchmarkBase { void runComparison(String name, String payload) { OnePassStringBench(name, payload).report(); - FixedStringBench(name, payload, LengthEncoding.u32).report(); + FixedStringBench(name, payload, .u32).report(); TwoPassStringBench(name, payload).report(); StandardDartCorrectBench(name, payload).report(); StandardDartNaiveBench(name, payload).report(); diff --git a/test/stream/stream_binary_reader_test.dart b/test/stream/stream_binary_reader_test.dart index 19874e6..7468e1a 100644 --- a/test/stream/stream_binary_reader_test.dart +++ b/test/stream/stream_binary_reader_test.dart @@ -78,7 +78,7 @@ void main() { test('readStringFixed handles chunk boundary', () { final writer = BinaryWriter() - ..writeStringFixed('Streaming', lengthEncoding: LengthEncoding.u16); + ..writeStringFixed('Streaming', lengthEncoding: .u16); final bytes = writer.takeBytes(); reader @@ -86,7 +86,7 @@ void main() { ..addChunk(bytes.sublist(5)); expect( - reader.readStringFixed(lengthEncoding: LengthEncoding.u16), + reader.readStringFixed(lengthEncoding: .u16), equals('Streaming'), ); }); diff --git a/test/unit/binary_reader_string_test.dart b/test/unit/binary_reader_string_test.dart index 8067d76..cf24ac8 100644 --- a/test/unit/binary_reader_string_test.dart +++ b/test/unit/binary_reader_string_test.dart @@ -73,7 +73,7 @@ void main() { final buffer = Uint8List.fromList([0, 0, 0, 0]); final reader = BinaryReader(buffer); expect( - reader.readStringFixed(lengthEncoding: LengthEncoding.u32), + reader.readStringFixed(lengthEncoding: .u32), equals(''), ); }); @@ -118,7 +118,7 @@ void main() { ]); final reader = BinaryReader(buffer); expect( - reader.readStringFixed(lengthEncoding: LengthEncoding.u64), + reader.readStringFixed(lengthEncoding: .u64), equals('DART'), ); }); diff --git a/test/unit/binary_writer_string_test.dart b/test/unit/binary_writer_string_test.dart index 9b80c31..3908f45 100644 --- a/test/unit/binary_writer_string_test.dart +++ b/test/unit/binary_writer_string_test.dart @@ -164,6 +164,64 @@ void main() { }); }); + group('Fixed-length strings (FixedString)', () { + test( + 'writeStringFixed throws RangeError if length exceeds u8 capacity', + () { + final longStr = 'A' * 256; + expect( + () => writer.writeStringFixed(longStr), + throwsRangeError, + ); + }, + ); + + test( + 'writeStringFixed throws RangeError if length exceeds u16 capacity', + () { + final longStr = 'A' * 65536; + expect( + () => writer.writeStringFixed( + longStr, + lengthEncoding: .u16, + ), + throwsRangeError, + ); + }, + ); + + test( + 'readStringFixed throws RangeError if declared length > ' + 'remaining bytes', + () { + writer + ..writeUint8(10) // Declared length 10 + ..writeString('123'); // Actual data 3 bytes + + final reader = BinaryReader(writer.takeBytes()); + expect( + reader.readStringFixed, + throwsRangeError, + ); + }, + ); + + test('readStringFixed works for all length encodings', () { + const testStr = 'Hello'; + for (final encoding in LengthEncoding.values) { + writer + ..reset() + ..writeStringFixed(testStr, lengthEncoding: encoding); + + final reader = BinaryReader(writer.takeBytes()); + expect( + reader.readStringFixed(lengthEncoding: encoding), + equals(testStr), + ); + } + }); + }); + group('Very large strings', () { test('writeString with string exceeding initial buffer size', () { final writer = BinaryWriter(initialBufferSize: 8); @@ -346,12 +404,12 @@ void main() { }); test('write with LengthEncoding.u16', () { - writer.writeStringFixed('ABC', lengthEncoding: LengthEncoding.u16); + writer.writeStringFixed('ABC', lengthEncoding: .u16); expect(writer.takeBytes(), equals([0, 3, 65, 66, 67])); }); test('write empty string with LengthEncoding.u32', () { - writer.writeStringFixed('', lengthEncoding: LengthEncoding.u32); + writer.writeStringFixed('', lengthEncoding: .u32); expect(writer.takeBytes(), equals([0, 0, 0, 0])); }); @@ -363,7 +421,7 @@ void main() { }); test('write with LengthEncoding.u64', () { - writer.writeStringFixed('DART', lengthEncoding: LengthEncoding.u64); + writer.writeStringFixed('DART', lengthEncoding: .u64); final bytes = writer.takeBytes(); expect(bytes.length, equals(8 + 4)); expect(bytes.sublist(0, 8), equals([0, 0, 0, 0, 0, 0, 0, 4])); From 1707569a1dd1dbe78814a3efdae305a189275ee3 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:43:25 +0300 Subject: [PATCH 5/6] test: improvements --- .../stream_binary_reader_coverage_test.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/stream/stream_binary_reader_coverage_test.dart b/test/stream/stream_binary_reader_coverage_test.dart index e3d96f3..1ed1929 100644 --- a/test/stream/stream_binary_reader_coverage_test.dart +++ b/test/stream/stream_binary_reader_coverage_test.dart @@ -148,9 +148,23 @@ void main() { expect(reader.readUint8, throwsA(isA())); }); - test('rollback handles no current reader', () { + test('rollback handles no current reader and maintains state', () { + final bytes = Uint8List.fromList([1, 2, 3]); + reader.addChunk(bytes); + + final initialAvailable = reader.availableBytes; + expect(initialAvailable, equals(3)); + reader.bookmark(); + // Rollback without consuming any data expect(() => reader.rollback(), returnsNormally); + + // State should be identical + expect(reader.availableBytes, equals(initialAvailable)); + + // Should still be able to read correctly + expect(reader.readUint8(), equals(1)); + expect(reader.availableBytes, equals(2)); }); test('readVarUint throws FormatException for oversized varint', () { From 6216ff57fba5d342e052b687bc097ca5fa368ac3 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 29 May 2026 12:44:09 +0300 Subject: [PATCH 6/6] upd: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f171d18..5112f38 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ```yaml dependencies: - pro_binary: ^5.0.0 + pro_binary: ^5.1.0 ``` ## Quick Start