diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 2f7cb3f76..dc79dbb6c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -12,5 +12,6 @@ export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; export 'occurrence_search_result.dart'; +export 'occurrence_trie.dart'; export 'signal_occurrence.dart'; export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart new file mode 100644 index 000000000..f6d65b2cb --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,104 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie.dart +// Compact storage for values keyed by hierarchy occurrence addresses. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// A prefix-sharing map from [OccurrenceAddress] values to values of type [T]. +/// +/// Common address prefixes are stored once, making this more compact than a +/// conventional map when many values belong to the same hierarchy subtree. +class OccurrenceTrie { + /// The root node, which represents [OccurrenceAddress.root]. + final _OccurrenceTrieNode _root = _OccurrenceTrieNode(); + + /// Whether this trie contains no values. + bool get isEmpty => _root.isEmpty; + + /// The value stored at [address], if any. + T? operator [](OccurrenceAddress address) { + var node = _root; + for (final index in _validatedPath(address)) { + final child = node.children[index]; + if (child == null) { + return null; + } + node = child; + } + return node.value; + } + + /// Associates [value] with [address]. + void operator []=(OccurrenceAddress address, T value) { + var node = _root; + for (final index in _validatedPath(address)) { + node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); + } + node.value = value; + } + + /// Removes and returns the value stored at [address], if any. + T? remove(OccurrenceAddress address) { + final path = _validatedPath(address); + final nodes = <_OccurrenceTrieNode>[_root]; + var node = _root; + for (final index in path) { + final child = node.children[index]; + if (child == null) { + return null; + } + nodes.add(child); + node = child; + } + + final previous = node.value; + if (previous == null) { + return null; + } + node.value = null; + for (var index = path.length - 1; index >= 0; index--) { + final child = nodes[index + 1]; + if (!child.isEmpty) { + break; + } + nodes[index].children.remove(path[index]); + } + return previous; + } + + /// Removes every value from this trie. + void clear() { + _root + ..value = null + ..children.clear(); + } + + /// Returns [address]'s valid, non-negative path. + static List _validatedPath(OccurrenceAddress address) { + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'An occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +/// A node in an [OccurrenceTrie]. +class _OccurrenceTrieNode { + /// Descendants indexed by their address path component. + final Map> children = {}; + + /// The value stored at this node, if one has been assigned. + T? value; + + /// Whether this node has neither a value nor descendants. + bool get isEmpty => value == null && children.isEmpty; +} diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart new file mode 100644 index 000000000..7a3bbf678 --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie_test.dart +// Tests for compact occurrence-address trie storage. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + test('stores values with shared occurrence-address prefixes', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + + trie[first] = 'first'; + trie[OccurrenceAddress.root] = 'root'; + trie[second] = 'second'; + + expect(trie[OccurrenceAddress.root], 'root'); + expect(trie[first], 'first'); + expect(trie[second], 'second'); + expect(trie[const OccurrenceAddress([0, 2, 6])], isNull); + }); + + test('prunes an address branch after removing its final value', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + trie[first] = 'first'; + trie[second] = 'second'; + + expect(trie.remove(first), 'first'); + expect(trie[first], isNull); + expect(trie[second], 'second'); + expect(trie.remove(second), 'second'); + expect(trie.isEmpty, isTrue); + }); + + test('accepts root addresses and rejects negative path indices', () { + final trie = OccurrenceTrie(); + + trie[OccurrenceAddress.root] = 'root'; + expect(trie[OccurrenceAddress.root], 'root'); + expect( + () => trie[const OccurrenceAddress([0, -1])], + throwsArgumentError, + ); + }); +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md index e40b328bb..fd2abee35 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -26,6 +26,48 @@ across DevTools packages. - ROHD extension client/status abstractions: `RohdExtensionClient`, `NullExtensionClient`, `RohdModuleInfo`, and `RohdFormatInfo`. +## Widgets & Utilities + +### UI Controls & Buttons + +- **`MarkdownHelpButton`** — A help button that displays Markdown content from an asset file in a dialog. Supports tooltip text and rich formatting. + +- **`ExportPngButton`** — A camera icon button for triggering PNG export functionality. Includes customizable tooltip text. + +- **`CrossProbeButton`** — A toolbar button for toggling cross-probing between viewers. Shows a bidirectional arrows icon that reflects the active/inactive state. + +### Overlays & Layout + +- **`AppBarOverlay`** — An auto-hiding AppBar that slides in from the top edge when the mouse approaches. When disabled, behaves like a standard AppBar. + +### Export & Capture + +- **`captureBoundaryToPng`** — Captures a `RepaintBoundary` as PNG and saves or downloads it. + +- **`showExportToast`** — Shows export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`buildGotoSourceMenuItems`** — Builds source-navigation menu items for ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`expandLogicType`**, **`formatFieldValue`**, and **`formatTypeTooltip`** — Format ROHD logic types and values for display. + +- **`BitFieldDef`**, **`showBitRangeDialog`**, and **`showDefineBitFieldsDialog`** — Define and edit bit-field ranges. + +- **`buildBitExpansionMenuItems`** and **`resolveBitExpansionMenuValue`** — Build and resolve the "Expand Bits" and "Define Bit Fields" actions used across signal selection overlays and panels. + +- **`SignalValueFormatRegistry`** — Shared registry for signal display-format preferences, allowing consistent formatting across multiple viewers. + +### Extension Integration + +- **`RohdExtensionClient`** — Abstract interface for querying the ROHD VS Code extension. Supports multiple implementations (DevTools, VS Code webview, offline mode). + +- **`RohdSourceFormat`** and **`RohdFormatInfo`** — Describe source formats and their availability. + ## Usage Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index 452567fae..ca31b9ace 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -36,6 +36,9 @@ export 'src/bit_field_utils.dart'; // Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers export 'src/bit_expansion_menu.dart'; +// Shared signal display-format preferences and value formatting +export 'src/signal_value_format_registry.dart'; + // ROHD extension client export 'src/rohd_extension_status.dart'; export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart new file mode 100644 index 000000000..7b1c0e3e9 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,293 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry.dart +// Shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:rohd/rohd.dart' + show LogicValue, LogicValueConstructionException; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show OccurrenceAddress, OccurrenceTrie; + +/// The available display formats for signal values. +enum SignalValueFormat { + /// The source waveform representation. + waveform, + + /// A binary representation. + binary, + + /// A hexadecimal representation. + hexadecimal, + + /// An unsigned decimal representation. + unsignedDecimal, + + /// A two's-complement signed decimal representation. + signedDecimal, + + /// An octal representation. + octal, + + /// An ASCII representation. + ascii, +} + +/// A format preference for one signal occurrence address. +class SignalValueFormatPreference { + /// Creates a preference for [address] using [format]. + SignalValueFormatPreference( + this.address, + this.format, + ); + + /// The occurrence address, including the signal index. + final OccurrenceAddress address; + + /// The selected display format. + final SignalValueFormat format; +} + +/// Shared display-format preferences keyed by occurrence address. +/// +/// Viewer packages publish [SignalValueFormat] values; embedded surfaces use +/// the same values without depending on viewer-local format enums. +class SignalValueFormatRegistry { + /// Prevents instantiation. + SignalValueFormatRegistry._(); + + /// Matches binary digits, including unknown values. + static final RegExp _binaryDigits = RegExp(r'^[01xz]+$'); + + /// Matches hexadecimal digits, including unknown values. + static final RegExp _hexadecimalDigits = RegExp(r'^[0-9a-fxz]+$'); + + /// Matches a character that distinguishes hexadecimal from decimal. + static final RegExp _hexadecimalMarker = RegExp(r'[a-fxz]'); + + /// Stores the registered preference for each signal occurrence. + static OccurrenceTrie _formatTrie = + OccurrenceTrie(); + + /// Tracks registry changes. + static final ValueNotifier _changes = ValueNotifier(0); + + /// Notifies listeners whenever occurrence-format preferences change. + static ValueListenable get changes => _changes; + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + final replacement = OccurrenceTrie(); + for (final preference in preferences) { + _validateSignalAddress(preference.address); + replacement[preference.address] = preference.format; + } + _formatTrie = replacement; + _notifyListeners(); + } + + /// Removes all occurrence-format preferences. + static void clear() { + if (_formatTrie.isEmpty) { + return; + } + _formatTrie.clear(); + _notifyListeners(); + } + + /// Sets [format] for each signal occurrence in [addresses]. + static void setFormatFor( + Iterable addresses, + SignalValueFormat format, + ) { + final requestedAddresses = addresses.toList(growable: false); + for (final address in requestedAddresses) { + _validateSignalAddress(address); + } + + var changed = false; + for (final address in requestedAddresses) { + final previous = _formatTrie[address]; + _formatTrie[address] = format; + changed = (previous != format) || changed; + } + if (changed) { + _notifyListeners(); + } + } + + /// Converts a serialized format name to its corresponding enum value. + /// + /// Returns `null` when [value] is not a known format name. + static SignalValueFormat? formatFromString(String value) { + for (final format in SignalValueFormat.values) { + if (format.name == value) { + return format; + } + } + return null; + } + + /// Converts [format] to its serialized format name. + static String formatToString(SignalValueFormat format) => format.name; + + /// Returns the requested format for [address], or the waveform default. + static SignalValueFormat formatFor(OccurrenceAddress address) { + return formatForAny([address]); + } + + /// Returns the first registered format matching [addresses]. + static SignalValueFormat formatForAny( + Iterable addresses, { + SignalValueFormat fallback = SignalValueFormat.waveform, + }) { + for (final address in addresses) { + if (address == null) { + continue; + } + _validateSignalAddress(address); + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + /// Increments the registry change generation after a successful mutation. + static void _notifyListeners() => _changes.value++; + + /// Ensures [address] identifies a signal rather than an occurrence. + static void _validateSignalAddress(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + } + + /// Returns whether [value] contains unknown (`x` or `z`) digits. + static bool _containsUnknownDigits(String value) { + final lower = value.toLowerCase(); + final apostrophe = lower.indexOf("'"); + final digits = apostrophe > 0 && apostrophe + 2 <= lower.length + ? lower.substring(apostrophe + 2) + : lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + return digits.contains('x') || digits.contains('z'); + } + + /// Formats a ROHD radix literal according to [format]. + static String formatValue( + String value, + SignalValueFormat format, + int width, + ) { + final waveformValue = _waveformValue(value, width); + if (waveformValue == null) { + return _canonicalWaveformValue(value, width); + } + final (logicValue, canonical) = waveformValue; + if (format == SignalValueFormat.waveform || + _containsUnknownDigits(canonical)) { + return canonical; + } + return switch (format) { + SignalValueFormat.binary => logicValue.toRadixString( + leadingZeros: true, + includeWidth: false, + sepChar: '', + ), + SignalValueFormat.hexadecimal => + logicValue.toRadixString(radix: 16, sepChar: ''), + SignalValueFormat.unsignedDecimal => + logicValue.toRadixString(radix: 10, includeWidth: false, sepChar: ''), + SignalValueFormat.signedDecimal => + logicValue.toBigInt().toSigned(logicValue.width).toString(), + SignalValueFormat.octal => + '0o${logicValue.toRadixString(radix: 8, includeWidth: false, sepChar: '')}', + SignalValueFormat.ascii => () { + final byteCount = (logicValue.width + 7) ~/ 8; + return String.fromCharCodes( + List.generate( + byteCount, + (index) { + final shift = (byteCount - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ); + }(), + SignalValueFormat.waveform => canonical, + }; + } + + /// Parses [value] as a waveform literal and returns it with its canonical + /// waveform representation. + static (LogicValue, String)? _waveformValue(String value, int width) { + final trimmed = value.trim().replaceAll('\u0000', ''); + if (trimmed.isEmpty) { + return null; + } + final lower = trimmed.toLowerCase(); + final displayWidth = width > 0 ? width : 1; + final isRadixLiteral = RegExp(r"^\d+'[bqodh]").hasMatch(lower); + final digits = lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + final radix = lower.startsWith('0x') + ? 'h' + : lower.startsWith('0b') + ? 'b' + : isRadixLiteral + ? lower[lower.indexOf("'") + 1] + : _binaryDigits.hasMatch(digits) + ? 'b' + : _hexadecimalDigits.hasMatch(digits) && + _hexadecimalMarker.hasMatch(digits) + ? 'h' + : 'd'; + final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; + try { + final logicValue = LogicValue.ofRadixString(radixLiteral); + return ( + logicValue, + _containsUnknownDigits(trimmed) + ? logicValue.toRadixString( + radix: switch (radix) { + 'b' => 2, + 'q' => 4, + 'o' => 8, + 'd' => 10, + 'h' => 16, + _ => throw StateError('Unsupported radix: $radix'), + }, + sepChar: '', + ) + : isRadixLiteral + ? trimmed + : logicValue.toString(), + ); + } on LogicValueConstructionException { + return null; + } on FormatException { + return null; + } + } + + /// Returns [value] in its canonical waveform representation when possible. + static String _canonicalWaveformValue(String value, int width) { + final waveformValue = _waveformValue(value, width); + return waveformValue?.$2 ?? value.trim().replaceAll('\u0000', ''); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index dcb1b7a95..3f24d6f92 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: {sdk: flutter} rohd: ^0.6.9 + rohd_hierarchy: + path: ../../../packages/rohd_hierarchy web: ^1.0.0 dev_dependencies: flutter_test: {sdk: flutter} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart new file mode 100644 index 000000000..fd742e953 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,262 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry_test.dart +// Tests for shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +void main() { + tearDown(SignalValueFormatRegistry.clear); + + test('formats bare binary and hexadecimal waveform values', () { + expect( + SignalValueFormatRegistry.formatValue( + '0000', + SignalValueFormat.waveform, + 4, + ), + "4'h0", + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.signedDecimal, + 8, + ), + '-1', + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'ff', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '255', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x0', + SignalValueFormat.unsignedDecimal, + 4, + ), + '0', + ); + }); + + test('canonicalizes bare waveform values with unknown bits', () { + expect( + SignalValueFormatRegistry.formatValue( + '10x0', + SignalValueFormat.waveform, + 4, + ), + "4'b10x0", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'f0xz', + SignalValueFormat.hexadecimal, + 16, + ), + "16'hf0XZ", + ); + }); + + test('uses ROHD radix literals for typed format conversions', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'd255", + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + '1010', + SignalValueFormat.octal, + 4, + ), + '0o12', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x4142', + SignalValueFormat.ascii, + 16, + ), + 'AB', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x${List.filled(33, '41').join()}', + SignalValueFormat.ascii, + 264, + ), + List.filled(33, 'A').join(), + ); + }); + + test('returns invalid radix literals unchanged', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'b2", + SignalValueFormat.waveform, + 8, + ), + "8'b2", + ); + }); + + test('looks up an occurrence address from the format trie', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.signedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.signedDecimal, + ); + }); + + test('looks up a fallback occurrence address', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.unsignedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatForAny([ + const OccurrenceAddress([7, 8, 9]), + const OccurrenceAddress([0, 2, 4]), + ]), + SignalValueFormat.unsignedDecimal, + ); + }); + + test('stores shared address prefixes once in the format trie', () { + SignalValueFormatRegistry.update([ + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 4]), + SignalValueFormat.unsignedDecimal, + ), + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 5]), + SignalValueFormat.signedDecimal, + ), + ]); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.unsignedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 5])), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 6])), + SignalValueFormat.waveform, + ); + }); + + test('rejects an invalid signal occurrence address', () { + expect( + () => SignalValueFormatRegistry.setFormatFor( + const [OccurrenceAddress([])], + SignalValueFormat.unsignedDecimal, + ), + throwsArgumentError, + ); + expect( + () => SignalValueFormatRegistry.formatFor( + const OccurrenceAddress([0, -1]), + ), + throwsArgumentError, + ); + }); + + test('preserves registry state when a preference batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.update([ + SignalValueFormatPreference(address, SignalValueFormat.signedDecimal), + SignalValueFormatPreference( + OccurrenceAddress.root, + SignalValueFormat.hexadecimal, + ), + ]), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('preserves registry state when an address batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.setFormatFor( + [address, OccurrenceAddress.root], + SignalValueFormat.signedDecimal, + ), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +}