The same model can be used by a normal program and a test program. Main supplies
input and prints a user report. PlannerTest supplies deliberate operations and
checks their results directly. It does not need to feed a Scanner to test every
object invariant. Both programs call the same Planner.java and Session.java.
A different entry, not a different implementation
This editor includes PlannerTest.java and selects it as the entry. Run starts its
main method. The model classes stay unchanged. Open the test tab and read its
labels before running; each label names a promised property rather than a private
field or a particular loop implementation.
The checks cover an empty plan, valid totals, initial completion, repeated
completion, invalid indexes, independent instances, topic trimming and rejected
additions. A full-plan case checks the 100-session boundary and the largest
allowed total. The tests do not claim to prove every possible extension. They
make specific assumptions about the current API and brief.
Test the tests with a deliberate defect
After observing a passing run, introduce one defect at a time and check that the
existing tests fail. Then restore the original source. A failed mutation check
is useful only if the source really changed and the failure is a relevant assertion,
not an unrelated syntax error. The course’s automated project checks repeat this
exercise for trimming, range validation, completion counting and index handling.
PlannerTest uses helper methods marked private static. They are ordinary methods,
with access restricted to the test class. expectInvalid catches the expected
exception, then verifies that it actually happened. If no exception is thrown,
the boolean remains false and check fails.
Run the model checksJavaimport java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
Planner planner = new Planner();
// Two lines per session: a topic, then whole-number minutes.
while (input.hasNextLine()) {
String topic = input.nextLine();
if (!input.hasNextLine()) {
System.out.println("Missing minutes for: " + topic);
break;
}
String minutesText = input.nextLine();
try {
int minutes = Integer.parseInt(minutesText.trim());
planner.add(topic, minutes);
} catch (NumberFormatException error) {
System.out.println("Skipped: minutes must be a whole number");
} catch (IllegalArgumentException error) {
System.out.println("Skipped: " + error.getMessage());
} catch (IllegalStateException error) {
System.out.println(error.getMessage());
break;
}
}
// This demo completes the first accepted session, if there is one.
planner.complete(0);
System.out.println(planner.report());
}
}
import java.util.ArrayList;
public class Planner {
private final ArrayList<Session> sessions = new ArrayList<>();
public void add(String topic, int minutes) {
if (sessions.size() >= 100) {
throw new IllegalStateException("Plan is full");
}
// Construct and validate before changing the list.
Session session = new Session(topic, minutes);
sessions.add(session);
}
public int size() {
return sessions.size();
}
public boolean complete(int index) {
if (index < 0 || index >= sessions.size()) {
return false;
}
sessions.get(index).complete();
return true;
}
public int plannedMinutes() {
int total = 0;
for (Session session : sessions) {
total += session.minutes();
}
return total;
}
public int completedMinutes() {
int total = 0;
for (Session session : sessions) {
if (session.isDone()) {
total += session.minutes();
}
}
return total;
}
public String report() {
int planned = plannedMinutes();
int completed = completedMinutes();
return size() + " sessions, " + completed + "/" + planned + " min complete";
}
}
public class Session {
private final String topic;
private final int minutes;
private boolean done;
public Session(String topic, int minutes) {
if (topic == null || topic.trim().isEmpty()) {
throw new IllegalArgumentException("Topic is required");
}
if (minutes < 1 || minutes > 240) {
throw new IllegalArgumentException("Minutes must be 1 to 240");
}
this.topic = topic.trim();
this.minutes = minutes;
}
public String topic() {
return topic;
}
public int minutes() {
return minutes;
}
public boolean isDone() {
return done;
}
public void complete() {
// Completing a session twice still counts it only once.
done = true;
}
}
public class PlannerTest {
public static void main(String[] args) {
Planner empty = new Planner();
check("empty report", empty.report().equals("0 sessions, 0/0 min complete"));
check("empty completion", !empty.complete(0));
Planner planner = new Planner();
planner.add(" Java ", 25);
planner.add("Reading", 10);
check("planned total", planner.plannedMinutes() == 35);
check("initial completion", planner.completedMinutes() == 0);
check("complete first", planner.complete(0));
check("complete again", planner.complete(0));
check("count once", planner.completedMinutes() == 25);
check("negative index", !planner.complete(-1));
check("index at size", !planner.complete(2));
check("report", planner.report().equals("2 sessions, 25/35 min complete"));
Session normalized = new Session(" Java ", 25);
check("trim topic", normalized.topic().equals("Java"));
check("new session", !normalized.isDone());
check("separate planner", empty.plannedMinutes() == 0);
expectInvalid(planner, null, 25);
expectInvalid(planner, " ", 25);
expectInvalid(planner, "Bad", 0);
expectInvalid(planner, "Bad", 241);
check("failed additions do not change size", planner.size() == 2);
check("failed additions do not change total", planner.plannedMinutes() == 35);
Planner full = new Planner();
for (int index = 0; index < 100; index++) {
full.add("Topic", 240);
}
check("bounded total", full.plannedMinutes() == 24000);
boolean rejected = false;
try {
full.add("Extra", 1);
} catch (IllegalStateException error) {
rejected = true;
}
check("full plan rejects addition", rejected);
check("full plan unchanged", full.size() == 100);
System.out.println("All planner checks passed");
}
private static void expectInvalid(Planner planner, String topic, int minutes) {
boolean rejected = false;
try {
planner.add(topic, minutes);
} catch (IllegalArgumentException error) {
rejected = true;
}
check("invalid addition rejected", rejected);
}
private static void check(String label, boolean condition) {
if (!condition) {
throw new AssertionError(label);
}
}
}
Check your prediction
All planner checks passed
The success line appears after all checks. Open Planner.java and temporarily replace completedMinutes’s isDone condition with true. The initial-completion or repeated-count evidence must fail. Reset the example before your next experiment.
A returned false and unchanged state are separate promisesJava
public class Main { public static void main(String[] args) { int count = 2; int index = 2; boolean valid = index >= 0 && index < count; System.out.println(valid); System.out.println(count); }}
Check your prediction
false2
An index equal to the size is outside the list. Returning false is useful only if the rejected operation does not also change the collection or completion flags.
Your turn
Exercise
Write a boundary check helper
Write checkIndex(int index, int size) returning true only for existing zero-based positions. Use check to verify negative index, first index, last index, index==size and an empty collection. Print Index checks passed after all comparisons.
Your workspaceJava
public class Main { public static void main(String[] args) { // Add checks for five boundary cases. } // Define checkIndex and check.}
Hint 1: the idea
The largest valid position is size-1, and no position is valid when size is zero.
Hint 2: a first step
Use a strict upper comparison. Pick checks on both sides of the boundary instead of several ordinary middle positions.
Show one solution
public class Main { public static void main(String[] args) { check("negative", !checkIndex(-1, 2)); check("first", checkIndex(0, 2)); check("last", checkIndex(1, 2)); check("at size", !checkIndex(2, 2)); check("empty", !checkIndex(0, 0)); System.out.println("Index checks passed"); } static boolean checkIndex(int index, int size) { return index >= 0 && index < size; } static void check(String label, boolean condition) { if (!condition) { throw new AssertionError(label); } }}
Index checks passed
Change it and explain
Temporarily change topic.trim() to topic in Session’s stored-field assignment;
which check catches it? Restore it. Then remove the duration range guard and
confirm that invalid additions are rejected by the tests. Do not judge a mutation
by whether the original demo still prints its old output.
Add one test of your own before extending the project: complete the second
session and expect all 35 minutes completed. Run it on the correct implementation,
then explain which plausible mistake it can distinguish. Keep test cases close
to the contract, so a harmless refactor does not require rewriting every test.
Completion is saved in this browser only. It does not measure mastery.