aboutsummaryrefslogtreecommitdiff
path: root/comp2511
diff options
context:
space:
mode:
Diffstat (limited to 'comp2511')
-rw-r--r--comp2511/blackout/BlackoutController.java230
-rw-r--r--comp2511/blackout/CloudStorageDevice.java22
-rw-r--r--comp2511/blackout/DesktopDevice.java14
-rw-r--r--comp2511/blackout/DeviceBase.java62
-rw-r--r--comp2511/blackout/ElephantSatellite.java49
-rw-r--r--comp2511/blackout/EntityBase.java324
-rw-r--r--comp2511/blackout/File.java87
-rw-r--r--comp2511/blackout/FileTransferException.java48
-rw-r--r--comp2511/blackout/HandheldDevice.java14
-rw-r--r--comp2511/blackout/LaptopDevice.java14
-rw-r--r--comp2511/blackout/RelaySatellite.java66
-rw-r--r--comp2511/blackout/SatelliteBase.java57
-rw-r--r--comp2511/blackout/ShrinkingSatellite.java56
-rw-r--r--comp2511/blackout/StandardSatellite.java46
-rw-r--r--comp2511/blackout/tests/Task1Tests.java81
-rw-r--r--comp2511/blackout/tests/Task2Tests.java206
-rw-r--r--comp2511/blackout/tests/TestHelpers.java14
17 files changed, 1390 insertions, 0 deletions
diff --git a/comp2511/blackout/BlackoutController.java b/comp2511/blackout/BlackoutController.java
new file mode 100644
index 0000000..fcbedc4
--- /dev/null
+++ b/comp2511/blackout/BlackoutController.java
@@ -0,0 +1,230 @@
+package unsw.blackout;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector; // prefer vectors for cache locality
+import java.util.Optional;
+
+import unsw.response.models.EntityInfoResponse;
+import unsw.response.models.FileInfoResponse;
+import unsw.utils.Angle;
+
+public class BlackoutController {
+ private Vector<EntityBase> Entities = new Vector<EntityBase>();
+
+ // We use optional as a more explicit way of saying we return null, or to represent infinity etc.
+ // Note, the HD part is not entirely implemented.
+
+ /**
+ * Returns an optional Entitybase, where optional means it does not exist.
+ */
+ private Optional<EntityBase> getEntity(String Id) {
+ for (EntityBase entity : this.Entities) {
+ if (entity.getId().equals(Id)) {
+ return Optional.of(entity);
+ }
+ }
+ return Optional.empty();
+ }
+ /**
+ * Removes an entity based on a String id, does nothing if it already exists.
+ */
+ private void removeEntity(String id) {
+ Optional<EntityBase> entity = getEntity(id);
+ if (entity.isEmpty()) {
+ return;
+ }
+ this.Entities.remove(entity.get());
+ }
+ /**
+ * Adds an entity based on a String id, does nothing if it already exists.
+ */
+ private void addEntity(EntityBase entity) {
+ if (getEntity(entity.getId()).isPresent()) {
+ return;
+ }
+ this.Entities.add(entity);
+ }
+
+// Public methods.
+ public void createDevice(String deviceId, String type, Angle position) {
+ if(type.equals(HandheldDevice.class.getSimpleName())) {
+ this.addEntity(new HandheldDevice(deviceId, position));
+ } else if (type.equals(LaptopDevice.class.getSimpleName())) {
+ this.addEntity(new LaptopDevice(deviceId, position));
+ } else if (type.equals(DesktopDevice.class.getSimpleName())) {
+ this.addEntity(new DesktopDevice(deviceId, position));
+ } else if (type.equals(CloudStorageDevice.class.getSimpleName())) {
+ this.addEntity(new CloudStorageDevice(deviceId, position));
+ }
+ }
+
+ public void removeDevice(String deviceId) {
+ Optional<EntityBase> entity = this.getEntity(deviceId);
+ if (entity.isEmpty() || !(entity.get() instanceof DeviceBase)) {
+ return;
+ }
+ this.removeEntity(deviceId);
+ }
+
+ public void createSatellite(String satelliteId, String type, double height, Angle position) {
+ if (type.equals(StandardSatellite.class.getSimpleName())) {
+ this.addEntity(new StandardSatellite(satelliteId, height, position));
+ } else if (type.equals(ShrinkingSatellite.class.getSimpleName())) {
+ this.addEntity(new ShrinkingSatellite(satelliteId, height, position));
+ } else if (type.equals(RelaySatellite.class.getSimpleName())) {
+ this.addEntity(new RelaySatellite(satelliteId, height, position));
+ } else if (type.equals(ElephantSatellite.class.getSimpleName())) {
+ this.addEntity(new ElephantSatellite(satelliteId, height, position));
+ }
+ }
+
+ public void removeSatellite(String satelliteId) {
+ Optional<EntityBase> entity = this.getEntity(satelliteId);
+ if (entity.isEmpty() || !(entity.get() instanceof SatelliteBase)) {
+ return;
+ }
+ this.removeEntity(satelliteId);
+ }
+
+ public List<String> listDeviceIds() {
+ ArrayList<String> list = new ArrayList<String>();
+ for (EntityBase entity : this.Entities) {
+ if (!(entity instanceof DeviceBase)) {
+ continue;
+ }
+ list.add(entity.getId());
+ }
+ return list;
+ }
+
+ public List<String> listSatelliteIds() {
+ ArrayList<String> list = new ArrayList<String>();
+ for (EntityBase entity : this.Entities) {
+ if (!(entity instanceof SatelliteBase)) {
+ continue;
+ }
+ list.add(entity.getId());
+ }
+ return list;
+ }
+
+ public void addFileToDevice(String deviceId, String filename, String content) {
+ Optional<EntityBase> entity = this.getEntity(deviceId);
+ if (entity.isEmpty() || !(entity.get() instanceof DeviceBase)) {
+ return;
+ }
+ DeviceBase device = (DeviceBase)entity.get();
+ device.addFile(filename, content);
+ }
+
+ public EntityInfoResponse getInfo(String id) {
+ Optional<EntityBase> optional_entity = this.getEntity(id);
+ if (optional_entity.isEmpty()) {
+ return null;
+ }
+ EntityBase entity = optional_entity.get();
+ Angle position = entity.getAngle();
+ double height = entity.getHeight();
+ String type = entity.getClass().getSimpleName();
+ Map<String, FileInfoResponse> files = new HashMap<>();
+ for (File file : entity.getReceivedFiles()) {
+ String filename = file.getFilename();
+ String transmitted_contents = file.getTransmittedContents();
+ int final_size = file.getContentsSize();
+ boolean is_transmitted = file.hasFullyTransmitted();
+
+ if (is_transmitted && entity.canShrink()) {
+ if (file.isQuantum()) {
+ final_size = (int)((double)final_size * (2.0 / 3.0) + 0.5);
+ }
+ }
+
+ files.put(filename, new FileInfoResponse(filename, transmitted_contents, final_size, is_transmitted));
+ }
+ return new EntityInfoResponse(id, position, height, type, files);
+ }
+
+ /**
+ * Helper function, sends all files associated with the entity to their targets if they exist.
+ */
+ private void sendEntityFiles(EntityBase entity) {
+ Vector<File> remove_files = new Vector<File>();
+
+ for (File file : entity.getSentFiles()) {
+ if (file.hasFullyTransmitted()) {
+ continue;
+ }
+
+ Optional<EntityBase> target = this.getEntity(file.getTargetId());
+ if (target.isEmpty()) { // as per the reference implementation, if it's deleted just ignore it (undef in spec)
+ continue;
+ }
+
+ if (entity.maybeTransferFile(file, target.get())) {
+ continue;
+ }
+
+ remove_files.add(file);
+ }
+
+ entity.removeSentFiles(remove_files);
+ }
+
+ public void simulate() {
+ for (EntityBase entity : this.Entities) {
+ entity.move();
+ }
+ // For sending files, we need the origin entity, available in BlackoutController.
+ for (EntityBase entity : this.Entities) {
+ this.sendEntityFiles(entity);
+ }
+ }
+
+ /**
+ * Simulate for the specified number of minutes.
+ * You shouldn't need to modify this function.
+ */
+ public void simulate(int numberOfMinutes) {
+ for (int i = 0; i < numberOfMinutes; i++) {
+ simulate();
+ }
+ }
+
+ public List<String> communicableEntitiesInRange(String id) {
+ Optional<EntityBase> optional_entity = this.getEntity(id);
+
+ if (optional_entity.isEmpty()) {
+ return null;
+ }
+
+ EntityBase entity = optional_entity.get();
+
+ List<String> communicable_entities = new ArrayList<String>();
+ for (EntityBase e : this.Entities) {
+ if (!entity.canCommunicate(e, this.Entities)) {
+ continue;
+ }
+ communicable_entities.add(e.getId());
+ }
+
+ return communicable_entities;
+ }
+
+ public void sendFile(String fileName, String fromId, String toId) throws FileTransferException {
+ // Assumes we can communicate! (part of the spec)
+ Optional<EntityBase> optional_entity_from = this.getEntity(fromId);
+ Optional<EntityBase> optional_entity_to = this.getEntity(toId);
+
+ if (optional_entity_from.isEmpty() || optional_entity_to.isEmpty()) {
+ return;
+ }
+
+ EntityBase entity_from = optional_entity_from.get();
+ EntityBase entity_to = optional_entity_to.get();
+
+ entity_from.sendFileTo(fileName, entity_to);
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/CloudStorageDevice.java b/comp2511/blackout/CloudStorageDevice.java
new file mode 100644
index 0000000..04e1c41
--- /dev/null
+++ b/comp2511/blackout/CloudStorageDevice.java
@@ -0,0 +1,22 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+
+// "This device acts like a desktop"
+// There is a small POTENTIAL issue where our satellites check the type of the device, and if our
+// device is supposed to act like a desktop, then the satellites should also be able to communicate
+// with our CloudStorageDevice if Desktops are compatible. This is unspecified!
+public class CloudStorageDevice extends DesktopDevice {
+ public CloudStorageDevice(String deviceID, Angle position) {
+ super(deviceID, position);
+ }
+
+ @ Override
+ final protected int getNumBytesInFiles() {
+ int count = 0;
+ for (File file : getReceivedFiles()) {
+ count += file.getContents().length();
+ }
+ return count;
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/DesktopDevice.java b/comp2511/blackout/DesktopDevice.java
new file mode 100644
index 0000000..5411411
--- /dev/null
+++ b/comp2511/blackout/DesktopDevice.java
@@ -0,0 +1,14 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+
+public class DesktopDevice extends DeviceBase {
+ public DesktopDevice(String deviceID, Angle position) {
+ super(deviceID, position);
+ }
+
+ @ Override
+ final public double getRange() {
+ return 200_000.0;
+ }
+}
diff --git a/comp2511/blackout/DeviceBase.java b/comp2511/blackout/DeviceBase.java
new file mode 100644
index 0000000..8f0b06e
--- /dev/null
+++ b/comp2511/blackout/DeviceBase.java
@@ -0,0 +1,62 @@
+package unsw.blackout;
+
+import java.util.Optional;
+
+import unsw.utils.Angle;
+import unsw.utils.MathsHelper;
+import static unsw.utils.MathsHelper.RADIUS_OF_JUPITER;
+
+public abstract class DeviceBase extends EntityBase {
+
+ /**
+ * DeviceBase is an abstract class containing common functionality between devices.
+ */
+ public DeviceBase(String deviceID, Angle position) {
+ super(deviceID, position, RADIUS_OF_JUPITER);
+ }
+
+ @ Override
+ final public double getVelocity() {
+ return 0.0; // Devices do not move.
+ }
+ @ Override
+ final protected boolean isSupportedEntity(EntityBase entity) {
+ if (entity instanceof DeviceBase) {
+ return false;
+ }
+ return true;
+ }
+ @ Override
+ final protected boolean isPhysicallyReachable(EntityBase entity) {
+ if (!(entity instanceof SatelliteBase)) {
+ return false;
+ }
+ if (!MathsHelper.isVisible(entity.getHeight(), entity.getAngle(), this.getAngle())) {
+ return false;
+ }
+ if (MathsHelper.getDistance(entity.getHeight(), entity.getAngle(), this.getAngle()) > this.getRange()) {
+ return false;
+ }
+ return true;
+ }
+
+ final protected Optional<Integer> getFileStoreLimit() {
+ return Optional.empty(); // Infinite file storage limit.
+ }
+ final protected Optional<Integer> getByteStoreLimit() {
+ return Optional.empty(); // Infinite byte storage limit.
+ }
+ final protected Optional<Integer> getByteDownloadSpeed() {
+ return Optional.empty(); // Infinite download speed
+ }
+ final protected Optional<Integer> getByteUploadSpeed() {
+ return Optional.empty(); // Infinite upload speed.
+ }
+
+ /**
+ * Adds a file without any bandwidth operation taking place.
+ */
+ void addFile(String filename, String contents) {
+ this.getReceivedFiles().add(new File(filename, contents, contents.length(), this.getId()));
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/ElephantSatellite.java b/comp2511/blackout/ElephantSatellite.java
new file mode 100644
index 0000000..79e2cd1
--- /dev/null
+++ b/comp2511/blackout/ElephantSatellite.java
@@ -0,0 +1,49 @@
+package unsw.blackout;
+
+import java.util.Optional;
+
+import unsw.utils.Angle;
+
+public class ElephantSatellite extends SatelliteBase {
+ public ElephantSatellite(String satelliteID, double height, Angle position) {
+ super(satelliteID, height, position);
+ }
+
+ @ Override
+ final public double getVelocity() {
+ return 2_500.0;
+ }
+ @ Override
+ final public double getRange() {
+ return 400_000.0;
+ }
+ @ Override
+ final public boolean canTransient() {
+ return true;
+ }
+ @ Override
+ final protected boolean isSupportedDeviceType(String type) {
+ if (type.equals(DesktopDevice.class.getSimpleName())) {
+ return true;
+ } else if (type.equals(LaptopDevice.class.getSimpleName())) {
+ return true;
+ }
+ return false;
+ }
+ @ Override
+ final protected Optional<Integer> getFileStoreLimit() {
+ return Optional.empty(); // Undefined in spec?
+ }
+ @ Override
+ final protected Optional<Integer> getByteStoreLimit() {
+ return Optional.of(90); // Max of 90 bytes.
+ }
+ @ Override
+ final protected Optional<Integer> getByteDownloadSpeed() {
+ return Optional.of(20); // 20 bytes per minute.
+ }
+ @ Override
+ final protected Optional<Integer> getByteUploadSpeed() {
+ return Optional.of(20); // 20 bytes per minute.
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/EntityBase.java b/comp2511/blackout/EntityBase.java
new file mode 100644
index 0000000..33241d9
--- /dev/null
+++ b/comp2511/blackout/EntityBase.java
@@ -0,0 +1,324 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+
+import java.util.Optional;
+import java.util.Vector;
+
+public abstract class EntityBase {
+ private String id;
+ private Angle position;
+ private double height;
+ private Vector<File> received_files = new Vector<File>();
+ private Vector<File> sent_files = new Vector<File>();
+
+ /**
+ * BaseEntity is a class that contains an ID and a type which represents any object in our network.
+ */
+ EntityBase(String id, Angle position, double height) {
+ this.id = id;
+ this.position = position;
+ this.height = height;
+ }
+
+ // Getters.
+ public String getId() {
+ return this.id;
+ }
+ public Angle getAngle() {
+ return this.position;
+ }
+ public double getHeight() {
+ return this.height;
+ }
+ public Vector<File> getReceivedFiles() {
+ return this.received_files;
+ }
+ public Vector<File> getSentFiles() {
+ return this.sent_files;
+ }
+ public void setAngle(Angle angle) {
+ this.position = angle;
+ }
+
+ /**
+ * Returns the velocity of the entity in km/s.
+ */
+ public abstract double getVelocity();
+ /**
+ * Returns the range of the entity in km.
+ */
+ public abstract double getRange();
+ /**
+ * Returns a boolean flagging if the entity may communicate with the other entity.
+ * Warning: it is possible that one entity may allow, while the other does not.
+ */
+ protected abstract boolean isSupportedEntity(EntityBase entity);
+ /**
+ * Returns a boolean flagging if the entity is physically in range and unobstructed
+ * to another entity.
+ */
+ protected abstract boolean isPhysicallyReachable(EntityBase entity);
+ /**
+ * Returns a boolean flagging if the entity may retransmit messages to other entities.
+ */
+ protected boolean canRelay() {
+ return false;
+ }
+ /**
+ * Returns a boolean flagging if the entity should NOT delete files when out of range.
+ */
+ protected boolean canTransient() {
+ return false;
+ }
+ /**
+ * Returns a boolean flagging if the entity may shrink files.
+ */
+ protected boolean canShrink() {
+ return false;
+ }
+ /**
+ * Returns an optional integer limit on the number of files it may hold.
+ * Returning no value means no limit.
+ */
+ protected abstract Optional<Integer> getFileStoreLimit();
+ /**
+ * Returns an optional integer limit on the number of bytes it may hold across all files.
+ * Returning no value means no limit.
+ */
+ protected abstract Optional<Integer> getByteStoreLimit();
+ /**
+ * Returns an optional integer limit on the number of bytes downloadable per minute.
+ * Returning no value means no limit.
+ */
+ protected abstract Optional<Integer> getByteDownloadSpeed();
+ /**
+ * Returns an optional integer limit on the number of bytes uploadable per minute.
+ * Returning no value means no limit.
+ */
+ protected abstract Optional<Integer> getByteUploadSpeed();
+
+ /**
+ * Helper function to calculate an offset to the position class in radians.
+ */
+ protected double getMoveOffsetRadians() {
+ return (this.getVelocity() / this.getHeight());
+ }
+ /**
+ * Moves the entity to the expected position after 1 minute.
+ */
+ public void move() {
+ double offset = this.getMoveOffsetRadians();
+ double rposition = this.position.toRadians() + offset;
+ this.position = Angle.fromRadians(rposition % Math.toRadians(360.0));
+ }
+
+ /**
+ * Helper function, returns the number of files that have not been fully transmitted in the vector.
+ */
+ static private int getNumUncompletedFiles(Vector<File> files) {
+ int count = 0;
+ for (File file : files) {
+ if (file.hasFullyTransmitted()) {
+ continue;
+ }
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Gets the number of bytes in received files.
+ */
+ protected int getNumBytesInFiles() {
+ int count = 0;
+ for (File file : this.received_files) {
+ count += file.getContents().length();
+ }
+ return count;
+ }
+
+ /**
+ * Returns the speed a file should be downloaded per tick for fairness during multiple transfers.
+ * Empty optional means infinite value.
+ */
+ private Optional<Integer> getDownloadBandwidth() {
+ Optional<Integer> download_speed = this.getByteDownloadSpeed();
+ if (download_speed.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(download_speed.get() / EntityBase.getNumUncompletedFiles(this.getReceivedFiles()));
+ }
+ /**
+ * Returns the speed a file should be uploaded per tick for fairness during multiple transfers.
+ * Empty optional means infinite value.
+ */
+ private Optional<Integer> getUploadBandwidth() {
+ Optional<Integer> upload_speed = this.getByteUploadSpeed();
+ if (upload_speed.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(upload_speed.get() / EntityBase.getNumUncompletedFiles(this.getSentFiles()));
+ }
+
+ /**
+ * Heper function, returns the min of two optional integers, or a concrete value representing infinity.
+ */
+ static private int getOptionalMin(Optional<Integer> a, Optional<Integer> b) {
+ if (a.isPresent() && b.isPresent()) {
+ return Math.min(a.get(), b.get());
+ } else if (a.isPresent()) {
+ return a.get();
+ } else if (b.isPresent()) {
+ return b.get();
+ }
+ return Integer.MAX_VALUE;
+ }
+ public void removeSentFiles(Vector<File> files) {
+ for (File file : files) {
+ getSentFiles().remove(file);
+ }
+ }
+ /**
+ * Transfers currently sending files by the expected values after 1 minute.
+ * Returns false if the file should be deleted.
+ */
+ public boolean maybeTransferFile(File file, EntityBase target) {
+ // Delete and early out files if we are out of range - but keep if transient!
+ if (!this.isPhysicallyReachable(target)) {
+ if (!target.canTransient()) {
+ target.getReceivedFiles().remove(file);
+ return false;
+ }
+ return true;
+ }
+
+ Optional<Integer> upload_bandwidth = this.getUploadBandwidth();
+ Optional<Integer> download_bandwidth = target.getDownloadBandwidth();
+
+ // The amount we can send is the min of our upload bandwidth and their download bandwidth.
+ int bandwidth = EntityBase.getOptionalMin(upload_bandwidth, download_bandwidth);
+ file.addBytes(bandwidth);
+ return true;
+ }
+
+ /**
+ * Recursive DPS helper function to determine whether an entity may communicate with another.
+ */
+ private boolean canCommunicateSearch(EntityBase target, Vector<EntityBase> entities, Vector<EntityBase> visited) {
+ visited.add(this);
+ for (EntityBase entity : entities) {
+ if (entity.equals(this)) {
+ continue;
+ }
+
+ if (visited.contains(entity)) {
+ continue;
+ }
+
+ if (!this.isSupportedEntity(entity) || !entity.isSupportedEntity(this)) {
+ continue;
+ }
+
+ if (!this.isPhysicallyReachable(entity)) {
+ continue;
+ }
+
+ if (target.equals(entity)) {
+ return true;
+ }
+
+ if (entity.canRelay()) {
+ if (entity.canCommunicateSearch(target, entities, visited)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ /**
+ * Determines whether an entity may communicate with another entity, including via hops from other entities.
+ */
+ public boolean canCommunicate(EntityBase target, Vector<EntityBase> entities) {
+ Vector<EntityBase> visited = new Vector<EntityBase>();
+ return canCommunicateSearch(target, entities, visited);
+ }
+
+ /**
+ * Helper to extract files via their filenames, returns an empty optional if it does not exist.
+ */
+ static private Optional<File> getFileFromFilename(String filename, Vector<File> files) {
+ for (File file : files) {
+ if (file.getFilename().equals(filename)) {
+ return Optional.of(file);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Sends a file to the entity, while respecting the constraints imposed by both entities.
+ */
+ public void sendFileTo(String filename, EntityBase target) throws FileTransferException {
+ Optional<File> origin_file = EntityBase.getFileFromFilename(filename, this.getReceivedFiles());
+
+ if (origin_file.isEmpty()) { // throw if the file does not exist
+ throw new FileTransferException.VirtualFileNotFoundException(filename);
+ }
+ if (!origin_file.get().hasFullyTransmitted()) { // throw if the file has not fully downloaded
+ throw new FileTransferException.VirtualFileNotFoundException(filename);
+ }
+
+ // throw if the file already exist or is currently downloading, nicely handles both here
+ if (EntityBase.getFileFromFilename(filename, target.getReceivedFiles()).isPresent()) {
+ throw new FileTransferException.VirtualFileAlreadyExistsException(filename);
+ }
+
+ Optional<Integer> upload_speed = this.getByteUploadSpeed(); // throw if we don't have enough bandwidth
+ if (upload_speed.isPresent() && EntityBase.getNumUncompletedFiles(this.sent_files) >= upload_speed.get()) {
+ throw new FileTransferException.VirtualFileNoBandwidthException(this.getId());
+ }
+
+ Optional<Integer> download_speed = target.getByteDownloadSpeed(); // throw if they don't have enough bandwidth
+ if (download_speed.isPresent() && EntityBase.getNumUncompletedFiles(target.getReceivedFiles()) >= download_speed.get()) {
+ throw new FileTransferException.VirtualFileNoBandwidthException(target.getId());
+ }
+
+ // Throw if they can't store any more files.
+ Optional<Integer> download_file_limit = target.getFileStoreLimit();
+ if (download_file_limit.isPresent() && target.getReceivedFiles().size() >= download_file_limit.get()) {
+ throw new FileTransferException.VirtualFileNoStorageSpaceException("Max Files Reached");
+ }
+
+ // Throw if they don't have enough space.
+ Optional<Integer> download_byte_limit = target.getByteStoreLimit();
+ int filesize = origin_file.get().getContents().length();
+ if (download_byte_limit.isPresent() && target.getNumBytesInFiles() + filesize >= download_byte_limit.get()) {
+ throw new FileTransferException.VirtualFileNoStorageSpaceException("Max Storage Reached");
+ }
+
+ // Finally we upload the file.
+ String contents = origin_file.get().getContents();
+ File upload_file = new File(filename, contents, 0, target.getId());
+ this.getSentFiles().add(upload_file);
+ target.getReceivedFiles().add(upload_file);
+ }
+
+ // We know that our ID is unique, so that's the only thing we need to compare here.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ EntityBase other = (EntityBase) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/File.java b/comp2511/blackout/File.java
new file mode 100644
index 0000000..df43e17
--- /dev/null
+++ b/comp2511/blackout/File.java
@@ -0,0 +1,87 @@
+package unsw.blackout;
+
+public class File {
+ private String filename;
+ private String contents;
+ private int transmitted;
+ private String target_id;
+
+ /**
+ * File represents a potentially in flight transmission. Transmitted should be <= contents.size().
+ */
+ File(String filename, String contents, int transmitted, String target_id) {
+ this.filename = filename;
+ this.contents = contents;
+ this.transmitted = transmitted;
+ this.target_id = target_id;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+ public String getContents() {
+ return contents;
+ }
+ public String getTransmittedContents() {
+ return this.contents.substring(0, this.transmitted);
+ }
+ public int getTransmitted() {
+ return transmitted;
+ }
+ public int getContentsSize() {
+ return contents.length();
+ }
+ public String getTargetId() {
+ return this.target_id;
+ }
+
+ /**
+ * Returns true if the file has fully transmitted, false otherwise.
+ */
+ public boolean hasFullyTransmitted() {
+ return this.transmitted == this.contents.length();
+ }
+ /**
+ * Returns true if the file contains quantum, false otherwise.
+ */
+ public boolean isQuantum() {
+ return this.contents.contains("quantum");
+ }
+
+
+ /**
+ * Add bytes to the transmitted content, clamps to the 0 and the filesize.
+ */
+ public void addBytes(int bytes) {
+ this.transmitted = Math.min(this.contents.length(), this.transmitted + bytes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ File other = (File) obj;
+ if (contents == null) {
+ if (other.contents != null)
+ return false;
+ } else if (!contents.equals(other.contents))
+ return false;
+ if (filename == null) {
+ if (other.filename != null)
+ return false;
+ } else if (!filename.equals(other.filename))
+ return false;
+ if (target_id == null) {
+ if (other.target_id != null)
+ return false;
+ } else if (!target_id.equals(other.target_id))
+ return false;
+ if (transmitted != other.transmitted)
+ return false;
+ return true;
+ }
+}
diff --git a/comp2511/blackout/FileTransferException.java b/comp2511/blackout/FileTransferException.java
new file mode 100644
index 0000000..e40c136
--- /dev/null
+++ b/comp2511/blackout/FileTransferException.java
@@ -0,0 +1,48 @@
+package unsw.blackout;
+
+/**
+ * Represents an exception that occured because of a file transfer.
+ */
+public class FileTransferException extends Exception {
+ public FileTransferException(String message) {
+ super(message);
+ }
+
+ /**
+ * Represents the case where the targeted file wasn't found on the source.
+ */
+ public static class VirtualFileNotFoundException extends FileTransferException {
+ public VirtualFileNotFoundException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Represents the case where the targeted file already existed on the target
+ * or was in the process of downloading.
+ */
+ public static class VirtualFileAlreadyExistsException extends FileTransferException {
+ public VirtualFileAlreadyExistsException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Represents the case when no more bandwidth exists for a satellite to
+ * be able to use for new devices.
+ */
+ public static class VirtualFileNoBandwidthException extends FileTransferException {
+ public VirtualFileNoBandwidthException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Occurs when a satellite runs out of space.
+ */
+ public static class VirtualFileNoStorageSpaceException extends FileTransferException {
+ public VirtualFileNoStorageSpaceException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/comp2511/blackout/HandheldDevice.java b/comp2511/blackout/HandheldDevice.java
new file mode 100644
index 0000000..587d7a4
--- /dev/null
+++ b/comp2511/blackout/HandheldDevice.java
@@ -0,0 +1,14 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+
+public class HandheldDevice extends DeviceBase {
+ public HandheldDevice(String deviceID, Angle position) {
+ super(deviceID, position);
+ }
+
+ @ Override
+ public double getRange() {
+ return 50_000.0;
+ }
+}
diff --git a/comp2511/blackout/LaptopDevice.java b/comp2511/blackout/LaptopDevice.java
new file mode 100644
index 0000000..b39c762
--- /dev/null
+++ b/comp2511/blackout/LaptopDevice.java
@@ -0,0 +1,14 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+
+public class LaptopDevice extends DeviceBase {
+ public LaptopDevice(String deviceID, Angle position) {
+ super(deviceID, position);
+ }
+
+ @ Override
+ public double getRange() {
+ return 100_000.0;
+ }
+}
diff --git a/comp2511/blackout/RelaySatellite.java b/comp2511/blackout/RelaySatellite.java
new file mode 100644
index 0000000..1087db8
--- /dev/null
+++ b/comp2511/blackout/RelaySatellite.java
@@ -0,0 +1,66 @@
+package unsw.blackout;
+
+import java.util.Optional;
+
+import unsw.utils.Angle;
+
+public class RelaySatellite extends SatelliteBase {
+ public RelaySatellite(String satelliteID, double height, Angle position) {
+ super(satelliteID, height, position);
+ }
+
+ @ Override
+ final public double getVelocity() {
+ return 1_500.0;
+ }
+ @ Override
+ final public double getRange() {
+ return 300_000.0;
+ }
+
+ /**
+ * Relay satellites oscillate between 140 and 90 degrees, so we override move to incorporate this behaviour.
+ */
+ private boolean is_counterclockwise = true;
+ @ Override
+ final public void move() {
+ double current = this.getAngle().toDegrees();
+
+ if (current < 140.0 || current > 190.0) {
+ if (current > 190.0 && current < 345.0) {
+ is_counterclockwise = false;
+ } else {
+ is_counterclockwise = true;
+ }
+ }
+
+ double offset = this.getMoveOffsetRadians();
+ double rposition = (this.getAngle().toRadians() + (is_counterclockwise ? offset : -offset)) % Math.toRadians(360.0);
+ setAngle(Angle.fromRadians(rposition + (rposition < 0 ? Math.toRadians(360.0) : 0)));
+ }
+
+ @ Override
+ final protected boolean isSupportedDeviceType(String type) {
+ return true; // Supports all devices!
+ }
+ @ Override
+ final protected boolean canRelay() {
+ return true;
+ }
+ @ Override
+ final protected Optional<Integer> getFileStoreLimit() {
+ return Optional.of(0); // Cannot store any files!
+ }
+ @ Override
+ final protected Optional<Integer> getByteStoreLimit() {
+ return Optional.of(0); // Cannot store any bytes!
+ }
+ @ Override
+ final protected Optional<Integer> getByteDownloadSpeed() {
+ return Optional.empty(); // Infinite download speed
+ }
+ @ Override
+ final protected Optional<Integer> getByteUploadSpeed() {
+ return Optional.empty(); // Infinite upload speed.
+ }
+}
diff --git a/comp2511/blackout/SatelliteBase.java b/comp2511/blackout/SatelliteBase.java
new file mode 100644
index 0000000..ed88216
--- /dev/null
+++ b/comp2511/blackout/SatelliteBase.java
@@ -0,0 +1,57 @@
+package unsw.blackout;
+
+import unsw.utils.Angle;
+import unsw.utils.MathsHelper;
+
+public abstract class SatelliteBase extends EntityBase {
+
+ /**
+ * SatelliteBase is an abstract class containing common functionality between satellites.
+ */
+ public SatelliteBase(String satelliteID, double height, Angle position) {
+ super(satelliteID, position, height);
+ }
+
+ /**
+ * Returns true if the device type is supported, false otherwise.
+ */
+ protected abstract boolean isSupportedDeviceType(String type);
+
+
+ @ Override
+ final protected boolean isSupportedEntity(EntityBase entity) {
+ if (entity instanceof SatelliteBase) {
+ return true;
+ }
+ return this.isSupportedDeviceType(entity.getClass().getSimpleName());
+ }
+
+ /**
+ * Helper function to differentiate between devices and satellite distance calls.
+ */
+ final private double getEntityDistance(EntityBase entity) {
+ if (entity instanceof SatelliteBase) {
+ return MathsHelper.getDistance(this.getHeight(), this.getAngle(), entity.getHeight(), entity.getAngle());
+ }
+ return MathsHelper.getDistance(this.getHeight(), this.getAngle(), entity.getAngle());
+ }
+ /**
+ * Helper function to differentiate between devices and satellite visibility calls.
+ */
+ final private boolean isEntityVisible(EntityBase entity) {
+ if (entity instanceof SatelliteBase) {
+ return MathsHelper.isVisible(this.getHeight(), this.getAngle(), entity.getHeight(), entity.getAngle());
+ }
+ return MathsHelper.isVisible(this.getHeight(), this.getAngle(), entity.getAngle());
+ }
+ @ Override
+ final protected boolean isPhysicallyReachable(EntityBase entity) {
+ if (!this.isEntityVisible(entity)) {
+ return false;
+ }
+ if (this.getEntityDistance(entity) > this.getRange()) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/comp2511/blackout/ShrinkingSatellite.java b/comp2511/blackout/ShrinkingSatellite.java
new file mode 100644
index 0000000..93dde50
--- /dev/null
+++ b/comp2511/blackout/ShrinkingSatellite.java
@@ -0,0 +1,56 @@
+package unsw.blackout;
+
+import java.util.Optional;
+
+import unsw.utils.Angle;
+
+public class ShrinkingSatellite extends SatelliteBase {
+ public ShrinkingSatellite(String satelliteID, double height, Angle position) {
+ super(satelliteID, height, position);
+ }
+
+ @ Override
+ final public double getVelocity() {
+ return 1_000.0;
+ }
+ @ Override
+ final public double getRange() {
+ return 200_000.0;
+ }
+ @ Override
+ final protected boolean isSupportedDeviceType(String type) {
+ return true;
+ }
+ @ Override
+ final protected boolean canShrink() {
+ return true;
+ }
+ @ Override
+ final protected Optional<Integer> getFileStoreLimit() {
+ return Optional.empty(); // No limit.
+ }
+ @ Override
+ final protected Optional<Integer> getByteStoreLimit() {
+ return Optional.of(150); // 150 bytes.
+ }
+ @ Override
+ final protected Optional<Integer> getByteDownloadSpeed() {
+ return Optional.of(15); // 15 bytes per minute.
+ }
+ @ Override
+ final protected Optional<Integer> getByteUploadSpeed() {
+ return Optional.of(10); // 10 bytes per minute.
+ }
+ @ Override
+ final protected int getNumBytesInFiles() {
+ int count = 0;
+ for (File file : getReceivedFiles()) {
+ if (file.isQuantum()) {
+ count += (int)((double)file.getContentsSize() * (2.0 / 3.0) + 0.5); // quantum satellites perform 2/3 compression on their files
+ continue;
+ }
+ count += file.getContentsSize();
+ }
+ return count;
+ }
+}
diff --git a/comp2511/blackout/StandardSatellite.java b/comp2511/blackout/StandardSatellite.java
new file mode 100644
index 0000000..10777b3
--- /dev/null
+++ b/comp2511/blackout/StandardSatellite.java
@@ -0,0 +1,46 @@
+package unsw.blackout;
+
+import java.util.Optional;
+
+import unsw.utils.Angle;
+
+public class StandardSatellite extends SatelliteBase {
+ public StandardSatellite(String satelliteID, double height, Angle position) {
+ super(satelliteID, height, position);
+ }
+
+ @ Override
+ final public double getVelocity() {
+ return 2_500.0;
+ }
+ @ Override
+ final public double getRange() {
+ return 150_000.0;
+ }
+ @ Override
+ final protected boolean isSupportedDeviceType(String type) {
+ if (type.equals(LaptopDevice.class.getSimpleName())) {
+ return true;
+ } else if (type.equals(HandheldDevice.class.getSimpleName())) {
+ return true;
+ }
+ return false;
+ }
+ @ Override
+ final protected Optional<Integer> getFileStoreLimit() {
+ return Optional.of(3); // Max of 3 files.
+ }
+ @ Override
+ final protected Optional<Integer> getByteStoreLimit() {
+ return Optional.of(80); // Max of 80 bytes.
+ }
+ @ Override
+ final protected Optional<Integer> getByteDownloadSpeed() {
+ return Optional.of(1); // 1 byte per minute.
+ }
+ @ Override
+ final protected Optional<Integer> getByteUploadSpeed() {
+ return Optional.of(1); // 1 byte per minute.
+ }
+}
+
diff --git a/comp2511/blackout/tests/Task1Tests.java b/comp2511/blackout/tests/Task1Tests.java
new file mode 100644
index 0000000..f5e2a55
--- /dev/null
+++ b/comp2511/blackout/tests/Task1Tests.java
@@ -0,0 +1,81 @@
+package blackout;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+import unsw.blackout.BlackoutController;
+import unsw.response.models.EntityInfoResponse;
+import unsw.utils.Angle;
+
+import java.util.Arrays;
+
+import static blackout.TestHelpers.assertListAreEqualIgnoringOrder;
+
+@TestInstance(value = Lifecycle.PER_CLASS)
+public class Task1Tests {
+ @Test
+ public void testCreateDeviceList() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+ controller.createDevice("DeviceB", "LaptopDevice", Angle.fromDegrees(310));
+ assertListAreEqualIgnoringOrder(Arrays.asList("DeviceA", "DeviceB"), controller.listDeviceIds());
+ }
+
+ @Test
+ public void testCreateSatelliteList() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createSatellite("S1", "StandardSatellite", 0, Angle.fromDegrees(310));
+ controller.createSatellite("S2", "StandardSatellite", 0, Angle.fromDegrees(310));
+ assertListAreEqualIgnoringOrder(Arrays.asList("S1", "S2"), controller.listSatelliteIds());
+ }
+
+ @Test
+ public void testRemoveDeviceList() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+ controller.createDevice("DeviceB", "LaptopDevice", Angle.fromDegrees(310));
+ assertListAreEqualIgnoringOrder(Arrays.asList("DeviceA", "DeviceB"), controller.listDeviceIds());
+
+ controller.removeDevice("DeviceB");
+ assertListAreEqualIgnoringOrder(Arrays.asList("DeviceA"), controller.listDeviceIds());
+ }
+
+ @Test
+ public void testRemoveSatelliteList() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createSatellite("S1", "StandardSatellite", 0, Angle.fromDegrees(310));
+ controller.createSatellite("S2", "StandardSatellite", 0, Angle.fromDegrees(310));
+ assertListAreEqualIgnoringOrder(Arrays.asList("S1", "S2"), controller.listSatelliteIds());
+
+ controller.removeSatellite("S2");
+ assertListAreEqualIgnoringOrder(Arrays.asList("S1"), controller.listSatelliteIds());
+ }
+
+ @Test
+ public void testAddFileToDeviceInfoResponse() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+ assert(controller.getInfo("DeviceA").getFiles().size() == 0);
+ controller.addFileToDevice("DeviceA", "filename", "contents");
+ assert(controller.getInfo("DeviceA").getFiles().size() == 1);
+ }
+
+ @Test
+ public void testInfoResponse() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createSatellite("Satellite1", "StandardSatellite", 314159, Angle.fromDegrees(310));
+ EntityInfoResponse info = controller.getInfo("Satellite1");
+ assert(info.getDeviceId().equals("Satellite1"));
+ assert(info.getPosition().equals(Angle.fromDegrees(310)));
+ assert(info.getHeight() == 314159);
+ assert(info.getType().equals("StandardSatellite"));
+ assert(info.getFiles().size() == 0);
+ }
+}
diff --git a/comp2511/blackout/tests/Task2Tests.java b/comp2511/blackout/tests/Task2Tests.java
new file mode 100644
index 0000000..5e8d427
--- /dev/null
+++ b/comp2511/blackout/tests/Task2Tests.java
@@ -0,0 +1,206 @@
+package blackout;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+import unsw.blackout.BlackoutController;
+import unsw.blackout.FileTransferException;
+import unsw.utils.Angle;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import static unsw.utils.MathsHelper.RADIUS_OF_JUPITER;
+
+@TestInstance(value = Lifecycle.PER_CLASS)
+public class Task2Tests {
+ @Test
+ public void testDownloadSpeed() {
+ BlackoutController controller = new BlackoutController();
+
+ // The expected download speed should be (download speed / num transferring files), with the remainder ignored.
+ controller.createSatellite("Satellite1", "ShrinkingSatellite", 1000 + RADIUS_OF_JUPITER, Angle.fromDegrees(320));
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+ controller.createDevice("DeviceB", "LaptopDevice", Angle.fromDegrees(310));
+
+ String contents = "a relatively long string";
+ controller.addFileToDevice("DeviceA", "file1", contents);
+ controller.addFileToDevice("DeviceB", "file2", contents);
+
+ assertDoesNotThrow(() -> controller.sendFile("file1", "DeviceA", "Satellite1"));
+ assertDoesNotThrow(() -> controller.sendFile("file2", "DeviceB", "Satellite1"));
+
+ // shrinking satellite is 15 bytes per tick, so should be 15/2 = 7.
+ controller.simulate();
+ assertEquals(7, controller.getInfo("Satellite1").getFiles().get("file1").getData().length());
+ assertEquals(7, controller.getInfo("Satellite1").getFiles().get("file2").getData().length());
+
+ // again, should be 14
+ controller.simulate();
+ assertEquals(14, controller.getInfo("Satellite1").getFiles().get("file1").getData().length());
+ assertEquals(14, controller.getInfo("Satellite1").getFiles().get("file2").getData().length());
+ }
+
+ @Test
+ public void testDeleteFiles() {
+ BlackoutController controller = new BlackoutController();
+
+ // Files that are being transferred should be deleted if they are in transmission, and kept if they are not.
+ controller.createSatellite("Satellite1", "StandardSatellite", 1000 + RADIUS_OF_JUPITER, Angle.fromDegrees(310));
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+
+ // Upload a file to the satellite, should be there after 1 tick.
+ controller.addFileToDevice("DeviceA", "file1", "1");
+ assertDoesNotThrow(() -> controller.sendFile("file1", "DeviceA", "Satellite1"));
+ controller.simulate();
+ assert(controller.getInfo("Satellite1").getFiles().size() == 1);
+
+ // Upload another file, which will not transmit in time.
+ controller.addFileToDevice("DeviceA", "file2", "this string will take a fair amount of ticks to transmit");
+ assertDoesNotThrow(() -> controller.sendFile("file2", "DeviceA", "Satellite1"));
+ controller.simulate();
+ assert(controller.getInfo("Satellite1").getFiles().size() == 2);
+
+ for (int i = 0; i < 50; ++i) {
+ controller.simulate();
+ }
+ // At this point we should be occluded and out of range,
+ // while the file was in transmission - zero files should be present.
+ assert(controller.getInfo("Satellite1").getFiles().size() == 1);
+ }
+
+ @Test
+ public void testTransientDeleteFile() {
+ BlackoutController controller = new BlackoutController();
+
+ // Files should NOT be deleted after occlusion or running out of distance.
+ controller.createSatellite("Satellite1", "ElephantSatellite", 1000 + RADIUS_OF_JUPITER, Angle.fromDegrees(330));
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(310));
+
+ // Upload a file which will not transmit in time.
+ controller.addFileToDevice("DeviceA", "file1", "this string will take a fair amount of ticks to transmit but just to be sure");
+ assertDoesNotThrow(() -> controller.sendFile("file1", "DeviceA", "Satellite1"));
+ controller.simulate();
+ assert(controller.getInfo("Satellite1").getFiles().size() == 1);
+ assert(controller.getInfo("Satellite1").getFiles().get("file1").hasTransferCompleted() == false);
+
+ // The file should NOT be deleted after this period.
+ // In addition the file should have transferred fully by this time.
+ for (int i = 0; i < 180; ++i) {
+ controller.simulate();
+ }
+ assert(controller.getInfo("Satellite1").getFiles().size() == 1);
+ assert(controller.getInfo("Satellite1").getFiles().get("file1").hasTransferCompleted() == true);
+ }
+
+ @Test
+ public void testRelayAccess() {
+ BlackoutController controller = new BlackoutController();
+
+ // We create two devices on one side of the planet, they should not be able to communicate.
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(180));
+ controller.createDevice("DeviceB", "LaptopDevice", Angle.fromDegrees(0));
+ assert(controller.communicableEntitiesInRange("DeviceA").size() == 0);
+ assert(controller.communicableEntitiesInRange("DeviceB").size() == 0);
+
+ // We create a chain of relays that should connect the two devices.
+ int OFFSET = 15_000;
+ int NUM_SATELLITES = 5;
+ for (int i = 0; i < NUM_SATELLITES; ++i) {
+ controller.createSatellite("Satellite" + i, "RelaySatellite", OFFSET + RADIUS_OF_JUPITER, Angle.fromDegrees(180 - i * (180 / NUM_SATELLITES)));
+ }
+
+ // We should be able to communicate with all relays, plus the other device.
+ assert(controller.communicableEntitiesInRange("DeviceA").size() == NUM_SATELLITES + 1);
+ assert(controller.communicableEntitiesInRange("DeviceB").size() == NUM_SATELLITES + 1);
+ }
+
+ @Test
+ public void testCloudStorageDevice() {
+ BlackoutController controller = new BlackoutController();
+
+ // The size of the string in cloudstoragedevice should be the same in the satellite
+ // but larger after transfer into the laptop device. (Files are only decompressed
+ // inside devices (minus cloudstoragedevice).
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(180));
+ controller.createDevice("DeviceB", "CloudStorageDevice", Angle.fromDegrees(180));
+ controller.createSatellite("Satellite1", "ShrinkingSatellite", 10000 + RADIUS_OF_JUPITER, Angle.fromDegrees(180));
+
+ // Compression isn't guaranteed, but if we repeat digits we can improve our chances.
+ // Send the file to the satellite, it should be the same size as in the cloudstoragedevice.
+ controller.addFileToDevice("DeviceB", "file", "biiiiiiiiiiiiiiiiiiiiiiiig");
+ assertDoesNotThrow(() -> controller.sendFile("file", "DeviceB", "Satellite1"));
+ controller.simulate(); // should arrive in two ticks
+ controller.simulate();
+ assert(controller.getInfo("Satellite1").getFiles().get("file").getFileSize() == controller.getInfo("DeviceB").getFiles().get("file").getFileSize());
+
+ assertDoesNotThrow(() -> controller.sendFile("file", "Satellite1", "DeviceA"));
+ controller.simulate();
+ // TODO make <
+ assert(controller.getInfo("Satellite1").getFiles().get("file").getFileSize() <= controller.getInfo("DeviceA").getFiles().get("file").getFileSize());
+ }
+
+ @Test
+ public void testSendFileExceptions() {
+ BlackoutController controller = new BlackoutController();
+
+ controller.createDevice("DeviceA", "LaptopDevice", Angle.fromDegrees(180));
+ controller.createDevice("DeviceB", "LaptopDevice", Angle.fromDegrees(180));
+ controller.createSatellite("Satellite1", "ShrinkingSatellite", 15000 + RADIUS_OF_JUPITER, Angle.fromDegrees(180));
+
+ // VirtualFileNotFoundException if the file doesn't exist on fromID
+ assertThrows(FileTransferException.VirtualFileNotFoundException.class, () -> controller.sendFile("?", "DeviceA", "Satellite1"));
+ // ... or it's a partial file.
+ controller.addFileToDevice("DeviceA", "file", "contents");
+ assertDoesNotThrow(() -> controller.sendFile("file", "DeviceA", "Satellite1"));
+ assertThrows(FileTransferException.VirtualFileNotFoundException.class, () -> controller.sendFile("file", "Satellite1", "DeviceB"));
+ controller.simulate(); // make the file arrive
+ assert(controller.getInfo("Satellite1").getFiles().get("file").hasTransferCompleted());
+
+ // VirtualFileAlreadyExistsException if the file already exists on targetId.
+ assertThrows(FileTransferException.VirtualFileAlreadyExistsException.class, () -> controller.sendFile("file", "Satellite1", "DeviceA"));
+ // or if it's currently downloading
+ controller.addFileToDevice("DeviceA", "file1", "contents");
+ assertDoesNotThrow(() -> controller.sendFile("file1", "DeviceA", "Satellite1"));
+ controller.simulate();
+ assert(controller.getInfo("Satellite1").getFiles().get("file1").hasTransferCompleted());
+ assertDoesNotThrow(() -> controller.sendFile("file1", "Satellite1", "DeviceB"));
+ assert(controller.getInfo("DeviceB").getFiles().get("file1").hasTransferCompleted() == false);
+ assertThrows(FileTransferException.VirtualFileAlreadyExistsException.class, () -> controller.sendFile("file1", "Satellite1", "DeviceB"));
+ controller.simulate(); // send all files
+
+ // At this point our satellite has 0 files currently downloading and 2 files stored.
+ // VirtualFileNoBandwidthException if we are sending too many files.
+ // 15 bytes / tick = max of 15 files.
+ assertDoesNotThrow(() -> {
+ for (int i = 2; i < 15 + 2; ++i) {
+ controller.addFileToDevice("DeviceA", "file" + i, "contents");
+ controller.sendFile("file" + i, "DeviceA", "Satellite1");
+ }
+ });
+
+ controller.addFileToDevice("DeviceA", "too many files file", "contents");
+ assertThrows(FileTransferException.VirtualFileNoBandwidthException.class, () -> controller.sendFile("too many files file", "DeviceA", "Satellite1"));
+
+ // VirtualFileNoStorageSpaceException should throw if the lack of room was due to a max file cap
+ // max file cap of three for StandardSatellite, we have to be careful to avoid it's bandwidth limits
+ controller.createSatellite("Satellite2", "StandardSatellite", 15000 + RADIUS_OF_JUPITER, Angle.fromDegrees(180));
+ assertDoesNotThrow(() -> {
+ for (int i = 0; i < 3; ++i) {
+ controller.addFileToDevice("DeviceA", "unique_file" + i, "1");
+ controller.sendFile("unique_file" + i, "DeviceA", "Satellite2");
+ controller.simulate();
+ }
+ });
+
+ controller.addFileToDevice("DeviceA", "unique_file3", "contents");
+ assertThrows(FileTransferException.VirtualFileNoStorageSpaceException.class, () -> controller.sendFile("unique_file3", "DeviceA", "Satellite2"));
+
+ // ... or if we exceed the storage byte limit
+ controller.createSatellite("Satellite3", "StandardSatellite", 15000 + RADIUS_OF_JUPITER, Angle.fromDegrees(180));
+ controller.addFileToDevice("DeviceA", "big_file", "a very long string that definitely exceeds the max byte storage amount of satellite3");
+ assertThrows(FileTransferException.VirtualFileNoStorageSpaceException.class, () -> controller.sendFile("big_file", "DeviceA", "Satellite3"));
+ }
+} \ No newline at end of file
diff --git a/comp2511/blackout/tests/TestHelpers.java b/comp2511/blackout/tests/TestHelpers.java
new file mode 100644
index 0000000..913784b
--- /dev/null
+++ b/comp2511/blackout/tests/TestHelpers.java
@@ -0,0 +1,14 @@
+package blackout;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.util.Collections;
+import java.util.List;
+
+public class TestHelpers {
+ public static<T extends Comparable<? super T>> void assertListAreEqualIgnoringOrder(List<T> a, List<T> b) {
+ Collections.sort(a);
+ Collections.sort(b);
+ assertArrayEquals(a.toArray(), b.toArray());
+ }
+}