Rewriting and Refactoring Hamurabi.bas Part 3.6 More Random Testing

Porting Hamurabi to Javascript has been pretty easy, but I got into the weeds writing tests, and then hit a swamp testing the few lines of game logic. Enter random testing, throwing semi-random data at the function, and seeing what comes out.

First, the good news. I factored out the code into libraries, so we now have this for hamurabi.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="refresh" content="30" />
    <script src="hamurabi.js"></script>
    <script src="test-lib.js"></script>
    <script src="testdata.js"></script>
    <script src="hamurabi-tests.js"></script>
</head>
<body>
<script>
test_start_tests(state, uistate);
</script>
</body>
</html>

Then, over in generate.html, we altered the code a little bit, so it will print out some Javascript code for us:

var r = generate_play_turn_object();

var testcases = [];
for(var i=0; i<5000; i++) {
    testcases.push( { id: i, data: r() } );
}
document.write("<pre>var testdata=");
document.write(JSON.stringify(testcases).replace(/}},{/g,"}},\n{"));
document.write(";</pre>");

To use this, I ran it in the browser, and then did a Save Page… and saved it as text to testdata.js.

It produced something like this:

var testdata=[{"id":0,"data":{"population":311,"year":2,"acres":27,"plant":294,"feed":247,"stores":1857}},
{"id":1,"data":{"population":388,"year":10,"acres":143,"plant":128,"feed":86,"stores":248}},
{"id":2,"data":{"population":1,"year":3,"acres":219,"plant":32,"feed":283,"stores":166}},
{"id":3,"data":{"population":1822,"year":1,"acres":3,"plant":114,"feed":349,"stores":953}},
{"id":4,"data":{"population":570,"year":10,"acres":300,"plant":297,"feed":303,"stores":1115}},
{"id":5,"data":{"population":1474,"year":10,"acres":82,"plant":272,"feed":309,"stores":571}},
{"id":6,"data":{"population":743,"year":2,"acres":148,"plant":5,"feed":125,"stores":1237}},
{"id":7,"data":{"population":63,"year":6,"acres":1,"plant":41,"feed":32,"stores":202}},
{"id":8,"data":{"population":699,"year":4,"acres":198,"plant":92,"feed":272,"stores":429}},
{"id":9,"data":{"population":511,"year":2,"acres":170,"plant":238,"feed":396,"stores":489}}...

The test data is an array of randomly generated inputs. I settled for 5000 of these.

I figured I need to replay this data, over and over.

The test function is pretty complicated. The stats on the outputs showed a lot of NaNs and Infinties, and that was bad. I looked at the outputs, and some of the inputs were producing these numbers. The first one that showed it had some obviously bad input.

The code needed to be made resilient to bad inputs. Bad inputs should throw some kind of error, and we should just expect these errors to be thrown. However, other kinds of errors should be considered real errors, re-thrown.

The outputs are tested at each run, and we look for out-of-bounds values.

At this time, though, I don’t have bounds checking code in play_turn(),
I don’t get any error messages.

function test_play_turn() {  
    // A "turn" reads the state, and applies a few properties to "play" the turn:
    // plant - number of acres to plant.
    // feed - number of people to feed.
    // The value of stores has already been modified before it's been played.
    var st = {};
    test_assert_fails(play_turn, {}, "0 play_turn should have failed on empty object");

    // play the testdata, 1x per data
    var results = [];
    var count = testdata.length;
    for(var i=0; i<count; i++) {
        var obj = testdata[i];
        for(var j=0; j<3; j++) {
            try {
                var res = play_turn(obj.data);
                results.push(res);
            } catch(err) {
                /*
                 * Ignore some exceptions that *should* be thrown
                 * when the input is bad.
                 */
                if (typeof(err)=="string") {
                    // if it's a string, do nothing
                } else {
                    throw err;
                }
            }
            // check the result to see if there's out-of-range numbers like
            // NaN and Infinity. If so, report it.
            try {
                values_out_of_range(res);
            } catch(err) {
                console.log(err, "@", obj.id);
            }
        }
    }
    // results = test_array_to_columns(results);
    // dump_results(results);

    function values_out_of_range(data) {
        for(var key in data) {
            if (isNaN(data[key])) { throw key + " is NaN"; }
            if (Infinity == data[key]) { throw key + " is Infinity"; }
            if (-Infinity == data[key]) { throw key + " is -Infinity"; }
        }
    }
    function dump_results(results) {
        var range;
        var variance;
        for(var key in results) {
            range = test_range(results[key]);
            variance = test_mean_variance(results[key]);
            console.log(key);
            console.log(range);
            console.log(variance);
        }
    }
}

The code to detect these exceptions thrown by bad inputs is pretty weak. I never bothered to make custom exceptions; instead, I just threw strings. Starting with this round of bug finding and fixing, I’ll throw some custom exceptions… but first, some reading.

generate

hamurabi

hamurabi

hamurabi-tests

testdata

test-lib

Leave a Reply