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

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:

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:

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 UnsupportedOperationExceptions, 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:

  1. Read the specification for the method, which describes what the method does and the method’s preconditions.
  2. 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.
  3. 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.
  4. Re-run the test case to see if progress has been made. If you see a java.lang.AssertionError (as distinct from a JUnit AssertionFailedError), 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:

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).

  1. Frequent-flier identification. Implement TODO 24 to identify frequent fliers as passengers who are on more than five ConAir flights.
  2. 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:

  1. Submitter metadata: Assert authorship over your work, and let us know how long the assignment took you to complete.
  2. 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.)
  3. 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):

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.