Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions lib/src/references/interface_reference.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2024-2025 Intel Corporation
// Copyright (C) 2024-2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// interface_reference.dart
Expand Down Expand Up @@ -509,24 +509,54 @@ class InterfaceReference<InterfaceType extends PairInterface>
extension _ExceptPairInterfaceExtensions on PairInterface {
/// Performs the same operation as [driveOther], but excludes ports listed in
/// [exceptPorts].
///
/// Throws [RohdBridgeException] if [exceptPorts] is provided and not empty
/// when either this interface or [other] has sub-interfaces.
void _driveOtherExcept(PairInterface other, Iterable<PairDirection> tags,
{required Set<String>? exceptPorts}) {
getPorts(tags).forEach((portName, thisPort) {
if (exceptPorts == null || !exceptPorts.contains(portName)) {
other.port(portName) <= thisPort;
}
});
final subInterfacesPresent =
subInterfaces.isNotEmpty || other.subInterfaces.isNotEmpty;
if (subInterfacesPresent &&
(exceptPorts != null && exceptPorts.isNotEmpty)) {
throw RohdBridgeException(
'Cannot use exceptPorts when driving interfaces with sub-interfaces');
}

if (subInterfacesPresent) {
driveOther(other, tags);
} else {
getPorts(tags).forEach((portName, thisPort) {
if (exceptPorts == null || !exceptPorts.contains(portName)) {
other.port(portName) <= thisPort;
}
});
}
}

/// Performs the same operation as [receiveOther], but excludes ports listed
/// in [exceptPorts].
///
/// Throws [RohdBridgeException] if [exceptPorts] is provided and not empty
/// when either this interface or [other] has sub-interfaces.
void _receiveOtherExcept(PairInterface other, Iterable<PairDirection> tags,
{required Set<String>? exceptPorts}) {
getPorts(tags).forEach((portName, thisPort) {
if (exceptPorts == null || !exceptPorts.contains(portName)) {
thisPort <= other.port(portName);
}
});
final subInterfacesPresent =
subInterfaces.isNotEmpty || other.subInterfaces.isNotEmpty;
if (subInterfacesPresent &&
(exceptPorts != null && exceptPorts.isNotEmpty)) {
throw RohdBridgeException(
'Cannot use exceptPorts when driving interfaces with sub-interfaces');
}

if (subInterfacesPresent) {
receiveOther(other, tags);
} else {
getPorts(tags).forEach((portName, thisPort) {
if (exceptPorts == null || !exceptPorts.contains(portName)) {
thisPort <= other.port(portName);
}
});
}
}

/// Creates a copy of an interface with optional port exclusions.
Expand All @@ -537,7 +567,16 @@ extension _ExceptPairInterfaceExtensions on PairInterface {
///
/// This is used internally when creating interface variants that exclude
/// certain ports during hierarchical interface operations.
///
/// Throws [RohdBridgeException] if [exceptPorts] is provided and not empty
/// when this interface has sub-interfaces.
PairInterface _cloneExcept({required Set<String>? exceptPorts}) {
if (subInterfaces.isNotEmpty &&
(exceptPorts != null && exceptPorts.isNotEmpty)) {
throw RohdBridgeException(
'Cannot use exceptPorts when cloning interfaces with sub-interfaces');
}

if (exceptPorts == null || exceptPorts.isEmpty) {
return clone();
}
Expand Down
255 changes: 255 additions & 0 deletions test/sub_interface_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright (C) 2026 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// sub_interface_test.dart
// Tests for interfaces with sub-interfaces.
//
// 2026 March 8
// Author: Max Korbel <max.korbel@intel.com>

import 'package:rohd/rohd.dart';
import 'package:rohd_bridge/rohd_bridge.dart';
import 'package:test/test.dart';

class SubIntf extends PairInterface {
SubIntf()
: super(
portsFromProvider: [Logic.port('subFp', 8)],
portsFromConsumer: [Logic.port('subFc', 8)],
);

@override
SubIntf clone() => SubIntf();
}

class IntfWithSub extends PairInterface {
IntfWithSub()
: super(
portsFromProvider: [Logic.port('topFp', 8)],
portsFromConsumer: [Logic.port('topFc', 8)],
) {
addSubInterface('sub', SubIntf());
}

@override
IntfWithSub clone() => IntfWithSub();
}

void main() {
group('interfaces with sub-interfaces', () {
test('connectUpTo passes signals through sub-interfaces', () async {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top.addSubModule(leaf);

leaf.interface('intf').connectUpTo(top.interface('intf'));

top.pullUpPort(leaf.createPort('dummy', PortDirection.inOut));

await top.build();

// top-level interface ports
top.interface('intf').port('topFp').port.put(0xAB);
expect(leaf.interface('intf').port('topFp').port.value.toInt(), 0xAB);

leaf.interface('intf').port('topFc').port.put(0xCD);
expect(top.interface('intf').port('topFc').port.value.toInt(), 0xCD);

// sub-interface ports (accessed via module ports)
top.input('intf_subFp').put(0x12);
expect(leaf.input('intf_subFp').value.toInt(), 0x12);

leaf.output('intf_subFc').put(0x34);
expect(top.output('intf_subFc').value.toInt(), 0x34);
});

test('connectDownTo passes signals through sub-interfaces', () async {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

top.addSubModule(leaf);

top.interface('intf').connectDownTo(leaf.interface('intf'));

top.pullUpPort(leaf.createPort('dummy', PortDirection.inOut));

await top.build();

// top-level interface ports
top.interface('intf').port('topFp').port.put(0xAB);
expect(leaf.interface('intf').port('topFp').port.value.toInt(), 0xAB);

leaf.interface('intf').port('topFc').port.put(0xCD);
expect(top.interface('intf').port('topFc').port.value.toInt(), 0xCD);

// sub-interface ports (accessed via module ports)
top.input('intf_subFc').put(0x12);
expect(leaf.input('intf_subFc').value.toInt(), 0x12);

leaf.output('intf_subFp').put(0x34);
expect(top.output('intf_subFp').value.toInt(), 0x34);
});

test('connectTo passes signals through sub-interfaces', () async {
final top = BridgeModule('top');

final provider = BridgeModule('provider')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final consumer = BridgeModule('consumer')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top
..addSubModule(provider)
..addSubModule(consumer);

provider.interface('intf').connectTo(consumer.interface('intf'));

top
..pullUpPort(provider.createPort('dummy', PortDirection.inOut))
..pullUpPort(consumer.createPort('dummy', PortDirection.inOut));

await top.build();

// top-level interface ports
provider.interface('intf').port('topFp').port.put(0xAB);
expect(consumer.interface('intf').port('topFp').port.value.toInt(), 0xAB);

consumer.interface('intf').port('topFc').port.put(0xCD);
expect(provider.interface('intf').port('topFc').port.value.toInt(), 0xCD);

// sub-interface ports (accessed via module ports)
provider.output('intf_subFp').put(0x12);
expect(consumer.input('intf_subFp').value.toInt(), 0x12);

consumer.output('intf_subFc').put(0x34);
expect(provider.input('intf_subFc').value.toInt(), 0x34);
});

test('connectUpTo throws when exceptPorts used with sub-interfaces', () {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top.addSubModule(leaf);

expect(
() => leaf
.interface('intf')
.connectUpTo(top.interface('intf'), exceptPorts: {'topFp'}),
throwsA(isA<RohdBridgeException>()),
);
});

test('connectDownTo throws when exceptPorts used with sub-interfaces', () {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

top.addSubModule(leaf);

expect(
() => top
.interface('intf')
.connectDownTo(leaf.interface('intf'), exceptPorts: {'topFp'}),
throwsA(isA<RohdBridgeException>()),
);
});

test('connectTo throws when exceptPorts used with sub-interfaces', () {
final top = BridgeModule('top');

final provider = BridgeModule('provider')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final consumer = BridgeModule('consumer')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top
..addSubModule(provider)
..addSubModule(consumer);

expect(
() => provider
.interface('intf')
.connectTo(consumer.interface('intf'), exceptPorts: {'topFp'}),
throwsA(isA<RohdBridgeException>()),
);
});

test('punchUpTo throws when exceptPorts used with sub-interfaces', () {
final top = BridgeModule('top');

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top.addSubModule(leaf);

expect(
() => leaf.interface('intf').punchUpTo(top, exceptPorts: {'topFp'}),
throwsA(isA<RohdBridgeException>()),
);
});

test('punchDownTo throws when exceptPorts used with sub-interfaces', () {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final leaf = BridgeModule('leaf');
top.addSubModule(leaf);

expect(
() => top.interface('intf').punchDownTo(leaf, exceptPorts: {'topFp'}),
throwsA(isA<RohdBridgeException>()),
);
});

test('punchUpTo passes signals through sub-interfaces', () async {
final top = BridgeModule('top');

final leaf = BridgeModule('leaf')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.consumer);

top.addSubModule(leaf);

leaf.interface('intf').punchUpTo(top);

await top.build();

top.input('intf_subFp').put(0x12);
expect(leaf.input('intf_subFp').value.toInt(), 0x12);

leaf.output('intf_subFc').put(0x34);
expect(top.output('intf_subFc').value.toInt(), 0x34);
});

test('punchDownTo passes signals through sub-interfaces', () async {
final top = BridgeModule('top')
..addInterface(IntfWithSub(), name: 'intf', role: PairRole.provider);

final leaf = BridgeModule('leaf');
top.addSubModule(leaf);

top.interface('intf').punchDownTo(leaf);

await top.build();

top.input('intf_subFc').put(0x12);
expect(leaf.input('intf_subFc').value.toInt(), 0x12);

leaf.output('intf_subFp').put(0x34);
expect(top.output('intf_subFp').value.toInt(), 0x34);
});
});
}