{"size": 37880, "ext": "js", "max_stars_repo_path": "tasks/castle.js", "max_stars_repo_name": "michaeljacobdavis/grunt-castle", "max_stars_repo_stars_event_min_datetime": null, "max_stars_repo_stars_event_max_datetime": null, "max_issues_repo_path": "tasks/castle.js", "max_issues_repo_name": "michaeljacobdavis/grunt-castle", "max_issues_repo_issues_event_min_datetime": null, "max_issues_repo_issues_event_max_datetime": null, "max_forks_repo_path": "tasks/castle.js", "max_forks_repo_name": "michaeljacobdavis/grunt-castle", "max_forks_repo_forks_event_min_datetime": null, "max_forks_repo_forks_event_max_datetime": null}
{"max_stars_count": null, "max_issues_count": null, "max_forks_count": null, "avg_line_length": 37.6540755467, "max_line_length": 142, "alphanum_fraction": 0.4432946146}
/*
 * grunt-castle
 *
 *
 * Copyright (c) 2013 WalmartLabs
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function (grunt) {

    var requirejs = require('requirejs');
    var chai = require('chai');
    var sinon = require('sinon');
    var sinonChai = require('sinon-chai');
    var fs = require('fs');
    var path = require('path');
    var Mocha = require('mocha');
    var handlebars = require('handlebars');
    var _ = require('lodash');
    var findup = require('findup-sync');
    var testModules = ['squire', 'chai', 'sinon', 'sinon-chai', 'grunt-castle'];
    var execFile = require("child_process").execFile;
    var util = require('util');
    var xml2js = require('xml2js');
    var XML = require('xml');
    var async = require('async');
    var mocha = new Mocha({ui:'bdd'});

    // CODE COVERAGE REPORTING UTILS
    function aggregate(results, result, spec) { // mocha json-cov reporter
        // single file being tested
        results = results || {
            hits: 0,
            misses: 0,
            sloc: 0,
            files: []
        };

        // Only report for the file we're testing
        var fileResults = _.filter(result.files, function(f) {
            // Take the filename and remove repo (since test doesn't have a repo and .js
            // since the tests are .html
            return spec.indexOf(f.filename.replace(/^repo\/|\.js$/ig, '')) !== -1;
        });

        if (fileResults && fileResults.length) {
            results.files = results.files.concat(fileResults);
        }
        results.hits += result.hits;
        results.misses += result.misses;
        results.sloc += result.sloc;

        results.files.sort(function(a, b) {
            return a.filename.localeCompare(b.filename);
        });

        if (results.sloc > 0) {
            results.coverage = (results.hits / results.sloc) * 100;
        }

        return results;
    }

    function coverageClass(n) { // mocha html-cov reporter
        if (n >= 75) {
            return 'high';
        }
        if (n >= 50) {
            return 'medium';
        }
        if (n >= 25) {
            return 'low';
        }
        return 'terrible';
    }

    function writeClientCoverage(results, dest) {
        var jade = require('jade');
        var templateFilePath = process.cwd() + '/node_modules/grunt-castle/client-cov-templates/client-coverage.jade';
        var templateSrc = fs.readFileSync(templateFilePath, 'utf8');
        var fn = jade.compile(templateSrc, { filename: templateFilePath });

        grunt.file.write(dest + '/index.html', fn({ cov: results, coverageClass: coverageClass }));
    }

    // TASK
    // task life cycle
    // 1. task entry points
    // 2. setup
    // 3. env specific tasks
    var castle = {

        // START SETUP
        setup: function (options) { // life cycle entry point
            this.options = options;
            this.injectTestingLibs();
            this.resolvePaths();
            this.updateCoveragePaths();
            this.requirejsConfigure();
        },

        // ENHANCEMENT: make configurable, so that consumers can use their prefered testing libs
        injectTestingLibs: function () {
            // setup globals for unit testing
            global.requirejs = requirejs;
            chai.use(sinonChai);
            global.chai = chai;
            global.assert = chai.assert;
            global.expect = chai.expect;
            global.sinon = sinon;
        },

        resolvePaths: function () {
            var options = this.options;
            var specs = options.specs;
            var requirejsConfs = options.requirejs;
            var self = this;

            function resolveGlobs(globs) {
                globs = _.isArray(globs) ? globs : [globs];

                return globs.map(function (glob) {
                    var firstChar = glob.substring(0, 1);
                    if (firstChar === '!') {
                        if (glob.substring(1, 1) === '/') {
                            return '!' + specs.baseUrl + glob.substring(1, 1);
                        } else {
                            return '!' + specs.baseUrl + '/' + glob.substring(1, 1);
                        }
                    } else if (firstChar === '/') {
                        return specs.baseUrl + glob;
                    } else {
                        return specs.baseUrl + '/' + glob;
                    }
                });
            }

            function resolvePaths(conf) {
                // set all paths to absolute
                conf.baseUrl = path.resolve(conf.baseUrl);
                for (var key in conf.paths) {
                    conf.paths[key] = path.resolve(conf.baseUrl, conf.paths[key]);
                }
            }

            function resolveMockPaths(options, env) {
                var paths = {};
                var mockFiles = grunt.file.expand(options.mocks[env].baseUrl + '/**/*.js');
                var appTarget = path.normalize(options.requirejs[env].baseUrl);
                var dirs = appTarget.split('/');
                var len = dirs.length;
                var upStr = '';

                for (var i = 0; i < len; i++) {
                    if (dirs[i] !== '.') {
                        upStr += '../';
                    }
                }

                mockFiles.forEach(function (mockFile) {
                    var basename = path.basename(mockFile, '.js');
                    paths[basename] = path.dirname(mockFile) + '/' + basename;
                    paths[basename] = path.normalize(upStr + paths[basename]);
                });

                for (var k in options.mocks[env].paths) {
                    options.mocks[env].paths[k] = path.normalize(upStr + options.mocks[env].baseUrl + '/' +
                        options.mocks[env].paths[k]);
                }

                options.mocks[env].paths = _.extend({}, requirejsConfs[env].paths, paths, options.mocks[env].paths);
                options.mocks[env].baseUrl = path.resolve(options.mocks[env].baseUrl);
            }

            resolveMockPaths(options, 'server');
            resolveMockPaths(options, 'client');

            ['server', 'client', 'common'].forEach(function (env) {
                specs[env] = grunt.file.expand(resolveGlobs(specs[env]));
                if (requirejsConfs[env]) {
                    resolvePaths(requirejsConfs[env]);
                }
            });
        },

        updateCoveragePaths: function () {
            if (!this.options.coverage) {
                return;
            }

            var options = this.options;
            var reporting = options.reporting;
            var exclude = reporting.coverage.exclude;

            reporting.src = path.resolve(reporting.src);
            reporting.coverage.dest = path.resolve(reporting.coverage.dest);

            ['server', 'client', 'common'].forEach(function (env) {
                if (options.requirejs[env]) {
                    var paths = options.requirejs[env].paths;
                    for (var key in paths) {
                        if (paths[key].indexOf('/' + exclude + '/') === -1) {
                            paths[key] = paths[key].replace(reporting.src, reporting.coverage.dest);
                        }
                    }
                }
            });
        },

        requirejsConfigure: function () {
            var configObj = _.clone(this.options.requirejs.server || this.options.requirejs, true);
            if (new RegExp(/^(l)?cov(-)?/i).test(this.options.args[0])) {
                //then we need to update the baseURL to point to the instrumented code
                configObj.baseUrl = this.options.reporting.coverage.dest;
            }
            // TODO: #15 configObj.context = configObj.context || 'castle';
            requirejs.config(configObj);

            global.castle = {};
            global.castle.config = this.options;
        },
        // END SETUP

        // START TASK ENTRY POINTS
        // START UNIT TESTING
        test: function (options) {
            var self = this;
            this.setup(options);

            if (options.server) {
                this.testServer(options.args[1], function () {
                    if (options.client) {
                        self.testClient(options.args[1], function () {
                            options.done();
                        });
                    } else {
                        options.done();
                    }
                });
            }
            if (!options.server && options.client) {
                this.testClient(options.args[1], function () {
                    options.done();
                });
            }
        },

        coverage: function (options) {
            var self = this;
            var waitFor = options.client && options.server ? 2 : 1;
            var ranCount = 0;

            function done() {
                ranCount++;
                if (waitFor === ranCount) {
                    options.done();
                }
            }

            options.coverage = true;
            this.setup(options);
            this.jscoverage(function (err) {
                if (options.client) {
                    self.writeClientSpecs(options.args[1], function () {
                        if (options.lcov) {
                            self.lcovClient(options.args[1], function () {
                                done();
                            });
                        } else {
                            self.coverageClient(options.args[1], function () {
                                done();
                            });
                        }
                    });
                }
                if (options.server) {
                    if (options.lcov) {
                        self.lcovServer(options.args[1], function () {
                            done();
                        });
                    } else {
                        self.coverageServer(options.args[1], function () {
                            done();
                        });
                    }
                }
            });
        },

        lcov: function (options) {
            var self = this;
            var waitFor = options.client && options.server ? 2 : 1;
            var ranCount = 0;

            function done() {
                ranCount++;
                if (waitFor === ranCount) {
                    options.done();
                }
            }

            options.coverage = true;
            this.setup(options);
            this.jscoverage(function (err) {
                if (options.client) {
                    if (self.specsWritten) {
                        self.coverageClient(options.args[1], function (results) {
                            done();
                        }, {type: 'lcov', reporter: 'json-cov'});
                    } else {
                        self.writeClientSpecs(options.args[1], function () {
                            self.coverageClient(options.args[1], function (results) {
                                done();
                            }, {type: 'lcov', reporter: 'json-cov'});
                        });
                    }
                }

                if (options.server) {
                    self.coverageServer(options.args[1], function (results) {
                        done();
                    }, true);
                }
            });
        },

        xunit: function (options) {
            var self = this;
            var waitFor = options.client && options.server ? 2 : 1;
            var ranCount = 0;

            function done() {
                ranCount++;
                if (waitFor === ranCount) {
                    options.done();
                }
            }

            this.setup(options);
            if (options.client) {
                if (self.specsWritten) {
                    self.xunitClient(options.args[1], function (results) {
                        done();
                    });
                } else {
                    self.writeClientSpecs(options.args[1], function () {
                        self.xunitClient(options.args[1], function (results) {
                            done();
                        });
                    });
                }
            }

            if (options.server) {
                self.xunitServer(options.args[1], function (results) {
                    done();
                });
            }
        },

        analyze: function (options) {
            var files = {};
            this.setup(options);

            files[path.resolve(this.options.reporting.dest + '/analysis')] = this.options.reporting.analysis.files;
            grunt.config.set('plato', {
                castle: {
                    files: files
                }
            });
            grunt.task.loadTasks('node_modules/grunt-castle/node_modules/grunt-plato/tasks');
            grunt.task.run('plato:castle');
            options.done();
        },
        // END TASK ENTRY POINTS

        testClient: function (file, callback) {
            var htmlSpecsPath = this.getHtmlSpecsPath();
            var self = this;
            var files;

            this.writeClientSpecs(file, function () {
                files = file ? ('/' + file + '.html') : '/**/*.html';

                grunt.task.loadTasks('node_modules/grunt-castle/node_modules/grunt-mocha/tasks');
                grunt.config.set('mocha', {
                    client: {
                        src: (htmlSpecsPath + files),
                        options: {
                            reporter: 'Spec'
                        }
                    }
                });
                grunt.task.run('mocha:client');

                callback();
            });
        },

        testServer: function (file, callback) {
            var specs = this.getSpecs('server');
            mocha.files = [];

            if (file) {
                var spec = this.resolveFileSpec(file, 'server');
                if (!spec) { // TODO: exit and log error
                    throw 'no spec found';
                }
                mocha.addFile(spec);
                mocha.reporter('spec').run(callback);
            } else {
                specs.forEach(function (spec, index) {
                    mocha.addFile(path.resolve(spec));
                });
                mocha.reporter('spec').run(function() {
                    callback();
                });
            }
        },
        // END UNIT TESTING

        // START COVERAGE
        jscoverage: function (callback) {
            var self = this;
            if (!this.alreadyInstrumented) {
                var jscovConf = grunt.config.get('jscoverage');

                if (grunt.file.exists(this.options.reporting.coverage.dest)) {
                    grunt.file.delete(this.options.reporting.coverage.dest);
                }

                var args2 = [];
                args2.push(this.options.reporting.src);
                args2.push(this.options.reporting.coverage.dest);

            if (!this.options.reporting.highlight) {
                args2.push('--no-highlight');
            }
            if (this.options.reporting.coverage.exclude) {
                args2.push('--exclude=' + this.options.reporting.coverage.exclude);
            }
            if (this.options.reporting.encoding) {
                args2.push('--encoding=' + this.options.reporting.encoding);
            }
            if (this.options.reporting.noInstrument) {
                var toIgnore = this.options.reporting.noInstrument;
                if (typeof toIgnore === 'string') {
                    args2.push('--no-instrument=' + toIgnore);
                } else {
                    for (var idx = 0; idx < toIgnore.length; idx++) {
                        args2.push('--no-instrument=' + toIgnore[idx]);
                    }
                }
            }
            if (this.options.reporting.jsVersion) {
                args2.push('--js-version=' + this.options.reporting.jsVersion);
            }

            grunt.util.spawn({
                    cmd: 'jscoverage',
                    args: args2,
                    opts: {
                        stdio: 'inherit'
                    }
                },
                function (error, result) {
                    if (error) {
                        grunt.log.error(result.stderr);
                        callback(false);
                    }
                    self.alreadyInstrumented = true;
                    grunt.log.writeln(result.stdout);
                    callback();
                });
            } else {
                callback();
            }
        },

        coverageClient: function (file, callback, lcov) {
            var options = this.options;
            var results;
            var count = 0;
            var files = [];
            var specs = grunt.file.expand(path.resolve(options.specs['client-target']) + '/**/*.html');
            var covReportPath = this.getCovReportPath('client');
            var self = this;

            async.eachSeries(specs, function(spec, mochaCallback) {
                grunt.log.writeln('running client spec:' + spec);
                var mocha = execFile('./node_modules/grunt-castle/node_modules/mocha-phantomjs/bin/mocha-phantomjs',
                    [spec, '-R', 'json-cov'],
                    { maxBuffer: 10000 * 1024, cwd: '.' },
                    function(error, stdout, stderr) {
                        if (!error) {
                            var result = JSON.parse(stdout);
                            /*if (!results) {
                                results = result;
                            } else {
                                results = aggregate(results, result, spec);
                            }*/
                            results = aggregate(results, result, spec);
                            count++;
                            if (count === specs.length) {
                                if (lcov) {
                                    self.lcovClient(results, callback);
                                } else {
                                    if (!grunt.file.exists(covReportPath)) {
                                        grunt.file.mkdir(covReportPath);
                                    }
                                    grunt.log.writeln('writing client coverage report');
                                    writeClientCoverage(results, covReportPath);
                                    return mochaCallback();
                                }
                            }
                            return mochaCallback();
                        } else {
                            grunt.log.error("error executing " + spec + " coverage report : " + error);
                            if (stderr) {
                                grunt.log.error(stderr);
                            }
                            return callback(error);
                        }
                    }
                );
            }, function(error) {
                if (error) {
                    grunt.log.error('error code: ' + error.code);
                    grunt.log.error('error signal: ' + error.signal);
                    process.exit(error.code || 1);
                } else {
                    return callback();
                }
            });
        },

        coverageServer: function (file, callback, lcov) {
            var covReportPath = this.getCovReportPath('server');
            var specs = this.getSpecs('server');
            var outFile = path.normalize(covReportPath + (lcov ? '/index.json' : '/index.html'));
            var reporter = lcov ? 'json-cov' : 'html-cov';
            var counter = 0;
            var output;
            var specCount = specs.length;
            var self = this;
            var _stdout = process.stdout.write;

            mocha.files = [];

            if (!grunt.file.exists(covReportPath)) {
                grunt.file.mkdir(covReportPath);
            }

            output = fs.createWriteStream(outFile, { flags: 'w' });
            function run() {
                grunt.log.writeln('running server specs...');
                process.stdout.write = function(chunk, encoding, cb) {
                    return output.write(chunk, encoding, cb);
                };
                mocha.reporter(reporter).run(function () {
                    output.end();
                    process.stdout.write = _stdout;
                    grunt.log.writeln('writing server coverage report');
                    if (lcov) {
                        self.lcovServer(callback);
                    } else {
                        callback();
                    }
                });
            }

            specs.forEach(function (spec) {
                grunt.log.writeln('adding server spec:' + spec);
                mocha.addFile(spec);
                counter++;
                if (counter === specCount) {
                    run();
                }
            });
        },

        lcovServer: function (callback) {
            var covReportPath = path.normalize(this.getCovReportPath('server') + '/index.json');
            var results = grunt.file.readJSON(covReportPath);
            this.writeLcovResults(results, 'server', callback);
        },

        lcovClient: function (results, callback) {
            this.writeLcovResults(results, 'client', callback);
        },

        xunitServer: function (file, callback) {
            var specs = this.getSpecs('server');
            var covReportPath = this.getCovReportPath('server');
            var filename = this.options.reporting && this.options.reporting.xunit ? ('/' + this.options.reporting.xunit.filename) : null;
            var outFile = covReportPath + (filename || '/xunit.xml');

            mocha.files = [];

            if (!grunt.file.exists(covReportPath)) {
                grunt.file.mkdir(covReportPath);
            }
            if (grunt.file.exists(outFile)) {
                grunt.file.delete(outFile);
            }
            var output = fs.createWriteStream(outFile, { flags: 'w' });
            var _stdout = process.stdout.write;
            grunt.log.writeln('Creating ' + outFile + ' ...');

            var runMocha = function() {

                process.stdout.write = function(chunk, encoding, cb) {
                    return output.write(chunk, encoding, cb);
                };
                mocha.reporter('xunit').run(function () {
                    output.end();
                    process.stdout.write = _stdout;
                    return callback();
                });
            };

            if (file) {
                var spec = this.resolveFileSpec(file, 'server');
                if (!spec) {
                    throw 'no spec found';
                }
                mocha.addFile(spec);
                runMocha();
            } else {
                specs.forEach(function (spec, index) {
                    mocha.addFile(path.resolve(spec));
                });
                runMocha();
            }
        },

        xunitClient: function (file, callback) {
            var options = this.options;
            var results = [];
            var files = [];
            var specs = grunt.file.expand(path.resolve(options.specs['client-target']) + '/**/*.html');
            var self = this;

            async.eachSeries(specs, function(spec, callback) {
                grunt.log.writeln('running client spec:' + spec);
                var mocha = execFile('./node_modules/grunt-castle/node_modules/mocha-phantomjs/bin/mocha-phantomjs',
                    [spec, '-R', 'xunit'],
                    { maxBuffer: 10000 * 1024, cwd: '.' },
                    function(error, stdout, stderr) {
                        if (!error) {
                            try {
                                var parser = new xml2js.Parser();
                                parser.parseString(stdout, function (err, result) {
                                    results.push(result);
                                });

                                if (results.length === specs.length) {
                                    self.writeXunitResults(results, callback);
                                }
                                return callback();
                            }
                            catch(err) {
                                return callback(err);
                            }
                        } else {
                            grunt.log.error("error executing " + spec + " xunit report : " + error);
                            grunt.log.error(stderr);
                            return callback(error);
                        }
                    }
                );
            }, function(error) {
                if (error) {
                    grunt.log.error('error code: ' + error.code);
                    grunt.log.error('error signal: ' + error.signal);
                    process.exit(error.code || 1);
                }
            });
        },
        // END COVERAGE

        // I/O
        writeLcovResults: function (results, env, callback) {
            var covReportPath = this.getCovReportPath(env);
            var lcovFilename = this.options.reporting && this.options.reporting.lcov ? ('/' + this.options.reporting.lcov.filename) : null;
            var lcovFile = covReportPath + (lcovFilename || '/index.lcov');
            var stream;

            if (!grunt.file.exists(covReportPath)) {
                grunt.file.mkdir(covReportPath);
            }
            if (grunt.file.exists(lcovFile)) {
                grunt.file.delete(lcovFile);
            }
            grunt.log.writeln('Creating ' + lcovFile + '...');
            stream = fs.createWriteStream(lcovFile);

            var fileCount = results.files.length;
            for (var j = 0; j < fileCount; j++) {
                var file = results.files[j];
                var lcov = 'SF:' + file.filename + '\n';
                var lines = file.source;
                for (var k in lines) {
                    if (lines.hasOwnProperty(k)) {
                        var line = lines[k];
                        if (line.coverage !== '') {
                            lcov = lcov + 'DA:' + k + ',' + line.coverage + '\n';
                        }
                    }
                }
                lcov = lcov + 'end_of_record\n';
                stream.write(lcov);
            }
            stream.end();
            if (env === 'server') {
                grunt.file.delete(covReportPath + '/index.json');
            }
            callback();
        },

        writeXunitResults: function (results, callback) {
            var covReportPath = this.getCovReportPath('client');
            var xunitFilename = this.options.reporting && this.options.reporting.xunit ? ('/' + this.options.reporting.xunit.filename) : null;
            var xunitFile = covReportPath + (xunitFilename || '/xunit.xml');

            if (!grunt.file.exists(covReportPath)) {
                grunt.file.mkdir(covReportPath);
            }
            if (grunt.file.exists(xunitFile)) {
                grunt.file.delete(xunitFile);
            }

            var fileCount = results.length;
            var details = {
                name: "Mocha Tests",
                tests: "0",
                failures: "0",
                errors: "0",
                skipped: "0",
                time: "0"
            };
            var contents = [ { _attr: details } ];
            for (var i = 0; i < fileCount; i++) {
                var detail = results[i].testsuite.$;
                var testCount = parseInt(detail.tests, 10);
                var skipCount = parseInt(detail.skipped, 10);
                details.tests = parseInt(details.tests, 10) + testCount + "";
                details.failures = parseInt(details.failures, 10) + parseInt(detail.failures, 10) + "";
                details.errors = parseInt(details.errors, 10) + parseInt(detail.errors, 10) + "";
                details.skipped = parseInt(details.skipped, 10) + skipCount + "";
                details.time = parseFloat(details.time, 10) + parseFloat(detail.time, 10) + "";
                details.timestamp = details.timestamp || detail.timestamp;

                if (results[i].testsuite.testcase) {
                    var testcases = results[i].testsuite.testcase;
                    for (var j = 0; j < testCount - skipCount; j++) {
                        contents.push( { testcase: [ { _attr: testcases[j].$ } ] });
                    }
                }
            }
            grunt.log.writeln('Creating ' + xunitFile + ' ...');
            var xunit = [ {testsuite: contents} ];
            fs.writeFile(xunitFile, XML(xunit, true), function (err) {
                if (err) {
                    grunt.log.error("error: " + err);
                    process.exit(1);
                } else {
                    callback();
                }
            });
        },

        writeClientSpecs: function (file, callback) {
            var specs = this.getSpecs('client');
            var self = this;
            var templateSrc = grunt.file.read(path.normalize(path.dirname(require.resolve('grunt-castle')) + '/spec.hbs'));
            var template = handlebars.compile(templateSrc);

            function updateConfig(config) {
                var paths = config.paths;

                function getPath(moduleMain) {
                    return path.normalize(path.dirname(moduleMain) + '/' + path.basename(moduleMain, '.js'));
                }

                testModules.forEach(function (module) {
                    if (module === 'squire') {
                        try {
                            require('squirejs');
                        } catch (e) {
                            paths[module] = getPath(require.resolve('squirejs'));
                        }
                    } else {
                        if (module === 'grunt-castle') {
                            paths['castle'] = path.dirname(require.resolve(module)) + '/castle';
                        } else if (module === 'chai') {
                            paths[module] = path.dirname(require.resolve(module)) + '/chai';
                        } else if (module === 'sinon') {
                            paths[module] = getPath(require.resolve('sinon-built'));
                        } else {
                            paths[module] = getPath(require.resolve(module));
                        }
                    }
                });

                return config;
            }

            function getRequirejsPath() {
                var rjs = path.dirname(require.resolve('requirejs'));
                rjs = rjs.split('/');
                rjs = rjs.slice(1, rjs.length - 1);
                return '/' + rjs.join('/') + '/require.js';
            }

            function writeSpec(spec, specHtmlPath, callback) {
                var configObj = updateConfig(self.options.requirejs.client || self.options.requirejs);

                //is this a coverage task?
                if (new RegExp(/^(l)?cov(-)?/i).test(self.options.args[0])) {
                    //then we need to update the baseURL to point to the instrumented code
                    configObj.baseUrl = self.options.reporting.coverage.dest;
                }
                var templateData = {
                    config: JSON.stringify(configObj),
                    spec: path.resolve(spec),
                    castle: JSON.stringify(global.castle.config),
                    basePath: process.cwd() + '/node_modules/grunt-castle',
                    requirejsPath: getRequirejsPath()
                };

                grunt.log.writeln('writing spec:' + specHtmlPath);
                grunt.file.write(specHtmlPath, template(templateData));
                callback();
            }

            if (file) {
                var spec = this.resolveFileSpec(file, 'client');
                var specHtmlPath = this.specPathToHtmlSpecPath(spec);
                writeSpec(spec, specHtmlPath, callback);
            } else {
                var counter = 0;
                var limit = specs.length;
                specs.forEach(function (spec) {
                    var specHtmlPath = self.specPathToHtmlSpecPath(path.resolve(spec));
                    writeSpec(spec, specHtmlPath, function () {
                        counter++;
                        if (counter === limit) {
                            self.specsWritten = true;
                            callback();
                        }
                    });
                });
            }
        },

        // UTILS
        getHtmlSpecsPath: function () {
            return path.resolve(this.options.specs['client-target']);
        },

        getCovReportPath: function (env) { // TODO: make the dir name configurable
            return path.resolve(this.options.reporting.dest + '/' + env + '-coverage');
        },


        getSpecs: function (env) {
            return this.options.specs.common.concat(this.options.specs[env]);
        },

        specPathToHtmlSpecPath: function (specPath) {
            var htmlSpecDir = this.getHtmlSpecsPath();
            var relativeSpecPath = specPath.replace(process.cwd() + '/', '').replace(this.options.specs.baseUrl + '/', '');
            var absoluteHtmlSpecPath = path.normalize(htmlSpecDir + '/' + relativeSpecPath);

            return path.normalize(path.dirname(absoluteHtmlSpecPath) + '/' + path.basename(absoluteHtmlSpecPath, '.js')) + '.html';
        },

        resolveFileSpec: function (spec, env) {
            var specs = this.getSpecs(env);
            var paths = [];

            paths = _.unique(specs.map(function (spec) {
                        return path.dirname(spec);
                    }).sort());

            var specPath;
            for (var i = 0; i < paths.length; i++) {
                if ((specPath = findup(spec + '.js', { cwd: paths[i], nocase: true }))) {
                    return specPath;
                }
            }
        }
    };

    function getModulePaths() {
        var paths = {};

        testModules.forEach(function (module) { // test modules is defined at the top of the file
            if (module === 'squire') {
                try {
                    require('squirejs');
                } catch (e) {
                    paths[module] = path.dirname(require.resolve('squirejs')) + '/Squire';
                }
            } else {
                paths[module] = path.dirname(require.resolve(module));
                if (module === 'grunt-castle') {
                    paths['castle'] = paths[module] + '/castle';
                    delete paths[module];
                }
            }
        });

        return paths;
    }

    grunt.registerMultiTask('castle', 'AMD testing harness and code anaysis', function () {
        // Merge task-specific and/or target-specific options with these defaults.
        var done = this.async(),
            options = this.options(),
            castlePaths = getModulePaths();

        if (options.requirejs.client) {
            _.extend(options.requirejs.client.paths, castlePaths);
        }
        if (options.requirejs.server) {
            _.extend(options.requirejs.server.paths, castlePaths);
        }
        if (options.requirejs.paths) {
            _.extend(options.requirejs.paths, castlePaths);
        }
        _.extend(options, {
            args: this.args,
            done: done
        });

        castle.alreadyInstrumented = grunt.option('alreadyInstrumented');

        if (!this.args[0]) {
            this.args[0] = 'test';
        }
        switch (this.args[0]) {
            case 'test':
                options.server = true;
                options.client = true;
                castle.test(options);
                break;
            case 'test-client':
                options.client = true;
                castle.test(options);
                break;
            case 'test-server':
                options.server = true;
                castle.test(options);
                break;
            case 'cov':
                options.server = true;
                options.client = true;
                castle.coverage(options);
                break;
            case 'cov-client':
                options.client = true;
                castle.coverage(options);
                break;
            case 'cov-server':
                options.server = true;
                castle.coverage(options);
                break;
            case 'lcov':
                options.server = true;
                options.client = true;
                castle.lcov(options);
                break;
            case 'lcov-client':
                options.client = true;
                castle.lcov(options);
                break;
            case 'lcov-server':
                options.server = true;
                castle.lcov(options);
                break;
            case 'analyze':
                castle.analyze(options);
                break;
            case 'xunit':
                options.server = true;
                options.client = true;
                castle.xunit(options);
                break;
            case 'xunit-client':
                options.client = true;
                castle.xunit(options);
                break;
            case 'xunit-server':
                options.server = true;
                castle.xunit(options);
                break;
        }

    });

};
