This section does not contain any design requirements (i.e. you could complete the whole project without reading this section). However, we’ve compiled our general guidelines for design, development, and testing.

Design Workflow

This project has a lot of moving parts, and it’s normal to feel overwhelmed by the amount of requirements you need to satisfy in your design. Here is one suggestion for how you can break down this project into more manageable steps:

  • Read through the entire spec. It’s easy to miss a design requirement, which might cause you trouble later when you have to redo your design to meet the requirement you missed. We suggest reading through the entire spec front-to-back at least twice, just to make sure that you have internalized all the design requirements.
  • Design each section in order. Start with user authentication: How do you ensure that users can log in? Focus on getting InitUser and GetUser properly designed, and don’t worry about file storage or sharing yet. Then, after you’re satisfied with your login functionality, move on to the file storage functions. Don’t worry about sharing yet, and just make sure that a single user is able to LoadFile and StoreFile properly. Then, you can move on to AppendToFile. Finally, once you’re satisfied with your single-user storage design, you can move on to sharing and revoking.
  • Don’t be afraid to redesign. It’s normal to change your design as you go. In particular, if you follow the order of functions in the spec, then AppendToFile might result in changes to LoadFile and StoreFile. Also, RevokeAccess might result in changes to CreateInvitation and AcceptInvitation. It’s easier to change your design while you’re in the design phase; by contrast, it’s harder to change your design after you’ve already implemented it in code.

Coding Workflow

  • Stay organized with helper functions. If you fit all your code in 8 functions, it’s easy for the functions to get bloated and hard to debug. By contrast, if you organize your code into helper functions, you can reuse code without needing to copy-paste code blocks, and you can also write unit tests to check that each helper function is working as intended.
  • Test as you go. Don’t write a huge chunk of code and then test it at the end. This usually results in a failed test, and now you have no idea which part of the giant code block is broken. Instead, write small pieces of code incrementally, and write tests as you go to check that your code is doing what it’s supposed to.
  • Don’t split the coding between partners. Sometimes, a 2-person project group will try to have each group member independently write half of the functions. As a design-oriented project, the code in different functions will often be connected in subtle ways, and it is difficult (if not impossible) to write code without understanding all the code that has been written so far. A better approach is to work together to figure out the high-level organization of your code. Ideally, you’d use a technique like pair programming to ensure that both partners understand the code being written. The only scenario where writing code individually might be useful is for isolated helper functions, where the behavior is clearly documented and the function can be tested and debugged in isolation. Staff are not responsible for helping you understand code that your partner wrote.

Development Environment

Visual Studio Code (VSCode)

VSCode is a very commonly used IDE, and provides a powerful set of code and debugging environments that can be exploited for Golang Projects. To setup VSCode for this project, follow these steps:

  • Install Golang. Make sure you have Golang installed before starting this guide.
  • Install the GoLang extension. In the “Extensions” tab (Use Ctrl+Shift+X to navigate you can’t find it), search up the Go extension that is created by Google.
  • Install the debugging environment Once the extension is installed, the lower right corner might and most likely will pop up a warning stating that analysis tools might be missing. If so, click on the install, and wait for the analysis tools to install. If you missed this the first time, press (Ctrl+Shift+P) and search up “Extensions: Install Missing Dependencies,” and follow the instructions.

Getting Started Coding

After your design review, you’re ready to start implementing your design in code. Follow these steps to get started:

  1. Install Golang. Go v1.20 is recommended.
  2. Complete the online Golang Tutorial.
    • The tutorial can take quite a bit of time to complete, so plan accordingly.
    • The tutorial is a helpful tool that you may end up referencing frequently, especially while learning Go for the first time.
  3. Accept the Project 2 GitHub Classroom Invite Link.
    • At this step, you may receive an email asking you to join the cs161-students organization.
  4. Enter a team name. If you’re working with a partner, only one partner should create a team - the other partner should join the team through the list of teams.
  5. In README.md, make sure to include the student IDs and emails of both you and your partner, as well as your team GitHub repository link.
  6. In the client_test directory of the checked out repository, run go test.
    • Go will automatically retrieve the dependencies defined in go.mod and run the tests defined in client_test.go and client_unittest.go.
    • It is expected that some tests will fail because you have not yet implemented all of the required functions.
  7. Optionally, we’ve provided a unit test framework that you can access in the client directory, where you can create unit tests for your implementation-specific functions inside client_unittest.go.
    • If you would like to only run unit tests, please rename the unit test file to client_unit_test.go (and an _ between unit and test) and run go test in the client directory. Reminder: If you later want to run unit tests with client tests together, make sure to change the name of the file back to client_unittest.go and run go test from the client_test directory.

If the starter code is buggy and you need to pull updates from the starter code repo, you can do so with these steps:

  1. Run only once:
    • If you use HTTP: git remote add starter https://github.com/cs161-staff/project2-starter-code.git
    • If you use SSH: git remote add starter git@github.com:cs161-staff/project2-starter-code
  2. Run each time you need to pull updates: git pull starter main

Please refer to our Troubleshooting Tips for an in-depth debugging guide!

Testing with Ginkgo

This section provides some basic documentation for Ginkgo, which is the framework you’ll be using to write your own test cases.

First, we recommend reading through the basic tests in client_test.go, especially the first few ones, since those are well-documented in-line. Then, come back to this documentation.

Basic Usage

You should be able to write most of your tests using some combination of calls to –

  1. Initialization methods (e.g. client.InitUser, client.GetUser)
  2. User-specific methods (e.g. alice.StoreFile, bob.LoadFile)
  3. Declarations of expected behavior (e.g. Expect(err).To(BeNil()))

Asserting Expected Behavior

To assert expected behavior, you may want to check (a) that an error did or didn’t occur, and/or (b) that some data was what you expected it to be. For example:

// Check that an error didn't occur
alice, err := client.InitUser("alice", "password")
Expect(err).To(BeNil())

// Check that an error didn't occur
err = alice.StoreFile("alice.txt", []byte("hello world"))
Expect(err).To(BeNil())

// Check that an error didn't occur AND that the data is what we expect
data, err := alice.LoadFile("alice.txt")
Expect(err).To(BeNil())
Expect(data).To(Equal([]byte("hello world")))

// Check that an error DID occur
data, err := alice.LoadFile("rubbish.txt")
Expect(err).ToNot(BeNil())

Organizing Tests

You can organize tests using some combination of Describe(...) containers, with tests contained within Specify(...) blocks. The more organization you have, the better! Read more about how to organize your tests here.

Running a Single Test Case

If you would like to run only one single test (and not both the client_test and client_unittest test suites), you can change the Specify of that test case to a FSpecify. For instance, if you have a test

Specify("Basic Test: Load and Store", func() {...})

you can rename it to

FSpecify("Basic Test: Load and Store", func() {...})

Then, if you click the Run/Debug button, you will be focusing on that specific test. If you have multiple FSpecify test cases, then all those will be run.

Warning: Please remove all FSpecify statements when you submit to the autograder (otherwise, the autograder will only run tests labelled FSpecify) or potentially break the autograder. We cannot promise we will re-run the autograder for you if you forgot to remove them.

Optional: Measure Local Test Coverage

To measure and visualize local test coverage (e.g. how many lines of your implementation your test file hits), you can run these commands in the root folder of your repository:

go test -v -coverpkg ./... ./... -coverprofile cover.out
go tool cover -html=cover.out

Coverage over your own implementation may serve as an indicator for how well your code will perform (with regards to coverage flags) when compared to the staff implementation! It should also help you write better unit testing to catch edge cases.