Reworking vm device adapter and adding tests for it.

This commit is contained in:
Florian Nücke
2020-12-12 12:09:44 +01:00
parent 117e17a6b6
commit 4ebc722126
7 changed files with 226 additions and 43 deletions

View File

@@ -4,6 +4,7 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
import li.cil.ceres.api.Serialized;
import li.cil.oc2.Constants;
import li.cil.oc2.OpenComputers;
import li.cil.oc2.api.bus.Device;
import li.cil.oc2.common.block.ComputerBlock;
import li.cil.oc2.common.bus.TileEntityDeviceBusController;
import li.cil.oc2.common.bus.TileEntityDeviceBusElement;
@@ -48,6 +49,7 @@ import javax.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Set;
import java.util.UUID;
import static java.util.Objects.requireNonNull;
@@ -474,12 +476,23 @@ public final class ComputerTileEntity extends AbstractTileEntity implements ITic
@Override
protected void onDevicesInvalid() {
runState = RunState.LOADING_DEVICES;
virtualMachine.rpcAdapter.pause();
}
@Override
protected void onDevicesValid() {
virtualMachine.rpcAdapter.resume();
protected void onDevicesValid(final boolean didDevicesChange) {
virtualMachine.rpcAdapter.resume(didDevicesChange);
}
@Override
protected void onDevicesAdded(final Set<Device> devices) {
virtualMachine.vmAdapter.addDevices(devices);
}
@Override
protected void onDevicesRemoved(final Set<Device> devices) {
virtualMachine.vmAdapter.removeDevices(devices);
}
}

View File

@@ -90,8 +90,10 @@ public final class RPCAdapter implements Steppable {
devicesById.clear();
}
public void resume() {
if (!isPaused) {
public void resume(final boolean didDevicesChange) {
isPaused = false;
if (!didDevicesChange) {
return;
}
@@ -136,8 +138,6 @@ public final class RPCAdapter implements Steppable {
devices.add(new RPCDeviceWithIdentifier(identifier, device));
devicesById.put(identifier, device);
});
isPaused = false;
}
public void tick() {

View File

@@ -61,9 +61,6 @@ public abstract class TileEntityDeviceBusController implements DeviceBusControll
}
elements.clear();
devices.clear();
deviceIds.clear();
scanDelay = 0; // scan as soon as possible
state = BusState.SCAN_PENDING;
}
@@ -72,18 +69,42 @@ public abstract class TileEntityDeviceBusController implements DeviceBusControll
public void scanDevices() {
onDevicesInvalid();
devices.clear();
deviceIds.clear();
final HashSet<Device> newDevices = new HashSet<>();
final HashMap<Device, Set<UUID>> newDeviceIds = new HashMap<>();
for (final DeviceBusElement element : elements) {
for (final Device device : element.getLocalDevices()) {
devices.add(device);
element.getDeviceIdentifier(device).ifPresent(identifier -> deviceIds
newDevices.add(device);
element.getDeviceIdentifier(device).ifPresent(identifier -> newDeviceIds
.computeIfAbsent(device, unused -> new HashSet<>()).add(identifier));
}
}
onDevicesValid();
final HashSet<Device> removedDevices = new HashSet<>(devices);
removedDevices.removeAll(newDevices);
onDevicesRemoved(removedDevices);
final HashSet<Device> addedDevices = new HashSet<>(newDevices);
addedDevices.removeAll(devices);
onDevicesAdded(addedDevices);
final boolean didDevicesChange = !removedDevices.isEmpty() || !addedDevices.isEmpty();
final boolean didDeviceIdsChange;
if (didDevicesChange) {
devices.clear();
devices.addAll(newDevices);
didDeviceIdsChange = true;
} else {
didDeviceIdsChange = deviceIds.entrySet().stream().anyMatch(entry ->
!Objects.equals(entry.getValue(), newDeviceIds.get(entry.getKey())));
}
if (didDeviceIdsChange) {
deviceIds.clear();
deviceIds.putAll(newDeviceIds);
}
onDevicesValid(didDevicesChange || didDeviceIdsChange);
}
@Override
@@ -208,7 +229,13 @@ public abstract class TileEntityDeviceBusController implements DeviceBusControll
protected void onDevicesInvalid() {
}
protected void onDevicesValid() {
protected void onDevicesValid(final boolean didDevicesChange) {
}
protected void onDevicesAdded(final Set<Device> devices) {
}
protected void onDevicesRemoved(final Set<Device> devices) {
}
///////////////////////////////////////////////////////////////////

View File

@@ -19,13 +19,14 @@ public final class ManagedInterruptAllocator implements InterruptAllocator {
public ManagedInterruptAllocator(final BitSet interrupts, final BitSet reservedInterrupts, final int interruptCount) {
this.interrupts = interrupts;
this.reservedInterrupts = reservedInterrupts;
this.managedInterrupts = new BitSet();
this.managedInterrupts = new BitSet(interruptCount);
this.interruptCount = interruptCount;
}
public void freeze() {
final long[] words = managedInterrupts.toLongArray();
managedMask = words.length > 0 ? (int) words[0] : 0;
isFrozen = true;
managedMask = (int) managedInterrupts.toLongArray()[0];
}
public void invalidate() {
@@ -64,7 +65,11 @@ public final class ManagedInterruptAllocator implements InterruptAllocator {
throw new IllegalStateException();
}
final int interruptBit = reservedInterrupts.nextClearBit(0);
final BitSet claimedInterrupts = new BitSet();
claimedInterrupts.or(interrupts);
claimedInterrupts.or(reservedInterrupts);
final int interruptBit = claimedInterrupts.nextClearBit(0);
if (interruptBit >= interruptCount) {
return OptionalInt.empty();
}

View File

@@ -8,7 +8,10 @@ import li.cil.sedna.api.device.InterruptController;
import li.cil.sedna.api.memory.MemoryMap;
import li.cil.sedna.riscv.device.R5PlatformLevelInterruptController;
import java.util.*;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Set;
public final class VirtualMachineDeviceBusAdapter {
private final MemoryMap memoryMap;
@@ -63,7 +66,14 @@ public final class VirtualMachineDeviceBusAdapter {
}
}
return incompleteLoads.isEmpty();
if (!incompleteLoads.isEmpty()) {
return false;
}
reservedInterrupts.clear();
reservedInterrupts.or(allocatedInterrupts);
return true;
}
public void unload() {
@@ -76,31 +86,35 @@ public final class VirtualMachineDeviceBusAdapter {
incompleteLoads.addAll(deviceContexts.keySet());
}
public void setDevices(final Collection<Device> devices) {
final HashSet<VMDevice> oldDevices = new HashSet<>(deviceContexts.keySet());
final HashSet<VMDevice> newDevices = new HashSet<>();
public void addDevices(final Set<Device> devices) {
for (final Device device : devices) {
if (device instanceof VMDevice) {
newDevices.add((VMDevice) device);
final VMDevice vmDevice = (VMDevice) device;
final ManagedVMContext context = deviceContexts.put(vmDevice, null);
if (context != null) {
context.invalidate();
}
incompleteLoads.add(vmDevice);
}
}
}
final HashSet<VMDevice> removedDevices = new HashSet<>(oldDevices);
removedDevices.removeAll(newDevices);
for (final VMDevice device : removedDevices) {
deviceContexts.remove(device).invalidate();
incompleteLoads.remove(device);
device.unload();
public void removeDevices(final Set<Device> devices) {
for (final Device device : devices) {
if (device instanceof VMDevice) {
final VMDevice vmDevice = (VMDevice) device;
final ManagedVMContext context = deviceContexts.remove(vmDevice);
if (context != null) {
context.invalidate();
}
incompleteLoads.remove(vmDevice);
vmDevice.unload();
}
}
final HashSet<VMDevice> addedDevices = new HashSet<>(newDevices);
addedDevices.removeAll(oldDevices);
for (final VMDevice device : addedDevices) {
deviceContexts.put(device, null);
incompleteLoads.add(device);
}
reservedInterrupts.clear();
reservedInterrupts.or(allocatedInterrupts);
}
}

View File

@@ -0,0 +1,125 @@
package li.cil.oc2.common.bus;
import li.cil.oc2.api.bus.device.vm.VMContext;
import li.cil.oc2.api.bus.device.vm.VMDevice;
import li.cil.oc2.api.bus.device.vm.VMDeviceLoadResult;
import li.cil.oc2.common.vm.VirtualMachineDeviceBusAdapter;
import li.cil.sedna.api.device.InterruptController;
import li.cil.sedna.api.memory.MemoryMap;
import li.cil.sedna.memory.SimpleMemoryMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.OptionalInt;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
public final class VMDeviceTests {
private MemoryMap memoryMap;
private InterruptController interruptController;
private VirtualMachineDeviceBusAdapter adapter;
@BeforeEach
public void setupEach() {
memoryMap = new SimpleMemoryMap();
interruptController = mock(InterruptController.class);
adapter = new VirtualMachineDeviceBusAdapter(memoryMap, interruptController);
}
@Test
public void addedDevicesHaveLoadCalled() {
final VMDevice device1 = mock(VMDevice.class);
final VMDevice device2 = mock(VMDevice.class);
when(device1.load(any())).thenReturn(VMDeviceLoadResult.success());
when(device2.load(any())).thenReturn(VMDeviceLoadResult.success());
adapter.addDevices(Collections.singleton(device1));
assertTrue(adapter.load());
verify(device1).load(any());
adapter.addDevices(Collections.singleton(device2));
assertTrue(adapter.load());
verifyNoMoreInteractions(device1);
verify(device2).load(any());
}
@Test
public void removedDevicesHaveUnloadCalled() {
final VMDevice device = mock(VMDevice.class);
when(device.load(any())).thenReturn(VMDeviceLoadResult.success());
adapter.addDevices(Collections.singleton(device));
assertTrue(adapter.load());
adapter.removeDevices(Collections.singleton(device));
verify(device).unload();
}
@Test
public void devicesHaveUnloadCalledOnGlobalUnload() {
final VMDevice device = mock(VMDevice.class);
when(device.load(any())).thenReturn(VMDeviceLoadResult.success());
adapter.addDevices(Collections.singleton(device));
assertTrue(adapter.load());
adapter.unload();
verify(device).unload();
}
@Test
public void devicesHaveLoadCalledAfterGlobalUnload() {
final VMDevice device = mock(VMDevice.class);
when(device.load(any())).thenReturn(VMDeviceLoadResult.success());
adapter.addDevices(Collections.singleton(device));
assertTrue(adapter.load());
verify(device).load(any());
adapter.unload();
verify(device).unload();
assertTrue(adapter.load());
verify(device, times(2)).load(any());
}
@Test
public void deviceCanClaimInterrupts() {
final VMDevice device = mock(VMDevice.class);
when(device.load(any())).thenAnswer(invocation -> {
final VMContext context = invocation.getArgument(0);
final OptionalInt interrupt = context.getInterruptAllocator().claimInterrupt();
assertTrue(interrupt.isPresent());
return VMDeviceLoadResult.success();
});
adapter.addDevices(Collections.singleton(device));
assertTrue(adapter.load());
verify(device).load(any());
}
@Test
public void deviceCannotClaimClaimedInterrupts() {
final int claimedInterrupt = 1;
final VMDevice device = mock(VMDevice.class);
when(device.load(any())).thenAnswer(invocation -> {
final VMContext context = invocation.getArgument(0);
final OptionalInt interrupt = context.getInterruptAllocator().claimInterrupt(claimedInterrupt);
assertTrue(interrupt.isPresent());
assertNotEquals(claimedInterrupt, interrupt.getAsInt());
return VMDeviceLoadResult.success();
});
adapter.claimInterrupt(claimedInterrupt);
adapter.addDevices(Collections.singleton(device));
assertTrue(adapter.load());
}
}

View File

@@ -100,8 +100,7 @@ public class RPCAdapterTests {
when(busController.getDeviceIdentifiers(device)).thenReturn(singleton(deviceId));
// trigger device cache rebuild
rpcAdapter.pause();
rpcAdapter.resume();
rpcAdapter.resume(true);
}
private JsonElement invokeMethod(final UUID deviceId, final String name, final Object... parameters) {