initial commit

This commit is contained in:
Patrik Persson 2024-06-10 10:53:11 +02:00
commit 3aca31de74
40 changed files with 1701 additions and 0 deletions

12
wash/.classpath Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="lib" path="/cs/labs.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="output" path="bin"/>
</classpath>

28
wash/.project Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>3. Washing machine</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<filteredResources>
<filter>
<id>1678721711437</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View file

@ -0,0 +1,25 @@
package actor;
public abstract class ActorThread<M> extends Thread {
// TODO: one suitable attribute here
/** Called by another thread, to send a message to this thread. */
public void send(M message) {
// TODO: implement this method (one or a few lines)
}
/** Returns the first message in the queue, or blocks if none available. */
protected M receive() throws InterruptedException {
// TODO: implement this method (one or a few lines)
return null;
}
/** Returns the first message in the queue, or blocks up to 'timeout'
milliseconds if none available. Returns null if no message is obtained
within 'timeout' milliseconds. */
protected M receiveWithTimeout(long timeout) throws InterruptedException {
// TODO: implement this method (one or a few lines)
return null;
}
}

View file

@ -0,0 +1,334 @@
package actor.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import actor.ActorThread;
@TestMethodOrder(OrderAnnotation.class)
class ActorThreadTest {
@Test
@Order(1)
void testBidirectional() throws InterruptedException {
checkMainPrints(ExampleBidirectional::main,
"ClientThread sending request\n" +
"request received by FibonacciThread\n" +
"received result fib(14) = 377\n" +
"FibonacciThread terminated\n");
}
@Test
@Order(2)
void testProducerConsumer() throws InterruptedException {
checkMainPrints(ExampleProducerConsumer::main,
"consumer eagerly awaiting messages...\n" +
"received [ole]\n" +
"received [dole]\n" +
"received [doff]\n" +
"all done\n");
}
@Test
@Order(3)
void testMessagingWithTimeout() throws InterruptedException {
checkMainPrints(ExampleMessagingWithTimeout::main,
"consumer eagerly awaiting messages...\n" +
"received [ole]\n" +
"received [dole]\n" +
"received [null]\n" +
"received [doff]\n" +
"all done\n");
}
@Test
@Order(4)
void testMessagingWithTimeoutButMessagesWaiting() throws InterruptedException {
checkMainPrints(ExampleReceiveWithTimeoutKeepsMessagesInOrder::main,
"consumer eagerly awaiting messages...\n" +
"received [yxi]\n" +
"received [kaxi]\n" +
"received [kolme]\n" +
"received [null]\n" +
"all done\n");
}
/**
* Verify that ActorThread has one single attribute
* of type BlockingQueue (or a type implementing BlockingQueue),
* and that the chosen queue type does not have a capacity limit
*/
@Test
@Order(5)
void testActorThreadUsesBlockingQueue() throws IllegalArgumentException, IllegalAccessException {
Field[] attributes = ActorThread.class.getDeclaredFields();
assertEquals(1, attributes.length, "expected one single attribute (BlockingQueue)");
if (attributes.length == 1) {
Field queueAttribute = attributes[0];
Class<?> attributeType = queueAttribute.getType();
assertTrue(BlockingQueue.class.isAssignableFrom(attributeType), "expected BlockingQueue attribute");
// inspect the actual attribute value
ActorThread<?> t = new ActorThread<>() { /* concrete subclass to abstract superclass */ };
queueAttribute.setAccessible(true);
Object value = queueAttribute.get(t);
assertFalse(value instanceof ArrayBlockingQueue, "ArrayBlockingQueue has a limited capacity: can lead to deadlock when used for message queues. Consider LinkedBlockingQueue");
assertFalse(value instanceof DelayQueue, "DelayQueue introduces additional delays: inappropriate for message queues. Consider LinkedBlockingQueue");
assertFalse(value instanceof PriorityBlockingQueue, "PriorityBlockingQueue reorders elements: inappropriate for message queues. Consider LinkedBlockingQueue");
assertFalse(value instanceof SynchronousQueue, "SynchronousQueue has a limited capacity: can lead to deadlock when used for message queues. Consider LinkedBlockingQueue");
}
}
/**
* Verify that ActorThread methods are not synchronized
* (they shouldn't be, as BlockingQueues are thread-safe)
*/
@Test
@Order(6)
void testActorThreadNotUsingSynchronized() {
Method[] methods = ActorThread.class.getDeclaredMethods();
assertEquals(3, methods.length, "expected three methods: send, receive, receiveWithTimeout");
for (Method m : methods) {
assertFalse(Modifier.isSynchronized(m.getModifiers()), "method " + m.getName() + " is synchronized, but shouldn't be");
}
}
/**
* Verify that ActorThread method send() is not declared
* 'throws InterruptedException'
*/
@Test
@Order(7)
void testSendNotDeclaredThrows() {
for (Method m : ActorThread.class.getDeclaredMethods()) {
if ("send".equals(m.getName())) {
Type[] exceptionsThrown = m.getExceptionTypes();
if (exceptionsThrown.length != 0) {
String[] exceptions = Stream.of(exceptionsThrown)
.map(t -> {
String n = t.getTypeName();
int dot = n.lastIndexOf('.');
return (dot >= 0) ? n.substring(dot + 1) : n;
}).toArray(String[]::new);
String typeString = String.join(", ", exceptions);
System.err.println("** ERROR:");
System.err.println("**");
System.err.println("** Method 'send()' was declared 'throws " + typeString + "'.");
System.err.println("** This method should not throw any exceptions.\n");
System.err.println("** HINT:");
System.err.println("**");
System.err.println("** Don't use the BlockingQueue method put(). Use add() or offer() instead.");
System.err.println("** Then remove 'throws " + typeString + "' from method 'send'.");
}
assertEquals(0, exceptionsThrown.length, "method 'send' should not throw any exceptions");
return;
}
}
fail("no method 'send' found in ActorThread");
}
/**
* Verify that ActorThread methods receive() and receiveWithTimeout()
* are declared 'throws InterruptedException'
*/
@Test
@Order(8)
void testReceivesDeclaredThrows() {
int k = 0;
for (Method m : ActorThread.class.getDeclaredMethods()) {
String name = m.getName();
if ("receive".equals(name) || "receiveWithTimeout".equals(name)) {
Type[] exceptionsThrown = m.getExceptionTypes();
Set<String> typeNames = Stream.of(exceptionsThrown).map(Type::getTypeName).collect(Collectors.toSet());
boolean throwsInterruptedException = typeNames.contains(InterruptedException.class.getTypeName());
assertTrue(throwsInterruptedException, "method '" + name + "' should be declared 'throws InterruptedException'");
k++;
}
}
assertEquals(2, k, "missing method 'receive' and/or 'receiveWithTimeout'");
}
@Test
@Order(9)
void testReceiveBlocks() throws InterruptedException {
AtomicBoolean interruptionHandledCorrectly = new AtomicBoolean(false);
ActorThread<?> blocker = new ActorThread<>() {
@Override
public void run() {
try {
receive();
} catch (InterruptedException e) {
// interruption expected: check
// InterruptedException handled correctly
interruptionHandledCorrectly.set(true);
} catch (Throwable unexpected) {
fail("unexpected exception", unexpected);
}
}
};
blocker.start();
expectThreadState(blocker, Thread.State.WAITING);
blocker.interrupt();
expectThreadState(blocker, Thread.State.TERMINATED);
assertTrue(interruptionHandledCorrectly.get(), "receive must not catch InterruptedException");
}
@Test
@Order(10)
void testReceiveWithTimeoutBlocks() throws InterruptedException {
AtomicBoolean interruptionHandledCorrectly = new AtomicBoolean(false);
ActorThread<?> blocker = new ActorThread<>() {
@Override
public void run() {
try {
receiveWithTimeout(60 * 60 * 1000); // one hour
} catch (InterruptedException e) {
// interruption expected: check
// InterruptedException handled correctly
interruptionHandledCorrectly.set(true);
} catch (Throwable unexpected) {
fail("unexpected exception", unexpected);
}
}
};
blocker.start();
expectThreadState(blocker, Thread.State.TIMED_WAITING);
blocker.interrupt();
expectThreadState(blocker, Thread.State.TERMINATED);
assertTrue(interruptionHandledCorrectly.get(), "receiveWithTimeout must not catch InterruptedException");
}
@Test
@Order(11)
void testReceiveWithTimeoutDelay() throws InterruptedException {
List<Long> measurements = new ArrayList<>();
ActorThread<?> delayed = new ActorThread<>() {
@Override
public void run() {
try {
long t0 = System.currentTimeMillis();
receiveWithTimeout(300);
long t1 = System.currentTimeMillis();
receiveWithTimeout(100);
long t2 = System.currentTimeMillis();
receiveWithTimeout(200);
long t3 = System.currentTimeMillis();
measurements.add(t1 - t0);
measurements.add(t2 - t1);
measurements.add(t3 - t2);
} catch (Throwable unexpected) {
fail("unexpected exception", unexpected);
}
}
};
delayed.start();
delayed.join();
assertEquals(3, measurements.size());
// allow 50ms additional delay: huge overkill
assertTrue(measurements.get(0) >= 300 && measurements.get(0) < 350);
assertTrue(measurements.get(1) >= 100 && measurements.get(1) < 150);
assertTrue(measurements.get(2) >= 200 && measurements.get(2) < 250);
}
@Test
@Order(12)
void testFinal() {
Field[] attributes = ActorThread.class.getDeclaredFields();
// previous tests check we have exactly one appropriate attribute
if (attributes.length >= 1) {
Field queueAttribute = attributes[0];
int mod = queueAttribute.getModifiers();
assertTrue(Modifier.isFinal(mod), "reference attribute should be final");
}
}
@Test
@Order(13)
void testPrivate() {
Field[] attributes = ActorThread.class.getDeclaredFields();
// previous tests check we have exactly one appropriate attribute
if (attributes.length >= 1) {
Field queueAttribute = attributes[0];
int mod = queueAttribute.getModifiers();
assertTrue(Modifier.isPrivate(mod), "queue attribute should be private");
}
}
// -----------------------------------------------------------------------
/** Helper interface for making lambdas, for a main function that throws InterruptedException */
private interface InterruptibleMain {
void invoke(String... args) throws InterruptedException;
}
/** Helper method: run a main method in another class, and check the printed output. */
private void checkMainPrints(InterruptibleMain main, String expectedOutput) throws InterruptedException {
PrintStream sysout = System.out;
OutputStream bos = new ByteArrayOutputStream();
try {
System.setOut(new PrintStream(bos, true));
main.invoke();
} finally {
System.setOut(sysout);
// make sure line feeds are always represented as "\n",
// regardless of what the system uses
String actualOutput = bos.toString().replace(System.getProperty("line.separator"), "\n");
assertEquals(expectedOutput, actualOutput);
}
}
/** Helper method: check thread t is in the given state. */
private static void expectThreadState(Thread t, Thread.State expectedState) throws InterruptedException {
// there is no way to wait for a thread state change:
// we have to resort to polling
long t0 = System.currentTimeMillis(), now = t0;
Thread.State actualState = t.getState();
while (actualState != expectedState
&& actualState != Thread.State.TERMINATED
&& now < t0 + 1000) // give up after one second
{
Thread.sleep(20);
actualState = t.getState();
now = System.currentTimeMillis();
}
assertEquals(expectedState, actualState);
}
}

View file

@ -0,0 +1,67 @@
package actor.test;
import actor.ActorThread;
public class ExampleBidirectional {
private ActorThread<Integer>
ct = new ClientThread(),
ft = new FibonacciThread();
class ClientThread extends ActorThread<Integer> {
@Override
public void run() {
try {
System.out.println("ClientThread sending request");
ft.send(14);
int reply = receive();
System.out.println("received result fib(14) = " + reply);
} catch (InterruptedException e) {
// not expected to happen
throw new Error(e);
}
}
}
class FibonacciThread extends ActorThread<Integer> {
@Override
public void run() {
try {
while (true) {
int n = receive();
System.out.println("request received by FibonacciThread");
int f2 = 0;
int f1 = 1;
for (int k = 2; k <= n; k++) {
int s = f2 + f1;
f2 = f1;
f1 = s;
Thread.sleep(100);
}
ct.send(f1);
}
} catch (InterruptedException e) {
System.out.println("FibonacciThread terminated");
}
}
}
public static void main(String[] args) throws InterruptedException {
ExampleBidirectional app = new ExampleBidirectional();
app.ct.start();
app.ft.start();
app.ct.join();
app.ft.interrupt();
app.ft.join();
}
}

View file

@ -0,0 +1,57 @@
package actor.test;
import actor.ActorThread;
public class ExampleMessagingWithTimeout {
private Producer p = new Producer();
private Consumer c = new Consumer();
class Producer extends Thread {
@Override
public void run() {
try {
Thread.sleep(300);
c.send("ole");
Thread.sleep(300);
c.send("dole");
Thread.sleep(1000);
c.send("doff");
} catch (InterruptedException e) {
// not expected to happen
throw new Error(e);
}
}
}
class Consumer extends ActorThread<String> {
@Override
public void run() {
try {
System.out.println("consumer eagerly awaiting messages...");
for (int k = 0; k < 3; k++) {
String s = receiveWithTimeout(500);
System.out.println("received [" + s + "]");
}
String s = receiveWithTimeout(2000);
System.out.println("received [" + s + "]");
} catch (InterruptedException e) {
// not expected to happen
throw new Error(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
ExampleMessagingWithTimeout app = new ExampleMessagingWithTimeout();
app.p.start();
app.c.start();
app.p.join();
app.c.join();
System.out.println("all done");
}
}

View file

@ -0,0 +1,55 @@
package actor.test;
import actor.ActorThread;
public class ExampleProducerConsumer {
private Producer p = new Producer();
private Consumer c = new Consumer();
class Producer extends Thread {
@Override
public void run() {
try {
Thread.sleep(300);
c.send("ole");
Thread.sleep(300);
c.send("dole");
Thread.sleep(300);
c.send("doff");
} catch (InterruptedException e) {
// not expected to happen
throw new Error(e);
}
}
}
class Consumer extends ActorThread<String> {
@Override
public void run() {
try {
System.out.println("consumer eagerly awaiting messages...");
for (int k = 0; k < 3; k++) {
String s = receive();
System.out.println("received [" + s + "]");
}
} catch (InterruptedException e) {
// not expected to happen
throw new Error(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
ExampleProducerConsumer app = new ExampleProducerConsumer();
app.p.start();
app.c.start();
app.p.join();
app.c.join();
System.out.println("all done");
}
}

View file

@ -0,0 +1,40 @@
package actor.test;
import actor.ActorThread;
/**
* This test was introduced to detect possible errors in the implementation
* of ActorThread.receiveWithTimeout().
*
* So if this test is the only one that fails, review your
* receiveWithTimeout() implementation. It must not remove more than one
* message from the queue.
*/
public class ExampleReceiveWithTimeoutKeepsMessagesInOrder {
public static void main(String[] args) throws InterruptedException {
ActorThread<String> c = new ActorThread<>() {
@Override
public void run() {
try {
System.out.println("consumer eagerly awaiting messages...");
for (int k = 0; k < 4; k++) {
String s = receiveWithTimeout(100);
System.out.println("received [" + s + "]");
}
} catch (InterruptedException unexpected) {
throw new Error(unexpected);
}
}
};
c.send("yxi");
c.send("kaxi");
c.send("kolme");
c.start();
c.join();
System.out.println("all done");
}
}

View file

@ -0,0 +1,7 @@
package wash.control;
interface Settings {
// simulation speed-up factor: 50 means the simulation is 50 times faster than
// real time. Modify this as you wish.
int SPEEDUP = 50;
}

View file

@ -0,0 +1,42 @@
package wash.control;
import actor.ActorThread;
import wash.io.WashingIO;
import wash.io.WashingIO.Spin;
public class SpinController extends ActorThread<WashingMessage> {
// TODO: add attributes
public SpinController(WashingIO io) {
// TODO
}
@Override
public void run() {
// this is to demonstrate how to control the barrel spin:
// io.setSpinMode(Spin.IDLE);
try {
// ... TODO ...
while (true) {
// wait for up to a (simulated) minute for a WashingMessage
WashingMessage m = receiveWithTimeout(60000 / Settings.SPEEDUP);
// if m is null, it means a minute passed and no message was received
if (m != null) {
System.out.println("got " + m);
}
// ... TODO ...
}
} catch (InterruptedException unexpected) {
// we don't expect this thread to be interrupted,
// so throw an error if it happens anyway
throw new Error(unexpected);
}
}
}

View file

@ -0,0 +1,18 @@
package wash.control;
import actor.ActorThread;
import wash.io.WashingIO;
public class TemperatureController extends ActorThread<WashingMessage> {
// TODO: add attributes
public TemperatureController(WashingIO io) {
// TODO
}
@Override
public void run() {
// TODO
}
}

View file

@ -0,0 +1,31 @@
package wash.control;
import actor.ActorThread;
import wash.io.WashingIO;
import wash.simulation.WashingSimulator;
public class Wash {
public static void main(String[] args) throws InterruptedException {
WashingSimulator sim = new WashingSimulator(Settings.SPEEDUP);
WashingIO io = sim.startSimulation();
ActorThread<WashingMessage> temp = new TemperatureController(io);
ActorThread<WashingMessage> water = new WaterController(io);
ActorThread<WashingMessage> spin = new SpinController(io);
temp.start();
water.start();
spin.start();
while (true) {
int n = io.awaitButton();
System.out.println("user selected program " + n);
// TODO:
// if the user presses buttons 1-3, start a washing program
// if the user presses button 0, and a program has been started, stop it
}
}
};

View file

@ -0,0 +1,30 @@
package wash.control;
import actor.ActorThread;
/**
* Class used for messaging
* - from washing programs to spin controller (SPIN_xxx)
* - from washing programs to temperature controller (TEMP_xxx)
* - from washing programs to water controller (WATER_xxx)
* - from controllers to washing programs (ACKNOWLEDGMENT)
*
* @param sender the thread that sent the message
* @param order an order, such as SPIN_FAST or WATER_DRAIN
*/
public record WashingMessage(ActorThread<WashingMessage> sender, Order order) {
// possible values for the 'order' attribute
public enum Order {
SPIN_OFF,
SPIN_SLOW,
SPIN_FAST,
TEMP_IDLE,
TEMP_SET_40,
TEMP_SET_60,
WATER_IDLE,
WATER_FILL,
WATER_DRAIN,
ACKNOWLEDGMENT
}
}

View file

@ -0,0 +1,79 @@
package wash.control;
import actor.ActorThread;
import wash.io.WashingIO;
import static wash.control.WashingMessage.Order.*;
/**
* Program 3 for washing machine. This also serves as an example of how washing
* programs can be structured.
*
* This short program stops all regulation of temperature and water levels,
* stops the barrel from spinning, and drains the machine of water.
*
* It can be used after an emergency stop (program 0) or a power failure.
*/
public class WashingProgram3 extends ActorThread<WashingMessage> {
private WashingIO io;
private ActorThread<WashingMessage> temp;
private ActorThread<WashingMessage> water;
private ActorThread<WashingMessage> spin;
public WashingProgram3(WashingIO io,
ActorThread<WashingMessage> temp,
ActorThread<WashingMessage> water,
ActorThread<WashingMessage> spin)
{
this.io = io;
this.temp = temp;
this.water = water;
this.spin = spin;
}
@Override
public void run() {
try {
System.out.println("washing program 3 started");
// Switch off heating
temp.send(new WashingMessage(this, TEMP_IDLE));
// Wait for temperature controller to acknowledge
WashingMessage ack1 = receive();
System.out.println("got " + ack1);
// Drain barrel, which may take some time. To ensure the barrel
// is drained before we continue, an acknowledgment is required.
water.send(new WashingMessage(this, WATER_DRAIN));
WashingMessage ack2 = receive(); // wait for acknowledgment
System.out.println("got " + ack2);
// Now that the barrel is drained, we can turn off water regulation.
water.send(new WashingMessage(this, WATER_IDLE));
WashingMessage ack3 = receive(); // wait for acknowledgment
System.out.println("got " + ack3);
// Switch off spin. We expect an acknowledgment, to ensure
// the hatch isn't opened while the barrel is spinning.
spin.send(new WashingMessage(this, SPIN_OFF));
WashingMessage ack4 = receive(); // wait for acknowledgment
System.out.println("got " + ack4);
// Unlock hatch
io.lock(false);
System.out.println("washing program 3 finished");
} catch (InterruptedException e) {
// If we end up here, it means the program was interrupt()'ed:
// set all controllers to idle
temp.send(new WashingMessage(this, TEMP_IDLE));
water.send(new WashingMessage(this, WATER_IDLE));
spin.send(new WashingMessage(this, SPIN_OFF));
System.out.println("washing program terminated");
}
}
}

View file

@ -0,0 +1,18 @@
package wash.control;
import actor.ActorThread;
import wash.io.WashingIO;
public class WaterController extends ActorThread<WashingMessage> {
// TODO: add attributes
public WaterController(WashingIO io) {
// TODO
}
@Override
public void run() {
// TODO
}
}

View file

@ -0,0 +1,32 @@
package wash.io;
/** Input/Output (IO) for our washing machine */
public interface WashingIO {
/** @return water level, in range 0..20 liters */
double getWaterLevel();
/** @return temperature, in degrees Celsius */
double getTemperature();
/** Blocks until a program button (0, 1, 2, 3) is pressed */
int awaitButton() throws InterruptedException;
/** Turn heating element on (true) or off (false) */
void heat(boolean on);
/** Set inlet valve to open (true) or closed (false) */
void fill(boolean on);
/** Turn drain pump on (true) or off (false) */
void drain(boolean on);
/** Set hatch to locked (true) or unlocked (false) */
void lock(boolean locked);
/** @param mode one of Spin.IDLE, Spin.LEFT, Spin.RIGHT, Spin.FAST */
void setSpinMode(Spin mode);
/** Values for setSpinMode */
enum Spin { IDLE, LEFT, RIGHT, FAST };
}