diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..16cde4f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Java CI + +on: + push: + pull_request: + +jobs: + quality: + name: Ant Quality Suite + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Install Ant + run: sudo apt-get update && sudo apt-get install -y ant + + - name: Run quality checks + run: ant quality diff --git a/.gitignore b/.gitignore index f1a3e5f..3642169 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ # JAR files everywhere EXCEPT lib/ *.jar !lib/*.jar +lib/tools/ +tools-cache/ # Ant build output build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dc2f739 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing + +This repository is intended for classroom use. Keep changes small, readable, +and easy for students to inspect. + +## Before Submitting Work + +Run the test suite: + +```bash +ant test +``` + +For larger changes, run the full build: + +```bash +ant all +``` + +To run every verification tool without rebuilding the Javadoc and JAR: + +```bash +ant quality +``` + +## Coding Expectations + +- Use clear names that match the simulation domain being modeled. +- Prefer simple control flow over clever shortcuts. +- Add or update tests when behavior changes. +- Keep generated files out of commits. The `build/` and `docs/` directories + are created by Ant and should remain untracked. + +## Suggested Student Workflow + +1. Pull the latest version of the repository. +2. Run `ant test` before making changes. +3. Make one focused change. +4. Run `ant test` again. +5. Run `ant quality` before submitting larger work. +6. Commit with a short message that explains the behavior changed. diff --git a/README.md b/README.md index b7494ae..7e8aa4c 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,17 @@ SimulationModeling/ | `ant test` | Compile and run JUnit 5 tests | | `ant javadoc` | Generate API docs in `docs/` | | `ant jar` | Package classes into `build/simulation.jar` | -| `ant all` | Full pipeline: clean → compile → test → javadoc → jar | +| `ant checkstyle` | Run style checks | +| `ant pmd` | Run PMD source analysis | +| `ant spotbugs` | Run SpotBugs bytecode analysis | +| `ant coverage` | Run tests and generate JaCoCo coverage reports | +| `ant quality` | Run tests, Checkstyle, PMD, SpotBugs, and JaCoCo | +| `ant all` | Full pipeline: clean → quality → javadoc → jar | | `ant clean` | Remove all generated files | | `ant resolve` | Download JUnit 5 JARs to `lib/` (needed only if lib/ is absent) | +| `ant resolve-tools` | Download quality-tool distributions to `lib/tools/` | -## Quick Start +## Quick Start for Students ```bash # 1. Clone the repository @@ -62,14 +68,32 @@ cd SimulationModeling # 2. Compile and run tests ant test -# 3. Build everything (clean + compile + test + javadoc + jar) +# 3. Try the small example simulation +java -cp build/classes simulation.examples.CountdownSimulation + +# 4. Run the full quality pipeline +ant quality + +# 5. Build everything (clean + quality + javadoc + jar) ant all -# 4. Open the generated API documentation +# 6. Open the generated API documentation open docs/index.html # macOS xdg-open docs/index.html # Linux ``` +Expected example output: + +```text +time 0.0: 5 +time 1.0: 4 +time 2.0: 3 +time 3.0: 2 +time 4.0: 1 +``` + +If `ant test` succeeds, your development environment is ready. + ## Extending the Simulation Framework Subclass `Simulation` and `Event` to model your problem: @@ -97,9 +121,28 @@ public class QueueingSimulation extends Simulation { } ``` -## Optional Build Tools +See `src/main/java/simulation/examples/CountdownSimulation.java` for a +complete runnable example. + +## Suggested First Student Tasks + +1. Run `ant test` and confirm all tests pass. +2. Run `simulation.examples.CountdownSimulation`. +3. Change the countdown length and rerun the example. +4. Create a new simulation package for your assigned model. +5. Add tests for any behavior you add or change. -`build.xml` contains commented-out targets for: +## Repository Hygiene + +- Commit source files, tests, documentation, and build configuration. +- Do not commit generated `build/` or `docs/` files. +- Keep dependency JARs in `lib/` unless your instructor gives different + course-specific instructions. +- Run `ant test` before each commit. + +## Quality Tools + +The project includes enabled Ant targets for: | Tool | Purpose | |------|---------| @@ -108,8 +151,16 @@ public class QueueingSimulation extends Simulation { | **PMD** | Source code analysis | | **JaCoCo** | Test coverage reporting | -To enable any of these, download the corresponding JAR(s) to `lib/` and -uncomment the relevant `` in `build.xml`. +Run everything with: + +```bash +ant quality +``` + +The first run downloads tool distributions into `lib/tools/`. Those files are +local build support and are ignored by Git. Reports are written under +`build/reports/`, including JaCoCo HTML coverage at +`build/reports/coverage/index.html`. ## Running Tests Independently diff --git a/build.properties b/build.properties index 6e0ca9d..e7ffbc4 100644 --- a/build.properties +++ b/build.properties @@ -3,8 +3,7 @@ # ============================================================ # --- Java compiler settings --- -java.source.version = 17 -java.target.version = 17 +java.release.version = 17 # --- Directory layout --- src.main.dir = src/main/java @@ -22,3 +21,21 @@ jar.file = ${build.dir}/simulation.jar # --- Project metadata (used in Javadoc) --- project.name = SimulationModeling project.version = 1.0 + +# --- Optional quality tool versions --- +checkstyle.version = 10.21.4 +spotbugs.version = 4.9.7 +pmd.version = 7.10.0 +jacoco.version = 0.8.12 +junit.platform.version = 1.10.2 + +# --- Optional quality tool locations --- +tools.dir = ${lib.dir}/tools +downloads.dir = ${tools.dir}/downloads +checkstyle.jar = ${tools.dir}/checkstyle/checkstyle-${checkstyle.version}-all.jar +spotbugs.home = ${tools.dir}/spotbugs/spotbugs-${spotbugs.version} +pmd.home = ${tools.dir}/pmd/pmd-bin-${pmd.version} +jacoco.home = ${tools.dir}/jacoco +jacoco.ant.jar = ${jacoco.home}/lib/jacocoant.jar +junit.console.jar = ${tools.dir}/junit/junit-platform-console-standalone-${junit.platform.version}.jar +jacoco.exec.file = ${reports.dir}/jacoco.exec diff --git a/build.xml b/build.xml index f465ac1..0397736 100644 --- a/build.xml +++ b/build.xml @@ -3,7 +3,8 @@ build.xml — Apache Ant build file for SimulationModeling ========================================================= Requires: Apache Ant 1.10.6+, JDK 17+ - JARs in lib/ are managed manually; run "ant resolve" once to download them. + JUnit JARs in lib/ are committed for classroom convenience. Quality tool + distributions are downloaded on demand to lib/tools/ by "ant resolve-tools". Main targets ============ @@ -13,18 +14,17 @@ test Run JUnit 5 tests; reports in build/reports javadoc Generate Javadoc into docs/ jar Package compiled classes into build/simulation.jar + checkstyle Run style checks + pmd Run PMD source analysis + spotbugs Run SpotBugs bytecode analysis + coverage Run tests with JaCoCo coverage + quality Run tests plus all quality tools clean Delete all generated files - all clean → compile → test → javadoc → jar - - Optional / commented-out targets - ================================= - checkstyle Style checker (uncomment and add checkstyle jar to lib/) - spotbugs Bug finder (uncomment and add spotbugs jars to lib/) - pmd PMD analyzer (uncomment and add pmd jars to lib/) - jacoco Code coverage (uncomment and add jacoco jars to lib/) + all clean → quality → javadoc → jar --> + xmlns:junitlauncher="antlib:org.apache.tools.ant.taskdefs.optional.junitlauncher" + xmlns:jacoco="antlib:org.jacoco.ant"> @@ -36,6 +36,23 @@ + + + + + + + + + + + + + + + + + @@ -82,6 +99,45 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -96,8 +152,7 @@ @@ -185,100 +239,101 @@ - + - - - - - + - - - - - + - - - - - + - - - - - - - + - + + + + + diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..2613d49 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/pmd-ruleset.xml b/pmd-ruleset.xml new file mode 100644 index 0000000..43f7c58 --- /dev/null +++ b/pmd-ruleset.xml @@ -0,0 +1,18 @@ + + + + + A small classroom-friendly PMD ruleset focused on likely mistakes. + + + + + + + + + + diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..8ac00bd --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/java/simulation/Event.java b/src/main/java/simulation/Event.java index 6fe092d..a2ec4e2 100644 --- a/src/main/java/simulation/Event.java +++ b/src/main/java/simulation/Event.java @@ -1,5 +1,7 @@ package simulation; +import java.util.Objects; + /** * Abstract base class for discrete-event simulation events. * @@ -70,6 +72,34 @@ public int compareTo(Event other) { return Double.compare(this.time, other.time); } + /** + * Compares events by concrete event type and scheduled time. + * + * @param obj the object to compare with this event + * @return {@code true} when both events have the same type and time + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Event other = (Event) obj; + return Double.compare(time, other.time) == 0; + } + + /** + * Returns a hash code consistent with {@link #equals(Object)}. + * + * @return hash code for this event + */ + @Override + public int hashCode() { + return Objects.hash(getClass(), time); + } + /** * Returns a string representation showing the event time. * diff --git a/src/main/java/simulation/EventQueue.java b/src/main/java/simulation/EventQueue.java index f320a90..5003d16 100644 --- a/src/main/java/simulation/EventQueue.java +++ b/src/main/java/simulation/EventQueue.java @@ -1,6 +1,7 @@ package simulation; import java.util.PriorityQueue; +import java.util.Queue; /** * A chronologically ordered queue of simulation {@link Event}s. @@ -22,7 +23,7 @@ */ public class EventQueue { - private final PriorityQueue queue; + private final Queue queue; /** * Creates an empty event queue. @@ -41,7 +42,9 @@ public void schedule(Event event) { if (event == null) { throw new NullPointerException("Cannot schedule a null event"); } - queue.offer(event); + if (!queue.offer(event)) { + throw new IllegalStateException("Unable to schedule event: " + event); + } } /** diff --git a/src/main/java/simulation/Statistics.java b/src/main/java/simulation/Statistics.java index 9dcbc3a..c23e4fe 100644 --- a/src/main/java/simulation/Statistics.java +++ b/src/main/java/simulation/Statistics.java @@ -62,7 +62,7 @@ public void observe(double value) { /** * Resets all accumulators, discarding previous observations. */ - public void reset() { + public final void reset() { count = 0; sum = 0.0; sumSquares = 0.0; diff --git a/src/main/java/simulation/examples/CountdownSimulation.java b/src/main/java/simulation/examples/CountdownSimulation.java new file mode 100644 index 0000000..da22463 --- /dev/null +++ b/src/main/java/simulation/examples/CountdownSimulation.java @@ -0,0 +1,65 @@ +package simulation.examples; + +import simulation.Event; +import simulation.Simulation; + +/** + * A tiny runnable simulation that schedules one event per tick. + * + *

Run it after compiling with: + *

{@code
+ * java -cp build/classes simulation.examples.CountdownSimulation
+ * }
+ */ +public class CountdownSimulation extends Simulation { + + private static final int DEFAULT_START = 5; + + private int remaining; + + /** + * Creates a countdown simulation. + * + * @param start the first number to print + */ + public CountdownSimulation(int start) { + remaining = start; + } + + /** + * Runs the example from the command line. + * + * @param args ignored + */ + public static void main(String[] args) { + new CountdownSimulation(DEFAULT_START).run(); + } + + @Override + protected void initialize() { + scheduleEvent(new TickEvent(0.0)); + } + + private void tick() { + System.out.printf("time %.1f: %d%n", getClock(), remaining); + remaining--; + + if (remaining <= 0) { + stop(); + } else { + scheduleEvent(new TickEvent(getClock() + 1.0)); + } + } + + private class TickEvent extends Event { + + TickEvent(double time) { + super(time); + } + + @Override + public void execute(Simulation sim) { + tick(); + } + } +} diff --git a/test/java/simulation/EventQueueTest.java b/test/java/simulation/EventQueueTest.java index 39e64b4..712372e 100644 --- a/test/java/simulation/EventQueueTest.java +++ b/test/java/simulation/EventQueueTest.java @@ -3,7 +3,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for {@link EventQueue}. diff --git a/test/java/simulation/EventTest.java b/test/java/simulation/EventTest.java index 509098c..1ca3e40 100644 --- a/test/java/simulation/EventTest.java +++ b/test/java/simulation/EventTest.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for {@link Event}. @@ -11,10 +13,10 @@ class EventTest { /** Concrete subclass used for testing. */ - private static class TestEvent extends Event { + private static class StubEvent extends Event { boolean executed = false; - TestEvent(double time) { + StubEvent(double time) { super(time); } @@ -27,53 +29,53 @@ public void execute(Simulation sim) { @Test @DisplayName("constructor stores the scheduled time") void constructorStoresTime() { - Event e = new TestEvent(3.14); + Event e = new StubEvent(3.14); assertEquals(3.14, e.getTime(), 1e-9); } @Test @DisplayName("constructor accepts time zero") void constructorAcceptsZero() { - Event e = new TestEvent(0.0); + Event e = new StubEvent(0.0); assertEquals(0.0, e.getTime()); } @Test @DisplayName("constructor rejects negative time") void constructorRejectsNegativeTime() { - assertThrows(IllegalArgumentException.class, () -> new TestEvent(-1.0)); + assertThrows(IllegalArgumentException.class, () -> new StubEvent(-1.0)); } @Test @DisplayName("compareTo: earlier event is less than later event") void compareToEarlierIsLess() { - Event early = new TestEvent(1.0); - Event late = new TestEvent(2.0); + Event early = new StubEvent(1.0); + Event late = new StubEvent(2.0); assertTrue(early.compareTo(late) < 0); } @Test @DisplayName("compareTo: later event is greater than earlier event") void compareToLaterIsGreater() { - Event early = new TestEvent(1.0); - Event late = new TestEvent(2.0); + Event early = new StubEvent(1.0); + Event late = new StubEvent(2.0); assertTrue(late.compareTo(early) > 0); } @Test @DisplayName("compareTo: equal times compare as zero") void compareToEqualIsZero() { - Event a = new TestEvent(5.0); - Event b = new TestEvent(5.0); + Event a = new StubEvent(5.0); + Event b = new StubEvent(5.0); assertEquals(0, a.compareTo(b)); } @Test @DisplayName("toString contains the class name and time") void toStringContainsInfo() { - Event e = new TestEvent(2.5); + Event e = new StubEvent(2.5); String s = e.toString(); - assertTrue(s.contains("TestEvent"), "should contain class name"); + assertTrue(s.contains("StubEvent"), "should contain class name"); assertTrue(s.contains("2.5") || s.contains("2.5000"), "should contain time"); } } diff --git a/test/java/simulation/SimulationTest.java b/test/java/simulation/SimulationTest.java index ce49267..8215d53 100644 --- a/test/java/simulation/SimulationTest.java +++ b/test/java/simulation/SimulationTest.java @@ -2,7 +2,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for {@link Simulation}. diff --git a/test/java/simulation/StatisticsTest.java b/test/java/simulation/StatisticsTest.java index c370912..f9444f0 100644 --- a/test/java/simulation/StatisticsTest.java +++ b/test/java/simulation/StatisticsTest.java @@ -3,7 +3,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Unit tests for {@link Statistics}.