Embercode
✕
VS CodeLesson 5 of 10

55 min read

Module assignment: the broken gradebook

The project

You have joined a small school as a volunteer developer. A teacher wrote a program called gradebook.js that reads a list of students with their scores and prints a report: each student's average and letter grade, the class average, and the top student. The teacher says: "It prints rubbish." Your job is to set up a proper workspace, reproduce the problem, find every bug using the method from the last lesson, fix them, and write down what you found.

This assignment uses the whole module: the folder and settings from lesson 1, the extensions and shortcuts from lesson 2, the terminal run and error reading from lesson 3, and the debugger from lesson 4. Plan for 60 to 90 minutes.

The spec

The program reads from standard input:

  • The first line holds an integer n, the number of students.
  • Then n lines follow. Each has a name (no spaces) and three scores, all separated by single spaces.

The program prints, in this order:

  1. One line per student: NAME: AVERAGE (GRADE), with the average to one decimal place.
  2. One line: class average: X, the mean of the students' averages, to one decimal place.
  3. One line: top: NAME, the student with the highest average. If two students tie, the first one in the input wins.

Grades follow these bands, applied to the average: 90 or more is A, 75 or more is B, 60 or more is C, anything lower is F.

Example

Input:

4
Anisha 90 95 100
Ravi 70 80 75
Meera 55 60 65
Kabir 88 92 90

Expected output:

Anisha: 95.0 (A)
Ravi: 75.0 (B)
Meera: 60.0 (C)
Kabir: 90.0 (A)
class average: 80.0
top: Anisha

A second example, with a failing student:

2
Asha 40 50 30
Vik 59 59 59
Asha: 40.0 (F)
Vik: 59.0 (F)
class average: 49.5
top: Vik

The broken program

This is the teacher's code. It is a complete program: put the example input in the Input box below the code and press Run to see the rubbish for yourself.

JavaScript · editable

With the first example as input, the output you see is:

Anisha: 3031700.0 (A)
Ravi: 236025.0 (A)
Meera: 185355.0 (A)
Kabir: 296430.0 (A)
class average: 1249836.7
top: 

Everything is wrong at once, which is typical: one bug can hide others behind it. There are four planted bugs, of four different kinds. You do not need to rewrite the program. Each fix changes one line or a few characters. Do not change the structure.

Requirements

Workspace

  • Create a folder forgestack/module05/gradebook and open the forgestack folder (not a single file) in VS Code.
  • Save the teacher's program as gradebook.js and the example input as week1-input.txt in that folder.
  • Add a workspace settings file .vscode/settings.json with at least: editor.formatOnSave set to true, files.autoSave set to afterDelay, editor.tabSize set to 2.
  • Add .vscode/extensions.json recommending esbenp.prettier-vscode.
  • Add a .prettierrc that sets semi to true and tabWidth to 2, and format gradebook.js with it.

Running

  • Run the program from the integrated terminal with the input piped from the file, using node gradebook.js < week1-input.txt, and record the wrong output in BUGS.md.
  • Add a .vscode/tasks.json task named "run gradebook" that runs the current file, bound to Ctrl+Shift+B.
  • Add a .vscode/launch.json configuration that debugs the current file in the integrated terminal.

Debugging

  • Find all four bugs. For each, write an entry in BUGS.md with these four lines: Symptom (what you saw), Prediction (what it should have been), First wrong step (the line and what the value was), and Fix (the change).
  • Use the debugger (a breakpoint and the Variables panel) to find at least one bug, and print statements to find at least one other. Note which tool you used for each.
  • Use a conditional breakpoint or a logpoint at least once.
  • After your fixes, the program prints the expected output for both examples above.
  • Remove every debug print and breakpoint-related leftover.

Stretch

  • Make the program accept an optional file path argument, so that node gradebook.js week1-input.txt also works, and use it in launch.json through the args setting.
  • Write check.js (a short script that runs the program on three inputs and compares output) and make all three tests pass.
  • Handle an input with a trailing blank line without crashing.

Grading rubric

LevelWhat it looks like
60 percentThe workspace is set up (folder open, settings file, formatter). The program runs from the terminal. At least two of the four bugs are found and fixed, and the output of the first example is correct for those parts. BUGS.md exists with at least two entries.
80 percentAll four bugs are fixed and both examples print the exact expected output. BUGS.md has four complete entries with symptom, prediction, first wrong step and fix. The debugger was used for at least one bug, with a note about what the Variables panel showed. tasks.json and launch.json work.
100 percentEverything above, plus: a conditional breakpoint or logpoint was used and described, the extensions and Prettier files are committed in the project, all debug leftovers are gone, the stretch items are done (file argument and check.js passing all three tests), and each BUGS.md entry explains why the bug caused the specific wrong number you saw (for example, why the average came out as 3031700).

Hints

Try each task before you open its hint. Hints are ordered from small nudge to bigger one.

Hint 1: where to start

Start with the first wrong thing in the output, and work backwards. The class average and the top line depend on the per-student lines, so a wrong per-student line hides everything after it. Fix and re-run, one bug at a time. Change one thing per run.

Hint 2: the huge averages

The averages are in the millions. Nothing legitimate produces that from numbers between 0 and 100. Print typeof s inside the average loop, or set a breakpoint on sum = sum + s; and look at s in the Variables panel. Remember from lesson 4: input arrives as text, and + on text joins it. What does 0 + "90" give? Compare with the example in which "5" + "3" printed 53.

Hint 3: the grades

After fixing the averages, Anisha: 95.0 (A) is right but Ravi: 75.0 shows C instead of B, and Kabir: 90.0 shows B instead of A. Look only at people whose average sits exactly on a boundary. Read the band description in the spec again: "90 or more". Which comparison does the code use?

Hint 4: the top student

The last line is top: with no name. That means topName was never changed from its starting value "". Put a breakpoint on the if (avg < topAvg) line and step through with F10. Predict, for the first student, whether the condition should be true. topAvg starts at 0 and the average is 95.

Hint 5: the class average

When the other three are fixed, the class average for the first example prints 106.7 instead of 80.0. Compute the expected value on paper: the four averages are 95, 75, 60 and 90. What do they add up to, and what should you divide by? Look at the divisor in the last lines of the program. The Debug Console is a good place to evaluate classTotal and count before the line runs.

Hint 6: running with input in the debugger

The debugger needs a place to read standard input. Use "console": "integratedTerminal" in launch.json. When the program starts, type the lines of input into the terminal, then press Ctrl+D (or Ctrl+Z then Enter in Windows) to say the input has ended. For the stretch goal, change the first line of the program to read from process.argv[2] || 0, and put "args": ["${workspaceFolder}/module05/gradebook/week1-input.txt"] in launch.json. Then you do not have to type the input at all.

Hint 7: logpoints and conditions

Right-click the gutter next to line 29 (the line that prints a student) and choose Add Logpoint. Type the message name={name} avg={avg} and run. The terminal shows one message per student, and the program never pauses. For a conditional breakpoint, right-click the red dot, choose Edit Breakpoint, pick Expression, and type name === "Ravi" to stop only on that student.

The configuration files

If you get stuck on the workspace part, here are working versions. Try writing them yourself first.

.vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "files.autoSave": "afterDelay",
  "editor.tabSize": 2
}

.vscode/extensions.json:

{
  "recommendations": ["esbenp.prettier-vscode"]
}

.prettierrc:

{
  "semi": true,
  "tabWidth": 2
}

.vscode/tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "run gradebook",
      "type": "shell",
      "command": "node",
      "args": ["${file}"],
      "group": { "kind": "build", "isDefault": true }
    }
  ]
}

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug gradebook",
      "program": "${workspaceFolder}/module05/gradebook/gradebook.js",
      "args": ["${workspaceFolder}/module05/gradebook/week1-input.txt"],
      "console": "integratedTerminal"
    }
  ]
}

Notice that this launch.json passes the input file as an argument, so it only works after you complete the stretch item that reads process.argv[2]. If you skip the stretch, drop the args line and type the input into the terminal.

The check.js script

This is the self-checking script for the stretch goal. It runs gradebook.js on three inputs and compares the printed text with the expected text. Save it next to gradebook.js and run node check.js.

const { spawnSync } = require("child_process");
 
const cases = [
  {
    name: "sample class",
    input: "4\nAnisha 90 95 100\nRavi 70 80 75\nMeera 55 60 65\nKabir 88 92 90\n",
    expected: "Anisha: 95.0 (A)\nRavi: 75.0 (B)\nMeera: 60.0 (C)\nKabir: 90.0 (A)\nclass average: 80.0\ntop: Anisha\n",
  },
  {
    name: "single student",
    input: "1\nSolo 100 100 100\n",
    expected: "Solo: 100.0 (A)\nclass average: 100.0\ntop: Solo\n",
  },
  {
    name: "failing student",
    input: "2\nAsha 40 50 30\nVik 59 59 59\n",
    expected: "Asha: 40.0 (F)\nVik: 59.0 (F)\nclass average: 49.5\ntop: Vik\n",
  },
];
 
let passed = 0;
for (const c of cases) {
  const run = spawnSync("node", ["gradebook.js"], { input: c.input, encoding: "utf8" });
  if (run.stdout === c.expected) {
    console.log("PASS", c.name);
    passed++;
  } else {
    console.log("FAIL", c.name);
    console.log("  expected:", JSON.stringify(c.expected));
    console.log("  got:     ", JSON.stringify(run.stdout));
  }
}
console.log(passed + " of " + cases.length + " passed");

On the broken program it prints three FAIL lines. After your fixes it prints:

PASS sample class
PASS single student
PASS failing student
3 of 3 passed

Reference solution

Open this only after you have tried hard. First the four bugs, then the fixed code.

The four bugs
  1. Scores are text, not numbers (a type bug). parts.slice(1) gives a list of strings such as "90". In average, sum = sum + s starts with the number 0 and adds a string, which joins text: 0 + "90" is "090", then "090" + "95" is "09095", and so on. The final "09095100" is divided by 3, which turns it back into a number: 3031700. That is why Anisha's average was 3031700.0. Fix: convert at the source with parts.slice(1).map(Number).

  2. Grade boundaries use the wrong comparison (a boundary bug). The spec says "90 or more", but the code tests avg > 90, which excludes exactly 90. Kabir with exactly 90.0 got a B, Ravi with 75.0 got a C, and Meera with 60.0 got an F. Fix: use >= in all three comparisons. (After bug 1 is fixed you can see this; before that every grade showed A because the averages were huge.)

  3. The top-student test points the wrong way (a logic bug). if (avg < topAvg) is true only when the new average is lower than the best so far. Since topAvg starts at 0 and no average is below 0, the body never runs and topName stays empty. Fix: if (avg > topAvg). Using a strict > also gives the "first student wins a tie" rule.

  4. The class average divides by the wrong count (an arithmetic bug). classTotal / (count - 1) divides by one less than the number of students. With four students the total is 320, and 320 divided by 3 is 106.7, not 80. Fix: divide by count.

The fixed program

This version also has the optional file argument from the stretch goal. Remove process.argv[2] || if you skipped it. Run it with the first example in the Input box:

JavaScript · editable
Anisha: 95.0 (A)
Ravi: 75.0 (B)
Meera: 60.0 (C)
Kabir: 90.0 (A)
class average: 80.0
top: Anisha

For the second example the output is:

Asha: 40.0 (F)
Vik: 59.0 (F)
class average: 49.5
top: Vik

Four edits: .map(Number), three >= signs, avg > topAvg, and count as the divisor. Every other line is unchanged, which is the point: the structure was fine, and each bug was one wrong token.

A sample BUGS.md
# Bugs in gradebook.js
 
## Bug 1: averages in the millions
Symptom: Anisha: 3031700.0 (A), every grade A.
Prediction: Anisha's average should be (90 + 95 + 100) / 3 = 95.0.
First wrong step: in average(), at sum = sum + s, s is the text "90", not a number.
Found with: debugger, breakpoint on that line, Variables panel showed s = "90" with quotes.
Fix: parts.slice(1).map(Number).
 
## Bug 2: wrong grades on exact boundaries
Symptom: Kabir (90.0) got B, Ravi (75.0) got C.
Prediction: 90 or more is A, 75 or more is B.
First wrong step: gradeFor uses > instead of >=.
Found with: print statement showing avg and the grade for each student.
Fix: >= in all three comparisons.
 
## Bug 3: empty top line
Symptom: "top: " with no name.
Prediction: topName should become Anisha on the first loop turn.
First wrong step: avg < topAvg is false for 95 < 0, so topName never changes.
Found with: conditional breakpoint on name === "Anisha".
Fix: avg > topAvg.
 
## Bug 4: class average 106.7
Symptom: class average 106.7 for the sample.
Prediction: (95 + 75 + 60 + 90) / 4 = 80.0.
First wrong step: the divisor is count - 1, which is 3.
Found with: Debug Console, evaluating classTotal and count.
Fix: divide by count.

Module summary

  • VS Code opens a folder as the workspace, and the window has an activity bar, side bar, editor, panel and status bar.
  • Settings come in default, user and workspace layers, and the .vscode folder shares project settings, tasks, debug setups and extension recommendations.
  • Extensions are code, so you judge the publisher and install count first. Prettier formats and ESLint warns.
  • The command palette, Ctrl+P, line-editing shortcuts, multiple cursors, F2 rename and snippets make editing fast.
  • The integrated terminal runs node file.js from the current folder, and Node reads the file from disk, so you save first.
  • Programs take input from stdin, arguments and environment variables, and report success with exit code 0.
  • An error message has a place, a kind, a message and a stack trace, and you read it from the top.
  • Debugging follows reproduce, predict, look, find, fix. The bug is at the first wrong step. Use prints, bisecting, breakpoints, stepping, watches and the call stack.

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Debugging basics