Sidenote - unit test
We have now completed the RPN engine and are able to pass do integrate the graphical operators as vector graphics is the purpose of PostScript. Before we step to that, look how we are testing and how are we dealing with errors.
As you have seen, all articles showed some example of the features but there was no systematic testing. What if other values are given? What if the values are invalid? Our code becomes more and more complex and we will need more and more time to track for errors when they happen.
This is where the concept of unit test comes in. Unit tests are automatic tests of a given function. Each test consists of a function, an input and an expected output. If the expected output does not come out, an error is thrown.
The idea is that you write a test set for each function (some write it even before writing the function itself) and then you run this tests automatically. Every time you change the codebase you run these tests to check if anything has gone broken.
The unit tests check for each element of the code if it is working properly. They do not check the entire system which would need integration tests that are much more complex. They are still useful, because they reduce possible error sources.
What kind of inputs would you provide? Valid inputs, invalid inputs, edge cases. Some use random inputs, but these would need you to use another algorithm to produce the expected result. We will go another way which is proposed by some: make your unit tests on open code. Inspect the code and provide tests so that each conditional branch is run at least once.
What kind of outputs would you check? Normally it would be the result of the function. But our operators are not functions. They do not have input either. The operators act on a context which is the stack and the dictionary. What we can do is use RPN code as input and the stack as output.
For example, we use "3 4 add" as input and [7] as expected output, run the RPN on it and check the stack. We do this with every operator for each branch inside each operator. We can write a general purpose test function that proceeds an array of tests and throws errors if the do not pass.
To be able to write that function, we need to refactor our code however. At this time, we used exception handling for errors. Each time an error occurs, the execution stops and we loose also context.
We can do better. Instead of throwing errors, we create a new type rpnError. If the operators has invalid inputs, it puts an rpnError on the stack and returns the context. This does RPN not stop from running. For the unit test this is ok, it can just verify the error on the stack.
For normal running it is not ok. The error should stop at last before the next operator is executed. We will add context.lasterror and check that if at the important points.
We build now our unit test, half verbose: It logs only errors.
And now integrate the unit test directly with the operator definitions
Voilà. I found several errors with the unitTest I could fix. I left one. false pushed 1 on the stack, not 0. This triggered multiple errors here.
For the operators def, put and putinterval, the result is not visible in the stack, but in the dictionary and the heap. To make a valid test, you must recall the dict entry to the stack. We will have to use this technique when we draw. Graph operators act mainly on the graph state and not on the stack. But for the unit test, we always can call values of the states to the stack, so the test is possible.
We have now built a solid unit test environment. For each operator so on, we will add unit tests that run automatically to prevent surprises.
We are ready to draw.
One last thing. I promised a weekday algorithm in PostScript earlier, here what I would do.
The operators takes day, month and year. It counts then the bisextile years. There are 97 for every 400 years, 24 for the resting 100 years and 1 for every resting period of four years.
It then creates an offset tables of beginning of months for the normal years and modifies the february entry of the current year is bisextile.
Finally, it adds altogether: years + bisextile years + month dayoffset + day and some constant to get the total number of days.
It then calculates mod by 7 to get the weekday
The operator does not check of days and months are valid. It does not work before 1983, as the gregorian calendar starts in October 1582 and some days are missing that month.
The codebase including the unitTest has now 910 lines ps20240708.js