Created
August 9, 2020 12:33
-
-
Save mightyhorst/ab3fa6646a0c05908fca9e0080304a2a to your computer and use it in GitHub Desktop.
Jest Parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @requires process | |
| */ | |
| const util = require('util'); | |
| const exec = util.promisify(require('child_process').exec); | |
| /** | |
| * @requires models | |
| */ | |
| const { | |
| TestModel, | |
| DescribeModel, | |
| ShouldModel, | |
| } = require('../models'); | |
| /** | |
| * @requires services | |
| */ | |
| const uuid = require('./uuid.service'); | |
| /** | |
| * @class JestResultsParserService | |
| * @description run jest on the server and pass results into models | |
| * | |
| */ | |
| class JestResultsParserService { | |
| async runJest(optionalPath) { | |
| try { | |
| const { stdout, stderr } = await exec('jest --verbose' + (optionalPath ? optionalPath : '')); | |
| return this.readLineByLine(stderr); | |
| } catch (err) { | |
| console.error(err); | |
| }; | |
| } | |
| readLineByLine(stdout) { | |
| /** | |
| * @var storage - temp variables for the last model or describe block | |
| */ | |
| var PASSING = []; | |
| var lastPassingModel = false; | |
| var lastPassingDescribeModel = false; | |
| /** | |
| * @step read each line | |
| */ | |
| const lines = stdout.toString().split('\n'); | |
| for (var line of lines) { | |
| var passingTest = line.match(/(?<=PASS ).*/g), | |
| passingShould = line.match(/(?<=β ).*/g); | |
| /** | |
| * @if PASS file path name | |
| */ | |
| if (passingTest) { | |
| const testModel = new TestModel(true, passingTest[0], passingTest[0]); | |
| PASSING.push(testModel); | |
| lastPassingModel = testModel; //--store for the next line | |
| lastPassingDescribeModel = false; //--reset | |
| } | |
| /** | |
| * @if β should expect | |
| */ | |
| else if (passingShould) { | |
| const shouldModel = new ShouldModel(true, passingShould[0]); | |
| if (lastPassingDescribeModel) lastPassingDescribeModel.addShould(shouldModel); | |
| } | |
| /** | |
| * @else describe block | |
| */ | |
| else { | |
| const describeModel = new DescribeModel(line, lastPassingDescribeModel ? lastPassingDescribeModel.id : null); | |
| if (lastPassingDescribeModel) lastPassingDescribeModel.addDescribe(describeModel); | |
| else lastPassingModel.addDescribe(describeModel); | |
| lastPassingDescribeModel = describeModel; | |
| } | |
| }; | |
| return { | |
| passing: PASSING, | |
| failing: '@todo' | |
| } | |
| }; | |
| } | |
| /** | |
| * | |
| * @class BlockModel | |
| * | |
| * @param {number} startIndex - charachter start index in the content string | |
| * @param {number} bracketIndent - how many brackets deep are we nested | |
| * @param {boolean} isBracketClosed - has the bracket been closed yet | |
| * | |
| * @summary ranges | |
| * @param {start:number, end:number} codeRange - start char index to end char index for code brackets | |
| * @param {start:number, end:number} keyWordRange? - start char index to end char index for key word | |
| * | |
| * @option | |
| * @param stringBetweenKeywordAndOpenbracket? - e.g. keyword [....] { | |
| * @param contentsBetweenKeywordAndOpenbracket? - e.g. [....] | |
| * | |
| * @option | |
| * @param {uuid} testModelId - add reference to describe model if its test model | |
| * @param {uuid} describeModelId - add reference to describe model if its describe model | |
| * @param {uuid} shouldModelId - add reference to describe model if its should model | |
| */ | |
| class BlockModel { | |
| constructor(startIndex, bracketIndent) { | |
| this.uuid = uuid(); | |
| /** | |
| * @param bracketIndent - how many brackets deep are we nested | |
| **/ | |
| this.bracketIndent = bracketIndent; | |
| this.isBracketClosed = false; | |
| /** | |
| * @param {start:number, end:number} codeRange - start char index to end char index for code brackets | |
| * @param {start:number, end:number} keyWordRange? - start char index to end char index for key word | |
| */ | |
| this.codeRange = { | |
| start: startIndex, | |
| end: null | |
| }; | |
| this.keyWordRange = { | |
| start: null, | |
| end: null | |
| }; | |
| /** | |
| * @param stringBetweenKeywordAndOpenbracket - e.g. keyword [....] { | |
| * @param contentsBetweenKeywordAndOpenbracket - e.g. [....] | |
| */ | |
| this.stringBetweenKeywordAndOpenbracket = null; | |
| this.contentsBetweenKeywordAndOpenbracket = null; | |
| /** | |
| * @option | |
| * @param {uuid} testModelId - add reference to describe model if its test model | |
| * @param {uuid} describeModelId - add reference to describe model if its describe model | |
| * @param {uuid} shouldModelId - add reference to describe model if its should model | |
| */ | |
| this.testModelId = null; | |
| this.describeModelId = null; | |
| this.shouldModelId = null; | |
| } | |
| } | |
| const ParserEnum = { | |
| JEST: 'JEST', | |
| JAVASCRIPT: 'JAVASCRIPT' | |
| } | |
| const KeywordEnum = { | |
| /** | |
| * @see javascript | |
| */ | |
| IMPORT: "import", | |
| CLASS: "class", | |
| FOR: "for", | |
| FOREACH: "forEach", | |
| FUNCTION: "function", | |
| NAMESPACE: "namespace", | |
| /** | |
| * @see test | |
| */ | |
| DESCRIBE: "describe", | |
| IT: "it", | |
| // EXPECT: "expect", | |
| /** | |
| * @see other - not matched but assumed if not matched | |
| */ | |
| UNKNOWN: 'UNKNOWN', | |
| METHOD: 'METHOD', | |
| } | |
| class JestSpecParseService { | |
| constructor(){ | |
| this.lastKeyWord = 'class'; | |
| this.resetModels(); | |
| } | |
| resetModels(){ | |
| this.BlockModels = []; | |
| this.TestModel; | |
| this.DescribeModels = []; | |
| this.ShouldModels = []; | |
| } | |
| parseJestFile(testFilePath, str) { | |
| this.resetModels(); | |
| const isPassed=null; | |
| this.TestModel = new TestModel(isPassed, testFilePath); | |
| return this.parse(str, ParserEnum.JEST); | |
| } | |
| parseJavascriptFile(str) { | |
| return this.parse(str, ParserEnum.JAVASCRIPT); | |
| } | |
| parse(str, checkForKeywordFn) { | |
| var bracketIndent = 0; | |
| for (var i = 0; i < str.length - 1; i++) { | |
| var char = str[i]; | |
| var keyword; | |
| switch(checkForKeywordFn){ | |
| case ParserEnum.JEST: | |
| keyword = this.checkForJestKeyword(i, str); | |
| case ParserEnum.JAVASCRIPT: | |
| keyword = this.checkForJavascriptKeyword(i, str); | |
| } | |
| /** | |
| * @desc open bracket | |
| */ | |
| if (char === '{') { | |
| bracketIndent++; | |
| const blockModel = new BlockModel(i, bracketIndent); | |
| blockModel.keyWord = this.lastKeyWord; | |
| blockModel.keyWordRange = this.lastKeyWordRange; | |
| /** | |
| * @desc match between the keyword e.g. "describe" and the start bracket e.g. "{" | |
| */ | |
| blockModel.contentsBetweenKeywordAndOpenbracket = str.substring(blockModel.keyWordRange.end, blockModel.codeRange.start); | |
| const quotesRegex = /(?:\")(.*?)(?:\")|(?:')(.*?)(?:')/g; | |
| blockModel.stringBetweenKeywordAndOpenbracket = blockModel.contentsBetweenKeywordAndOpenbracket.match(quotesRegex); | |
| if(blockModel.stringBetweenKeywordAndOpenbracket) blockModel.stringBetweenKeywordAndOpenbracket = blockModel.stringBetweenKeywordAndOpenbracket.map(m=>m.replace(/\"/g, '')).reduce((_, m)=>m.replace(/\"/g, '')); | |
| /** | |
| * @desc @hack: since method has no keyword, if the last keyword was something different reset the default keyword to method | |
| * @todo change lastKeyWord to method if Javascript | |
| */ | |
| // if (checkForKeywordFn===ParserEnum.JAVASCRIPT && this.lastKeyWord != 'method') {UNKNOWN | |
| if (this.lastKeyWord !== KeywordEnum.UNKNOWN) { | |
| this.lastKeyWord = KeywordEnum.UNKNOWN; | |
| } | |
| this.BlockModels.push(blockModel); | |
| /** | |
| * @desc find the parent | |
| * β’ if the bracket indent is > 1 then this is nested | |
| * β’ the last block processed should be the parent | |
| **/ | |
| if(blockModel.bracketIndent > 1){ | |
| const lastBlockModel = this.BlockModels[this.BlockModels.length - 2]; | |
| blockModel.parentId = lastBlockModel.uuid; | |
| } | |
| } | |
| /** | |
| * @desc end bracket | |
| */ | |
| else if (char === '}') { | |
| const blockModel = this.BlockModels.find(block => { | |
| return block.bracketIndent === bracketIndent && block.isBracketClosed === false; | |
| }); | |
| blockModel.codeRange.end = i; | |
| blockModel.contents = str.substring(blockModel.codeRange.start, blockModel.codeRange.end + 1); | |
| blockModel.isBracketClosed = true; | |
| bracketIndent--; | |
| } | |
| } | |
| return this.BlockModels; | |
| } | |
| checkForJestKeyword(i, str) { | |
| const keywords = [ | |
| KeywordEnum.IMPORT, | |
| KeywordEnum.DESCRIBE, | |
| KeywordEnum.IT, | |
| // KeywordEnum.EXPECT, | |
| ]; | |
| const nextAcceptableChar = { | |
| 'import': [' '], | |
| 'describe': ['('], | |
| 'it': ['('], | |
| // 'expect': ['('], | |
| }; | |
| return this.checkForKeyword(keywords, nextAcceptableChar, i, str); | |
| } | |
| checkForJavascriptKeyword(i, str) { | |
| const keywords = [ | |
| KeywordEnum.IMPORT, | |
| KeywordEnum.CLASS, | |
| KeywordEnum.FOR, | |
| KeywordEnum.FOREACH, | |
| KeywordEnum.FUNCTION, | |
| KeywordEnum.NAMESPACE, | |
| ]; | |
| const nextAcceptableChar = { | |
| 'class': [' '], | |
| }; | |
| return this.checkForKeyword(keywords, nextAcceptableChar, i, str); | |
| } | |
| checkForKeyword(keywords, nextAcceptableChar, i, str) { | |
| // console.log('========== check for keyword ===========', str[i]); | |
| for (var k = 0; k < keywords.length; k++) { | |
| var keyword = keywords[k]; | |
| /** | |
| * @desc check that there are enough chars for the keyword check | |
| */ | |
| if (i >= keyword.length) { | |
| /** | |
| * @todo make sure the next acceptable character is a openeing bracket of a space epeneding on the keyword | |
| **/ | |
| const theLastChars = str.substring((i - keyword.length), i); | |
| const theNextChar = str.substring(i, i+1); | |
| const isTheNextCharValid = nextAcceptableChar[keyword] ? | |
| nextAcceptableChar[keyword].find(m => theNextChar === m) : | |
| true; | |
| /** | |
| * @desc we have a hit, lets grab the key word and mark the start and end character index so we can chop up the string later | |
| * @todo isTheNextCharValid in the if statement | |
| */ | |
| if (theLastChars === keyword ) { | |
| // if (theLastChars === keyword && isTheNextCharValid) { | |
| // console.log('β keyword:', {keyword, i, keywordLength: keyword.length, endIndex: i + keyword.length, substr: str.substring(i - keyword.length, i)}) | |
| this.lastKeyWord = keyword; | |
| this.lastKeyWordRange = { | |
| start: i - keyword.length, | |
| length: keyword.length, | |
| end: i, | |
| check: str.substring(i - keyword.length, i) | |
| } | |
| return keyword; | |
| } | |
| } | |
| } | |
| /** | |
| * @desc no matches | |
| */ | |
| return false; | |
| } | |
| convertBlocksToJestModels(){ | |
| this.BlockModels.forEach(blockModel => { | |
| this.convertBlockToJestModel(blockModel); | |
| }); | |
| return; | |
| /** | |
| * @desc NOT USED - refactored to the convertBlockToJestModel method | |
| * @deprecated NOT USED | |
| */ | |
| // this.DescribeModels.map(describeModel => { | |
| // const blockModel = this.BlockModels.find(m=> m.parentId === describeModel.blockModelId); | |
| // const parentBlockModel = this.BlockModels.find(m=> m.uuid === describeModel.blockModelId); | |
| // console.log('π₯π₯π₯π₯ β’convertBlocksToJestModels β’DescribeModels', { | |
| // describeModel, | |
| // blockModelId: blockModel.uuid, | |
| // parentBlockModelId: parentBlockModel.uuid, | |
| // blockModel: {keyWord: blockModel.keyWord, uuid: blockModel.uuid, describeModelId: blockModel.describeModelId, shouldModelId: parentBlockModel.shouldModelId, stringBetweenKeywordAndOpenbracket: blockModel.stringBetweenKeywordAndOpenbracket}, | |
| // parentBlockModel: {keyWord: parentBlockModel.keyWord, uuid: parentBlockModel.uuid, describeModelId: parentBlockModel.describeModelId, stringBetweenKeywordAndOpenbracket: parentBlockModel.stringBetweenKeywordAndOpenbracket} | |
| // }); | |
| // return describeModel; | |
| // /** | |
| // * @todo xxxx | |
| // */ | |
| // /** | |
| // * @desc Parent Block | |
| // */ | |
| // if(parentBlockModel){ | |
| // switch(parentBlockModel.keyWord){ | |
| // /** | |
| // * @desc Parent Block is a descibe model | |
| // */ | |
| // case KeywordEnum.DESCRIBE: | |
| // describeModel.parentId = parentBlockModel.describeModelId; | |
| // const parentDescribeModel = this.DescribeModels.find(m=>m.id === parentBlockModel.describeModelId); | |
| // if(parentDescribeModel){ | |
| // parentDescribeModel.addDescribe(describeModel); | |
| // } | |
| // else console.error('this should not fire', {parentBlockModel, parentDescribeModel}); | |
| // break; | |
| // /** | |
| // * @desc if the Parent Block is not a descibe model, it MUST be a test model | |
| // */ | |
| // default: | |
| // if(parentBlockModel.testModelId) | |
| // describeModel.parentId = parentBlockModel.testModelId; | |
| // describeModel.testModelId = this.TestModel.id; | |
| // this.TestModel.addDescribe(describeModel); | |
| // break; | |
| // } | |
| // } | |
| // /** | |
| // * @desc No Describe Parent - so use the TestModel | |
| // */ | |
| // else{ | |
| // describeModel.testModelId = this.TestModel.id; | |
| // this.TestModel.addDescribe(describeModel); | |
| // } | |
| // console.log('β’ DescribeModels', { | |
| // blockModelId: blockModel.uuid, | |
| // parentBlockModel: parentBlockModel.uuid, | |
| // 'describeModel.parentId': describeModel.parentId | |
| // }); | |
| // }) | |
| /** | |
| * @desc | |
| */ | |
| } | |
| convertBlockToJestModel(blockModel){ | |
| console.log('\n\n\nππππππ'); | |
| /** | |
| * @description find the parent ID | |
| */ | |
| let parentBlockModel, parentDescribeModel; | |
| if(blockModel.parentId){ | |
| parentBlockModel = this.BlockModels.find(m => m.uuid === blockModel.parentId); | |
| if(parentBlockModel) { | |
| parentDescribeModel = this.DescribeModels.find(d => d.id === parentBlockModel.describeModelId); | |
| } | |
| console.log('π β’ convertBlockToJestModel', {'blockModel.parentId':blockModel.parentId, 'blockModel.uuid': blockModel.uuid}) | |
| console.log('π β’ convertBlockToJestModel πparentModel', {parentBlockModel: {uuid: parentBlockModel.uuid, 'parentBlockModel.describeModelId': parentBlockModel.describeModelId, parentDescribeModel}}) | |
| } | |
| console.log('π β’ convertBlockToJestModel', {[blockModel.keyWord]: blockModel.stringBetweenKeywordAndOpenbracket}) | |
| console.log('\n\n\nππππππ'); | |
| switch(blockModel.keyWord){ | |
| case KeywordEnum.DESCRIBE: | |
| console.log('\n\n\nπ₯π₯π₯π₯π₯π₯π₯'); | |
| /** | |
| * @description create a describe block | |
| */ | |
| const describeModel = new DescribeModel(blockModel.stringBetweenKeywordAndOpenbracket, parentDescribeModel ? parentDescribeModel.id : null); | |
| this.DescribeModels.push(describeModel); | |
| /** | |
| * @description link the block model and describe model | |
| */ | |
| describeModel.blockModelId = blockModel.uuid; | |
| blockModel.describeModelId = describeModel.id; | |
| /** | |
| * @desc Add the describeModel to its parent: either a describe or test model | |
| */ | |
| if(parentDescribeModel){ | |
| parentDescribeModel.addDescribe(describeModel); | |
| console.log('π₯ β’ convertBlockToJestModel - adding to the DESCRIBE parent ', {describeModels: parentDescribeModel.describeModels}) | |
| } | |
| else{ | |
| this.TestModel.addDescribe(describeModel); | |
| console.log('π₯ β’ convertBlockToJestModel - adding to the TEST parent ', {describeModels: this.TestModel.describeModels}) | |
| } | |
| // console.log('π₯ β’ convertBlockToJestModel', {'describeModel.parentId': describeModel.parentId, 'describeModel.id': describeModel.id, 'describeModel.blockModelId':describeModel.blockModelId, 'blockModel.describeModelId': blockModel.describeModelId}); | |
| console.log('π₯π₯π₯π₯π₯π₯π₯\n\n\n'); | |
| break; | |
| case KeywordEnum.IT: | |
| console.log('\n\n\nπ¦π¦π¦π¦π¦'); | |
| /** | |
| * @description create a should model | |
| */ | |
| const isPassed = null; | |
| const shouldModel = new ShouldModel(isPassed, blockModel.stringBetweenKeywordAndOpenbracket); | |
| shouldModel.addCode(blockModel.contents); | |
| this.ShouldModels.push(shouldModel); | |
| console.log('π¦ β’ KeywordEnum.IT', {shouldModel}); | |
| /** | |
| * @description link the blcok model and the should model | |
| */ | |
| shouldModel.blockModelId = blockModel.uuid; | |
| blockModel.shouldModelId = shouldModel.id; | |
| /** | |
| * @desc Add the describeModel to its parent: either a describe or test model | |
| */ | |
| if(parentDescribeModel){ | |
| parentDescribeModel.addShould(shouldModel); | |
| console.log('π¦ β’ KeywordEnum.IT - adding SHOULD to the DESCRIBE parent ', {shouldModels: parentDescribeModel.shouldModels}) | |
| } | |
| console.log('π¦π¦π¦π¦π¦\n\n\n'); | |
| break; | |
| } | |
| } | |
| } | |
| /** ππππππππππ | |
| * | |
| * @main | |
| * | |
| */ | |
| const jestResultsParseService = new JestResultsParserService(); | |
| const jestFileParseService = new JestSpecParseService(); | |
| (async () => { | |
| /** | |
| * @step - parse the jest results | |
| */ | |
| const testResults = await jestResultsParseService.runJest(); | |
| console.log(testResults); | |
| /** | |
| * @step - parse the jest file | |
| */ | |
| var txtController = ` | |
| class Hello{ | |
| method1(){ | |
| this.doSomething(); | |
| for(var i = 0; i < something; i++){ | |
| //loop body | |
| } | |
| } | |
| method2(){ | |
| this.doDifferent(); | |
| } | |
| } | |
| export default class TimeService { | |
| static formatTime(timeInMs: number) : string { | |
| let minutes = Math.floor(timeInMs / 60000); | |
| let seconds = (timeInMs % 60000) / 1000; | |
| let timeString = seconds + "s"; | |
| if (minutes !== 0) | |
| { | |
| timeString = minutes + "m" + timeString; | |
| } | |
| return timeString; | |
| } | |
| static formatTimeFull(timeInMs: number) : string { | |
| let minutes = Math.floor(timeInMs / 60000); | |
| let seconds = (timeInMs % 60000) / 1000; | |
| let timeString = seconds + " secs"; | |
| if (minutes !== 0) | |
| { | |
| timeString = minutes + " mins. " + timeString; | |
| } | |
| return timeString; | |
| } | |
| } | |
| ` | |
| const txtTest = ` | |
| import React from 'react'; | |
| import { checkProps } from '../../common/utils/test-helpers'; | |
| import Card from './card.component'; | |
| describe("CardComponent", () => { | |
| describe("CardComponent - Checking prop types", () => { | |
| it ("Should not throw any warnings", () => { | |
| const expectedProps = { | |
| name : "card name" | |
| } | |
| const propErrors = checkProps(Card, expectedProps); | |
| expect(propErrors).toEqual({hello: 1234}); | |
| }) | |
| }) | |
| }) | |
| ` | |
| /** | |
| * | |
| * @desc parse jest file | |
| * | |
| */ | |
| console.log(` | |
| /** ππππππππππππ | |
| * | |
| * π @step parse jest file | |
| * | |
| */ | |
| `); | |
| const testFileName = '/path/to/testfile.test.tsx'; | |
| const blocks = jestFileParseService.parseJestFile(testFileName, txtTest); | |
| jestFileParseService.convertBlocksToJestModels(); | |
| console.log('\n TEST MODELS', jestFileParseService.TestModel); | |
| console.log('DESCRIBE MODELS', jestFileParseService.DescribeModels); | |
| console.log('SHOULD MODELS', jestFileParseService.ShouldModels.map(s => s.toJson())); | |
| })(); | |
| module.exports = {JestResultsParserService, JestSpecParseService} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment