(Click on the
icon above for shortcut links)
hwtest is a library that helps you check your answers to programming
problems. I will generically refer to a problem set as a "homework", whether
it is graded or ungraded, done in class or out of class.
Your teacher will distribute starter code for a homework, as well as descriptions of the problems in the homework. The descriptions might be included as comments in the starter code, or might be someplace else, like a course webpage.
The starter code will likely be a directory with a single .scala file
(perhaps with a name like hw1.scala) and several other supporting files
and directories. I will assume the directory is in the form of an sbt
project, in which case the hw1.scala file will be in either the top-level
directory or in a src/main/scala sub-directory.
Open the hw1.scala file, and it will look something like this:
object hw1 extends hwtest.hw("CS123"):
def userName = ???
def biggest(x: Int, y: Int): Int =
???
test("biggest", biggest, "x", "y")
def smallest(x: Int, y: Int, z: Int): Int =
???
ignoretest("smallest", smallest, "x", "y", "z")Normally, the first thing you should do is fill in the userName, but (just
this once) try running the code without filling in your userName to see what
happens. (If you're using sbt, you can do this as sbt run or even
better sbt ~run, which will automatically re-run the tests every time
you save the file.) It will print something like
CS123: hw1 (hwtest 1.0.0)
scala.NotImplementedError: an implementation is missing
at scala.Predef$.$qmark$qmark$qmark(Predef.scala:344)
at hw1$.userName(hw1.scala:2)
but with several more lines of stack trace. The scala.NotImplementedError...
part indicates that an exception was thrown. Search through the stack trace
for the first at line that comes from your file, in this case
at hw1$.userName(hw1.scala:2)
which tells you that you should look at line 2 in your code, which is
def userName = ???The ??? is a placeholder for code that hasn't been filled in.
It always throws NotImplementedError when run. Replace the
??? with a string containing your name, as in
def userName = "Margaret Hamilton" // but with your name obviously(Margaret Hamilton is a famous computer scientist best known for leading the development of the flight software for the Apollo missions.)
If you used sbt ~run, the code will re-run as soon as you save your change.
If not, you can sbt run again. Either way, the code now outputs something
like
CS123: hw1 (hwtest 1.0.0)
Margaret Hamilton
Data source: hw1.tests (remote)
Begin testing biggest at Sat Aug 13 11:02:48 EDT 2022
Test #1 *** FAILED ***
x = 7
y = 7
scala.NotImplementedError: an implementation is missing
Test #2 *** FAILED ***
x = 1
y = 2
scala.NotImplementedError: an implementation is missing
XX
Passed 0/4 tests in 0.58 seconds.
***** Ignoring tests for smallest.
You can see that the first two test cases for the biggest function
failed, again reporting the scala.NotImplementedError because
the body of the biggest function is still ???. The XX after
the message for Test #2 indicates that two subsequent test cases also
failed. By default, the system will only report the details of
the first two failed test cases (per test), and reports any further
failures with an X. Successful test cases are reported with a ..
It may seem strange to hide some of the failure messages, but there are two reasons for this. First, seeing page after page of failure messages can be overwhelming. Second, if all the failures are because of the same bug, there's an excellent chance that two such messages will be enough to find the error, and if the failures stem from different bugs, then seeing many such messages can actually make it harder to find any one of those bugs.
Now you might replace the ??? in biggest with
if x < y then x
else yand now the output says
CS123: hw1 (hwtest 1.0.0)
Margaret Hamilton
Data source: hw1.tests (remote)
Begin testing biggest at Sat Aug 13 11:05:27 EDT 2022
.
Test #2 *** FAILED ***
x = 1
y = 2
Expected answer: 2
Received answer: 1
Test #3 *** FAILED ***
x = 4
y = 3
Expected answer: 4
Received answer: 3
X
Passed 1/4 tests in 0.08 seconds.
***** Ignoring tests for smallest.
Hmm, that passed the first test case, but then failed the next three.
Looking at the results for the second and third test cases, you seem to
be returning the smallest number, not the biggest number. Going back
to your code, you realize you wrote < in the if when you meant >.
(Weird. How did the first test case pass? Maybe x and y were equal?)
You change the < to > and get
CS123: hw1 (hwtest 1.0.0)
Margaret Hamilton
Data source: hw1.tests (remote)
Begin testing biggest at Sat Aug 13 11:08:32 EDT 2022
....
Passed 4/4 tests in 0.03 seconds.
***** Ignoring tests for smallest.
Yay!
Now you're ready to work on 'smallest'. You first delete ignore
from the ignoretest(...), leaving test(...). You remove the
??? and start to write a bunch of nested ifs. After a few minutes,
you delete them all, and write
-biggest(-x, biggest(-y, -z))You run the code and get
CS123: hw1 (hwtest 1.0.0)
Margaret Hamilton
Data source: hw1.tests (remote)
Begin testing biggest at Sat Aug 13 11:21:40 EDT 2022
....
Passed 4/4 tests in 0.11 seconds.
Begin testing smallest at Sat Aug 13 11:21:52 EDT 2022
..........
Passed 10/10 tests in 0.16 seconds.
Victory!
hwtest will normally download the test data every time you run your
code. Once your code has gotten as far as printing
Begin testing [...]
it has successfully downloaded the test data. Even if your
code fails every test, hwtest will keep a local copy of the test
data. This means that you can continue to make progress even if the
network or wifi goes down, or if you otherwise lose network access
(for example, maybe you're on an airplane or you've gone spelunking
for the weekend).
One implication of this is that it's a good idea to download the homework
as soon as it's available. Set your userName and run the code right
away so hwtest can fetch the test cases. Even if you don't work on the
problems right away, at least you'll have the test data.
Note that your teacher could update the test data after the homework has
been assigned, maybe even multiple times. hwtest will automatically fetch
the latest version of the test data every time you run the code (unless there
are network issues, in which case it will use the most-recently downloaded
copy).
If you run your code and it reports that you passed, say 60/100 tests, and then you run the code again a few minutes later and it reports 97/150 tests passed, this is why.
The starter code may sometimes say
testV(...){...}(or ignoretestV) instead of test/ignoretest.
Here's the difference: test is for functions where there is a unique answer
for each distinct set of inputs. For example, consider addition. Given
the inputs 4 and 7, the only correct answer is 11. In contrast, testV
is intended for functions the could possibly have more than one correct answer
for each distinct set of inputs. For example, if a function takes a
non-empty list of integers and returns the index of the maximum element, there
could be several legal answers (eg, List(1,5,5,2,5,3) could legitimately
return 1, 2, or 4). Any of those answers would be acceptable.
Alternatively, sometimes a teacher might use testV even when test would
work because testV allows greater customization of error messages.
From the student point of view, there's no need to treat it any differently
than test. Implement your function, and use any error messages to help
you debug your code.
If needed, you can change testV to ignoretestV, just like changing test
to ignoretest. Never delete or comment out a testV or ignoretestV.
Looking at the results of failed tests is a powerful aid to debugging. But
sometimes you may want to do some good old-fashioned println-debugging.
That's where the debug command comes in.
To do println-debugging:
- Usually, start by changing any
test/testVcommands for the function being debugged toignoretest/ignoretestV. - Next, include one or more manual calls to the function inside a
debugas in
def foo(n): Int = ...
ignoretest("foo", foo, "n")
debug("index out of bounds in foo") {
for i <- 1 to 10 do
println(s"foo($i) = ${foo(i)}")
}- Optionally, you can also add
print/printlns inside the function being debugged. However, do NOT wrap those innerprint/printlns in their owndebugstatements. (If you do, there is a good chance that the printed output will display in a weird order.) - When you are ready to change the
ignoretestback totest, first comment out or remove any extraprint/printlns inside the function being debugged. Then change thedebugtoignoredebuguntil you're sure you don't need it anymore.
WARNINGS:
- Don't put
print/printlns at the top level of your object. They will usually display at weird place in your output. - Contrariwise, don't put a
debugcommand inside the function that you are trying to debug. Again, this will usually display at a weird place in your output.
To protect against infinite loops/infinite recursion, hwtest will cancel the
remaining cases in a test if any one test case takes too much time (usually
half a second). The cancelled test case counts as having failed.
However, because of things like
JIT compilation,
there can be a lot of variability in the reported times of your functions.
This is especially true of the very first test/testV in the homework!
If a test/testV times out, you can try running it again, in case the
problem was transient. If it still times out, it most likely means that
your code really is too slow. If so, look for ways to speed it up.
- If you have nested loops, can you reduce the number of loops (for example, turning three nested loops into two nested loops)?
- Are you abusing any data structures?
- If you are repeatedly looking up an index in a list, can you switch to an array instead?
- If you are repeatedly looking for a particular value in a list or array, can you switch to a set instead?
- If you are repeatedly adding to the back of a list, can you add to
the front instead, possibly reversing the list later? Or can you use a
ListBufferinstead? - If you are repeatedly adding to the front of an array, can you use a
list instead? If you are repeatedly adding to the back of an array,
can you use an
ArrayBufferinstead?
Here's some advice for avoiding common mistakes:
-
DON'T EVER delete or comment out a
test/ignoretest/testV/ignoretestVline.- If you do, your code will most likely fail with a parse error.
- If you don't want to run that test, change it to
ignoretest/ignoretestV. - If your code for a particular function doesn't compile, and that's stopping
you from testing the rest of your homework, you can comment out the body
of the function (and replace that body with
???).
-
DON'T change the name, parameters, or type of a function being tested.
- If you do, your program will almost certainly either (1) not compile or (2) crash with a parse error.
- If you feel like it would be better for the function to use a different interface, you can define a helper function, and have the main function call the helper function.
-
DON'T leave an
ignoretest/ignoretestVin your code if you are turning it in.- Sometimes I see students get a function running, then change the
testback toignoretestwhile they are working on other problems. Just leave it astest-- if the function is running, it only produces a few lines of output! If you turn it in asignoretest, you may risk not getting credit for a problem you actually solved. - Even if you're failing some test cases, turning it in as
testmay help you get partial credit.
- Sometimes I see students get a function running, then change the
-
DON'T forget to change the
???in theuserName. Also, don't delete or comment out thedef userNameline.