Assignment 2: ConAir
ConAir is an imaginary new airline whose headquarters is in Connecticut. They require new flight management software to automate their operation of a fleet of small airplanes.
In this assignment, you will complete a simple program providing a small subset of the features of this flight management system. Your ConAir system will manage passengers and flights, allowing an administrator to interact with it through text commands. The administrator will be able to schedule new flights, assign passengers to flights, de-assign passengers from flights, and view information about passengers and flights. They can also perform basic analyses across all flights, such as checking if passengers have been assigned to flights with insufficient layover times between them, or identifying frequent fliers eligible for rewards.
As part of this assignment, you will implement an abstraction for a mutable set of passengers. This Java class will be used to track all passengers assigned to a flight. Since the integrity of passenger and flight data is very important, you will practice defensive programming techniques and unit testing throughout this assignment. Your implementation of this abstraction will leverage our first data structure, the re-sizable array.
Learning Objectives
- Implement object behaviors while maintaining class invariants
- Defend against precondition and invariant violations using
assert
statements - Write unit test cases and run integration tests
- Implement a re-sizable array data structure
- Perform programming tasks that require interaction among multiple classes
Recommended schedule
Start early.
This assignment is longer and more involved than Assignment 1. We recommend spreading your work over at least 5 days: roughly 1 for each of the numbered sections in the “assignment walk-through” described later in this handout. Here is one example of what a schedule to complete this assignment might be:
- Day 1. Skim this handout entirely. Download the release code and set
it up in IntelliJ. Make sure you can run the test suites and the
main application (some test cases are expected to fail). Complete
Passenger.java
. Begin by carefully studying its walk-through and TODOs. Seek clarity on any terms or concepts that you are unfamiliar with before proceeding. Then, complete the TODOs inPassenger.java
andPassengerTest.java
. - Day 2. Complete
PassengerSet.java
. Be aware thatPassengerSet.java
involves significantly more programming than you have encountered in CS 2110 this semester. You will need to devote effort both to thePassengerSet.java
class and its unit tests, which are inPassengerSetTest.java
. - Day 3. Complete the
Flight
class and its unit tests (FlightTest.java
). - Day 4. Complete and test as many methods as you can in class
ConAir.java
(these require more problem solving than the previous tasks). - Day 5. Run the main application (
Main.java
). Use the provided test script commands and check the output against the provided expected output. Complete the reflection document. Finish any further testing that you’d like to do. Re-format and re-read your code. Ensure that your code conforms to our style guide and that it complies with all specified implementation constraints. Submit to CMSX.
Collaboration policy
This assignment is to be completed as an individual. You may talk with others to discuss Java syntax, debugging tips, or navigating the IntelliJ IDE, but you must not discuss algorithms that might be used to solve problems, and you must never show your in-progress or completed code to another student. Consulting hours are the best way to get individualized assistance at the source-code level.
Frequently asked questions
If needed, there will be a pinned post on Ed to collect any clarifications about this assignment. Please review that pinned post before asking a new question, in case your concern was already addressed. You should also review the FAQ before submitting, to see if there have been any clarifications that might help you improve your solution to this assignment.
Prohibited libraries
To ensure that you get sufficient practice with Java’s core facilities, the following are not allowed in your solution to this assignment:
- Any classes from Java’s standard library outside of the
java.lang
package (which is imported implicitly)- In particular, you may not use any classes or methods from
java.util
, including theArrays
class.
- In particular, you may not use any classes or methods from
System.arraycopy()
Don’t worry—we’ll open up more of the standard library in future assignments.
Interoperability
While coding, you must not alter the client interfaces of classes used in this project. That means you must neither modify any of the public method signatures or specifications provided with the release code, nor add new non-private methods. We should be able to mix and match classes and tests from different students and still end up with a working program—this degree of modularity and interoperability is possible thanks to abstraction and encapsulation.
We will mix and match your classes and tests with our reference implementations, so preserving everyone’s client interface is critical for the autograder’s operation. One consequence is that any helper methods you choose to add must be private, and you must not submit unit tests for them (unit tests can’t access private methods, nor would our reference implementations pass tests for methods they don’t have). This testing restriction is unfortunate, but you can still achieve coverage of your helper methods by testing the public methods that call them. See the student handbook for more about this and other restrictions on assignments in this course.
I. Getting Started
Start early.
We said that before, but it doesn’t hurt to repeat a very important piece of advice. Please keep track of how much time you spend on this assignment. There is a place in the reflection document for reporting this time, and to assert authorship.
Skim this entire assignment handout and the release code before starting.
The release code is commented with various TODO statements numbered 1
through 25. We strongly recommend that you implement the classes and
tests in increasing order of TODO ID to avoid confusion. Test each
function before moving on; otherwise, you won’t be able to
determine which code is at fault for a failing test, as later methods
rely on the correct functionality of earlier ones. Remember that as
you complete each task, you should remove the comment line with the
TODO
, since the task has been done (this makes it easier for you to spot tasks that haven’t been done yet).
Setup.
Download the release code from the CMSX assignment page; it is a ZIP
file named “a2-release.zip”. Extract its contents to your assignments
directory for this course and open the enclosed “a2” folder as a
project in IntelliJ. Confirm
that you can run the unit tests in PassengerTest.java
and
PassengerSetTest.java
. They should produce
UnsupportedOperationException
s, because you have not yet implemented
any of the methods. Then open “src/cs2110/Main.java” and run
its main()
method. You will be prompted to enter a command. Just type
exit, and it should finish executing.
Assert Statements.
Throughout the assignment you will use assert
statements to defend
against bugs due to violations of preconditions or class invariants.
IntelliJ automatically enables these statements when running unit
tests, but they are disabled by default when running main()
methods. To enable them, after running Main
, click its green arrow
again, then select “Modify Run Configuration…”. To the right of
“Build and run”, click “Modify options”, and in the menu that appears,
under “Java”, select “Add VM options”. A text field will appear named
“VM options”; in it, type -ea
, then click “OK”. Now assert
statements will not be ignored when running Main
.
II. Assignment walk-through
1. Class Passenger
(TODOs 1–7)
The Passenger
class represents information associated with a passenger
on any flights that ConAir manages, such as their name and their current
number of reserved flights. The provided code specifies all of
the behavior of a passenger as methods, but most fields have not been
declared, and therefore all method bodies requiring access to those
fields are missing as well.
Start by declaring private fields to represent a passenger’s state; since the fields are private, you may name them however you like (but keep our style guidelines in mind). JavaDoc comments must accompany all field declarations and describe what the fields mean, what legal values they may contain, and what other constraints hold for them. The collection of comments on these fields is called the class invariant.
Next, implement the method assertInv()
to check whether the constraints
mentioned in the invariant are satisfied. Use assert
statements to
verify each condition (see PassengerSet.java
for an example).
Liberally calling assertInv()
throughout the class implementation
can help catch bugs earlier. Since the class invariant is implicitly
part of every method’s pre- and post-conditions, you should in
principle call it on entry and exit of every public method and at the
end of public constructors. We recognize, though, that this can feel
tedious, especially for simple methods, so the rubric requires the
invariant to be asserted only as part of method and constructor
post-conditions and allows checks to be omitted from simple
observers (an observer is a method that returns a property of an
object without making any modifications).
Reminder: in this course, we implicitly require that reference fields, arguments, and return values not be null (unless otherwise specified), so you don’t need to say this in your specifications. However, you should assert that they are non-null as part of checking preconditions and class invariants, at least in this assignment. Be sure to check for null before performing any other operations on the object.
Now, for each method in the Passenger
class do the following:
- Read the specification for the method, which describes what the method does and the method’s preconditions.
- Identify which, if any, test cases cover the method. Confirm that you agree with how the method is being used and what the expected results should be. Run the test case to see where it currently fails.
- Write the method body, implementing what the method specifications say the method should do. Assert that argument preconditions and invariants are satisfied as a defensive measure.
- Re-run the test case to see if progress has been made. If you see a
java.lang.AssertionError
(as distinct from a JUnitAssertionFailedError
), immediately diagnose how a precondition or invariant was violated.
Observe that method specifications never mention fields. Since fields are private, clients in principle do not know what the fields are, so referring to them in a spec would be meaningless from their perspective.
TODOs 6A, 6B, and 6C in “PassengerTest.java” require you to write test
case(s) for a method that has already been implemented (though it
depends on TODOs 3–5). Look to the other test cases for inspiration
and syntax, and remember that, when calling assertEquals()
, the
expected value comes first. All tests are required to have a
human-readable description of the scenario(s) that they cover in a
@DisplayName
string; the “given-when-then” template is a good way to
start (though deviations are fine to make the descriptions more
natural).
Tip: if you have trouble finding a TODO, check out the dedicated “TODO” panel in IntelliJ via View | Tool Windows, or try Edit | Find | Find in Files….
2. Class PassengerSet
(TODOs 8–13)
The flight management system will often need to track sets of distinct passengers on a flight. Your first thought might be to use an array, but arrays have some drawbacks for this application:
- They have a fixed length: they cannot grow or shrink.
- They allow duplicate values.
Instead, we will define a new class to represent a set of distinct passengers that can grow and shrink as passengers are added and removed. A type supporting these operations is known as a set, and the data structure we will use to implement the type is a re-sizable array.
A re-sizable array is an inefficient data structure for implementing sets, for reasons we will discuss later in the course. Right now we are more concerned with correctness than efficiency.
The fields, class invariant, and invariant-checking method have all been given, as have all public method specifications. But the test suite does not cover most of these methods. TODO 8 describes how you will expand this suite—as you read and understand each method spec for TODOs 11–13, write (or add to) a test case that exercises that method. Doing so will build your intuition for how the method is expected to behave. The practice of writing test cases before implementing the corresponding method bodies is known as test-driven development (TDD).
Complete TODOs 9–13 in the manner described in TODO 8, running relevant tests after each one to catch bugs as early as possible. Continue to practice good defensive programming by asserting that preconditions and invariants are satisfied. In the end, every public method should be exercised by at least one test case. Use IntelliJ’s “Run with coverage” to check how well your test suite covers the lines of “PassengerSet.java” (full method coverage is required; full line coverage is preferred).
At least one of your test cases will need to be informed by “glass-box testing.” Resizing the backing store is a likely source of bugs, but the client cannot control when resizing happens. As the implementer, though, you know how many passengers need to be added to exceed your initial capacity, and you must write a test case to check resizing behavior.
Pay careful attention to the fact that objects are to be compared
using .equals()
, not ==
.
3. Class Flight
(TODOs 14–22)
Class Flight
represents a scheduled flight being managed by ConAir.
Aside from some immutable information about the flight itself, it
maintains a set of passengers with reservations. In this role,
Flight
is responsible for checking if passengers are on another
flight with a conflicting schedule. Also, Flight
assigns an airplane
category for a flight, depending on the number of passengers who are
currently on the flight. As ConAir is still new, it only has three
categories of small airplanes in its fleet: category 1 (up to 10
passengers), category 2 (up to 20 passengers), and category 3 (up to
40 passengers).
As suggested by the TODO ordering, practice test-driven development again here—study the public method specifications, then write black-box test cases illustrating how these methods would be called and what the expected results would be. As you tackle each method, your test suite should get longer and greener one test case at a time. In the end, every public method should be exercised by at least one test case (again, use IntelliJ’s “Run with coverage” to check your test coverage).
Pay attention to your line coverage of
formatDepartureTime()
—methods related to dates and times are
traditional hotspots for bugs.
4. Class ConAir
(TODOs 23-25)
Finally, we get to the flight management system itself. An instance
of class ConAir
keeps track of its passengers and the flights that
it manages. Each flight and passenger is associated with an ID
number.
These ID
numbers (often called “handles”) enable clients of a class to
unambiguously identify flights or passengers without needing to have a
reference to an object.
Internally, ConAir’s ID numbers start at 0 and increment with each flight that is
created (for example, the first flight or passenger that is created
has ID number 0; the next has ID number 1; and so forth); however, clients should be oblivious to this.
As a micro-version of a real flight management system, ConAir
is
limited in its ability to support lots of flights (and
passengers). Each instance of ConAir
has a fixed upper bound on the
maximum number of flights and passengers, and hence a maximum ID
number. Flights and passengers are stored in arrays that are allocated
to be “big enough” to handle the maximum number (this upper-bound
restriction illustrates what life is like without dynamic data
structures like re-sizable arrays; it is not necessarily good
design!). Internally, ConAir
uses ID numbers as indices into those
arrays. Before adding a flight or passenger, a client of ConAir
must
use a method to query whether there is room for another flight. For
example, canAddFlight()
must be called before addFlight()
. We
refer to this programming pattern as asking permission, as in, “the
client must ask permission before attempting an operation that will
change the state.”
Your job is to add useful functionality to ConAir
. More points are
earned for each piece of functionality you implement, assuming that
prior classes work correctly. (In other words, it is more important to
spend time testing and debugging parts 1–3 than implementing
additional methods in part 4.) Each method you choose to implement
must be accompanied by test cases with at least two assertions (TODO
23).
- Frequent-flier identification. Implement TODO 24 to identify frequent fliers as passengers who are on more than five ConAir flights.
- Tight connection warnings. Implement TODO 25 to return whether a passenger has insufficient layover time between two of ConAir’s connecting flights (assuming flights are on the same day).
Recall that we are more interested in correctness than efficiency for now. You don’t have to worry about the efficiency of your implementations. Indeed, some of those methods could be implemented more efficiently if we had better data structures—but those will come later in the semester.
5. Assembling an application
“Main.java” implements a simple user interface for ConAir. You should
not change any code in “Main.java”, but you should run it to confirm
that all your classes are functioning as expected when integrated into
an application. You will need to complete Passenger
, PassengerSet
,
Flight
, and ConAir
before you can run Main
meaningfully.
To see which commands are understood by the interface, type help. Command arguments must be separated by spaces. Note: In this simple interface, names containing spaces, like “Mary Kate”, are not permitted (you can work with such names in your unit tests, however).
How to use Main.
We provide a sample test script named “test-script.txt” in the tests
directory. Enter its commands into the interface and confirm that you
get the expected output, which can be found in “expected-output.txt”.
Congratulations—you just performed an end-to-end test!
Optional: write your own test scripts.
You can write your own test scripts if you want to further test your
application. If you do, you’ll note that running those scripts
repeatedly is painful, because you have to manually type all the
commands. Fortunately, Main
was written to process commands from a
file rather than interactively. To run a test script, edit the Run
Configuration for Main
. Go to the Program Arguments field in the
popup window and enter the name of your test script file. For example,
if your file is called “script1.txt”, you would enter “script1.txt” in
the program arguments. (This file must be located in the tests
sub-folder.) Then click OK and run the main method in
“Main.java”. It will read the commands in the test script file and run
them in order.
6. Reflecting on your work
Stepping back and thinking about how you approached an assignment (a habit called metacognition) will help you make new mental connections and better retain the skills you just practiced. Therefore, each assignment will ask you to briefly document your reflection in a file called “reflection.txt”. This file is typically divided into three sections:
- Submitter metadata: Assert authorship over your work, and let us know how long the assignment took you to complete.
- Verification questions: These will ask for a result that is easily obtained by running your assignment. (Do not attempt to answer these using analysis; the intent is always for you to run your program, possibly provide it with some particular input, and copy its output.)
- Reflection questions: These will ask you write about your experience completing the assignment. We’re not looking for an essay, but we do generally expect a few complete sentences. Answers should be specific, rather than general or vague.
Respond to the TODOs in “reflection.txt” (you can edit this file in IntelliJ).
III. Grading
This assignment is evaluated as follows (note: weights are approximate and may be adjusted slightly for the whole class during grading):
- Submitted and compiles (24%)
- Core classes fulfill specifications (31%)
- Implemented functionality in
ConAir
(8%) - Complies with implementation constraints, including required defensive programming measures (15%)
- Test cases are valid, well documented, and provide good coverage (13%)
- Exhibits good code style (4%)
- Responds to reflection questions (5%)
Formatting is a subset of style. To be on the safe side, ensure that our style scheme is installed and that you activate “Reformat Code” before submitting. Graders will deduct for obvious violations that detract from readability, including improper indentation and misaligned braces.
Beyond formatting, choose meaningful local variable names, follow Java’s capitalization conventions (camelCase), and look for ways to simplify your logic. If the logic is subtle or the intent of a statement is not obvious, clarify with an implementation comment.
IV. Submission
Upload your “Passenger.java”, “PassengerSet.java”, “Flight.java”, and “ConAir.java” files, as well as their corresponding test suites (“PassengerTest.java”, “PassengerSetTest.java”, “FlightTest.java”, and “ConAirTest.java”) and your “reflection.txt”, to CMSX before the deadline. If you forgot where your project is saved on your computer, you can right-click on “Passenger.java” in IntelliJ’s project browser and select “Open In”, then your file explorer (e.g. “Explorer” for Windows, “Finder” for Mac). Be careful to only submit “.java” files, not files with other extensions (e.g. “.class”). Note that your test suite will be under “tests/cs2110/”, while your other files will be under “src/cs2110/”.
After you submit, CMSX will automatically send your submission to a smoketester, which is a separate system that runs your solution against the same tests that we provided to you in the release code. The purpose of the smoketester is to give you confidence that you submitted correctly. You should receive an email from the smoketester shortly after submitting. Read it carefully, and if it doesn’t match your expectations, confirm that you uploaded the intended version of your file (it will be attached to the smoketester feedback). Be aware that these emails occasionally get mis-classified as spam, so check your spam folder. It is also possible that the smoketester may fall behind when lots of students are submitting at once. Remember that the smoketester is just running the same tests that you are running in IntelliJ yourself, so don’t panic if its report gets lost—we will grade all work that is submitted to CMSX, whether or not you receive the smoketester email.