1
0
mirror of https://github.com/JuanCanham/HackMyResume.git synced 2024-06-30 23:40:05 +01:00

Refactor error handling.

Work towards better debug/log/stack trace options for error cases.
This commit is contained in:
hacksalot 2016-01-07 15:54:10 -05:00
parent 43419c27cf
commit cb3488276d
4 changed files with 183 additions and 135 deletions

View File

@ -28,102 +28,122 @@ Error-handling routines for HackMyResume.
err: function( ex, shouldExit ) {
var msg = '', exitCode;
var msg = '', exitCode, log = console.log, showStack = false;
// If the exception has been handled elsewhere and shouldExit is true,
// let's get out of here, otherwise silently return.
if( ex.handled ) {
if( shouldExit )
process.exit( exitCode );
return;
}
// Get an error message -- either a HackMyResume error message or the
// exception's associated error message
if( ex.fluenterror ){
switch( ex.fluenterror ) {
case HACKMYSTATUS.themeNotFound:
msg = "The specified theme couldn't be found: " + ex.data;
break;
case HACKMYSTATUS.copyCSS:
msg = "Couldn't copy CSS file to destination folder";
break;
case HACKMYSTATUS.resumeNotFound:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = chalk.yellow("Please ") + chalk.yellow.bold("give me a command") +
chalk.yellow(" (");
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync( PATH.resolve(__dirname, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = chalk.yellow('Invalid command: "') + chalk.yellow.bold(ex.attempted) + chalk.yellow('"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify an output file name') +
chalk.yellow(' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify the filename of the resume') +
chalk.yellow(' to create.');
break;
case HACKMYSTATUS.wkhtmltopdf:
msg = chalk.red.bold('ERROR: PDF generation failed. ') + chalk.red('Make sure wkhtmltopdf is ' +
'installed and accessible from your path.');
if( ex.inner ) msg += chalk.red('\n' + ex.inner);
break;
case HACKMYSTATUS.invalid:
msg = chalk.red.bold('ERROR: Validation failed and the --assert option was specified.');
break;
}
var errInfo = get_error_msg( ex );
msg = errInfo.msg;
exitCode = ex.fluenterror;
showStack = errInfo.showStack;
}
else {
msg = ex.toString();
exitCode = 4;
exitCode = -1;
// Deal with pesky 'Error:' prefix.
var idx = msg.indexOf('Error: ');
msg = idx === -1 ? msg : msg.substring( idx + 7 );
}
// Deal with pesky 'Error:' prefix.
var idx = msg.indexOf('Error: ');
var trimmed = idx === -1 ? msg : msg.substring( idx + 7 );
// Log non-HackMyResume-handled errors in red with ERROR prefix. Log HMR
// errors as-is.
ex.fluenterror ?
log( msg.toString() ) :
log( chalk.red.bold('ERROR: ' + msg.toString()) );
// If this is an unhandled error, or a specific class of handled error,
// output the error message and stack.
if( !ex.fluenterror || ex.fluenterror < 3 ) { // TODO: magic #s
console.log( chalk.red.bold('ERROR: ' + trimmed.toString()) );
if( ex.code !== 'ENOENT' ) // Don't emit stack for common stuff
console.log( chalk.gray(ex.stack) );
}
else {
console.log( trimmed.toString() );
}
// Usually emit the stack
( showStack && ex.code !== 'ENOENT' ) && log( chalk.gray(ex.stack) );
// Let the error code be the process's return code.
if( shouldExit || ex.shouldExit )
process.exit( exitCode );
( shouldExit || ex.shouldExit ) && process.exit( exitCode );
}
};
function get_error_msg( ex ) {
var msg = '', withStack = false;
switch( ex.fluenterror ) {
case HACKMYSTATUS.themeNotFound:
msg = "The specified theme couldn't be found: " + ex.data;
break;
case HACKMYSTATUS.copyCSS:
msg = "Couldn't copy CSS file to destination folder";
break;
case HACKMYSTATUS.resumeNotFound:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.missingCommand:
msg = chalk.yellow("Please ") + chalk.yellow.bold("give me a command") +
chalk.yellow(" (");
msg += Object.keys( FCMD.verbs ).map( function(v, idx, ar) {
return (idx === ar.length - 1 ? chalk.yellow('or ') : '') +
chalk.yellow.bold(v.toUpperCase());
}).join( chalk.yellow(', ')) + chalk.yellow(").\n\n");
msg += chalk.gray(FS.readFileSync( PATH.resolve(__dirname, '../use.txt'), 'utf8' ));
break;
case HACKMYSTATUS.invalidCommand:
msg = chalk.yellow('Invalid command: "') + chalk.yellow.bold(ex.attempted) + chalk.yellow('"');
break;
case HACKMYSTATUS.resumeNotFoundAlt:
msg = chalk.yellow('Please ') + chalk.yellow.bold('feed me a resume') +
chalk.yellow(' in either FRESH or JSON Resume format.');
break;
case HACKMYSTATUS.inputOutputParity:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify an output file name') +
chalk.yellow(' for every input file you wish to convert.');
break;
case HACKMYSTATUS.createNameMissing:
msg = chalk.yellow('Please ') + chalk.yellow.bold('specify the filename of the resume') +
chalk.yellow(' to create.');
break;
case HACKMYSTATUS.wkhtmltopdf:
msg = chalk.red.bold('ERROR: PDF generation failed. ') + chalk.red('Make sure wkhtmltopdf is ' +
'installed and accessible from your path.');
if( ex.inner ) msg += chalk.red('\n' + ex.inner);
withStack = true;
break;
case HACKMYSTATUS.invalid:
msg = chalk.red.bold('ERROR: Validation failed and the --assert option was specified.');
break;
case HACKMYSTATUS.invalidTarget:
ex.data.forEach(function(d){
msg += chalk.red.bold('The ' + ex.theme.name + " theme doesn't support the " + d.format + " format.\n");
});
break;
}
return {
msg: msg,
withStack: withStack
};
}
}());

View File

@ -18,7 +18,8 @@ Status codes for HackMyResume.
createNameMissing: 8,
wkhtmltopdf: 9,
missingPackageJSON: 10,
invalid: 11
invalid: 11,
invalidTarget: 12
};
}());

View File

@ -15,6 +15,7 @@ Implementation of the 'generate' verb for HackMyResume.
, MD = require('marked')
, MKDIRP = require('mkdirp')
, EXTEND = require('../utils/extend')
, HACKMYSTATUS = require('../core/status-codes')
, parsePath = require('parse-filepath')
, _opts = require('../core/default-options')
, FluentTheme = require('../core/fresh-theme')
@ -48,30 +49,19 @@ Implementation of the 'generate' verb for HackMyResume.
*/
function build( src, dst, opts, logger, errHandler ) {
// Housekeeping
//_opts = extend( true, _opts, opts );
_log = logger || console.log;
_err = errHandler || error;
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim())|| 'modern';
_opts.prettify = opts.prettify === true ? _opts.prettify : false;
_opts.css = opts.css || 'embed';
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
//_opts.noTips = opts.noTips;
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
if( src.length > 1 && ( !dst || !dst.length ) ) {
dst.push( src.pop() );
}
prep( src, dst, opts, logger, errHandler );
// Load the theme...we do this first because the theme choice (FRESH or
// JSON Resume) determines what format we'll convert the resume to.
var tFolder = verify_theme( _opts.theme );
var theme = load_theme( tFolder );
// Check for invalid outputs
var inv = verify_outputs( dst, theme );
if( inv && inv.length ) {
throw { fluenterror: HACKMYSTATUS.invalidTarget, data: inv, theme: theme };
}
// Load input resumes...
if( !src || !src.length ) { throw { fluenterror: 3 }; }
var sheets = ResumeFactory.load(src, {
@ -134,6 +124,33 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Prepare for a BUILD run.
*/
function prep( src, dst, opts, logger, errHandler ) {
// Housekeeping
_log = logger || console.log;
_err = errHandler || error;
//_opts = extend( true, _opts, opts );
_opts.theme = (opts.theme && opts.theme.toLowerCase().trim()) || 'modern';
_opts.prettify = opts.prettify === true;
_opts.css = opts.css || 'embed';
_opts.pdf = opts.pdf;
_opts.wrap = opts.wrap || 60;
_opts.stitles = opts.sectionTitles;
_opts.tips = opts.tips;
_opts.noTips = opts.noTips;
// If two or more files are passed to the GENERATE command and the TO
// keyword is omitted, the last file specifies the output file.
( src.length > 1 && ( !dst || !dst.length ) ) && dst.push( src.pop() );
}
/**
Generate a single target resume such as "out/rez.html" or "out/rez.doc".
@param targInfo Information for the target resume.
@ -147,6 +164,9 @@ Implementation of the 'generate' verb for HackMyResume.
}
try {
if( !targInfo.fmt ) {
return;
}
var f = targInfo.file
, fType = targInfo.fmt.outFormat
, fName = PATH.basename(f, '.' + fType)
@ -242,6 +262,27 @@ Implementation of the 'generate' verb for HackMyResume.
/**
Ensure that user-specified outputs/targets are valid.
*/
function verify_outputs( targets, theme ) {
return _.reject(
targets.map( function( t ) {
var pathInfo = parsePath( t );
return {
format: pathInfo.extname.substr(1)
};
}),
function(t) {
return t.format === 'all' || theme.hasFormat( parsePath( t.format ).extname.substr(1));
}
);
}
/**
Expand output files. For example, "foo.all" should be expanded to
["foo.html", "foo.doc", "foo.pdf", "etc"].

View File

@ -36,47 +36,42 @@ describe('Testing CLI interface', function () {
var ft = 'node_modules/fresh-test-resumes/src/';
[
[ 'new', [sb + 'new-fresh-resume.json'], [], opts, ' (FRESH format)' ],
[ 'new', [sb + 'new-jrs-resume.json'], [], opts2, ' (JRS format)'],
[ 'new', [sb + 'new-1.json', sb + 'new-2.json', sb + 'new-3.json'], [], opts, ' (multiple FRESH resumes)' ],
[ 'new', [sb + 'new-jrs-1.json', sb + 'new-jrs-2.json', sb + 'new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ],
[ 'new', [sb + 'new-fresh-resume.json'], [], opts, ' (FRESH format)' ],
[ 'new', [sb + 'new-jrs-resume.json'], [], opts2, ' (JRS format)'],
[ 'new', [sb + 'new-1.json', sb + 'new-2.json', sb + 'new-3.json'], [], opts, ' (multiple FRESH resumes)' ],
[ 'new', [sb + 'new-jrs-1.json', sb + 'new-jrs-2.json', sb + 'new-jrs-3.json'], [], opts, ' (multiple JRS resumes)' ],
[ 'validate', [ft + 'jane-fullstacker.fresh.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ],
[ 'validate', [ft + 'johnny-trouble.fresh.json'], [], opts, ' (johnny-trouble|FRESH)' ],
[ 'validate', [sb + 'new-fresh-resume.json'], [], opts, ' (new-fresh-resume|FRESH)' ],
[ 'validate', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks.json|JRS)' ],
[ 'validate', ['test/resumes/jrs-0.0.0/jane-incomplete.json'], [], opts2, ' (jane-incomplete.json|JRS)' ],
[ 'validate', [sb + 'new-1.json', sb + 'new-jrs-resume.json', sb + 'new-1.json', sb + 'new-2.json', sb + 'new-3.json'], [], opts, ' (5|BOTH)' ],
[ 'analyze', [ft + 'jane-fullstacker.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ],
[ 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' ],
[ 'build',
[ ft + 'jane-fullstacker.fresh.json',
ft + 'override/jane-fullstacker-override.fresh.json' ],
[ sb + 'merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)' ],
[ 'build',
[ ft + 'jane-fullstacker.fresh.json'],
[ sb + 'shouldnt-exist.pdf' ],
EXTEND(true, opts, { theme: 'awesome' }),
' (jane-q-fullstacker + Awesome + PDF|FRESH)' ]
[ 'analyze', [ft + 'jane-fullstacker.json'], [], opts, ' (jane-q-fullstacker|FRESH)' ],
[ 'analyze', ['test/resumes/jrs-0.0.0/richard-hendriks.json'], [], opts2, ' (richard-hendriks|JRS)' ],
[ 'build', [ ft + 'jane-fullstacker.fresh.json', ft + 'override/jane-fullstacker-override.fresh.json' ], [ sb + 'merged/jane-fullstacker-gamedev.fresh.all'], opts, ' (jane-q-fullstacker w/ override|FRESH)' ],
[ '!build', [ ft + 'jane-fullstacker.fresh.json'], [ sb + 'shouldnt-exist.pdf' ], EXTEND(true, opts, { theme: 'awesome' }), ' (jane-q-fullstacker + Awesome + PDF|FRESH)' ],
[ '!new', [], [], opts, " (when a filename isn't specified)" ]
].forEach( function(a) {
run.apply( null, a );
run.apply( /* The players of */ null, a );
});
fail( 'new', [], [], opts, " (when a filename isn't specified)" );
function logMsg() {
}
function run( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should SUCCEED' + msg, function () {
var shouldSucceed = true;
if( verb[0] === '!' ) {
verb = verb.substr(1);
shouldSucceed = false;
}
it( 'The ' + verb.toUpperCase() + ' command should ' + (shouldSucceed ? ' SUCCEED' : ' FAIL') + msg, function () {
function runIt() {
try {
FCMD.verbs[verb]( src, dst, opts, opts.silent ?
logMsg : function(msg){ msg = msg || ''; console.log(msg); } );
function(){} : function(msg){ msg = msg || ''; console.log(msg); } );
}
catch(ex) {
console.error(ex);
@ -84,19 +79,10 @@ describe('Testing CLI interface', function () {
throw ex;
}
}
runIt.should.not.Throw();
});
}
function fail( verb, src, dst, opts, msg ) {
msg = msg || '.';
it( 'The ' + verb.toUpperCase() + ' command should FAIL' + msg, function () {
function runIt() {
FCMD.verbs[verb]( src, dst, opts, logMsg );
}
runIt.should.Throw();
if( shouldSucceed )
runIt.should.not.Throw();
else
runIt.should.Throw();
});
}