From cb3488276d086c664a2cb7640e5ac6beeb0d4335 Mon Sep 17 00:00:00 2001 From: hacksalot Date: Thu, 7 Jan 2016 15:54:10 -0500 Subject: [PATCH] Refactor error handling. Work towards better debug/log/stack trace options for error cases. --- src/core/error-handler.js | 174 +++++++++++++++++++++----------------- src/core/status-codes.js | 3 +- src/verbs/build.js | 77 +++++++++++++---- test/test-cli.js | 64 ++++++-------- 4 files changed, 183 insertions(+), 135 deletions(-) diff --git a/src/core/error-handler.js b/src/core/error-handler.js index dadfae8..b29e4e3 100644 --- a/src/core/error-handler.js +++ b/src/core/error-handler.js @@ -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 + }; + } + + + }()); diff --git a/src/core/status-codes.js b/src/core/status-codes.js index f6d1d71..2e3d72e 100644 --- a/src/core/status-codes.js +++ b/src/core/status-codes.js @@ -18,7 +18,8 @@ Status codes for HackMyResume. createNameMissing: 8, wkhtmltopdf: 9, missingPackageJSON: 10, - invalid: 11 + invalid: 11, + invalidTarget: 12 }; }()); diff --git a/src/verbs/build.js b/src/verbs/build.js index 7b5c012..d68f546 100644 --- a/src/verbs/build.js +++ b/src/verbs/build.js @@ -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"]. diff --git a/test/test-cli.js b/test/test-cli.js index a479580..68577c5 100644 --- a/test/test-cli.js +++ b/test/test-cli.js @@ -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(); }); }