├── .prettierignore ├── .eslintignore ├── .babelrc ├── src ├── index.js ├── doctest.js ├── safe_eval.js └── doctest_parser.js ├── .gitignore ├── .prettierrc ├── .eslintrc ├── test ├── support │ ├── sample_error_module.js │ ├── sample_passing_class.js │ ├── sample_failing_module.js │ └── sample_passing_module.js ├── doctest_parser_test.js ├── doctest_test.js └── safe_eval_test.js ├── webpack.config.js ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── LICENSE ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ./dist/index.js 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import doctest from './doctest' 2 | 3 | export default doctest 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Static artifacts 2 | /node_modules 3 | 4 | # OSX system files 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "arrow-parens": ["error", "always"], 5 | "function-paren-newline": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/support/sample_error_module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the last element in an array 3 | * @param {array} array - The array to get the last value from 4 | * @example 5 | * last([1,2,3]) 6 | * //=> 3 7 | * @example 8 | * last([[]) 9 | * //=> undefined 10 | */ 11 | export function last(array) { 12 | return array[array.length - 1] 13 | } 14 | 15 | export function first(array) { 16 | return array[0] 17 | } 18 | -------------------------------------------------------------------------------- /test/support/sample_passing_class.js: -------------------------------------------------------------------------------- 1 | // This is an example of a string helper module 2 | 3 | class Arithmetic { 4 | constructor() {} 5 | 6 | /** 7 | * @example 8 | * add(1, 2) 9 | * //=> 3 10 | * @example 11 | * add(1, 9) 12 | * //=> 10 13 | */ 14 | add(a, b) { 15 | return a + b 16 | } 17 | 18 | /** 19 | * @example 20 | * subtract(10, 2) 21 | * //=> 8 22 | */ 23 | subtract(a, b) { 24 | return a - b 25 | } 26 | } 27 | 28 | export { Arithmetic } 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | devtool: false, 5 | entry: { 6 | app: './src/index.js', 7 | }, 8 | output: { 9 | library: 'index', 10 | libraryTarget: 'commonjs2', 11 | filename: 'index.js', 12 | }, 13 | resolve: { 14 | modules: [__dirname, 'node_modules', 'src'], 15 | extensions: ['*', '.js', '.json'], 16 | }, 17 | resolveLoader: { 18 | modules: [path.join(__dirname, 'node_modules')], 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | use: ['babel-loader'], 25 | include: [path.join(__dirname, 'src')], 26 | exclude: /node_modules/, 27 | }, 28 | ], 29 | }, 30 | target: 'node', 31 | } 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ supabase ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | 16 | runs-on: ${{ matrix.platform }} 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Get npm cache directory 22 | id: npm-cache 23 | run: | 24 | echo "::set-output name=dir::$(npm config get cache)" 25 | 26 | - uses: actions/cache@v1 27 | with: 28 | path: ${{ steps.npm-cache.outputs.dir }} 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | 33 | - name: Set up Node 34 | uses: actions/setup-node@v1 35 | with: 36 | node-version: 12.x 37 | 38 | - run: npm install 39 | 40 | - run: npm run test 41 | -------------------------------------------------------------------------------- /src/doctest.js: -------------------------------------------------------------------------------- 1 | /* globals it */ 2 | import fs from 'fs' 3 | import { expect } from 'chai' 4 | import parseDoctests from './doctest_parser' 5 | import evalDoctest from './safe_eval' 6 | 7 | const defaultTestingFunction = (actual, expected, doctest) => { 8 | it(`doctest: ${doctest.resultString}`, () => { 9 | if (actual.result && expected.result) { 10 | expect(actual.result).to.eql(expected.result) 11 | } 12 | }) 13 | } 14 | 15 | export default (filePath, options = {}) => { 16 | const file = fs.readFileSync(filePath, 'utf8') 17 | const doctests = parseDoctests(file) 18 | doctests.forEach((doctest, index) => { 19 | const { actual, expected } = evalDoctest(doctest, filePath, options.instance) 20 | if (actual.error) { 21 | throw actual.error 22 | } else if (expected.error) { 23 | throw expected.error 24 | } else { 25 | const { testingFunction = defaultTestingFunction } = options 26 | testingFunction(actual, expected, doctest, index) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Supabase 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 | -------------------------------------------------------------------------------- /test/doctest_parser_test.js: -------------------------------------------------------------------------------- 1 | /* globals it describe */ 2 | import fs from 'fs' 3 | import { expect } from 'chai' 4 | import parseDoctests from '../src/doctest_parser' 5 | 6 | const SAMPLE_MODULE = './test/support/sample_passing_module.js' 7 | const FILE = fs.readFileSync(SAMPLE_MODULE, 'utf8') 8 | 9 | describe('parseDocs', () => { 10 | it('should return an array objects with the string to eval and the expected value', () => { 11 | const [firstDoctest, secondDoctest, thirdDoctest, fourthDoctest] = parseDoctests(FILE) 12 | 13 | expect(firstDoctest.resultString).to.equal("titleize('wOaH')") 14 | expect(firstDoctest.stringToEval).to.equal("'Woah'") 15 | 16 | expect(secondDoctest.resultString).to.equal("titleize('w')") 17 | expect(secondDoctest.stringToEval).to.equal("'W'") 18 | 19 | expect(thirdDoctest.resultString).to.equal("stringData( 'woah')") 20 | expect(thirdDoctest.stringToEval).to.equal('{ length: 4, vowels: 2, consonants: 2}') 21 | 22 | expect(fourthDoctest.resultString).to.equal("split('why am i doing this?', ' ')") 23 | expect(fourthDoctest.stringToEval).to.equal("[ 'why', 'am', 'i', 'doing', 'this?' ]") 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/safe_eval.js: -------------------------------------------------------------------------------- 1 | /* eslint no-eval: "off", no-console: "off" */ 2 | import path from 'path' 3 | 4 | export const evalExpression = (evalString, filePath) => { 5 | try { 6 | if (filePath !== undefined) filePath = filePath.replace(/\\/g,'/'); 7 | const code = `require('${filePath}').${evalString}` 8 | const result = eval(code) 9 | return { result } 10 | } catch (error) { 11 | return { error } 12 | } 13 | } 14 | 15 | export const evalValue = evalString => { 16 | const wrappedEvalString = `(${evalString})` 17 | try { 18 | return { result: eval(wrappedEvalString) } 19 | } catch (error) { 20 | return { error } 21 | } 22 | } 23 | 24 | export default ({ resultString, stringToEval }, filePath, instance) => { 25 | const fullFilePath = path.join(process.cwd(), filePath) 26 | if (!instance) { 27 | const actual = evalExpression(resultString, fullFilePath) 28 | const expected = evalValue(stringToEval) 29 | return { actual, expected } 30 | } else { 31 | const result = eval(`instance.${resultString};`) 32 | const actual = { result } 33 | const expected = evalValue(stringToEval) 34 | return { actual, expected } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase/doctest-js", 3 | "version": "0.1.0", 4 | "description": "Run doc examples as tests", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "webpack -p", 8 | "test": "mocha --require babel-register", 9 | "test:watch": "npm run test -- --watch", 10 | "lint": "eslint {src,test}/**/*.js webpack.config.js", 11 | "format": "prettier {src,test}/**/*.js webpack.config.js --write", 12 | "pretty": "prettier --write \"**/*.js\"" 13 | }, 14 | "author": "Supabase", 15 | "repository": "https://github.com/supabase/doctest-js", 16 | "license": "MIT", 17 | "dependencies": { 18 | "babel-core": "^6.26.3", 19 | "babel-register": "^6.26.0", 20 | "chai": "^4.1.2", 21 | "jest": "^26.0.0", 22 | "jison": "^0.4.18", 23 | "lex": "^1.7.9" 24 | }, 25 | "devDependencies": { 26 | "babel-loader": "^8.0.6", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-stage-0": "^6.24.1", 29 | "eslint": "^5.16.0", 30 | "eslint-config-airbnb": "^17.1.1", 31 | "eslint-plugin-import": "^2.9.0", 32 | "eslint-plugin-jsx-a11y": "^6.0.3", 33 | "eslint-plugin-react": "^7.7.0", 34 | "mocha": "^8.0.1", 35 | "prettier": "^2.0.1", 36 | "webpack": "^5.1.0", 37 | "webpack-cli": "^4.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/doctest_test.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | import { expect } from 'chai' 3 | import doctest from '../src' 4 | 5 | const SAMPLE_PASSING_MODULE_PATH = './test/support/sample_passing_module.js' 6 | const SAMPLE_PASSING_CLASS_PATH = './test/support/sample_passing_class.js' 7 | const SAMPLE_FAILING_MODULE_PATH = './test/support/sample_failing_module.js' 8 | const SAMPLE_ERROR_MODULE_PATH = './test/support/sample_error_module.js' 9 | const { Arithmetic } = require('./support/sample_passing_class.js') 10 | 11 | describe('passing doctest', () => { 12 | doctest(SAMPLE_PASSING_MODULE_PATH) 13 | doctest(SAMPLE_PASSING_CLASS_PATH, { instance: new Arithmetic() }) 14 | }) 15 | 16 | describe('failing doctest', () => { 17 | doctest(SAMPLE_FAILING_MODULE_PATH, { 18 | testingFunction: (actual, expected, _doctest, index) => { 19 | if (index === 4) { 20 | it('should fail', () => { 21 | expect(actual.result).to.not.eql(expected.result) 22 | }) 23 | } else { 24 | it('should not fail', () => { 25 | expect(actual.result).to.eql(expected.result) 26 | }) 27 | } 28 | }, 29 | }) 30 | }) 31 | 32 | describe('error doctest', () => { 33 | it('should raise an error', () => { 34 | expect(() => { 35 | doctest(SAMPLE_ERROR_MODULE_PATH) 36 | }).to.throw(Error) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/safe_eval_test.js: -------------------------------------------------------------------------------- 1 | /* globals it describe */ 2 | import { expect } from 'chai' 3 | import { evalExpression, evalValue } from '../src/safe_eval' 4 | 5 | describe('evalExpression', () => { 6 | it('should gracefully handle syntax errors', () => { 7 | const evalString = 'varvar funfun == () =>' 8 | const { error } = evalExpression(evalString) 9 | expect(error.constructor).to.equal(SyntaxError) 10 | }) 11 | 12 | it('should gracefully handle runtime errors', () => { 13 | const evalString = 'nonExistantFunction()' 14 | const { error } = evalExpression(evalString) 15 | expect(error.constructor).to.equal(Error) 16 | }) 17 | }) 18 | 19 | describe('evalValue', () => { 20 | it('should properly resolve simple values', () => { 21 | const evalString = '5' 22 | const { result } = evalValue(evalString) 23 | expect(result).to.equal(5) 24 | }) 25 | 26 | it('should properly resolve more complex values', () => { 27 | const evalString = '{woah: "wow", insane: "right?", someNumber: 9}' 28 | const { result } = evalValue(evalString) 29 | const { woah, insane, someNumber } = result 30 | expect(woah).to.equal('wow') 31 | expect(insane).to.equal('right?') 32 | expect(someNumber).to.equal(9) 33 | }) 34 | 35 | it('should gracefully handle syntax errors', () => { 36 | const evalString = '* 5 *' 37 | const { error } = evalValue(evalString) 38 | expect(error.constructor).to.equal(SyntaxError) 39 | expect(error.message).to.equal("Unexpected token '*'") 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /test/support/sample_failing_module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the value stored in the object where the nested keys point to 3 | * @params {object} object - Object with value to dig out 4 | * @params {array} - nestedKeys - Can be array or '.' delimited string 5 | * @example 6 | * dig({a: {b: {c: 'd'}}}, ['a', 'b', 'c']) 7 | * //=> 'd' 8 | * @example 9 | * dig({a: {b: {c: 'd'}}}, 'a.b.c') 10 | * //=> 'd' 11 | */ 12 | export function dig(object, nestedKeys) { 13 | const keys = nestedKeys.constructor === Array ? nestedKeys : nestedKeys.split('.') 14 | let value = object 15 | keys.forEach(key => { 16 | value = value[key] 17 | }) 18 | return value 19 | } 20 | 21 | /** 22 | * Returns an object of the elements grouped by the function 23 | * @params {array} elements - the elements to be grouped 24 | * @params {function} fun - the function who's return value will be the key in the return object 25 | * @returns {object} 26 | * @example 27 | * groupBy([1, 1, 2, 3, 5, 8, 11], (val) => val % 2 ? 'odd' : 'even' ) 28 | * //=> {odd: [1, 1, 3, 5, 11], even: [2, 8]} 29 | */ 30 | export function groupBy(elements, fun) { 31 | const grouping = {} 32 | elements.forEach(element => { 33 | const value = fun(element) 34 | grouping[value] = (grouping[value] || []).concat([element]) 35 | }) 36 | return grouping 37 | } 38 | 39 | /** Returns the keypaths for the object 40 | * @params {object} object - The object to get the keys from 41 | * @returns {object} 42 | * @example 43 | * keyPaths( { a: { b: ['c'] } } ) 44 | * //=> [ ['a'], ['a', 'b'], ['a', 'b', '0'] ] 45 | */ 46 | export function keyPaths(object, parentKeys = []) { 47 | const type = object.constructor 48 | if (type !== Array && type !== Object) return [parentKeys] 49 | let paths = [] 50 | if (parentKeys.length) paths = paths.concat([parentKeys]) 51 | Object.keys(object).forEach(key => { 52 | const subKeyPaths = keyPaths(object[key], parentKeys.concat([key])) 53 | paths = paths.concat(subKeyPaths) 54 | }) 55 | return paths 56 | } 57 | 58 | /** 59 | * Merges two objects into one new object 60 | * object2 will overwrite matching keys in object1 61 | * @params {object} object1 - First Object 62 | * @params {object} object2 - Second Object 63 | * @returns {object} 64 | * @example 65 | * merge({woah: 'we', seeya: 'soon'}, {done: 'merged', seeya: 'later'}) 66 | * //=> {woah: 'we', didnt: 'merge', allthe: 'way'} 67 | */ 68 | export function merge(object1, object2) { 69 | const newObject = {} 70 | Object.keys(object1).forEach(key => { 71 | newObject[key] = object1[key] 72 | }) 73 | Object.keys(object2).forEach(key => { 74 | newObject[key] = object2[key] 75 | }) 76 | return newObject 77 | } 78 | 79 | /** 80 | * Returns the object as an array 81 | * @params {object} object - Object with numbers as keys 82 | * @returns {array} 83 | * @example 84 | * numberKeyedObjectToArray({1: 'one', 2: 'two'}) 85 | * //=> [ , 'one', 'two' ] 86 | */ 87 | export function numberKeyedObjectToArray(object) { 88 | const array = [] 89 | Object.keys(object).forEach(key => { 90 | array[key] = object[key] 91 | }) 92 | return array 93 | } 94 | -------------------------------------------------------------------------------- /src/doctest_parser.js: -------------------------------------------------------------------------------- 1 | import { Parser } from 'jison' 2 | import Lexer from 'lex' 3 | 4 | // parser states 5 | const NO_STATE = 'NO_STATE' 6 | const IN_COMMENT = 'IN_COMMENT' 7 | const IN_EXAMPLE = 'IN_EXAMPLE' 8 | const IN_RETURN_VALUE = 'IN_RETURN_VALUE' 9 | 10 | // basic grammar 11 | const grammar = { 12 | bnf: { 13 | expression: [['EOF', 'return $1;']], 14 | }, 15 | } 16 | 17 | export default text => { 18 | // lexer parser setup 19 | const lexer = new Lexer() 20 | const parser = new Parser(grammar) 21 | parser.lexer = lexer 22 | const doctests = [] 23 | let doctestIndex = -1 24 | let state = NO_STATE 25 | let isClass = false 26 | // begin multi-line comment 27 | lexer.addRule(/\/\*/, () => { 28 | if (state === NO_STATE) { 29 | state = IN_COMMENT 30 | } 31 | }) 32 | 33 | // end multi-line comment 34 | lexer.addRule(/\*\//, () => { 35 | if (state === IN_RETURN_VALUE) { 36 | state = NO_STATE 37 | } 38 | }) 39 | 40 | // begin doctest example 41 | lexer.addRule(/@example/, () => { 42 | if (state === IN_COMMENT || state === IN_RETURN_VALUE) { 43 | state = IN_EXAMPLE 44 | doctestIndex += 1 45 | doctests[doctestIndex] = { 46 | resultString: '', 47 | stringToEval: '', 48 | } 49 | } 50 | }) 51 | 52 | // begin doctest return value 53 | lexer.addRule(/\/\/=>/, () => { 54 | if (state === IN_EXAMPLE) { 55 | state = IN_RETURN_VALUE 56 | } 57 | }) 58 | 59 | // ignore multi-line comment start 60 | // this is a bit naive as it only uses spaces/indentation to cleanse 61 | lexer.addRule(/\n\* /, () => {}) 62 | lexer.addRule(/\r\n\* /, () => {}) 63 | lexer.addRule(/\n \* /, () => {}) 64 | lexer.addRule(/\r\n \* /, () => {}) 65 | lexer.addRule(/\n \* /, () => {}) 66 | lexer.addRule(/\r\n \* /, () => {}) 67 | lexer.addRule(/\n \* /, () => {}) 68 | lexer.addRule(/\r\n \* /, () => {}) 69 | lexer.addRule(/\n \* /, () => {}) 70 | lexer.addRule(/\r\n \* /, () => {}) 71 | lexer.addRule(/\n \* /, () => {}) 72 | lexer.addRule(/\r\n \* /, () => {}) 73 | lexer.addRule(/\n \* /, () => {}) 74 | lexer.addRule(/\r\n \* /, () => {}) 75 | 76 | // add chars to appropriate section 77 | 78 | lexer.addRule(/\n|./, lexme => { 79 | if (state === IN_EXAMPLE) { 80 | doctests[doctestIndex].resultString += lexme 81 | } else if (state === IN_RETURN_VALUE) { 82 | doctests[doctestIndex].stringToEval += lexme 83 | } 84 | }) 85 | 86 | lexer.addRule(/\r\n|./, lexme => { 87 | if (state === IN_EXAMPLE) { 88 | doctests[doctestIndex].resultString += lexme 89 | } else if (state === IN_RETURN_VALUE) { 90 | doctests[doctestIndex].stringToEval += lexme 91 | } 92 | }) 93 | 94 | // eof 95 | lexer.addRule(/$/, () => 'EOF') 96 | parser.parse(text) 97 | // trim everythhing 98 | const sanitizedDoctests = doctests.map(({ resultString, stringToEval }) => ({ 99 | resultString: resultString.trim(), 100 | stringToEval: stringToEval.trim(), 101 | })) 102 | return sanitizedDoctests 103 | } 104 | -------------------------------------------------------------------------------- /test/support/sample_passing_module.js: -------------------------------------------------------------------------------- 1 | // This is an example of a string helper module 2 | 3 | /** 4 | * Returns a word with all letter downcases except the first letter 5 | * @param {string} word - The word to be titleized 6 | * @return {string} The string titlelized 7 | * @example 8 | * titleize('wOaH') 9 | * //=> 'Woah' 10 | * @example 11 | * titleize('w') 12 | * //=> 'W' 13 | */ 14 | export function titleize(word) { 15 | switch (word.length) { 16 | case 0: 17 | return '' 18 | case 1: 19 | return word.toUpperCase() 20 | default: 21 | return word[0].toUpperCase() + word.slice(1, word.length).toLowerCase() 22 | } 23 | } 24 | 25 | /** 26 | * Returns fairly unnecessary and uninteresting information about a string 27 | * @param {string} string - The string of disinterest 28 | * @return {object} Useless information 29 | * @example 30 | * stringData( 31 | * 'woah' 32 | * ) 33 | * //=> { 34 | * length: 4, 35 | * vowels: 2, 36 | * consonants: 2 37 | * } 38 | */ 39 | export function stringData(string) { 40 | const vowels = string 41 | .toLowerCase() 42 | .split('') 43 | .filter(char => ['a', 'e', 'i', 'o', 'u', 'y'].find(v => char === v)).length 44 | return { 45 | vowels, 46 | length: string.length, 47 | consonants: string.length - vowels, 48 | } 49 | } 50 | 51 | /** 52 | * Does the same thing as String.prototype.split 53 | * @param {string} string - The string you should be using .split on 54 | * @param {string} delimiter - The arg you would pass to .split 55 | * @return {array} The exact same thing .splt would return 56 | * @example 57 | * split('why am i doing this?', ' ') 58 | * //=> [ 'why', 'am', 'i', 'doing', 'this?' ] 59 | */ 60 | export function split(string, delimter) { 61 | return string.split(delimter) 62 | } 63 | 64 | /** 65 | * @example 66 | * add(1, 2) 67 | * //=> 3 68 | * @example add(3, 4) 69 | * //=> 7 70 | * @example add(3, 4) 71 | * //=> 7 72 | */ 73 | export function add(a, b) { 74 | return a + b 75 | } 76 | 77 | /** 78 | * Github Issue: https://github.com/supabase/doctest-js/issues/1 79 | * @param {object} obj 80 | * @private 81 | * @returns {string} 82 | * 83 | * @example objectToQueryString({ 84 | * param1: 'hello', 85 | * param2: 'world' 86 | * }) 87 | * //=> 'param1=hello¶m2=world' 88 | */ 89 | export function objectToQueryString(obj) { 90 | return Object.keys(obj) 91 | .map(param => `${param}=${obj[param]}`) 92 | .join('&') 93 | } 94 | 95 | /** 96 | * Github Issue: https://github.com/supabase/doctest-js/issues/1 97 | * Converts the value of an individual column 98 | * @param {String} columnName The column that you want to convert 99 | * @param {{name: String, type: String}[]} columns All of the columns 100 | * @param {Object} records The map of string values 101 | * @param {Array} skipTypes An array of types that should not be converted 102 | * 103 | * @example convertColumn( 104 | * 'age', 105 | * [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], 106 | * ['Paul', '33'], 107 | * [] 108 | * ) 109 | * //=> 33 110 | * @example convertColumn( 111 | * 'age', 112 | * [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], 113 | * ['Paul', '33'], 114 | * ['int4'] 115 | * ) 116 | * //=> '33' 117 | */ 118 | export const convertColumn = (columnName, columns, records, skipTypes) => { 119 | let column = columns.find(x => x.name == columnName) 120 | let columnNum = columns.findIndex(x => x.name == columnName) 121 | if (skipTypes.includes(column.type)) return noop(records[columnNum]) 122 | else return convertCell(column.type, records[columnNum]) 123 | } 124 | export const noop = val => { 125 | return val 126 | } 127 | export const convertCell = (type, val) => { 128 | return parseInt(val) 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doctest-js 2 | 3 | Let your documentation be your testing suite. 4 | 5 | Write [JSDoc](http://usejsdoc.org/about-getting-started.html) style doc examples on all your functions and then test them using `doctest-js`. 6 | 7 | **Contents** 8 | 9 | - [Getting Started](#getting-started) 10 | - [1. Install](#1-install) 11 | - [2. Write @example comments](#2-write-example-comments) 12 | - [3. Run the tests](#3-run-the-tests) 13 | - [Advanced](#advanced) 14 | - [Multiple tests](#multiple-tests) 15 | - [Testing classes](#testing-classes) 16 | - [Usage online](#usage-online) 17 | - [Contributing](#contributing) 18 | - [Credits](#credits) 19 | - [Status](#status) 20 | 21 | ## Getting Started 22 | 23 | ### 1. Install 24 | 25 | ```sh 26 | npm install @supabase/doctest-js 27 | ``` 28 | 29 | ### 2. Write @example comments 30 | 31 | Create a [JSDoc style @example](https://jsdoc.app/tags-example.html) on any functions that you want tested. 32 | 33 | ```javascript 34 | /** 35 | * Returns the sum of 2 numbers 36 | * 37 | * @example sum(1, 2) 38 | * //=> 3 39 | */ 40 | export const sum = (a, b) => { 41 | return a + b 42 | } 43 | ``` 44 | 45 | Note that the expected return value must be prefixed by `//=>`. 46 | 47 | ### 3. Run the tests 48 | 49 | Import the doctest function in your test suite and point it at the file. 50 | 51 | ```javascript 52 | import doctest from '@supabase/doctest-js'; 53 | 54 | describe('Doctests', () => { 55 | // file paths are relative to root of directory 56 | doctest('src/sum.js') 57 | doctest('src/someOtherFile.js') 58 | }) 59 | ``` 60 | 61 | ## Advanced 62 | 63 | ### Multiple tests 64 | 65 | You can run multiple tests for the same function. 66 | 67 | ```javascript 68 | /** 69 | * @example sum(1, 2) 70 | * //=> 3 71 | * @example sum(3, 4) 72 | * //=> 7 73 | */ 74 | export const sum = (a, b) => { 75 | return a + b 76 | } 77 | ``` 78 | 79 | ### Testing classes 80 | 81 | Testing classes requires you to pass a newed up instance of the class into the test itself. Here is a simple example: 82 | 83 | ```js 84 | // Arithmetic.js - a basic class which we need to test 85 | 86 | class Arithmetic { 87 | constructor() {} 88 | 89 | /** 90 | * @example add(1, 2) 91 | * //=> 3 92 | */ 93 | add(a, b) { 94 | return a + b 95 | } 96 | } 97 | 98 | export { Arithmetic } 99 | ``` 100 | 101 | ```js 102 | // Arithmetic.test.js 103 | 104 | const { Arithmetic } = require('./Arithmetic.js') 105 | 106 | describe('passing doctest', () => { 107 | doctest('./Arithmetic.js', { instance: new Arithmetic() }) 108 | }) 109 | ``` 110 | 111 | ## Usage online 112 | 113 | See this in the wild: 114 | 115 | - [supabase/postgrest-js](https://github.com/supabase/postgrest-js/blob/master/test/unit/Doctests.test.js) 116 | 117 | ## Contributing 118 | 119 | - Fork the repo on GitHub 120 | - Clone the project to your own machine 121 | - Commit changes to your own branch 122 | - Push your work back up to your fork 123 | - Submit a Pull request so that we can review your changes and merge 124 | 125 | ## Credits 126 | 127 | * Inspired by [Elixir Doctests](https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html) 128 | * Original fork of [mainshayne223/doctest-js](https://github.com/MainShayne233/js-doctest). See [issue #1](https://github.com/MainShayne233/js-doctest/issues/1). 129 | 130 | ## Status 131 | 132 | Ready for production! Watch and star this repo to keep updated on releases. 133 | 134 | ![Watch this repo](https://gitcdn.xyz/repo/supabase/monorepo/master/web/static/watch-repo.gif "Watch this repo") 135 | 136 | 137 | ## Sponsors 138 | 139 | We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. Thanks to these sponsors who are making the OSS ecosystem better for everyone. 140 | 141 | [![Worklife VC](https://user-images.githubusercontent.com/10214025/90451355-34d71200-e11e-11ea-81f9-1592fd1e9146.png)](https://www.worklife.vc) 142 | [![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) 143 | 144 | --------------------------------------------------------------------------------