├── ContextManager ├── ContextManager.js └── README.md ├── FormatLogger ├── FormatLogger.js └── README.md ├── LICENSE ├── Logger └── Logger.js ├── Properties ├── Properties.js ├── PropertiesUnitTests.js └── README.md ├── README.md ├── UnitTesting ├── README.md ├── UnitTesting.js └── UnitTestingTests.js └── VerboseErrors ├── README.md └── VerboseErrors.js /ContextManager/ContextManager.js: -------------------------------------------------------------------------------- 1 | (function (__g__) { 2 | // private stuff 3 | const _settings_ = Symbol('settings'); 4 | const _state_ = Symbol('state'); 5 | 6 | let parseSettings = function (opt) { 7 | opt = opt || {}; 8 | opt.param = opt.param || null; 9 | opt.enter = opt.enter || function () {}; 10 | opt.exit = opt.exit || function () {}; 11 | opt.error = opt.error || function () {}; 12 | opt.proxy = opt.proxy || false; 13 | return opt; 14 | } 15 | 16 | class ContextManager { 17 | 18 | constructor () { 19 | // default settings 20 | this[_settings_] = parseSettings(); 21 | } 22 | 23 | get settings () { 24 | return this[_settings_]; 25 | } 26 | 27 | set enter (func) { 28 | this[_settings_].enter = func; 29 | } 30 | 31 | set exit (func) { 32 | this[_settings_].exit = func; 33 | } 34 | 35 | set error (func) { 36 | this[_settings_].error = func; 37 | } 38 | 39 | set param (obj) { 40 | this[_settings_].param = obj; 41 | } 42 | 43 | set state (obj) { 44 | if (obj === null) 45 | this[_state_] = this.defaultObject; 46 | else 47 | this[_state_] = obj; 48 | } 49 | 50 | get state () { 51 | return this[_state_]; 52 | } 53 | 54 | defaultObject () { 55 | return {}; 56 | } 57 | 58 | with (func) { 59 | var param, result, state; 60 | 61 | this[_state_] = state = this.defaultObject(); 62 | 63 | // get the parameter 64 | param = this[_settings_].param; 65 | 66 | // execute the enter function 67 | this[_settings_].enter.call(state); 68 | 69 | try { 70 | 71 | // bind it so we can access via `this` // execute the body 72 | result = func.call(state, param); 73 | 74 | } catch (err) { 75 | // execute the error handler 76 | // error handler can return null to indicate it should be swallowed 77 | let swallow = this[_settings_].error.call(state, err) === null; 78 | 79 | // if error happened, call error function 80 | // if it returns null swallow it, otherwise reraise 81 | if (!swallow) 82 | throw (err); 83 | 84 | } finally { 85 | 86 | // execute the exit 87 | this[_settings_].exit.call(state); 88 | } 89 | 90 | return result; 91 | } 92 | } 93 | 94 | // export 95 | __g__.ContextManager = ContextManager; 96 | })(this); 97 | -------------------------------------------------------------------------------- /ContextManager/README.md: -------------------------------------------------------------------------------- 1 | # ContextManager 2 | 3 | Have some piece of code execute when entering a code block, execute the code block, and have another piece of code execute when exiting the block. Refer to state with `this` throughout the whole context. 4 | 5 | Optionally swallow errors if they occur. 6 | 7 | ## Getting Started 8 | 9 | Copy and paste the code to your project. Use it. 10 | 11 | The following silly example illustrates how `this` holds state throughout each of the functions: 12 | 13 | ```js 14 | function myFunction () { 15 | // initialize 16 | let ctx = new ContextManager(); 17 | 18 | // save an array to state 19 | ctx.enter = function () { 20 | this.noun = "World"; 21 | this.log = []; 22 | }; 23 | 24 | // output the array to the logger 25 | ctx.exit = function () { 26 | Logger.log(this.log.join('\n')); 27 | }; 28 | 29 | // execute the main body, enter and exit will be called 30 | ctx.with(function () { 31 | this.log.push(`Hello ${this.noun}`); 32 | }); 33 | 34 | // Logger outputs "Hello World" 35 | } 36 | ``` 37 | 38 | By default, `this` is just a regular object. If you want it to be something else, then subclass: 39 | 40 | ```js 41 | function myFunction () { 42 | class MyManager extends ContextManager { 43 | defaultObject () { 44 | return {log:[]}; // `this` is now an object with log array already 45 | } 46 | } 47 | 48 | let ctx = new MyManager(); 49 | 50 | ctx.enter = function () { 51 | this.log.push('entering'); 52 | }; 53 | ctx.exit = function () { 54 | this.log.push('exiting'); 55 | Logger.log(this.log.join('\n')); 56 | }; 57 | ctx.error = function () { 58 | this.log.push('See no error, hear no error'); 59 | return null; // return null swallows the error 60 | }; 61 | ctx.param = "World"; 62 | 63 | ctx.with(function (text) { 64 | this.log.push('Inside body'); 65 | throw Error("Error here, but does not actually error out"); 66 | }); 67 | } 68 | ``` 69 | 70 | Output: 71 | 72 | ``` 73 | entering 74 | Inside body 75 | See no error, hear no error 76 | exiting 77 | ``` 78 | 79 | ## Movitation 80 | 81 | Context managers are a concept in Python that is really quite useful. In my case, I use them to implement unit testing, as a unit test may possibly fail, but we want the code to continue executing. 82 | 83 | -------------------------------------------------------------------------------- /FormatLogger/FormatLogger.js: -------------------------------------------------------------------------------- 1 | (function (__g__) { 2 | /* 3 | Template tag 4 | Usage __log__`${variables}`; 5 | */ 6 | function __log__(strings, ...values) { 7 | const output = strings.map( (string, index) => `${string}${values[index] ? values[index] : ''}` ); 8 | Logger.log(output.join('')); 9 | } 10 | 11 | function configure (config) { 12 | config = config || {}; 13 | config.useLogger = config.useLogger || false; 14 | config.transformers = config.transformers || {}; 15 | config.defaultTransformString = config.defaultTransformString || "{0}"; 16 | config.pprintNewlines = config.pprintNewlines || true; 17 | config.pprintWhitespace = config.pprintWhitespace || 4; 18 | if (config.useLogger) 19 | config.loggerObject = Logger; 20 | else 21 | config.loggerObject = console; 22 | return config; 23 | } 24 | 25 | /* 26 | 27 | */ 28 | function action(options) { 29 | 30 | __g__.__log__ = __log__; 31 | 32 | let config = configure(options); 33 | 34 | // ValueError :: String -> Error 35 | var ValueError = function(message) { 36 | var err = new Error(message); 37 | err.name = 'ValueError'; 38 | return err; 39 | }; 40 | 41 | // defaultTo :: a,a? -> a 42 | var defaultTo = function(x, y) { 43 | return y == null ? x : y; 44 | }; 45 | 46 | // create :: Object -> String,*... -> String 47 | var create = function() { 48 | 49 | return function(template) { 50 | var args = Array.prototype.slice.call(arguments, 1); 51 | var idx = 0; 52 | var state = 'UNDEFINED'; 53 | 54 | return template.replace( 55 | /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, 56 | function(match, literal, key, xf) { 57 | if (literal != null) { 58 | return literal; 59 | } 60 | if (key.length > 0) { 61 | if (state === 'IMPLICIT') { 62 | throw ValueError('cannot switch from ' + 63 | 'implicit to explicit numbering'); 64 | } 65 | state = 'EXPLICIT'; 66 | } else { 67 | if (state === 'EXPLICIT') { 68 | throw ValueError('cannot switch from ' + 69 | 'explicit to implicit numbering'); 70 | } 71 | state = 'IMPLICIT'; 72 | key = String(idx); 73 | idx += 1; 74 | } 75 | var value = defaultTo('', lookup(args, key.split('.'))); 76 | if (xf == null) { 77 | return value; 78 | } else if (Object.prototype.hasOwnProperty.call(config.transformers, xf)) { 79 | return config.transformers[xf](value); 80 | } else { 81 | throw ValueError('no transformer named "' + xf + '"'); 82 | } 83 | } 84 | ); 85 | }; 86 | }; 87 | 88 | var lookup = function(obj, path) { 89 | if (!/^\d+$/.test(path[0])) { 90 | path = ['0'].concat(path); 91 | } 92 | for (var idx = 0; idx < path.length; idx += 1) { 93 | var key = path[idx]; 94 | if (typeof obj[key] === 'function') 95 | obj = obj[key](); 96 | else 97 | obj = obj[key]; 98 | } 99 | return obj; 100 | }; 101 | 102 | Object.defineProperty(Object.prototype, 'stringify', { 103 | get: function () { 104 | return function (pretty) { 105 | pretty = pretty || false; 106 | if (pretty) 107 | return (config.pprintNewlines ? "\n" : "") + 108 | config.defaultTransformString.__format__(JSON.stringify(this, null, config.pprintWhitespace), this); 109 | else 110 | return config.defaultTransformString.__format__(JSON.stringify(this), this); 111 | } 112 | }, 113 | configurable: true, 114 | enumerable: false, 115 | }); 116 | 117 | Object.defineProperty(Object.prototype, 'typeof_', { 118 | get: function () { 119 | var result = typeof this; 120 | switch (result) { 121 | case 'string': 122 | break; 123 | case 'boolean': 124 | break; 125 | case 'number': 126 | break; 127 | case 'object': 128 | case 'function': 129 | switch (this.constructor) { 130 | case new String().constructor: 131 | result = 'String'; 132 | break; 133 | case new Boolean().constructor: 134 | result = 'Boolean'; 135 | break; 136 | case new Number().constructor: 137 | result = 'Number'; 138 | break; 139 | case new Array().constructor: 140 | result = 'Array'; 141 | break; 142 | case new RegExp().constructor: 143 | result = 'RegExp'; 144 | break; 145 | case new Date().constructor: 146 | result = 'Date'; 147 | break; 148 | case Function: 149 | result = 'Function'; 150 | break; 151 | default: 152 | result = this.constructor.toString(); 153 | var m = this.constructor.toString().match(/function\s*([^( ]+)\(/); 154 | if (m) 155 | result = m[1]; 156 | else 157 | result = this.constructor.name; // it's an ES6 class, use name of constructor 158 | break; 159 | } 160 | break; 161 | } 162 | return result.substr(0, 1).toUpperCase() + result.substr(1); 163 | }, 164 | configurable: true, 165 | enumerable: false, 166 | }); 167 | 168 | Object.defineProperty(Object.prototype, 'print', { 169 | get: function () { 170 | return this.stringify(false); 171 | }, 172 | configurable: true, 173 | enumerable: false, 174 | }); 175 | 176 | Object.defineProperty(Object.prototype, '__print__', { 177 | get: function () { 178 | config.loggerObject.log.call(config.loggerObject, this.stringify(false) ); 179 | }, 180 | configurable: true, 181 | enumerable: false, 182 | }); 183 | 184 | Object.defineProperty(Object.prototype, 'pprint', { 185 | get: function () { 186 | return this.stringify(true); 187 | }, 188 | configurable: true, 189 | enumerable: false, 190 | }); 191 | 192 | Object.defineProperty(Object.prototype, '__pprint__', { 193 | get: function () { 194 | config.loggerObject.log.call(config.loggerObject, this.stringify(true) ); 195 | }, 196 | configurable: true, 197 | enumerable: false, 198 | }); 199 | 200 | Object.defineProperty(String.prototype, '__log__', { 201 | get: function () { 202 | return function() { 203 | config.loggerObject.log.call(config.loggerObject, this.__format__.apply(this, Array.prototype.slice.call(arguments)) ); 204 | }.bind(this); 205 | }, 206 | configurable: true, 207 | enumerable: false, 208 | }); 209 | 210 | Object.defineProperty(String.prototype, '__error__', { 211 | get: function () { 212 | return function() { 213 | config.loggerObject.error.call(config.loggerObject, this.__format__.apply(this, Array.prototype.slice.call(arguments)) ); 214 | }.bind(this); 215 | }, 216 | configurable: true, 217 | enumerable: false, 218 | }); 219 | 220 | Object.defineProperty(String.prototype, '__info__', { 221 | get: function () { 222 | return function() { 223 | config.loggerObject.info.call(config.loggerObject, this.__format__.apply(this, Array.prototype.slice.call(arguments)) ); 224 | }.bind(this); 225 | }, 226 | configurable: true, 227 | enumerable: false, 228 | }); 229 | 230 | Object.defineProperty(String.prototype, '__warn__', { 231 | get: function () { 232 | return function() { 233 | config.loggerObject.warn.call(config.loggerObject, this.__format__.apply(this, Array.prototype.slice.call(arguments)) ); 234 | }.bind(this); 235 | }, 236 | configurable: true, 237 | enumerable: false, 238 | }); 239 | 240 | Object.defineProperty(String.prototype, '__format__', { 241 | get: function () { 242 | var $format = create(config.transformers); 243 | return function () { 244 | var args = Array.prototype.slice.call(arguments); 245 | args.unshift(this); 246 | return $format.apply(__g__, args); 247 | } 248 | }, 249 | configurable: true, 250 | enumerable: false, 251 | }); 252 | } 253 | 254 | class FormatLogger { 255 | 256 | constructor (options) { 257 | // execute with some options 258 | action(options); 259 | } 260 | 261 | static init () { 262 | // execute with default options 263 | action({ 264 | useLogger: true, // uses Logger.log, false for console.log 265 | defaultTransformString: '<{0}> ({1.typeof_})', 266 | pprintNewLines: false 267 | }); 268 | } 269 | 270 | } 271 | 272 | __g__.FormatLogger = FormatLogger; 273 | 274 | 275 | })(this); 276 | -------------------------------------------------------------------------------- /FormatLogger/README.md: -------------------------------------------------------------------------------- 1 | # FormatLogger.gs 2 | 3 | Make templated strings and log output with apps scripting a cinch. 4 | 5 | ## FormatLogger.gs Quickstart 6 | 7 | Copy and paste the code, initialize so that it is live. This augments the String.prototype and Object.prototype, and adds `__log__` to the global scope. 8 | 9 | ```js 10 | FormatLogger.init(); 11 | const greeting = 'Hello'; 12 | const noun = 'World'; 13 | __log__`${greeting}, ${noun}.` 14 | ``` 15 | 16 | outputs: 17 | 18 | ``` 19 | Hello, World. 20 | ``` 21 | 22 | Use `__log__` to quickly output to console.log. If you want to include type info and better formating: 23 | 24 | ```js 25 | const example = [ 26 | { 27 | greeting: 'Hello', 28 | noun: 'World' 29 | },{ 30 | greeting: '你好', 31 | noun: '世界' 32 | } 33 | ]; 34 | FormatLogger.init(); 35 | __log__`${example.__pprint__}`; 36 | ``` 37 | 38 | ouptuts 39 | 40 | ``` 41 | <[ 42 | { 43 | "greeting": "Hello", 44 | "noun": "World" 45 | }, 46 | { 47 | "greeting": "你好", 48 | "noun": "世界" 49 | } 50 | ]> (Array) 51 | ``` 52 | 53 | If you remove FormatLogger.init() from the code, __log__ will raise error, allowing the developer to ensure all such unneeded "print" statements are removed from production code. 54 | 55 | There are additional features, outlined below: 56 | 57 | 58 | ```js 59 | FormatLogger.init(); 60 | var obj = {verb: 'Hello', noun: 'World'}; 61 | var arr = ['hello', 'world']; 62 | var helloWorld = "{verb}, {noun}".format(obj); 63 | obj.__print__; 64 | obj.__pprint__; // pretty print with whitespace 65 | arr.__pprint__; 66 | ``` 67 | 68 | The output (minus log info): 69 | 70 | ``` 71 | <{"verb":"Hello","noun":"World"}> (Object) 72 | <{ 73 | "verb": "Hello", 74 | "noun": "World" 75 | }> (Object) 76 | <[ 77 | "hello", 78 | "world" 79 | ]> (Array) 80 | ``` 81 | 82 | A more versatile method is also available, `__log__` that allows you to combine objects, and even reference properties: 83 | 84 | ```js 85 | "{0.print}\n{1.print}".__log__(obj); 86 | "{0.typeof_} of length {0.length}".__log__(arr); 87 | ``` 88 | 89 | Output: 90 | 91 | ``` 92 | <{"verb":"Hello","noun":"World"}> (Object) 93 | <["hello","world"]> (Array) 94 | Array of length 2 95 | ``` 96 | 97 | Under the hood all this is implemented with `String.prototype.format`, which works like so: 98 | 99 | ```js 100 | "{hello}, {world}".__format__({hello:'hello', world:'world'}); 101 | ``` 102 | 103 | 104 | ## Motivation 105 | 106 | There is a certain premium on being able to log output and reason about one's own code. Stackdriver logging is a massive improvement for the stack in so many ways, but for this individual sitting behind the keyboard there is nothing like having fast ways to do two things: 107 | 108 | 1. Templated strings 109 | 2. Output templated strings and objects to an instant log 110 | 111 | It is intended that this library may well be removed from the final product, or even well before that; this is why all the methods have double underlines: It makes them easy to find for removal. 112 | 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Classroom Tech Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Logger/Logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Drop-in replacement for Logger.log that uses a spreadsheet backend instead. 3 | * Copyrite Adam Morris classroomtechtools.ctt@gmail.com 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software 5 | * without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | * persons to whom the Software is furnished to do so, subject to the following conditions: 7 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 9 | * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 10 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | 1) Requires V8 runtime 13 | 2) Copy and paste code into project 14 | 3) When auto is true (default), Logger.log will go to spreadsheet instead, much quicker 15 | * optionally set auto to false and use ReplaceLogger() 16 | 4) View the log which will output the url of the spreadsheet created / used 17 | 5) Same spreadsheet is reused on suqsequent runs 18 | 6) If you want to specify which spreadsheet to output logs to, set auto to false and: 19 | ReplaceLogger(id=''); 20 | Optionally also provide tab name: 21 | ReplaceLogger(id='', sheet=''); 22 | 23 | * Details: 24 | // Copy and paste code into project. (Why not make it a proper library? Because to make it a drop-in replacement, it needs to have access to the global scope, which a library doesn't have) 25 | // use Logger.log as normal (when auto is true, else you need call to ReplaceLogger) 26 | Logger.log('Outputs to spreadsheet'); 27 | // objects passed to Logger.log are pretty printed 28 | Logger.log({hi: 'hi', arr: ['wow', 234343]}); 29 | // optionally, use Logger and string literals 30 | const where = 'spreadsheet'; 31 | Logger`this will also output to ${where}`; 32 | 33 | */ 34 | 35 | (function (__g__) { 36 | 37 | // This will replace Logger. If set to false, you'll have to initialize the library with call to ReplaceLogger() 38 | const auto = true; 39 | 40 | // If for some reason you want to use a different spreadsheet each time, or for just one execution, can set reset to true 41 | const reset = false; 42 | 43 | // Shouldn't need to change this 44 | const PROP = '__getLogger__.id'; 45 | const PROPSERVICE = PropertiesService.getUserProperties; 46 | 47 | function _getErrorObject(){ 48 | try { throw new Error('fake error') } catch(err) { return err; } 49 | } 50 | 51 | function _getLineNumOfCallee() { 52 | const err = _getErrorObject(); 53 | const target_stack = err.stack.split("\n").slice(5); // 5 because that's where it is in the stack 54 | //const index = caller_line.indexOf("at "); 55 | return '→ ' + target_stack.join("\n"); 56 | } 57 | 58 | class SS { 59 | constructor (id=null, sheetName) { 60 | this.id = id; 61 | this.sheetName = sheetName; 62 | this.spreadsheet = null; 63 | if (this.id === null) 64 | this.create(); 65 | else 66 | this.open(); 67 | } 68 | 69 | static new (...args) { 70 | return new SS(...args); 71 | } 72 | 73 | create() { 74 | const [rows, cols] = [3, 2]; 75 | this.spreadsheet = SpreadsheetApp.create('Logger', rows, cols); 76 | this.sheet = this.spreadsheet.getSheetByName(this.sheetName); 77 | this.id = this.spreadsheet.getId(); 78 | this.sheet.getRange(1, 1, 1, 3).setValues([['Output', 'Location', 'Date']]); 79 | this.first(); 80 | } 81 | 82 | open() { 83 | this.spreadsheet = SpreadsheetApp.openById(this.id); 84 | this.sheet = this.spreadsheet.getSheetByName(this.sheetName); 85 | this.id = this.spreadsheet.getId(); 86 | this.first(); 87 | } 88 | 89 | first () { 90 | // draw a line whenever we've opened the file 91 | this.sheet.getRange(2, 1, 1, 3).setBorder(true, null, null, null, null, null, '#000000', SpreadsheetApp.BorderStyle.SOLID); 92 | } 93 | 94 | prepend(text) { 95 | // first column of row should contain plaintext, or stringified version of itself 96 | const cell_a = (function (txt) { 97 | if (text instanceof String || typeof text === 'string') 98 | return text; 99 | else 100 | return JSON.stringify(text, null, 4); 101 | })(text); 102 | 103 | const cell_b = _getLineNumOfCallee(); 104 | 105 | // second column of row should contain date in easy to read format 106 | const cell_c = (new Date()).toLocaleString(); 107 | 108 | const data = [ [cell_a, cell_b, cell_c] ]; 109 | 110 | this.sheet.insertRowsAfter(1, data.length); 111 | this.sheet.getRange(2, 1, data.length, data[0].length).setValues(data); 112 | } 113 | 114 | get url () { 115 | return this.spreadsheet.getUrl(); 116 | } 117 | 118 | } 119 | 120 | let ssObj = null; 121 | const state = {}; 122 | 123 | const __Logger__ = __g__.Logger; 124 | 125 | Logger_ = function (strings, ...values) { 126 | const text = strings.map( (string, index) => `${string}${values[index] ? values[index] : ''}` ); 127 | ssObj.prepend(text.join('')); 128 | }; 129 | Logger_.log = function (text) { 130 | ssObj.prepend(text); 131 | }; 132 | 133 | __g__.ReplaceLogger = function (id=null, sheet='Sheet1') { 134 | [state.id, state.sheet] = [id, sheet]; 135 | 136 | if (state.id === null) { 137 | // pull in from properties, if available, remains null if not 138 | const props = PROPSERVICE(); 139 | state.id = props.getProperty(PROP); 140 | } 141 | 142 | // either opens existing or creates new 143 | ssObj = SS.new(state.id, state.sheet); 144 | 145 | if (state.id === null) { 146 | state.id = ssObj.id; 147 | const props = PROPSERVICE(); 148 | props.setProperty(PROP, state.id); 149 | } 150 | 151 | // Output with link 152 | Logger.log("Find logs at the following url:"); 153 | Logger.log(`\n${ssObj.url}\n`); 154 | __g__.Logger = Logger_; 155 | }; 156 | 157 | __g__.UnreplaceLogger = function () { 158 | __g__.Logger = __Logger__; 159 | }; 160 | 161 | if (reset) { 162 | PROPSERVICE().deleteProperty(PROP); 163 | } 164 | 165 | if (auto) ReplaceLogger(); 166 | })(this); 167 | -------------------------------------------------------------------------------- /Properties/Properties.js: -------------------------------------------------------------------------------- 1 | (function(__g__) { 2 | 3 | const _config_ = Symbol('config'); 4 | function configure(config) { 5 | config = config || {jsons:true}; 6 | config.jsons = config.jsons == undefined ? true : config.jsons; 7 | config.dates = config.dates == undefined ? false : config.dates; 8 | if (config.dates && !config.jsons) throw Error("jsons needs to be true for dates: true to be meaningful"); 9 | if (Object.keys(config).length > 2) throw Error(`Unknown property: ${Object.keys(config)}`); 10 | return config; 11 | } 12 | 13 | class Utils { 14 | 15 | constructor (dates=true) { 16 | this.dates = dates; 17 | } 18 | 19 | static isSerializedDate(dateValue) { 20 | // Dates are serialized in TZ format, example: '1981-12-20T04:00:14.000Z'. 21 | const datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; 22 | return Utils.isString(dateValue) && datePattern.test(dateValue); 23 | } 24 | 25 | static isString(value) { 26 | return typeof value === 'string' || value instanceof String; 27 | } 28 | 29 | static dateReviver(key, value) { 30 | if (Utils.isSerializedDate(value)) { 31 | return new Date(value); 32 | } 33 | return value; 34 | } 35 | 36 | static dateReplacer(key, value) { 37 | if (value instanceof Date) { 38 | const timezoneOffsetInHours = -(this.getTimezoneOffset() / 60); //UTC minus local time 39 | const sign = timezoneOffsetInHours >= 0 ? '+' : '-'; 40 | const leadingZero = (Math.abs(timezoneOffsetInHours) < 10) ? '0' : ''; 41 | 42 | //It's a bit unfortunate that we need to construct a new Date instance 43 | //(we don't want _this_ Date instance to be modified) 44 | let correctedDate = new Date(this.getFullYear(), this.getMonth(), 45 | this.getDate(), this.getHours(), this.getMinutes(), this.getSeconds(), 46 | this.getMilliseconds()); 47 | correctedDate.setHours(this.getHours() + timezoneOffsetInHours); 48 | const iso = correctedDate.toISOString().replace('Z', ''); 49 | 50 | return iso + sign + leadingZero + Math.abs(timezoneOffsetInHours).toString() + ':00'; 51 | } 52 | return value; 53 | } 54 | 55 | static serialize(value, dates=true) { 56 | /* 57 | if (dates) 58 | return JSON.stringify(value, Utils.dateReplacer); // replacer not strictly required and is faster without 59 | */ 60 | return JSON.stringify(value); 61 | } 62 | 63 | static deserialize(value, dates=true) { 64 | if (dates) 65 | return JSON.parse(value, Utils.dateReviver); 66 | return JSON.parse(value); 67 | } 68 | 69 | } 70 | 71 | class Properties { 72 | 73 | constructor (instance, config) { 74 | this[_config_] = configure(config); 75 | this.instance = instance; 76 | } 77 | 78 | static scriptStore (config={}) { 79 | return new Properties(PropertiesService.getScriptProperties(), config); 80 | } 81 | 82 | static documentStore (config={}) { 83 | return new Properties(PropertiesService.getDocumentProperties(), config); 84 | } 85 | 86 | static userStore (config={}) { 87 | return new Properties(PropertiesService.getUserProperties(), config); 88 | } 89 | 90 | static get utils () { 91 | // return serialiser who knows what to do with dates, if on 92 | return Utils; 93 | } 94 | 95 | set (key, value) { 96 | if (this[_config_].jsons) value = Properties.utils.serialize(value, this[_config_].dates); 97 | else if (typeof value !== 'string') throw TypeError("non-string passed, turn on jsons?"); 98 | return this.instance.setProperty(key, value); 99 | } 100 | 101 | get (key) { 102 | let value = this.instance.getProperty(key); 103 | if (value === null || value === undefined) return null; // always return null when not present (or undefined?) 104 | if (this[_config_].jsons) { 105 | value = Properties.utils.deserialize(value, this[_config_].dates); 106 | } 107 | return value; 108 | } 109 | 110 | getKeys () { 111 | return this.instance.getKeys(); 112 | } 113 | 114 | getAll () { 115 | const keys = this.getKeys(); 116 | let properties = {}; 117 | for (let key of keys) { 118 | properties[key] = this.get(key); 119 | } 120 | return properties; 121 | } 122 | 123 | setProperties (properties) { 124 | // make a copy of properties 125 | var copied = {}; 126 | if (this[_config_].jsons) { 127 | for (let key in properties) { 128 | copied[key] = Properties.utils.serialize(properties[key], this[_config_].dates); 129 | } 130 | } 131 | return this.instance.setProperties(copied); 132 | } 133 | 134 | remove (key) { 135 | return this.instance.deleteProperty(key); 136 | } 137 | 138 | removeAll () { 139 | return this.instance.deleteAllProperties(); 140 | } 141 | 142 | } 143 | 144 | __g__.Properties = Properties; 145 | 146 | })(this); 147 | 148 | -------------------------------------------------------------------------------- /Properties/PropertiesUnitTests.js: -------------------------------------------------------------------------------- 1 | function test_Properties() { 2 | UnitTesting.init(); 3 | 4 | describe("initializaion via static methods", function () { 5 | 6 | it("initialize via three modes: script, user, and document stores", function () { 7 | var actual; 8 | actual = Properties.userStore(); 9 | assert.notUndefined({actual: actual}); 10 | actual = Properties.documentStore(); 11 | assert.notUndefined({actual: actual}); 12 | actual = Properties.scriptStore(); 13 | assert.notUndefined({actual: actual}); 14 | }); 15 | 16 | it("initing with date: false stores dates as ISO strings", function () { 17 | const lib = Properties.userStore({dates:false}); 18 | const expected = new Date(); 19 | lib.set('date', expected); 20 | const actual = lib.get('date'); 21 | assert.objectEquals({actual: actual, expected:expected.toISOString()}); 22 | }); 23 | 24 | it("initing with jsons: false but dates: true throws error", function () { 25 | assert.throwsError(function () { 26 | const lib = Properties.userStore({jsons: false, dates: true}); 27 | }); 28 | }); 29 | 30 | 31 | it("utils.serialize and utils.deseralize persists dates correctly with defaults", function () { 32 | const expected = {date: new Date()}; 33 | const serialized = Properties.utils.serialize(expected); 34 | const actual = Properties.utils.deserialize(serialized); 35 | assert.objectEquals({actual: actual, expected: expected}); 36 | }); 37 | 38 | }); 39 | 40 | describe("getting and setting values", function () { 41 | 42 | it("get and set persists jsons by default", function () { 43 | const lib = Properties.scriptStore(/* no params */); 44 | const expected = {key: 'value'}; 45 | lib.set('key', expected); 46 | const actual = lib.get('key'); 47 | assert.objectEquals({actual: actual, expected: expected}); 48 | }); 49 | 50 | it("get and set persists strings with jsons = false", function () { 51 | const lib = Properties.scriptStore({jsons:false}); 52 | const expected = 'string'; 53 | lib.set('key', expected); 54 | const actual = lib.get('key'); 55 | assert.equals({actual: actual, expected: expected}); 56 | }); 57 | 58 | it("trying to persist non-strings with jsons = false throws error", function () { 59 | const lib = Properties.scriptStore({jsons:false}); 60 | const expected = {obj:'obj'}; 61 | lib.remove('key'); 62 | assert.throwsTypeError(_ => lib.set('key', expected)); 63 | const actual = lib.get('key'); 64 | assert.null_({actual: actual}); 65 | }); 66 | 67 | it(".setProperties with an nested object with nested arrays, primitives, objects, and dates, and persists", function () { 68 | const lib = Properties.scriptStore({jsons: true, dates: true}); 69 | const expected = {arr: [1, 2, 4.3343433, "five"], obj: {prop:'prop', date: new Date()}}; 70 | lib.removeAll(); 71 | lib.setProperties(expected); 72 | //const keys = lib.getKeys(); 73 | const actual = lib.getAll(); 74 | assert.objectEquals({actual: actual, expected:expected}); 75 | }); 76 | 77 | }); 78 | 79 | 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /Properties/README.md: -------------------------------------------------------------------------------- 1 | # Properties 2 | 3 | Makes using `PropertyServices` in Google Apps Scripting a cinch. Can handle objects and maintains date values. 4 | 5 | Unit tests have all the gory details. 6 | 7 | ## Motivation 8 | 9 | AppsScripts' `PropertyServices` is one of easiest and best ways to maintain persistent state, such as application state or user preferences. Why not make it even easier? 10 | 11 | ## Quickstart 12 | 13 | Copy the code in `Properties.js` into you project, use it: 14 | 15 | ```js 16 | // initialize available stores, with sensible default values 17 | const lib = Properties.scriptStore(); // or 18 | const lib = Properties.userStore(); // or 19 | const lib = Properites.documentStore(); 20 | 21 | // set keys to values of any kind, including nested objects with dates! 22 | lib.set('key', 564); 23 | lib.set('obj', {nested: [1, 2, new Date(), 400.2, {hi='hi'}]}); 24 | 25 | // retrieve values with get 26 | const value = lib.get('obj') 27 | value.nested[4].hi; // 'hi' 28 | 29 | // retrieve keys with getKeys 30 | const keys = lib.getKeys(); // array 31 | 32 | // bulk update 33 | lib.setProperties({ 34 | key: 0, 35 | obj: {} 36 | }); 37 | 38 | // remove keys 39 | lib.remove('key'); // just the one 40 | lib.removeAll(); // all of them 41 | 42 | // get everything 43 | // (possibly very slow: goes through each key and stores onto new object) 44 | const props = lib.getAll(); // returns key value object 45 | ``` 46 | 47 | ## Features 48 | 49 | It works by serializing all objects. If you want to turn if off, because you just want the plain strings stored and don't want the performance penalty of converting them, initialize like this: 50 | 51 | ```js 52 | const lib = Properties.scriptStore({jsons: false}); 53 | lib.set('key', 'just a string, thanks'); 54 | ``` 55 | 56 | It also ensures that dates are stored and retrieved correctly, implemented by a `replacer` and `reviver` function passed to `JSON.stringify` and `JSON.parse`, respectively. This feature can also exact a not insignicant performance, so if the dev knows there will be dates, can turn it off. 57 | 58 | ```js 59 | const lib = Properties.scriptStore({jsons: true, dates: false}); 60 | ``` 61 | 62 | But initing the library like this is nonsensical and will throw an error: 63 | 64 | ```js 65 | const lib = Properties.scriptStore({jsons: false, dates: true}); 66 | ``` 67 | 68 | ## De-serializer 69 | 70 | Internally it uses a simple utility to ensure that dates are stored and retrieved properly. This functionality is also exposed via `utils` property: 71 | 72 | ```js 73 | const date = new Date(); 74 | const string = Properties.utils.serializer(date); 75 | const result = Properties.utils.deserializer(date); 76 | date.getTime() == result.getTime(); // true 77 | ``` 78 | 79 | If you using the serializer for convenience, but don't want the performance penalty of maintaining correct dates (because you know there aren't any dates), the second parameter can be set to false. 80 | 81 | ```js 82 | Properties.utils.serializer({obj:'some value'}, false); 83 | ``` 84 | 85 | ## Unit Tests 86 | 87 | To run them yourself, copy the code in this repo `UnitTests/UnitTesting.js` and add to project. Also add `Properties/PropertiesUnitTests.js` and then run `test_Properties` function. Logger output: 88 | 89 | ``` 90 | initializaion via static methods 91 | ✔ initialize via three modes: script, user, and document stores 92 | ✔ initing with date: false stores dates as ISO strings 93 | ✔ initing with jsons: false but dates: true throws error 94 | ✔ utils.serialize and utils.deseralize persists dates correctly with defaults 95 | 96 | getting and setting values 97 | ✔ get and set persists jsons by default 98 | ✔ get and set persists strings with jsons = false 99 | ✔ trying to persist non-strings with jsons = false throws error 100 | ✔ .setProperties with an nested object with nested arrays, primitives, objects, and dates, and persists 101 | 102 | ``` 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modularLibrariesV8 2 | Collection of libraries for Google AppsScripts with V8 engine 3 | -------------------------------------------------------------------------------- /UnitTesting/README.md: -------------------------------------------------------------------------------- 1 | # UnitTesting.gs 2 | 3 | Assertion and unit testing of modular libraries. 4 | 5 | ## UnitTesting.gs Quickstart 6 | 7 | Copy and paste the [code](https://github.com/classroomtechtools/modularLibraries.gs/blob/master/UnitTesting/UnitTesting.gs), and initialize it so that it is live. 8 | 9 | ```js 10 | Import.UnitTesting.init(); // can be safely executed multiple times during execution 11 | 12 | describe("Tests", function () { 13 | it("This one fails", function () { 14 | assert.equals({ 15 | comment: 'If it fails, it displays in the log', 16 | expected: 'Yes', 17 | actual: 'No' 18 | }); 19 | }); 20 | }); 21 | ``` 22 | 23 | The output (minus log info): 24 | 25 | ``` 26 | Tests 27 | ✘ tests 28 | Error: Comment: This one fails -- Failure: Expected Yes but was No at pkg.utgs.Utgs:189 29 | at pkg.utgs.Utgs:157 30 | at pkg.utgs.Utgs:449 31 | at test:6 32 | at pkg.utgs.Utgs:263 33 | at pkg.utgs.Utgs:840 34 | at test:5 35 | at pkg.utgs.Utgs:263 36 | at pkg.utgs.Utgs:823 37 | at test:4 (myFunction) 38 | ``` 39 | 40 | List of available assertions. If there is a `{}` that means it is an object with `expected`, `actual` and optional `comment` properties. If `any` can be anything, if `func` must be a function. 41 | 42 | ```js 43 | assert.equals({}) 44 | assert.true_(any) 45 | assert.false_(any) 46 | assert.null_(any); 47 | assert.notNull(any) 48 | assert.undefined_(any) 49 | assert.notUndefined(any); 50 | assert.NaN_(any); 51 | assert.notNaN(any); 52 | assert.evaluatesToTrue(any); 53 | assert.evaluatesToFalse(any); 54 | 55 | assert.arrayEquals({}); 56 | assert.arrayEqualsIgnoringOrder({}); 57 | assert.objectEquals({}); 58 | assert.hashEquals({}); 59 | assert.roughlyEquals({}); // also tolerance property required 60 | assert.contains({value: any, collection: any}); 61 | 62 | assert.throwsError(func) 63 | assert.throwsTypeError(func) 64 | assert.throwsRangeError(func) 65 | assert.throwsReferenceError(func) 66 | 67 | assert.doesNotThrowError(func) 68 | 69 | 70 | ``` 71 | 72 | ## Unit tests! 73 | 74 | This package has unit tests on itself, which is also useful to check out how to use it. 75 | 76 | ## Motivation 77 | 78 | Unit testing is worth it. 79 | 80 | ## Thanks 81 | 82 | Much of the original code came from [GSUnit](https://sites.google.com/site/scriptsexamples/custom-methods/gsunit), with additional refactoring and the additional function assertions. 83 | -------------------------------------------------------------------------------- /UnitTesting/UnitTesting.js: -------------------------------------------------------------------------------- 1 | (function(__g__) { 2 | 3 | if (typeof __g__.assert != 'undefined') { 4 | // already added, don't do again 5 | return; 6 | } 7 | 8 | var _log = []; 9 | log = function (str) { 10 | _log.push(str); 11 | }; 12 | 13 | 14 | var UtgsUnit = {}; // private methods 15 | 16 | /** 17 | * For convenience, a variable that equals "undefined" 18 | */ 19 | var UtgsUnit_UNDEFINED_VALUE; 20 | 21 | /** 22 | * Predicate used for testing JavaScript == (i.e. equality excluding type) 23 | */ 24 | UtgsUnit.DOUBLE_EQUALITY_PREDICATE = (var1, var2) => var1 == var2; 25 | 26 | /** 27 | * Predicate used for testing JavaScript === (i.e. equality including type) 28 | */ 29 | UtgsUnit.TRIPLE_EQUALITY_PREDICATE = (var1, var2) => var1 === var2; 30 | 31 | /* 32 | * Predicate used for testing Javascript date equality 33 | */ 34 | UtgsUnit.DATE_EQUALITY_PREDICATE = (var1, var2) => var1.getTime() === var2.getTime(); 35 | 36 | 37 | /** 38 | * Predicate used for testing whether two obects' toStrings are equal 39 | */ 40 | UtgsUnit.TO_STRING_EQUALITY_PREDICATE = (var1, var2) => var1.toString() === var2.toString(); 41 | 42 | /** 43 | * Hash of predicates for testing equality by primitive type 44 | */ 45 | UtgsUnit.PRIMITIVE_EQUALITY_PREDICATES = { 46 | 'String': UtgsUnit.DOUBLE_EQUALITY_PREDICATE, 47 | 'Number': UtgsUnit.DOUBLE_EQUALITY_PREDICATE, 48 | 'Boolean': UtgsUnit.DOUBLE_EQUALITY_PREDICATE, 49 | 'Date': UtgsUnit.DATE_EQUALITY_PREDICATE, 50 | 'RegExp': UtgsUnit.TO_STRING_EQUALITY_PREDICATE, 51 | 'Function': UtgsUnit.TO_STRING_EQUALITY_PREDICATE 52 | } 53 | 54 | /** 55 | * @param Any object 56 | * @return String - the type of the given object 57 | * @private 58 | */ 59 | UtgsUnit.trueTypeOf = function(something) { 60 | var result = typeof something; 61 | try { 62 | switch (result) { 63 | case 'string': 64 | break; 65 | case 'boolean': 66 | break; 67 | case 'number': 68 | break; 69 | case 'object': 70 | case 'function': 71 | switch (something.constructor) { 72 | case new String().constructor: 73 | result = 'String'; 74 | break; 75 | case new Boolean().constructor: 76 | result = 'Boolean'; 77 | break; 78 | case new Number().constructor: 79 | result = 'Number'; 80 | break; 81 | case new Array().constructor: 82 | result = 'Array'; 83 | break; 84 | case new RegExp().constructor: 85 | result = 'RegExp'; 86 | break; 87 | case new Date().constructor: 88 | result = 'Date'; 89 | break; 90 | case Function: 91 | result = 'Function'; 92 | break; 93 | default: 94 | const m = something.constructor.toString().match(/function\s*([^( ]+)\(/); 95 | if (m) 96 | result = m[1]; 97 | else 98 | break; 99 | } 100 | break; 101 | } 102 | } 103 | finally { 104 | result = result.substr(0, 1).toUpperCase() + result.substr(1); 105 | return result; 106 | } 107 | } 108 | 109 | UtgsUnit.displayStringForValue = function(aVar) { 110 | let result = `<${aVar}>`; 111 | if (!(aVar === null || aVar === UtgsUnit_UNDEFINED_VALUE)) { 112 | result += ` (${UtgsUnit.trueTypeOf(aVar)})`; 113 | } 114 | return result; 115 | } 116 | 117 | UtgsUnit.validateArguments = function(opt, fields) { 118 | fields = fields.split(' '); 119 | for (let f=0; f < fields.length; f++) { 120 | if (!opt.hasOwnProperty(fields[f])) { 121 | throw UtgsUnit.AssertionArgumentError(`Assertions needs property ${fields[f]} in opt argument`); 122 | } 123 | } 124 | opt.comment = opt.comment || ''; 125 | } 126 | 127 | UtgsUnit.checkEquals = (var1, var2) => var1 === var2; 128 | 129 | UtgsUnit.checkNotUndefined = (aVar) => aVar !== UtgsUnit_UNDEFINED_VALUE; 130 | 131 | UtgsUnit.checkNotNull = (aVar) => aVar !== null; 132 | 133 | /** 134 | * All assertions ultimately go through this method. 135 | */ 136 | UtgsUnit.assert = function(comment, booleanValue, failureMessage) { 137 | if (!booleanValue) 138 | throw new UtgsUnit.Failure(comment, failureMessage); 139 | } 140 | 141 | 142 | /** 143 | * @class 144 | * A UtgsUnit.Failure represents an assertion failure (or a call to fail()) during the execution of a Test Function 145 | * @param comment an optional comment about the failure 146 | * @param message the reason for the failure 147 | */ 148 | UtgsUnit.Failure = function(comment, message) { 149 | /** 150 | * Declaration that this is a UtgsUnit.Failure 151 | * @ignore 152 | */ 153 | this.isUtgsUnitFailure = true; 154 | /** 155 | * An optional comment about the failure 156 | */ 157 | this.comment = comment; 158 | /** 159 | * The reason for the failure 160 | */ 161 | this.UtgsUnitMessage = message; 162 | /** 163 | * The stack trace at the point at which the failure was encountered 164 | */ 165 | // this.stackTrace = UtgsUnit.Util.getStackTrace(); 166 | 167 | let failComment = ''; 168 | if (comment != null) failComment = `Comment: ${comment}`; 169 | message = message || ''; 170 | throw Error(`${failComment}\n\t\t -- Failure: ${message}\n `); 171 | } 172 | 173 | 174 | /** 175 | * @class 176 | * A UtgsUnitAssertionArgumentError represents an invalid call to an assertion function - either an invalid argument type 177 | * or an incorrect number of arguments 178 | * @param description a description of the argument error 179 | */ 180 | UtgsUnit.AssertionArgumentError = function(description) { 181 | /** 182 | * A description of the argument error 183 | */ 184 | this.description = description; 185 | throw Error(`Argument error: ${description}`); 186 | } 187 | 188 | 189 | /** 190 | * @class 191 | * @constructor 192 | * Contains utility functions for the UtgsUnit framework 193 | */ 194 | UtgsUnit.Util = {}; 195 | try { 196 | UtgsUnit.Util.ContextManager = ContextManager; 197 | } catch(err) { 198 | throw Error("Please install ContextManager") 199 | } 200 | 201 | /** 202 | * Standardizes an HTML string by temporarily creating a DIV, setting its innerHTML to the string, and the asking for 203 | * the innerHTML back 204 | * @param html 205 | */ 206 | UtgsUnit.Util.standardizeHTML = function(html) { 207 | let translator = document.createElement("DIV"); 208 | translator.innerHTML = html; 209 | return UtgsUnit.Util.trim(translator.innerHTML); 210 | } 211 | 212 | /** 213 | * Returns whether the given string is blank after being trimmed of whitespace 214 | * @param string 215 | */ 216 | UtgsUnit.Util.isBlank = function(string) { 217 | return UtgsUnit.Util.trim(string) == ''; 218 | } 219 | 220 | /** 221 | * Returns the name of the given function, or 'anonymous' if it has no name 222 | * @param aFunction 223 | */ 224 | UtgsUnit.Util.getFunctionName = function(aFunction) { 225 | const regexpResult = aFunction.toString().match(/function(\s*)(\w*)/); 226 | if (regexpResult && regexpResult.length >= 2 && regexpResult[2]) { 227 | return regexpResult[2]; 228 | } 229 | return 'anonymous'; 230 | } 231 | 232 | /** 233 | * Returns the current stack trace 234 | */ 235 | UtgsUnit.Util.getStackTrace = function() { 236 | let result = ''; 237 | 238 | if (arguments.caller !== undefined) { 239 | for (let a = arguments.caller; a != null; a = a.caller) { 240 | result += `> ${UtgsUnit.Util.getFunctionName(a.callee)}\n`; 241 | if (a.caller == a) { 242 | result += `*`; 243 | break; 244 | } 245 | } 246 | } 247 | else { // Mozilla, not ECMA 248 | // fake an exception so we can get Mozilla's error stack 249 | try 250 | { 251 | foo.bar; 252 | } 253 | catch(exception) 254 | { 255 | const stack = UtgsUnit.Util.parseErrorStack(exception); 256 | for (let i = 1; i < stack.length; i++) 257 | { 258 | result += `> ${stack[i]}\n`; 259 | } 260 | } 261 | } 262 | 263 | return result; 264 | } 265 | 266 | /** 267 | * Returns an array of stack trace elements from the given exception 268 | * @param exception 269 | */ 270 | UtgsUnit.Util.parseErrorStack = function(exception) { 271 | let stack = []; 272 | 273 | if (!exception || !exception.stack) { 274 | return stack; 275 | } 276 | 277 | const stacklist = exception.stack.split('\n'); 278 | 279 | for (let i = 0; i < stacklist.length - 1; i++) { 280 | const framedata = stacklist[i]; 281 | 282 | let name = framedata.match(/^(\w*)/)[1]; 283 | if (!name) { 284 | name = 'anonymous'; 285 | } 286 | 287 | stack[stack.length] = name; 288 | } 289 | // remove top level anonymous functions to match IE 290 | 291 | while (stack.length && stack[stack.length - 1] == 'anonymous') { 292 | stack.length = stack.length - 1; 293 | } 294 | return stack; 295 | } 296 | 297 | /** 298 | * Strips whitespace from either end of the given string 299 | * @param string 300 | */ 301 | UtgsUnit.Util.trim = function(string) { 302 | if (string == null) 303 | return null; 304 | 305 | let startingIndex = 0; 306 | let endingIndex = string.length - 1; 307 | 308 | const singleWhitespaceRegex = /\s/; 309 | while (string.substring(startingIndex, startingIndex + 1).match(singleWhitespaceRegex)) 310 | startingIndex++; 311 | 312 | while (string.substring(endingIndex, endingIndex + 1).match(singleWhitespaceRegex)) 313 | endingIndex--; 314 | 315 | if (endingIndex < startingIndex) 316 | return ''; 317 | 318 | return string.substring(startingIndex, endingIndex + 1); 319 | } 320 | 321 | UtgsUnit.Util.getKeys = function(obj) { 322 | let keys = []; 323 | for (const key in obj) { 324 | keys.push(key); 325 | } 326 | return keys; 327 | } 328 | 329 | // private function here that makes context managers 330 | //: 331 | 332 | UtgsUnit.Util.inherit = function(superclass, subclass) { 333 | var x = function() {}; 334 | x.prototype = superclass.prototype; 335 | subclass.prototype = new x(); 336 | } 337 | 338 | __g__.assert = { 339 | 340 | FailError: UtgsUnit.Failure, 341 | 342 | contextManager: UtgsUnit.Util.ContextManager, 343 | 344 | /** 345 | * Checks that two values are equal (using ===) 346 | * @param {String} comment optional, displayed in the case of failure 347 | * @param {Value} expected the expected value 348 | * @param {Value} actual the actual value 349 | * @throws UtgsUnit.Failure if the values are not equal 350 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 351 | */ 352 | equals: function (opt) { 353 | UtgsUnit.validateArguments(opt, 'expected actual'); 354 | UtgsUnit.assert(opt.comment, UtgsUnit.checkEquals(opt.expected, opt.actual), `Expected ${opt.expected} but was ${opt.actual}`); 355 | }, 356 | 357 | 358 | /** 359 | * Checks that the given boolean value is true. 360 | * @param {String} comment optional, displayed in the case of failure 361 | * @param {Boolean} value that is expected to be true 362 | * @throws UtgsUnit.Failure if the given value is not true 363 | * @throws UtgsUnitInvalidAssertionArgument if the given value is not a boolean or if an incorrect number of arguments is passed 364 | */ 365 | assert: function (opt) { 366 | UtgsUnit.validateArguments(opt, 'actual'); 367 | if (typeof(opt.actual) !== 'boolean') 368 | throw new UtgsUnit.AssertionArgumentError('Bad argument to assert(boolean)'); 369 | 370 | UtgsUnit.assert(opt.comment, opt.actual === true, 'Call to assert(boolean) with false'); 371 | }, 372 | 373 | 374 | /** 375 | * Synonym for true_ 376 | * @see #assert 377 | */ 378 | true_: function (opt) { 379 | this.assert(opt); 380 | }, 381 | 382 | /** 383 | * Checks that a boolean value is false. 384 | * @param {String} comment optional, displayed in the case of failure 385 | * @param {Boolean} value that is expected to be false 386 | * @throws UtgsUnit.Failure if value is not false 387 | * @throws UtgsUnitInvalidAssertionArgument if the given value is not a boolean or if an incorrect number of arguments is passed 388 | */ 389 | false_: function (opt) { 390 | UtgsUnit.validateArguments(opt, 'actual'); 391 | 392 | if (typeof(opt.actual) !== 'boolean') 393 | throw new UtgsUnit.AssertionArgumentError('Bad argument to false_(boolean)'); 394 | 395 | UtgsUnit.assert(opt.comment, opt.actual === false, 'Call to false_(boolean) with true'); 396 | }, 397 | 398 | /** 399 | * Checks that two values are not equal (using !==) 400 | * @param {String} comment optional, displayed in the case of failure 401 | * @param {Value} value1 a value 402 | * @param {Value} value2 another value 403 | * @throws UtgsUnit.Failure if the values are equal 404 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 405 | */ 406 | notEqual: function (opt) { 407 | UtgsUnit.validateArguments(opt, 'expected actual'); 408 | UtgsUnit.assert(opt.comment, opt.expected !== opt.actual, `Expected not to be ${opt.expected}`); 409 | }, 410 | 411 | /** 412 | * Checks that a value is null 413 | * @param {opt} 414 | * @throws UtgsUnit.Failure if the value is not null 415 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 416 | */ 417 | null_: function (opt) { 418 | UtgsUnit.validateArguments(opt, 'actual'); 419 | UtgsUnit.assert(opt.comment, opt.actual === null, `Expected ${UtgsUnit.displayStringForValue(null)} but was ${opt.actual}`); 420 | }, 421 | 422 | /** 423 | * Checks that a value is not null 424 | * @param {opt} value the value 425 | * @throws UtgsUnit.Failure if the value is null 426 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 427 | */ 428 | notNull: function(opt) { 429 | UtgsUnit.validateArguments(opt, 'actual'); 430 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotNull(opt.actual), `Expected not to be ${UtgsUnit.displayStringForValue(null)}`); 431 | }, 432 | 433 | /** 434 | * Checks that a value is undefined 435 | * @param {opt} 436 | * @throws UtgsUnit.Failure if the value is not undefined 437 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 438 | */ 439 | undefined_: function (opt) { 440 | UtgsUnit.validateArguments(opt, 'actual'); 441 | UtgsUnit.assert(opt.comment, opt.actual === UtgsUnit_UNDEFINED_VALUE, `Expected ${UtgsUnit.displayStringForValue(UtgsUnit_UNDEFINED_VALUE)} but was ${UtgsUnit.displayStringForValue(opt.actual)}`); 442 | }, 443 | 444 | /** 445 | * Checks that a value is not undefined 446 | * @param {opt} comment optional, displayed in the case of failure 447 | * @throws UtgsUnit.Failure if the value is undefined 448 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 449 | */ 450 | notUndefined: function (opt) { 451 | UtgsUnit.validateArguments(opt, 'actual'); 452 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotUndefined(opt.actual), `Expected not to be ${UtgsUnit.displayStringForValue(UtgsUnit_UNDEFINED_VALUE)}`); 453 | }, 454 | 455 | /** 456 | * Checks that a value is NaN (Not a Number) 457 | * @param {opt} comment optional, displayed in the case of failure 458 | * @throws UtgsUnit.Failure if the value is a number 459 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 460 | */ 461 | NaN_: function (opt) { 462 | UtgsUnit.validateArguments(opt, 'actual'); 463 | UtgsUnit.assert(opt.comment, isNaN(opt.actual), 'Expected NaN'); 464 | }, 465 | 466 | /** 467 | * Checks that a value is not NaN (i.e. is a number) 468 | * @param {String} comment optional, displayed in the case of failure 469 | * @param {Number} value the value 470 | * @throws UtgsUnit.Failure if the value is not a number 471 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 472 | */ 473 | notNaN: function (opt) { 474 | UtgsUnit.validateArguments(opt, 'actual'); 475 | UtgsUnit.assert(opt.comment, !isNaN(opt.actual), 'Expected not NaN'); 476 | }, 477 | 478 | /** 479 | * Checks that an object is equal to another using === for primitives and their object counterparts but also desceding 480 | * into collections and calling objectEquals for each element 481 | * @param {Object} opt 482 | * @throws UtgsUnit.Failure if the actual value does not equal the expected value 483 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 484 | */ 485 | objectEquals: function (opt) { 486 | UtgsUnit.validateArguments(opt, 'expected actual'); 487 | if (opt.expected === opt.actual) 488 | return; 489 | 490 | let isEqual = false; 491 | 492 | const typeOfVar1 = UtgsUnit.trueTypeOf(opt.expected); 493 | const typeOfVar2 = UtgsUnit.trueTypeOf(opt.actual); 494 | if (typeOfVar1 == typeOfVar2) { 495 | const primitiveEqualityPredicate = UtgsUnit.PRIMITIVE_EQUALITY_PREDICATES[typeOfVar1]; 496 | if (primitiveEqualityPredicate) { 497 | isEqual = primitiveEqualityPredicate(opt.expected, opt.actual); 498 | } else { 499 | const expectedKeys = UtgsUnit.Util.getKeys(opt.expected).sort().join(", "); 500 | const actualKeys = UtgsUnit.Util.getKeys(opt.actual).sort().join(", "); 501 | if (expectedKeys != actualKeys) { 502 | UtgsUnit.assert(opt.comment, false, `Expected keys ${expectedKeys} but found ${actualKeys}`); 503 | } 504 | for (const i in opt.expected) { 505 | this.objectEquals({comment: `{opt.comment} nested ${typeOfVar1} key ${i}\n`, 506 | expected:opt.expected[i], 507 | actual:opt.actual[i]}); 508 | } 509 | isEqual = true; 510 | } 511 | } 512 | UtgsUnit.assert(opt.comment, isEqual, `Expected ${UtgsUnit.displayStringForValue(opt.expected)} but was ${UtgsUnit.displayStringForValue(opt.actual)}`); 513 | }, 514 | 515 | /** 516 | * Checks that an array is equal to another by checking that both are arrays and then comparing their elements using objectEquals 517 | * @param {Object} 518 | * {Object.expected} value the expected array 519 | * {Object.actual} value the actual array 520 | * @throws UtgsUnit.Failure if the actual value does not equal the expected value 521 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 522 | */ 523 | arrayEquals: function (opt) { 524 | UtgsUnit.validateArguments(opt, 'expected actual'); 525 | if (UtgsUnit.trueTypeOf(opt.expected) != 'Array' || UtgsUnit.trueTypeOf(opt.actual) != 'Array') { 526 | throw new UtgsUnit.AssertionArgumentError('Non-array passed to arrayEquals'); 527 | } 528 | this.objectEquals(opt); 529 | }, 530 | 531 | /** 532 | * Checks that a value evaluates to true in the sense that value == true 533 | * @param {String} comment optional, displayed in the case of failure 534 | * @param {Value} value the value 535 | * @throws UtgsUnit.Failure if the actual value does not evaluate to true 536 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 537 | */ 538 | evaluatesToTrue: function (opt) { 539 | UtgsUnit.validateArguments(opt, 'actual'); 540 | if (!opt.actual) 541 | this.fail(opt.comment); 542 | }, 543 | 544 | /** 545 | * Checks that a value evaluates to false in the sense that value == false 546 | * @param {String} comment optional, displayed in the case of failure 547 | * @param {Value} value the value 548 | * @throws UtgsUnit.Failure if the actual value does not evaluate to true 549 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 550 | */ 551 | evaluatesToFalse: function (opt) { 552 | UtgsUnit.validateArguments(opt, 'actual'); 553 | if (opt.actual) 554 | this.fail(opt.comment); 555 | }, 556 | 557 | /** 558 | * Checks that a hash is has the same contents as another by iterating over the expected hash and checking that each 559 | * key's value is present in the actual hash and calling equals on the two values, and then checking that there is 560 | * no key in the actual hash that isn't present in the expected hash. 561 | * @param {String} comment optional, displayed in the case of failure 562 | * @param {Object} value the expected hash 563 | * @param {Object} value the actual hash 564 | * @throws UtgsUnit.Failure if the actual hash does not evaluate to true 565 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 566 | */ 567 | hashEquals: function (opt) { 568 | UtgsUnit.validateArguments(opt, 'actual expected'); 569 | for (const key in opt.expected) { 570 | this.notUndefined({comment: `Expected hash had key ${key} that was not found in actual`, 571 | actual:opt.actual[key]}); 572 | this.equals({comment:`Value for key ${key} mismatch -- expected = ${opt.expected[key]}, actual = ${opt.actual[key]}`, 573 | expected:opt.expected[key], 574 | actual:opt.actual[key]} 575 | ); 576 | } 577 | for (var key in opt.actual) { 578 | this.notUndefined({comment:`Actual hash had key ${key} that was not expected`, actual:opt.expected[key]}); 579 | } 580 | }, 581 | 582 | /** 583 | * Checks that two value are within a tolerance of one another 584 | * @param {String} comment optional, displayed in the case of failure 585 | * @param {Number} value1 a value 586 | * @param {Number} value1 another value 587 | * @param {Number} tolerance the tolerance 588 | * @throws UtgsUnit.Failure if the two values are not within tolerance of each other 589 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments is passed 590 | */ 591 | roughlyEquals: function (opt) { 592 | UtgsUnit.validateArguments(opt, 'actual expected tolerance'); 593 | this.true_({comment: `Expected ${opt.expected} but got ${opt.actual} which was more than ${opt.tolerance} away`, 594 | actual:Math.abs(opt.expected - opt.actual) < opt.tolerance}); 595 | }, 596 | 597 | /** 598 | * Checks that a collection contains a value by checking that collection.indexOf(value) is not -1 599 | * @param {Object} 600 | * @param {Object.collection} 601 | * @param {Object.value} 602 | * @throws UtgsUnit.Failure if the collection does not contain the value 603 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments are passed 604 | */ 605 | contains: function (opt) { 606 | UtgsUnit.validateArguments(opt, 'value collection'); 607 | this.true_({comment: `Expected ${opt.collection} to contain ${opt.value}`, 608 | actual: opt.collection.indexOf(opt.value) != -1}); 609 | }, 610 | 611 | /** 612 | * Checks that two arrays have the same contents, ignoring the order of the contents 613 | * @param {Object} 614 | * @param {Object.expected} array1 first array 615 | * @param {Object.actual} second array 616 | * @throws UtgsUnit.Failure if the two arrays contain different contents 617 | * @throws UtgsUnitInvalidAssertionArgument if an incorrect number of arguments are passed 618 | */ 619 | arrayEqualsIgnoringOrder: function(opt) { 620 | UtgsUnit.validateArguments(opt, 'expected actual'); 621 | 622 | const notEqualsMessage = `Expected arrays ${opt.expected} and ${opt.actual} to be equal (ignoring order)`; 623 | const notArraysMessage = `Expected arguments ${opt.expected} and ${opt.actual} to be arrays`; 624 | 625 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotNull(opt.expected), notEqualsMessage); 626 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotNull(opt.actual), notEqualsMessage); 627 | 628 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotUndefined(opt.expected.length), notArraysMessage); 629 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotUndefined(opt.expected.join), notArraysMessage); 630 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotUndefined(opt.actual.length), notArraysMessage); 631 | UtgsUnit.assert(opt.comment, UtgsUnit.checkNotUndefined(opt.actual.join), notArraysMessage); 632 | 633 | UtgsUnit.assert(opt.comment, UtgsUnit.checkEquals(opt.expected.length, opt.actual.length), notEqualsMessage); 634 | 635 | for (let i = 0; i < opt.expected.length; i++) { 636 | let found = false; 637 | for (let j = 0; j < opt.actual.length; j++) { 638 | try { 639 | this.objectEquals({comment: notEqualsMessage, 640 | expected:opt.expected[i], 641 | actual: opt.actual[j]}); 642 | found = true; 643 | } catch (ignored) { 644 | } 645 | } 646 | UtgsUnit.assert(opt.comment, found, notEqualsMessage); 647 | } 648 | }, 649 | 650 | throws: function (opt, func) { 651 | UtgsUnit.validateArguments(opt, 'expectedError'); 652 | if (typeof(func) !== 'function') throw UtgsUnit.Failure("Must have function"); 653 | let caughtError = false; 654 | 655 | try { 656 | func.call(); 657 | } catch (err) { 658 | caughtError = true; 659 | UtgsUnit.assert(opt.comment, err instanceof opt.expectedError, `Expected thrown error to be of type ${(opt.expectedError.name || opt.expectedError.toString())}`); 660 | } 661 | 662 | if (!caughtError) 663 | throw UtgsUnit.Failure("No error was thrown, expecting error of type '" + opt.expectedError.name); 664 | }, 665 | 666 | doesNotThrow: function (opt, func) { 667 | UtgsUnit.validateArguments(opt, 'unexpectedError'); 668 | if (typeof(func) !== 'function') throw UtgsUnit.Failure("Must have function"); 669 | 670 | try { 671 | func.call(); 672 | } catch (err) { 673 | UtgsUnit.assert(opt.comment, err instanceof opt.unexpectedError, "Did not expect to throw error of type " + opt.unexpectedError.name); 674 | } 675 | }, 676 | 677 | /* TODO: Fix the use of assert.result */ 678 | throwsError: function (comment, func) { 679 | const saved = assert.result; 680 | 681 | if (arguments.length == 1) { 682 | func = comment; 683 | comment = ''; 684 | } 685 | let ret = this.throws.call(this, {expectedError:Error}, func); 686 | if (assert.result == false && saved == true) { 687 | assert.result = true; 688 | } 689 | return ret; 690 | }, 691 | 692 | doesNotThrowError: function (comment, func) { 693 | if (arguments.length == 1) { 694 | func = comment; 695 | comment = ''; 696 | } 697 | return this.doesNotThrow.call(this, {unexpectedError: Error}, func); 698 | }, 699 | 700 | throwsTypeError: function (comment, func) { 701 | if (arguments.length == 1) { 702 | func = comment; 703 | comment = ''; 704 | } 705 | return this.throws.call(this, {expectedError: TypeError}, func); 706 | }, 707 | 708 | throwsRangeError: function (comment, func) { 709 | if (arguments.length == 1) { 710 | func = comment; 711 | comment = ''; 712 | } 713 | return this.throws.call(this, {expectedError: RangeError, 714 | comment:comment}, func); 715 | }, 716 | 717 | throwsReferenceError: function (comment, func) { 718 | if (arguments.length == 1) { 719 | func = comment; 720 | comment = ''; 721 | } 722 | return this.throws.call(this, {comment: comment, 723 | expectedError: ReferenceError}, func); 724 | }, 725 | 726 | describe: function (description, body) { 727 | let ctx = new UtgsUnit.Util.ContextManager(); 728 | ctx.enter = () => { _log = ['\n\n' + description]; }; 729 | ctx.exit = () => { 730 | _log.push('\n'); 731 | Logger.log(_log.join('\n')); 732 | _log = []; 733 | }; 734 | ctx.with(body); 735 | }, 736 | 737 | withContext: function (body, options) { 738 | let ctx = new UtgsUnit.Util.ContextManager(options); 739 | ctw.with(body); 740 | }, 741 | 742 | it: function (shouldMessage, body) { 743 | let ctx = new UtgsUnit.Util.ContextManager(); 744 | ctx.enter = function () { 745 | this.result = "\t✔ " + shouldMessage; 746 | }; 747 | ctx.error = function (err, obj) { 748 | this.result = '\t✘ ' + shouldMessage + '\n\t\t' + err + (err.stack ? err.stack : err.toString()); 749 | return null; 750 | }; 751 | ctx.exit = function (obj) { 752 | log(this.result); 753 | }; 754 | ctx.params = {}; 755 | //go 756 | ctx.with(body); 757 | }, 758 | 759 | skip: function (shouldMessage, body) { 760 | log("\t☛ " + shouldMessage + '... SKIPPED'); 761 | }, 762 | 763 | /** 764 | * Causes a failure 765 | * @param failureMessage the message for the failure 766 | */ 767 | fail: function (failureMessage) { 768 | throw new UtgsUnit.Failure("Call to fail()", failureMessage); 769 | }, 770 | } 771 | 772 | __g__.describe = assert.describe; 773 | __g__.it = assert.it; 774 | 775 | let _result_ = Symbol('result'); 776 | 777 | class UnitTesting { 778 | static set result (value) { 779 | this[_result_] = value; 780 | } 781 | 782 | static get result () { 783 | return this[_result_]; 784 | } 785 | 786 | static init () { 787 | // ValueError :: String -> Error 788 | const ValueError = function(message) { 789 | var err = new Error(message); 790 | err.name = 'ValueError'; 791 | return err; 792 | }; 793 | 794 | // defaultTo :: a,a? -> a 795 | const defaultTo = function(x, y) { 796 | return y == null ? x : y; 797 | }; 798 | 799 | // create :: Object -> String,*... -> String 800 | const create = function() { 801 | 802 | return function(template) { 803 | const args = Array.prototype.slice.call(arguments, 1); 804 | let idx = 0; 805 | let state = 'UNDEFINED'; 806 | 807 | return template.replace( 808 | /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, 809 | function(match, literal, key, xf) { 810 | if (literal != null) { 811 | return literal; 812 | } 813 | if (key.length > 0) { 814 | if (state === 'IMPLICIT') { 815 | throw ValueError('cannot switch from ' + 816 | 'implicit to explicit numbering'); 817 | } 818 | state = 'EXPLICIT'; 819 | } else { 820 | if (state === 'EXPLICIT') { 821 | throw ValueError('cannot switch from ' + 822 | 'explicit to implicit numbering'); 823 | } 824 | state = 'IMPLICIT'; 825 | key = String(idx); 826 | idx += 1; 827 | } 828 | const value = lookup(args, key.split('.')); 829 | if (xf == null) { 830 | return `<${value}> (${typeof value})`; 831 | } else if (Object.prototype.hasOwnProperty.call({}, xf)) { 832 | return config.transformers[xf](value); 833 | } else { 834 | throw ValueError('no transformer named "' + xf + '"'); 835 | } 836 | } 837 | ); 838 | }; 839 | }; 840 | 841 | const lookup = function(obj, path) { 842 | if (!/^\d+$/.test(path[0])) { 843 | path = ['0'].concat(path); 844 | } 845 | for (let idx = 0; idx < path.length; idx += 1) { 846 | const key = path[idx]; 847 | if (typeof obj[key] === 'function') 848 | obj = obj[key](); 849 | else 850 | obj = obj[key]; 851 | } 852 | return obj; 853 | }; 854 | 855 | if (typeof String.prototype.__format__ === 'undefined') { 856 | 857 | Object.defineProperty(String.prototype, '__format__', { 858 | get: function () { 859 | const $format = create({}); 860 | return function () { 861 | var args = Array.prototype.slice.call(arguments); 862 | args.unshift(this); 863 | return $format.apply(__g__, args); 864 | } 865 | }, 866 | configurable: true, 867 | enumerable: false, 868 | }); 869 | } 870 | } 871 | } 872 | 873 | __g__.UnitTesting = UnitTesting; 874 | 875 | })(this); 876 | -------------------------------------------------------------------------------- /UnitTesting/UnitTestingTests.js: -------------------------------------------------------------------------------- 1 | /* 2 | Imported via dev.gs 3 | */ 4 | 5 | function testing_utgs() { 6 | UnitTesting.init(); 7 | 8 | /* 9 | Under the hood, the describe and it functions, as well as the assert.throws* methods, 10 | are implemented with a context manager, whose functionality is tested here 11 | (With describe and it methods!) 12 | */ 13 | (function ContextManagerTests () { 14 | 15 | describe("Context manager", function () { 16 | 17 | it(".state saves and is available after execution", function () { 18 | let ctx = new ContextManager(); 19 | ctx.with(function () { 20 | this.hi = 'hi' 21 | }); 22 | let actual = ctx.state; 23 | let expected = {hi: 'hi'}; 24 | assert.objectEquals({actual, expected}); 25 | }); 26 | 27 | it("subclassing to get different defaultObject", function () { 28 | class MyContextManager extends ContextManager { 29 | defaultObject () { return []; }; 30 | } 31 | let ctx = new MyContextManager(); 32 | ctx.with(function () { 33 | this.push('hi'); 34 | }); 35 | let actual = ctx.state; 36 | let expected = ['hi']; 37 | assert.arrayEquals({actual, expected}); 38 | }); 39 | 40 | it("error handler receives error and can swallow error", function () { 41 | let ctx = new ContextManager(); 42 | ctx.error = function (err) { 43 | this.errorMessage = err.toString(); 44 | return null; 45 | }; 46 | ctx.with(function () { 47 | throw Error("message"); 48 | }); 49 | let actual = ctx.state; 50 | let expected = {errorMessage: "Error: message"}; 51 | assert.objectEquals({actual:actual, expected:expected}); 52 | }); 53 | 54 | it("Saves state between enter and exit calls, return this", function () { 55 | let ctx = new ContextManager(); 56 | ctx.enter = function () { 57 | this.log = []; 58 | this.log.push('entering'); 59 | }; 60 | ctx.exit = function () { 61 | this.log.push('exiting'); 62 | }; 63 | 64 | let actual = ctx.with(function () { 65 | this.log.push('inside body'); 66 | return this; 67 | }); 68 | let expected = {log:['entering', 'inside body', 'exiting']}; 69 | assert.objectEquals({actual: actual, expected:expected}); 70 | }); 71 | 72 | it("Can be passed a parameter", function () { 73 | let ctx = new ContextManager(); 74 | const param = "param"; 75 | ctx.param = param; 76 | ctx.with(function (param) { 77 | this.param = param; 78 | }); 79 | let actual = ctx.state; 80 | let expected = {param:param}; 81 | assert.objectEquals({actual:actual, expected:expected}); 82 | }); 83 | 84 | }); 85 | 86 | })(); 87 | 88 | 89 | /* 90 | Tests the assert* range of functions 91 | */ 92 | (function AssertionTests () { 93 | 94 | describe("Pass when actual meets expected", function () { 95 | it("assertTrue", function () { 96 | assert.true_({actual:true}); 97 | }); 98 | 99 | it("assertFalse", function () { 100 | assert.false_({actual:false}); 101 | }); 102 | 103 | it("assertEquals", function () { 104 | assert.equals({actual:true,expected:true}); 105 | }); 106 | 107 | it("assertNotEquals", function () { 108 | assert.notEqual({expected:true,actual:false}); 109 | }); 110 | 111 | it("assertNull", function () { 112 | assert.null_({actual:null}); 113 | }); 114 | 115 | it("assertNotNull", function () { 116 | assert.notNull({actual:undefined}); 117 | assert.notNull({actual:0}); 118 | }); 119 | 120 | it("assertUndefined", function () { 121 | assert.undefined_({actual:undefined}); 122 | }); 123 | 124 | it("assertNotUndefined", function () { 125 | assert.notUndefined({actual:null}); 126 | }); 127 | 128 | it("assertNaN", function () { 129 | assert.NaN_({actual:NaN}); 130 | }); 131 | 132 | it("assetNotNaN", function () { 133 | assert.notNaN({actual:0}); 134 | }); 135 | 136 | it("assertObjectEquals", function () { 137 | assert.objectEquals({expected:{hi:'hi'}, actual:{hi:'hi'}}); 138 | }); 139 | 140 | it("assertObjectEquals with date", function () { 141 | const date = new Date(); 142 | assert.objectEquals({expected:{date:date}, actual:{date:date}, comment: "date embedded in object"}); 143 | assert.objectEquals({expected:date, actual:date, comment: "date on its own"}); 144 | }); 145 | 146 | it("assertArrayEquals", function () { 147 | assert.arrayEquals({expected: ['hello', 'world'], actual: ['hello', 'world']}); 148 | }); 149 | 150 | it("assertEvaluatesToTrue", function () { 151 | assert.evaluatesToTrue({actual:1}); 152 | assert.evaluatesToTrue({actual:true}); 153 | assert.evaluatesToTrue({actual:'hi'}); 154 | }); 155 | 156 | it("assertEvaluatesToFalse", function () { 157 | assert.evaluatesToFalse({actual:0}); 158 | assert.evaluatesToFalse({actual:false}); 159 | assert.evaluatesToFalse({actual:''}); 160 | }); 161 | 162 | it("assertHashEquals", function () { 163 | assert.hashEquals({expected:{hi:'hi'}, actual:{hi:'hi'}}); 164 | }); 165 | 166 | it("assertRoughlyEquals", function () { 167 | assert.roughlyEquals({expected:1,actual:1.5,tolerance:1}); 168 | }); 169 | 170 | it("assertContains", function () { 171 | assert.contains({value: 1, collection:[1, 2]}); 172 | }); 173 | 174 | it("assertArrayEqualsIgnoringOrder", function () { 175 | assert.arrayEqualsIgnoringOrder({expected: [2, 1], actual:[1, 2]}); 176 | }); 177 | 178 | it("assertThrowsError", function () { 179 | assert.throwsError(function () { 180 | throw new TypeError("expected error thrown"); 181 | }); 182 | }); 183 | 184 | it("assertDoesNotThrowError", function () { 185 | assert.doesNotThrowError(function () { 186 | "do nothing"; 187 | }); 188 | }); 189 | 190 | it("assertThrowsTypeError", function () { 191 | assert.throwsTypeError(function () { 192 | throw new TypeError("error thrown!"); 193 | }); 194 | }); 195 | 196 | it("assertThrowsTypeError", function () { 197 | assert.throwsTypeError(function () { 198 | throw new TypeError("error thrown!"); 199 | }); 200 | }); 201 | 202 | it("assertThrowsRangeError", function () { 203 | assert.throwsRangeError(function () { 204 | throw new RangeError("error thrown!"); 205 | }); 206 | }); 207 | 208 | }); 209 | 210 | 211 | describe("Fail when actual does not match expected", function () { 212 | it("assertTrue fails", function () { 213 | assert.throwsError(function () { 214 | assert.true_(false); 215 | }); 216 | }); 217 | 218 | it("assertFalse fails", function () { 219 | assert.throwsError(function () { 220 | assert.false_(true); 221 | }); 222 | }); 223 | 224 | it("assertEquals fails", function () { 225 | assert.throwsError(function () { 226 | assert.equals(true, false); 227 | }); 228 | }); 229 | 230 | it("assertNotEquals fails", function () { 231 | assert.throwsError(function () { 232 | assert.notEqual(true, true); 233 | }); 234 | }); 235 | 236 | it("assertNull fails", function () { 237 | assert.throwsError(function () { 238 | assert.null_(''); 239 | }); 240 | }); 241 | 242 | it("assertNotNull", function () { 243 | assert.throwsError(function () { 244 | assert.notNull(null); 245 | }); 246 | }); 247 | 248 | it("assertUndefined", function () { 249 | assert.throwsError(function () { 250 | assert.undefined_(null); 251 | }); 252 | }); 253 | 254 | it("assertNotUndefined", function () { 255 | assert.throwsError(function () { 256 | assert.notUndefined(undefined); 257 | }); 258 | }); 259 | 260 | it("assertNaN", function () { 261 | assert.throwsError(function () { 262 | assert.NaN_(0); 263 | }); 264 | }); 265 | 266 | it("assetNotNaN", function () { 267 | assert.throwsError(function () { 268 | assert.notNaN(NaN); 269 | }); 270 | }); 271 | 272 | it("assertObjectEquals", function () { 273 | assert.throwsError(function () { 274 | assert.objectEquals({hi:'hi'}, {hi:'hi', something:'hi'}); 275 | }); 276 | }); 277 | 278 | it("assertArrayEquals", function () { 279 | assert.throwsError(function () { 280 | assert.arrayEquals(['hello', 'world'], ['hello']); 281 | }); 282 | }); 283 | 284 | it("assertEvaluatesToTrue", function () { 285 | assert.throwsError(function () { 286 | assert.evaluatesToTrue(false); 287 | }); 288 | }); 289 | 290 | it("assertEvaluatesToFalse", function () { 291 | assert.throwsError(function () { 292 | assert.evaluatesToFalse(true); 293 | }); 294 | }); 295 | 296 | it("assertHashEquals", function () { 297 | assert.throwsError(function () { 298 | assert.hashEquals({expected: {hi:'hi'}, actual:{hi:'hello'}}); 299 | }); 300 | }); 301 | 302 | it("assertRoughlyEquals", function () { 303 | assert.throwsError(function () { 304 | assert.roughlyEquals({expected: 1, 305 | actual:2, 306 | tolerance:1}); 307 | }); 308 | }); 309 | 310 | it("assertContains", function () { 311 | assert.throwsError(function () { 312 | assert.contains(1, [0, 2]); 313 | }); 314 | }); 315 | 316 | it("assertArrayEqualsIgnoringOrder", function () { 317 | assert.throwsError(function () { 318 | assert.arrayEqualsIgnoringOrder([2, 1], [1, 2, 3]); 319 | }); 320 | }); 321 | 322 | it("assertThrowsError fails when non-Error thrown", function () { 323 | assert.throwsError(function () { 324 | throw new TypeError("expected error thrown"); 325 | }); 326 | }); 327 | 328 | it("assertThrowsTypeError fails when non-TypeError thrown", function () { 329 | assert.throwsError("I am prepared", function () { 330 | assert.throwsTypeError("throws error", function () { 331 | throw new Error("wrong error thrown!"); 332 | }); 333 | }); 334 | }); 335 | 336 | it("assertThrowsTypeError fails when non-ReferenceError thrown", function () { 337 | assert.throwsError(function () { 338 | assert.throwsReferenceError(function () { 339 | throw new TypeError("wrong error thrown!"); 340 | }); 341 | }); 342 | }); 343 | 344 | it("assertThrowsRangeError fails when non-RangeError thrown", function () { 345 | assert.throwsError(function () { 346 | assert.throwsRangeError(function () { 347 | throw new Error("wrong error thrown!"); 348 | }); 349 | }); 350 | }); 351 | }); 352 | 353 | })(); 354 | 355 | } 356 | -------------------------------------------------------------------------------- /VerboseErrors/README.md: -------------------------------------------------------------------------------- 1 | # VerboseErrors 2 | 3 | Errors that tell us more about what's happening in Google Apps Script's online editor, made a cinch. 4 | 5 | ## Tutorial 6 | 7 | When you throw an error in the online editor, you get some pretty limited information: 8 | 9 | ``` 10 | // Code.gs: 11 | function error_() { 12 | throw new Error("Woops"); 13 | } 14 | function entrypoint() { 15 | error_(); 16 | } 17 | 18 | // Error output: 19 | Error: "Woops" (line 2, file "Code") 20 | ``` 21 | 22 | That's it? Where did the error come from? No stacktrace? Shouldn't it say something like: 23 | 24 | ``` 25 | Error: "Woops" at error_ (Code 2:3) at entrypoint (Code 5:3) (line 2, file "Code") 26 | ``` 27 | 28 | That's more like it. See below for installation instructions. 29 | 30 | But wait there's more. If you check the logs, it's also output there. But wait there's more. What if you want to see variable values at the time of the error, to help you reason about your code? 31 | 32 | ``` 33 | // js: 34 | function error_() { 35 | const code = 404; 36 | throw new Error("Woops", {code}); 37 | } 38 | function entrypoint() { 39 | error_(); 40 | } 41 | 42 | ``` 43 | 44 | Check the logs: 45 | 46 | ``` 47 | Woops at error_ (Code:3:11) at entrypoint_here (Code:6:5) 48 | code=404 49 | ``` 50 | 51 | Very cool that you can throw errors and pass it state information at the time of the error. 52 | 53 | ## Installation 54 | 55 | Copy and paste the code into your project. Decide if you want to use it as a drop in replacement. 56 | 57 | ```js 58 | const dropInReplacement = true; 59 | ``` 60 | 61 | If it's true, that means that you have to do this when throwing errors (notice the `new` keyword): 62 | 63 | ```js 64 | throw new Error(); 65 | ``` 66 | 67 | If false, then you have to do a bit more typing for it to work as expected: 68 | 69 | ```js 70 | class CustomError extends VerboseError {} 71 | 72 | throw new CustomError(msg) 73 | ``` 74 | 75 | Check out the settings info on line 6: 76 | 77 | ```js 78 | constructor(message, {verbose=true, logger=Logger, ...kwargs}={}) { 79 | ``` 80 | 81 | If you have no idea what `constructor` is, nevermind, look at the rest of it. If `verbose` is `true`, it'll output your error info and any state object you pass it `{code}` above (which is actually shorthand for `{code: 404}`. You can change it to `false` there if you don't want that. You can change `logger` to `console` if for some reason you want to use `console.log` instead of `Logger.log`. 82 | 83 | If the above is gibberish, nevermind it's not really all that important, just start using it. When you're done debugging, delete the project file. 84 | 85 | Start using it. Whenever you throw Errors you will not automatically get the described behaviour in the tutorial. 86 | 87 | ## Creating custom errors 88 | 89 | If you want to make a custom class such as `CustomError`, no problem, do it like this: 90 | 91 | ```js 92 | class CustomClass extends Error {} 93 | ``` 94 | 95 | and use it just like any other error: 96 | 97 | ```js 98 | throw new CustomClass("A custom error!"); 99 | ``` 100 | 101 | and voila you have made a custom error but with all the features already baked in (such is the power of classes). 102 | 103 | ## Notes & miscellaneous 104 | 105 | In Rhino, you could throw errors without the `new` keyword: 106 | 107 | ```js 108 | // Rhino: 109 | throw Error("This is an error thrown without the new keyword"); 110 | throw new Error("This is an error thrown without the new keyword"); 111 | ``` 112 | 113 | They were equivalent, and it looks like this is true with V8 too. But because VerboseLogger uses classes to implement, you **must** use the `new` keyword. So it's better that you just get used to doing it "the right way," so if you see the following error: 114 | 115 | ```js 116 | throw Error(); // "Class constructor VerboseError cannot be invoked without 'new'" 117 | ``` 118 | 119 | That's why. Just add the `new` keyword as expected: 120 | 121 | ```js 122 | throw new Error(); // aaaaah 123 | ``` 124 | -------------------------------------------------------------------------------- /VerboseErrors/VerboseErrors.js: -------------------------------------------------------------------------------- 1 | (function (__g__) { 2 | 3 | // If true, this will make throw new Error() verbose powers: 4 | const dropInReplacement = true; 5 | 6 | class VerboseError extends Error { 7 | constructor(message, {verbose=true, logger=Logger, ...kwargs}={}) { 8 | // allow the parent class to do its thang 9 | super(message); 10 | 11 | // display the name of the class in the error message 12 | this.name = this.constructor.name; 13 | 14 | // modify the error message to display class name, hyphen, then message 15 | // PLUS the stacktrace (so, so useful) 16 | this.message += this.stack.split('\n').slice(1, -1).join('\n'); 17 | 18 | verbose && logger.log(this.message); 19 | // output to the logger any extra info relevant to understanding what's happening at that point in code 20 | for (let keyword in kwargs) { 21 | verbose && logger.log(`${keyword}=${kwargs[keyword]}`); 22 | } 23 | } 24 | } 25 | 26 | if (dropInReplacement) __g__.Error = VerboseError; 27 | else __g__.VerboseError = VerboseError; 28 | 29 | })(this); 30 | 31 | --------------------------------------------------------------------------------