Intro

Here’s an optional intro video for the lab with some explanations of concepts, examples of usage of methods mentioned in the spec, and an intro to the coding portion of the lab. It is a very useful video, but was recorded in Spring 2020 by the wonderful Michelle. Thus, some of the information about assignment logistics is out of date or irrelevant (e.g. mentions of a “Project 2”, which we have not done). All the information in the video is covered in the spec and timestamps for topics are in the video description. *

So far in this class, you have exclusively made programs whose state only persists while the program is running, and all traces of the program disappear once the program exits. For example, in Project 1, you created two data structures but there was no way to save the deque, quit java, turn off your computer, eat a sandwich, and then reload your deque. In this lab, we will go over two methods to make the state of your program persist past the execution of your program: one through writing plain text to a file, and the other through serializing objects to a file. This will be directly applicable Project 2 Gitlet as well as any future projects you want to do where you want to be able to save state between programs.

As always, you can get the skeleton files with the following command:

 git pull skeleton main

Files and Directories in Java

Before we jump into manipulating files and directories in Java, let’s go through some file system basics.

Current Working Directory

The current working directory (CWD) of a Java program is the directory from where you execute that Java program. Examples follow for Windows & Mac/Linux users - they are very similar, just different stylistically.

Windows For example, let’s say we have this small Java program located in the folder C:/Users/Zoe/example (or ~/example) named Example.java:

// file C:/Users/Zoe/example/Example.java
class Example {
  public static void main(String[] args) {
     System.out.println(System.getProperty("user.dir"));
  }
}

This is a program that prints out the CWD of that Java program.

If I ran:

cd C:/Users/Zoe/example/
javac Example.java
java Example

the output should read:

C:\Users\Zoe\example

Mac & Linux For example, for Mac & Linux users, let’s say we have this small Java program located in the folder /home/Zoe/example (or ~/example) named Example.java:

// file /home/Zoe/example/example.java
class Example {
  public static void main(String[] args) {
     System.out.println(System.getProperty("user.dir"));
  }
}

This is a program that prints out the CWD of that Java program.

If I ran:

cd /home/Zoe/Example
javac Example.java
java Example

the output should read:

/home/Zoe/example

IntelliJ In IntelliJ, you can view the CWD of your program under Run > Edit Configurations > Working Directory.

IntelliJ Working Directory.

Terminal In terminal / Git Bash, the command pwd will give you the CWD.

Absolute and Relative Paths

A path is the location of a file or directory. There are two kinds of paths: absolute paths and relative paths. An absolute path is the location of a file or directory relative to the root of the file system. In the example above, the absolute path of Example.java was C:/Users/Zoe/example/Example.java (Windows) or /home/Zoe/example/Example.java (Mac/Linux). Notice that these paths start with the root which is C:/ for Windows and / for Mac/Linux. A relative path is the location of a file or directory relative to the CWD of your program. In the example above, if I was in the C:/Users/Zoe/example/ (Windows) or /home/Zoe/example/ (Mac/Linux) folders, then the relative path to Example.java would just be Example.java. If I were in C:/Users/Zoe/ or /home/Zoe/, then the relative path to Example.java would be example/Example.java.

Note: the root of your file system is different from your home directory. Your home directory is usually located at C:/Users/<your username> (Windows) or /home/<your username> (Mac/Linux). We use ~ as a shorthand to refer to your home directory, so when you are at ~/repo, you are actually at C:/Users/<your username>/repo (Windows) or /home/<your username>/repo (Mac/Linux).

When using paths, . refers to the CWD. Therefore, the relative path ./example/Example.java is the same as example/Example.java.

Setup for Lab9 and Gitlet

There is a small bit of setup required for this lab, as well as Gitlet. Specifically, it is necessary to use our special debugger. Please follow these steps for Mac and Linux, and these steps for Windows.

Java and Compilation

A walkthrough of “Java Compilation” and “Make” can be found here. The video simply goes over the steps listed in the spec, so if you find yourself confused on the directions then check it out.

Up until now, we have been running our Java code on IntelliJ by clicking the magic green button. Perhaps surprisingly, there are other more primitive ways of running Java code that aren’t graphical. What we’re referring to here is compiling and running Java code through the command line (your terminal). You may be used to this idea from CS 61A, where we often ran code directly from terminal using python3 myprogram.py.

The Java implementations we use compile Java source code, written by the programmer in a .java file, into Java .class files containing Java byte code, which may then be executed by a separate program. Often, this separate program, called java, does a mix of interpreting the class file and compiling it into machine code and then having the bare hardware execute it.

We’re going to walk through how to compile and run a .java file from just your terminal. In order for this all to work, ensure that the following are both version 15 or above:

$ javac -version
$ java -version

NOTE: If you’re in Windows, make sure that you’re using a Bash prompt. That is, if your terminal starts with “C:", you’re in the wrong kind of terminal window. Open up git bash intead.

If they aren’t, please redo the relevant part from lab1setup, under the “Configure your Computer” section.

First ensure your current working directory is su21-p***/lab09/capers. These commands will take you there:

$ cd $REPO_DIR
$ cd lab09/capers

While you’re here, go ahead and run the ls command. You’ll see all the capers files, but the one we want to focus on is a file called Main.java.

To compile the source file and all of its dependencies, run this command within your terminal:

$ javac *.java

The *.java wildcard simply returns all the .java files in the current directory. Run ls again, and you’ll see a bunch of new .class files, including Main.class. These files constitute the compiled code. Let’s see what it looks like.

$ cat Main.class

That command will print out the contents of the file. You’ll see mostly garbage with many special characters. This is called bytecode, and even though it looks foreign to us, the java program can take this compiled code and actually interpret it to run the program. Let’s see it happen:

$ java Main

Oops! We got an error.

Error: Could not find or load main class Main
Caused by: java.lang.NoClassDefFoundError: capers/Main (wrong name: Main)

If we were to translate this error to English, it’s saying “I don’t know what Main you’re talking about.” That’s because Main.java is inside a package, so we must use it’s fully canonical name which is capers.Main. To do this:

$ cd ..                 # takes us up a directory to su21-p***/lab09
$ java capers.Main

And now the program finally runs and prints out

Must have at least one argument

The lesson: to run a Java file that is within a package, we must enter the parent directory (in our case, lab09) and use the fully canonical name.

One last thing about command line execution: how do we pass arguments to the main method? Recall that when we run a class (i.e. java Main), what really happens is the main(String[] args) method of the class is called. To pass arguments, simply add them in the call to java:

$ java capers.Main story "this is a single argument"

As demonstrated, you can have a space in one of your elements of String[] args by wrapping that argument in quotation marks.

In the above execution, the String[] args variable had these contents:

{"story", "this is a single argument"}

You’ll be using the String[] args variable in this lab and in Gitlet. Some skeleton is already provided to show you how it’s done in the main method of the Main class.

Although we will be using command-line compilation to run and debug our code, we will still be using IntelliJ to make edits to the code. Please open up Lab 9 in IntelliJ as normal.

Open Main.java, and find the line (around line number 40) that looks like:

if (args.length == 0) {
    exitWithError("Must have at least one argument");
}

Immediately below this code add System.out.println("args: " + Arrays.toString(args));, so that the code now reads:

if (args.length == 0) {
    exitWithError("Must have at least one argument");
}
System.out.println("args: " + Arrays.toString(args));

The word Arrays will be in red, so you’ll want to put your cursor on the red word and press alt+enter (or option+enter on a Mac) to import the java.util.Arrays class. Once you’ve done this, try compiling and running the code from the command line (not IntelliJ!) using the commands below:

$ javac capers/Main.java
$ java capers.Main story "this is a single argument"

This time, your program should print out:

args: [story, this is a single argument]

Note: You may notice a bunch of .class files in IntelliJ in the same folder as your code. As you may recall, we did not have such .class files in project 0, project 1, or in previous labs. This is because IntelliJ stores the .class files that it generates in another folder (usually called either out or target).

Make

Unfortunately, using JUnit testing for this lab and project 2 is awkward, so we’re going to use a more sophisticated command line based testing infrastructure to run automated tests for this lab and project 2.

The key reason is that Capers (this lab) and Gitlet (project 2) are both programs that have persistent state, i.e. each time you run them, they “remember” what they did during previous runs of the program. For example, when you call git add, then git status, the git program somehow keeps track of which files were added, even though it completely terminates execution in between the add and status commands.

For example, a test for the status command for Gitlet would need to:

  1. Run Gitlet’s Main class with the add argument.
  2. Let your program finish and exit.
  3. Run Gitlet’s Main class a second time with the status argument.
  4. Test that the printed output of the program shows that it correctly “remembers” what happened in the earlier execution of the program.

Another issue that makes testing hard is that the output of Gitlet is complicated. Gitlet tests must be able to parse the printed output of the program, while also testing that various files contain the right contents.

The end result of all of this is that we’ll be using a custom command line testing suite built by Paul Hilfinger and 61B TAs that uses python as the basic engine for verifying program correctness for this lab and in project 2.

The testing suite is executed using a standard unix tool called make. We won’t talk about how to use make in detail. Just know that there are two things that you can use make for:

  1. Compiling your code by using the make command.
  2. Running the test suite by using make check command.

In order to use make, you’ll need to follow the brief instructions here to install make. Additionally, you’ll need Python so follow these instructions if you do not already have Python installed.

Once you have make and python installed, let’s see how to use make to compile your code.

You can run make within any directory as long as it’s within lab09 ( i.e. lab09/capers, lab09, lab09/testing), though for simplicity let’s just stay in the lab09 directory on our terminals). Try typing the following command in your terminal.

$ make

When you run this command, it will compile all of the .java files in your project directory and place the .class files in the project folder (in our case, the capers folder). The output will look something like:

"/Library/Developer/CommandLineTools/usr/bin/make" -C capers default
javac -g -Xlint:unchecked -Xlint:deprecation -cp "..::;..;" CapersRepository.java Dog.java Main.java Utils.java
touch sentinel

You don’t need to understand what any of this means, but basically it’s just compiling your files.

To run the test suite, enter:

$ make check

NOTE: If you get an error that looks like “/bin/bash: python3: command not found” or “python3: Permission denied”, see the FAQ at the end of this spec.

make check launches our tests and prints out which ones you passed and which ones you did not. Initially, you’ll fail all of the tests since you haven’t completed this lab assignment yet.

There is also a third way to use make. Specifically make clean will remove all the .class files and other clutter if it bothers you having so many files in your project folder. We suggest running this after you’re done testing for the moment and want to go back to editing your .java files in IntelliJ.

Terminology note: check and clean are known as make targets. You can actually define more targets for make by creating a custom Makefile. Doing so is well beyond the scope of 61B.

If the above command successfully ran (i.e., you are able to see test output), then you’re ready to start the lab! If you did encounter issues regarding not being able to run make, then get assistance from your TA by putting yourself on the lab queue.

We’ll now move onto how to manipulate files and directories in Java.

File & Directory Manipulation in Java

The Java File class represents a file or directory in your operating system and allows you to do operations on those files and directories. In this class, you usually will want to be doing operations on files and directories by referring to them to their relative paths. You’ll want any new files or directories you create to be in the same directory as where you run your program (in this lab, the ~/su20-su***/lab09 folder) and not some random place on your computer.

Files

You can make a File object in Java with the File constructor and passing in the path to the file:

 File f = new File("dummy.txt");

The above path is a relative path where we are referring to the file dummy.txt in our Java program’s CWD. You can think of this File object as a reference to the actual file dummy.txt - when we create the new File object, we aren’t actually creating the dummy.txt file itself, we are just saying, “in the future, when I do operations with f, I want to do these operations on dummy.txt”. To actually create this dummy.txt file, we could call

f.createNewFile();

and then the file dummy.txt will actually now exist (and you could see it in File Explorer / Finder). However, f.createNewFile() actually throws an exception! We’ve learned about this and how to handle it (hint hint! try catch), but we’ve also created a method that handles it for you. We recommend you use our Utils.writeContents(f, "") to make an empty file, rather than ever calling f.createNewFile(). Our Utils methods can help abstract away some of the complexity of the Java file manipulation methods.

You can check if the file “dummy.txt” already exists or not with the exists method of the File class:

f.exists()

We can also write to the file with the following:

Utils.writeContents(f, "Hello World");

Now dummy.txt would now have the text “Hello World” in it. Note that Utils is a helper class provided in this lab and project 2 and is not a part of standard Java.

Directories

Directories in Java are also represented with File objects. For example, you can make a File object that represents a directory:

File d = new File("dummy");

Similar to files, this directory might not actually exist in your file system. To actually create the folder in your file system, you can run:

    d.mkdir();

and now there should be a folder called dummy in your CWD.

Summary

There are many more ways to manipulate files in Java, and you can explore more by looking at the File javadocs and Googling. There are a ton of resources online and, if you Google it, doing more extensive file operations in Java can get a bit complicated. I’d recommend understanding the basics by doing this lab, and in the future if you come across a use case you don’t know how to handle, then start searching or asking on Ed. For this lab and Gitlet, we provide you with a Utils.java class that has many useful helper functions for file operations.

Serializable

Writing text to files is great and all, but what if we want to save some more complex state in our program? For example, what if we want to be able to save our Deques so we can come back to them later? We could somehow write a toString method to convert a Deque to a String and then write that String to a file. If we do that though, we would also need to figure out how to load the Deque by parsing that file, which can get complicated.

Luckily, we have an alternative called serialization which Java has already implemented for us. Serialization is the process of translating an object to a series of bytes that can then be stored in the file. We can then deserialize those bytes and get the original object back.

To enable this feature for a given class in Java, this simply involves implementing the java.io.Serializable interface:

import java.io.Serializable;

class Deque implements Serializable {
    ...
}

This interface has no methods; it simply marks its subtypes for the benefit of some special Java classes for performing I/O on objects. For example,

Deque d = ....;
File outFile = new File(saveFileName);
try {
    ObjectOutputStream out =
        new ObjectOutputStream(new FileOutputStream(outFile));
    out.writeObject(d);
    out.close();
} catch (IOException excp) {
    ...
}

will convert d to a stream of bytes and store it in the file whose name is stored in saveFileName. The object may then be reconstructed with a code sequence such as

Deque d;
File inFile = new File(saveFileName);
try {
    ObjectInputStream inp =
        new ObjectInputStream(new FileInputStream(inFile));
    d = (Deque) inp.readObject();
    inp.close();
} catch (IOException | ClassNotFoundException excp) {
    ...
    d = null;
}

The Java runtime does all the work of figuring out what fields need to be converted to bytes and how to do so. We have provided helper function in Utils.java that does the above two for you.

Note: There are some limitations to Serializable that are noted in the Project 2 spec. You will not encounter them in this lab.

Exercise: Canine Capers

For this lab, you will be writing a program that will be taking advantage of file operations and serialization. We have provided you with three files:

  • Main.java: The main method of your program. Run it with java capers.Main [args] to do the operations specified below. The majority of the FIXMEs in this program are in here.
  • Dog.java: Represents a dog that has a name, breed, and age. Contains a few FIXMEs.
  • Utils.java: Utility functions for file operations and serialization. These are a subset of those provided with Gitlet, so not all will be used.

You can change the skeleton files in any way you want as long as the spec and comment above the main method in Main.java is satisfied. You do not need to worry about error cases or invalid input. You should be able to complete this lab with just the methods provided in Utils.java and other File class methods mentioned in this spec, but feel free to experiment with other methods.

Main

You should allow Main to run with the following three commands:

  • story [text]: Appends “text” + a newline to a story file in the .capers directory. Additionally, prints out the current story.
  • dog [name] [breed] [age]: Persistently creates a dog with the specified parameters; should also print the dog’s toString(). Assume dog names are unique.
  • birthday [name]: Advances a dog’s age persistently and prints out a celebratory message.

All persistent data should be stored in a “.capers” directory in the current working directory.

Recommended file structure (you do not have to follow this):

.capers/ -- top level folder for all persistent data
    - dogs/ -- folder containing all of the persistent data for dogs
    - story -- file containing the current story

You should not create these manually, your program should create these folders and files.

Note: Naming a folder or file with a period in the front makes it hidden - to be able to see it in terminal, run ls -a instead of just ls. If you want to remove all saved data from your program, just remove the .capers directory (NOT the capers directory) with rm -rf .capers. Be very careful with this command– it will delete anything you tell it to!

Suggested Order of Completion

Please be sure to read the comments above each method in the skeleton for a description of what they do.

  1. Fill out the main method in Main.java. This should consist mostly of calling other methods.
  2. Fill out CAPERS_FOLDER in Main.java, then DOG_FOLDER in Dog.java, and then setUpPersistence in Main.java.
  3. Fill out writeStory in Main.java. The story command should now work.
  4. Fill out saveDog and then fromFile in Dog.java. You will also need to address the FIXME at the top of Dog.java. Remember dog names are unique!
  5. Fill out makeDog and celebrateBirthday in Main.java using methods in Dog.java. You will find the haveBirthday method in the Dog class useful. The dog and birthday commands should now work.

Each FIXME should take at most around 8 lines, but many are fewer.

Usage

The easiest way to run and test your program is to compile it in terminal with javac and then run it from there. E.g.

 cd ~/repo/lab09              # Make sure you are in your lab09 folder (NOT the lab09/capers folder)
 javac capers/*.java          # Make sure to recompile your program each time you make changes!
 java capers.Main [args]      # Run the commands you want! e.g., java story hello

For the story command, if you want to pass in a long string that includes spaces as the argument, you will want to put it in quotes, e.g.

 java capers.Main story "hello world"

If running in IntelliJ, you will need to use Run > Edit Configurations > Program Arguments to add the command line arguments.

Useful Util Functions

Useful Util functions (as a start, may need more and you may not need all of them):

  • writeContents - writes out strings/byte arrays to a file
  • readContentsAsString - reads in a file as a string
  • readContents - reads in a file as a byte array
  • writeObject - writes a serializable object to a file
  • readObject - reads in a serializable object from a file. You can get a Class object by using .class, e.g. Dog.class.
  • join - joins together strings or files into a path. E.g. Utils.join(“.capers”, “dogs”) would give you a File object with the path of “.capers/dogs”

Testing

There are two ways to test your program: simply running the program, and then using written tests and our special debugger!

You can test your program yourself by running it in the command line. The Gradescope autograder will also run a small set of tests. The AG tests are a combination of running these commands in order:

$ java capers.Main story Hello
Hello

$ java capers.Main story World
Hello
World

$ java capers.Main dog Sammie Samoyed 5
Woof! My name is Sammie and I am a Samoyed! I am 5 years old! Woof!
$ java capers.Main birthday Sammie
Woof! My name is Sammie and I am a Samoyed! I am 6 years old! Woof!
Happy birthday! Woof! Woof!
$ java capers.Main dog Larry Lab 11
Woof! My name is Larry and I am a Lab! I am 11 years old! Woof!
$ java capers.Main birthday Sammie
Woof! My name is Sammie and I am a Samoyed! I am 7 years old! Woof!
Happy birthday! Woof! Woof!
$ java capers.Main birthday Larry
Woof! My name is Larry and I am a Lab! I am 12 years old! Woof!
Happy birthday! Woof! Woof!

It also ignores whitespace at the beginning and end of the output so don’t worry too much about that.

At the very end of the spec, we will learn more about the other way we can test and debug code for Capers and Gitlet, using our special debugger.

Git Branches (Optional)

Branching is a very powerful git operation that lets you try out new features or implementations without muddying your working solution. A branch represents an independent line of development and thus when you want to add a new feature, or fix a big, you can create a new branch to encapsulate your changes. This makes it unlikely that non functional code will end up in your project!

Branching can save a lot of time and tears when managing large, shared repos, such as the repository you will be working in to code Gitlet.

The default branch is main, though before October 2020, the default was actually named master. So far, all of your work has gone on this branch.

Branch Example: Building a Game

Let’s say we are building a fun game. We have just finished the base implementation and can’t wait to show our family tomorrow. However, there is one new feature we are dying to add. What should we do?

If we just start working from where we are, we would have to do a lot of work tomorrow to ensure that the version of code running is the code that works as expected.

All on main

Branching gives us the freedom to try implementing this new feature while not breaking our working game.

Let’s say we make a new branch called feature_branch.

We could do this with the command git branch <branch name>, so for us git branch feature_branch. This will create a new branch (starting from where we are) but will not do anything else. If we ran git branch, which displays all the current branches in our repo, we would see that we are still on the main branch, denoted with a * next to the name. We want all our new changes to go here, so we have to move onto the new branch before we can make any new changes. You can swap to a new branch by doing git checkout <branch name>, so for us git checkout feature_branch. Once we execute this command if we run git branch again we will see that the * has moved from main to feature_branch. We have successfully swapped branches!! Now new commits will be on the feature_branch rather than our main branch.

Branching to Avoid Bugs

Sometimes the changes we make don’t turn out as we want them to. When we implement these changes on branches, we can ignore them or delete the entire branch– easy peasy. We don’t have to worry about removing all the changes we made as the clean code is still on our main branch!

Changes to Ignore

Branching to Merge New Features.

Sometimes however we are able to complete the new feature before our family comes over to play the game! In this case, we can use git merge <branch name> to merge the code in our branch with the clean code on main!

Changes to Merge

To do this, we have to go back to main, as we want our resulting functioning code to remain on main. We do this with the same command as before, but swap feature_branch with main: git checkout main. Now if we run git branch we will see that the * is back next to main!

Now we can merge in the changes we made in feature_branch by executing git merge feature_branch. This creates our merge commit, commit number 4. It will have all the new code for our feature mixed with our functioning game code!

Branch Usage

Branching can be very useful to maintain order in shared repos. Ultimately, however, the way you organize your repository is entirely up to you. As a final reminder, if throughout this process you get a detached HEAD state, check out the git WTFs guide.

Submission

You should have made changes in capers/Main.java and capers/Dog.java. You should not be submitting a .capers data folder. Do not use git add . or git add -A to add your files, and git add your files one by one. One partner can submit the lab as always, through the Gradescope interface such as:

 git commit -m "submitting lab09. So ready for Gitlet!!"
 git push origin main

You can then go to Gradescope, and add your partner to your submission. There is no style check for this lab.

Mandatory Epilogue: Debugging


Before completing this section you should have passed all of the make check tests. You should also have submitted to Gradescope. Even though you have your points for the lab, there is one more thing to do.

In this section, we’ll talk about how to use the IntelliJ debugger to debug lab 9. This might seem impossible at first glance, since we’re running everything from the command line. However, IntelliJ provides a feature called “remote JVM debugging” that will allow you to add breakpoints that trigger during our integration tests.

Start by using git to checkout the original version of the skeleton code. That is, after checking out, you should be back right where you started at the very beginning of this lab. To do this you can:

  1. From your su21-p*** directory, run git log
  2. Identify a commit from BEFORE you had made any changes to lab09 and copy the first 6 digits of the commit ID (the long gibberish of numbers and letters).
  3. Run git checkout <HASH ID HERE> lab09

Do not include the angled brackets <> in the last step.

A walkthrough of the remainder of this section of the lab can be found here. The video simply goes over the steps listed in the spec, so if you find yourself confused on the directions then check it out. Note that this video is from Spring 2021, and the assignment may vary. However, they key is learning how to use the debugger!

Without JUnit tests, you may be wondering how to debug your code. We’ll walk you through how you will do that in Capers and in Gitlet.

First, let’s discuss how you would know you have a bug. If you run the the make check tests, you’ll see that you’re failing the test test02-two-part-story.in. Now we need to figure out which execution of your program was buggy. Remember that our tests will run your program multiple times; in this case, the .in file has 2 lines that call capers.Main, so this test runs it twice (though in Gitlet it’ll typically be more than that).

To debug this integration test, we first need to let IntelliJ know that we want to debug remotely. Navigate to your IntelliJ and open your lab09 project if you don’t have it open already. At the top, go to “Run” -> “Run”:

Run to Run

You’ll get a box asking you to Edit Configurations that will look like the below:

Edit Box

Yours might have more or less of those boxes with other names if you tried running a class within IntelliJ already. If that’s the case, just click the one that says “Edit Configurations”

In this box, you’ll want to hit the “+” button in the top left corner and select “Remote JVM Debug.” It should now look like this:

Remote Debug

We just need the default settings. You should add a descriptive name in the top box, perhaps “Capers Remote Debug”. After you add a name, go ahead and hit “Apply” and then exit from this screen. Before we leave IntelliJ, place a breakpoint in the main method of the Main class so we can actually debug. Make sure this breakpoint will actually be reached, so just put it on the first line of the main method.

Now you’ll navigate to the testing directory within your terminal. The script that will connect to the IntelliJ JVM is runner.py: use the following command to launch the testing script:

python3 runner.py --debug our/test02-two-part-story.in

If you wanted to run a different test, then simply put a different .in file. If you’d like the .capers folder to stay after the test is completed to investigate its contents, then use the --keep flag:

python3 runner.py --keep --debug our/test02-two-part-story.in

For our example it doesn’t matter what you do; we’ve just included it in case you’d like to take a look around. By default, the .capers that is generated is deleted.

If you see an error message, then it means you are either not in the testing directory, or your REPO_DIR environment variable isn’t set correctly. Check those two things, and if you’re still confused then ask a TA.

Otherwise, you should be ready to debug! You’ll see something like this:

   ============================================================================
  |                   ~~~~~  You are in debug mode  ~~~~~                      |
  |   In this mode, you will be shown each command from the test case.         |
  |                                                                            |
  |   There are three commands:                                                |
  |                                                                            |
  |   1. 'n' - type in 'n' to go to the next command without debugging the     |
  |            current one (analogous to "Step Over" in IntelliJ).             |
  |                                                                            |
  |   2. 's' - type in 's' to debug the current command (analogous to          |
  |            "Step Into" in IntelliJ). Make sure to set breakpoints!         |
  |                                                                            |
  |   3. 'q' - type in 'q' to quit and stop debugging. If you had the `--keep` |
  |            flag, then your directory state will be saved and you can       |
  |            investigate it.                                                 |
   ============================================================================

test02-two-part-story:
>>> capers story "Hello"
>

The top box contains helpful tips. What we see next is the name of the .in file we’re debugging, then a series of lines that begin with >>> and >.

Lines that begin with >>> are the capers commands that will be run on your Main class, i.e. a specific execution of your program. These correspond to the commands we saw in the .in file on the right side of the >.

Lines that begin with > are for you to enter debug commands on. The 3 commands are listed in the helpful box.

Remember that each input file will list multiple commands and therefore multiple executions of our program. We need to first figure out what command is the culprit.

Type in the single character “n” (short for “next”) to execute this command without debugging it. You can think of it as bringing you to the next command.

One of these will error: either your code will produce a runtime error, or your output wasn’t the same. For example:

   ============================================================================
  |                   ~~~~~  You are in debug mode  ~~~~~                      |
  |   In this mode, you will be shown each command from the test case.         |
  |                                                                            |
  |   There are three commands:                                                |
  |                                                                            |
  |   1. 'n' - type in 'n' to go to the next command without debugging the     |
  |            current one (analogous to "Step Over" in IntelliJ).             |
  |                                                                            |
  |   2. 's' - type in 's' to debug the current command (analogous to          |
  |            "Step Into" in IntelliJ). Make sure to set breakpoints!         |
  |                                                                            |
  |   3. 'q' - type in 'q' to quit and stop debugging. If you had the `--keep` |
  |            flag, then your directory state will be saved and you can       |
  |            investigate it.                                                 |
   ============================================================================

test02-two-part-story:
>>> capers story "Hello"
> n
ERROR (incorrect output)

Directory state saved in test02-two-part-story_0

Ran 1 tests. 0 passed.

For us, it was our first command. Notice also it tells us Directory state saved in test02-two-part-story_0 because we had the --keep flag enabled. We could now investigate that directory to see what happened. If we debugged again with the --keep flag on the same test, we’ll get a new directory Directory state saved in test02-two-part-story_1 and so on.

Once you’ve found the command that errors, do it all again except now you can hit “s” (short for “step”) to “step into” that command, so to speak. Really what happens is the IntelliJ JVM waits for our script to start and then attaches itself to that execution. So after you press “s”, you should hit the “Debug” button in IntelliJ. Make sure in the top right the configuration is set to the name of the remote JVM config you added earlier (this is why it is helpful to give it a good name).

This will stop your program at wherever your breakpoint was as it’s trying to run that command you hit “s” on. Now you can use your normal debugging techniques to step around and see if you’re improperly reading/writing some data or some other mistake.

You might get scenarios where the command you’re debugging did everything it was supposed to: in these cases, it means you had a bug on a previous command with persistence. For example: let’s say your second invocation looks like it is doing everything correctly, except when it tries to read the previous story (that should have been persistently stored in a file) it receives a blank file (or maybe the file isn’t even there). Then, even though the second execution of the program has output that doesn’t match the expected, it was really the previous (first) execution that has the bug since it didn’t properly persist the data.

These are very common since persistence is a new and initially tricky concept, so when debugging, your first priority is to find the execution that produced the bug. If you didn’t, then you would be debugging the second (non-buggy) execution for hours to no avail, since the bug already happened.

Debugging will look exactly the same for Gitlet as described in this section. Even though your work for this debugging epilogue is not graded, you should still complete it! You will be very upset later trying to debug Gitlet without these skills.

Don’t forget to checkout your lab09 back to your solution and not the skeleton! You can do this by repeating the steps you did earlier, but checking out using the commit ID corresponding to your solution.

Tips, FAQs, Misconceptions

Tips

These are tips if you’re stuck!

  • setUpPersistence: In setUpPersistence, you should make sure that if the files and folders you need for the program to work don’t exist yet that they are made.
  • writeStory: You should be using readContentsAsString and writeContents. Since the story is just plain text (i.e. it’s just a string), you do not need to serialize anything.
  • saveDog: You should be using writeObject, since Dogs aren’t Strings so we want to be able to serialize them. Make sure you’re writing your dog to a File object that represents a file and not a folder!
  • fromFile: You should be using readObject. This should be similar to saveDog except you’re loading a Dog from your filesystem instead of writing it!

FAQs & Misconceptions

  • If make check isn’t working, you may need to add an additional argument when you call make check. Most computers take the command python3 in order to run Python. However, on some computers you may need to type python or py to run Python. If this is the case for you, you should use the flag:
$ make check PYTHON=<whatever you use to run Python files>

So if you use py to run Python, then use the command:

$ make check PYTHON=py

If you don’t want to keep adding this extra text to make check every time, you can edit line 25 of the file called Makefile.

  • writeObject: writeObject takes in (1) the File object that represents the file you want to write the object to and (2) the object you want to serialize and write into the file. The first argument should be a File object that represents a file on your filesystem, not a directory.
  • The second argument to our Utils.readObject requires a .class object. For example, if we wanted to read an object in as a Deque, we might do readObject(ad, Deque.class).
  • File objects can represent both files and directories in your filesystem. The only way to differentiate between them is the methods you use with the File object. You can check if a File object represents a directory with .isDir(), which you shouldn’t need for the lab since you should already know which File objects represent files and which represent directories.
  • Creating a new File object in Java does not create the corresponding file or directory on your computer. The file is only created when you call .createNewFile() or mkdir() on that File object. You can think of File objects as pointers to files or directories - you can have multiple of them, and whenever you want to actually change the corresponding file or directory, you will need to call specific methods (usually the ones in Utils with “read” and “write” in the name).
  • Utils.join(File d, String s) is shorthand for new File(File d, String s) (and Utils.join(String d, String s) is shorthand for new File(new File(d), String s)), both of which will create a new File object that represents the file or folder called s in the d directory. Again, this doesn’t make the actual file/folder in your filesystem until you call appropriate methods.
  • When we say “make changes persistently”, that means you should make the changes in Java and then also make sure that those changes are reflected on your filesystem by writing those changes back into the appropriate files.

Credits

Capers was originally written by Sean Dooher in Fa19. Spec was written by Michelle Hwang in Sp20 and adapted for Su20 by Zoe Plaxco.