From afe60d0dd7e5de437423386e32cab60554ab788a Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:04:48 +0000 Subject: [PATCH] Initial commit --- .github/workflows/java-sbt.yml | 21 +++ .gitignore | 46 ++++++ .gitpod.yml | 10 ++ README.md | 70 +++++++++ build.sbt | 30 ++++ project/plugins.sbt | 4 + .../luc/cs271/arrayqueue/FixedArrayQueue.java | 81 ++++++++++ .../edu/luc/cs271/arrayqueue/SimpleQueue.java | 88 +++++++++++ .../cs271/arrayqueue/SingleQueueService.java | 59 +++++++ .../luc/cs271/arrayqueue/TestSimpleQueue.java | 147 ++++++++++++++++++ .../arrayqueue/TestSimpleQueueJqwik.java | 58 +++++++ 11 files changed, 614 insertions(+) create mode 100644 .github/workflows/java-sbt.yml create mode 100644 .gitignore create mode 100644 .gitpod.yml create mode 100644 README.md create mode 100644 build.sbt create mode 100644 project/plugins.sbt create mode 100644 src/main/java/edu/luc/cs271/arrayqueue/FixedArrayQueue.java create mode 100644 src/main/java/edu/luc/cs271/arrayqueue/SimpleQueue.java create mode 100644 src/main/java/edu/luc/cs271/arrayqueue/SingleQueueService.java create mode 100644 src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueue.java create mode 100644 src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueueJqwik.java diff --git a/.github/workflows/java-sbt.yml b/.github/workflows/java-sbt.yml new file mode 100644 index 0000000..1e2aebb --- /dev/null +++ b/.github/workflows/java-sbt.yml @@ -0,0 +1,21 @@ +name: Java sbt CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Run tests + run: sbt jacoco diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c095fe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +logs/ +proguard_cache/ +target/ +tmp/ + +# Local configuration file (sdk path, etc) +local.properties +.cache/ +.classpath +.project +.settings/ +*.iml +.idea/ +local.sbt +.bsp/ +.bloop/ +.metals/ +.vscode/ +project/.bloop/ +project/metals.sbt +project/project/ +.jqwik-database + +# Emacs files +*~ +\#*\# + +# Mac files +.DS_Store + +# SBT configuration files +# remove if specific version of SBT is required +project/build.properties diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..914dd1b --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,10 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) +# and commit this file to your remote git repository to share the goodness with others. + +tasks: + - init: | + echo "sdkman_auto_answer=true" >> $HOME/.sdkman/etc/config + sdk install java 17.0.10-tem + sdk install sbt + sbt "Test / compile" diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd8cf7c --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Stateful unit testing using Jqwik actions: fixed-size circular queue + +In this project, we'll implement and thoroughly test our own array-based data structure, a fixed-size circular queue! + +## Objectives + +An understanding of the following concepts and techniques: + +- ADT implementation perspective +- queue ADT (see also [java.util.Queue](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Queue.html)) +- implementing queue as a circular array +- queues with fixed versus growing capacity +- algorithms based on the queue's FIFO policy +- interface-based testing +- stateful property-based testing +- initial exposure to concurrency + +## Instructions + +In this project, you will have the opportunity to implement a generic queue as a circular array and use this implementation in the context of a typical queue-based application. + +Specifically: + +1. Complete the TODO items in the `FixedArrayQueue` implementation until the tests in `TestSimpleQueue` pass. +1. Complete the main class `SingleQueueService`, which reads successive input lines until EOF and +puts them on a queue that the background consumer activity processes. +1. When running the main class, note that the consumer is set to serve customers at a fixed rate. +By entering customers' names at different rates, try to create scenarios where customers arrive infrequently enough for the queue to remain empty, or in such quick succession that the queue becomes full (by pasting several of them at once). +1. Complete the TODO items in the `TestSimpleQueueJqwik` test suite until it reflects the stated pre- and postconditions of the `SimpleQueue` methods. +1. Answer the following questions in a separate file `Answers.md`: + - Why does `FixedArrayQueue` require an explicit constructor? + - What happens when you offer an item to a full `FixedArrayQueue`? + - What happens when you poll an empty `FixedArrayQueue`? + - What is the time and (extra) space complexity of each of the `FixedArrayQueue` methods? + - How exhaustively does the `TestSimpleQueue` test suite test the implementation, both conceptually and in terms of actual coverage? + - How exhaustively does the `TestSimpleQueueJqwik` test suite test the implementation, both conceptually and in terms of actual coverage? + - What kind of test cases does the `simpleQueueActions` method generate? + +## Running the code + +To run the tests: + + sbt jacoco + +To run the main program: + + sbt run + +## Grading (total 10 points) + +- 3.5 completion of items marked TODO in `FixedArrayQueue` and tests passing +- 1 completion of `SingleQueueService` and correct behavior +- 3.5 completion of items marked TODO in `TestSimpleQueueJqwik` and working +- 2 written part + - 1.5 responses to the questions above + - 0.5 grammar, style, formatting + +## Extra credit + +Clearly indicate in your `Answers.md` file any extra credit attempted. + +- 0.5 apply `checkSimpleQueue` property to different queue capacities +- 1 add a `clear` method, which removes all elements in the queue, to interface, impmlementation, and tests + +## References + +- [java.util.Queue interface](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Queue.html) +- [Introduction and Array Implementation of Circular Queue](https://www.geeksforgeeks.org/introduction-and-array-implementation-of-circular-queue) +- [Zombies testing](https://hackernoon.com/zombie-testing-one-behavior-at-a-time-9s2m3zjo) +- [Zombies testing with circular queue/buffer example](http://blog.wingman-sw.com/tdd-guided-by-zombies) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..4422f68 --- /dev/null +++ b/build.sbt @@ -0,0 +1,30 @@ +name := "arrayqueue-jqwik-java-sbt" + +version := "0.4" + +compile / javacOptions += "-Xlint:all" + +javaOptions += "-enableassertions" + +ThisBuild / libraryDependencies ++= Seq( + "net.aichler" % "jupiter-interface" % "0.11.1" % Test, + "net.jqwik" % "jqwik" % "1.8.2" % Test +) + +Test / parallelExecution := false + +jacocoReportSettings := JacocoReportSettings() + .withThresholds( + JacocoThresholds( + instruction = 80, + method = 100, + branch = 100, + complexity = 100, + line = 90, + clazz = 100) + ) + .withFormats(JacocoReportFormats.HTML) + +jacocoExcludes := Seq("**.SingleQueueService*") + +enablePlugins(JavaAppPackaging) diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..27e79ca --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,4 @@ +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") +addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.11.1") +addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.4.0") +addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") diff --git a/src/main/java/edu/luc/cs271/arrayqueue/FixedArrayQueue.java b/src/main/java/edu/luc/cs271/arrayqueue/FixedArrayQueue.java new file mode 100644 index 0000000..46eeaab --- /dev/null +++ b/src/main/java/edu/luc/cs271/arrayqueue/FixedArrayQueue.java @@ -0,0 +1,81 @@ +package edu.luc.cs271.arrayqueue; + +import java.util.ArrayList; +import java.util.List; + +public class FixedArrayQueue implements SimpleQueue { + + private final int capacity; + + private int size; + + private int front; + + private int rear; + + private final E[] data; + + // TODO why do we need an explicit constructor? + + @SuppressWarnings("unchecked") + public FixedArrayQueue(final int capacity) { + // TODO check argument validity + + this.capacity = capacity; + this.data = (E[]) new Object[capacity]; + this.size = 0; + this.front = 0; + this.rear = capacity - 1; + } + + @Override + public boolean offer(final E obj) { + // TODO + + return false; + } + + @Override + public E peek() { + // TODO + + return null; + } + + @Override + public E poll() { + // TODO + + return null; + } + + @Override + public boolean isEmpty() { + // TODO + return true; + } + + @Override + public boolean isFull() { + // TODO + return false; + } + + @Override + public int size() { + return size; + } + + @Override + public int capacity() { + return capacity; + } + + @Override + public List asList() { + // TODO implement using an ArrayList preallocated with the right size + final ArrayList result = null; + + return result; + } +} diff --git a/src/main/java/edu/luc/cs271/arrayqueue/SimpleQueue.java b/src/main/java/edu/luc/cs271/arrayqueue/SimpleQueue.java new file mode 100644 index 0000000..d01d6af --- /dev/null +++ b/src/main/java/edu/luc/cs271/arrayqueue/SimpleQueue.java @@ -0,0 +1,88 @@ +package edu.luc.cs271.arrayqueue; + +import java.util.List; + +/** + * Generic interface for a first-in-first-out (FIFO) data structure where objects are inserted into + * and removed from opposeite ends. + * + *

Based on Koffman & Wolfgang, Data Structures 3rd ed, Wiley 2015. + * + * @param The element type + */ +public interface SimpleQueue { + /** + * Adds an item to the end of the queue and returns a boolean to indicate whether the attempt + * succeded. + * + * @pre True. + * @post The queue is nonempty and one item larger than before. + * @param obj The object to be inserted + * @return true if the object has been inserted, false otherwise + */ + boolean offer(E obj); + + /** + * Returns the object at the front of the queue without removing it. + * + * @pre True. + * @post The queue remains unchanged. + * @return The object at the front of the queue if one exists, null otherwise + */ + E peek(); + + /** + * Returns the object at the front of the queue and removes it. + * + * @pre True. + * @post The queue is one item smaller than before. + * @return The object at the front of the queue if one exists, null otherwise + */ + E poll(); + + /** + * Returns true if the queue is empty; otherwise, returns false. + * + * @pre True. + * @post The queue remains unchanged. + * @return true if the queue is empty, false otherwise + */ + boolean isEmpty(); + + /** + * Returns true if the queue is full; otherwise, returns false. + * + * @pre True. + * @post The queue remains unchanged. + * @return true if the queue is full, false otherwise + */ + boolean isFull(); + + /** + * Returns the size of the queue. + * + * @pre True. + * @post The queue remains unchanged. + * @return the size of the queue + */ + int size(); + + /** + * Returns the capacity (maximum size) of the queue. + * + * @pre True. + * @post The queue remains unchanged. + * @return the capacity of the queue + */ + int capacity(); + + /** + * Returns a Java list containing the items currently in the queue. The item at the front of the + * queue is the first item of the list (at index 0). + * + * @pre True. + * @post The queue remains unchanged. + * @return The list containing the items in the queue + */ + List asList(); +} diff --git a/src/main/java/edu/luc/cs271/arrayqueue/SingleQueueService.java b/src/main/java/edu/luc/cs271/arrayqueue/SingleQueueService.java new file mode 100644 index 0000000..b57726d --- /dev/null +++ b/src/main/java/edu/luc/cs271/arrayqueue/SingleQueueService.java @@ -0,0 +1,59 @@ +package edu.luc.cs271.arrayqueue; + +import java.util.Scanner; + +public class SingleQueueService { + + /** Service time per customer in ms. */ + static final int SERVICE_TIME = 2000; + + /** Reads successive input lines until EOF and tries to add them to the queue for processing. */ + public static void main(final String[] args) throws InterruptedException { + // queue for customer names + final SimpleQueue queue = new FixedArrayQueue<>(5); + + // lock object for thread safety + final var lock = new Object(); + + // background activity for serving customers + final Thread consumer = + new Thread(() -> { + while (true) { + String current; + int remaining; + synchronized (lock) { + current = null; // TODO try to take next name from queue + remaining = 0; // TODO determine resulting size of queue + } + if (current == null) { + System.out.println("no one waiting"); + } else { + System.out.println(current + " is being served, " + remaining + " still waiting"); + } + try { + Thread.sleep(SERVICE_TIME); + } catch (final InterruptedException ex) { + return; + } + } + }); + consumer.setDaemon(true); + consumer.start(); + + // foreground activity for reading customer names from input + final var input = new Scanner(System.in); + System.out.print("enter next customer: "); + while (input.hasNextLine()) { + final var name = input.nextLine(); + var result = false; + synchronized (lock) { + // TODO try to add this name to the queue + } + if (result) { + System.out.println(name + " has joined the queue"); + } else { + System.out.println("queue full, " + name + " unable to join"); + } + } + } +} diff --git a/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueue.java b/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueue.java new file mode 100644 index 0000000..a690b8a --- /dev/null +++ b/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueue.java @@ -0,0 +1,147 @@ +package edu.luc.cs271.arrayqueue; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +class TestSimpleQueue { + + private SimpleQueue fixture; + + @BeforeEach + void setUp() { + fixture = new FixedArrayQueue<>(2); + } + + @AfterEach + void tearDown() { + fixture = null; + } + + @Test + void testNegativeCapacity() { + assertThrows( + IllegalArgumentException.class, + () -> new FixedArrayQueue<>(-12) + ); + } + + @Test + void testInitial() { + assertTrue(fixture.isEmpty()); + assertFalse(fixture.isFull()); + assertEquals(0, fixture.size()); + assertEquals(2, fixture.capacity()); + assertNull(fixture.peek()); + assertNull(fixture.poll()); + } + + @Test + void testAfterOffer() { + final var value = "hello"; + assertTrue(fixture.offer(value)); + assertFalse(fixture.isEmpty()); + assertEquals(1, fixture.size()); + assertEquals(value, fixture.peek()); + } + + @Test + void testOfferThenPoll() { + final var value = "hello"; + assertTrue(fixture.offer(value)); + assertEquals(value, fixture.poll()); + assertTrue(fixture.isEmpty()); + } + + @Test + void testOffer2ThenPoll2() { + final var value1 = "hello"; + final var value2 = "world"; + assertTrue(fixture.offer(value1)); + assertTrue(fixture.offer(value2)); + assertTrue(fixture.isFull()); + assertEquals(value1, fixture.poll()); + assertEquals(value2, fixture.poll()); + assertTrue(fixture.isEmpty()); + } + + @Test + void testOffer3Poll3() { + final var value1 = "hello"; + final var value2 = "world"; + final var value3 = "what"; + assertTrue(fixture.offer(value1)); + assertTrue(fixture.offer(value2)); + assertEquals(value1, fixture.poll()); + assertTrue(fixture.offer(value3)); + assertEquals(value2, fixture.poll()); + assertEquals(value3, fixture.poll()); + assertTrue(fixture.isEmpty()); + } + + @Test + void testOffer5Poll5() { + final var value1 = "hello"; + final var value2 = "world"; + final var value3 = "what"; + final var value4 = "up"; + final var value5 = "today"; + assertTrue(fixture.offer(value1)); + assertTrue(fixture.offer(value2)); + assertEquals(value1, fixture.poll()); + assertTrue(fixture.offer(value3)); + assertEquals(value2, fixture.poll()); + assertEquals(value3, fixture.poll()); + assertTrue(fixture.offer(value4)); + assertTrue(fixture.offer(value5)); + assertEquals(value4, fixture.poll()); + assertEquals(value5, fixture.poll()); + assertTrue(fixture.isEmpty()); + } + + @Test + void testOffer3() { + final var value1 = "hello"; + final var value2 = "world"; + final var value3 = "what"; + assertTrue(fixture.offer(value1)); + assertTrue(fixture.offer(value2)); + assertFalse(fixture.offer(value3)); + assertEquals(2, fixture.size()); + } + + @Test + void testAsListEmpty() { + assertEquals(0, fixture.asList().size()); + } + + @Test + void testAsListNonempty() { + final var value1 = "hello"; + final var value2 = "world"; + fixture.offer(value1); + fixture.offer(value2); + final var list = fixture.asList(); + assertEquals(2, list.size()); + assertEquals(List.of(value1, value2), list); + } + + @Test + void testAsListNonempty2() { + final var value1 = "hello"; + final var value2 = "world"; + final var value3 = "what"; + fixture.offer(value1); + fixture.offer(value2); + fixture.poll(); + fixture.offer(value3); + final var list = fixture.asList(); + assertEquals(2, fixture.size()); + assertEquals(2, list.size()); + assertEquals(List.of(value2, value3), list); + } +} diff --git a/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueueJqwik.java b/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueueJqwik.java new file mode 100644 index 0000000..11a2185 --- /dev/null +++ b/src/test/java/edu/luc/cs271/arrayqueue/TestSimpleQueueJqwik.java @@ -0,0 +1,58 @@ +package edu.luc.cs271.arrayqueue; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +import net.jqwik.api.*; +import net.jqwik.api.constraints.*; +import net.jqwik.api.state.*; + + +class TestSimpleQueueJqwik { + + class OfferAction implements Action.Independent> { + @Override + public boolean precondition(final SimpleQueue queue) { + // TODO implement precondition for offer method + return true; + } + @Override + public Arbitrary>> transformer() { + final var offerElements = Arbitraries.strings().alpha().ofLength(5); + return offerElements.map(element -> Transformer.mutate( + String.format("offer(%s)", element), + queue -> { + // TODO capture state before offer, perform, and check postcondition + + } + )); + } + } + + private Action> poll() { + return Action.>builder() + .describeAs("poll") + .justMutate(queue -> { + // TODO capture state before poll, perform, and check postcondition + + }); + } + + // TODO extra credit: apply property to different queue capacities + @Property + void checkSimpleQueue(@ForAll("simpleQueueActions") final ActionChain> chain) { + // TODO insert observable data invariant(s) for 0 <= size <= capacity + chain + + .run(); + } + + @Provide + Arbitrary>> simpleQueueActions() { + return ActionChain + .>startWith(() -> new FixedArrayQueue(1)) + .withAction(new OfferAction()) + .withAction(poll()); + } +}