Saturday, June 25, 2016

Everything You Know is Wrong!

The Background


Test your backwards IQ in this indie trivia game.  I developed this game in Unity3D with a friend of mine, Don.  We came up with the idea of having a trivia game where you would know all the answers but all of the logic is reversed.  The result is quite fun and is especially challenging in cases where what is being asked is negative.  For example: Don't click on the red circle. 

Filling out questions to tempt the user into a wrong answer was almost as fun as playing the game.  The sliding bar quickly disappears giving the user a sense of urgency and you will make mistakes playing this game.

The Game


What you know won't help you, because every answer you know is wrong.  Some times it's not as easy to answer as you think.



Check out the game and let me know you feedback.  Is it good, does the game annoy you?   Whatever.  Check it out and have some fun with it.

Thursday, April 14, 2016

Break Down Data Into a Tree of Categories - Easy Category Tree Breakdown

When you are looking at data it can be helpful to place it into categories based using what I call a category tree breakdown.  This can give us additional information that may not be apparent without looking at the breakdown.

In this simple example, say we have gathered a simple dataset on various political candidates, including: First Name, Last Name, Party Affiliation, Gender, State Affiliation, and Job.  Maybe we would want to see which genders were associated with which jobs or what the similarities are across party or state affiliations. 

Each of these could be done individually in SQL, but that would be inefficient and wouldn't expose the power of being able to perform a breakdown on the fly.

The Dataset in PHP

$rows = array(
    array('fname' => 'Ted', 'lname' => 'Cruz', 'party' => 'Republican', 'gender' => 'male', 'state' => 'Texas', 'job' => 'Senator'),
    array('fname' => 'John', 'lname' => 'Kasich', 'party' => 'Republican', 'gender' => 'male', 'state' => 'Ohio', 'job' => 'Governor'),
    array('fname' => 'Donald', 'lname' => 'Trump', 'party' => 'Republican', 'gender' => 'male', 'state' => 'New York', 'job' => 'Chairman'),
    array('fname' => 'Hillary', 'lname' => 'Clinton', 'party' => 'Democrat', 'gender' => 'female', 'state' => 'New York', 'job' => 'Secretary of State'),
    array('fname' => 'Bernie', 'lname' => 'Sanders', 'party' => 'Democrat', 'gender' => 'male', 'state' => 'Vermont', 'job' => 'Senator'),

);
All I need to do to perform a breakdown is to supply the function with the full dataset and specify which keys to look at to start building categories.  The order of the breakdown is important and determines the structure of the tree that is generated as a result.

For example, let's say I want to look at party affiliation.  Then for each party I want to know what are the positions held by those candidates.  Then for each of those jobs I want to see which states have an affiliation.
That can be done by specifying the breakdown array as follows:

$breakdown_arr = array('party', 'job', 'state');
show_breakdown($breakdown_arr, $rows);
I like to use named keys in my array because that makes it easier to read than integer indexes.
When this breakdown is applied to the dataset, the following result displays:

        Republican
            Senator
                Texas
            Governor
                Ohio
            Chairman
                New York
        Democrat
            Secretary of State
                New York
            Senator
                Vermont


Specify Any Type of Breakdown


The breakdown can be specified in any order we want.  Say we want to look again at the party affiliation, but this time by gender and first name:

$breakdown_arr = array('party', 'gender', 'fname');
show_breakdown($breakdown_arr, $rows);
This shows me a completely different take on the data.  We see here that while the Republican party is dominated by males, the Democratic party is split evenly by gender.

        Republican
            male
                Ted
                John
                Donald
        Democrat
            female
                Hillary
            male
                Bernie



The Algorithm For Performing the Breakdown


function get_array_breakdown($breakdown_indexes, $rows, &$data = null) {
    if (count($breakdown_indexes) < 1) {
        return array();
    }
    if (!$data) {
        $data = new datum();
    }
    $metric_index = array_shift($breakdown_indexes);
    $to_add = array();
    foreach ($rows as $row) {
        $val = $row[$metric_index];
        $to_add[$val][] = $row;
    }
    //Now loop through the children to continue the breakdown
    foreach ($to_add as $key => $to_add_val) {
        $datum = new datum();
        $datum->name = $key;
        $datum->data = $to_add_val;
        $data->addChild($datum);
        get_arrays_for_breakdown($breakdown_indexes, $to_add[$key], $datum);
    }
    return $data;
}

The way this code works is that it continually slices off a category index to look for in the dataset.  Then it loops through the passed rows and puts them into groups based on their values for the index.  These newly categorized values (which are stored in the $to_add array) need to be again gone through for the next breakdown (if there is one) and so are passed along to the next iteration so the next breakdown can be checked.  The remaining breakdown list, the unchecked values that were just added, and the $data object containing the result are all passed back into the method as a recursive call.

When there are no more rows to be added and subsequently checked, the recursive call stops being made and the resulting $data object is returned to the original calling function.

Unfortunately this call can be expensive when many rows and categories are being parsed.  If you have 'n' rows and 'm' columns to review so the big O(n) notation and where k is the number of distinct entries for each category is something like O(n) = n+k^m.

Try this algorithm to find interesting facts on your own datasets.

Complete Example For Performing Category Tree Breakdowns in PHP


<?php
//first name, last name, position
$rows = array(
    array('fname' => 'Ted', 'lname' => 'Cruz', 'party' => 'Republican', 'gender' => 'male', 'state' => 'Texas', 'job' => 'Senator'),
    array('fname' => 'John', 'lname' => 'Kasich', 'party' => 'Republican', 'gender' => 'male', 'state' => 'Ohio', 'job' => 'Governor'),
    array('fname' => 'Donald', 'lname' => 'Trump', 'party' => 'Republican', 'gender' => 'male', 'state' => 'New York', 'job' => 'Chairman'),
    array('fname' => 'Hillary', 'lname' => 'Clinton', 'party' => 'Democrat', 'gender' => 'female', 'state' => 'New York', 'job' => 'Secretary of State'),
    array('fname' => 'Bernie', 'lname' => 'Sanders', 'party' => 'Democrat', 'gender' => 'male', 'state' => 'Vermont', 'job' => 'Senator'),
);

$breakdown_indexes = array('party', 'job', 'state');
show_breakdown($breakdown_indexes, $rows);
/*
Output:
    For breakdown in form of: ["party","job","state"]
    Breakdown result is:

        Republican
            Senator
                Texas
            Governor
                Ohio
            Chairman
                New York
        Democrat
            Secretary of State
                New York
            Senator
                Vermont
*/
$breakdown_indexes = array('party', 'gender', 'fname');
show_breakdown($breakdown_indexes, $rows);
/*
Output:
    For breakdown in form of: ["party","gender","fname"]
    Breakdown result is:

        Republican
            male
                Ted
                John
                Donald
        Democrat
            female
                Hillary
            male
                Bernie
*/

function show_breakdown($breakdown_indexes, $rows) {
    $new_data = get_array_breakdown($breakdown_indexes, $rows);
    echo 'For breakdown in form of: '. json_encode($breakdown_indexes).PHP_EOL;
    echo 'Breakdown result is: '.PHP_EOL;
    echo $new_data;
    echo PHP_EOL;

    return $new_data;
}

function get_array_breakdown($breakdown_indexes, $rows, &$data = null) {
    if (count($breakdown_indexes) < 1) {
        return array();
    }
    if (!$data) {
        $data = new datum();
    }
    $metric_index = array_shift($breakdown_indexes);
    $to_add = array();
    foreach ($rows as $row) {
        $val = $row[$metric_index];
        $to_add[$val][] = $row;
    }
    //Now loop through the children to continue the breakdown
    foreach ($to_add as $key => $to_add_val) {
        $datum = new datum();
        $datum->name = $key;
        $datum->data = $to_add_val;
        $data->addChild($datum);
        get_arrays_for_breakdown($breakdown_indexes, $to_add[$key], $datum);
    }

    return $data;
}

class datum {
    public $name = '';
    public $data;
    public $children = array();

    private $depth = 0;

    public function addChild($child) {
        $child->depth = $this->depth + 1;
        $this->children[] = $child;
    }

    public function __toString() {
        $str = $this->toStringHelper();
        foreach ($this->children as $child) {
            $str .= $child;
        }

        return $str;
    }

    private function toStringHelper() {
        $str = str_pad(' ', $this->depth * 4);
        $str .= $this->name;
        $str .= PHP_EOL;

        return $str;
    }
}

Tuesday, April 12, 2016

Working with D3 - Having an AJAX Loader Image

While D3 makes it easy to get started adding interesting visualizations to your web application and there are a lot of great examples on https://github.com/mbostock/d3/wiki/Gallery.  The problem is that almost none of these examples are ready for prime-time without some serious tweaking.  Either they need to be converted from using static data to relying on AJAX calls, or more importantly they do not support updates.

One suggestion is to use the excellent http://nvd3.org/ library by individuals who have taken the time to address these issues and provide a product that is production ready.

Another change you may need is to provide some kind of indication to the user while data is being loaded into the visualization.

Preparing for adding a loader - CSS code


First you will need to prepare an image to be used as your loading image.  One with a transparent background would likely work best.

In your style.css or whatever you are using for your style-sheet, place the following code that specifies the positioning of the loading image and opacity D3 svg container.  Also, you'll probably want to specify a minimum size of the container that will hold the D3 chart so the loading image knows where to position itself.

    div.ajax_loading {
        position: relative;
    }

    /* Grab only the direct descendant of the div that is an image */
    div.ajax_loading > img {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
        margin: auto;
    }

    /* Mark all children svg objects as being 50% transparent */
    div.ajax_loading svg {
        opacity: 0.5;
    }

    /* It is best to size your chart so the loader always knows where to display */
    /* An id specifier '#' was used here */
    #performance_chart {
        min-width: 400px;
        min-height: 300px;
    }


Add the Loading Image to the HTML code


Now you will need to add the 'ajax_loading' class to the HTML DIV that will contain the D3 visualization.  Within that DIV you will add the image.

    <div id="performance_chart" class="ajax_loading">
        <img src="img/ajax-loader.gif"/>
    </div>

This will display the loading image by default and also show a nice dimming effect on any svg objects contained in the target DIV.

The image I used has an opaque background, so it only looks right if you are dimming the DIV as we are doing here.  Otherwise you may want to use one with a transparent background.

Loading Image


Add Javascript Code to Control the Loading Image


You'll need to activate and deactivate the loading image from whatever code you are using to call and load the data.  In my case, before every AJAX call I display the loader and once the data is received I hide the loader using the following code.

    function setLoadingIndicator(id, status) {
        d3.select("#" + id).classed("ajax_loading", status);
        d3.select("#" + id + " img").classed("hidden", !status);
    }

Example program:

    //Show loading indicator
    setLoadingIndicator("chart_id", true);
    //Make the call to get data
    $.ajax({
        ...
        success: function(data) {
            //Draw the D3 chart
            draw_chart("chart_id", data);
            //Hide loading indicator
            setLoadingIndicator("chart_id", false);
        },
        error: function(result) {
            setLoadingIndicator("chart_id", false);
        }
    }); 


And that's all there is to it.

Sunday, January 17, 2016

Ionic Angular App as a Website Front-end - AJAX/CORS solutions

Ionic and Angular are great frameworks for building single page web apps.  If you want to create an app that you can then deploy on almost any mobile device out there based on HTML5 and Javascript (JS), then you are starting in a good place.

If you are just building an app that does not communicate to other websites and deals only with information stored locally or already on the app, then you will have no issues.  What the guides do not explain well is all the issues you will have communicating with other websites.  These are JS features (or limitations) that protect users from malicious js code.  Native Android (dalvik) and iOS (Objective-C/Swift) apps do not have that limitations, so native developers may be unfamiliar with these restrictions.

This article will help you identify and solve some common pitfalls when building an ionic app.  The source code to the sample ionic app is available on github.

Typical architecture for an Ionic App as a web site front-end.

 

Make sure your server allows remote access by Javascript:


If you are communicating to another server, you will need to set it up to allow for Cross-Origin Resouce Shared (CORS) access or JS will not be able to read the responses.  If the server you want to talk to is not your own and does not have Access-Control-Allow-Origin in its response headers set to where you need it (generally '*' or 'file://'), then you'll need to find a different way such as a proxy or a native app.
In Apache's httpd.conf file, you will need something like this on the directory you want to allow access to:
<Directory "/your_www_dir">
  #You may prefer "file://" instead of "*" because it is more restrictive
  Header set Access-Control-Allow-Origin "*"
  ...
Or this can be done in your host language.  For PHP:
//When testing app locally, it will be run from localhost:8100
$client_origin = "";
if (array_key_exists('HTTP_ORIGIN', $_SERVER)) {
    $client_origin = $_SERVER['HTTP_ORIGIN'];
}
if (stristr($client_origin, 'localhost') !== false) {
    $origin = "http://localhost:8100"; //test host
} else {
    $origin = "file://";
}
//Allow only from two specific locations: testing, and from JS app
header("Access-Control-Allow-Origin: $origin");
//To be less restrictive, you can use this line below
//header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");

Make sure your Ionic project has whitelists enabled and configured:


Newer versions of Ionic projects seem to have this enabled by default, but if you do not have the whitelist plugin already, you will need to install it.

Check if the whitelist plugin is installed:
$ ionic plugin list
cordova-plugin-console 1.0.2 "Console"

...
cordova-plugin-whitelist 1.2.0 "Whitelist"

If not, install it:
$ ionic plugin add https://github.com/apache/cordova-plugin-whitelist.git
Configure the app's config.xml file to allow access to outside sites (i.e. beyond 'file://'), add after <content src="index.html"/>.  Add the last two lines to allow Android/iOS apps to open sites other than you own (if desired):
<access origin="*"/>
<allow-intent href="*"/>
<allow-navigation href="*"/>

For iOS apps, check your App Transport Security (ATS) settings:


When you do an app build, a platforms/ios/{app_name}/{app_name}-Info.plist file is created.  If your app ONLY accesses HTTPS, you won't need to set this.  Otherwise add the bolded lines to the file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$
<plist version="1.0">
  <dict>
    ...
    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSAllowsArbitraryLoads</key>
      <true/>
    </dict>

  </dict>
</plist>
If this is not set, you may have communication issues and see the following error:
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

Load your code or try the sample project:


The sample project uses the Angular $http service to communicate to a sample website.  I use http://espn.go.com as an example because they send an Access-Control-Allow-Origin = '*' and allows us to test our app.  You can see the response in the Message section and any errors in the Error section.


iOS app publishing tips


Getting an app running in the Apple App Store has its own set of challenges.  Check out these Ionic iOS App Publishing tips: Publishing an Ionic Angular App for iOS - The Hidden Steps & Pitfalls.


Something you should never ever do - have Allow Origin set to * and Allow Credentials to true



According to the spec you can't have Allow Origin = "*" and Allow Credentials set to "true".  There is a really good reason for this.  It prevents malicious Javascript from using your credentials saved in cookies against you.  Say you logged into a website and it knows who you are through a cookie.  If in another window you ran some malicious JS code and both of these settings were enabled, the JS code could look like a logged in user to the remote server.  Not so bad if it is a sports website, but horrendous if it is your banking site.  With all that said there is a way to do this in Apache.  Just echo the given origin back to the user.  The browser never sees it as Allow Origin=* so it allows the request.

This code in your httpd.conf completely exposes your users to malicious JS code:

   <Directory "/somedir">
       SetEnvIf Origin "^(.*)$" ORIGIN_SUB_DOMAIN=$1
       Header set Access-Control-Allow-Origin "%{ORIGIN_SUB_DOMAIN}e" env=ORIGIN_SUB_DOMAIN
       Header set Access-Control-Allow-Credentials "true"





Publishing an Ionic Angular App for iOS - The Hidden Steps & Pitfalls



The manual for Ionic makes it seem so easy to get started - and it generally is... that is until you go to publish your app to the Apple store and run on real devices.  It can be hard getting started if you don't know you need any of these crucial elements.  Later versions of Ionic already include some of these steps making future builds easier.  Here is a list of some solutions to pitfalls I've run across publishing Ionic apps to the Apple App Store.


Make sure you have your provisioning license




In using iOS, you'll need to create an AppId and with it an associated provisioning license at developer.apple.com.  This needs to match the widget id in the Ionic config.xml.

Create and Set the widget id for a provisioning license developer.apple.com, then update config.xml:
<widget id="com.company_name.app_name" version="x.x.x" ...

Make sure the system can find your provisioning license


In my system it initially was not able to find the license.  So, I needed to perform this step in order to properly deploy
$ sudo cp -r ~/Library/MobileDevice/ /Library/
If this is not set correctly, you can get the following error when deploying your app to a device:
Check dependencies
Code Sign error: No provisioning profiles found: No non–expired provisioning profiles were found.
** BUILD FAILED **

When accessing non-HTTPS endpoints, check your App Transport Security


When you perform ionic build ios, a platforms/ios/{app_name}/{app_name}-Info.plist file is created.  If your app ONLY accesses HTTPS, you won't need to set this.  Otherwise add the bolded lines to the file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$
<plist version="1.0">
  <dict>
    ...
    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSAllowsArbitraryLoads</key>
      <true/>
    </dict>

  </dict>
</plist>
If this is not set, you may have communication issues and see the following error:
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

Building your app in Xcode


In order to send your app to the Apple App Store, you will need to load your app in Xcode.  When you perform ionic build ios it creates a file in your platforms/ios/{app_name}.xcodeproj file that you open in Xcode.

In my version of the project I needed to make some modifications to the project in order to build.  Under Build Settings / Search Paths / Header Search Paths, I added:
$(OBJROOT)/UninstalledProducts/$(PLATFORM_NAME)/include
To both Debug & Release for Any SDK.


Select the correct orientation and display settings


Under General / Deployment Info, Select Add device Orientations.  Do not select a Main Interface - this should be left blank.  Make sure you have selected all desired orientations and 'Requires full screen'.  It will put these keys in the plist file, although that does not seem to be the only location they are required because setting these without doing so in Xcode does not set the values properly.
    <key>UIRequiresFullScreen</key>
    <true/>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>

Error if Main Interface .xib is selected:
2015-10-28 08:29:57.260 AppName[89398:1651075] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIApplication 0x787193c0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key view.'

Setup the custom icons for the app


Set your icon & splash screen in the resources folder.  Use a minimum of 192x192 for the icon and 2208x2208 for the splash screen (it will be cropped for portrait/landscape).  Run ionic resources to automatically compile the icons into your project so it will appear on the device:
$ ionic resources
This section came from help from: http://blog.ionic.io/automating-icons-and-splash-screens/

Export the app from Xcode and upload the app to the Apple App Store


Initially the Project / Archive will not be available.  Next to triangle play button and the square stop button, the current device your can deploy to is selected.  Click the area to change this from iPhone to Generic iOS device.  It does not look like an input combo box, but you can click on it to change it.

Click here to change the deploy target and activate the Archive selection under the Product menu


You'll need to export the project to an archive to publish to iTunes connect and then run have processed through the app store.  Open Xcode.  Select Project / Archive from the Xcode menu.


Dealing with Cross-Origin Resource Sharing


Getting communications working to remote sites can have many potential issues to be worked out.
Check for my full post on settings needed for making CORS requests: Ionic Angular App as a Website Front-end - AJAX/CORS solutions.




Thursday, January 15, 2015

Resolved - Apache Alias or Symlinks Not Working with Unexpected 403 Forbidden Error

Apache Forbidden Access Issues

Linking files to Apache on a new install of Cent OS 7 with Apache 2.4 was not quite as smooth as I imagined.

The Setup


I'm working a new server and I want to link to another location so I can use Dropbox to work on files locally and have them automatically updated to my development system.  Since it is easiest to install in the root directory, the Dropbox files automatically get setup in the root user's home directory.  That is not a good place to link the document root to in Apache.  The most logical thing to do was to copy those files out to another better location where they can be served by Apache.  These files were still owned by root and I didn't want to change that, so I creating a symlink to get it to work.

As root (note: my document root was /var/html)
cd /var/html
ln -s /usr/demo/html/ demo

When trying to run in Apache, I would still get the Forbidden error message.





NOTE: When Apache follows symlinks, the path must be accessible all the way down by the calling user (this means you need execute access in the folder you are linking and the parent folders above it).  To make sure this folder is accessible by others, I would use the following command:
chmod o+x /usr /usr/demo /usr/demo/html 

That didn't work for me, but it should work.  I just didn't realize the underlying problem I was experiencing which I will get to in a minute.  So now, I'm thinking I'll try to use an alias and edited and saved the new config file.  

Opening Apache config, I edited it as follows:

sudo nano /etc/httpd/conf/httpd.conf

Alias /demo /usr/demo/html

<Directory "/usr/demo/html">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    allow from all

</Directory>

All the online literature was pointing me in this direction.
Since I am using the new CentOS7 I need to restart the service using the system control program.
sudo systemctl restart httpd.service
#but, on most servers this is: 
#sudo /etc/init.d/httpd restart

The server restarted properly but I am still not able to access the page and still the Forbidden error pops up in my browser.  Looking in the /var/log/httpd/error_log was somewhat helpful:

[Thu Jan 15 14:37:07.549412 2015] [authz_core:error] [pid 30582] [client x.x.x.x:yyyy] AH01630: client denied by server configuration: /usr/demo/html/test.php


This was telling me that I didn't have a linux permission error accessing the file, but that I had an Apache configuration file error.  Back into the httpd.conf file.


The Solution


After a little digging, I found that Apache 2.4 (that I had on the new server) handles permissions differently that the previous version 2.2 that I was used to using.

Finally modifying my httpd.conf file resulted in:

Alias /demo /usr/demo/html

<Directory "/usr/demo/html">
    Options FollowSymLinks
    AllowOverride None
    Require all granted

</Directory>

Now everything works.  I just hadn't been aware that setting aliases in new Apache 2.4 installtion requires a couple changes in the httpd.conf file to get things working properly.  All this time it was just I was using: 
Order allow,deny
allow from all

when I should have been using:
Require all granted

Hopefully this helps someone else save some time.

Sunday, January 4, 2015

Don't Look Bad - Test Your Website

Don't be caught looking like a fool when your website fails unexpectedly.  Find out errors ahead of time using automatic testing techniques.  In this article we discuss automatic regression testing.  Use PHP to setup your own continuous integration server.

When you are responsible for a updating a website that is in active use,  you want to feel confident that the changes you make to the code do not break any currently working pages.  Nothing is worse that making a small change to fix one problem and find out later through a customer that the unintended consequence causes another page to stop working.  Prevent yourself from experiencing that embarrassment and setup automated webpage checking.  It's not too difficult and you will thank yourself later when you identify errors before pushing changes made on your development server to the production server.

Types of Testing

  • Unit Testing - Using a tool like PHPUnit, tests the individual components of your programs and be run each time code changes are made.  
  • Regression Testing - The subject of this article are tests help verify that the webpage works as a whole.  It is designed to supplement unit testing and provide early notification when any page or web service breaks.  Scheduling this test to run repeatedly is the next best thing to having your own continuous integration service, but is much easier to setup.
  • Integration/UI Testing - Using a UI simulator like Selenium tests the end-user's experience on the website.  While useful, this is time consuming to setup and is easily defeated when site layout is changed.  This testing is beyond the scope of this article. 

 

First Things First, the Overall Process


  1. Separate development servers from production.  It is recommended to have a separate development server and production server.  Development servers can be low power and low memory machines which are not only inexpensive to maintain, but have the added benefit of exposing any performance issues early.  
  2. Perform unit testing on the development server each time you make code changes.
  3. Perform automatic regression tests from a different computer to the one you are checking.  If the computer you are testing goes down, you want to be notified.  If your testing is done on the same computer, then if that computer fails, you run the risk of not being notified when there is a serious problem.  Run an automated task on the production server that checks the development server.  
  4. Test in both directions.  Run an automated task on the development server that checks the production server.  
  5. Confidently push code changes to the production server after testing successfully passes.

 

Writing a Regression Test - Get what you expect, not what you don't 


Get the first page you want to check and identify three things, the URL of the page, the content that is expected to be on the page (ie. page title), and content that should never be on the page (error messages, etc).  The test will verify that content you expect to be there is present and that content you don't want is not present.  This could be any page you want and I am using checkliststogo.com's homepage (a site I develop and maintain).  The error messages I use are geared towards a Linux, PHP, and Apache server, but the same basics apply to other systems.  Make sure error reporting is on in your programming language (for PHP this can be done by setting error_reporting = E_ALL in /etc/php.ini).
URL - "www.checkliststogo.com"
Expected content - "Checklists ToGo", "Popular Checklists", " WorxForUs &copy;"
Error content -"Parse error: syntax error", "{local filesystem root path to your site}", in my case: "/home/ec2-user/www/htdocs/"

Using the local filesystem page path in the check for error content combined with the web server language displaying errors is an extremely powerful and easy way to check for site errors.  Syntax errors, database errors, run-time errors, and all kinds of problems are all easily detected since errors report a filesystem trace including the root path when errors occur.  NOTE: The relative path below the filesystem root should not be used in the error detection since those strings will be found in page links.

Running the Regression Test - Main Code


Now that we know the site and what strings to check for we can build the program to run the actual test in a file called sample_index_test.php.

<?php
    include_once("validate_site_helper.php");

    $url = "http://www.checkliststogo.com";
    //These strings must not be in the page content to pass testing
    $err_arr = array();
    $err_arr[] = "Parse error: syntax error";
    $err_arr[] = "/home/ec2-user/www/htdocs/"; //This is probably the best detector in this group

    //These strings are required to be in the page content to pass testing
    $pass_arr = array();
    $pass_arr[] = "Checklists ToGo"; //check page title is on page
    $pass_arr[] = "Popular Checklists"; //check sample header
    $pass_arr[] = "WorxForUs &copy;"; //check copyright

    //Check the site
    $result = validate_site_helper::check_site($url, $err_arr, $pass_arr, basename(__FILE__));

    //Report the test result
    if (!$result->success) {
        $message = "Page {$url} testing failed - {$result->error}";
        handle_error_notification($message);
    } else {
        //(optional) let developer know the site was ok
        echo ("Site {$url} is ok");
    }

    //This is your custom module to send the display to the administrator or developer
    function handle_error_notification($message) {
        //Notify admin of failure - email, print to screen, etc.
        //Please see other blog entries on sending emails which are a great notifier
        echo ("ERROR: {$message}");
    }
?>


The validate site helper encapsulates all this checking and returns a result object that lets you know how the testing went.

The error notification is going to be different for each system and is beyond the scope of this article.  In my case, I like to use Amazon Simple Email Service (tutorial here) to send emails to myself when errors are detected and find that works very well.  


Validate Site Helper - Code


The validate_site_helper does all the hard work of getting the URL page content, parsing the text for the expected and error strings and then returns the result.

<?php

class validation_result {
        public $success = true;
        public $error = ""; //for passing errors
        public $subject = ""; //for providing a quick summary to email
}

/**
 *  validate_site_helper - this is a tool to capture and parse a specific web site page
 * @author sbossen
 */
class validate_site_helper {

        protected static function check_site_helper($site_content, $host_url, $err_indicators_arr, $pass_indicators_arr, $calling_file) {
                $result = new validation_result();
                //using try here so any parse errors will be caught by this script
                try {
                        $ctg_content = $site_content;
                        //check for the errors
                        foreach ($err_indicators_arr as $err_str) {
                                if (stristr($ctg_content, $err_str)) {
                                        $result->success = false;
                                        $result->error .= "Suspected error indication: '$err_str' found in generated page content.\r\n";
                                }
                        }
                        //check for the required items
                        foreach ($pass_indicators_arr as $pass_str) {
                                if (!stristr($ctg_content, $pass_str)) {
                                        $result->success = false;
                                        $result->error .= "Validation indication: '$pass_str' was not found in generated page content.\r\n";
                                }
                        }

                        if (!$result->success) {
                                $result->subject = "$host_url - Warning - $calling_file";
                        }
                } catch (Exception $e) {
                        $result->success = false;
                        //email user
                        $body = $e->getMessage()."\r\n".$e->getTraceAsString();
                        $result->error .= $body;
                        $result->subject = "$host_url - Execution Error - $calling_file";
                }
                return $result;
        }

        public static function check_site($host_url, $err_indicators_arr, $pass_indicators_arr, $calling_file) {
                $result = new validation_result();
                //using try here so any network errors will be caught by this script
                try {
                        $ctg_content = file_get_contents($host_url);
                        $result = validate_site_helper::check_site_helper($ctg_content, $host_url, $err_indicators_arr, $pass_indicators_arr, $calling_file);
                } catch (Exception $e) {
                        $result->success = false;
                        //email user
                        $body = $e->getMessage()."\r\n".$e->getTraceAsString();
                        $result->error .= $body;
                        $result->subject = "$host_url - Execution Error - $calling_file";
                }
                return $result;
        }

        public static function check_site_with_post($host_url, $post_params_array, $err_indicators_arr, $pass_indicators_arr, $calling_file) {
                $result = new validation_result();
                //using try here so any errors will be caught by this script and emailed
                try {
                        // use key 'http' even if you send the request to https://...
                        $options = array(
                                'http' => array(
                                        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
                                        'method'  => 'POST',
                                        'content' => http_build_query($post_params_array),
                                ),
                        );
                        $context  = stream_context_create($options);
                        $ctg_content = file_get_contents($host_url, false, $context);

                        $result = validate_site_helper::check_site_helper($ctg_content, $host_url, $err_indicators_arr, $pass_indicators_arr, $calling_file);
                } catch (Exception $e) {
                        $result->success = false;
                        //email user
                        $body = $e->getMessage()."\r\n".$e->getTraceAsString();
                        $result->error .= $body;
                        $result->subject = "$host_url - Execution Error - $calling_file";
                }
                return $result;
        }

}

?>

Validation Results

 

This works by using PHP's built in file_get_contents function which grabs the contents of the URL from the server.  That is handled beneath a try function that captures errors such as the page not being found and allows the script to continue and report the error back to the user.  Otherwise, if the page could not be retrieved the notification code would not execute which would be a big problem.


The returned validation_result object is just a holder to pass along the results of the validation.  When you get the results, you'll want to pass them on somewhere to let the developer know that an error has occurred.  In the sample code here we are just outputting to the screen.  

This code was tested under multiple failure scenarios, including:
  • Server is offline (IP address could not be resolved)
  • Page is not authorized
  • Page does not exist
  • Page is blank

    ERROR: Page http://
    www.checkliststogo.com/ctg/app testing failed - Validation indication: 'Checklists ToGo' was not found in generated page content. Validation indication: 'Popular Checklists' was not found in generated page content. Validation indication: 'WorxForUs ©' was not found in generated page content.
  • Page is OK

    Site http://www.checkliststogo.com/ctg/app is ok

Automating the Testing


When the validation code is ready, you'll want to continually run it.  An easy way to do this in Linux is using the cron tool or Task Scheduler for Windows.

For me, I just run this test every hour on the 14th minute:
    sudo crontab -e
    14 * * * * php {path to file}/sample_index_test.php
To finalize the change and write the updated task to the system
    :w

Of course, you will need to have added the email notification (or other system) since cron will only output to the console and you will not see it directly.

If you find this code useful, please let me know in the comments, give a +1, or send a smiley cat picture.