How the TMC server works

A fairly simple templating system in node

  //7 Feb 2010
    //Includes handler for live test stats GET request with JSON string param
    
//29 Aug 2010 - it is important that you go to 127.0.0.1:8080, not localhost:8080
    //F8 will run
    
//20 Sep 2010
    //uses .content files for HTML and .json files for element content and attributes
    //will check all related files for timeliness before reconstructing any HTML files
    
//24 Sep 2010
    //just uses .json files to build HTML if the server-cached page is not current
    
//27 Sep 2010
    //In Geany, F5 runs, F8 inspects and F9 lints
    
//10 Nov 2010
    //Handles KL as well as TMC - not happy with node-html-proxy
    
//1 Jan 2011
    //added deliverMail capability from the old KL node.js
    //not that the flow logic is at the end of the httpServer function to keep JSlint happy
    
var sys = require('sys'),
        fs = require('fs'),
        url = require('url'),
        querystring = require('querystring'),
        http = require('http'),
        path = require('path'),
        //elf = require("../modules/node-elf-logger/lib/elf-logger"),
        TMCroot,// = __dirname + '/',//the current directory
        dom = require("jsdom/jsdom/level1/core").dom.level1.core,
        window = require("jsdom/jsdom/browser").windowAugmentation(dom).window,
        document = window.document,
        lib = require('email'),
        Email = lib.Email;
        KL = false;
    
var requested = (function () {
        //this creates an initial object for thisRequest to augment with a URL
        var _mimeTypes = {//more at http://www.iana.org/assignments/media-types/
           '.html': {contentType: 'text/html'}, 
           '.txt': {contentType: 'text/plain'}, 
           '.js': {contentType: 'text/javascript'}, 
           '.json': {contentType: 'text/javascript'}, 
           '.JSN': {contentType: 'text/javascript'}, 
           '.css': {contentType: 'text/css'}, 
           '.png': {contentType: 'image/png'}, 
           '.jpeg': {contentType: 'image/jpeg'}, 
           '.jpg': {contentType: 'image/jpeg'}, 
           '.gif': {contentType: 'image/gif'}, 
           '.ico': {contentType: 'image/x-icons'},
           '.xls': {contentType: 'application/vnd.ms-excel'}
        };
        return function (request) {
            if (/katylou/.test(request.headers.host)) {//.match(/katylou/)) {
                TMCroot = "/media/sdcard4/node/KL100618/KL/";
                KL = true;
            } else {
                TMCroot = __dirname + '/';
                KL = false;
            }    
            var requestFile = url.parse(request.url).pathname.replace(/%20/g, " "),
                fileExtension = path.extname(requestFile),
                thisRequest;
            if ((thisRequest = _mimeTypes[fileExtension])) {
                thisRequest.url = (TMCroot + request.url).replace(/\/\//g, '\/');
            } else {//serve index page for unrecognised mime types
                thisRequest = {contentType: "text/html", url: TMCroot + "index.html"};
            }
            return thisRequest;
        };
    }());
    
httpServer = http.createServer(function (request, response) {
        //console.log(sys.inspect(request));
        //function serve404() {
            ////serve a 404 page with a red banner
            //var _404page = "<div id='pageContent'><p>Nothing like that here!</p>" +
                //"<p id='footer'><a href='/'>Return to Testing Misconfused</a></p>" +
                //"<script>document.getElementsByTagName('H1')[0].style.backgroundColor='#b00b00';</script>" +
                //"</div></body></html>";
            //fs.readFile(TMCroot + "TMCheader.html", function (err, data) {
                //document.innerHTML = data;
                //document.body.appendChild(document.createElement("DIV"));
                //document.body.lastChild.id = "pageContent";
                //document.body.lastChild.innerHTML = _404page;
                //response.writeHead(404, { 'Content-Type': "text/html", "Expires": "now" });
                //response.end(document.outerHTML);
            //});
        //}
        function serve404() {
            //serve a 404 page with a red banner
            var _404page = "That page does not exist - please go back to the home page";
            response.writeHead(404, { 'Content-Type': "text/html", "Expires": "now" });
            response.end(_404page);
        }
        function serveContent(thisURL) {
            //if the .html page is current, serve it, 
            //otherwise construct from the header and .content
            var statFiles = [thisURL, TMCroot + "TMCheader.html", thisURL.replace(".html", ".json")],
                mTimes = [], statFile, content;
            //also need to check for any CODE files referenced in the .json
            //need to update the cached page if these have changed
            fs.readFile(statFiles[2], function (err, data) {
                if (!err) {
                    content = JSON.parse(data.toString());
                    for (var key in content.edits) {
                        if (key.split(/(_|\.)/)[0] === "CODE") {
                            statFiles.push(content.edits[key]);
                        }
                    }
                    getMtime(0);
                } else {
                    serve404();
                }
            });
            function getMtime(i) {
                if ((statFile = statFiles[i])) {
                    fs.stat(statFile, function (err, stats) {
                        mTimes[i++] = err ? 0 : stats.mtime;
                        getMtime(i);
                    });
                } else {
                    if (mTimes[2] === 0) {
                        serve404(); //no content for this .html 
                    } else {
                        if ((mTimes[0] === 0) || (mTimes[0] < Math.max.apply(Math, mTimes))) {
                            try {
                                constructPage(thisURL); //.html is not current, so build it
                            } catch (ex) {
                                serve404();
                            }
                        } else {
                            currentHTML(thisURL); //.html is current, so serve it
                        }
                    }
                }
            }
        }
        function currentHTML(thisURL) {//serves a .html file with immediate expiry
            fs.readFile(thisURL, function (err, data) {
                if (err) {
                    serve404();
                } else {
                    response.writeHead(200, { 'Content-Type': "text/html", "Expires": "now" });
                    response.end(data);
                }
            });
        }
        function $ID(_id) {
            return document.getElementById(_id);
        }
        function $TAG(_tag) {
            var tags = document.getElementsByTagName(_tag);
            return (tags.length === 1) ? tags[0] : tags;
        }
        function constructPage(thisURL) {
            //this will build the pageContent div from the relevant .json file
            //the tags need to appear in sequence 
            //runs of tags, such as Ps can be condensed into an array
            //be careful to underscore and number any potential duplicate tag keys
            //ids and classes can be assigned using standard CSS notation
            var content,
                docBody,
                edits,
                editTag,
                editClass,
                editID,
                editSet,
                thisEdit,
                codeSamples = [],
                codeSample;
            function addCodeSamples(i) {
                if ((codeSample = codeSamples[i++])) {
                    fs.readFile(codeSample, function (err, csData) {
                        csData = csData.toString()
    				.replace(/&/g, "&amp;")
    				.replace(/</g, "&lt;")
    				.replace(/\>/g, "&gt;");
                        document.getElementById("CODE_" + i).innerHTML = csData;
                        addCodeSamples(i);
                    });
                } else {
                    response.writeHead(200, { 'Content-Type': "text/html", "Expires": "now" });
                    fs.writeFile(thisURL, document.outerHTML, function (err) {
                        //sys.puts(document.outerHTML);
                        response.end(document.outerHTML);
                    });
                }
            }
            fs.readFile("TMCheader.html", function (err, pageData) {
                if (!err) {
                    document.innerHTML = pageData;
                    //sys.puts(thisURL);
                    fs.readFile(thisURL.replace(".html", ".json"), function (err, data) {
                        if (!err) {
                            content = JSON.parse(data.toString());
                            $TAG("TITLE").innerHTML = content.TITLE;
                            document.body.appendChild(document.createElement("DIV"));
                            docBody = document.body.lastChild;
                            docBody.id = "pageContent";
                            edits = content.edits;
                            for (var edit in edits) {
                                editTag = edit.split(/(#|\.|_)/)[0];
                                editID = edit.split("#")[1];
                                editClass = edit.split(".")[1];
                                thisEdit = edits[edit];
                                if (typeof (thisEdit) === "string") {
                                    editSet = [];
                                    editSet.push(thisEdit);
                                } else {
                                    editSet = thisEdit;
                                }
                                addElements();
                            }
                            //sys.puts(document.outerHTML);
                            addCodeSamples(0);
                        } else {
                            serve404();
                        }
                    });
                } else {
                    serve404();
                }
            });
            function $NEW() {
                var _element = document.createElement(editTag);
                (editID) && ((_element.id = editID));
                (editClass) && ((_element.className = editClass));
                return _element;
            }
            function addElements() {
                var newElement,
                    attribute;
                switch (editTag) {
                    case "OL": //array value is the list items
                        newElement = $NEW();
                        newElement.innerHTML = buildList(editSet);
                        docBody.appendChild(newElement);
                        break;
                    case "A": //array value is the href and text
                        newElement = $NEW();
                        newElement.href = editSet[0];
                        newElement.appendChild(document.createTextNode(editSet[1]));
                        docBody.appendChild(newElement);
                        break;
                    case "CODE": //array value is the href and text
                        docBody.appendChild(document.createElement("PRE"));
                        newElement = $NEW();
                        //add the file name but don't actually fetch the codesample until
                        //just before delivery, above
                        codeSamples.push(editSet[0]);
                        newElement.id = "CODE_" + codeSamples.length;
                        docBody.lastChild.appendChild(newElement);
                        break;
                    case "SCRIPT":
                        newElement = $NEW();
                        newElement.src = editSet[0];
                        document.body.appendChild(newElement);
                        break;
                    default: //array value is for separate, consecutive tags
                        editSet.forEach(function (newValue, i) {
                            newElement = $NEW();
                            attribute = (editTag === "IMG") ? "src" : "innerHTML";
                            newElement[attribute] = newValue;
                            docBody.appendChild(newElement);
                        });
                }
            }
        }
        function buildList(listArray) {
            var lists = document.createDocumentFragment();
            listArray.forEach(function (item) {
                lists.appendChild(document.createElement("LI"));
                lists.lastChild.innerHTML = item;
            });
            return lists.innerHTML;
        }
        function deliverMail(message) { 
            getPostParams(request, function (qs) {
                response.writeHead(200, {'Content-Type': 'text/html'});
                var myMail = new Email({
                    to: qs.email,//'nicktulett@yahoo.com',
                    from: 'kate@katylou.co.uk',
                    replyTo:'kate@katylou.co.uk',
                    subject:"Katy Lou email confirmation",//'greetings',
                    body: "<html><head><title>Katy Lou - Textiles Artist</title></head><body>" + qs.name + "<br><br>Thanks for your email - I'll be in touch soon:<br><br>" + qs.comments + "</body></html>",//'This is the <b>message</b>, enjoy.',
                    bodyType: 'html',
                    altText: "Thanks for your email - I'll be in touch soon:\n\n" + qs.comments, //'This is the text alternative.\n\nEnjoy.',
                    timeout: 5000
                });
                //and send an email to katylou, don't forget
                myMail.send(function (err) {
                    if (err) {
                        console.log(err);
                        response.end("I'm so sorry, there was a problem with your email!<br /><br />Please check your details and try again<br /><br />");
                    } else {
                        response.end("Thanks for your email<br><br>I'll be in touch soon");
                        myMail.to = "kate@katylou.co.uk";
                        myMail.from = qs.email;
                        myMail.subject = "WEBSITE email";
                        myMail.send(function (err) {
                            if (err) {
                                console.log(err);
                            }
                        });
                    }
                });
            });
        }
        
        function getPostParams(req, callback){ 
            var body = ''; 
            req.addListener('data', function(chunk){
                body += chunk;
            }) 
            .addListener('end', function() { 
                var obj = querystring.parse(  body.replace( /\+/g, ' ' ) ) ;
                callback( obj );
            });
        } 
        if (request.url) {   
    //        if (request.url.match(/\.mail/)) {
            if (/\.mail/.test(request.url)) {
                deliverMail('<h3>You have mail</h3>');
            } else {
    //            if (request.url.match(/handler\?stats=/)) {
                if (/handler\?stats=/.test(request.url)) {
                    fs.writeFile("testStats.JSN", request.url.split("?stats=")[1].replace(/%22/g, '"'), 
                        function (err) {
                            response.writeHead(200, { 
                                "Content-Type": "text/html", 
                                "Content-Length": 5, //testRun$.length,
                                "Expires": "now" });
                            response.end("ROGER");//testRun$);          
                        }
                    );
                } else {
                    var thisRequest = requested(request);
                    //if it's a request for .html, serve the content
                    if (thisRequest.contentType === "text/html") {
                        if (KL) {
                            currentHTML(thisRequest.url);
                        } else {
                            serveContent(thisRequest.url);
                        }
                    } else {
                        //otherwise serve the file
                        fs.readFile(thisRequest.url, function (err, data) {
                            if (err) {
                                serve404();
                            } else {
                                if (thisRequest.contentType  === 'text/javascript') {
                                    response.writeHead(200, { 'Content-Type': thisRequest.contentType, "Expires": "now" });
                                } else {
                                    response.writeHead(200, { 'Content-Type': thisRequest.contentType, "Expires": "Fri, 15 Aug 2025 02:30:00 GMT" });
                                }
                                response.end(data);
                            }
                        });
                    }
                }
            }
        }
    
});
    var goPort = process.argv[2] || 8000;
    httpServer.listen(goPort);
    
//elf.createLogger(httpServer, {
    //    dir: TMCroot + "logs",
    //    template: "{cs(host)}/{date}.log",
    //    fields: ['date','time','c-ip','cs-method','cs-uri','sc-status','cs(user-agent)']
    //});
    
sys.puts('Server running at http://127.0.0.1:' + goPort + '/');
    
process.on('uncaughtException', function (err) {
        console.log('Caught exception: ' + err);
        var myMail = new Email({
            to: ['nicktulett@yahoo.com', 'ntulett@algorithmics.com', 'katetulett@hotmail.co.uk'],
            from: 'kate@katylou.co.uk',
            replyTo:'kate@katylou.co.uk',
            subject:"Katy Lou website unavailable!",
            body: err,
            timeout: 5000
        });
        myMail.send(function (merr) {
            if (merr) {
               console.log(merr);
            }
            process.exit(0);
        });
    });
  

Where a template is a .json file:

  {"TITLE": "Node this",
    "edits": {
        "H2": "How the TMC server works",
        "H3#pageTitle": "A fairly simple templating system in node",
        "CODE.javascript": "go.js",
        "P": "Where a template is a .json file:",
        "CODE_1.javascript": "node.json",
        "EM": "And, yes, you are looking at the template for this page and the code that's serving it",
        "P_1": "Templates can include arrays for repeated or special tags:",
        "CODE_2.javascript": "automation.json",
        "P#footer": "Node out",
        "SCRIPT": "formatter.js",
        "SCRIPT_1": "/static/highlight/highlight.pack.js"
    }
}

And, yes, you are looking at the template for this page and the code that's serving it

Templates can include arrays for repeated or special tags:

  {"TITLE": "Automation: You don't buy a dog and bark yourself",
    "edits": {
        "H2": "Test Automation",
        "H3#pageTitle": "Not just for test execution",
        "P": [
            "I've been a satisfied user of TestComplete since 2002 (up to the recent takeover, at any rate).",
            "Lately, I've also branched out into writing web application unit and performance tests in Visual Studio",
            "If you're looking for advice about how to capture certain components in your application or you want to know how to use a particular feature of some test tool, please google somewhere else.",
            "What I want to talk about is automating the <em>process</em> of testing. Not just capture-replay but test set-up, measurement, reporting and the other things.",
            "Having said that, I'm not an unreasonable man and there are a few things that I have learnt about TestComplete and VisualStudio that I am willing to pass on.",
            "It should be noted that I started out with TC2 using DelphiScript as it had the best feature-set. A few years ago I began to dabble in VBscript, to make it easier for new members of the team.",
            "Now I am trying to maintain an all-Javascript lifestyle, so I develop scripts in C#/C#script/JScript/Javascript (which I will just refer to as <em>JS</em>). I make no apologies for not translating my older scripts - <em>I simply cannot be bothered</em>"
        ],
        "H3": "Sample TestComplete scripts",
        "OL": [
            "<a href='TCExcel.html'>Working with Excel cells and macros</a>"
        ],
        "H3_1": "Process automation",
        "P_1": [
            "Using an automation tool like TC or VS isn't just limited to capture-replay and data-driven tweaking of the subsequent scripts.",
            "When you ask yourself <em>why</em> you are testing, it's to help the team make decisions. The decisions may be about design, resourcing or release dates but they all depend on the <em>information</em> you can provide as a result of your testing.",
            "Accepting this role, you soon find yourself spending more time shuffling facts and figures than breaking software. That's where automation steps in again to help you:"
        ],
        "OL_1": [
            "Gather", "Interpret", "Report", "Maintain"
        ],
        "P_2": "The data your tests have generated",
        "H4": "Gathering testing information",
        "P_3": [
            "Think carefully about what you can glean about the AUT from your tests, not just individual bugs, but trends that can emerge over time or by concentrating on certain areas of functionality",
            "By maintaining a database of test conditions and test results, you can cross-reference the data to check:"
        ],
        "OL_2": [
            "Is the quality (test pass rates) improving as we approach release?",
            "When were regressions introduced?",
            "Was user validation affected by the move to a new database?",
            "Did changes to matrix functions improve performance?",
            "etc"
        ],
        "P_4": [
            "To perform this sort of analysis, you need to understand joins and sub-queries in SQL. This is beyond the scope of this article, MSDN is a good source.",
            "In addition, you may not be able to glean all the information you need to classify the AUT from the specs, but you can also use automated scripts to parse information out of the AUT itself. Regular expressions may come into play here, so it's well worth your while becoming familiar with those."
        ],
	"H4_1": "Maintaining test results",
	"P_5": [
		"If you're keeping all your test results in source control (and you should), you may want to automate the process of maintaining them.",
		"This is especially relevant if a major change to the AUT requires mass updates to the master results."
	],
	"OL_3": [
            "<a href='TF.html'>Maintaining test results via the TFS API</a>"
        ],
        "P#footer": "Automatic, for the people",
        "SCRIPT": "/static/highlight/highlight.pack.js"
    }
}