Thursday 28 January 2021

Fiction Review: The Girl Who Lived Twice

The latest Millennium Series novel, The Girl Who Lived Twice, from David Lagercrantz is a huge yawn.

There, I said it.


It's the third novel that he has written since the death of the original creator of the Millennium Series in 2004. And it seems fatigue is setting in. I find myself not caring very much what happens in the novel because all the characters feel like strangers to me, to some degree or other.

You'll find plenty of complimentary reviews if you do a simple web search. And even those feel forced, as though the reviewers are struggling mightily for something positive to say. This review will not be one of them.

The Premise

Some hobo dies and Mikael finds himself investigating. Meanwhile, Lisbeth is hot on the trail of Camilla, her evil sister. Will the paths of Mikael and Lisbeth cross once more?

The Characters

Lisbeth Salander. She's now become almost unbelievably bad-ass, and somehow less compelling as a character because of it. It was her fragility coupled with her brilliance that drew me in from the first novel, and now somehow it's gone.

Mikael Blomkvist
. Comes across as really lost and befuddled at the start of the novel, a far cry from the shrewd and persistent investigator we've grown with the last several novels. Mid-life crisis, perhaps?

Camilla Salander. Lisbeth's twin sister. She's beautiful, but cold. There's a certain psychosis to her narcissim. But as a villain, she's a lot less interesting now.

Jurij Bogdanov, Camilla's security guy. A competent fellow with a drug history, who managed to turn life around. Like Felix, he's always playing catch-up with Lisbeth. Interestingly, though, Lisbeth considers him "in the same league as Plague", whom she holds in certain regard.

Frederika Nyman. A medical examiner who believes in human dignity. It's this very dedication that leads her to call Mikael to investigate even though it's clearly beyond her duty.

Kadi Linder. We first meet her when she takes over Lisbeth's apartment. Tall, lively, elegant.

Vladimir Kuznetsov
. Described as an old man with white hair, a pot belly and thin legs, kind of like Santa Claus. An accomplished liar who manages to take this to a whole new level by creating fake news.

Felix, Kuznetsov's Chief Technician. Hipster guy who's run ragged trying to keep up with Lisbeth when she starts hacking Kuznetsov's network.

Sofie Melker. Mikael's young colleague at The Millennium. The first time we see her, she gets angry and tells Mikael off, which I think is pretty plucky.

Paulina Müller, a woman that Lisbeth has an affair with. She's an abused wife.

Dragan Armansky makes an appearance to provide some background information for Mikael, and that's all his involvement in this story. Pity. I like Dragan.

Hans Faste is back as the idiot police officer.

Inspector Carl Wernersson is a Faste-like one-time character who responds to Catrin's call to the police.

Jan Bublanski. The Jewish policeman is his aging kindly wise self when he first appears. and we hear that he's been engaged to Farah Sharif.

Catrin Lindås. Described as a regally beautiful woman with a haughty air. Polarizing figure online, right-winger and feminist. Has a prickly demeanor towards Mikael at first.

Sonia Modig
makes a return, but seems to serve little purpose in the story other than as an errand girl for Bublanski.

Kurt Widmark. Owns an electronics store and proves to be of help to Sonia, despite his curt (heh heh) demeanor.

Ivan Galinov. An old, highly intelligent agent with a sinister reputation. He has a prosthetic eye, and is said to be cold as ice. Close to Camilla.

Johannes Forsell. Defence Minister. Fit and intelligent. Described by Mikael as a lively and enthusiastic interview. Some recent events have reduced him to a nervous wreck.

Rebecka, Forsell's Jewish wife. A concerned spouse who has to put up with the smear campaigns started by Kuznetsov, especially the anti-Semetic ones. Later on, she sticks by her husband, though she's clearly in over her head.

Robert Carson from Colorado, who is friendly to Mikael and even quite enthusiastically assists in his research.

Plague. Other than a glorified cameo from our favorite geek, not much. What a pity.

Erika Berger. She's not involved much in the story and functions as someone for Mikael to bounce ideas off of.

Charlie Nilsson. He owns a liquor store and is investigated by Sonia regarding the beggar's death. Charlie panics quite easier and gabbles on when he's nervous.

Thomas Müller. Paulina's abusive husband who gets his comeuppance at the hands of Lisbeth.

Chief Inspector Ulrike Jenssen. A police officer who has to deal with the fallout after Lisbeth pays Thomas a visit.

Marko Sandström. President of the Svavelsjö MC. Surprisingly not scruffy-looking and well-spoken.

Krille. A common thug who is part of Svavelsjö MC.

Heikki Järvinen. A man who gets into a fight with the beggar at the beginning of the story, and gets interviewed by Bublanski. Comes across as just another drunk.

Elin Felke, a woman who was part of the Everest expedition. She turns out to be extremely helpful in helping Blomkvist put piece together.

Conny Andersson. A common thug from Svavelsjö MC that quickly gets softened up by Lisbeth.

Svante Lindberg. Colleague of Johannes Forsell. Described as hearty and fit, but also shrewd and manipulative. He's the one behind the entire blackmail angle.

Else Sandberg. A young medical intern mentioned at the start of the story, and then again in the middle assisting Officer Bublanski in the ongoing investigation.

Janek Kowalski. An old man who happens to be some kind of espionage operative.

The Mood

Meh. The author fails to recapture the suspense that made the original series by Larsson such a hit. In fact, he's been failing for the last two novels already.

What I liked

The way Lisbeth finds out Camilla's address is pretty neat. Under the guise of catching her balance while attacking Camilla, she attaches a GPS tracker to Camilla's car. That's the kind of thing I want to see from a Millennium Series tech thriller.

When Lisbeth gets bad-ass. Lisbeth kicking Conny Andersson's ass was pretty great. And she even made it all look so casual. Lisbeth's dispensation of retribution on Thomas is predictable, but oh-so-vintage Salander.

Camilla's death, for someone so narcissistic, is really quite poetic.

What I didn't

Erika and Gregor are getting divorced. Not that it bothers me, but... so what?

The sudden bout of passion-filled sex between Catrin and Mikael is interesting but just short of being believable.

The twist that Camilla was sexually abused by Zalachenko. Jesus, the guy's been dead for three books. Do we really need to keep shitting on his grave? He was evil, we get it!

Consistency of characters. We're treated to paragraph upon paragraph of how loyal Bogdanov is to Camilla in this book and the previous one... but it's also Bogdanov suddenly deciding that he's had enough of Camilla's shit, to betray her. This is such crap writing. The Deux Ex Machina is clumsy AF.

Conclusion

This review is short, way shorter than any other review I've ever written on the Millennium Series. There's a reason for that: this novel doesn't deserve better. It's a lackluster effort that feels like an insult to the memory of Stieg Larsson. Larsson's work could occasionally be one-dimensional in some places, and sometimes accused of being overly convoluted. But it was gripping stuff.

I'm sorry to say this, but the series has run its course. Lagercrantz is definitely running out of ideas at this point, and the book is pretty much a waste of paper. If he wants to retain reader interest (thankfully, it seems that this will be his final Millennium Series novel), he's going to have to do much better than this tripe.

My Rating

3 / 10

I ain't reading this book twice,
T___T

Friday 22 January 2021

Some basic realities behind the WhatsApp exodus

What a coincidence. At the beginning of this month, I was speaking of data privacy in tech. A couple weeks later, Facebook has once again given us something to talk about. This time, it's the Facebook-owned WhatsApp that has generated the controversy with an update to their Privacy Policy. They will apparently be sharing data with their parent company.

Now, this shouldn't come as any sort of surprise; after all, WhatsApp warned us back in 2016. What did surprise me was the sheer amount of people who upped and left, and started new accounts on apps such as Telegram and Signal.

Time to ditch WhatsApp?


Like, why now? All of a sudden, this reminded them that Mark Zuckerberg can't be trusted with their data? Kind of late, isn't it?

The alleged privacy issue

People have stated that this change to the Privacy Policy was what prompted them to move. Bear with me a moment; I'm about to explain why this is silly.

Firstly, WhatsApp's messaging service is as secure as ever, even from Facebook. No one short of a truly gifted (and determined) hacker can read your perfectly uninteresting messages and listen in on your perfectly uninteresting conversations. The data that is meant to be shared, is with WhatsApp's business entities, and even that is limited data that can be used for targeted advertising. In short, these changes to the privacy policy are only in effect with regard to conversations with businesses on WhatsApp.

Businesses. Not individuals.

So the panicked masses jumping ship over to seemingly more secure apps are really just overreacting. Chill! There's no guarantee that Telegram and Signal won't go the same way WhatsApp has gone. All it takes - all it ever seems to take - is an interested buyer. Sure, the owner of Telegram, Pavel Durov, has pledged never to sell. The co-founder of Signal and also co-founder of WhatsApp, Brian Acton, is all about the data privacy. Then again, how well do you know these guys? Promises are easy to make and even easier to break.

Secondly, I'd like to remind people that WhatsApp cost them nothing to install and use. My stance back then is pretty much the same now - if you aren't paying to use something, suck it up. If your precious privacy was really all that important, you'd spend a few bucks protecting it, wouldn't you?

Nothing is for free. Ever.

From the first day I started using WhatsApp, I've assumed that anything I say can be accessed and used against me. I don't expect anything to be private. It's a risk that every online user has to live with.

That said...

I actually do think it's a good idea to ditch WhatsApp. Though not for the knee-jerk reaction that many are having. You see, I don't like near-monopolies in tech. It spells trouble. That puts too much power into the hands of one party, and gives them the ability to screw us over all at once, whether intentionally or otherwise.

Thus, if people really wish to leave WhatsApp, I'm all for the idea. I just think most peoples' reasons for doing so aren't altogether sensible.

As of now, WhatsApp has announced a postponement of the planned update. I'm not sure how this is supposed to help; the damage is well and truly done. It's not like this will magically win back all the trust that wasn't there in the first place. Most people, if they actually look at the updated privacy policy, should not find much to be alarmed at. It's really WhatsApp's parent company, Facebook, that doesn't inspire confidence in users. And let's be honest here; who could blame them?

So long, Zuckers!
T___T

Monday 18 January 2021

Web Tutorial: Basic JavaScript Unit Testing Suite (Part 2/2)

In the previous part to this tutorial, we wrote two functions for testing. Those were fairly straightforward cases. Now, what if you had a different kind of function to test, one with a potentially unlimited number of parameters?

I present to you, getAverage(). It accepts one argument - an array of multiple values - and returns the mean of all these values.
function getCircleCircumference(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * 2;
}

function getAverage(arr)
{
    var total = 0;

    for (var i = 0; i < arr.length; i++)
    {
        total += arr[i];
    }

    return total / arr.length;
}


For this, we also need to cater for three cases. First, if there are no values in the array. Return null.
function getAverage(arr)
{
    if (arr.length == 0) return null;

    var total = 0;

    for (var i = 0; i < arr.length; i++)
    {
        total += arr[i];
    }

    return total / arr.length;
}


Second, if any of the values in the array are negative or not a number. Also return null.
function getAverage(arr)
{
    if (arr.length == 0) return null;
    if (arr.filter(function(x) { return x < 0 || isNaN(x); } ).length > 0) return null;

    var total = 0;

    for (var i = 0; i < arr.length; i++)
    {
        total += arr[i];
    }

    return total / arr.length;
}


And finally, if there's only one value in the array. Then simply return that value.
function getAverage(arr)
{
    if (arr.length == 0) return null;
    if (arr.filter(function(x) { return x < 0 || isNaN(x); } ).length > 0) return null;
    if (arr.length == 1) return arr[0];

    var total = 0;

    for (var i = 0; i < arr.length; i++)
    {
        total += arr[i];
    }

    return total / 0;
}


And then we add another test group to testGroups. Call it "getAverage" or whatever.
let testGroups =
[
    {
        "name": "getCircleArea",
        "tests":
        [
            {
                "title": "Circle Area should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return correct calculation.",
                "args": [5],
                "unit": getCircleArea,
                "expected": (3.142 * 5 * 5)
            },                        
        ]
    },
    {
        "name": "getCircleCircumference",
        "tests":
        [
            {
                "title": "Circle Circumference should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleCircumference,
                "expected": null
            },
            {
                "title": "Circle Circumference should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleCircumference,
                "expected": null
            },
            {
                "title": "Circle Circumference should return correct calculation.",
                "args": [5],
                "unit": getCircleCircumference,
                "expected": (3.142 * 5 * 2)
            }                        
        ]
    },
    {
        "name": "getAverage",
    }

];


And here, I've populated the tests array. Pay attention to the args and expected properties.
{
    "name": "getAverage",
    "tests":
    [
        {
            "title": "Average should return null if empty array.",
            "args": [[]],
            "unit": getAverage,
            "expected": null
        },
        {
            "title": "Average should return null if any values in array are negative.",
            "args": [[5, -5, 0]],
            "unit": getAverage,
            "expected": null
        },
        {
            "title": "Average should return null if any values in array are NaN.",
            "args": [[5, 15, "x"]],
            "unit": getAverage,
            "expected": null
        },
        {
            "title": "Average should return x if x is the ony value.",
            "args": [[100]],
            "unit": getAverage,
            "expected": 100
        },
        {
            "title": "Average should return average of all numbers in array.",
            "args": [[0, 2, 10, 4]],
            "unit": getAverage,
            "expected": 4
        }                        
    ]

}


Now refresh, and let's ruminate on just how easy it is to add tests now that we've done the hard bit.


We should really test for errors and exceptions, but let's do it on a sightly less... ugly interface? Create the CSS class, group. Give it nice round corners, a grey border, the works. Set font and shit. Have at it.
<style>
    .group
    {
        width: 90%;
        height: auto;
        border: 1px solid #CCCCCC;
        border-radius: 5px;
        font-family: arial;
        font-size: 14px;
    }

</style>


In the prepareDom() method, add this line to set each fieldset's class to group.
for (var i = 0; i < testGroups.length; i++)
{
    var groupFieldset = document.createElement("fieldset");
    groupFieldset.className = "group";

    var legend = document.createElement("legend");
    legend.innerHTML = testGroups[i].name;
    groupFieldset.appendChild(legend);

    for (var j = 0; j < testGroups[i].tests.length; j++)
    {
        var testDiv = document.createElement("div");
        testDiv.id = "group_" + i + "_test_" + j;
        groupFieldset.appendChild(testDiv);
    }
    
    body[0].appendChild(groupFieldset);
}    


What a dramatic difference a little bit of styling makes, eh?


Next up, we style the test output. Create the CSS classes test, success and failure.
<style>
    .group
    {
        width: 90%;
        height: auto;
        border: 1px solid #CCCCCC;
        border-radius: 5px;
        font-family: arial;
        font-size: 14px;
    }

    .test
    {

    }

    .success
    {

    }

    .failure
    {

    }

</style>


For test, I've elected for more rounded borders and a font color of white. For success, I use a bright green and red for failure. Choose your own colors!
<style>
    .group
    {
        width: 90%;
        height: auto;
        border: 1px solid #CCCCCC;
        border-radius: 5px;
        font-family: arial;
        font-size: 14px;
    }

    .test
    {
        width: 90%;
        height: auto;
        margin: 5px auto 0 auto;
        border-radius: 3px;    
        color: #FFFFFF;    
        padding: 1em;
        font-size: 0.8em;
    }


    .success
    {
        background-color: #00FF00;    
    }

    .failure
    {
        background-color: #FF0000;
    }

</style>


Back to the nested For loop in the prepareDom() method. Add this line so that each div with test output is now styled with test.
for (var i = 0; i < testGroups.length; i++)
{
    var groupFieldset = document.createElement("fieldset");
    groupFieldset.className = "group";

    var legend = document.createElement("legend");
    legend.innerHTML = testGroups[i].name;
    groupFieldset.appendChild(legend);

    for (var j = 0; j < testGroups[i].tests.length; j++)
    {
        var testDiv = document.createElement("div");
        testDiv.className = "test";
        testDiv.id = "group_" + i + "_test_" + j;
        groupFieldset.appendChild(testDiv);
    }

    body[0].appendChild(groupFieldset);
}


Now, remember the Try-catch block in the testUnits() method? Add this line to handle failure. It will style the div with failure in addition to test.
try
{
    result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
    exceptionThrown = true;
    document.getElementById("group_" + i + "_test_" + j).className = "test failure";
    str += err + "<br />";
}


You guessed it - we'll add the appropriate CSS classes in these cases too.
try
{
    result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
    exceptionThrown = true;
    document.getElementById("group_" + i + "_test_" + j).className = "test failure";
    str += err + "<br />";
}

if (!exceptionThrown)
{
    if (result == testGroups[i].tests[j].expected)
    {
        document.getElementById("group_" + i + "_test_" + j).className = "test success";
        str += "Test successful.";
    }
    else
    {
        document.getElementById("group_" + i + "_test_" + j).className = "test failure";
        str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
    }    
}


And the screen is awash with green!


Let's try to produce errors and exceptions, but instead of messing with the functions, let's do it by tampering with the tests. In the final test of the group named "getAverage", change the expected value.
{
    "title": "Average should return average of all numbers in array.",
    "args": [[0, 2, 10, 4]],
    "unit": getAverage,
    "expected": 0
}    


Here, the screen tells you what's what!


Change the value back to 4, and now in the args array, change it to a null instead of an array.
{
    "title": "Average should return average of all numbers in array.",
    "args": [null],
    "unit": getAverage,
    "expected": 4
}    


Here you see the exception message!


One last thing!

It's all well and good now, but if you have a lot of tests in a group, you generally want to know how many tests passed. And manually counting is just a little less lazy than a good programmer should aspire to be.

In the prepareDOM() method, add an id to the legend when it's created.
var legend = document.createElement("legend");
legend.id = "legend_" + i;
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);


In the testUnits() method, make the following changes. Within the nested For loop, declare passed and set it to 0. This means passed will be 0 at the start of every test group.
for (var i = 0; i < testGroups.length; i++)
{
    var passed = 0;

    for (var j = 0; j < testGroups[i].tests.length; j++)
    {


Every time a test is passed, increment passed.
if (!exceptionThrown)
{
    if (result == testGroups[i].tests[j].expected)
    {
        document.getElementById("group_" + i + "_test_" + j).className = "test success";
        str += "Test successful.";
        passed ++;
    }
    else
    {
        document.getElementById("group_" + i + "_test_" + j).className = "test failure";
        str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
    }    
}


And at the end of the outer loop, use passed to set the legend within the fieldset.
        if (!exceptionThrown)
        {
            if (result == testGroups[i].tests[j].expected)
            {
                document.getElementById("group_" + i + "_test_" + j).className = "test success";
                str += "Test successful.";
                passed ++;
            }
            else
            {
                document.getElementById("group_" + i + "_test_" + j).className = "test failure";
                str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
            }    
        }

        document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
    }

    document.getElementById("legend_" + i).innerHTML = testGroups[i].name + " (" + passed + "/" + testGroups[i].tests.length + ")";
}


Great work. We can now see how many tests passed in each group!



Some last words...

I can't stress this enough - if you want to do serious automated unit testing, this will not be adequate. The purpose of this exercise was really to illustrate a very basic testing process, and hopefully give you an appreciation of just how much automated testing accomplishes.

Wishing you all the very test!
T___T

Saturday 16 January 2021

Web Tutorial: Basic JavaScript Unit Testing Suite (Part 1/2)

A big part of software development is testing. In particular, automated testing. Unit testing. There are all sorts of frameworks for that now, but back then, I wrote my own.

Recently, I came across that piece of code I wrote back then to test simple JavaScript functions. I spruced it up, made it pretty, and today I submit that to you for your consideration.

Unit testing basically consists of a unit - usually a very small function - for which you determine both the input and the expected output. The program then does an assert - a comparison between the expected output and the actual output - to determine if the test passes or fails.

You see, vanilla JavaScript has no native assert function. So I made one. This little testing suite is painfully basic - you'd probably do better relying on one of the several JavaScript Unit testing frameworks out there. But it does serve to illustrate how things work.

The setup

Here's some boilerplate HTML.
<!DOCTYPE html>
<html>
    <head>
        <title>Unit testing</title>
        <style>

        </style>

        <script>

        </script>
    </head>

    <body>

    </body>
</html>


For this, we will be testing a function, getCircleArea(). It uses a parameter, r, which is the radius, and then a simple formula to calculate the area.
<script>
    function getCircleArea(r)
    {
        let pi = 3.142;

        return pi * r * r;
    }

</script>


But here's the thing about Unit Testing - you have to write code that is testable. That includes taking care of all the edge cases you can think of. What if r is negative? What if it's not a number?
<script>
    function getCircleArea(r)
    {
        if (isNaN(r)) return null;
        if (r < 0) return null;


        let pi = 3.142;

        return pi * r * r;
    }
</script>


That's the function... now let's define the test cases. For this, we use the object testGroups. It's an array of objects. We will have only one group, for now.
<script>
    let testGroups =
    [
        {

        }
    ];


    function getCircleArea(r)
    {
        if (isNaN(r)) return null;
        if (r < 0) return null;

        let pi = 3.142;

        return pi * r * r;
    }
</script>


Each group has a name. Call it anything you want, but in the interest of good organization, I would name it after the function we are testing.
let testGroups =
[
    {
        "name": "getCircleArea"
    }
];


For every group, there are multiple tests. So make tests an array of objects. Each object has these properties:

title - a description of the test. It's a description, so make it descriptive!
args - an array consisting of all the arguments we are passing into the function to be tested.
unit - the function to be tested.
expected - the value of the output.

let testGroups =
[
    {
        "name": "getCircleArea",
        "tests":
        [
            {

            },
            {

            },
            {

            }                        
        ]

    }
];


Below, I have filled in the values for you.
"tests":
[
    {
        "title": "Circle Area should return null if radius is negative.",
        "args": [-5],
        "unit": getCircleArea,
        "expected": null

    },
    {
        "title": "Circle Area should return null if radius is NaN.",
        "args": ['abc'],
        "unit": getCircleArea,
        "expected": null

    },
    {
        "title": "Circle Area should return correct calculation.",
        "args": [5],
        "unit": getCircleArea,
        "expected": (3.142 * 5 * 5)

    }                        
]


Now, create the object unitTesting. It has two methods - prepareDOM() and testUnits().
let testGroups =
[
    {
        "name": "getCircleArea",
        "tests":
        [
            {
                "title": "Circle Area should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return correct calculation.",
                "args": [5],
                "unit": getCircleArea,
                "expected": (3.142 * 5 * 5)
            }                        
        ]
    }
];

let unitTesting =
{
    prepareDOM: function()
    {
    
    },
    testUnits: function()
    {

    }
}


function getCircleArea(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * r;
}


You may as well, at this point, ensure that they fire off when the page is loaded.
<body onload="unitTesting.prepareDOM();unitTesting.testUnits();">

</body>


prepareDOM()
ensures that the DOM has placeholders for your script to populate later. We begin by grabbing the body using the getElementsByTagName() method.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");
},


Then, using a For loop, iterate through the testGroups array.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");

    for (var i = 0; i < testGroups.length; i++)
    {

    }
    
},


Create a fieldset for every element in the array, and append it to the body.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");

    for (var i = 0; i < testGroups.length; i++)
    {
        var groupFieldset = document.createElement("fieldset");

        body[0].appendChild(groupFieldset);

    }    
},


Create a legend tag, insert the name of the group, and append it to the fieldset before appending it to the body.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");

    for (var i = 0; i < testGroups.length; i++)
    {
        var groupFieldset = document.createElement("fieldset");

        var legend = document.createElement("legend");
        legend.innerHTML = testGroups[i].name;
        groupFieldset.appendChild(legend);


        body[0].appendChild(groupFieldset);
    }    
},


Yep, that's how it's done!


Now iterate through the tests array within each group.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");

    for (var i = 0; i < testGroups.length; i++)
    {
        var groupFieldset = document.createElement("fieldset");

        var legend = document.createElement("legend");
        legend.innerHTML = testGroups[i].name;
        groupFieldset.appendChild(legend);

        for (var j = 0; j < testGroups[i].tests.length; j++)
        {

        }

        
        body[0].appendChild(groupFieldset);
    }    
},


Create a div, give it a unique id, and append it to the fieldset. When you rerun the code, you won't see anything new because we haven't populated these divs.
prepareDOM: function()
{
    var body = document.getElementsByTagName("body");

    for (var i = 0; i < testGroups.length; i++)
    {
        var groupFieldset = document.createElement("fieldset");

        var legend = document.createElement("legend");
        legend.innerHTML = testGroups[i].name;
        groupFieldset.appendChild(legend);

        for (var j = 0; j < testGroups[i].tests.length; j++)
        {
            var testDiv = document.createElement("div");
            testDiv.id = "group_" + i + "_test_" + j;
            groupFieldset.appendChild(testDiv);

        }
        
        body[0].appendChild(groupFieldset);
    }    
},


That's for preparing the DOM; now let's actually test these units using the testUnits() method. Declare two variables - result and exceptionThrown. Because when you run functions, two things basically happen. You might get a result, which is either correct or incorrect. You could have an exception thrown, which is definitely unexpected behavior. We want to cater for all these cases.
testUnits: function()
{
    var result, exceptionThrown;
}


So, again, iterate through the testGroups array and within it, iterate through the tests array.
testUnits: function()
{
    var result, exceptionThrown;

    for (var i = 0; i < testGroups.length; i++)
    {
        for (var j = 0; j < testGroups[i].tests.length; j++)
        {

        }
    }

}


Declare str, and make it a string that tells us what function is being run, using the title property. Append the string to the div in question.
testUnits: function()
{
    var result, exceptionThrown;

    for (var i = 0; i < testGroups.length; i++)
    {
        for (var j = 0; j < testGroups[i].tests.length; j++)
        {
            var str = "Running " + testGroups[i].tests[j].title + "<br />";

            document.getElementById("group_" + i + "_test_" + j).innerHTML = str;

        }
    }
}


See what we're doing here? Good.

Now set exceptionThrown to false and result to undefined.
var str = "Running " + testGroups[i].tests[j].title + "<br />";

exceptionThrown = false;
result = undefined;


document.getElementById("group_" + i + "_test_" + j).innerHTML = str;


Then use a Try-catch. In the Try block, set result to the result returned when we pass in args to the function to be run. For this, we use the apply() method.
var str = "Running " + testGroups[i].tests[j].title + "<br />";

exceptionThrown = false;
result = undefined;

try
{
    result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{

}


document.getElementById("group_" + i + "_test_" + j).innerHTML = str;


In the Catch block, we cater for the case where an exception is thrown. If so, set exceptionThrown to true and make the exception message part of str.
var str = "Running " + testGroups[i].tests[j].title + "<br />";

exceptionThrown = false;
result = undefined;

try
{
    result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
    exceptionThrown = true;
    str += err + "<br />";

}
                            
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;


After that, check if exceptionThrown is true. If not, check result. If result is the same as the expected property, the test is a success.
try
{
    result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
    exceptionThrown = true;
    str += err + "<br />";
}

if (!exceptionThrown)
{
    if (result == testGroups[i].tests[j].expected)
    {
        str += "Test successful.";
        passed ++;
    }
    else
    {

    }    
}


document.getElementById("group_" + i + "_test_" + j).innerHTML = str;


If not, it's a failure and we want to tell the user why it's a failure by stating what the result was and what we expected instead.
if (!exceptionThrown)
{
    if (result == testGroups[i].tests[j].expected)
    {
        str += "Test successful.";
    }
    else
    {
        str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
    }    
}


Check this out!

Now let's deliberately make the function wrong. What happens?
function getCircleArea(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * r * r;
}


Yes, we get a fail case.

Undo that change, and now let's make this guy throw an exception.
function getCircleArea(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * i;
}


Great!

Undo that change as well, and now let's add a second function to be tested. It's the getCircleCircumference() function.
function getCircleArea(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * r;
}

function getCircleCircumference(r)
{
    if (isNaN(r)) return null;
    if (r < 0) return null;

    let pi = 3.142;

    return pi * r * 2;
}


Make a new test group.
let testGroups =
[
    {
        "name": "getCircleArea",
        "tests":
        [
            {
                "title": "Circle Area should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return correct calculation.",
                "args": [5],
                "unit": getCircleArea,
                "expected": (3.142 * 5 * 5)
            }                        
        ]
    },
    {
        "name": "getCircleCircumference"
    }

];


And a new series of tests.
let testGroups =
[
    {
        "name": "getCircleArea",
        "tests":
        [
            {
                "title": "Circle Area should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleArea,
                "expected": null
            },
            {
                "title": "Circle Area should return correct calculation.",
                "args": [5],
                "unit": getCircleArea,
                "expected": (3.142 * 5 * 5)
            }                    
        ]
    },
    {
        "name": "getCircleCircumference",
        "tests":
        [
            {
                "title": "Circle Circumference should return null if radius is negative.",
                "args": [-5],
                "unit": getCircleCircumference,
                "expected": null
            },
            {
                "title": "Circle Circumference should return null if radius is NaN.",
                "args": ['abc'],
                "unit": getCircleCircumference,
                "expected": null
            },
            {
                "title": "Circle Circumference should return correct calculation.",
                "args": [5],
                "unit": getCircleCircumference,
                "expected": (3.142 * 5 * 2)
            }                
        ]

    }
];


It's all coming together, isn't it?

This is not complete yet, but we're off to a decent start. It's really bare bones at the moment.

Next

Another function to test, and prettying things up.

Sunday 10 January 2021

Why some things should not be automated, redux

Previously, I wrote a piece on why there are some things we should not automate. Today's blogpost is not so much a revisitation of the concept, but rather an expansion upon that idea.

I obtained that insight through something my wife did. Recently, Mrs TeochewThunder had something to tell me. It was along the lines of how lucky she felt to have met me, and to have married a man like myself who had her back unconditionally and... well, you get the idea.

Awww how sweet, you say? This did not produce an "Awww" moment for me. Instead, my first reaction was alarm; to ask if she was feeling OK, or if awful something had happened. You see, my wife isn't given to overt displays of affection and/or gratitude. She's not cold, it's just not part of the culture. Ergo, when she suddenly gave me this spiel after months of not saying anything to this effect, naturally it gave me cause for concern. Like, was she depressed or something?

Turned out to be a false alarm. Apparently, amid the COVID-19 crisis and all, she had some time for introspection and somehow realized she had a lot to be thankful for.

But here's my point exactly. Stuff like this, ideally, should be done with greater frequency and lesser intensity. Holding it in before letting it out in some explosive burst is counter-productive. Sure, it looks good in trashy soap operas. It's impractical in real life.

Kind of like how regularly giving my mother money is better than giving her one lump sum at one go. Or regularly putting money aside is better than having a yearly quota that I try to meet at one glorious shot.

Because it forms good habits. Healthy habits.

Here's how we can expand on this concept. There are plenty of things where this is applicable. Where doing it in regular, reasonably-sized doses is healthier than saving it for later.

Eating and drinking

You know how some people starve themselves on some diet or other, then give themselves a cheat meal of some sort? Well, don't take it to extremes. Don't, for instance, gorge yourself at one sitting and then starve the next few days. Yes, I know some animals do that. Surprise, surprise - the human body isn't made for that.
You're not a python;
don't eat like one.

Eat regularly. In fact, don't wait till you're hungry. Eat when it's time to eat, and keep the portions reasonable. That way, your body has a regular supply of fuel instead of having to constantly adjust and anticipate the next intake. If I know there's a buffet lined up in the evening, I'm still going to eat at noon anyway. And if that means I somehow have less capacity later on, so be it.

The same case could be made for hydrating yourself. Don't wait till you're thirsty to drink. By the time your body feels thirsty, it's probably too late. That's exactly why I keep a source of water nearby while I work... though admittedly I could improve on remembering to drink from it. Ah well... baby steps, amirite?

Exercise

Some doctors recommend five days a week of cardio, and honestly it's a bit of a struggle to maintain. But it is way worse to lead a sedentary lifestyle for six days a week, then put in one intense session at the gym. You risk getting injured, and it doesn't magically erase whatever ills that were accumulated during six days of relative inactivity.
Run, baby, run.

I don't pretend to be some hunk. That said, I think I could definitely be in worse shape. I'm probably in better shape than most guys my age, or even ten years younger.

That's because I place my faith in regular and frequent doses of exercise, rather than torturing myself maybe once a week with a personal trainer. If that works for you, great, but I believe medical practitioners are with me on this one.

Breaks at work

French author Jules Renard once said something to this effect.

"Laziness is nothing more than the habit of resting before you get tired."


I don't think of it as laziness - rather, I think of it as pre-empting fatigue. Frequent breaks at work should be the norm.

Pushed a commit? Take a break.

Finished a test and the result was successful? Or even unsuccessful? Take a break.

Finished a decent-sized paragraph of documentation? Take a break. Hell, take two!

Take a break. Take several.

The thing here is that sitting your ass down to concentrate like a mofo for hours helps no one, least of all what you're trying to accomplish. Even in a normal situation, one makes mistakes. If you get tired, you make really silly mistakes. I've found that working late into the night produced errors I normally wouldn't have made otherwise. What's the solution to this? Avoid working late into the night. Duh.

This goes double for endeavors that require a great deal of thinking... such as writing code. Frequent breaks help. I'm not immune to the temptation of delaying the gratification of a well-earned rest by obsessively hammering away at code. But no, that rarely ends well. Grinding away all year round hoping for a well-earned vacation overseas is even more foolish... as the COVID-19 outbreak has ultimately proved.

The same could also be applied to sleeping in general. A habit of resting and rising early is better than starving yourself of sleep the entire week, and then sleeping in on weekends. To be really honest, I'm still working on this one.

Get on it!

The key word here is consistency. Don't wait until there's a need (money, food, water, rest, etc) before you actually make an effort to fulfil those needs. Pre-empt those needs. Because those needs will always be there. Be consistent. Your body and wallet will thank you.

Yours in automation,
T___T

Wednesday 6 January 2021

Uproars over user data privacy

This blogpost was meant as a commentary on what has transpired lately between tech giants Facebook and Google, but in light of recent events, has been somewhat expanded.

If you haven't heard about the latest spat between Apple and Facebook by now, you're probably living under a rock somewhere, or this stuff just doesn't interest you very much. Honestly, I wouldn't blame you either. These two have a love-hate relationship that goes way back when.

The most recent one started when Apple announced Privacy Policy changes with their new operating system, iOS 14. This would compel developers who wish to make iOS apps, to inform the app's users as to how their data is being tracked, and seek these users' explicit permission to do so. A year before that, Apple's CEO Tim Cook appeared to take a shot at Facebook in this column.
Consumers shouldn't have to tolerate another year of companies irresponsibly amassing huge user profiles, data breaches that seem out of control and the vanishing ability to control our own digital lives.

Shots fired.


Facebook responded by putting up full-page ads criticizing Apple's latest initiative, and claiming that those changes would hurt small businesses.

And then it was on.

The back-and-forth has only grown over the course of the past week, though from personal observation, public opinion seems decidedly tilted in Apple's favor. Still, the two sides of the argument are not without their respective merits and I beg your indulgence as I examine them here.

Apple's side

Apple's stated position is fairly straightforward, as it were - users should be informed as to how they are being tracked, and they should have a choice.

The slightly less straightforward part is that currently, users that are being tracked are invariably using some free service somewhere, and this is bad for Apple's subscription-based business model. So does this mean that Apple aren't genuinely concerned abut user privacy? I think a more useful question here would be - do users actually stand to gain if Apple gets their way?

Facebook's side

Facebook argues that being able to track user data, and thus, provide targeted ads, is a great source of revenue and helps keeps services and content free. Granted, giving users a choice invariably means that many of them aren't going to make choices that would please Facebook.

They are also arguing that small businesses (many of which apparently also depend on Facebook) would suffer if users were able to disallow tracking, though I really don't know anybody who actually buys that Facebook is really that concerned about small businesses. Even some of their own employees think that this is corporate bullshit.

My side

What do I think about all this? For starters, regardless of Apple's true intentions, I see no harm (to users anyway) in giving users a choice. Knowledge is power, after all. Apple isn't disallowing tracking on iOS - but ensuring that users know about it and can choose whether or not to let it happen. That's all right in my book.

On the other hand, I don't actually have a problem with allowing the likes of Facebook and Google to track me... and if I do have a problem, I can always choose not to use their services. Say what you like about Google and Facebook, but their services, even the free ones, are quality. They provide me many conveniences in life such as email, maps, site analytics and Social Media. But nothing in life is actually free, and certainly nothing stays free forever. I accept that if I want to continue using their products without paying a cent for them, I am going to have to give something back. And in this case, it means my data - browsing history, geographical locations, online purchases, and so on. Do I like it? No, I certainly don't. But I can live with that tradeoff.

Mark Zuckerberg, CEO of Facebook, has this motto: Move fast and break things. Well, it looks like Zuckerberg is going to have to do some of that fast-moving if Apple gets its way. Or perhaps the problem is precisely that Facebook moved too fast in capitalizing on targeted data as a main source of revenue, and now it's too late to course-correct. After all, it's been years and certain things are entrenched.

How much do you value your privacy, then? I'm sure plenty of people would choose not to be tracked, and that's fair. Would they be open to having to actually pay for the services they use? Because we can't have it both ways. We've all had it good up to now, but change is inevitable.

In other news...

Yesterday, Singapore had her own little furore when it was revealed during a Parliamentary Session that the TraceTogether tokens that I wrote about last June, could indeed be used to trace people. Apparently, the Criminal Procedure Code gives the Singapore Police Force blanket permission to do so. This was at odds with the statement Minister of Foreign Affairs Vivian Balakrishnan had made back then, that the data from TraceTogether would only be "purely for contact tracing. Period.".

Amid the cries of righteous fury from the unwashed masses, I'm willing to give Balakrishnan the benefit of the doubt when he says that he forgot about that provision. The man's a doctor by trade, not a lawyer. And there were more pressing concerns at the time... such as a global pandemic. Cynically, he could have, of course, decided to omit that little detail deliberately. And knowing what drama queens some of my fellow Singaporeans are, I find it hard to blame him.

But blame him, I shall.

Man, did it ever occur to him that this would come back to bite him in the ass later? No one of reasonably sound mind is arguing that terrorists and criminals should be exempt from having the data in their TraceTogether tokens tracked (though, if you are, feel free to take the next shuttle to Mars - I don't think you and I belong on the same planet), and yes, it's common sense that the police should have the power to use every means available to them to track down criminal activity. But why, in that case, make such an easily disprovable statement at all? So silly.
This can be used to track you.

However, my position regarding this apparent breach of data privacy remains similar to my stance towards Facebook and Google tracking my data; mainly, I don't give a shit. Sure, I value my privacy. I just happen to value my civic duty a little more.

And to all those lovely folks raising a ruckus over this, here's some food for thought.

Just because the police can use that data against you, it doesn't follow that they will. What possible reason would they have to do so? Your phones are connected to the internet. You probably drive a car that has a GPS installed. There's CCTV all over the island. And you're worried about this token? Are you for real?

This would be like if I had a loaded gun in one hand, and a Swiss Army knife in the other, both of which could be used to kill you... and you obsessed over that stupid knife.

Get a grip, children!
Not-so-privately,
T___T

Friday 1 January 2021

A phishy move by GoDaddy

Hosting provider GoDaddy made waves over the Christmas period a couple weeks ago, though sadly not for good reasons. What transpired was that they subjected their employees to a phishing test, with an email that promised a juicy Christmas bonus (650 USD, if I'm not mistaken) if they filled out an online form with personal details, and hit the Send button. Employees who fell for it, were later sent another email informing them that they had failed the test, and would have to report for security awareness training.



Talk about a dick move! GoDaddy came under fire - mostly from Twitter - for that stunt, and has since apologized.

Here are some thoughts on this debacle. I have so many questions.

For starters, no one's denying that the subject matter of their testing email was pretty insensitive, especially in light of Americans as a whole being concerned with job losses and all. But OK, Management being tone-deaf assholes? How is that news? Why's everyone acting like it's such a novelty? Though, Twitter being the melodramatic outrage machine that it has been over the past decade, it's no real surprise that people are blowing things up.

Secondly, is no one concerned about the fact that GoDaddy's employees can be that gullible? Clicking on a phishing email is one thing. They actually filled up that form and sent their details! Sweet Jesus, they didn't even stop to consider that if their own company had indeed sent that email, the company wouldn't need them to send their details because the company would already have those details? Show of hands - who's got GoDaddy as their hosting provider and is harboring some serious doubts now?

And lastly, why are GoDaddy's employees so desperate for money that they would do that? Doesn't the company pay them enough? On that evidence, clearly not! I mean, if someone offered me 650 USD in return for going through the trouble of filling up a form, I wouldn't think it was worth my time. GoDaddy's employees, on the other hand... it was so easy it was like shooting phish in a barrel. (hur hur)

What GoDaddy needs to do

Well, as trollish at it was, GoDaddy had the right idea as to how to perform a phishing test. What they got wrong was the frequency. Their employees failed because obviously they weren't desensitized to this enough. The remedy to that is to conduct these tests so often that their employees will automatically start ignoring anything from the company that promises them money...

...I kid, OK? Un-bunch your knickers, honey. That was a joke.

Actually what GoDaddy should have done in the first place was assume that everyone falls for phishing scams, and just skip the test. Sign their employees up for training right off the bat. This way, they would have avoided a whole lot of embarrassment, hurt feelings and Twitter outrage.

Now that's a fine kettle of phish!
T___T