Assignment 2: CMSμ

The CMSX system we are using in this class was developed by Cornell students over the past 20 years. It offers features to manage large classes not found in products like Canvas.

In this assignment, you will be designing a simple system, CMSμ (μ for “micro”), which provides a small subset of the features of a course management system. The CMSμ system will manage the enrollment of students in courses, allowing an administrator to interact with it through text commands. The user will be able to add new students and courses, enroll students in courses, drop students from courses, and view information about specific courses and students. They can also perform analyses across all enrollments for a semester, such as identifying students with schedule conflicts, or those enrolled for too many credits.

As part of the assignment, you will implement an abstraction for a mutable set of students. This class will be used to keep track of all students enrolled in a course as well as to store the results of analyses that identify multiple students. Since the integrity of student data is so important, you will practice defensive programming techniques and unit testing throughout. Your implementation of this abstraction will leverage our first data structure, the resizable array.

Learning Objectives

Start early. This project 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 walkthrough found later in this handout. Here is an example of what that schedule could 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 should refrain from discussing algorithms that might be used to solve the 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 where we will collect any clarifications for this assignment. Please review it before asking a new question in case your concern has already been addressed. You should also review the FAQ before submitting to see whether there are any clarifications that might help you improve your solution.

Prohibited libraries

To ensure you get sufficient practice with Java’s core facilities, the following are not allowed in your solution to this assignment:

I. Getting Started

Start early. We said that before, but it doesn’t hurt to repeat that important piece of advice. Please keep track of how much time you spend on this assignment. There is a comment at the top of “CMSu.java” for reporting this time, along with asserting authorship.

Skim this entire assignment handout and the release code before starting. The release code is commented with various TODO statements numbered 1 through 26. Please implement the classes and tests in this order to avoid confusion. Test each piece of functionality 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.

Setup. Download the release code from the CMSX assignment page; it is a ZIP file named “a2-release.zip”. Follow the same procedure as for A1 to extract the release files, open them as a project in IntelliJ, select a JDK, and add JUnit 5 as a test dependency by resolving import errors in “tests/cs2110/StudentTest.java”. Confirm that you can run the unit test suites in StudentTest and StudentSetTest. They should produce UnsupportedOperationExceptions, because you have not yet implemented any of the methods. Then open “src/cs2110/Main.java” and run Main.main. You will be prompted to enter a command. Just type exit, and it should finish executing.

Assertions. Throughout the assignment you will use assert statements to catch bugs that lead 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 walkthrough

1. Class Student (TODOs 1–7)

Class Student represents information associated with a student tracked by the course management system, such as their names and course enrollments. The given code specifies all of the behavior of a student as methods, but no fields have been declared, and therefore all method bodies requiring access to those fields are missing as well.

Start by declaring private fields to represent a student’s state; since the fields are private, you may name them however you like (but keep our style guidelines in mind). Comments must accompany the declarations of all fields to describe what the fields mean, what legal values they may contain, and what constraints hold for them. The collection of the comments on these fields is called the class invariant.

Next, implement method assertInv() to check whether the constraints mentioned in the invariant are satisfied. Use assert statements to verify each condition (see StudentSet for an example). Liberally calling this method throughout the class implementation will help catch bugs sooner. 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 constrictors. 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), meaning that you don’t need to say this in your specifications. You should assert that such values are non-null when checking preconditions and class invariants.

Now, for each method in the Student class do the following:

  1. Read the specification for the method describing what the method does as well as its 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 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 the method specs never mention fields. Since the 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.

TODO 6, in “StudentTest.java”, requires you to write your own test case(s) for a method that has already been implemented (though it depends on TODOs 4–5). Look to the other test cases for inspiration and syntax, and remember that, when calling assertEquals(), the expected value comes first.

Tip: if you have trouble finding a TODO, check out the dedicated “TODO” panel at the bottom of IntelliJ, or try Edit | Find | Find in Files….

2. Class StudentSet (TODOs 8–13)

The course management system will often need to track sets of distinct students with something in common, such as all students enrolled in a particular course, or all students enrolled in at least a certain number of credits. 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 students that can grow and shrink as students 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 resizable array.

A resizable 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. This will build your intuition for how the method is expected to behave. The practice of writing test cases before implementing corresponding method bodies is known as test-driven development (TDD).

Complete TODOs 9–13 in the manner described by 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 “StudentSet.java”.

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 students need to be added to exceed your initial capacity, and you must write a test case that achieves this.

Pay careful attention to the fact that objects are to be compared using .equals(), not ==.

3. Class Course (TODOs 14–22)

Class Course represents a course being managed by the CMS. Aside from some immutable information about the course itself, it maintains a set of student enrollments. In this role, it is responsible for maintaining the consistency of students’ credit counts.

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

4. Class CMSu (TODOs 23-26)

Finally, we get to the course management system itself. An instance of class CMSu keeps track of which courses and students it is managing. Each course and each student is associated with an ID number. ID numbers start at 0 and increment with each entity that is created. For example, the first course that is created has ID number 0; the next, 1; and so forth. The same is true for students: the first student that is created has ID number 0, etc. These ID numbers enable clients to unambiguously identify courses and students without needing to have a reference to an object.

As a micro-version of a real course management system, CMSu is limited in its ability to support lots of students and courses. Each instance of CMSu has a fixed upper bound on the maximum number of students and courses, hence the maximum ID numbers. Students and courses are each stored in arrays, and the arrays are allocated to be “big enough” to handle those maximum numbers (this illustrates what life is like without dynamic data structures like resizable arrays; do not mistake this for good design). Internally, CMSu uses ID numbers as indices into those arrays. Before adding a student or a course, a client of CMSu must use a method to query whether there is room for another entity. For example, canAddCourse() must be called before addCourse(). 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 the system. 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 covering at least two different scenarios (TODO 23).

  1. Overlapping enrollments. Implement TODO 24 to determine whether a given student is taking two classes whose meeting times overlap.
  2. Credit limits. Implement TODO 25 to identify all students who are enrolled in more credits than a specified limit.
  3. Credit consistency checker. Implement TODO 26 to audit the data in the CMS and ensure that the tally of credits in each Student object matches the sum of credits for the Course objects where the student is enrolled.

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 CMSu. You should not change any of the code in “Main.java”, but you should run it to confirm that all of your classes are functioning as expected when integrated into an application. You will need to complete Student, StudentSet, Course, and CMSu before you can run Main.

To see which commands are understood by the interface, type help. Command arguments must be separated by spaces. In this simple interface, string arguments are not permitted to contain spaces.

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

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

III. Scoring

This assignment is evaluated in the following categories:

IV. Submission

Double-check that you have provided the information requested in the header comment in “CMSu.java” (name, NetID, hours worked).

Upload your “Student.java”, “StudentSet.java”, “Course.java”, and “CMSu.java” files, as well as their corresponding test suites (“StudentTest.java”, “StudentSetTest.java”, “CourseTest.java”, and “CMSuTest.java”), to CMSX before the deadline. If you forgot where your project is saved on your computer, you can right-click on “Student.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 misclassified 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 email.