├── .vscode └── settings.json ├── bin └── concordia.js ├── media ├── process.png ├── example-en.gif └── concordia-init.gif ├── mkdocs.yml ├── docs ├── README.md ├── concordia.asta ├── en │ ├── dev │ │ ├── dependencies.md │ │ ├── states.md │ │ ├── queries.md │ │ └── data-generation.md │ ├── versioning.md │ ├── readme.md │ ├── migration.md │ ├── faq.md │ ├── cycle.md │ ├── how-it-works.md │ ├── plugins │ │ └── codeceptjs.md │ └── introduction.md └── pt │ ├── versioning.md │ ├── migration.md │ ├── faq.md │ ├── readme.md │ ├── cycle.md │ ├── plugins │ └── codeceptjs.md │ └── how-it-works.md ├── .gitignore ├── CHANGELOG.md ├── modules ├── language │ ├── index.ts │ ├── LanguageDictionary.ts │ ├── data │ │ └── map.ts │ └── KeywordDictionary.ts ├── util │ ├── file │ │ ├── FileEraser.ts │ │ ├── path-transformer.ts │ │ ├── FileWriter.ts │ │ ├── index.ts │ │ ├── FileHandler.ts │ │ ├── FileReader.ts │ │ ├── FileChecker.ts │ │ ├── DirSearcher.ts │ │ └── FileSearcher.ts │ ├── fs │ │ ├── index.ts │ │ └── ext-changer.ts │ ├── CaseType.ts │ ├── type-checking.ts │ ├── index.ts │ ├── p-all.ts │ ├── remove-duplicated.ts │ ├── matches.ts │ ├── case-conversor.ts │ └── best-match.ts ├── dbi │ ├── index.ts │ ├── Queryable.ts │ ├── InMemoryTableInterface.ts │ └── DatabaseInterface.ts ├── ast │ ├── FileInfo.ts │ ├── Constant.ts │ ├── ListItem.ts │ ├── Regex.ts │ ├── LongString.ts │ ├── Language.ts │ ├── Text.ts │ ├── Background.ts │ ├── RegexBlock.ts │ ├── ConstantBlock.ts │ ├── VariantBackground.ts │ ├── Variant.ts │ ├── Spec.ts │ ├── UIElement.ts │ ├── UIPropertyTypes.ts │ ├── UIPropertyReference.ts │ ├── Block.ts │ ├── Scenario.ts │ ├── Table.ts │ ├── Import.ts │ ├── Feature.ts │ ├── Task.ts │ ├── Node.ts │ ├── VariantLike.ts │ ├── index.ts │ ├── TestEvent.ts │ └── Database.ts ├── compiler │ ├── FileCompilationListener.ts │ └── CompilerListener.ts ├── plugin │ ├── index.ts │ ├── PluginFinder.ts │ └── PluginListener.ts ├── error │ ├── Warning.ts │ ├── index.ts │ ├── SemanticException.ts │ ├── RuntimeException.ts │ ├── FileProblemMapper.ts │ ├── LocatedException.ts │ └── ErrorSorting.ts ├── testdata │ ├── limits │ │ ├── DoubleLimits.ts │ │ ├── LongLimits.ts │ │ ├── DateLimits.ts │ │ ├── StringLimits.ts │ │ ├── TimeLimits.ts │ │ └── DateTimeLimits.ts │ ├── random │ │ ├── README.md │ │ ├── Random.ts │ │ ├── RandomDouble.ts │ │ ├── RandomLong.ts │ │ ├── RandomDate.ts │ │ ├── RandomTime.ts │ │ ├── RandomShortTime.ts │ │ ├── RandomDateTime.ts │ │ └── RandomShortDateTime.ts │ ├── raw │ │ ├── RangeAnalyzer.ts │ │ └── RawDataGenerator.ts │ ├── index.ts │ ├── InvertedLogicListBasedDataGenerator.ts │ ├── InvertedLogicQueryBasedDataGenerator.ts │ └── DataTestCaseNames.ts ├── lexer │ ├── LexicalException.ts │ ├── KeywordBasedLexer.ts │ ├── TableLexer.ts │ ├── FeatureLexer.ts │ ├── UIPropertyLexer.ts │ ├── DatabaseLexer.ts │ ├── RegexBlockLexer.ts │ ├── TestCaseLexer.ts │ ├── StepThenLexer.ts │ ├── UIElementLexer.ts │ ├── VariantLexer.ts │ ├── DatabasePropertyLexer.ts │ ├── StepWhenLexer.ts │ ├── StepOtherwiseLexer.ts │ ├── ConstantBlockLexer.ts │ ├── StepAndLexer.ts │ ├── BackgroundLexer.ts │ ├── StepGivenLexer.ts │ ├── ScenarioLexer.ts │ ├── VariantBackgroundLexer.ts │ ├── ImportLexer.ts │ ├── NamePlusNumberNodeLexer.ts │ ├── NodeLexer.ts │ ├── LongStringLexer.ts │ ├── index.ts │ ├── TextLexer.ts │ └── CommentHandler.ts ├── nlp │ ├── NLPException.ts │ ├── Intents.ts │ ├── NLPEntity.ts │ ├── index.ts │ ├── EntityHandler.ts │ ├── NLPResult.ts │ ├── syntax │ │ ├── SyntaxRuleBuilder.ts │ │ └── SyntaxRule.ts │ └── NLPUtil.ts ├── db │ ├── AlaSqlTypes.ts │ ├── index.ts │ ├── QueryCache.ts │ └── database-package-manager.ts ├── parser │ ├── SyntacticException.ts │ ├── ListItemNodeParser.ts │ ├── TagCollector.ts │ ├── NodeParser.ts │ ├── TextCollector.ts │ ├── DatabaseParser.ts │ ├── RegexBlockParser.ts │ ├── ConstantBlockParser.ts │ ├── TableRowParser.ts │ ├── TableParser.ts │ ├── ImportParser.ts │ ├── AfterAllParser.ts │ ├── BeforeAllParser.ts │ ├── ScenarioParser.ts │ ├── index.ts │ ├── AfterFeatureParser.ts │ ├── BeforeFeatureParser.ts │ ├── ListItemParser.ts │ ├── StepOtherwiseParser.ts │ └── AfterEachScenarioParser.ts ├── req │ ├── index.ts │ ├── DocumentProcessor.ts │ ├── NodeTypes.ts │ ├── LineChecker.ts │ ├── Symbols.ts │ └── Expressions.ts ├── semantic │ ├── single │ │ ├── index.ts │ │ ├── DocumentAnalyzer.ts │ │ ├── BatchDocumentAnalyzer.ts │ │ └── ScenarioDA.ts │ ├── index.ts │ ├── SpecificationAnalyzer.ts │ ├── TableSSA.ts │ ├── ConstantSSA.ts │ └── DatabaseSSA.ts ├── testscenario │ └── index.ts ├── testscript │ └── TestScriptExecutionListener.ts ├── testcase │ ├── TestCaseGeneratorListener.ts │ ├── UIETestPlan.ts │ └── TestPlan.ts ├── report │ ├── TestReporter.ts │ └── JSONTestReporter.ts ├── main.ts └── selection │ ├── TagUtil.ts │ └── FilterCriterion.ts ├── __tests__ ├── db │ ├── users.json │ ├── ConnectionResult.spec.ts │ ├── QueryParser.spec.ts │ └── database-package-manager.spec.ts ├── tsconfig.json ├── util │ ├── case-conversor.spec.ts │ ├── matches.spec.ts │ ├── escape.spec.ts │ ├── package-installation.spec.ts │ ├── file │ │ └── ext-changer.spec.ts │ ├── best-match.spec.ts │ └── remove-duplicated.spec.ts ├── selection │ ├── CombinationStrategy.spec.ts │ └── TagUtil.spec.ts ├── req │ └── Expressions.spec.ts ├── compiler │ └── CompilerFacade.spec.ts ├── testdata │ └── random │ │ ├── RandomLong.spec.ts │ │ ├── RandomDouble.spec.ts │ │ ├── RandomDate.spec.ts │ │ ├── RandomTime.spec.ts │ │ ├── RandomShortTime.spec.ts │ │ ├── RandomDateTime.spec.ts │ │ └── RandomShortDateTime.spec.ts ├── lexer │ ├── TextLexer.spec.ts │ ├── StepWhenLexer.spec.ts │ ├── StepThenLexer.spec.ts │ ├── StepAndLexer.spec.ts │ ├── StepGivenLexer.spec.ts │ ├── DatabaseLexer.spec.ts │ ├── ConstantBlockLexer.spec.ts │ └── RegexBlockLexer.spec.ts ├── plugin │ └── plugin-loader.spec.ts ├── language │ └── locale-manager.spec.ts ├── SimpleCompiler.ts └── nlp │ └── SyntaxRuleBuilder.spec.ts ├── launch.json ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── LICENSE.txt ├── sonar-project.properties ├── tsconfig.json └── lib └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /bin/concordia.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import '../dist/main.js'; 3 | -------------------------------------------------------------------------------- /media/process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagodp/concordialang/HEAD/media/process.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Concordia 2 | theme: readthedocs 3 | nav: 4 | - Home: ../README.md -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - [English](en/readme.md) 4 | - [Português](pt/readme.md) -------------------------------------------------------------------------------- /docs/concordia.asta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagodp/concordialang/HEAD/docs/concordia.asta -------------------------------------------------------------------------------- /media/example-en.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagodp/concordialang/HEAD/media/example-en.gif -------------------------------------------------------------------------------- /media/concordia-init.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagodp/concordialang/HEAD/media/concordia-init.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | docs/features/*.testcase 4 | .scannerwork 5 | coverage 6 | sonar-report.xml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Please see [our releases](https://github.com/thiagodp/concordialang/releases). -------------------------------------------------------------------------------- /modules/language/index.ts: -------------------------------------------------------------------------------- 1 | export * from './KeywordDictionary'; 2 | export * from './LanguageDictionary'; 3 | export * from './locale-manager'; 4 | -------------------------------------------------------------------------------- /modules/util/file/FileEraser.ts: -------------------------------------------------------------------------------- 1 | export interface FileEraser { 2 | 3 | erase( filePath: string, checkIfExists: boolean ): Promise< boolean >; 4 | 5 | } -------------------------------------------------------------------------------- /modules/util/fs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FSDirSearcher" 2 | export * from "./FSFileHandler" 3 | export * from "./FSFileSearcher" 4 | export * from "./ext-changer" 5 | -------------------------------------------------------------------------------- /modules/dbi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConnectionResult'; 2 | export * from './DatabaseInterface'; 3 | export * from './InMemoryTableInterface'; 4 | export * from './Queryable'; 5 | -------------------------------------------------------------------------------- /modules/ast/FileInfo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File information. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export interface FileInfo { 7 | path: string; 8 | hash: string; 9 | } -------------------------------------------------------------------------------- /modules/util/CaseType.ts: -------------------------------------------------------------------------------- 1 | export enum CaseType { 2 | CAMEL = 'camel', 3 | PASCAL = 'pascal', 4 | SNAKE = 'snake', 5 | KEBAB = 'kebab', // a.k.a. dash 6 | NONE = 'none' 7 | } -------------------------------------------------------------------------------- /modules/ast/Constant.ts: -------------------------------------------------------------------------------- 1 | import { BlockItem } from './Block'; 2 | 3 | /** 4 | * Constant node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface Constant extends BlockItem { 9 | } -------------------------------------------------------------------------------- /modules/ast/ListItem.ts: -------------------------------------------------------------------------------- 1 | import { ContentNode } from "./Node"; 2 | 3 | /** 4 | * List item node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface ListItem extends ContentNode { 9 | } -------------------------------------------------------------------------------- /modules/ast/Regex.ts: -------------------------------------------------------------------------------- 1 | import { BlockItem } from './Block'; 2 | 3 | /** 4 | * Regular expression node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface Regex extends BlockItem { 9 | } -------------------------------------------------------------------------------- /modules/ast/LongString.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | 3 | /** 4 | * Long String, also known as Py String. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface LongString extends Node { 9 | } -------------------------------------------------------------------------------- /modules/compiler/FileCompilationListener.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface FileCompilationListener { 3 | 4 | fileStarted( path: string ): void; 5 | 6 | fileFinished( path: string, durationMS: number ): void; 7 | 8 | } -------------------------------------------------------------------------------- /modules/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PackageBasedPluginFinder'; 2 | export * from './plugin-loader'; 3 | export * from './PluginData'; 4 | export * from './PluginFinder'; 5 | export * from './PluginListener'; 6 | -------------------------------------------------------------------------------- /modules/ast/Language.ts: -------------------------------------------------------------------------------- 1 | import { ValuedNode } from './Node'; 2 | 3 | /** 4 | * Language node 5 | * 6 | * @author Thiago Delgado Pinto 7 | * @see /doc/langspec/asl-en.md 8 | */ 9 | export interface Language extends ValuedNode { 10 | } -------------------------------------------------------------------------------- /modules/ast/Text.ts: -------------------------------------------------------------------------------- 1 | import { ContentNode } from './Node'; 2 | 3 | /** 4 | * Text node. Occurs when all the other nodes are not recognized. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface Text extends ContentNode { 9 | } -------------------------------------------------------------------------------- /__tests__/db/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "Alice", "username": "alice", "password": "a-l1-c3", "age": 21 }, 3 | { "name": "Bob", "username": "bob", "password": "b04P4s$", "age": 53 }, 4 | { "name": "Jack", "username": "jack", "password": "jaaacCkK", "age": 16 } 5 | ] -------------------------------------------------------------------------------- /modules/ast/Background.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | import { Step } from './Step'; 3 | 4 | /** 5 | * Background node. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export interface Background extends Node { 10 | sentences: Array< Step >; 11 | } -------------------------------------------------------------------------------- /modules/error/Warning.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from "./LocatedException"; 2 | 3 | /** 4 | * Warning 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class Warning extends LocatedException { 9 | name = 'Warning'; 10 | isWarning = true; 11 | } -------------------------------------------------------------------------------- /modules/testdata/limits/DoubleLimits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Limits for double values. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export abstract class DoubleLimits { 7 | static MIN: number = Number.MIN_SAFE_INTEGER; 8 | static MAX: number = Number.MAX_SAFE_INTEGER; 9 | } -------------------------------------------------------------------------------- /modules/testdata/limits/LongLimits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Limits for date values. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export abstract class LongLimits { 7 | static MIN: number = Number.MIN_SAFE_INTEGER; 8 | static MAX: number = Number.MAX_SAFE_INTEGER; 9 | } -------------------------------------------------------------------------------- /modules/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorSorting'; 2 | export * from './FileProblemMapper'; 3 | export * from './LocatedException'; 4 | export * from './ProblemMapper'; 5 | export * from './RuntimeException'; 6 | export * from './SemanticException'; 7 | export * from './Warning'; 8 | -------------------------------------------------------------------------------- /modules/util/file/path-transformer.ts: -------------------------------------------------------------------------------- 1 | 2 | export function toUnixPath( path: string ): string { 3 | return path ? path.replace( /\\\\?/g, '/' ) : ''; 4 | } 5 | 6 | export function toWindowsPath( path: string ): string { 7 | return path ? path.replace( /\//g, '\\\\' ) : ''; 8 | } -------------------------------------------------------------------------------- /modules/error/SemanticException.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeException } from './RuntimeException'; 2 | 3 | /** 4 | * Semantic exception. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class SemanticException extends RuntimeException { 9 | name = 'SemanticException' 10 | } -------------------------------------------------------------------------------- /modules/lexer/LexicalException.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from "../error/LocatedException"; 2 | 3 | /** 4 | * Lexical exception. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class LexicalException extends LocatedException { 9 | name = 'LexicalError' 10 | } -------------------------------------------------------------------------------- /modules/nlp/NLPException.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from '../error/LocatedException'; 2 | 3 | /** 4 | * Natural Language Processing Exception 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class NLPException extends LocatedException { 9 | name = 'NLPError' 10 | } -------------------------------------------------------------------------------- /modules/util/file/FileWriter.ts: -------------------------------------------------------------------------------- 1 | export interface FileWriter { 2 | 3 | /** 4 | * Writes a file 5 | * 6 | * @param filePath File path 7 | * @param cotent Content to write 8 | */ 9 | write( filePath: string, content: string ): Promise< void >; 10 | 11 | } -------------------------------------------------------------------------------- /modules/db/AlaSqlTypes.ts: -------------------------------------------------------------------------------- 1 | import alasql from 'alasql'; 2 | 3 | // @ts-ignore 4 | export class AlaSqlDatabase extends alasql.Database { // declare as a type does not work 5 | 6 | exec( cmd: string, params?: any, cb?: any ) { 7 | return super.exec( cmd, params, cb ); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /modules/parser/SyntacticException.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from "../error/LocatedException"; 2 | 3 | /** 4 | * Syntactic exception 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class SyntacticException extends LocatedException { 9 | name = 'SyntacticError' 10 | } -------------------------------------------------------------------------------- /modules/req/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AugmentedSpec'; 2 | export * from './DocumentProcessor'; 3 | export * from './DocumentUtil'; 4 | export * from './Expressions'; 5 | export * from './Keywords'; 6 | export * from './LineChecker'; 7 | export * from './NodeTypes'; 8 | export * from './Symbols'; 9 | -------------------------------------------------------------------------------- /modules/semantic/single/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BatchDocumentAnalyzer'; 2 | export * from './DatabaseDA'; 3 | export * from './DocumentAnalyzer'; 4 | export * from './ImportDA'; 5 | export * from './ScenarioDA'; 6 | export * from './UIElementDA'; 7 | export * from './VariantGivenStepDA'; 8 | 9 | -------------------------------------------------------------------------------- /modules/ast/RegexBlock.ts: -------------------------------------------------------------------------------- 1 | import { Block } from './Block'; 2 | import { Node } from './Node'; 3 | import { Regex } from './Regex'; 4 | 5 | /** 6 | * Regular expression block node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface RegexBlock extends Node, Block< Regex > { 11 | } -------------------------------------------------------------------------------- /modules/util/file/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DirSearcher'; 2 | export * from './FileChecker'; 3 | export * from './FileEraser'; 4 | export * from './FileHandler'; 5 | export * from './FileReader'; 6 | export * from './FileSearcher'; 7 | export * from './FileWriter'; 8 | export * from './path-transformer'; 9 | -------------------------------------------------------------------------------- /modules/ast/ConstantBlock.ts: -------------------------------------------------------------------------------- 1 | import { Block } from './Block'; 2 | import { Constant } from './Constant'; 3 | import { Node } from './Node'; 4 | 5 | /** 6 | * Constant block node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface ConstantBlock extends Node, Block< Constant > { 11 | } -------------------------------------------------------------------------------- /modules/nlp/Intents.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Intents for NLP 3 | * 4 | * @author Thiago Delgado Pinto 5 | * 6 | * @see Entities 7 | */ 8 | export enum Intents { 9 | ALL = '*', 10 | TEST_CASE = 'testcase', 11 | UI = 'ui', 12 | // UI_ITEM_QUERY = 'ui_item_query', 13 | DATABASE = 'database' 14 | } -------------------------------------------------------------------------------- /modules/ast/VariantBackground.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | import { VariantLike } from './VariantLike'; 3 | 4 | /** 5 | * Variant Background 6 | * 7 | * @author Thiago Delgado Pinto 8 | * 9 | * @see VariantLike 10 | */ 11 | export interface VariantBackground extends VariantLike, Node { 12 | } 13 | -------------------------------------------------------------------------------- /modules/testdata/random/README.md: -------------------------------------------------------------------------------- 1 | Most of the classes on this folder was based on [FunTester](https://github.com/funtester/funtester)'s classes for random value generation. See [funtester.common.util.rand](https://github.com/funtester/funtester/tree/master/funtester/funtester-common/src/main/java/org/funtester/common/util/rand). -------------------------------------------------------------------------------- /modules/util/file/FileHandler.ts: -------------------------------------------------------------------------------- 1 | import { FileChecker } from "./FileChecker"; 2 | import { FileEraser } from "./FileEraser"; 3 | import { FileReader } from "./FileReader"; 4 | import { FileWriter } from "./FileWriter"; 5 | 6 | export interface FileHandler 7 | extends FileChecker, FileReader, FileWriter, FileEraser { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | 5 | "module": "ES2015", 6 | "target": "ES2018", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | 10 | "lib": [ 11 | "ES2015", 12 | "DOM" 13 | ] 14 | }, 15 | "include": [ 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /modules/ast/Variant.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from './Node'; 2 | import { MayHaveTags } from './Tag'; 3 | import { State, VariantLike } from './VariantLike'; 4 | 5 | /** 6 | * Variant 7 | * 8 | * @see VariantLike 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export interface Variant extends VariantLike, NamedNode, MayHaveTags { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /modules/testdata/limits/DateLimits.ts: -------------------------------------------------------------------------------- 1 | import { LocalDate } from "@js-joda/core"; 2 | 3 | /** 4 | * Limits for date values. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export abstract class DateLimits { 9 | static MIN: LocalDate = LocalDate.of( 0, 1, 1 ); // 0000-01-01 10 | static MAX: LocalDate = LocalDate.of( 9999, 12, 31 ); // 9999-12-31 11 | } -------------------------------------------------------------------------------- /modules/semantic/single/DocumentAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '../../ast/Document'; 2 | import { SemanticException } from '../../error/SemanticException'; 3 | 4 | /** 5 | * Document analyzer. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export interface DocumentAnalyzer { 10 | 11 | analyze( doc: Document, errors: SemanticException[] ): void; 12 | 13 | } -------------------------------------------------------------------------------- /modules/ast/Spec.ts: -------------------------------------------------------------------------------- 1 | import { Document } from './Document'; 2 | 3 | /** 4 | * Specification 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class Spec { 9 | 10 | public basePath: string = null; 11 | 12 | public docs: Document[] = []; 13 | 14 | constructor( basePath?: string ) { 15 | this.basePath = basePath || process.cwd(); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /modules/dbi/Queryable.ts: -------------------------------------------------------------------------------- 1 | export interface Queryable { 2 | 3 | /** 4 | * Queries the database. 5 | * 6 | * @param cmd Command to execute. 7 | * @param params Parameters of the command. Optional. 8 | * @return A promise to an array of values, usually objects. 9 | */ 10 | query( cmd: string, params?: any[] ): Promise< any[] >; 11 | 12 | } -------------------------------------------------------------------------------- /modules/plugin/PluginFinder.ts: -------------------------------------------------------------------------------- 1 | import { NewOrOldPluginData } from './PluginData'; 2 | 3 | /** 4 | * Finds plug-ins that generate and execute test scripts. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface PluginFinder { 9 | 10 | /** 11 | * Finds plug-ins and returns their data. 12 | */ 13 | find(): Promise< NewOrOldPluginData[] >; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/util/file/FileReader.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface FileReader { 3 | 4 | /** 5 | * Reads a file content from a path. 6 | * 7 | * @param filePath File path 8 | */ 9 | read( filePath: string ): Promise< string >; 10 | 11 | /** 12 | * Reads a file content from a path. 13 | * 14 | * @param filePath File path 15 | */ 16 | readSync( filePath: string ): string; 17 | 18 | } -------------------------------------------------------------------------------- /modules/ast/UIElement.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from './Node'; 2 | import { MayHaveTags } from './Tag'; 3 | import { UIElementInfo, UIProperty } from './UIProperty'; 4 | 5 | /** 6 | * UI element node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface UIElement extends NamedNode, MayHaveTags { 11 | items: UIProperty[]; 12 | info?: UIElementInfo; // information added during the semantic analysis 13 | } -------------------------------------------------------------------------------- /modules/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AlaSqlDatabaseInterface'; 2 | export * from './AlaSqlTableCreator'; 3 | export * from './AlaSqlTypes'; 4 | export * from './DatabaseConnectionChecker'; 5 | export * from './DatabaseJSDatabaseInterface'; 6 | export * from './DatabaseToAbstractDatabase'; 7 | export * from './DatabaseTypes'; 8 | export * from './QueryCache'; 9 | export * from './QueryParser'; 10 | export * from './SqlHelper'; 11 | -------------------------------------------------------------------------------- /modules/testdata/raw/RangeAnalyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Range analyzer 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export interface RangeAnalyzer { 7 | 8 | hasValuesBelowMin(): boolean; 9 | 10 | hasValuesAboveMax(): boolean; 11 | 12 | hasValuesBetweenMinAndMax(): boolean; 13 | 14 | isZeroBetweenMinAndMax(): boolean; 15 | 16 | isZeroBelowMin(): boolean; 17 | 18 | isZeroAboveMax(): boolean; 19 | 20 | } -------------------------------------------------------------------------------- /modules/semantic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AfterAllSSA'; 2 | export * from './BatchSpecificationAnalyzer'; 3 | export * from './BeforeAllSSA'; 4 | export * from './ConstantSSA'; 5 | export * from './DatabaseSSA'; 6 | export * from './DuplicationChecker'; 7 | export * from './FeatureSSA'; 8 | export * from './ImportSSA'; 9 | export * from './SpecificationAnalyzer'; 10 | export * from './TableSSA'; 11 | export * from './TestCaseSSA'; 12 | -------------------------------------------------------------------------------- /modules/testdata/limits/StringLimits.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Limits for string values. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export abstract class StringLimits { 7 | static MIN: number = 0; 8 | static MAX: number = 32767; // max short 9 | // Since MAX can produce very long strings for testing purposes, we are also defining 10 | // a "usual" maximum length value. 11 | static MAX_USUAL: number = 127; // max byte 12 | } -------------------------------------------------------------------------------- /docs/en/dev/dependencies.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | About the virtual namespaces: 4 | - `ast` depends on 5 | - `nlp`: 6 | - `Step` depends on `NLPResult` 7 | - `UIElement` depends on `Entities` and `NLPResult` 8 | - `nlp` has no dependencies 9 | - `req` depends on 10 | - `ast`: 11 | - `DatabaseInterface` depends on `Database` 12 | - `InMemoryTableInterface` depends on `Table` 13 | - `LocatedException` depends on `Location` 14 | -------------------------------------------------------------------------------- /modules/ast/UIPropertyTypes.ts: -------------------------------------------------------------------------------- 1 | export enum UIPropertyTypes { 2 | ID = 'id', 3 | TYPE = 'type', 4 | EDITABLE = 'editable', 5 | DATA_TYPE = 'datatype', 6 | VALUE = 'value', 7 | MIN_LENGTH = 'minlength', 8 | MAX_LENGTH = 'maxlength', 9 | MIN_VALUE = 'minvalue', 10 | MAX_VALUE = 'maxvalue', 11 | FORMAT = 'format', 12 | REQUIRED = 'required', 13 | LOCALE = 'locale', 14 | LOCALE_FORMAT = 'localeFormat', 15 | } -------------------------------------------------------------------------------- /modules/language/LanguageDictionary.ts: -------------------------------------------------------------------------------- 1 | import { NLPTrainingIntentExample } from '../nlp'; 2 | import { DataTestCaseNames } from '../testdata/DataTestCaseNames'; 3 | import { KeywordDictionary } from './KeywordDictionary'; 4 | 5 | export interface LanguageDictionary { 6 | 7 | keywords: KeywordDictionary; 8 | 9 | nlp: object; 10 | 11 | training: NLPTrainingIntentExample[]; 12 | 13 | testCaseNames: DataTestCaseNames; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/ast/UIPropertyReference.ts: -------------------------------------------------------------------------------- 1 | import { Location } from "concordialang-types"; 2 | import { ContentNode } from "./Node"; 3 | import { UIPropertyTypes } from "./UIPropertyTypes"; 4 | 5 | 6 | export class UIPropertyReference implements ContentNode { 7 | 8 | nodeType: string = 'ui_property_ref'; 9 | location: Location = null; 10 | content: string; 11 | 12 | uiElementName: string; 13 | property: UIPropertyTypes | string; 14 | 15 | } -------------------------------------------------------------------------------- /modules/util/file/FileChecker.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface FileChecker { 3 | 4 | /** 5 | * Returns `true` whether the given file exists. 6 | * 7 | * @param filePath File path 8 | */ 9 | exists( filePath: string ): Promise< boolean >; 10 | 11 | /** 12 | * Returns `true` whether the given file exists. 13 | * 14 | * @param filePath File path 15 | */ 16 | existsSync( filePath: string ): boolean; 17 | 18 | } -------------------------------------------------------------------------------- /launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/.bin/jest", 11 | "--runInBand" 12 | ], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /modules/ast/Block.ts: -------------------------------------------------------------------------------- 1 | import { ListItem } from './ListItem'; 2 | import { HasItems, HasName, HasValue } from './Node'; 3 | 4 | /** 5 | * Block node. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export interface Block< T extends BlockItem > extends HasItems< T > { 10 | items: T[]; 11 | } 12 | 13 | /** 14 | * Block item node. 15 | * 16 | * @author Thiago Delgado Pinto 17 | */ 18 | export interface BlockItem extends ListItem, HasName, HasValue { 19 | } -------------------------------------------------------------------------------- /__tests__/util/case-conversor.spec.ts: -------------------------------------------------------------------------------- 1 | import { removeDiacritics } from '../../modules/util/case-conversor'; 2 | 3 | describe( 'case-conversor', () => { 4 | 5 | describe( '#removeDiacritics', () => { 6 | 7 | it.each( [ 8 | [ 'àäãâáÀÄÃÂÁ', 'aaaaaAAAAA' ], 9 | [ 'çÇñÑ', 'cCnN' ], 10 | ] )( '%s -> %s', ( given, expected ) => { 11 | const r = removeDiacritics( given ); 12 | expect( r ).toEqual( expected ); 13 | } ); 14 | 15 | } ); 16 | 17 | } ); 18 | -------------------------------------------------------------------------------- /modules/ast/Scenario.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from './Node'; 2 | import { Step } from './Step'; 3 | import { Variant } from './Variant'; 4 | import { VariantBackground } from './VariantBackground'; 5 | 6 | /** 7 | * Scenario node. 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export interface Scenario extends NamedNode { 12 | description?: string; 13 | sentences: Step[]; 14 | variantBackground?: VariantBackground; 15 | variants?: Variant[]; 16 | } -------------------------------------------------------------------------------- /modules/nlp/NLPEntity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NLP Entity. Currently it has the same structure of Bravey's Entity. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export interface NLPEntity { 7 | entity: string; // Entity type. 8 | string: string; // Raw text representing the entity. 9 | position: number; // Entity position in a sentence. 10 | value: any; // Entity logic value. 11 | priority: number; // Entity relative priority. 12 | } -------------------------------------------------------------------------------- /modules/error/RuntimeException.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from "./LocatedException"; 2 | 3 | /** 4 | * Runtime exception 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class RuntimeException extends LocatedException { 9 | name = 'RuntimeException'; 10 | 11 | public static createFrom( error: Error ): RuntimeException { 12 | const e = new RuntimeException( error.message ); 13 | e.stack = error.stack; 14 | return e; 15 | } 16 | } -------------------------------------------------------------------------------- /modules/lexer/KeywordBasedLexer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines a common interface for lexers based on keywords. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export interface KeywordBasedLexer { 7 | 8 | /** 9 | * Returns the affect dictionary keyword. 10 | */ 11 | affectedKeyword(): string; 12 | 13 | /** 14 | * Update the words. 15 | * 16 | * @param words Words to be updated. 17 | */ 18 | updateWords( words: string[] ); 19 | 20 | } -------------------------------------------------------------------------------- /__tests__/util/matches.spec.ts: -------------------------------------------------------------------------------- 1 | import { matches } from '../../modules/util/matches'; 2 | 3 | describe( '#matches', () => { 4 | 5 | it( 'returns the full match and group match by default', () => { 6 | let r = matches( /(foo)/, 'foo' ); 7 | expect( r ).toEqual( [ 'foo', 'foo' ] ); 8 | } ); 9 | 10 | it( 'can ignore full matches', () => { 11 | let r = matches( /(foo)/, 'foo', true ); 12 | expect( r ).toEqual( [ 'foo' ] ); 13 | } ); 14 | 15 | } ); 16 | -------------------------------------------------------------------------------- /modules/ast/Table.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode, Node } from './Node'; 2 | 3 | /** 4 | * Table node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface Table extends NamedNode { 9 | 10 | /** Name converted to snake_case, generated when parsed */ 11 | internalName: string; 12 | 13 | rows: TableRow[]; 14 | } 15 | 16 | /** 17 | * Table row node. 18 | * 19 | * @author Thiago Delgado Pinto 20 | */ 21 | export interface TableRow extends Node { 22 | cells: string[]; 23 | } -------------------------------------------------------------------------------- /modules/testscenario/index.ts: -------------------------------------------------------------------------------- 1 | export * from './locale'; 2 | export * from './LocaleContext'; 3 | export * from './PreTestCase'; 4 | export * from './PreTestCaseGenerator'; 5 | export * from './ReferenceReplacer'; 6 | export * from './StepHandler'; 7 | export * from './TargetTypeUtil'; 8 | export * from './TestScenario'; 9 | export * from './TestScenarioGenerator'; 10 | export * from './UIPropertyReferenceReplacer'; 11 | export * from './value-formatter'; 12 | export * from './VariantStateDetector'; 13 | -------------------------------------------------------------------------------- /modules/parser/ListItemNodeParser.ts: -------------------------------------------------------------------------------- 1 | import { ListItem } from '../ast/ListItem'; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { ParsingContext } from './ParsingContext'; 4 | 5 | /** 6 | * List item node parser. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface ListItemNodeParser { 11 | 12 | isAccepted( node: ListItem, it: NodeIterator ): boolean; 13 | 14 | handle( node: ListItem, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean; 15 | 16 | } -------------------------------------------------------------------------------- /modules/ast/Import.ts: -------------------------------------------------------------------------------- 1 | import { ValuedNode } from './Node'; 2 | 3 | /** 4 | * Import node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | * @see /doc/langspec/asl-en.md 8 | */ 9 | export interface Import extends ValuedNode { 10 | 11 | // Path according to the location of the current document. For instance, 12 | // if the current document is in "some/dir" and the import is "../file.ext", 13 | // then the resolved path will be "some/file.ext". 14 | // @see ImportSDA 15 | resolvedPath?: string; 16 | } -------------------------------------------------------------------------------- /modules/error/FileProblemMapper.ts: -------------------------------------------------------------------------------- 1 | import { toUnixPath } from '../util/file/path-transformer'; 2 | import { ProblemMapper, GENERIC_ERROR_KEY } from './ProblemMapper'; 3 | 4 | /** 5 | * Maps file paths to errors. 6 | */ 7 | export class FileProblemMapper extends ProblemMapper { 8 | 9 | constructor() { 10 | super( true ); 11 | } 12 | 13 | /** @inheritDoc */ 14 | protected convertKey( key: string ): string { 15 | return GENERIC_ERROR_KEY === key ? key : toUnixPath( key ); 16 | } 17 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This project uses EditorConfig (http://editorconfig.org) to standardize 2 | # tabulation, charset and other attributes of source files. Please head to 3 | # http://editorconfig.org/#download to check if your editor comes with 4 | # EditorConfig bundled or to download a plugin. 5 | 6 | root = true 7 | 8 | [*] 9 | charset = utf-8 10 | indent_style = tab 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [{*.json,*.yml}] 16 | indent_style = space 17 | indent_size = 2 -------------------------------------------------------------------------------- /modules/language/data/map.ts: -------------------------------------------------------------------------------- 1 | import { LanguageDictionary } from '../LanguageDictionary'; 2 | import en from './en'; 3 | import pt from './pt'; 4 | 5 | export type LanguageMap = Record< string, LanguageDictionary >; 6 | 7 | export const availableLanguages = [ 'en', 'pt' ]; 8 | 9 | const map: LanguageMap = { 10 | 'en': en, 11 | 'pt': pt, 12 | }; 13 | 14 | export function dictionaryForLanguage( language: string ): LanguageDictionary { 15 | return map[ language ] || en; 16 | } 17 | 18 | export default map; 19 | 20 | 21 | -------------------------------------------------------------------------------- /__tests__/selection/CombinationStrategy.spec.ts: -------------------------------------------------------------------------------- 1 | import { OneWiseStrategy } from "../../modules/selection/CombinationStrategy"; 2 | 3 | describe( 'CombinationStrategy', () => { 4 | 5 | it( 'OneWiseStrategy', () => { 6 | 7 | const obj = { 8 | "a": [ 1, 2, 3 ], 9 | "b": [ "A", "B" ] 10 | }; 11 | 12 | const s = new OneWiseStrategy( "seed-example" ); 13 | const r = s.combine( obj ); 14 | 15 | expect( r ).toEqual( [ 16 | { "a": 1, "b": "A" }, 17 | { "a": 2, "b": "B" }, 18 | { "a": 3, "b": "A" }, 19 | ] ); 20 | } ); 21 | 22 | } ); 23 | -------------------------------------------------------------------------------- /modules/lexer/TableLexer.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../ast/Table"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a Table. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class TableLexer extends NamedNodeLexer< Table > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.TABLE ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.TABLE_ROW ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/testscript/TestScriptExecutionListener.ts: -------------------------------------------------------------------------------- 1 | import { TestScriptExecutionResult } from "concordialang-types"; 2 | 3 | /** 4 | * Script execution listener 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface TestScriptExecutionListener { 9 | 10 | testScriptExecutionDisabled(): void; 11 | 12 | announceTestScriptExecutionStarted(): void; 13 | announceTestScriptExecutionError( error: Error ): void; 14 | announceTestScriptExecutionFinished(): void; 15 | 16 | showTestScriptAnalysis( r: TestScriptExecutionResult ): void; 17 | } -------------------------------------------------------------------------------- /modules/testdata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataGenerator'; 2 | export * from './DataGeneratorBuilder'; 3 | export * from './DataTestCase'; 4 | export * from './DataTestCaseAnalyzer'; 5 | export * from './DataTestCaseNames'; 6 | export * from './DataTestCaseVsValueType'; 7 | export * from './InvertedLogicListBasedDataGenerator'; 8 | export * from './InvertedLogicQueryBasedDataGenerator'; 9 | export * from './ListBasedDataGenerator'; 10 | export * from './QueryBasedDataGenerator'; 11 | export * from './RegexBasedDataGenerator'; 12 | export * from './UIElementValueGenerator'; 13 | -------------------------------------------------------------------------------- /modules/util/fs/ext-changer.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | import { parse, join } from 'path'; 3 | 4 | export function changeFileExtension( 5 | file: string, 6 | extension: string 7 | ) { 8 | const { dir, name } = parse( file ); 9 | return join( dir, name + extension ); 10 | } 11 | 12 | 13 | export function addTimeStampToFilename( 14 | file: string, 15 | dateTime: Date 16 | ): string { 17 | const { dir, name, ext } = parse( file ); 18 | return join( dir, name + '-' + format( dateTime, 'yyyy-MM-dd_HH-mm-ss' ) + ext ); 19 | } 20 | -------------------------------------------------------------------------------- /modules/testdata/raw/RawDataGenerator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Raw data generator 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export interface RawDataGenerator< T > { 7 | 8 | lowest(): T; 9 | 10 | randomBelowMin(): T; 11 | 12 | justBelowMin(): T; 13 | 14 | min(): T; 15 | 16 | justAboveMin(): T; 17 | 18 | zero(): T; 19 | 20 | median(): T; 21 | 22 | randomBetweenMinAndMax(): T; 23 | 24 | justBelowMax(): T; 25 | 26 | max(): T; 27 | 28 | justAboveMax(): T; 29 | 30 | randomAboveMax(): T; 31 | 32 | greatest(): T; 33 | 34 | } -------------------------------------------------------------------------------- /modules/lexer/FeatureLexer.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from "../ast/Feature"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a Feature. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class FeatureLexer extends NamedNodeLexer< Feature > { 11 | 12 | constructor( words: Array< string > ) { 13 | super( words, NodeTypes.FEATURE ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.SCENARIO ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/UIPropertyLexer.ts: -------------------------------------------------------------------------------- 1 | import { UIProperty } from "../ast/UIProperty"; 2 | import { NodeTypes } from '../req/NodeTypes'; 3 | import { ListItemLexer } from './ListItemLexer'; 4 | 5 | /** 6 | * Detects a UIProperty node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class UIPropertyLexer extends ListItemLexer< UIProperty > { 11 | 12 | constructor() { 13 | super( NodeTypes.UI_PROPERTY ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.UI_PROPERTY ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/DatabaseLexer.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '../ast/Database'; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a Database node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class DatabaseLexer extends NamedNodeLexer< Database > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.DATABASE ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.DATABASE ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/RegexBlockLexer.ts: -------------------------------------------------------------------------------- 1 | import { RegexBlock } from "../ast/RegexBlock"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { BlockLexer } from "./BlockLexer"; 4 | 5 | /** 6 | * Detects a Regex Block. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class RegexBlockLexer extends BlockLexer< RegexBlock > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.REGEX_BLOCK ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.REGEX ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/TestCaseLexer.ts: -------------------------------------------------------------------------------- 1 | import { TestCase } from "../ast/TestCase"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a TestCase. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class TestCaseLexer extends NamedNodeLexer< TestCase > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.TEST_CASE ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_GIVEN ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/StepThenLexer.ts: -------------------------------------------------------------------------------- 1 | import { StepThen } from "../ast/Step"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { StartingKeywordLexer } from './StartingKeywordLexer'; 4 | 5 | /** 6 | * Detects a Then node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class StepThenLexer extends StartingKeywordLexer< StepThen > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.STEP_THEN ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_AND ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/UIElementLexer.ts: -------------------------------------------------------------------------------- 1 | import { UIElement } from "../ast/UIElement"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a UI Element. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class UIElementLexer extends NamedNodeLexer< UIElement > { 11 | 12 | constructor( words: Array< string > ) { 13 | super( words, NodeTypes.UI_ELEMENT ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.UI_PROPERTY ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/VariantLexer.ts: -------------------------------------------------------------------------------- 1 | import { Variant } from '../ast/Variant'; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamePlusNumberNodeLexer } from "./NamePlusNumberNodeLexer"; 4 | 5 | /** 6 | * Detects a Variant. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class VariantLexer extends NamePlusNumberNodeLexer< Variant > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.VARIANT ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_GIVEN ]; 19 | } 20 | } -------------------------------------------------------------------------------- /modules/req/DocumentProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Document processor 3 | * 4 | * onStart() is always executed. 5 | * onError() is only executed in case of error (e.g. permission error, read error) 6 | * onLineRead() is only executed when a line is read. 7 | * onFinish() is only executed when there are no errors. 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export interface DocumentProcessor { 12 | 13 | onStart( name?: string ): void; 14 | 15 | onError( message: string ): void; 16 | 17 | onLineRead( line: string, lineNumber: number ): void; 18 | 19 | onFinish(): void; 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/DatabasePropertyLexer.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseProperty } from '../ast/Database'; 2 | import { NodeTypes } from '../req/NodeTypes'; 3 | import { ListItemLexer } from './ListItemLexer'; 4 | 5 | /** 6 | * DatabaseProperty lexer. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class DatabasePropertyLexer extends ListItemLexer< DatabaseProperty > { 11 | 12 | constructor() { 13 | super( NodeTypes.DATABASE_PROPERTY ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.DATABASE_PROPERTY ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/util/type-checking.ts: -------------------------------------------------------------------------------- 1 | 2 | export function isString( val: any ): boolean { 3 | return typeof val === 'string' 4 | || ( ( isDefined( val ) && 'object' === typeof val ) && '[object String]' === Object.prototype.toString.call( val ) ); 5 | } 6 | 7 | export function isNumber( val: any ): boolean { 8 | return isDefined( val ) && ! isNaN( val ); 9 | } 10 | 11 | export function isDefined( val: any ): boolean { 12 | return typeof val != 'undefined' && val !== null; 13 | } 14 | 15 | export function valueOrNull< T >( val: T ): T | null { 16 | return isDefined( val ) ? val : null; 17 | } 18 | -------------------------------------------------------------------------------- /modules/lexer/StepWhenLexer.ts: -------------------------------------------------------------------------------- 1 | import { StepWhen } from "../ast/Step"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { StartingKeywordLexer } from './StartingKeywordLexer'; 4 | 5 | /** 6 | * Detects a When node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class StepWhenLexer extends StartingKeywordLexer< StepWhen > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.STEP_WHEN ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_AND, NodeTypes.STEP_THEN ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/testcase/TestCaseGeneratorListener.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from "../error/LocatedException"; 2 | import { Warning } from "../error/Warning"; 3 | 4 | export interface TestCaseGeneratorListener { 5 | 6 | testCaseGenerationStarted( strategyWarnings: Warning[] ): void; 7 | 8 | testCaseProduced( 9 | dirTestCases: string, 10 | filePath: string, 11 | testCasesCount: number, 12 | errors: LocatedException[], 13 | warnings: Warning[] 14 | ): void; 15 | 16 | testCaseGenerationFinished( filesCount: number, testCasesCount: number, durationMs: number ): void; 17 | } -------------------------------------------------------------------------------- /modules/lexer/StepOtherwiseLexer.ts: -------------------------------------------------------------------------------- 1 | import { StepOtherwise } from '../ast/Step'; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { StartingKeywordLexer } from './StartingKeywordLexer'; 4 | 5 | /** 6 | * Detects an Otherwise node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class StepOtherwiseLexer extends StartingKeywordLexer< StepOtherwise > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.STEP_OTHERWISE ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_AND ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/ConstantBlockLexer.ts: -------------------------------------------------------------------------------- 1 | import { ConstantBlock } from "../ast/ConstantBlock"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { BlockLexer } from "./BlockLexer"; 4 | 5 | /** 6 | * Detects a Contant Block. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class ConstantBlockLexer extends BlockLexer< ConstantBlock > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.CONSTANT_BLOCK ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ 19 | NodeTypes.CONSTANT 20 | ]; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /modules/lexer/StepAndLexer.ts: -------------------------------------------------------------------------------- 1 | import { StepAnd } from "../ast/Step"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { StartingKeywordLexer } from './StartingKeywordLexer'; 4 | 5 | /** 6 | * Detects an And node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class StepAndLexer extends StartingKeywordLexer< StepAnd > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.STEP_AND ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_AND, NodeTypes.STEP_WHEN, NodeTypes.STEP_THEN ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/lexer/BackgroundLexer.ts: -------------------------------------------------------------------------------- 1 | import { Background } from "../ast/Background"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { BlockLexer } from "./BlockLexer"; 4 | 5 | /** 6 | * Detects a Background block. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class BackgroundLexer extends BlockLexer< Background > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.BACKGROUND ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_GIVEN, NodeTypes.VARIANT_BACKGROUND, NodeTypes.SCENARIO ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/dbi/InMemoryTableInterface.ts: -------------------------------------------------------------------------------- 1 | import { Table } from "../ast/Table"; 2 | import { Queryable } from "./Queryable"; 3 | 4 | /** 5 | * In-memory table interface 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export interface InMemoryTableInterface extends Queryable { 10 | 11 | /** 12 | * Checks if it is connected to an in-memory table. 13 | */ 14 | isConnected(): Promise< boolean >; 15 | 16 | /** 17 | * Creates a in-memory table. 18 | */ 19 | connect( table: Table ): Promise< void >; 20 | 21 | /** 22 | * Clears the in-memory table. 23 | */ 24 | disconnect(): Promise< void >; 25 | } -------------------------------------------------------------------------------- /modules/lexer/StepGivenLexer.ts: -------------------------------------------------------------------------------- 1 | import { StepGiven } from "../ast/Step"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { StartingKeywordLexer } from './StartingKeywordLexer'; 4 | 5 | /** 6 | * Detects a Given node. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class StepGivenLexer extends StartingKeywordLexer< StepGiven > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.STEP_GIVEN ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_AND, NodeTypes.STEP_WHEN, NodeTypes.STEP_THEN ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/parser/TagCollector.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "../ast/Tag"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NodeIterator } from './NodeIterator'; 4 | 5 | /** 6 | * Tag collector 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class TagCollector { 11 | 12 | public addBackwardTags( it: NodeIterator, targetTags: Tag[] ) { 13 | let itClone: NodeIterator = it.clone(); 14 | while ( itClone.hasPrior() && itClone.spyPrior().nodeType === NodeTypes.TAG ) { 15 | let tag = itClone.prior() as Tag; 16 | targetTags.unshift( tag ); // Inserts in the beginning 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /modules/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionMap'; 2 | export * from './Actions'; 3 | export * from './ActionTargets'; 4 | export * from './best-match'; 5 | export * from './case-conversor'; 6 | export * from './CaseType'; 7 | export * from './date-time-validation'; 8 | export * from './matches'; 9 | export * from './p-all'; 10 | export * from './package-installation'; 11 | export * from './remove-duplicated'; 12 | export * from './run-command'; 13 | export * from './time-format'; 14 | export * from './type-checking'; 15 | export * from './UIElementNameHandler'; 16 | export * from './UIElementPropertyExtractor'; 17 | export * from './ValueTypeDetector'; 18 | -------------------------------------------------------------------------------- /modules/lexer/ScenarioLexer.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from "../ast/Scenario"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { NamedNodeLexer } from "./NamedNodeLexer"; 4 | 5 | /** 6 | * Detects a Scenario. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class ScenarioLexer extends NamedNodeLexer< Scenario > { 11 | 12 | constructor( words: Array< string > ) { 13 | super( words, NodeTypes.SCENARIO ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_GIVEN, NodeTypes.SCENARIO, NodeTypes.VARIANT_BACKGROUND, NodeTypes.VARIANT ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/parser/NodeParser.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../ast/Node'; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { ParsingContext } from './ParsingContext'; 4 | 5 | /** 6 | * Node parser 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface NodeParser< T extends Node > { 11 | 12 | /** 13 | * Perform a syntactic analysis of the given node. 14 | * 15 | * @param node Node to be analyzed. 16 | * @param context Parsing context. 17 | * @param it Node iterator. 18 | * @param errors Detected errors. 19 | */ 20 | analyze( node: T, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean; 21 | 22 | } -------------------------------------------------------------------------------- /modules/lexer/VariantBackgroundLexer.ts: -------------------------------------------------------------------------------- 1 | import { VariantBackground } from "../ast/VariantBackground"; 2 | import { NodeTypes } from "../req/NodeTypes"; 3 | import { BlockLexer } from "./BlockLexer"; 4 | 5 | /** 6 | * Detects a Variant Background block. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class VariantBackgroundLexer extends BlockLexer< VariantBackground > { 11 | 12 | constructor( words: string[] ) { 13 | super( words, NodeTypes.VARIANT_BACKGROUND ); 14 | } 15 | 16 | /** @inheritDoc */ 17 | suggestedNextNodeTypes(): string[] { 18 | return [ NodeTypes.STEP_GIVEN, NodeTypes.SCENARIO, NodeTypes.VARIANT ]; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /modules/util/p-all.ts: -------------------------------------------------------------------------------- 1 | import pMap from 'p-map'; 2 | 3 | export const pAll = ( iterable, options ) => pMap( iterable, ( element: any ) => element(), options ); 4 | 5 | export const runAllWithoutThrow = async ( iterable, options, errors: Error[] = [] ) => { 6 | try { 7 | await pAll( iterable, options ); 8 | } catch ( err ) { 9 | if ( err[ '_errors' ] ) { // AggregateError - see https://github.com/sindresorhus/aggregate-error 10 | for ( const individualError of err[ '_errors' ] ) { 11 | errors.push( individualError.message ); 12 | } 13 | } else { 14 | errors.push( err ); 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /modules/ast/Feature.ts: -------------------------------------------------------------------------------- 1 | import { Background } from './Background'; 2 | import { NamedNode } from './Node'; 3 | import { Scenario } from './Scenario'; 4 | import { MayHaveTags } from './Tag'; 5 | import { Text } from './Text'; 6 | import { UIElement } from './UIElement'; 7 | import { VariantBackground } from './VariantBackground'; 8 | 9 | /** 10 | * Feature node. 11 | * 12 | * @author Thiago Delgado Pinto 13 | */ 14 | export interface Feature extends NamedNode, MayHaveTags { 15 | 16 | description?: string; 17 | sentences?: Text[]; 18 | background?: Background; 19 | variantBackground?: VariantBackground; 20 | scenarios?: Scenario[]; 21 | uiElements?: UIElement[]; 22 | } -------------------------------------------------------------------------------- /modules/util/file/DirSearcher.ts: -------------------------------------------------------------------------------- 1 | 2 | export type DirSearchOptions = { 3 | /** Base directory to search */ 4 | directory: string, 5 | /** Recursive search */ 6 | recursive: boolean, 7 | /** 8 | * Regex to compare. If it evaluates to `true` the directory is included 9 | * in the results. 10 | */ 11 | regexp: RegExp, 12 | }; 13 | 14 | export interface DirSearcher { 15 | 16 | /** 17 | * Return a list of directories, according to the given options. 18 | * Returned list contains absolute paths. 19 | * 20 | * @param options Options 21 | * @return List of directories 22 | */ 23 | search( options: DirSearchOptions ): Promise< string[] >; 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - "*" 6 | push: 7 | branches: 8 | - master 9 | - v1 10 | - v2 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | node-version: [10, 12, 14] 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /modules/report/TestReporter.ts: -------------------------------------------------------------------------------- 1 | import { TestScriptExecutionResult } from "concordialang-types"; 2 | 3 | /** 4 | * Test reporter options 5 | */ 6 | export interface TestReporterOptions {} 7 | 8 | /** 9 | * Test script execution reporter 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export interface TestReporter< Opt extends TestReporterOptions > { 14 | 15 | /** 16 | * Reports a single test script execution result. 17 | * 18 | * @param result Result to report. 19 | * @param options Reporting options. 20 | * @returns Promise< void > 21 | */ 22 | report( 23 | result: TestScriptExecutionResult, 24 | options?: Opt 25 | ): Promise< void >; 26 | 27 | } -------------------------------------------------------------------------------- /__tests__/util/escape.spec.ts: -------------------------------------------------------------------------------- 1 | import { escapeChar, escapeString } from '../../modules/testdata/util/escape'; 2 | 3 | describe( 'escape', () => { 4 | 5 | describe( '#escapeChar', () => { 6 | 7 | it( 'escapes a character', () => { 8 | expect( escapeChar( '>' ) ).toEqual( '\\>' ); 9 | } ); 10 | 11 | } ); 12 | 13 | 14 | describe( '#escapeString', () => { 15 | 16 | it( 'check unbalanced backslash', () => { 17 | expect( escapeString( `\\foo` ) ).toEqual( `\\\\foo` ); 18 | } ); 19 | 20 | it( 'check unbalanced single quotes', () => { 21 | expect( escapeString( `'foo` ) ).toEqual( `foo` ); 22 | expect( escapeString( `'foo'` ) ).toEqual( `\\'foo\\'` ); 23 | } ); 24 | 25 | } ); 26 | 27 | } ); 28 | -------------------------------------------------------------------------------- /modules/req/NodeTypes.ts: -------------------------------------------------------------------------------- 1 | import { Keywords } from "./Keywords"; 2 | 3 | /** 4 | * Node types 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export abstract class NodeTypes extends Keywords { 9 | 10 | // Not available in Gherkin 11 | 12 | static REGEX: string = 'regex'; 13 | static CONSTANT: string = 'constant'; 14 | static UI_PROPERTY: string = 'uiProperty'; 15 | static DATABASE_PROPERTY: string = 'databaseProperty'; 16 | 17 | // Also available in Gherkin 18 | 19 | static TAG: string = 'tag'; 20 | static TABLE_ROW: string = 'tableRow'; 21 | static LONG_STRING: string = 'longString'; // a.k.a. py string 22 | static TEXT: string = 'text'; // not empty content 23 | 24 | } -------------------------------------------------------------------------------- /modules/testdata/limits/TimeLimits.ts: -------------------------------------------------------------------------------- 1 | import { LocalTime } from "@js-joda/core"; 2 | 3 | /** 4 | * Limits for time values. Milliseconds are ignored. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export abstract class TimeLimits { 9 | static MIN: LocalTime = LocalTime.of( 0, 0, 0 ); // 00:00:00.000 10 | static MAX: LocalTime = LocalTime.of( 23, 59, 59 ); // 23:59:59.000 11 | } 12 | 13 | /** 14 | * Limits for short time values. Seconds are ignored. 15 | * 16 | * @author Thiago Delgado Pinto 17 | */ 18 | export abstract class ShortTimeLimits { 19 | static MIN: LocalTime = LocalTime.of( 0, 0, 0, 0 ); // 00:00:00.000 20 | static MAX: LocalTime = LocalTime.of( 23, 59, 0, 0 ); // 23:59:00.0 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Concordia 2 | 3 | Copyright (c) Thiago Delgado Pinto 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as 7 | published by the Free Software Foundation, either version 3 of the 8 | License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . -------------------------------------------------------------------------------- /__tests__/db/ConnectionResult.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionCheckResult, ConnectionResult } from '../../modules/dbi/ConnectionResult'; 2 | 3 | describe( 'ConnectionResult', () => { 4 | 5 | describe( 'ConnectionCheckResult', () => { 6 | 7 | it( 'can filter succeeded results', () => { 8 | 9 | const [ foo, bar, zoo ] = [ 10 | { success: true } as ConnectionResult, 11 | { success: false } as ConnectionResult, 12 | { success: true } as ConnectionResult, 13 | ]; 14 | 15 | const map = { 16 | 'foo': foo, 17 | 'bar': bar, 18 | 'zoo': zoo, 19 | }; 20 | 21 | const c = new ConnectionCheckResult( false, map ); 22 | const r = c.succeededResults(); 23 | expect( r ).toEqual( [ foo, zoo ] ); 24 | } ); 25 | 26 | } ); 27 | 28 | } ); 29 | -------------------------------------------------------------------------------- /modules/ast/Task.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | 3 | // 4 | // Task example 1: 5 | // ``` 6 | // Before all the tests: 7 | // - Run script "script-name" 8 | // - Run command "command-name" 9 | // - Run command `cmd /k dir` 10 | // ``` 11 | 12 | export interface TaskContent extends Node { 13 | 14 | action: 'script' | 'command'; 15 | // name or content is used, bot not both 16 | name?: string; 17 | content?: string; 18 | } 19 | 20 | export interface Task extends Node { 21 | 22 | // When: 23 | // 'BAT' = before all the tests, 24 | // 'BET' = before each test, 25 | // 'AET' = after each test, 26 | // 'AAT' = after all the tests 27 | when: 'BAT' | 'BET' | 'AAT' | 'AET'; 28 | 29 | content: Array< TaskContent >; 30 | } -------------------------------------------------------------------------------- /modules/nlp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseTrainingExamples'; 2 | export * from './DatabasePropertyRecognizer'; 3 | export * from './DateTimeExpressions'; 4 | export * from './Entities'; 5 | export * from './EntityHandler'; 6 | export * from './EntityRecognizerMaker'; 7 | export * from './GivenWhenThenSentenceRecognizer'; 8 | export * from './Intents'; 9 | export * from './NLP'; 10 | export * from './NLPBasedSentenceRecognizer'; 11 | export * from './NLPEntity'; 12 | export * from './NLPException'; 13 | export * from './NLPResult'; 14 | export * from './NLPTrainer'; 15 | export * from './NLPTrainingData'; 16 | export * from './NLPTrainingDataConversor'; 17 | export * from './NLPUtil'; 18 | export * from './NodeSentenceRecognizer'; 19 | export * from './UIPropertyRecognizer'; 20 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar-project.properties 2 | # must be unique in a given SonarQube instance 3 | sonar.projectKey=concordialang 4 | 5 | # --- optional properties --- 6 | 7 | # defaults to project key 8 | #sonar.projectName=My project 9 | # defaults to 'not provided' 10 | #sonar.projectVersion=1.0 11 | 12 | # Path is relative to the sonar-project.properties file. Defaults to . 13 | 14 | sonar.sourceEncoding=UTF-8 15 | sonar.language=ts 16 | sonar.sources=./modules 17 | sonar.tests=./__tests__ 18 | sonar.exclusions=bin, coverage, data, dist, docs, lib, media, node_modules 19 | 20 | sonar.testExecutionReportPaths=coverage/sonar-report.xml 21 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 22 | 23 | # Encoding of the source code. Default is default system encoding 24 | -------------------------------------------------------------------------------- /__tests__/util/package-installation.spec.ts: -------------------------------------------------------------------------------- 1 | import { joinDatabasePackageNames, makeDatabasePackageNameFor } from '../../modules/util/package-installation'; 2 | 3 | describe( 'package-installation', () => { 4 | 5 | it( 'completes a database name with the package name', () => { 6 | expect( makeDatabasePackageNameFor( 'mysql' ) ).toEqual( 'database-js-mysql' ); 7 | } ); 8 | 9 | it( 'keeps a correct package name', () => { 10 | expect( makeDatabasePackageNameFor( 'database-js-mysql' ) ).toEqual( 'database-js-mysql' ); 11 | } ); 12 | 13 | it( 'joins multiple package names', () => { 14 | const names = [ 'mysql', 'database-js-json', 'ini' ]; 15 | expect( joinDatabasePackageNames( names ) ) 16 | .toEqual( 'database-js-mysql database-js-json database-js-ini' ); 17 | } ); 18 | 19 | } ); -------------------------------------------------------------------------------- /modules/lexer/ImportLexer.ts: -------------------------------------------------------------------------------- 1 | import isValidPath from 'is-valid-path'; 2 | 3 | import { Import } from '../ast/Import'; 4 | import { NodeTypes } from '../req/NodeTypes'; 5 | import { QuotedNodeLexer } from './QuotedNodeLexer'; 6 | 7 | 8 | /** 9 | * Detects an Import. 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class ImportLexer extends QuotedNodeLexer< Import > { 14 | 15 | constructor( words: Array< string > ) { 16 | super( words, NodeTypes.IMPORT ); 17 | } 18 | 19 | /** @inheritDoc */ 20 | suggestedNextNodeTypes(): string[] { 21 | return [ NodeTypes.FEATURE, NodeTypes.VARIANT ]; 22 | } 23 | 24 | /** @inheritdoc */ 25 | public isValidName( name: string ): boolean { 26 | return isValidPath( name ); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /modules/util/file/FileSearcher.ts: -------------------------------------------------------------------------------- 1 | 2 | /** @see Options */ 3 | export type FileSearchOptions = { 4 | directory: string, 5 | recursive: boolean, 6 | extensions: string[], 7 | file: string[], 8 | ignore: string[], 9 | }; 10 | 11 | export type FileSearchResults = { 12 | files: string[], 13 | warnings: string[] 14 | }; 15 | 16 | export interface FileSearcher { 17 | 18 | /** 19 | * Returns a search result with a list of files and warnings, according to 20 | * the given options. Returned files have absolute paths. 21 | * 22 | * @param options Options 23 | * @return Search results 24 | * 25 | * @throws Error When a given directory does not exist. 26 | */ 27 | searchFrom( options: FileSearchOptions ): Promise< FileSearchResults >; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/req/Expressions.spec.ts: -------------------------------------------------------------------------------- 1 | import { Expressions } from "../../modules/req/Expressions"; 2 | 3 | describe( 'Expressions', () => { 4 | 5 | it( 'espaces a char for a regex correctly', () => { 6 | expect( Expressions.escape( '.' ) ).toBe( "\\." ); 7 | } ); 8 | 9 | it( 'espaces all chars for a regex correctly', () => { 10 | expect( Expressions.escapeAll( [ '.', '[' ] ) ) 11 | .toEqual( [ "\\.", "\\[" ] ); 12 | } ); 13 | 14 | it( 'creates a regex to ignore the given characters', () => { 15 | let r = Expressions.anythingBut( [ '"' ] ).test( 'hello world' ); 16 | expect( r ).toBeTruthy(); 17 | 18 | r = Expressions.anythingBut( [ '"' ] ).test( 'hello "world' ); 19 | expect( r ).toBeFalsy(); 20 | } ); 21 | 22 | 23 | } ); -------------------------------------------------------------------------------- /modules/testdata/limits/DateTimeLimits.ts: -------------------------------------------------------------------------------- 1 | import { LocalDateTime } from "@js-joda/core"; 2 | 3 | /** 4 | * Limits for datetime values. Ignores milliseconds. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export abstract class DateTimeLimits { 9 | static MIN: LocalDateTime = LocalDateTime.of( 0, 1, 1, 0, 0, 0 ); // 0000-01-01 00:00:00 10 | static MAX: LocalDateTime = LocalDateTime.of( 9999, 12, 31, 23, 59, 59 ); // 9999-12-31 23:59:59.0 11 | } 12 | 13 | /** 14 | * Limits for short datetime values. Ignores seconds. 15 | * 16 | * @author Thiago Delgado Pinto 17 | */ 18 | export abstract class ShortDateTimeLimits { 19 | static MIN: LocalDateTime = LocalDateTime.of( 0, 1, 1, 0, 0 ); // 0000-01-01 00:00 20 | static MAX: LocalDateTime = LocalDateTime.of( 9999, 12, 31, 23, 59 ); // 9999-12-31 23:59 21 | } 22 | -------------------------------------------------------------------------------- /docs/en/dev/states.md: -------------------------------------------------------------------------------- 1 | # States 2 | 3 | **Preconditions** are always denoted by a `Given` step. **State Calls** are always denoted by a `When` step. **Postconditions** are always denoted by a `Then` step. These steps are replaced as follows: 4 | 5 | - A step with a **Precondition** is replaced by the steps from the Variant that produces the referred state. Whether that Variant also have Preconditions, they are replaced likewise. 6 | 7 | - A step with a **State Call** is replaced by the steps from the Variant that produces the referred state, but the precondition steps of that Variant are *not* included. 8 | 9 | - A step with a **Postcondition** is removed. 10 | 11 | ## State Checking 12 | 13 | Preconditions and State Calls are checked against Postconditions produced by Variants of different Features. Just the imported Features are analyzed. -------------------------------------------------------------------------------- /modules/report/JSONTestReporter.ts: -------------------------------------------------------------------------------- 1 | import { TestScriptExecutionResult } from 'concordialang-types'; 2 | 3 | import { FileBasedTestReporter, FileBasedTestReporterOptions } from './FileBasedTestReporter'; 4 | 5 | /** 6 | * JSON-based test script execution reporter. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class JSONTestReporter extends FileBasedTestReporter { 11 | 12 | /** @inheritdoc */ 13 | async report( 14 | result: TestScriptExecutionResult, 15 | options?: FileBasedTestReporterOptions 16 | ): Promise { 17 | const fileName = this.makeFilename( options ); 18 | await this._fileWriter.write( fileName, JSON.stringify( result, undefined, "\t" ) ); 19 | } 20 | 21 | /** @inheritdoc */ 22 | fileExtension(): string { 23 | return '.json'; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /modules/ast/Node.ts: -------------------------------------------------------------------------------- 1 | import { Location } from 'concordialang-types'; 2 | 3 | /** 4 | * @author Thiago Delgado Pinto 5 | */ 6 | 7 | export interface Node { 8 | nodeType: string; 9 | location: Location; 10 | } 11 | 12 | export interface HasContent { 13 | content: string; // Useful content, ignoring symbols and keywords 14 | } 15 | 16 | export interface HasName { 17 | name: string; 18 | } 19 | 20 | export interface HasValue { 21 | value: string; 22 | } 23 | 24 | export interface HasItems< T extends Node > { 25 | items: T[]; 26 | } 27 | 28 | export interface NamedNode extends Node, HasName { 29 | } 30 | 31 | export interface ValuedNode extends Node, HasValue { 32 | } 33 | 34 | export interface ContentNode extends Node, HasContent { 35 | } 36 | 37 | export interface NodeWithNameAndValue extends ContentNode, HasName, HasValue { 38 | } -------------------------------------------------------------------------------- /modules/util/remove-duplicated.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove duplicated items from contiguous arrays with same type. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export function removeDuplicated< T >( 7 | arr: Array< T >, 8 | areEqual: ( a: T, b: T ) => boolean = ( a: T, b: T ) => a === b 9 | ): number { 10 | let removeCount = 0; 11 | for ( let end = arr.length; end >= 0; --end ) { 12 | const down = arr[ end ]; 13 | if ( undefined === down ) { 14 | continue; 15 | } 16 | for ( let d = end - 1; d >= 0; --d ) { 17 | const prior = arr[ d ]; 18 | if ( prior !== undefined && areEqual( down, prior ) ) { 19 | arr.splice( end, 1 ); 20 | ++removeCount; 21 | break; 22 | } 23 | } 24 | } 25 | return removeCount; 26 | } -------------------------------------------------------------------------------- /modules/nlp/EntityHandler.ts: -------------------------------------------------------------------------------- 1 | import { Entities } from './Entities'; 2 | import { NLPEntity } from './NLPEntity'; 3 | import { NLPResult } from './NLPResult'; 4 | 5 | /** 6 | * Entity handler 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class EntityHandler { 11 | 12 | with( r: NLPResult, target: Entities ): NLPEntity[] { 13 | if ( ! r.entities ) { 14 | return []; 15 | } 16 | return r.entities.filter( e => e.entity === target ); 17 | } 18 | 19 | count( r: NLPResult, target: Entities ): number { 20 | return this.with( r, target ).length; 21 | } 22 | 23 | has( r: NLPResult, target: Entities ): boolean { 24 | return this.count( r, target ) > 0; 25 | } 26 | 27 | values( r: NLPResult, target: Entities ): any[] { 28 | return this.with( r, target ).map( e => e.value ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/main.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { main } from './cli/cli-main.js'; 4 | 5 | declare global { 6 | interface ImportMeta { 7 | url: string; 8 | } 9 | } 10 | 11 | // Supported in ES2020+ but it worked flawlessly in ES2015/ES2018 (Node 10) 12 | // @ts-ignore 13 | const __dirname = path.join(path.dirname(decodeURI(new URL(import.meta.url).pathname))).replace(/^\\([A-Z]:\\)/, "$1"); 14 | 15 | 16 | process.on( 'uncaughtException', console.error ); 17 | 18 | process.on( 'SIGINT', () => { // e.g., Terminate execution with Ctrl + C 19 | console.log( '\nAborted. Bye!' ); 20 | process.exit( 1 ); 21 | } ); 22 | 23 | main( __dirname, process.cwd() ) 24 | .then( ( success: boolean ) => { 25 | process.exit( success ? 0 : 1 ); 26 | } ) 27 | .catch( err => { 28 | console.error( err ); 29 | process.exit( 1 ); 30 | } ); 31 | -------------------------------------------------------------------------------- /modules/testcase/UIETestPlan.ts: -------------------------------------------------------------------------------- 1 | import { Step } from "../ast/Step"; 2 | import { DataTestCase } from "../testdata/DataTestCase"; 3 | import { DTCAnalysisResult } from "../testdata/DataTestCaseAnalyzer"; 4 | 5 | export class UIETestPlan { 6 | 7 | constructor( 8 | public readonly dtc: DataTestCase, 9 | public readonly result: DTCAnalysisResult, 10 | public readonly otherwiseSteps: Step[] 11 | ) { 12 | } 13 | 14 | hasOtherwiseSteps(): boolean { 15 | return ( this.otherwiseSteps || [] ).length > 0; 16 | } 17 | 18 | isResultInvalid(): boolean { 19 | return DTCAnalysisResult.INVALID === this.result; 20 | } 21 | 22 | /** Remember: still have to analyse whether the steps have Then without states */ 23 | shouldFail(): boolean { 24 | return this.isResultInvalid() && ! this.hasOtherwiseSteps(); 25 | } 26 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "modules/*": [ "modules/*" ], 6 | "@/*": [ "modules/*" ] 7 | }, 8 | "outDir": "dist", 9 | 10 | "module": "ES2015", 11 | "target": "ES2018", 12 | "moduleResolution": "node", 13 | 14 | "esModuleInterop": true, 15 | "removeComments": true, 16 | "declaration": false, 17 | "declarationMap": false, 18 | "sourceMap": true, 19 | 20 | "noImplicitAny": false, 21 | "allowJs": true, 22 | "keyofStringsOnly": true, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ], 26 | 27 | "lib": [ 28 | "ES2015", 29 | "DOM" 30 | ] 31 | }, 32 | "include": [ 33 | "lib", 34 | "modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /modules/plugin/PluginListener.ts: -------------------------------------------------------------------------------- 1 | import { PluginData } from "./PluginData"; 2 | 3 | export interface PluginListener { 4 | 5 | warn( message: string ): void; 6 | 7 | 8 | drawPluginList( plugins: PluginData[] ): void; 9 | 10 | drawSinglePlugin( p: PluginData ): void; 11 | 12 | showMessagePluginNotFound( name: string ): void; 13 | 14 | showMessagePluginAlreadyInstalled( name: string ): void; 15 | 16 | showMessageCouldNoFindInstalledPlugin( name: string ): void; 17 | 18 | showMessagePackageFileNotFound( file: string ): void; 19 | 20 | warnAboutOldPluginVersion(): void; 21 | 22 | showPluginServeUndefined( name: string ): void; 23 | 24 | showPluginServeStart( name: string ): void; 25 | 26 | showCommandStarted( command: string ): void; 27 | 28 | showCommandFinished( code: number ): void; 29 | 30 | showError( e: Error ): void; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /modules/parser/TextCollector.ts: -------------------------------------------------------------------------------- 1 | import { Text } from '../ast/Text'; 2 | import { NodeTypes } from '../req/NodeTypes'; 3 | import { NodeIterator } from './NodeIterator'; 4 | 5 | /** 6 | * Text collector 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class TextCollector { 11 | 12 | /** 13 | * Add forward text nodes. 14 | * 15 | * @param it Node iterator 16 | * @param target Where to put the nodes found. 17 | * @param changeIterator If the iterator can be changed. 18 | */ 19 | public addForwardTextNodes( it: NodeIterator, target: Text[], changeIterator: boolean = false ) { 20 | let nodeIt: NodeIterator = changeIterator ? it : it.clone(); 21 | while ( nodeIt.hasNext() && nodeIt.spyNext().nodeType === NodeTypes.TEXT ) { 22 | let text = nodeIt.next() as Text; 23 | target.push( text ); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /docs/en/dev/queries.md: -------------------------------------------------------------------------------- 1 | ### References inside queries 2 | - Constant: 3 | - `[something]` **can** be used whether the project adopts [AlaSQL](https://github.com/agershun/alasql), with the restriction that [it uses](https://github.com/agershun/alasql#read-and-write-excel-and-raw-data-files) only numbers inside brackets, to refer columns of CSV files. E.g., `"SELECT [3] as city, [4] as population from csv( 'path/to/file.csv')"`. So a [Constant](#constants) name could not be a number. 4 | - `[something]` **can** be used whether the project adopts [Database-JS](https://github.com/mlaanderson/database-js), with the restriction that it uses dollar signs (`$`) to reference rows and columns in Excel files. E.g., `"SELECT * FROM [Sheet1$A1:C52]"`. So [Constants](#constants) names could *not* have dollar signs. 5 | - References that do not match the format of a [Constant](#constants) name must be ignored, i.e., not replaced by a value. -------------------------------------------------------------------------------- /modules/testcase/TestPlan.ts: -------------------------------------------------------------------------------- 1 | import { UIETestPlan } from './UIETestPlan'; 2 | 3 | /** 4 | * A test plan can be applied to test scenarios to produce test cases. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class TestPlan { 9 | 10 | /** 11 | * DataTestCases to apply for each UI Element variable in a test scenario. 12 | */ 13 | dataTestCases: Map< string, UIETestPlan > = new Map< string, UIETestPlan >(); 14 | 15 | /** 16 | * Indicates whether at least one of the DataTestCases generates an invalid data. 17 | * This can determine if oracles should exist to replace Then steps. 18 | */ 19 | hasAnyInvalidResult(): boolean { 20 | for ( let [ /* uieVar */, uiePlan ] of this.dataTestCases ) { 21 | if ( uiePlan.isResultInvalid() ) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules/util/matches.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns matches, ignoring undefined values. It is capable of ignoring full matches. 3 | * 4 | * @param regex Regex 5 | * @param text Text 6 | * @param ignoresFullMatch Ignores the full match 7 | */ 8 | export function matches( regex: RegExp, text: string, ignoresFullMatch: boolean = false ): string[] { 9 | // Assures a global regex, to avoid infinite loop 10 | let rx: RegExp = ( regex.global ) ? regex : new RegExp( regex.source, 'g' ); 11 | let results: string[] = []; 12 | let match: RegExpExecArray = null; 13 | while ( ( match = rx.exec( text ) ) !== null ) { 14 | // Add all the groups, but the full match 15 | results.push.apply( results, ignoresFullMatch 16 | ? match.filter( ( val, idx ) => !! val && idx > 0 ) 17 | : match.filter( ( val ) => !! val ) 18 | ); 19 | // Avoid infinite loop 20 | rx.lastIndex = match.index + ( match[ 0 ].length || 1 ); 21 | } 22 | return results; 23 | } 24 | -------------------------------------------------------------------------------- /modules/parser/DatabaseParser.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '../ast/Database'; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from './NodeParser'; 4 | import { ParsingContext } from './ParsingContext'; 5 | 6 | /** 7 | * Database parser 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export class DatabaseParser implements NodeParser< Database > { 12 | 13 | analyze( 14 | node: Database, 15 | context: ParsingContext, 16 | it: NodeIterator, 17 | errors: Error[] 18 | ): boolean { 19 | 20 | // Adjusts the context 21 | context.resetInValues(); 22 | context.currentDatabase = node; 23 | 24 | // Checks the structure 25 | if ( ! context.doc.databases ) { 26 | context.doc.databases = []; 27 | } 28 | 29 | // Adds the node 30 | context.doc.databases.push( node ); 31 | 32 | return true; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /modules/testdata/random/Random.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom'; 2 | 3 | /** 4 | * Predictable random number generator. 5 | * 6 | * @author Thiago Delgado Pinto 7 | * @see https://github.com/davidbau/seedrandom 8 | * @see https://github.com/nquinlan/better-random-numbers-for-javascript-mirror 9 | */ 10 | export class Random { 11 | 12 | private _prng: any; 13 | 14 | /** 15 | * @param seed Seed (optional). Defaults to the current timestamp. 16 | */ 17 | constructor( seed?: string ) { 18 | // Uses Johannes Baagøe's extremely fast Alea PRNG 19 | this._prng = seedrandom.alea( seed || Date.now().toString() ); 20 | } 21 | 22 | /** 23 | * Generates a double >= 0 and < 1. 24 | */ 25 | generate(): number { 26 | return this._prng(); // 32 bits of randomness in a double 27 | //return this._prng().double(); // 56 bits of randomness in a double 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /modules/db/QueryCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Query cache. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export class QueryCache { 7 | 8 | // query => [ { field1 => value1, field2 => value2, ... }, { ... } ] 9 | // ex: 'SELECT bla' => [ { 'col1': 'valA', 'col2': 'valB' }, { 'col1': 'valC', 'col2': 'valD' } ] 10 | private _cache: Map< string, Map< string, any >[] > = new Map< string, Map< string, any >[] >(); 11 | 12 | has( query: string ): boolean { 13 | return this._cache.has( query ); 14 | } 15 | 16 | put( query: string, values: Map< string, any >[] ): void { 17 | this._cache.set( query, values ); 18 | } 19 | 20 | get( query: string ): Map< string, any >[] { 21 | return this._cache.get( query ); 22 | } 23 | 24 | remove( query: string ): boolean { 25 | return this._cache.delete( query ); 26 | } 27 | 28 | clear(): void { 29 | this._cache.clear(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /__tests__/util/file/ext-changer.spec.ts: -------------------------------------------------------------------------------- 1 | import { toUnixPath } from '../../../modules/util/file'; 2 | import { changeFileExtension } from '../../../modules/util/fs/ext-changer'; 3 | 4 | describe( 'ext-changer', () => { 5 | 6 | describe( '#changeFileExtension', () => { 7 | 8 | it( 'changes a file without directories', () => { 9 | const r = changeFileExtension( 'a.feature', '.testcase' ); 10 | expect( r ).toBe( 'a.testcase' ); 11 | } ); 12 | 13 | it( 'changes a file with directories', () => { 14 | const r = changeFileExtension( '/path/to/a.feature', '.testcase' ); 15 | expect( toUnixPath( r ) ).toBe( '/path/to/a.testcase' ); 16 | } ); 17 | 18 | it( 'requires node path library when not defined', () => { 19 | const r = changeFileExtension( 'a.feature', '.testcase' ); 20 | expect( r ).toBe( 'a.testcase' ); 21 | } ); 22 | 23 | } ); 24 | 25 | } ); -------------------------------------------------------------------------------- /modules/semantic/SpecificationAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import Graph from 'graph.js/dist/graph.full.js'; 2 | 3 | import { ProblemMapper } from '../error/ProblemMapper'; 4 | import { AugmentedSpec } from '../req/AugmentedSpec'; 5 | import { DuplicationChecker } from './DuplicationChecker'; 6 | 7 | /** 8 | * Specification semantic analyzer. 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export abstract class SpecificationAnalyzer { 13 | 14 | protected readonly _checker = new DuplicationChecker(); 15 | 16 | /** 17 | * Analyzes the given specification. 18 | * 19 | * @param problems Maps errors and warnings. 20 | * @param spec Specification to analyze. 21 | * @param graph Graph that maps specification's documents. 22 | * @returns `true` if successful. 23 | */ 24 | public abstract analyze( 25 | problems: ProblemMapper, 26 | spec: AugmentedSpec, 27 | graph: Graph, 28 | ): Promise< boolean >; 29 | 30 | } -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | The `lib` directory contains adapted libraries that were not installed using `npm`. 2 | 3 | ## Bravey 4 | 5 | **WARNING: Do not overwrite `bravey.js` with a newer version.** See the following notes. 6 | 7 | [bravey.js](https://github.com/BraveyJS/Bravey) was adapted in order to avoid transforming the sentences to lowercase. The following transformations were made: 8 | 9 | **a)** methods `getEntities()` had the first line commented, in more than one place, in order to avoid the text to be cleaned: 10 | ```javascript 11 | string = Bravey.Text.clean(string) 12 | ``` 13 | 14 | **b)** method `test()` had the first line commented, in order to avoid the text to be cleaned: 15 | ```javascript 16 | text = Bravey.Text.clean(text); 17 | ``` 18 | 19 | **c)** method `clean()` changed to not remove `[` and `]` 20 | 21 | **d)** debug mode 22 | 23 | **e)** support to Date values. 24 | 25 | **f)** support to Time values. 26 | 27 | **g)** support to DateTime values. 28 | -------------------------------------------------------------------------------- /modules/lexer/NamePlusNumberNodeLexer.ts: -------------------------------------------------------------------------------- 1 | import { NamedNode } from '../ast/Node'; 2 | import { Expressions } from '../req/Expressions'; 3 | import { NamedNodeLexer } from './NamedNodeLexer'; 4 | 5 | /** 6 | * Detects a node in the format "keyword number: name" (e.g. "variant 1: buy with credit card"). 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class NamePlusNumberNodeLexer< T extends NamedNode > extends NamedNodeLexer< T > { 11 | 12 | constructor( _words: Array< string >, _nodeType: string ) { 13 | super( _words, _nodeType ); 14 | } 15 | 16 | protected makeRegexForTheWords( words: string[] ): string { 17 | return '^' + Expressions.OPTIONAL_SPACES_OR_TABS 18 | + '(' + words.join( '|' ) + ')' 19 | + Expressions.OPTIONAL_SPACES_OR_TABS 20 | + Expressions.AN_INTEGER_NUMBER + '?' // optional 21 | + this.separator() 22 | + Expressions.ANYTHING; // the name 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /docs/en/versioning.md: -------------------------------------------------------------------------------- 1 | # Versioning 2 | 3 | Concordia's version numbering is based on [Semantic Versioning](https://semver.org). 4 | 5 | Although Semantic Versioning is conceived for [API](https://en.wikipedia.org/wiki/Application_programming_interface)s instead of for *Applications*, we adopt a very similar convention. Thus, changes become predictable and you can know, from the version numbers, when a version is no more compatible with a previous version. 6 | 7 | Given a version `MAJOR`.`MINOR`.`UPDATE`: 8 | - `MAIOR` is increased when the **Compiler** or the **Language** is no more compatible with the previous version. 9 | - `MINOR` is increased when adding functionality in a backwards-compatible manner. 10 | - `UPDATE` is in increased when there are fixes, little changes or little novelties, and all of them are backwards-compatible. 11 | 12 | Examples: 13 | - `0.2.0` is compatible with `0.1.0` 14 | - `0.1.1` is compatible with `0.1.0` 15 | - `1.0.0` is *not* compatible with `0.2.0` -------------------------------------------------------------------------------- /modules/selection/TagUtil.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "../ast/Tag"; 2 | 3 | /** 4 | * Tag utilities 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class TagUtil { 9 | 10 | isNameInKeywords( tag: Tag, keywords: string[] ): boolean { 11 | return keywords.indexOf( tag.name.toLowerCase() ) >= 0; 12 | } 13 | 14 | tagsWithNameInKeywords( tags: Tag[], keywords: string[] ): Tag[] { 15 | return tags.filter( ( t: Tag ) => this.isNameInKeywords( t, keywords ) ); 16 | } 17 | 18 | contentOfTheFirstTag( tags: Tag[] ): string | null { 19 | return ( tags.length > 0 ) ? tags[ 0 ].content : null; 20 | } 21 | 22 | numericContentOfTheFirstTag( tags: Tag[] ): number | null { 23 | const content = this.contentOfTheFirstTag( tags ); 24 | if ( content !== null ) { 25 | const num = parseInt( content ); 26 | return isNaN( num ) ? null : num; 27 | } 28 | return null; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules/compiler/CompilerListener.ts: -------------------------------------------------------------------------------- 1 | import { AppOptions } from '../app/options/app-options'; 2 | import { ProblemMapper } from '../error/ProblemMapper'; 3 | 4 | export interface CompilerListener { 5 | 6 | // Seed 7 | 8 | announceSeed( seed: string, generatedSeed: boolean ): void; 9 | announceRealSeed( realSeed: string ): void; 10 | 11 | // File searcher 12 | 13 | announceFileSearchStarted(): void; 14 | announceFileSearchWarnings( warnings: string[] ): void; 15 | announceFileSearchFinished( durationMS: number, filesFoundCount: number, filesIgnoredCount: number ): void; 16 | 17 | // Compiler 18 | 19 | announceCompilerStarted( options: AppOptions ): void; 20 | 21 | announceCompilerFinished( 22 | compiledFilesCount: number, 23 | featuresCount: number, 24 | testCasesCount: number, 25 | durationMS: number 26 | ): void; 27 | 28 | reportProblems( problems: ProblemMapper, basePath: string ): void; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /modules/util/case-conversor.ts: -------------------------------------------------------------------------------- 1 | //import { camel, kebab, pascal, snake } from 'case'; 2 | import _case from 'case'; 3 | import { CaseType } from './CaseType'; 4 | 5 | const { camel, kebab, pascal, snake } = _case; 6 | 7 | export function convertCase( text: string, type: CaseType | string ): string { 8 | switch ( type.toString().trim().toLowerCase() ) { 9 | case CaseType.CAMEL: return camel( text ); 10 | case CaseType.PASCAL: return pascal( text ); 11 | case CaseType.SNAKE: return snake( text ); 12 | case CaseType.KEBAB: return kebab( text ); 13 | default: return text; // do nothing 14 | } 15 | } 16 | 17 | export function upperFirst( text: string ): string { 18 | if ( !! text[ 0 ] ) { 19 | return text[ 0 ].toUpperCase() + text.substr( 1 ); 20 | } 21 | return text; 22 | } 23 | 24 | 25 | export function removeDiacritics( text: string ): string { 26 | return text.normalize( "NFD" ).replace( /[\u0300-\u036f]/g, "" ); 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/compiler/CompilerFacade.spec.ts: -------------------------------------------------------------------------------- 1 | import { filterFilesToCompile } from '../../modules/compiler/CompilerFacade'; 2 | 3 | describe( 'CompilerFacade', () => { 4 | 5 | describe( '#filterFilesToCompile', () => { 6 | 7 | it( 'does not include testcase files that have a corresponding feature file', () => { 8 | 9 | const r = filterFilesToCompile( [ 10 | '/path/to/foo.feature', 11 | '/path/to/foo.testcase', 12 | '/path/to/bar.feature', 13 | '/path/to/bar.testcase', 14 | '/path/to/zoo.testcase', 15 | ], '.feature', '.testcase' ); 16 | 17 | expect( r ).toHaveLength( 3 ); 18 | expect( r ).toEqual( 19 | [ 20 | '/path/to/foo.feature', 21 | '/path/to/bar.feature', 22 | '/path/to/zoo.testcase', 23 | ] 24 | ); 25 | 26 | } ); 27 | 28 | } ); 29 | 30 | } ); 31 | -------------------------------------------------------------------------------- /modules/nlp/NLPResult.ts: -------------------------------------------------------------------------------- 1 | import { NLPEntity } from "./NLPEntity"; 2 | 3 | /** 4 | * NLP Result. Currently it has the same structure of Bravey's NlpResult. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface NLPResult { 9 | 10 | // Number of found entities. 11 | found: number; 12 | // Ordered list of found entities. 13 | entities: NLPEntity[]; 14 | // A mapped version of the entities, in which the key is the entity id and value is a NLPEntity. 15 | entitiesIndex: Map< string, NLPEntity >; 16 | // Matched intent. 17 | intent: string; 18 | // Score of the matched sentence intent. E.g., 0.8999999999999999 19 | score: number; 20 | // Sentence with recognized entities, E.g., "Hello {name}". 21 | text: string; 22 | 23 | // IGNORED Bravey ATTRIBUTES: 24 | // 25 | // exceedEntities: boolean; 26 | // extraEntities: boolean; 27 | // missingEntities: boolean; 28 | // sentences: Array< { string: string } | NLPEntity > 29 | 30 | } 31 | -------------------------------------------------------------------------------- /docs/en/readme.md: -------------------------------------------------------------------------------- 1 | # 📖 Documentation 2 | 3 | ## Approach 4 | 5 | - [How it works](how-it-works.md) 6 | - [Recommended usage cycle](cycle.md) 7 | 8 | ## Language 9 | 10 | - [Overview of the Language](language.md) 11 | - [Actions](actions.md) 12 | - [Example](example.md) 13 | - [Generated Test Cases](test-cases.md) 14 | 15 | ## Tool 16 | 17 | - [Plug-ins](plugins.md) 18 | - [Configuration file](config.md) 19 | - [Tips and tricks](tips-and-tricks.md) 20 | - [Migration Guide](migration.md) 21 | - [Breaking Changes](breaking-changes.md) 22 | - [Version Numbering](versioning.md) 23 | 24 | ## Development 25 | 26 | - [Roadmap](../roadmap.md) 27 | - [Development guidelines](development.md) 28 | 29 | #### Technical notes 30 | 31 | - [User Interface Elements' Properties](dev/properties.md) 32 | - [Queries](dev/queries.md) 33 | - [States](dev/states.md) 34 | - [Test Cases](dev/test-cases.md) 35 | - [Test Scenarios](dev/test-scenarios.md) 36 | - [Data generation](dev/data-generation.md) 37 | 38 | ## Other 39 | 40 | - [FAQ](faq.md) -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomLong.spec.ts: -------------------------------------------------------------------------------- 1 | import { Random } from '../../../modules/testdata/random/Random'; 2 | import { RandomLong } from '../../../modules/testdata/random/RandomLong'; 3 | 4 | describe( 'RandomLong', () => { 5 | 6 | let random: RandomLong = new RandomLong( new Random() ); 7 | 8 | it( 'generates a random value between min and max, inclusive', () => { 9 | const min = -2, max = 2; 10 | let val: number = random.between( min, max ); 11 | expect( val ).toBeGreaterThanOrEqual( min ); 12 | expect( val ).toBeLessThanOrEqual( max ); 13 | } ); 14 | 15 | it( 'generates a value greater than a min value', () => { 16 | const min = -2; 17 | let val: number = random.after( min ); 18 | expect( val ).toBeGreaterThan( min ); 19 | } ); 20 | 21 | it( 'generates a value less than a max value', () => { 22 | const max = 2; 23 | let val: number = random.before( max ); 24 | expect( val ).toBeLessThan( max ); 25 | } ); 26 | 27 | } ); -------------------------------------------------------------------------------- /modules/nlp/syntax/SyntaxRuleBuilder.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxRule } from "./SyntaxRule"; 2 | 3 | /** 4 | * Rule Builder. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class SyntaxRuleBuilder { 9 | 10 | /** 11 | * Creates an array of rules applying the default rule to each object, 12 | * and then applying the partial rule. 13 | * 14 | * @param partialRules Partial rules. 15 | * @param defaultRule Default rule. 16 | * @return Array with rules. 17 | */ 18 | public build( 19 | partialRules: Array< SyntaxRule >, 20 | defaultRule: SyntaxRule 21 | ): Array< SyntaxRule > { 22 | let rules = []; 23 | for ( let rule of partialRules ) { 24 | // Starts with the default rules 25 | let newRule = Object.assign( {}, defaultRule ); 26 | // Then receives the new rules 27 | newRule = Object.assign( newRule, rule ); 28 | rules.push( newRule ); 29 | } 30 | return rules; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /modules/ast/VariantLike.ts: -------------------------------------------------------------------------------- 1 | import { Step } from './Step'; 2 | 3 | /** 4 | * VariantLike is **not** a node. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface VariantLike { 9 | 10 | sentences: Step[]; 11 | 12 | // Detected during test scenario generation: 13 | 14 | preconditions?: State[]; 15 | stateCalls?: State[]; 16 | postconditions?: State[]; 17 | } 18 | 19 | 20 | /** 21 | * State is **not** a node. 22 | * 23 | * @author Thiago Delgado Pinto 24 | */ 25 | export class State { 26 | 27 | constructor( 28 | public name: string, 29 | public stepIndex: number, 30 | public notFound?: boolean // Occurs when the State reference is not found 31 | ) { 32 | } 33 | 34 | toString(): string { 35 | return this.name; 36 | } 37 | 38 | equals( state: State ): boolean { 39 | return this.nameEquals( state.name ); 40 | } 41 | 42 | nameEquals( name: string ): boolean { 43 | return this.name.toLowerCase() === name.toLowerCase(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/lexer/NodeLexer.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../ast/Node'; 2 | import { LexicalException } from "./LexicalException"; 3 | 4 | 5 | export interface LexicalAnalysisResult< T extends Node > { 6 | nodes: Array< T >, 7 | errors: Array< LexicalException > 8 | warnings?: Array< LexicalException > 9 | } 10 | 11 | /** 12 | * Node lexer 13 | * 14 | * @author Thiago Delgado Pinto 15 | */ 16 | export interface NodeLexer< T extends Node > { 17 | 18 | /** 19 | * Returns the target node type. 20 | */ 21 | nodeType(): string; 22 | 23 | /** 24 | * Suggests nodes types as the next ones to verify. 25 | */ 26 | suggestedNextNodeTypes(): string[]; 27 | 28 | /** 29 | * Perform a lexical analysis of a line. Returns null if the line 30 | * does not contain the node, or a lexical analysis result otherwise. 31 | * 32 | * @param line Line. 33 | * @param lineNumber Line number. 34 | */ 35 | analyze( line: string, lineNumber?: number ): LexicalAnalysisResult< T > | null; 36 | 37 | } -------------------------------------------------------------------------------- /modules/ast/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Background'; 2 | export * from './Block'; 3 | export * from './Constant'; 4 | export * from './ConstantBlock'; 5 | export * from './Database'; 6 | export * from './Document'; 7 | export * from './Feature'; 8 | export * from './FileInfo'; 9 | export * from './Import'; 10 | export * from './Language'; 11 | export * from './ListItem'; 12 | export * from './LongString'; 13 | export * from './Node'; 14 | export * from './Regex'; 15 | export * from './RegexBlock'; 16 | export * from './Rule'; 17 | export * from './Scenario'; 18 | export * from './Spec'; 19 | export * from './Step'; 20 | export * from './Table'; 21 | export * from './Tag'; 22 | export * from './Task'; 23 | export * from './TestCase'; 24 | export * from './TestEvent'; 25 | export * from './Text'; 26 | export * from './UIElement'; 27 | export * from './UIProperty'; 28 | export * from './UIPropertyReference'; 29 | export * from './UIPropertyTypes'; 30 | export * from './Variant'; 31 | export * from './VariantBackground'; 32 | export * from './VariantLike'; 33 | -------------------------------------------------------------------------------- /docs/en/dev/data-generation.md: -------------------------------------------------------------------------------- 1 | # Notes on Data Generation 2 | 3 | ## Queries 4 | 5 | 1. Always return rows from the *first* column. 6 | 7 | - E.g., `"SELECT name, email FROM user"` will always return data from the `"name"` column. 8 | 9 | ## Regular Expressions 10 | 11 | 1. They will *not* check UI Elements' data type. 12 | 13 | - E.g., the following regular expression will generate data such as `US$ 159.03`, although the declared data type (`double`) would not accept `US$` as a valid value. 14 | ```concordia 15 | UI Element: salary 16 | - data type is double 17 | - min value is 1000.00 18 | - format is "US\$[0-9]{1,7}\.[0-9]{2}" 19 | ``` 20 | 21 | ## Computation 22 | 23 | (future) 24 | 25 | 1. Declaration specify programming language. Default is `javascript`. E.g.: 26 | ``` 27 | ```javascript 28 | ``` 29 | 30 | 2. May access specification elements through global variables: 31 | - `$uielement` 32 | - `$constant` 33 | - `$database` 34 | 35 | ```javascript 36 | $uielement[ 'username' ].value = $uielement[ 'name' ].value.split( ' ' )[ 0 ]; 37 | ``` -------------------------------------------------------------------------------- /modules/testdata/InvertedLogicListBasedDataGenerator.ts: -------------------------------------------------------------------------------- 1 | import { ListBasedDataGenerator } from "./ListBasedDataGenerator"; 2 | 3 | export class InvertedLogicListBasedDataGenerator< T > { 4 | 5 | /** 6 | * Constructor 7 | * 8 | * @param _gen List-based data generator 9 | */ 10 | constructor( 11 | private readonly _gen: ListBasedDataGenerator< T > 12 | ) { 13 | } 14 | 15 | // DATA GENERATION 16 | 17 | public firstElement(): T | null { 18 | return this._gen.notInSet(); 19 | } 20 | 21 | public secondElement(): T | null { 22 | return this._gen.notInSet(); 23 | } 24 | 25 | public randomElement(): T | null { 26 | return this._gen.notInSet(); 27 | } 28 | 29 | public penultimateElement(): T | null { 30 | return this._gen.notInSet(); 31 | } 32 | 33 | public lastElement(): T | null { 34 | return this._gen.notInSet(); 35 | } 36 | 37 | public notInSet(): T | null { 38 | return this._gen.randomElement(); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /__tests__/lexer/TextLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { TextLexer } from "../../modules/lexer/TextLexer"; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'TextLexer', () => { 5 | 6 | let lexer = new TextLexer(); 7 | 8 | it( 'does not recognize empty lines', () => { 9 | expect( lexer.analyze( '' ) ).toBeNull(); 10 | } ); 11 | 12 | it( 'does not recognize comment lines', () => { 13 | expect( lexer.analyze( '\t #comment' ) ).toBeNull(); 14 | } ); 15 | 16 | it( 'detects anything as text', () => { 17 | let line = " \t \t anything here \t"; 18 | let r = lexer.analyze( line, 1 ); 19 | expect( r ).toBeDefined(); 20 | let node = r.nodes[ 0 ]; 21 | // Location 22 | expect( node.location.line ).toBe( 1 ); 23 | expect( node.location.column ).toBe( 8 ); 24 | // Keyword 25 | expect( node.nodeType ).toBe( NodeTypes.TEXT ); 26 | // Content 27 | expect( node.content ).toBe( ' \t \t anything here \t' ); 28 | } ); 29 | 30 | } ); -------------------------------------------------------------------------------- /__tests__/lexer/StepWhenLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { StepWhenLexer } from "../../modules/lexer/StepWhenLexer"; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'StepWhenLexer', () => { 5 | 6 | // IMPORTANT: This lexer inherits from StartingKeywordLexer 7 | // and StartingKeywordLexerTest checks many important aspects 8 | // that does not need to be repeated here. 9 | 10 | let words = [ 'when' ]; 11 | let lexer = new StepWhenLexer( words ); 12 | 13 | it( 'detects correctly', () => { 14 | let line = " \t \t When \t the world and everybody on it \t"; 15 | let r = lexer.analyze( line, 1 ); 16 | expect( r ).toBeDefined(); 17 | let node = r.nodes[ 0 ]; 18 | // Location 19 | expect( node.location.line ).toBe( 1 ); 20 | expect( node.location.column ).toBe( 8 ); 21 | // Keyword 22 | expect( node.nodeType ).toBe( NodeTypes.STEP_WHEN ); 23 | // Content 24 | expect( node.content ).toBe( 'When \t the world and everybody on it' ); 25 | } ); 26 | 27 | } ); -------------------------------------------------------------------------------- /modules/semantic/TableSSA.ts: -------------------------------------------------------------------------------- 1 | import Graph from 'graph.js/dist/graph.full.js'; 2 | 3 | import { ProblemMapper } from '../error/ProblemMapper'; 4 | import { SemanticException } from '../error/SemanticException'; 5 | import { AugmentedSpec } from '../req/AugmentedSpec'; 6 | import { SpecificationAnalyzer } from './SpecificationAnalyzer'; 7 | 8 | /** 9 | * Analyzes Tables from a specification. 10 | * 11 | * It checks for: 12 | * - duplicated names 13 | * 14 | * @author Thiago Delgado Pinto 15 | */ 16 | export class TableSSA extends SpecificationAnalyzer { 17 | 18 | /** @inheritDoc */ 19 | public async analyze( 20 | problems: ProblemMapper, 21 | spec: AugmentedSpec, 22 | graph: Graph, 23 | ): Promise< boolean > { 24 | 25 | let errors: SemanticException[] = []; 26 | this._checker.checkDuplicatedNamedNodes( spec.tables(), errors, 'table' ); 27 | const ok1 = 0 === errors.length; 28 | if ( ! ok1 ) { 29 | problems.addGenericError( ...errors ); 30 | } 31 | return ok1; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /docs/en/migration.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | 4 | - [From `0.x` to `1.x`](#from-0x-to-1x) 5 | - [See also](#see-also) 6 | 7 | ## From `0.x` to `1.x` 8 | 9 | 1. **Update your configuration file, if needed** 10 | 11 | 1. Whether you project has a configuration file `.concordiarc`, open it with a text editor. 12 | 2. If the file has a property `"plugin"` with the value `"codeceptjs"`, you must change it to `"codeceptjs-webdriverio"`. 13 | 14 | 2. **Install the new plug-in** 15 | 16 | You can install any of the [available plug-ins](./plugins.md), currently `codeceptjs-webdriverio` or `codeceptjs-appium`. 17 | 18 | Example: 19 | ```bash 20 | concordia --plugin-install codeceptjs-webdriverio 21 | ``` 22 | 23 | 👉 On **Linux** and **MacOS**, it is needed to use `sudo` before the command. Example: 24 | ```bash 25 | sudo concordia --plugin-install codeceptjs-webdriverio 26 | ``` 27 | 28 | ## See also 29 | 30 | - [Information about the breaking changes](breaking-changes.md) 31 | - [Concordia's version numbering](versioning.md) -------------------------------------------------------------------------------- /modules/testdata/InvertedLogicQueryBasedDataGenerator.ts: -------------------------------------------------------------------------------- 1 | import { QueryBasedDataGenerator } from "./QueryBasedDataGenerator"; 2 | 3 | export class InvertedLogicQueryBasedDataGenerator< T > { 4 | 5 | constructor( 6 | private readonly _gen: QueryBasedDataGenerator< T > 7 | ) { 8 | } 9 | 10 | // DATA GENERATION 11 | 12 | public async firstElement(): Promise< T | null > { 13 | return await this._gen.notInSet(); 14 | } 15 | 16 | public async secondElement(): Promise< T | null > { 17 | return await this._gen.notInSet(); 18 | } 19 | 20 | public async randomElement(): Promise< T | null > { 21 | return await this._gen.notInSet(); 22 | } 23 | 24 | public async penultimateElement(): Promise< T | null > { 25 | return await this._gen.notInSet(); 26 | } 27 | 28 | public async lastElement(): Promise< T | null > { 29 | return await this._gen.notInSet(); 30 | } 31 | 32 | public async notInSet(): Promise< T | null > { 33 | return await this._gen.randomElement(); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /__tests__/lexer/StepThenLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { StepThenLexer } from "../../modules/lexer/StepThenLexer"; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'StepThenLexer', () => { 5 | 6 | // IMPORTANT: This lexer inherits from StartingKeywordLexer 7 | // and StartingKeywordLexerTest checks many important aspects 8 | // that does not need to be repeated here. 9 | 10 | let words = [ 'then' ]; 11 | let lexer = new StepThenLexer( words ); // under test 12 | 13 | it( 'detects correctly', () => { 14 | let line = " \t \t Then \t the world and everybody on it \t"; 15 | let r = lexer.analyze( line, 1 ); 16 | expect( r ).toBeDefined(); 17 | let node = r.nodes[ 0 ]; 18 | // Location 19 | expect( node.location.line ).toBe( 1 ); 20 | expect( node.location.column ).toBe( 8 ); 21 | // Keyword 22 | expect( node.nodeType ).toBe( NodeTypes.STEP_THEN ); 23 | // Content 24 | expect( node.content ).toBe( 'Then \t the world and everybody on it' ); 25 | } ); 26 | 27 | } ); -------------------------------------------------------------------------------- /modules/semantic/ConstantSSA.ts: -------------------------------------------------------------------------------- 1 | import Graph from 'graph.js/dist/graph.full.js'; 2 | 3 | import { ProblemMapper } from '../error/ProblemMapper'; 4 | import { SemanticException } from '../error/SemanticException'; 5 | import { AugmentedSpec } from '../req/AugmentedSpec'; 6 | import { SpecificationAnalyzer } from './SpecificationAnalyzer'; 7 | 8 | /** 9 | * Analyzes Constants from a specification. 10 | * 11 | * It checks for: 12 | * - duplicated names 13 | * 14 | * @author Thiago Delgado Pinto 15 | */ 16 | export class ConstantSSA extends SpecificationAnalyzer { 17 | 18 | /** @inheritDoc */ 19 | public async analyze( 20 | problems: ProblemMapper, 21 | spec: AugmentedSpec, 22 | graph: Graph, 23 | ): Promise< boolean > { 24 | 25 | let errors: SemanticException[] = []; 26 | this._checker.checkDuplicatedNamedNodes( spec.constants(), errors, 'constant' ); 27 | const ok1 = 0 === errors.length; 28 | if ( ! ok1 ) { 29 | problems.addGenericError( ...errors ); 30 | } 31 | return ok1; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /docs/pt/versioning.md: -------------------------------------------------------------------------------- 1 | # Versionamento 2 | 3 | A numeração de versões do projeto Concordia é baseada no [Versionamento Semântico](https://semver.org/lang/pt-BR/). 4 | 5 | Apesar de o Versionamento Semântico ser concebido para [API](https://pt.wikipedia.org/wiki/Interface_de_programa%C3%A7%C3%A3o_de_aplica%C3%A7%C3%B5es)s e não para *Aplicações*, adotamos uma convenção muito similar. Dessa forma, mudanças se tornam previsíveis e você consegue saber, pela numeração, quando uma versão não é mais compatível com a versão anterior. 6 | 7 | Dada uma versão `MAIOR`.`MENOR`.`ATUALIZAÇÃO`: 8 | - A numeração de `MAIOR` é incrementada quando o **Compilador** ou a **Linguagem** deixam de ser compatíveis com a versão anterior. 9 | - A numeração de `MENOR` é incrementada ao adicionar funcionalidade(s) mantendo a compatibilidade. 10 | - A numeração de `ATUALIZAÇÃO` é incrementada quando há correções, pequena alterações, ou pequenas novidades, todas compatíveis com a versão anterior. 11 | 12 | Exemplos: 13 | - `0.2.0` é compatível com `0.1.0` 14 | - `0.1.1` é compatível com `0.1.0` 15 | - `1.0.0` *não* é compatível com `0.2.0` -------------------------------------------------------------------------------- /docs/en/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | ### *Why Concordia?* 4 | 5 | Concordia [is a roman goddess](https://www.britannica.com/topic/Concordia-Roman-goddess) who was the personification of *"concord"* or *"agreement"*. The idea is that the language may help users, stakeholders, and the software team to discuss and to reach an agreement about the software requirements. This shared understanding is essencial to the software construction. 6 | 7 | ### *How to generate test cases or test scripts for a single file?* 8 | 9 | Just inform the parameter `--files` with the desired file. For example: 10 | ```console 11 | concordia --files="myfile.feature" ... 12 | ``` 13 | However, whether your file imports other files, you need to include them too: 14 | 15 | ```console 16 | concordia --files="myfile.feature,other.feature" ... 17 | ``` 18 | 19 | ### *How to execute a single test script?* 20 | 21 | This is yet not supported by Concordia. However, you can use your testing tool directly. For example, this will execute the test `myfile.js` with CodeceptJS: 22 | ```console 23 | codeceptjs run --steps myfile.js 24 | ``` 25 | -------------------------------------------------------------------------------- /modules/selection/FilterCriterion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter criterion 3 | * 4 | * TO-DO: Support AND and OR constructions. 5 | * TO-DO: When AND and OR constructions are supported, replace 6 | * IMPORTANCE_XXX filters with TAG_VALUE_AS_NUMBER_XXX. 7 | * E.g., IMPORTANCE_EQ = 7 could be generalized as 8 | * TAG_EQ = 'importance' AND TAG_VALUE_AS_NUMBER_EQ = 7. 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export enum FilterCriterion { 13 | 14 | // GTE = Greater Than or Equal to 15 | // LTE = Less Than or Equal to 16 | // EQ = Equal to 17 | // NEQ = Not Equal to 18 | 19 | IMPORTANCE_GTE = 'importance_gte', 20 | IMPORTANCE_LTE = 'importance_lte', 21 | IMPORTANCE_EQ = 'importance_eq', 22 | 23 | IGNORE_TAG_NOT_DECLARED = 'ignore_tag_not_declared', 24 | 25 | TAG_EQ = 'tag_eq', 26 | TAG_NEQ = 'tag_neq', 27 | TAG_IN = 'tag_in', 28 | TAG_NOT_IN = 'tag_not_in', 29 | 30 | NAME_EQ = 'name_eq', 31 | NAME_STARTING_WITH = 'name_starting_with', 32 | NAME_ENDING_WITH = 'name_ending_with', 33 | NAME_CONTAINING = 'name_containing' 34 | } -------------------------------------------------------------------------------- /__tests__/lexer/StepAndLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { StepAndLexer } from "../../modules/lexer/StepAndLexer"; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'StepAndLexer', () => { 5 | 6 | // IMPORTANT: This lexer inherits from StartingKeywordLexer 7 | // and StartingKeywordLexerTest checks many important aspects 8 | // that does not need to be repeated here. 9 | 10 | let words = [ 'and' ]; 11 | let lexer = new StepAndLexer( words ); // under test 12 | 13 | it( 'detects correctly', () => { 14 | let line = " \t \t And \t the world and everybody on it \t"; 15 | let r = lexer.analyze( line, 1 ); 16 | expect( r ).toBeDefined(); 17 | expect( r.nodes ).toHaveLength( 1 ); 18 | let node = r.nodes[ 0 ]; 19 | // Location 20 | expect( node.location.line ).toBe( 1 ); 21 | expect( node.location.column ).toBe( 8 ); 22 | // Keyword 23 | expect( node.nodeType ).toBe( NodeTypes.STEP_AND ); 24 | // Content 25 | expect( node.content ).toBe( 'And \t the world and everybody on it' ); 26 | } ); 27 | 28 | } ); -------------------------------------------------------------------------------- /docs/pt/migration.md: -------------------------------------------------------------------------------- 1 | # Guia de Migração 2 | 3 | - [Versão `0.x` para `1.x`](#vers%C3%A3o-0x-para-1x) 4 | - [Saiba mais](#saiba-mais) 5 | 6 | ## Versão `0.x` para `1.x` 7 | 8 | 1. **Atualize seu arquivo de configuração, caso necessário** 9 | 10 | 1. Caso seu projeto possua o arquivo de configuração `.concordiarc`, abra-o com um editor de texto. 11 | 2. Se no arquivo houver a propriedade `"plugin"` com o valor `"codeceptjs"`, você deve atualizar o valor para `"codeceptjs-webdriverio"`. 12 | 13 | 2. **Instale o novo plug-in** 14 | 15 | Você pode instalar um dos [plug-ins disponíveis](./plugins.md), atualmente `codeceptjs-webdriverio` ou `codeceptjs-appium`. 16 | 17 | Exemplo: 18 | ```bash 19 | concordia --plugin-install codeceptjs-webdriverio 20 | ``` 21 | 👉 No **Linux** ou no **MacOS**, é necessário usar `sudo`. Exemplo: 22 | ```bash 23 | sudo concordia --plugin-install codeceptjs-webdriverio 24 | ``` 25 | 26 | ## Saiba mais 27 | 28 | - [Quais foram as quebras de compatibilidade](breaking-changes.md) 29 | - [Como o projeto Concordia numera suas versões](versioning.md) -------------------------------------------------------------------------------- /modules/parser/RegexBlockParser.ts: -------------------------------------------------------------------------------- 1 | import { RegexBlock } from "../ast/RegexBlock"; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from "./NodeParser"; 4 | import { ParsingContext } from "./ParsingContext"; 5 | import { SyntacticException } from "./SyntacticException"; 6 | 7 | /** 8 | * Regex block parser 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class RegexBlockParser implements NodeParser< RegexBlock > { 13 | 14 | /** @inheritDoc */ 15 | public analyze( node: RegexBlock, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 16 | 17 | if ( context.doc.regexBlock ) { 18 | let e = new SyntacticException( 'Just one regex block declaration is allowed.', node.location ); 19 | errors.push( e ); 20 | return false; 21 | } 22 | 23 | // Adjust the context 24 | context.resetInValues(); 25 | context.inRegexBlock = true; 26 | context.currentRegexBlock = node; 27 | 28 | // Add to the doc 29 | context.doc.regexBlock = node; 30 | 31 | return true; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /__tests__/lexer/StepGivenLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { StepGivenLexer } from "../../modules/lexer/StepGivenLexer"; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'StepGivenLexer', () => { 5 | 6 | // IMPORTANT: This lexer inherits from StartingKeywordLexer 7 | // and StartingKeywordLexerTest checks many important aspects 8 | // that does not need to be repeated here. 9 | 10 | let words = [ 'given' ]; 11 | let lexer = new StepGivenLexer( words ); // under test 12 | 13 | it( 'detects correctly', () => { 14 | let line = " \t \t Given \t the world and everybody on it \t"; 15 | let r = lexer.analyze( line, 1 ); 16 | expect( r ).toBeDefined(); 17 | expect( r.nodes ).toHaveLength( 1 ); 18 | let node = r.nodes[ 0 ]; 19 | // Location 20 | expect( node.location.line ).toBe( 1 ); 21 | expect( node.location.column ).toBe( 8 ); 22 | // Keyword 23 | expect( node.nodeType ).toBe( NodeTypes.STEP_GIVEN ); 24 | // Content 25 | expect( node.content ).toBe( 'Given \t the world and everybody on it' ); 26 | } ); 27 | 28 | } ); -------------------------------------------------------------------------------- /modules/req/LineChecker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Line checker 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export class LineChecker { 7 | 8 | public isEmpty( line: string ): boolean { 9 | return 0 === line.trim().length; 10 | } 11 | 12 | public countLeftSpacesAndTabs( line: string ): number { 13 | let i = 0, len = line.length, found = true, ch; 14 | while ( i < len && found ) { 15 | ch = line.charAt( i++ ); 16 | found = ( ' ' == ch || "\t" == ch ); 17 | } 18 | return i - 1; 19 | } 20 | 21 | public caseInsensitivePositionOf( text: string, line: string ): number { 22 | return line.toLowerCase().indexOf( text.toLowerCase() ); 23 | } 24 | 25 | public textAfterSeparator( separator: string, line: string ) { 26 | let i = line.indexOf( separator ); 27 | return i >= 0 && i < ( line.length - 1 ) ? line.substr( i + 1 ) : ''; 28 | } 29 | 30 | public textBeforeSeparator( separator: string, line: string ) { 31 | let i = line.indexOf( separator ); 32 | return i > 0 ? line.substring( 0, i ) : ''; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomDouble.spec.ts: -------------------------------------------------------------------------------- 1 | import { Random } from '../../../modules/testdata/random/Random'; 2 | import { RandomDouble } from '../../../modules/testdata/random/RandomDouble'; 3 | 4 | describe( 'RandomDouble', () => { 5 | 6 | let random: RandomDouble = new RandomDouble( new Random() ); 7 | let delta: number = 0.0000000001; 8 | 9 | it( 'generates a random value between min and max, inclusive', () => { 10 | const x = 100; 11 | const min = x + delta; 12 | const max = x + ( delta * 2 ); 13 | let val: number = random.between( min, max ); 14 | expect( val ).toBeGreaterThanOrEqual( min ); 15 | expect( val ).toBeLessThanOrEqual( max ); 16 | } ); 17 | 18 | it( 'generates a value greater than a min value', () => { 19 | const min = -2.0; 20 | let val: number = random.after( min, delta ); 21 | expect( val ).toBeGreaterThan( min ); 22 | } ); 23 | 24 | it( 'generates a value less than a max value', () => { 25 | const max = 2.0; 26 | let val: number = random.before( max, delta ); 27 | expect( val ).toBeLessThan( max ); 28 | } ); 29 | 30 | } ); -------------------------------------------------------------------------------- /modules/parser/ConstantBlockParser.ts: -------------------------------------------------------------------------------- 1 | import { ConstantBlock } from "../ast/ConstantBlock"; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from "./NodeParser"; 4 | import { ParsingContext } from "./ParsingContext"; 5 | import { SyntacticException } from "./SyntacticException"; 6 | 7 | /** 8 | * Constant block parser 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class ConstantBlockParser implements NodeParser< ConstantBlock > { 13 | 14 | /** @inheritDoc */ 15 | public analyze( node: ConstantBlock, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 16 | 17 | if ( context.doc.constantBlock ) { 18 | let e = new SyntacticException( 'Just one constant block declaration is allowed.', node.location ); 19 | errors.push( e ); 20 | return false; 21 | } 22 | 23 | // Adjust the context 24 | context.resetInValues(); 25 | context.inConstantBlock = true; 26 | context.currentConstantBlock = node; 27 | 28 | // Add to the doc 29 | context.doc.constantBlock = node; 30 | 31 | return true; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /modules/parser/TableRowParser.ts: -------------------------------------------------------------------------------- 1 | import { TableRow } from "../ast/Table"; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from "./NodeParser"; 4 | import { ParsingContext } from "./ParsingContext"; 5 | import { SyntacticException } from './SyntacticException'; 6 | 7 | /** 8 | * TableRow parser. 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class TableRowParser implements NodeParser< TableRow > { 13 | 14 | /** @inheritDoc */ 15 | public analyze( node: TableRow, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 16 | 17 | if ( ! context.inTable || ! context.currentTable ) { 18 | let e = new SyntacticException( 19 | 'A table row should be declared after a Table declaration.', node.location ); 20 | errors.push( e ); 21 | return false; 22 | } 23 | 24 | // Checks the structure 25 | if ( ! context.currentTable.rows ) { 26 | context.currentTable.rows = []; 27 | } 28 | 29 | // Adds the node 30 | context.currentTable.rows.push( node ); 31 | 32 | return true; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /docs/pt/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | > Esse arquivo necessita de tradução para Português. 4 | 5 | ### *Why Concordia?* 6 | 7 | Concordia [is a roman goddess](https://www.britannica.com/topic/Concordia-Roman-goddess) who was the personification of *"concord"* or *"agreement"*. The idea is that the language may help users, stakeholders, and the software team to discuss and to reach an agreement about the software requirements. This shared understanding is essencial to the software construction. 8 | 9 | 10 | ### *How to generate test cases or test scripts for a single file?* 11 | 12 | Just inform the parameter `--files` with the desired file. For example: 13 | ```console 14 | concordia --files="myfile.feature" ... 15 | ``` 16 | However, whether your file imports other files, you need to include them too: 17 | 18 | ```console 19 | concordia --files="myfile.feature,other.feature" ... 20 | ``` 21 | 22 | ### *How to execute a single test script?* 23 | 24 | This is yet not supported by Concordia. However, you can use your testing tool directly. For example, this will execute the test `myfile.js` with CodeceptJS: 25 | ```console 26 | codeceptjs run --steps myfile.js 27 | ``` 28 | -------------------------------------------------------------------------------- /modules/error/LocatedException.ts: -------------------------------------------------------------------------------- 1 | import { Location } from 'concordialang-types'; 2 | 3 | /** 4 | * Provides an exception that contains information about its location. 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export abstract class LocatedException extends Error { 9 | 10 | name = 'LocatedException'; 11 | isWarning: boolean = false; 12 | 13 | constructor( 14 | message?: string, 15 | public location?: Location, 16 | messageShouldIncludeFilePath: boolean = false 17 | ) { 18 | super( LocatedException.makeExceptionMessage( message, location, messageShouldIncludeFilePath ) ); 19 | } 20 | 21 | public static makeExceptionMessage( 22 | originalMessage?: string, 23 | location?: Location, 24 | includeFilePath: boolean = false 25 | ): string { 26 | let msg = ''; 27 | if ( location ) { 28 | msg += '(' + location.line + ',' + location.column + ') '; 29 | if ( includeFilePath && location.filePath ) { 30 | msg += location.filePath + ': '; 31 | } 32 | } 33 | msg += originalMessage || ''; 34 | return msg; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /docs/en/cycle.md: -------------------------------------------------------------------------------- 1 | # ♺ Recommended usage cycle 2 | 3 | 1. Write or update your requirements specification with the *Concordia Language* and validate it with users or stakeholders; 4 | 5 | 2. Use *Concordia Compiler* to generate tests from the specification and to run them; 6 | 7 | 3. If the tests **failed**, there are some possibilities: 8 | 9 | 1. You still haven't implemented the corresponding behavior in your application. In this case, just implement it and run the tests again. 10 | 11 | 2. Your application is behaving differently from the specification. In this case, it may have bugs or you or your team haven't implemented the behavior exactly like described in the specification. - Whether the application has a bug, we are happy to have discovered it! Just fix it and run the tests again to make sure that the bug is gone. 12 | - Otherwise, you can decide between **changing your application** to behave exactly like the specification describes, or **changing the specification** to match your application behavior. In the latter case, back to step `1`. 13 | 14 | 4. If the tests **passed**, *great job!* Now you can write new requirements or add more test cases, so just back to step `1`. 15 | -------------------------------------------------------------------------------- /modules/parser/TableParser.ts: -------------------------------------------------------------------------------- 1 | import { CaseType } from "../util/CaseType"; 2 | import { Table } from "../ast/Table"; 3 | import { convertCase, removeDiacritics } from "../util/case-conversor"; 4 | import { NodeIterator } from './NodeIterator'; 5 | import { NodeParser } from "./NodeParser"; 6 | import { ParsingContext } from "./ParsingContext"; 7 | 8 | /** 9 | * Table parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class TableParser implements NodeParser< Table > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: Table, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Checks the structure 19 | if ( ! context.doc.tables ) { 20 | context.doc.tables = []; 21 | } 22 | 23 | // Generates the internal name 24 | node.internalName = removeDiacritics( convertCase( node.name, CaseType.SNAKE ) ); 25 | 26 | // Adjusts the content 27 | context.resetInValues(); 28 | context.inTable = true; 29 | context.currentTable = node; 30 | 31 | // Adds the node 32 | context.doc.tables.push( node ); 33 | 34 | return true; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /__tests__/plugin/plugin-loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { OldPluginData, PluginData } from "../../modules/plugin/PluginData"; 2 | import { loadPlugin } from "../../modules/plugin/plugin-loader"; 3 | 4 | describe( 'plugin-loader', () => { 5 | 6 | it( 'does not load with an empty plugin data', async () => { 7 | const pluginData = { 8 | } as PluginData; 9 | await expect( loadPlugin( pluginData ) ).rejects.toThrowError(); 10 | } ); 11 | 12 | it( 'tries to load with an old plugin structure', async () => { 13 | 14 | const pluginData = { 15 | name: 'foo', 16 | description: '...', 17 | version: '1.0', 18 | authors: [], 19 | main: undefined, 20 | class: 'FooClass', 21 | file: 'old.js' 22 | } as OldPluginData; 23 | 24 | await expect( loadPlugin( pluginData ) ).rejects.toThrowError( /^Cannot find module 'old.js'/ ); 25 | } ); 26 | 27 | it( 'tries to load with new plugin structure', async () => { 28 | 29 | const pluginData = { 30 | name: 'bar', 31 | description: '...', 32 | version: '1.0', 33 | authors: [], 34 | main: 'new.js' 35 | } as PluginData; 36 | 37 | await expect( loadPlugin( pluginData ) ).rejects.toThrowError( /^Cannot find module 'new.js'/ ); 38 | } ); 39 | 40 | } ); 41 | -------------------------------------------------------------------------------- /modules/error/ErrorSorting.ts: -------------------------------------------------------------------------------- 1 | import { LocatedException } from './LocatedException'; 2 | 3 | /** 4 | * Returns the errors sorted by `location`, without considering the file name. 5 | * 6 | * When two locations are not defined for comparison, it considers the flag 7 | * `isWarning`. 8 | * 9 | * @param errors Errors 10 | */ 11 | export function sortErrorsByLocation( errors: LocatedException[] ): LocatedException[] { 12 | 13 | const compare = ( a: LocatedException, b: LocatedException ) => { 14 | 15 | if ( a.location && b.location ) { 16 | // Compare the line 17 | let lineDiff: number = a.location.line - b.location.line; 18 | if ( 0 === lineDiff ) { // Same line, so let's compare the column 19 | return a.location.column - b.location.column; 20 | } 21 | return lineDiff; 22 | } 23 | // No location, so let's compare the error type 24 | // If both are warnings, they are equal 25 | if ( a.isWarning && b.isWarning ) { 26 | return 0; 27 | } 28 | return a.isWarning ? 1 : -1; 29 | }; 30 | 31 | // return Array.sort( errors, compare ); 32 | return errors.sort( compare ); 33 | } 34 | -------------------------------------------------------------------------------- /modules/parser/ImportParser.ts: -------------------------------------------------------------------------------- 1 | import { Import } from '../ast/Import'; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from "./NodeParser"; 4 | import { ParsingContext } from "./ParsingContext"; 5 | import { SyntacticException } from "./SyntacticException"; 6 | 7 | /** 8 | * Import parser. 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class ImportParser implements NodeParser< Import > { 13 | 14 | /** @inheritDoc */ 15 | public analyze( 16 | node: Import, 17 | context: ParsingContext, 18 | it: NodeIterator, 19 | errors: Error[] 20 | ): boolean { 21 | 22 | // Checks the structure 23 | if ( ! context.doc.imports ) { 24 | context.doc.imports = []; 25 | } 26 | 27 | // Checks if a feature is declared before it 28 | if ( context.doc.feature ) { 29 | let e = new SyntacticException( 'An import must be declared before a feature.', node.location ); 30 | errors.push( e ); 31 | return false; 32 | } 33 | 34 | // Add the import node to the document 35 | context.doc.imports.push( node ); 36 | 37 | return true; 38 | } 39 | } -------------------------------------------------------------------------------- /modules/parser/AfterAllParser.ts: -------------------------------------------------------------------------------- 1 | import { AfterAll } from '../ast/TestEvent'; 2 | import { isDefined } from '../util/type-checking'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from './ParsingContext'; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * AfterAll parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class AfterAllParser implements NodeParser< AfterAll > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: AfterAll, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Check whether a similar node was already declared 19 | if ( isDefined( context.doc.afterAll ) ) { 20 | let e = new SyntacticException( 21 | 'Event already declared: After All', node.location ); 22 | errors.push( e ); 23 | return false; 24 | } 25 | 26 | // Adjust the context 27 | context.resetInValues(); 28 | context.inAfterAll = true; 29 | 30 | // Adjust the document 31 | context.doc.afterAll = node; 32 | 33 | return true; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /docs/pt/readme.md: -------------------------------------------------------------------------------- 1 | # 📖 Documentação 2 | 3 | ## Abordagem 4 | 5 | - [Como funciona](how-it-works.md) 6 | - [Ciclo de uso recomendado](cycle.md) 7 | 8 | ## Linguagem e ferramenta 9 | 10 | - [Visão geral da linguagem](language.md) 11 | - [Ações](actions.md) 12 | - [Exemplo](example.md) 13 | - [Casos de Teste gerados](test-cases.md) 14 | 15 | ## Compilador e plug-ins 16 | 17 | - [Plug-ins](plugins.md) 18 | - [Arquivo de configuração](config.md) 19 | - [Dicas e truques](tips-and-tricks.md) 20 | - [Guia de Migração](migration.md) 21 | - [Quebras de Compatibilidade](breaking-changes.md) 22 | - [Numeração de Versões](versioning.md) 23 | 24 | ## Desenvolvimento 25 | 26 | - [Roadmap](../roadmap.md) *(inglês)* 27 | - [Diretrizes de desenvolvimento](development.md) *(inglês)* 28 | 29 | #### Anotações técnicas 30 | 31 | - [Propriedades de Elementos de Interface de Usuário](../en/dev/properties.md) *(inglês)* 32 | - [Consultas](../en/dev/queries.md) *(inglês)* 33 | - [Estados](../en/dev/states.md) *(inglês)* 34 | - [Casos de Teste](../en/dev/test-cases.md) *(inglês)* 35 | - [Cenários de Teste](../en/dev/test-scenarios.md) *(inglês)* 36 | - [Geração de dados](../en/dev/data-generation.md) *(inglês)* 37 | 38 | ## Outros 39 | 40 | - [FAQ](faq.md) -------------------------------------------------------------------------------- /modules/parser/BeforeAllParser.ts: -------------------------------------------------------------------------------- 1 | import { BeforeAll } from '../ast/TestEvent'; 2 | import { isDefined } from '../util/type-checking'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from './ParsingContext'; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * BeforeAll parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class BeforeAllParser implements NodeParser< BeforeAll > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: BeforeAll, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Check whether a similar node was already declared 19 | if ( isDefined( context.doc.beforeAll ) ) { 20 | let e = new SyntacticException( 21 | 'Event already declared: Before All', node.location ); 22 | errors.push( e ); 23 | return false; 24 | } 25 | 26 | // Adjust the context 27 | context.resetInValues(); 28 | context.inBeforeAll = true; 29 | 30 | // Adjust the document 31 | context.doc.beforeAll = node; 32 | 33 | return true; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /__tests__/lexer/DatabaseLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseLexer } from '../../modules/lexer/DatabaseLexer'; 2 | import { NodeTypes } from '../../modules/req/NodeTypes'; 3 | 4 | describe( 'DatabaseLexer', () => { 5 | 6 | // IMPORTANT: Since DatabaseLexer inherits from NamedNodeLexer and it does not add 7 | // behavior, the tests in NameNodeLexerTest already cover most important cases. 8 | 9 | let words = [ 'database' ]; 10 | let lexer = new DatabaseLexer( words ); // under test 11 | 12 | it( 'detects correctly', () => { 13 | let r = lexer.analyze( 'Database: My DB', 1 ); 14 | expect( r ).not.toBeNull(); 15 | expect( r.errors ).toHaveLength( 0 ); 16 | expect( r.nodes ).toHaveLength( 1 ); 17 | let n = r.nodes[ 0 ]; 18 | expect( n.nodeType ).toBe( NodeTypes.DATABASE ); 19 | expect( n.name ).toBe( 'My DB' ); 20 | } ); 21 | 22 | it( 'ignores a comment', () => { 23 | let r = lexer.analyze( 'Database: My DB#comment', 1 ); 24 | expect( r ).not.toBeNull(); 25 | expect( r.errors ).toHaveLength( 0 ); 26 | expect( r.nodes ).toHaveLength( 1 ); 27 | let n = r.nodes[ 0 ]; 28 | expect( n.name ).toBe( 'My DB' ); 29 | } ); 30 | 31 | } ); -------------------------------------------------------------------------------- /modules/ast/TestEvent.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | import { Step } from './Step'; 3 | 4 | /** 5 | * Test event 6 | */ 7 | export interface TestEvent extends Node { 8 | 9 | /** 10 | * Normal Given-When-Then sentences. Events about Feature and Scenario usually 11 | * can interact with the application through its user interface, while 12 | * the others can't. 13 | */ 14 | sentences: Step[]; 15 | } 16 | 17 | /** 18 | * Executed before all the tests. Should be declared once in all the specification. 19 | */ 20 | export interface BeforeAll extends TestEvent {} 21 | 22 | /** 23 | * Executed after all the tests. Should be declared once in all the specification. 24 | */ 25 | export interface AfterAll extends TestEvent {} 26 | 27 | /** 28 | * Executed before the current feature. 29 | */ 30 | export interface BeforeFeature extends TestEvent {} 31 | 32 | /** 33 | * Executed after the current feature. 34 | */ 35 | export interface AfterFeature extends TestEvent {} 36 | 37 | /** 38 | * Executed before each scenario. 39 | */ 40 | export interface BeforeEachScenario extends TestEvent {} 41 | 42 | /** 43 | * Executed after each scenario. 44 | */ 45 | export interface AfterEachScenario extends TestEvent {} 46 | -------------------------------------------------------------------------------- /modules/req/Symbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Language symbols. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export abstract class Symbols { 7 | 8 | // prefixes 9 | 10 | static COMMENT_PREFIX = '#'; 11 | static IMPORT_PREFIX = '"'; 12 | static TAG_PREFIX = '@'; 13 | static LANGUAGE_PREFIX = '#'; 14 | static PY_STRING_PREFIX = '"""'; 15 | static TABLE_PREFIX = '|'; 16 | static LIST_ITEM_PREFIX = '-'; 17 | static UI_ELEMENT_PREFIX = '{'; 18 | static UI_LITERAL_PREFIX = '<'; 19 | static CONSTANT_PREFIX = '['; 20 | 21 | // sufixes 22 | static IMPORT_SUFFIX = '"'; 23 | static UI_LITERAL_SUFFIX = '>'; 24 | static UI_ELEMENT_SUFFIX = '}'; 25 | static CONSTANT_SUFFIX = ']'; 26 | 27 | // separators 28 | 29 | static LANGUAGE_SEPARATOR = ':'; 30 | static TITLE_SEPARATOR = ':'; 31 | static TABLE_CELL_SEPARATOR = '|'; 32 | static IMPORT_SEPARATOR = ','; 33 | static REGEX_SEPARATOR = ':'; 34 | static TAG_VALUE_SEPARATOR = ','; 35 | static VALUE_SEPARATOR = ','; 36 | 37 | static FEATURE_TO_UI_ELEMENT_SEPARATOR = ':'; 38 | static UI_PROPERTY_REF_SEPARATOR = '|'; 39 | 40 | // wrappers 41 | 42 | static VALUE_WRAPPER = '"'; 43 | static COMMAND_WRAPPER = "'"; 44 | } -------------------------------------------------------------------------------- /docs/pt/cycle.md: -------------------------------------------------------------------------------- 1 | # ♺ Ciclo de uso recomendado 2 | 3 | 1. Escreva ou atualize sua especificação de requisitos com a *Linguagem Concordia* e valide-a com usuários ou interessados; 4 | 5 | 2. Use o **Compilador Concordia** para gerar testes a partir da especificação e os execute; 6 | 7 | 3. Se os testes **falharam**, há algumas possibilidades, como: 8 | 9 | 1. Você não implementou o comportamento correspondente na sua aplicação. Nesse caso, basta implementar e executar os testes novamente. 10 | 11 | 2. Sua aplicação está se comportando diferente do especificado. Nesse caso, ela pode ter bugs ou pode ser que você, ou sua equipe, não tenham implementado o compartamento exatamente como descrito na especificação. 12 | 13 | - Se ela tem um bug, ficamos felizes em tê-lo descoberto! Corrija-o e execute os testes novamente, para ter certeza que ele se foi. 14 | 15 | - Caso contrário, você pode decidir em **alterar a sua aplicação** para se comportar exatamente como havia sido especificado, ou **alterar a especificação** para casar com o comportamento da sua aplicação. No último caso, volte ao passo `1`. 16 | 17 | 4. Se os testes **passaram**, *bom trabalho!* Agora você pode escrever novos requisitos or adicionar mais casos testes. Nesse caso, basta voltar ao passo `1`. 18 | -------------------------------------------------------------------------------- /modules/util/best-match.ts: -------------------------------------------------------------------------------- 1 | 2 | type TextMatch = { 3 | value: string, 4 | index: number, 5 | rating: number 6 | }; 7 | 8 | /** 9 | * Returns the best match of a text compared to a list. 10 | * 11 | * @param text Text to compare. 12 | * @param values Values to compare. 13 | * @param comparingFunction Comparing function. 14 | */ 15 | export function bestMatch( 16 | text: string, 17 | values: string[], 18 | comparingFunction: ( a: string, b: string ) => number 19 | ): TextMatch | null { 20 | const matches = sortedMatches( text, values, comparingFunction ); 21 | const [ first ] = matches; 22 | return first || null; 23 | } 24 | 25 | /** 26 | * Returns a list of matches sorted by rating (descending). 27 | * 28 | * @param text Text to compare. 29 | * @param values Values to compare. 30 | * @param comparingFunction Comparing function. 31 | */ 32 | export function sortedMatches( 33 | text: string, 34 | values: string[], 35 | comparingFunction: ( a: string, b: string ) => number 36 | ): TextMatch[] { 37 | 38 | if ( ! text || ! values || values.length < 1 || ! comparingFunction ) { 39 | return []; 40 | } 41 | 42 | return values 43 | .map( ( v, i ) => ({ value: v, index: i, rating: comparingFunction( text, v ) }) ) 44 | .sort( ( a, b ) => b.rating - a.rating ); 45 | } 46 | -------------------------------------------------------------------------------- /__tests__/db/QueryParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { QueryParser } from '../../modules/db/QueryParser'; 2 | 3 | describe( 'QueryParser', () => { 4 | 5 | let parser = new QueryParser(); // under test 6 | 7 | it( 'parses all variables correctly', () => { 8 | let result = parser.parseAnyVariables( 'SELECT a, b FROM {one}, {two} WHERE {three} and {foo:bar}' ); 9 | let [ r1, r2, r3, r4 ] = result; 10 | expect( r1 ).toBe( 'one' ); 11 | expect( r2 ).toBe( 'two' ); 12 | expect( r3 ).toBe( 'three' ); 13 | expect( r4 ).toBe( 'foo:bar' ); 14 | } ); 15 | 16 | it( 'parses all names correctly', () => { 17 | let result = parser.parseAnyNames( 'SELECT a, b FROM [one], [two] WHERE [three]' ); 18 | let [ x, y, z ] = result; 19 | expect( x ).toBe( 'one' ); 20 | expect( y ).toBe( 'two' ); 21 | expect( z ).toBe( 'three' ); 22 | } ); 23 | 24 | it( 'does not parse excel table names as names', () => { 25 | let result = parser.parseAnyNames( 26 | 'SELECT a, b FROM [one], [excel table$], [excel$A1$B2], [two] WHERE [three]' ); 27 | let [ x, y, z ] = result; 28 | expect( x ).toBe( 'one' ); 29 | expect( y ).toBe( 'two' ); 30 | expect( z ).toBe( 'three' ); 31 | } ); 32 | 33 | } ); -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomDate.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalDate } from "@js-joda/core"; 2 | import { Random } from "../../../modules/testdata/random/Random"; 3 | import { RandomDate } from "../../../modules/testdata/random/RandomDate"; 4 | import { RandomLong } from "../../../modules/testdata/random/RandomLong"; 5 | 6 | 7 | describe( 'RandomDate', () => { 8 | 9 | let random: RandomDate = new RandomDate( new RandomLong( new Random() ) ); 10 | 11 | it( 'generates a random value between min and max, inclusive', () => { 12 | const min = LocalDate.of( 2018, 1, 1 ), max = LocalDate.of( 2018, 1, 31 ); 13 | const val: LocalDate = random.between( min, max ); 14 | expect( val.isAfter( min ) || val.isEqual( min ) ).toBeTruthy(); 15 | expect( val.isBefore( max ) || val.isEqual( max ) ).toBeTruthy(); 16 | } ); 17 | 18 | it( 'generates a value greater than a min value', () => { 19 | const min = LocalDate.of( 2018, 1, 1 ); 20 | const val: LocalDate = random.after( min ); 21 | expect( val.isAfter( min ) ).toBeTruthy(); 22 | } ); 23 | 24 | it( 'generates a value less than a max value', () => { 25 | const max = LocalDate.of( 2018, 1, 31 ); 26 | const val: LocalDate = random.before( max ); 27 | expect( val.isBefore( max ) ).toBeTruthy(); 28 | } ); 29 | 30 | } ); -------------------------------------------------------------------------------- /__tests__/lexer/ConstantBlockLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConstantBlock } from '../../modules/ast'; 2 | import { ConstantBlockLexer } from '../../modules/lexer/ConstantBlockLexer'; 3 | import { NodeTypes } from '../../modules/req/NodeTypes'; 4 | 5 | describe( 'ConstantBlockLexer', () => { 6 | 7 | let words = [ 'constants' ]; 8 | let lexer = new ConstantBlockLexer( words ); // under test 9 | 10 | // IMPORTANT: Since the lexer under test inherits from another lexer and 11 | // there are tests for the parent class, few additional tests are necessary. 12 | 13 | it( 'detects in the correct position', () => { 14 | let line = '\tConstants\t:\t'; 15 | let node = lexer.analyze( line, 1 ).nodes[ 0 ]; 16 | expect( node ).toEqual( 17 | { 18 | nodeType: NodeTypes.CONSTANT_BLOCK, 19 | location: { line: 1, column: 2 } 20 | } as ConstantBlock 21 | ); 22 | } ); 23 | 24 | it( 'ignores a comment', () => { 25 | let line = 'Constants:# some comment here'; 26 | let node = lexer.analyze( line, 1 ).nodes[ 0 ]; 27 | expect( node ).toEqual( 28 | { 29 | nodeType: NodeTypes.CONSTANT_BLOCK, 30 | location: { line: 1, column: 1 } 31 | } as ConstantBlock 32 | ); 33 | } ); 34 | 35 | } ); -------------------------------------------------------------------------------- /__tests__/lexer/RegexBlockLexer.spec.ts: -------------------------------------------------------------------------------- 1 | import { RegexBlock } from '../../modules/ast'; 2 | import { RegexBlockLexer } from '../../modules/lexer/RegexBlockLexer'; 3 | import { NodeTypes } from '../../modules/req/NodeTypes'; 4 | 5 | describe( 'RegexBlockLexer', () => { 6 | 7 | let words = [ 'regular expressions' ]; 8 | let lexer = new RegexBlockLexer( words );; // under test 9 | 10 | // IMPORTANT: Since the lexer under test inherits from another lexer and 11 | // there are tests for the parent class, few additional tests are necessary. 12 | 13 | it( 'detects in the correct position', () => { 14 | let line = '\tRegular expressions\t:\t'; 15 | let node = lexer.analyze( line, 1 ).nodes[ 0 ]; 16 | expect( node ).toEqual( 17 | { 18 | nodeType: NodeTypes.REGEX_BLOCK, 19 | location: { line: 1, column: 2 } 20 | } as RegexBlock 21 | ); 22 | } ); 23 | 24 | it( 'ignores a comment', () => { 25 | let line = 'Regular expressions:# some comment here'; 26 | let node = lexer.analyze( line, 1 ).nodes[ 0 ]; 27 | expect( node ).toEqual( 28 | { 29 | nodeType: NodeTypes.REGEX_BLOCK, 30 | location: { line: 1, column: 1 } 31 | } as RegexBlock 32 | ); 33 | } ); 34 | 35 | } ); -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomTime.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalTime } from "@js-joda/core"; 2 | import { Random } from "../../../modules/testdata/random/Random"; 3 | import { RandomLong } from "../../../modules/testdata/random/RandomLong"; 4 | import { RandomTime } from "../../../modules/testdata/random/RandomTime"; 5 | 6 | 7 | describe( 'RandomTime', () => { 8 | 9 | let random: RandomTime = new RandomTime( new RandomLong( new Random() ) ); 10 | 11 | it( 'generates a random value between min and max, inclusive', () => { 12 | const min = LocalTime.of( 12, 0, 0 ), max = LocalTime.of( 13, 0, 0 ); 13 | const val: LocalTime = random.between( min, max ); 14 | expect( val.isAfter( min ) || 0 === val.compareTo( min ) ).toBeTruthy(); 15 | expect( val.isBefore( max ) || 0 === val.compareTo( max ) ).toBeTruthy(); 16 | } ); 17 | 18 | it( 'generates a value greater than a min value', () => { 19 | const min = LocalTime.of( 12, 0, 0 ); 20 | const val: LocalTime = random.after( min ); 21 | expect( val.isAfter( min ) ).toBeTruthy(); 22 | } ); 23 | 24 | it( 'generates a value less than a max value', () => { 25 | const max = LocalTime.of( 13, 0, 0 ); 26 | const val: LocalTime = random.before( max ); 27 | expect( val.isBefore( max ) ).toBeTruthy(); 28 | } ); 29 | 30 | } ); -------------------------------------------------------------------------------- /modules/nlp/syntax/SyntaxRule.ts: -------------------------------------------------------------------------------- 1 | import { Entities } from "../Entities"; 2 | 3 | /** 4 | * Minimal and maximal values of each target. 5 | * 6 | * They will be only considered if they appear in "targets". 7 | * If they do appear in "targets", they will be *disconsidered* if: 8 | * - min > minTargets 9 | * - max > maxTargets 10 | * 11 | */ 12 | export interface Occurrence { 13 | min: number, 14 | max: number 15 | } 16 | 17 | export type EntityOccurrence = { 18 | [ key in keyof typeof Entities ]?: Occurrence 19 | }; 20 | 21 | export type SyntaxRule = EntityOccurrence & { 22 | 23 | /** Rule name */ 24 | name?: string; 25 | 26 | /** Minimal number of targets accepted. It has precedence over all 'min' values. */ 27 | minTargets?: number; 28 | /** Maximal number of targets accepted. It has precedence over all 'max' values. */ 29 | maxTargets?: number; 30 | 31 | /** 32 | * Accepted targets (NLP entities). 33 | * When "maxTargets" is 1 and "targets" has more than one ui element, 34 | * it accepts one OR another. 35 | * When "maxTargets" > 1, the minimal of each target should be configured. 36 | */ 37 | targets?: Array< Entities >; 38 | 39 | /** 40 | * Other entity/action or entities/actions that must be used together. 41 | */ 42 | mustBeUsedWith?: Array< string >; 43 | } 44 | -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomShortTime.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalTime } from "@js-joda/core"; 2 | import { Random } from "../../../modules/testdata/random/Random"; 3 | import { RandomLong } from "../../../modules/testdata/random/RandomLong"; 4 | import { RandomShortTime } from "../../../modules/testdata/random/RandomShortTime"; 5 | 6 | 7 | describe( 'RandomShortTime', () => { 8 | 9 | let random: RandomShortTime = new RandomShortTime( new RandomLong( new Random() ) ); 10 | 11 | it( 'generates a random value between min and max, inclusive', () => { 12 | const min = LocalTime.of( 12, 0 ), max = LocalTime.of( 13, 0 ); 13 | const val: LocalTime = random.between( min, max ); 14 | expect( val.isAfter( min ) || 0 === val.compareTo( min ) ).toBeTruthy(); 15 | expect( val.isBefore( max ) || 0 === val.compareTo( max ) ).toBeTruthy(); 16 | } ); 17 | 18 | it( 'generates a value greater than a min value', () => { 19 | const min = LocalTime.of( 12, 0 ); 20 | const val: LocalTime = random.after( min ); 21 | expect( val.isAfter( min ) ).toBeTruthy(); 22 | } ); 23 | 24 | it( 'generates a value less than a max value', () => { 25 | const max = LocalTime.of( 13, 0 ); 26 | const val: LocalTime = random.before( max ); 27 | expect( val.isBefore( max ) ).toBeTruthy(); 28 | } ); 29 | 30 | } ); -------------------------------------------------------------------------------- /__tests__/util/best-match.spec.ts: -------------------------------------------------------------------------------- 1 | import { sortedMatches, bestMatch } from '../../modules/util/best-match'; 2 | 3 | describe( 'best-match', () => { 4 | 5 | describe( 'sortedMatches', () => { 6 | 7 | it.each( [ 8 | [ undefined, [ 'a' ], ( a, b ) => 1 ], 9 | [ 'a', undefined, ( a, b ) => 1 ], 10 | [ 'a', [ 'a' ], undefined ], 11 | ] )( 'returns an empty array when something is undefined', 12 | ( t, v, f ) => { 13 | expect( sortedMatches( t, v, f ) ).toEqual( [] ); 14 | } ); 15 | 16 | it( 'sorts matches by rating descending', () => { 17 | expect( sortedMatches( 18 | 'a', [ 'a', 'b' ], ( a, b ) => a == b ? 1: 0 19 | ) ).toEqual( [ 20 | { value: 'a', index: 0, rating: 1 }, 21 | { value: 'b', index: 1, rating: 0 }, 22 | ] ); 23 | } ); 24 | 25 | } ); 26 | 27 | 28 | describe( 'bestMatch', () => { 29 | 30 | it.each( [ 31 | [ undefined, [ 'a' ], ( a, b ) => 1 ], 32 | [ 'a', undefined, ( a, b ) => 1 ], 33 | [ 'a', [ 'a' ], undefined ], 34 | ] )( 'returns null when something is undefined', 35 | ( t, v, f ) => { 36 | expect( bestMatch( t, v, f ) ).toEqual( null ); 37 | } ); 38 | 39 | it( 'returns the best match', () => { 40 | expect( bestMatch( 41 | 'a', [ 'a', 'b' ], ( a, b ) => a == b ? 1: 0 42 | ) ).toEqual( 43 | { value: 'a', index: 0, rating: 1 } 44 | ); 45 | } ); 46 | 47 | } ); 48 | 49 | } ); 50 | -------------------------------------------------------------------------------- /modules/testdata/random/RandomDouble.ts: -------------------------------------------------------------------------------- 1 | import { DoubleLimits } from '../limits/DoubleLimits'; 2 | import { Random } from './Random'; 3 | 4 | /** 5 | * Generates random double values. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export class RandomDouble { 10 | 11 | constructor( private _random: Random ) { 12 | } 13 | 14 | /** 15 | * Generates a random number between a minimum and a maximum value, both 16 | * inclusive. 17 | * 18 | * @param min The minimum value (inclusive). 19 | * @param max The maximum value (inclusive). 20 | * @return A number between the minimum and the maximum. 21 | */ 22 | public between( min: number, max: number ): number { 23 | let num = this._random.generate(); 24 | return min + ( num * ( max - min ) ); 25 | } 26 | 27 | /** 28 | * Generates a random value before a maximum value. 29 | * 30 | * @param max The maximum value. 31 | * @return A random value before the maximum value. 32 | */ 33 | public before( value: number, delta: number ): number { 34 | return this.between( DoubleLimits.MIN, value - delta ); 35 | } 36 | 37 | /** 38 | * Generates a random value after a minimum value. 39 | * 40 | * @param min The minimum value. 41 | * @return A random value after the minimum value. 42 | */ 43 | public after( value: number, delta: number ): number { 44 | return this.between( value + delta, DoubleLimits.MAX ); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /__tests__/util/remove-duplicated.spec.ts: -------------------------------------------------------------------------------- 1 | import { removeDuplicated } from '../../modules/util/remove-duplicated'; 2 | 3 | describe( 'removeDuplicated', () => { 4 | 5 | it( 'removes using strict equality by default', () => { 6 | let arr = [ 1, 2, 3, 2, 4 ]; 7 | removeDuplicated( arr ); 8 | expect( arr ).toEqual( [ 1, 2, 3, 4 ] ); 9 | } ); 10 | 11 | it( 'uses comparison function #1', () => { 12 | let arr = [ 1, 2, 3, 2, 4 ]; 13 | removeDuplicated( arr, ( a, b ) => a === b ); 14 | expect( arr ).toEqual( [ 1, 2, 3, 4 ] ); 15 | } ); 16 | 17 | it( 'uses comparison function #2', () => { 18 | let arr = [ 1, 1 ]; 19 | removeDuplicated( arr, ( a, b ) => a === b ); 20 | expect( arr ).toEqual( [ 1 ] ); 21 | } ); 22 | 23 | it( 'removes objects #1', () => { 24 | const e1 = new Error( '1' ); 25 | const e2 = new Error( '2' ); 26 | let arr = [ e1, e2, e1 ]; 27 | removeDuplicated( arr, ( a, b ) => a.message === b.message ); 28 | expect( arr ).toEqual( [ e1, e2 ] ); 29 | } ); 30 | 31 | it( 'removes objects #2', () => { 32 | const e1 = new Error( '1' ); 33 | const e2 = new Error( '2' ); 34 | let arr = [ e1, e1, e2, e1, e2, e2 ]; 35 | removeDuplicated( arr, ( a, b ) => a.message === b.message ); 36 | expect( arr ).toEqual( [ e1, e2 ] ); 37 | } ); 38 | 39 | } ); -------------------------------------------------------------------------------- /modules/parser/ScenarioParser.ts: -------------------------------------------------------------------------------- 1 | import { Scenario } from '../ast/Scenario'; 2 | import { NodeIterator } from './NodeIterator'; 3 | import { NodeParser } from './NodeParser'; 4 | import { ParsingContext } from './ParsingContext'; 5 | import { SyntacticException } from './SyntacticException'; 6 | 7 | /** 8 | * Scenario parser 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class ScenarioParser implements NodeParser< Scenario > { 13 | 14 | /** @inheritDoc */ 15 | public analyze( node: Scenario, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 16 | 17 | // Checks if a feature has been declared before it 18 | if ( ! context.doc.feature ) { 19 | let e = new SyntacticException( 20 | 'A scenario must be declared after a feature.', node.location ); 21 | errors.push( e ); 22 | return false; 23 | } 24 | 25 | // Prepare the feature to receive the scenario 26 | let feature = context.doc.feature; 27 | if ( ! feature.scenarios ) { 28 | feature.scenarios = []; 29 | } 30 | 31 | // Adds the scenario to the feature 32 | feature.scenarios.push( node ); 33 | 34 | // Adjust the context 35 | context.resetInValues(); 36 | context.inScenario = true; 37 | context.currentScenario = node; 38 | 39 | return true; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /modules/testdata/DataTestCaseNames.ts: -------------------------------------------------------------------------------- 1 | export interface DataTestCaseNames { 2 | 3 | // value 4 | VALUE_LOWEST: string; 5 | VALUE_RANDOM_BELOW_MIN: string; 6 | VALUE_JUST_BELOW_MIN: string; 7 | VALUE_MIN: string; 8 | VALUE_JUST_ABOVE_MIN: string; 9 | VALUE_ZERO: string; 10 | VALUE_MEDIAN: string; 11 | VALUE_RANDOM_BETWEEN_MIN_MAX: string; 12 | VALUE_JUST_BELOW_MAX: string; 13 | VALUE_MAX: string; 14 | VALUE_JUST_ABOVE_MAX: string; 15 | VALUE_RANDOM_ABOVE_MAX: string; 16 | VALUE_GREATEST: string; 17 | 18 | // length 19 | LENGTH_LOWEST: string; // zero/empty 20 | LENGTH_RANDOM_BELOW_MIN: string; 21 | LENGTH_JUST_BELOW_MIN: string; 22 | LENGTH_MIN: string; 23 | LENGTH_JUST_ABOVE_MIN: string; 24 | LENGTH_MEDIAN: string; 25 | LENGTH_RANDOM_BETWEEN_MIN_MAX: string; 26 | LENGTH_JUST_BELOW_MAX: string; 27 | LENGTH_MAX: string; 28 | LENGTH_JUST_ABOVE_MAX: string; 29 | LENGTH_RANDOM_ABOVE_MAX: string; 30 | LENGTH_GREATEST: string; 31 | 32 | // format 33 | FORMAT_VALID: string; 34 | FORMAT_INVALID: string; 35 | 36 | // set 37 | SET_FIRST_ELEMENT: string; 38 | // SET_SECOND_ELEMENT: string; 39 | SET_RANDOM_ELEMENT: string; 40 | // SET_PENULTIMATE_ELEMENT: string; 41 | SET_LAST_ELEMENT: string; 42 | SET_NOT_IN_SET: string; 43 | 44 | // required 45 | REQUIRED_FILLED: string; 46 | REQUIRED_NOT_FILLED: string; 47 | 48 | // computation 49 | COMPUTATION_RIGHT: string; 50 | COMPUTATION_WRONG: string; 51 | } -------------------------------------------------------------------------------- /modules/semantic/single/BatchDocumentAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '../../ast/Document'; 2 | import { ProblemMapper } from '../../error/ProblemMapper'; 3 | import { SemanticException } from "../../error/SemanticException"; 4 | import { DatabaseDA } from './DatabaseDA'; 5 | import { DocumentAnalyzer } from './DocumentAnalyzer'; 6 | import { ImportDA } from './ImportDA'; 7 | import { ScenarioDA } from './ScenarioDA'; 8 | import { UIElementDA } from './UIElementDA'; 9 | import { VariantGivenStepDA } from './VariantGivenStepDA'; 10 | 11 | /** 12 | * Executes a series of semantic analyzers to a document. 13 | * 14 | * @author Thiago Delgado Pinto 15 | */ 16 | export class BatchDocumentAnalyzer { 17 | 18 | private readonly _analyzers: DocumentAnalyzer[]; 19 | 20 | constructor() { 21 | this._analyzers = [ 22 | new ImportDA(), 23 | new ScenarioDA(), 24 | new DatabaseDA(), 25 | new UIElementDA(), 26 | new VariantGivenStepDA() 27 | ]; 28 | } 29 | 30 | public analyze( doc: Document, errorMapper: ProblemMapper ) { 31 | for ( let analyzer of this._analyzers ) { 32 | const errors: SemanticException[] = []; 33 | analyzer.analyze( doc, errors ); 34 | if ( errors.length > 0 ) { 35 | errorMapper.addError( doc.fileInfo.path, ...errors ); 36 | } 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /modules/lexer/LongStringLexer.ts: -------------------------------------------------------------------------------- 1 | import { LongString } from '../ast/LongString'; 2 | import { NodeTypes } from '../req/NodeTypes'; 3 | import { Symbols } from '../req/Symbols'; 4 | import { LexicalAnalysisResult, NodeLexer } from './NodeLexer'; 5 | 6 | /** 7 | * Detects anything not empty. 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export class LongStringLexer implements NodeLexer< LongString > { 12 | 13 | /** @inheritDoc */ 14 | public nodeType(): string { 15 | return NodeTypes.LONG_STRING; 16 | } 17 | 18 | /** @inheritDoc */ 19 | suggestedNextNodeTypes(): string[] { 20 | return [ NodeTypes.LONG_STRING ]; 21 | } 22 | 23 | /** @inheritDoc */ 24 | public analyze( line: string, lineNumber?: number ): LexicalAnalysisResult< LongString > { 25 | 26 | // Empty line is not accepted 27 | if ( 0 === line.trim().length ) { 28 | return null; 29 | } 30 | 31 | // It must start with three quotes ("""), exactly. It may have spaces 32 | let re = new RegExp( '^""" *(' + Symbols.COMMENT_PREFIX + '.*)?$', 'u' ); 33 | if ( ! re.test( line ) ) { 34 | return null; 35 | } 36 | 37 | let node = { 38 | nodeType: NodeTypes.LONG_STRING, 39 | location: { line: lineNumber || 0, column: 1 } 40 | } as LongString; 41 | 42 | return { nodes: [ node ], errors: [] }; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /modules/testdata/random/RandomLong.ts: -------------------------------------------------------------------------------- 1 | import { LongLimits } from "../limits/LongLimits"; 2 | import { Random } from "./Random"; 3 | 4 | /** 5 | * Generates random long integer values. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export class RandomLong { 10 | 11 | constructor( private _random: Random ) { 12 | } 13 | 14 | /** 15 | * Generates a random number between a minimum and a maximum value, both 16 | * inclusive. 17 | * 18 | * @param min The minimum value (inclusive). 19 | * @param max The maximum value (inclusive). 20 | * @return A number between the minimum and the maximum. 21 | */ 22 | public between( min: number, max: number ): number { 23 | min = Math.ceil( min ); 24 | max = Math.floor( max ); 25 | return Math.floor( this._random.generate() * ( max - min + 1 ) ) + min; 26 | } 27 | 28 | /** 29 | * Generates a random value less than a maximum value. 30 | * 31 | * @param max The maximum value. 32 | * @return A random value less than a maximum value. 33 | */ 34 | public before( max: number ): number { 35 | return this.between( LongLimits.MIN, max - 1 ); 36 | } 37 | 38 | /** 39 | * Generates a random value greater than a minimum value. 40 | * 41 | * @param min The minimum value. 42 | * @return A random value greater than a minimum value. 43 | */ 44 | public after( min: number ): number { 45 | return this.between( min + 1, LongLimits.MAX ); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /docs/en/how-it-works.md: -------------------------------------------------------------------------------- 1 | # 🧠 How it works 2 | 3 | ![Process](../../media/process.png) 4 | 5 | 1. It reads your `.feature` and `.testcase` files, and uses a [lexer](https://en.wikipedia.org/wiki/Lexical_analysis) and a [parser](https://en.wikipedia.org/wiki/Parsing#Computer_languages) to identify and check documents' structure. 6 | 7 | 2. It uses [Natural Language Processing](https://en.wikipedia.org/wiki/Natural-language_processing) (NLP) to identify sentences' [intent](http://mrbot.ai/blog/natural-language-processing/understanding-intent-classification/). This increases the chances of recognizing sentences written in different styles. 8 | 9 | 3. It performs [semantic analysis](https://en.wikipedia.org/wiki/Semantic_analysis_(compilers)) to check recognized declarations. 10 | 11 | 4. It uses the specification to infer the most suitable *test cases*, *test data*, and *test oracles*, and then generates `.testcase` files in Concordia Language. 12 | 13 | 5. It transforms all the test cases into test scripts (that is, source code) using a plug-in. 14 | 15 | 6. It executes the test scripts with the plug-in. These test scripts will check your application's behavior through its user interface. 16 | 17 | 7. It reads and presents execution results. These results relate failing tests to the specification, in order to help you understanding the possible reasons of a failure. 18 | 19 | 20 | 👉 See the [set of generated test cases](docs/test-cases.md). 21 | -------------------------------------------------------------------------------- /modules/nlp/NLPUtil.ts: -------------------------------------------------------------------------------- 1 | import { NLPEntity } from "./NLPEntity"; 2 | import { NLPResult } from "./NLPResult"; 3 | 4 | /** 5 | * NLP utilities. 6 | * 7 | * @author Thiago Delgado Pinto 8 | */ 9 | export class NLPUtil { 10 | 11 | entitiesNamed( name: string, nlpResult: NLPResult ): NLPEntity[] { 12 | if ( ! name || ! nlpResult ) { 13 | return []; 14 | } 15 | return nlpResult.entities.filter( e => name === e.entity ); 16 | } 17 | 18 | hasEntityNamed( name: string, nlpResult: NLPResult ): boolean { 19 | return this.entitiesNamed( name, nlpResult ).length > 0; 20 | } 21 | 22 | /** 23 | * Returns true if the NLPResult has all the informed entity names. 24 | * 25 | * @param names 26 | * @param nlpResult 27 | */ 28 | hasEntitiesNamed( names: string[], nlpResult: NLPResult ): boolean { 29 | return names.every( name => this.hasEntityNamed( name, nlpResult ) ); 30 | } 31 | 32 | entityNamed( name: string, nlpResult: NLPResult ): NLPEntity | null { 33 | if ( ! name || ! nlpResult ) { 34 | return null; 35 | } 36 | return nlpResult.entities.find( e => name === e.entity ) || null; 37 | } 38 | 39 | valuesOfEntitiesNamed( name: string, nlpResult: NLPResult ): string[] { 40 | if ( ! name || ! nlpResult ) { 41 | return []; 42 | } 43 | return nlpResult.entities.filter( e => name === e.entity ).map( e => e.value ); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /docs/en/plugins/codeceptjs.md: -------------------------------------------------------------------------------- 1 | # Plug-ins for CodeceptJS 2 | 3 | Available plug-ins for CodeceptJS: 4 | - `codeceptjs` for testing web applications 5 | - `codeceptjs-appium` for testing mobile or desktop applications 6 | 7 | The above plug-ins shall generate a default configuration file `codecept.json` when that file is not found. Instead, you can configure CodeceptJS by yourself with the following command: 8 | 9 | ```bash 10 | codeceptjs init 11 | ``` 12 | 13 | 14 | ## `codeceptjs` 15 | 16 | Generates the following `codecept.json`: 17 | 18 | ```json 19 | { 20 | "tests": "test/**/*.js", 21 | "output": "output", 22 | "helpers": { 23 | "WebDriverIO": { 24 | "browser": "chrome", 25 | "url": "http://localhost", 26 | "windowSize": "maximize", 27 | "smartWait": 5000, 28 | "timeouts": { 29 | "script": 60000, 30 | "page load": 10000 31 | } 32 | }, 33 | "DbHelper": { 34 | "require": "./node_modules/codeceptjs-dbhelper" 35 | }, 36 | "CmdHelper": { 37 | "require": "./node_modules/codeceptjs-cmdhelper" 38 | } 39 | }, 40 | "bootstrap": false, 41 | "mocha": { 42 | "reporterOptions": { 43 | "codeceptjs-cli-reporter": { 44 | "stdout": "-", 45 | "options": { 46 | "steps": true 47 | } 48 | }, 49 | "json": { 50 | "stdout": "output/output.json" 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ## `codeceptjs-appium` 58 | 59 | Generates the following `codecept.json`: 60 | 61 | ```json 62 | ``` -------------------------------------------------------------------------------- /docs/pt/plugins/codeceptjs.md: -------------------------------------------------------------------------------- 1 | # Plug-ins para CodeceptJS 2 | 3 | Plug-ins disponíveis para CodeceptJS: 4 | - `codeceptjs` para testar aplicações para a web 5 | - `codeceptjs-appium` para testar aplicações para dispositivos móveis e desktop 6 | 7 | Os plug-ins acima gerarão um arquivo de configuração `codecept.json` quando este não for encontrado. Caso desejar, você mesmo pode configurar o CodeceptJS pelo seguinte comando: 8 | 9 | ```bash 10 | codeceptjs init 11 | ``` 12 | 13 | 14 | ## `codeceptjs` 15 | 16 | Gera o seguinte `codecept.json`: 17 | 18 | ```json 19 | { 20 | "tests": "test/**/*.js", 21 | "output": "output", 22 | "helpers": { 23 | "WebDriverIO": { 24 | "browser": "chrome", 25 | "url": "http://localhost", 26 | "windowSize": "maximize", 27 | "smartWait": 5000, 28 | "timeouts": { 29 | "script": 60000, 30 | "page load": 10000 31 | } 32 | }, 33 | "DbHelper": { 34 | "require": "./node_modules/codeceptjs-dbhelper" 35 | }, 36 | "CmdHelper": { 37 | "require": "./node_modules/codeceptjs-cmdhelper" 38 | } 39 | }, 40 | "bootstrap": false, 41 | "mocha": { 42 | "reporterOptions": { 43 | "codeceptjs-cli-reporter": { 44 | "stdout": "-", 45 | "options": { 46 | "steps": true 47 | } 48 | }, 49 | "json": { 50 | "stdout": "output/output.json" 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ## `codeceptjs-appium` 58 | 59 | Gera o seguinte `codecept.json`: 60 | 61 | ```json 62 | ``` -------------------------------------------------------------------------------- /modules/lexer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BackgroundLexer'; 2 | export * from './BlockLexer'; 3 | export * from './CommentHandler'; 4 | export * from './ConstantBlockLexer'; 5 | export * from './ConstantLexer'; 6 | export * from './DatabaseLexer'; 7 | export * from './DatabasePropertyLexer'; 8 | export * from './FeatureLexer'; 9 | export * from './ImportLexer'; 10 | export * from './KeywordBasedLexer'; 11 | export * from './LanguageLexer'; 12 | export * from './Lexer'; 13 | export * from './LexicalException'; 14 | export * from './ListItemLexer'; 15 | export * from './LongStringLexer'; 16 | export * from './NamedNodeLexer'; 17 | export * from './NamePlusNumberNodeLexer'; 18 | export * from './NodeLexer'; 19 | export * from './QuotedNodeLexer'; 20 | export * from './RegexBlockLexer'; 21 | export * from './RegexLexer'; 22 | export * from './ScenarioLexer'; 23 | export * from './StartingKeywordLexer'; 24 | export * from './StepAndLexer'; 25 | export * from './StepGivenLexer'; 26 | export * from './StepOtherwiseLexer'; 27 | export * from './StepThenLexer'; 28 | export * from './StepWhenLexer'; 29 | export * from './TableLexer'; 30 | export * from './TableRowLexer'; 31 | export * from './TagLexer'; 32 | export * from './TestCaseLexer'; 33 | export * from './TestEventLexer'; 34 | export * from './TextLexer'; 35 | export * from './UIElementLexer'; 36 | export * from './UIPropertyLexer'; 37 | export * from './VariantBackgroundLexer'; 38 | export * from './VariantLexer'; 39 | -------------------------------------------------------------------------------- /__tests__/db/database-package-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { fs, vol } from 'memfs'; 2 | import * as path from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | import { allInstalledDatabases } from '../../modules/db/database-package-manager'; 6 | import { FSDirSearcher } from '../../modules/util/fs/FSDirSearcher'; 7 | 8 | describe( 'database-package-manager', () => { 9 | 10 | const currentDir: string = path.normalize( process.cwd() ); 11 | const localModulesDir: string = path.join( currentDir, 'node_modules' ); 12 | 13 | // beforeEach( () => { 14 | // vol.mkdirpSync( currentDir, { recursive: true } as any ); // Synchronize - IMPORTANT! - mkdirpSync, not mkdirSync 15 | // vol.mkdirpSync( localModulesDir ); 16 | // } ); 17 | 18 | afterEach( () => { 19 | vol.reset(); // erase in-memory structure 20 | } ); 21 | 22 | it( 'finds it correctly', async () => { 23 | 24 | vol.mkdirpSync( currentDir, { recursive: true } as any ); // Synchronize - IMPORTANT! - mkdirpSync, not mkdirSync 25 | vol.mkdirpSync( localModulesDir ); 26 | 27 | vol.mkdirpSync( path.join( localModulesDir, 'database-js' ) ); 28 | vol.mkdirpSync( path.join( localModulesDir, 'database-js-json' ) ); 29 | 30 | const s = new FSDirSearcher( fs, promisify ); 31 | const r = await allInstalledDatabases( localModulesDir, s ); 32 | 33 | expect( r.length ).toEqual( 1 ); 34 | expect( r[ 0 ] ).toEqual( 'json' ); 35 | } ); 36 | 37 | } ); 38 | -------------------------------------------------------------------------------- /modules/ast/Database.ts: -------------------------------------------------------------------------------- 1 | import { ListItem } from './ListItem'; 2 | import { HasItems, HasValue, NamedNode } from './Node'; 3 | 4 | // Example: 5 | // ``` 6 | // Database: My Test DB 7 | // - type is "mysql" 8 | // - path is "mytestdb" 9 | // - host is "127.0.0.1" 10 | // - username is "admin" 11 | // - password is "adminpass" 12 | // - charset is "UTF-8" 13 | // ``` 14 | 15 | /** 16 | * Database node. 17 | * 18 | * @author Thiago Delgado Pinto 19 | */ 20 | export interface Database extends NamedNode, HasItems< DatabaseProperty > { 21 | } 22 | 23 | /** 24 | * Database item node. 25 | * 26 | * @author Thiago Delgado Pinto 27 | */ 28 | export interface DatabaseProperty extends ListItem, HasValue { 29 | property: string; 30 | } 31 | 32 | /** 33 | * Database properties. 34 | * 35 | * Files could also be represented as a database, using "type", "path", and maybe "options". 36 | * Example: { type: 'json', path: 'C://path/to/file.json' } 37 | * 38 | * @author Thiago Delgado Pinto 39 | */ 40 | export enum DatabaseProperties { 41 | TYPE = 'type', // should work as a "driver". e.g. 'mysql', 'mongodb', ... 42 | PATH = 'path', // also serves as "name" or "alias" 43 | HOST = 'host', 44 | PORT = 'port', 45 | USERNAME = 'username', 46 | PASSWORD = 'password', 47 | CHARSET = 'charset', 48 | OPTIONS = 'options' 49 | } 50 | 51 | export enum DatabasePropertyAlias { 52 | NAME = 'name' // alias for "path" 53 | } 54 | -------------------------------------------------------------------------------- /__tests__/selection/TagUtil.spec.ts: -------------------------------------------------------------------------------- 1 | import { TagUtil } from "../../modules/selection/TagUtil"; 2 | import { Tag } from "../../modules/ast/Tag"; 3 | 4 | describe( 'TagUtil', () => { 5 | 6 | it( 'filters the tag names that has the given keywords', () => { 7 | 8 | const [ t1, t2, t3 ]: Tag[] = [ 9 | { 10 | name: 'foo' 11 | } as Tag, 12 | { 13 | name: 'bar' 14 | } as Tag, 15 | { 16 | name: 'foo' 17 | } as Tag, 18 | ]; 19 | 20 | const u = new TagUtil(); 21 | const r = u.tagsWithNameInKeywords( [ t1, t2, t3 ], [ 'foo' ] ); 22 | expect( r ).toEqual( [ t1, t3 ] ); 23 | } ); 24 | 25 | 26 | it( 'recognizes the content of the first tag', () => { 27 | 28 | const tag = { 29 | content: 'x' 30 | } as Tag; 31 | 32 | const u = new TagUtil(); 33 | const r = u.contentOfTheFirstTag( [ tag ] ); 34 | expect( r ).toEqual( 'x' ); 35 | } ); 36 | 37 | 38 | it( 'recognizes a numeric content of the first tag', () => { 39 | 40 | const tag = { 41 | content: '10' 42 | } as Tag; 43 | 44 | const u = new TagUtil(); 45 | const r = u.numericContentOfTheFirstTag( [ tag ] ); 46 | expect( r ).toEqual( 10 ); 47 | } ); 48 | 49 | 50 | it( 'recognizes an invalid numeric content of the first tag as null', () => { 51 | 52 | const tag = { 53 | content: 'x' 54 | } as Tag; 55 | 56 | const u = new TagUtil(); 57 | const r = u.numericContentOfTheFirstTag( [ tag ] ); 58 | expect( r ).toEqual( null ); 59 | } ); 60 | 61 | } ); 62 | -------------------------------------------------------------------------------- /__tests__/language/locale-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { fs, vol } from 'memfs'; 2 | import * as path from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | import { installedDateLocales } from '../../modules/language/locale-manager'; 6 | import { FSDirSearcher } from '../../modules/util/fs/FSDirSearcher'; 7 | 8 | describe( 'locale-manager', () => { 9 | 10 | const currentDir: string = path.normalize( process.cwd() ); 11 | const localModulesDir: string = path.join( currentDir, 'node_modules' ); 12 | const libraryDir = path.join( localModulesDir, 'date-fns' ); 13 | const localeDir = path.join( libraryDir, 'locale' ); 14 | 15 | beforeEach( () => { 16 | vol.mkdirpSync( currentDir, { recursive: true } as any ); // Synchronize - IMPORTANT! - mkdirpSync, not mkdirSync 17 | vol.mkdirpSync( localModulesDir ); 18 | vol.mkdirpSync( libraryDir ); 19 | vol.mkdirpSync( localeDir ); 20 | } ); 21 | 22 | afterEach( () => { 23 | vol.reset(); // erase in-memory structure 24 | } ); 25 | 26 | it( 'finds locales correctly', async () => { 27 | 28 | const enUS = 'en-US'; 29 | const ptBR = 'pt-BR'; 30 | vol.mkdirpSync( path.join( localeDir, enUS ) ); 31 | vol.mkdirpSync( path.join( localeDir, ptBR ) ); 32 | 33 | const s = new FSDirSearcher( fs, promisify ); 34 | const r = await installedDateLocales( localModulesDir, s, path ); 35 | 36 | expect( r.length ).toEqual( 2 ); 37 | expect( r ).toEqual( [ enUS, ptBR ] ); 38 | } ); 39 | 40 | } ); 41 | -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomDateTime.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalDateTime } from "@js-joda/core"; 2 | import { Random } from "../../../modules/testdata/random/Random"; 3 | import { RandomDateTime } from "../../../modules/testdata/random/RandomDateTime"; 4 | import { RandomLong } from "../../../modules/testdata/random/RandomLong"; 5 | 6 | 7 | describe( 'RandomDateTime', () => { 8 | 9 | let random: RandomDateTime = new RandomDateTime( new RandomLong( new Random() ) ); 10 | 11 | it( 'generates a random value between min and max, inclusive', () => { 12 | const min = LocalDateTime.of( 2018, 1, 1, 12, 0, 0 ); 13 | const max = LocalDateTime.of( 2018, 1, 31, 13, 0, 0 ); 14 | const val: LocalDateTime = random.between( min, max ); 15 | expect( val.isAfter( min ) || 0 === val.compareTo( min ) ).toBeTruthy(); 16 | expect( val.isBefore( max ) || 0 === val.compareTo( max ) ).toBeTruthy(); 17 | } ); 18 | 19 | it( 'generates a value greater than a min value', () => { 20 | const min = LocalDateTime.of( 2018, 1, 1, 12, 0, 0 ); 21 | const val: LocalDateTime = random.after( min ); 22 | expect( val.isAfter( min ) ).toBeTruthy(); 23 | } ); 24 | 25 | it( 'generates a value less than a max value', () => { 26 | const max = LocalDateTime.of( 2018, 1, 31, 13, 0, 0 ); 27 | const val: LocalDateTime = random.before( max ); 28 | expect( val.isBefore( max ) ).toBeTruthy(); 29 | } ); 30 | 31 | } ); -------------------------------------------------------------------------------- /modules/testdata/random/RandomDate.ts: -------------------------------------------------------------------------------- 1 | import { ChronoUnit, LocalDate } from "@js-joda/core"; 2 | import { DateLimits } from "../limits/DateLimits"; 3 | import { RandomLong } from "./RandomLong"; 4 | 5 | /** 6 | * Generates random date values. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class RandomDate { 11 | 12 | constructor( private _randomLong: RandomLong ) { 13 | } 14 | 15 | /** 16 | * Returns a random date between two given dates, both inclusive. 17 | * 18 | * @param min Minimum date 19 | * @param max Maximum date 20 | */ 21 | public between( min: LocalDate, max: LocalDate ): LocalDate { 22 | const daysBetween: number = min.until( max, ChronoUnit.DAYS ); 23 | if ( 0 === daysBetween ) { 24 | return min; 25 | } 26 | const days: number = this._randomLong.between( 0, daysBetween ); 27 | return min.plusDays( days ); 28 | } 29 | 30 | /** 31 | * Returns a random date before the given date. 32 | * 33 | * @param max Maximum date 34 | */ 35 | public before( max: LocalDate ): LocalDate { 36 | return this.between( DateLimits.MIN, max.minusDays( 1 ) ); 37 | } 38 | 39 | /** 40 | * Returns a random date after the given date. 41 | * 42 | * @param min Minimum date 43 | */ 44 | public after( min: LocalDate ): LocalDate { 45 | return this.between( min.plusDays( 1 ), DateLimits.MAX ); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /modules/semantic/single/ScenarioDA.ts: -------------------------------------------------------------------------------- 1 | import { Document, Scenario } from '../../ast'; 2 | import { SemanticException } from "../../error/SemanticException"; 3 | import { DuplicationChecker } from "../DuplicationChecker"; 4 | import { DocumentAnalyzer } from './DocumentAnalyzer'; 5 | 6 | /** 7 | * Analyzes Scenario declarations for a single document. 8 | * 9 | * It checks for: 10 | * - Duplicated scenario names 11 | * 12 | * @author Thiago Delgado Pinto 13 | */ 14 | export class ScenarioDA implements DocumentAnalyzer { 15 | 16 | /** @inheritDoc */ 17 | public analyze( doc: Document, errors: SemanticException[] ): void { 18 | 19 | // Checking the document structure 20 | if ( ! doc.feature ) { 21 | return; // nothing to do 22 | } 23 | if ( ! doc.feature.scenarios ) { 24 | doc.feature.scenarios = []; 25 | return; // nothing to do 26 | } 27 | 28 | this.checkForDuplicatedScenarios( doc, errors ); 29 | } 30 | 31 | private checkForDuplicatedScenarios( doc: Document, errors: SemanticException[] ) { 32 | let duplicated: Scenario[] = ( new DuplicationChecker() ) 33 | .withDuplicatedProperty( doc.feature.scenarios, 'name' ); 34 | for ( let dup of duplicated ) { 35 | let msg = 'Duplicated scenario "' + dup.name + '".'; 36 | let err = new SemanticException( msg, dup.location ); 37 | errors.push( err ); 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /__tests__/testdata/random/RandomShortDateTime.spec.ts: -------------------------------------------------------------------------------- 1 | import { LocalDateTime } from "@js-joda/core"; 2 | import { Random } from "../../../modules/testdata/random/Random"; 3 | import { RandomShortDateTime } from "../../../modules/testdata/random/RandomShortDateTime"; 4 | import { RandomLong } from "../../../modules/testdata/random/RandomLong"; 5 | 6 | 7 | describe( 'RandomShortDateTime', () => { 8 | 9 | let random: RandomShortDateTime = new RandomShortDateTime( new RandomLong( new Random() ) ); 10 | 11 | it( 'generates a random value between min and max, inclusive', () => { 12 | const min = LocalDateTime.of( 2018, 1, 1, 12, 0 ); 13 | const max = LocalDateTime.of( 2018, 1, 31, 13, 0 ); 14 | const val: LocalDateTime = random.between( min, max ); 15 | expect( val.isAfter( min ) || 0 === val.compareTo( min ) ).toBeTruthy(); 16 | expect( val.isBefore( max ) || 0 === val.compareTo( max ) ).toBeTruthy(); 17 | } ); 18 | 19 | it( 'generates a value greater than a min value', () => { 20 | const min = LocalDateTime.of( 2018, 1, 1, 12, 0 ); 21 | const val: LocalDateTime = random.after( min ); 22 | expect( val.isAfter( min ) ).toBeTruthy(); 23 | } ); 24 | 25 | it( 'generates a value less than a max value', () => { 26 | const max = LocalDateTime.of( 2018, 1, 31, 13, 0 ); 27 | const val: LocalDateTime = random.before( max ); 28 | expect( val.isBefore( max ) ).toBeTruthy(); 29 | } ); 30 | 31 | } ); -------------------------------------------------------------------------------- /modules/testdata/random/RandomTime.ts: -------------------------------------------------------------------------------- 1 | import { ChronoUnit, LocalTime } from "@js-joda/core"; 2 | import { TimeLimits } from "../limits/TimeLimits"; 3 | import { RandomLong } from "./RandomLong"; 4 | 5 | /** 6 | * Generates random time values. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class RandomTime { 11 | 12 | constructor( private _randomLong: RandomLong ) { 13 | } 14 | 15 | /** 16 | * Returns a random time between two given values, both inclusive. 17 | * 18 | * @param min Minimum time 19 | * @param max Maximum time 20 | */ 21 | public between( min: LocalTime, max: LocalTime ): LocalTime { 22 | const diffInSeconds: number = min.until( max, ChronoUnit.SECONDS ); 23 | if ( 0 === diffInSeconds ) { 24 | return min; 25 | } 26 | const seconds = this._randomLong.between( 0, diffInSeconds ); 27 | return min.plusSeconds( seconds ); 28 | } 29 | 30 | /** 31 | * Returns a random time before the given time. 32 | * 33 | * @param max Maximum time 34 | */ 35 | public before( max: LocalTime ): LocalTime { 36 | return this.between( TimeLimits.MIN, max.minusSeconds( 1 ) ); 37 | } 38 | 39 | /** 40 | * Returns a random time after the given time. 41 | * 42 | * @param min Minimum time 43 | */ 44 | public after( min: LocalTime ): LocalTime { 45 | return this.between( min.plusSeconds( 1 ), TimeLimits.MAX ); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /docs/en/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Language 4 | 5 | ### Features 6 | 7 | #### Improves the communication 8 | 9 | Concordia is inspired in [Gherkin](), a language used by many BDD/ATDD/SbE tools. Unfortunately, Gherkin has no different terms to separate high-level, business-directed declarations from low-level, technology-directed declarations. For example, a `Scenario` is used to describe both of them. 10 | 11 | Misunderstands leads to software that does not behave as intended. 12 | 13 | 14 | #### Flexible 15 | 16 | For example, the following sentences are considered equal: 17 | 18 | ```gherkin 19 | Given that I type "Alice" in <#name> 20 | ``` 21 | ```gherkin 22 | Given that I enter "Alice" in <#name> 23 | ``` 24 | ```gherkin 25 | Given that I fill <#name> with "Alice" 26 | ``` 27 | 28 | We use [NLP](https://en.wikipedia.org/wiki/Natural_language_processing) with a dictionary-based approach to try to understand what you mean. The Compiler will warn you when it can't understand something. 29 | 30 | #### Adaptable 31 | 32 | Since the language is based on dictionaries, new terms or new training sentences can be added by you (or your company) to improve its capacity to understand what you mean. You can also suggest new terms by [opening an Issue](https://github.com/thiagodp/concordialang/issues/new). 33 | 34 | #### Translatable 35 | 36 | New languages can be added easily by translating a JSON file (*e.g.*, `dist/data/en.json`) and putting it to the `dist/data` folder. 37 | 38 | -------------------------------------------------------------------------------- /modules/parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AfterAllParser'; 2 | export * from './AfterEachScenarioParser'; 3 | export * from './AfterFeatureParser'; 4 | export * from './BackgroundParser'; 5 | export * from './BeforeAllParser'; 6 | export * from './BeforeEachScenarioParser'; 7 | export * from './BeforeFeatureParser'; 8 | export * from './ConstantBlockParser'; 9 | export * from './ConstantParser'; 10 | export * from './DatabaseParser'; 11 | export * from './DatabasePropertyParser'; 12 | export * from './FeatureParser'; 13 | export * from './ImportParser'; 14 | export * from './LanguageParser'; 15 | export * from './ListItemNodeParser'; 16 | export * from './ListItemParser'; 17 | export * from './NodeIterator'; 18 | export * from './NodeParser'; 19 | export * from './Parser'; 20 | export * from './ParsingContext'; 21 | export * from './RegexBlockParser'; 22 | export * from './RegexParser'; 23 | export * from './ScenarioParser'; 24 | export * from './StepAndParser'; 25 | export * from './StepGivenParser'; 26 | export * from './StepOtherwiseParser'; 27 | export * from './StepThenParser'; 28 | export * from './StepWhenParser'; 29 | export * from './SyntacticException'; 30 | export * from './TableParser'; 31 | export * from './TableRowParser'; 32 | export * from './TagCollector'; 33 | export * from './TestCaseParser'; 34 | export * from './TextCollector'; 35 | export * from './UIElementParser'; 36 | export * from './UIPropertyParser'; 37 | export * from './VariantBackgroundParser'; 38 | export * from './VariantParser'; 39 | -------------------------------------------------------------------------------- /docs/pt/how-it-works.md: -------------------------------------------------------------------------------- 1 | # 🧠 Como o Compilador Concordia funciona 2 | 3 | ![Process](../../media/process.png) 4 | 5 | 1. Lê arquivos `.feature` e `.testcase` e usa um [lexer](https://pt.wikipedia.org/wiki/An%C3%A1lise_l%C3%A9xica) e um [parser](https://pt.wikipedia.org/wiki/An%C3%A1lise_sint%C3%A1tica_(computa%C3%A7%C3%A3o)) para identificar e verificar a estrutura dos documentos. 6 | 7 | 2. Usa [processamento de linguagem natural](https://pt.wikipedia.org/wiki/Processamento_de_linguagem_natural) para identificar a [intenção](http://mrbot.ai/blog/natural-language-processing/understanding-intent-classification/) das sentenças. Isso aumenta as changes de reconhecer sentenças em diferentes estilos de escrita. 8 | 9 | 3. Realiza uma [análise semântica](https://pt.wikipedia.org/wiki/An%C3%A1lise_sem%C3%A2ntica) para checar as declarações reconhecidas. 10 | 11 | 4. Usa a especificação para inferir os casos de teste, dados de teste e oráculos de teste e gera arquivos `.testcase` em Linguagem Concordia. 12 | 13 | 5. Transforma todos os casos de teste em scripts de teste (isso é, código-fonte) usando um plug-in. 14 | 15 | 6. Executa os scripts de teste através do mesmo plug-in. Esses scripts irão verificar o comportamento da aplicação através de sua interface de usuário. 16 | 17 | 7. Lê e apresenta os resultados da execução. Esses resultados relacionam testes que falharam com a especificação, de forma a ajudar a você a decidir as possíveis razões. 18 | 19 | 20 | > 👉 Veja também os [tipos de casos de teste gerados](test-cases.md). -------------------------------------------------------------------------------- /modules/db/database-package-manager.ts: -------------------------------------------------------------------------------- 1 | import { DirSearcher, DirSearchOptions } from "../util/file"; 2 | import { makeDatabasePackageNameFor, makePackageInstallCommand, makePackageUninstallCommand, PackageManager } from "../util/package-installation"; 3 | import { runCommand } from "../util/run-command"; 4 | 5 | 6 | export async function allInstalledDatabases( 7 | baseDirectory: string, 8 | dirSearcher: DirSearcher 9 | ): Promise< string[] > { 10 | 11 | const options: DirSearchOptions = { 12 | directory: baseDirectory, 13 | recursive: false, 14 | regexp: /database\-js\-(.+)$/ 15 | }; 16 | 17 | const directories = await dirSearcher.search( options ); 18 | if ( directories.length < 1 ) { 19 | return []; 20 | } 21 | 22 | const extractName = dir => options.regexp.exec( dir )[ 1 ]; 23 | return directories.map( extractName ); 24 | } 25 | 26 | export async function installDatabases( 27 | databasesOrPackageNames: string[], 28 | tool: PackageManager 29 | ): Promise< number > { 30 | const packages = databasesOrPackageNames.map( makeDatabasePackageNameFor ); 31 | const cmd = makePackageInstallCommand( packages.join( ' ' ), tool ); 32 | return await runCommand( cmd ); 33 | } 34 | 35 | export async function uninstallDatabases( 36 | databasesOrPackageNames: string[], 37 | tool: PackageManager 38 | ): Promise< number > { 39 | const packages = databasesOrPackageNames.map( makeDatabasePackageNameFor ); 40 | const cmd = makePackageUninstallCommand( packages.join( ' ' ), tool ); 41 | return await runCommand( cmd ); 42 | } 43 | -------------------------------------------------------------------------------- /modules/req/Expressions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Commonly-used regular expressions. 3 | * 4 | * @author Thiago Delgado Pinto 5 | */ 6 | export abstract class Expressions { 7 | 8 | static AT_LEAST_ONE_SPACE_OR_TAB_OR_COMMA: string = '(?:\t| |,)+'; // "?:" means "not remember" 9 | static OPTIONAL_SPACES_OR_TABS: string = '(?:\t| )*'; // "?:" means "not remember" 10 | 11 | static ANYTHING: string = '.*'; 12 | 13 | static SOMETHING_INSIDE_QUOTES = '("[^"\r\n]*")'; 14 | 15 | static A_NUMBER = '([0-9]+(\.[0-9]+)?)'; // integer or double 16 | 17 | static AN_INTEGER_NUMBER = '([0-9]+)'; 18 | 19 | /** 20 | * Escape characters to be used in a regex. 21 | * 22 | * @param text Text to be escaped. 23 | */ 24 | public static escape( text: string ): string { 25 | return text.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); 26 | } 27 | 28 | /** 29 | * Return escaped values. 30 | * 31 | * @param values Values to be escaped. 32 | */ 33 | public static escapeAll( values: string[] ): string[] { 34 | return values.map( ( val ) => Expressions.escape( val ) ); 35 | } 36 | 37 | /** 38 | * Returns a string with a regex to contain all the possible characters 39 | * except the given ones. 40 | * 41 | * @param values Not desired values. 42 | */ 43 | public static anythingBut( values: string[], modifiers: string = 'ug' ): RegExp { 44 | return new RegExp( '^((?![' + values.join( '' ) + ']).)*$', modifiers ); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /modules/testdata/random/RandomShortTime.ts: -------------------------------------------------------------------------------- 1 | import { ChronoUnit, LocalTime } from "@js-joda/core"; 2 | import { ShortTimeLimits } from "../limits/TimeLimits"; 3 | import { RandomLong } from "./RandomLong"; 4 | 5 | /** 6 | * Generates random short time values. 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export class RandomShortTime { 11 | 12 | constructor( private _randomLong: RandomLong ) { 13 | } 14 | 15 | /** 16 | * Returns a random short time between two given values, both inclusive. 17 | * 18 | * @param min Minimum time 19 | * @param max Maximum time 20 | */ 21 | public between( min: LocalTime, max: LocalTime ): LocalTime { 22 | const diffInMinutes: number = min.until( max, ChronoUnit.MINUTES ); 23 | if ( 0 === diffInMinutes ) { 24 | return min; 25 | } 26 | const minutes = this._randomLong.between( 0, diffInMinutes ); 27 | return min.plusMinutes( minutes ); 28 | } 29 | 30 | /** 31 | * Returns a random short time before the given time. 32 | * 33 | * @param max Maximum time 34 | */ 35 | public before( max: LocalTime ): LocalTime { 36 | return this.between( ShortTimeLimits.MIN, max.minusMinutes( 1 ) ); 37 | } 38 | 39 | /** 40 | * Returns a random short time after the given time. 41 | * 42 | * @param min Minimum time 43 | */ 44 | public after( min: LocalTime ): LocalTime { 45 | return this.between( min.plusMinutes( 1 ), ShortTimeLimits.MAX ); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /__tests__/SimpleCompiler.ts: -------------------------------------------------------------------------------- 1 | import { Document, FileInfo } from '../modules/ast'; 2 | import { SingleFileCompiler } from '../modules/compiler/SingleFileCompiler'; 3 | import { FileProblemMapper } from '../modules/error'; 4 | import languageMap from '../modules/language/data/map'; 5 | import { Lexer } from '../modules/lexer/Lexer'; 6 | import { NLPBasedSentenceRecognizer } from '../modules/nlp/NLPBasedSentenceRecognizer'; 7 | import { NLPTrainer } from '../modules/nlp/NLPTrainer'; 8 | import { Parser } from '../modules/parser/Parser'; 9 | import { AugmentedSpec } from '../modules/req/AugmentedSpec'; 10 | 11 | /** 12 | * Useful for testing purposes. 13 | * 14 | * TO-DO: Refactor its content to use SingleFileCompiler 15 | */ 16 | export class SimpleCompiler { 17 | 18 | constructor( public language = 'pt' ) { 19 | } 20 | 21 | lexer: Lexer = new Lexer( this.language, languageMap ); 22 | 23 | parser = new Parser(); 24 | 25 | nlpTrainer = new NLPTrainer( languageMap ); 26 | nlpRec: NLPBasedSentenceRecognizer = new NLPBasedSentenceRecognizer( this.nlpTrainer ); 27 | 28 | compiler = new SingleFileCompiler( this.lexer, this.parser, this.nlpRec, this.language ); 29 | 30 | async addToSpec( 31 | spec: AugmentedSpec, 32 | lines: string[], 33 | fileInfo?: FileInfo 34 | ): Promise< Document > { 35 | const doc = await this.compiler.processLines( 36 | new FileProblemMapper(), fileInfo ? fileInfo.path || '' : '', lines ); 37 | spec.addDocument( doc ); 38 | return doc; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /modules/language/KeywordDictionary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keyword dictionary 3 | * 4 | * @see Keywords 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export interface KeywordDictionary { // properties should exist in Keywords 9 | 10 | // Not available in Gherkin 11 | 12 | import: string[]; 13 | regexBlock: string[]; 14 | constantBlock: string[]; 15 | variant: string[]; 16 | variantBackground: string[]; 17 | testCase: string[]; 18 | uiElement: string[]; 19 | database: string[]; 20 | 21 | beforeAll: string[]; 22 | afterAll: string[]; 23 | beforeFeature: string[]; 24 | afterFeature: string[]; 25 | beforeEachScenario: string[]; 26 | afterEachScenario: string[]; 27 | 28 | i: string[]; 29 | is: string[]; 30 | with: string[]; 31 | valid: string[]; 32 | invalid: string[]; 33 | random: string[]; 34 | from: string[]; 35 | 36 | tagGlobal: string[]; 37 | tagFeature: string[]; 38 | tagScenario: string[]; 39 | tagVariant: string[]; 40 | tagImportance: string[]; 41 | tagIgnore: string[]; 42 | tagGenerated: string[]; 43 | tagFail: string[]; 44 | tagGenerateOnlyValidValues: string[]; 45 | 46 | // Also available in Gherkin 47 | 48 | language: string[]; 49 | 50 | feature: string[]; 51 | background: string[]; 52 | scenario: string[]; 53 | 54 | stepGiven: string[]; 55 | stepWhen: string[]; 56 | stepThen: string[]; 57 | stepAnd: string[]; 58 | stepOtherwise: string[]; 59 | 60 | table: string[]; 61 | 62 | } -------------------------------------------------------------------------------- /modules/parser/AfterFeatureParser.ts: -------------------------------------------------------------------------------- 1 | import { AfterFeature } from '../ast/TestEvent'; 2 | import { isDefined } from '../util/type-checking'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from './ParsingContext'; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * AfterFeature parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class AfterFeatureParser implements NodeParser< AfterFeature > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: AfterFeature, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Check whether a Feature was declared 19 | if ( ! context.doc.feature ) { 20 | let e = new SyntacticException( 21 | 'The event After Feature must be declared after a Feature', node.location ); 22 | errors.push( e ); 23 | return false; 24 | } 25 | 26 | // Check whether a similar node was already declared 27 | if ( isDefined( context.doc.afterFeature ) ) { 28 | let e = new SyntacticException( 29 | 'Event already declared: After Feature', node.location ); 30 | errors.push( e ); 31 | return false; 32 | } 33 | 34 | // Adjust the context 35 | context.resetInValues(); 36 | context.inAfterFeature = true; 37 | 38 | // Adjust the document 39 | context.doc.afterFeature = node; 40 | 41 | return true; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/parser/BeforeFeatureParser.ts: -------------------------------------------------------------------------------- 1 | import { BeforeFeature } from '../ast/TestEvent'; 2 | import { isDefined } from '../util/type-checking'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from './ParsingContext'; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * BeforeFeature parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class BeforeFeatureParser implements NodeParser< BeforeFeature > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: BeforeFeature, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Check whether a Feature was declared 19 | if ( ! context.doc.feature ) { 20 | let e = new SyntacticException( 21 | 'The event Before Feature must be declared after a Feature', node.location ); 22 | errors.push( e ); 23 | return false; 24 | } 25 | 26 | // Check whether a similar node was already declared 27 | if ( isDefined( context.doc.beforeFeature ) ) { 28 | let e = new SyntacticException( 29 | 'Event already declared: Before Feature', node.location ); 30 | errors.push( e ); 31 | return false; 32 | } 33 | 34 | // Adjust the context 35 | context.resetInValues(); 36 | context.inBeforeFeature = true; 37 | 38 | // Adjust the document 39 | context.doc.beforeFeature = node; 40 | 41 | return true; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/testdata/random/RandomDateTime.ts: -------------------------------------------------------------------------------- 1 | import { ChronoUnit, LocalDateTime } from "@js-joda/core"; 2 | import { DateTimeLimits } from '../limits/DateTimeLimits'; 3 | import { RandomLong } from './RandomLong'; 4 | 5 | 6 | /** 7 | * Generates random datetime values. 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export class RandomDateTime { 12 | 13 | constructor( private _randomLong: RandomLong ) { 14 | } 15 | 16 | /** 17 | * Returns a random date time between two given values, both inclusive. 18 | * 19 | * @param min Minimum date time 20 | * @param max Maximum date time 21 | */ 22 | public between( min: LocalDateTime, max: LocalDateTime ): LocalDateTime { 23 | const diffInSeconds: number = min.until( max, ChronoUnit.SECONDS ); 24 | if ( 0 === diffInSeconds ) { 25 | return min; 26 | } 27 | const seconds = this._randomLong.between( 0, diffInSeconds ); 28 | return min.plusSeconds( seconds ); 29 | } 30 | 31 | /** 32 | * Returns a random date time before the given date time. 33 | * 34 | * @param max Maximum date time 35 | */ 36 | public before( max: LocalDateTime ): LocalDateTime { 37 | return this.between( DateTimeLimits.MIN, max.minusSeconds( 1 ) ); 38 | } 39 | 40 | /** 41 | * Returns a random date time after the given date time. 42 | * 43 | * @param min Minimum date time 44 | */ 45 | public after( min: LocalDateTime ): LocalDateTime { 46 | return this.between( min.plusSeconds( 1 ), DateTimeLimits.MAX ); 47 | } 48 | } -------------------------------------------------------------------------------- /modules/lexer/TextLexer.ts: -------------------------------------------------------------------------------- 1 | import { Text } from '../ast/Text'; 2 | import { LineChecker } from "../req/LineChecker"; 3 | import { NodeTypes } from '../req/NodeTypes'; 4 | import { Symbols } from '../req/Symbols'; 5 | import { LexicalAnalysisResult, NodeLexer } from "./NodeLexer"; 6 | 7 | /** 8 | * Detects anything not empty. 9 | * 10 | * @author Thiago Delgado Pinto 11 | */ 12 | export class TextLexer implements NodeLexer< Text > { 13 | 14 | private _lineChecker: LineChecker = new LineChecker(); 15 | 16 | /** @inheritDoc */ 17 | public nodeType(): string { 18 | return NodeTypes.TEXT; 19 | } 20 | 21 | /** @inheritDoc */ 22 | suggestedNextNodeTypes(): string[] { 23 | return [ NodeTypes.TEXT ]; 24 | } 25 | 26 | /** @inheritDoc */ 27 | public analyze( line: string, lineNumber?: number ): LexicalAnalysisResult< Text > { 28 | 29 | let trimmedLine = line.trim(); 30 | 31 | // Empty line is not accepted 32 | if ( 0 === trimmedLine.length ) { 33 | return null; 34 | } 35 | 36 | // Comment is not accepted 37 | const commentPos = trimmedLine.indexOf( Symbols.COMMENT_PREFIX ); 38 | if ( 0 === commentPos ) { 39 | return null; 40 | } 41 | 42 | const pos = this._lineChecker.countLeftSpacesAndTabs( line ); 43 | 44 | let node = { 45 | nodeType: NodeTypes.TEXT, 46 | location: { line: lineNumber || 0, column: pos + 1 }, 47 | content: line 48 | }; 49 | 50 | return { nodes: [ node ], errors: [] }; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /modules/lexer/CommentHandler.ts: -------------------------------------------------------------------------------- 1 | import { Symbols } from "../req/Symbols"; 2 | 3 | /** 4 | * Command handler 5 | * 6 | * @author Thiago Delgado Pinto 7 | */ 8 | export class CommentHandler { 9 | 10 | remove( content: string ): string { 11 | // Comment is the first character after trim left 12 | if ( 0 === content.trimLeft().indexOf( Symbols.COMMENT_PREFIX ) ) { 13 | return content.substring( 0, content.indexOf( Symbols.COMMENT_PREFIX ) ); 14 | } 15 | // There is content before the comment, let's get the last index 16 | let commentPos = content.lastIndexOf( Symbols.COMMENT_PREFIX ); 17 | if ( commentPos < 0 ) { // not found 18 | return content; 19 | } 20 | // Check whether it has any terminator after it 21 | let lastValueIndex = content.lastIndexOf( Symbols.VALUE_WRAPPER ); 22 | let lastUILiteralIndex = content.lastIndexOf( Symbols.UI_LITERAL_SUFFIX ); 23 | let lastCommandIndex = content.lastIndexOf( Symbols.COMMAND_WRAPPER ); 24 | if ( ( lastValueIndex >= 0 && commentPos < lastValueIndex ) || 25 | ( lastUILiteralIndex >= 0 && commentPos < lastUILiteralIndex ) || 26 | ( lastCommandIndex >= 0 && commentPos < lastCommandIndex ) 27 | ) { 28 | return content; 29 | } 30 | return content.substring( 0, commentPos ); 31 | } 32 | 33 | removeComment( content: string, ignoreTrim: boolean = false ): string { 34 | const result = this.remove( content ); 35 | return ignoreTrim ? result : result.trim(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /modules/dbi/DatabaseInterface.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../ast/Database"; 2 | import { Table } from "../ast/Table"; 3 | import { Queryable } from "./Queryable"; 4 | 5 | /** 6 | * Database interface 7 | * 8 | * @author Thiago Delgado Pinto 9 | */ 10 | export interface DatabaseInterface extends Queryable { 11 | 12 | /** 13 | * Returns true if the given database type driver is based on a connection to a file. 14 | * 15 | * @param databaseType Database type 16 | */ 17 | hasFileBasedDriver( databaseType: string ): boolean; 18 | 19 | /** 20 | * Checks if the database is connected. 21 | */ 22 | isConnected(): Promise< boolean >; 23 | 24 | /** 25 | * Connects to the database. 26 | */ 27 | connect( db: Database, basePath?: string ): Promise< boolean >; 28 | 29 | /** 30 | * Disconnects from the database. 31 | */ 32 | disconnect(): Promise< boolean >; 33 | 34 | /** 35 | * Reconnect to the database. 36 | */ 37 | reconnect(): Promise< boolean >; 38 | 39 | /** 40 | * Executes a command. 41 | * 42 | * @param cmd Command to execute. 43 | * @param params Parameters of the command. Optional. 44 | * @return A promise to an array of values, usually objects. 45 | */ 46 | exec( cmd: string, params?: any[] ): Promise< void | any[] >; 47 | 48 | 49 | /** 50 | * Creates a database table from the given table node. 51 | * 52 | * @param table Table node. 53 | */ 54 | createTable( table: Table ): Promise< boolean >; 55 | 56 | 57 | /// @see more methods in Queryable 58 | 59 | } -------------------------------------------------------------------------------- /modules/parser/ListItemParser.ts: -------------------------------------------------------------------------------- 1 | import { ListItem } from '../ast/ListItem'; 2 | import { ConstantParser } from './ConstantParser'; 3 | import { DatabasePropertyParser } from './DatabasePropertyParser'; 4 | import { ListItemNodeParser } from './ListItemNodeParser'; 5 | import { NodeIterator } from './NodeIterator'; 6 | import { NodeParser } from './NodeParser'; 7 | import { ParsingContext } from './ParsingContext'; 8 | import { RegexParser } from './RegexParser'; 9 | import { UIPropertyParser } from './UIPropertyParser'; 10 | 11 | /** 12 | * Parses a ListItem node and decide what node type it will be. 13 | * 14 | * @author Thiago Delgado Pinto 15 | */ 16 | export class ListItemParser implements NodeParser< ListItem > { 17 | 18 | private _nodeParsers: ListItemNodeParser[] = []; 19 | 20 | constructor() { 21 | this._nodeParsers.push( new ConstantParser() ); 22 | this._nodeParsers.push( new RegexParser() ); 23 | this._nodeParsers.push( new UIPropertyParser() ); 24 | this._nodeParsers.push( new DatabasePropertyParser() ); 25 | } 26 | 27 | analyze( 28 | node: ListItem, 29 | context: ParsingContext, 30 | it: NodeIterator, 31 | errors: Error[] 32 | ): boolean { 33 | 34 | if ( ! it.hasPrior() ) { 35 | return false; // Nothing to do here 36 | } 37 | 38 | for ( let p of this._nodeParsers ) { 39 | if ( p.isAccepted( node, it ) ) { 40 | p.handle( node, context, it, errors ); 41 | } 42 | } 43 | 44 | // Stay as a ListItem 45 | return true; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /modules/parser/StepOtherwiseParser.ts: -------------------------------------------------------------------------------- 1 | import { StepOtherwise, UIProperty } from '../ast'; 2 | import { NodeTypes } from '../req/NodeTypes'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from "./ParsingContext"; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * Step Otherwise node parser. 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class StepOtherwiseParser implements NodeParser< StepOtherwise > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: StepOtherwise, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Checks prior nodes 19 | const allowedPriorNodes = [ 20 | NodeTypes.UI_PROPERTY, 21 | NodeTypes.STEP_OTHERWISE, 22 | NodeTypes.STEP_AND 23 | ]; 24 | 25 | if ( ! it.hasPrior() || allowedPriorNodes.indexOf( it.spyPrior().nodeType ) < 0 ) { 26 | let e = new SyntacticException( 27 | 'The "' + node.nodeType + '" clause must be declared after a UI Element Property.', 28 | node.location 29 | ); 30 | errors.push( e ); 31 | return false; 32 | } 33 | 34 | let prior: UIProperty = it.spyPrior() as any; 35 | 36 | // Checks the structure 37 | if ( ! prior.otherwiseSentences ) { 38 | prior.otherwiseSentences = []; 39 | } 40 | 41 | // Adds the node 42 | prior.otherwiseSentences.push( node ); 43 | 44 | return true; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /modules/parser/AfterEachScenarioParser.ts: -------------------------------------------------------------------------------- 1 | import { AfterEachScenario } from '../ast/TestEvent'; 2 | import { isDefined } from '../util/type-checking'; 3 | import { NodeIterator } from './NodeIterator'; 4 | import { NodeParser } from './NodeParser'; 5 | import { ParsingContext } from './ParsingContext'; 6 | import { SyntacticException } from './SyntacticException'; 7 | 8 | /** 9 | * AfterEachScenario parser 10 | * 11 | * @author Thiago Delgado Pinto 12 | */ 13 | export class AfterEachScenarioParser implements NodeParser< AfterEachScenario > { 14 | 15 | /** @inheritDoc */ 16 | public analyze( node: AfterEachScenario, context: ParsingContext, it: NodeIterator, errors: Error[] ): boolean { 17 | 18 | // Check whether a Feature was declared 19 | if ( ! context.doc.feature ) { 20 | let e = new SyntacticException( 21 | 'The event After Each Scenario must be declared after a Feature', node.location ); 22 | errors.push( e ); 23 | return false; 24 | } 25 | 26 | // Check whether a similar node was already declared 27 | if ( isDefined( context.doc.afterEachScenario ) ) { 28 | let e = new SyntacticException( 29 | 'Event already declared: After Each Scenario', node.location ); 30 | errors.push( e ); 31 | return false; 32 | } 33 | 34 | // Adjust the context 35 | context.resetInValues(); 36 | context.inAfterEachScenario = true; 37 | 38 | // Adjust the document 39 | context.doc.afterEachScenario = node; 40 | 41 | return true; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/semantic/DatabaseSSA.ts: -------------------------------------------------------------------------------- 1 | import Graph from 'graph.js/dist/graph.full.js'; 2 | 3 | import { DatabaseConnectionChecker } from '../db/DatabaseConnectionChecker'; 4 | import { ProblemMapper } from '../error/ProblemMapper'; 5 | import { SemanticException } from '../error/SemanticException'; 6 | import { AugmentedSpec } from '../req/AugmentedSpec'; 7 | import { SpecificationAnalyzer } from './SpecificationAnalyzer'; 8 | 9 | /** 10 | * Analyzes Databases in a specification. 11 | * 12 | * It checks for: 13 | * - duplicated names 14 | * - connection to the defined databases <<< NEEDED HERE ??? 15 | * 16 | * @author Thiago Delgado Pinto 17 | */ 18 | export class DatabaseSSA extends SpecificationAnalyzer { 19 | 20 | /** @inheritDoc */ 21 | public async analyze( 22 | problems: ProblemMapper, 23 | spec: AugmentedSpec, 24 | graph: Graph, 25 | ): Promise< boolean > { 26 | 27 | let errors: SemanticException[] = []; 28 | this._checker.checkDuplicatedNamedNodes( spec.databases(), errors, 'database' ); 29 | const ok1 = 0 === errors.length; 30 | if ( ! ok1 ) { 31 | problems.addGenericError( ...errors ); 32 | } 33 | 34 | const ok2 = await this.checkConnections( problems, spec ); 35 | 36 | return ok1 && ok2; 37 | } 38 | 39 | private async checkConnections( 40 | problems: ProblemMapper, 41 | spec: AugmentedSpec 42 | ): Promise< boolean > { 43 | let checker = new DatabaseConnectionChecker(); 44 | let r = await checker.check( spec, problems ); 45 | return r ? r.success : false; 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /modules/testdata/random/RandomShortDateTime.ts: -------------------------------------------------------------------------------- 1 | import { ChronoUnit, LocalDateTime } from "@js-joda/core"; 2 | import { ShortDateTimeLimits } from '../limits/DateTimeLimits'; 3 | import { RandomLong } from './RandomLong'; 4 | 5 | 6 | /** 7 | * Generates random short datetime values. 8 | * 9 | * @author Thiago Delgado Pinto 10 | */ 11 | export class RandomShortDateTime { 12 | 13 | constructor( private _randomLong: RandomLong ) { 14 | } 15 | 16 | /** 17 | * Returns a random short datetime between two given values, both inclusive. 18 | * 19 | * @param min Minimum date time 20 | * @param max Maximum date time 21 | */ 22 | public between( min: LocalDateTime, max: LocalDateTime ): LocalDateTime { 23 | const diffInMinutes: number = min.until( max, ChronoUnit.MINUTES ); 24 | if ( 0 === diffInMinutes ) { 25 | return min; 26 | } 27 | const minutes = this._randomLong.between( 0, diffInMinutes ); 28 | return min.plusMinutes( minutes ); 29 | } 30 | 31 | /** 32 | * Returns a random short datetime before the given date time. 33 | * 34 | * @param max Maximum date time 35 | */ 36 | public before( max: LocalDateTime ): LocalDateTime { 37 | return this.between( ShortDateTimeLimits.MIN, max.minusMinutes( 1 ) ); 38 | } 39 | 40 | /** 41 | * Returns a random short datetime after the given date time. 42 | * 43 | * @param min Minimum date time 44 | */ 45 | public after( min: LocalDateTime ): LocalDateTime { 46 | return this.between( min.plusMinutes( 1 ), ShortDateTimeLimits.MAX ); 47 | } 48 | } -------------------------------------------------------------------------------- /__tests__/nlp/SyntaxRuleBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import { Entities } from '../../modules/nlp'; 2 | import { SyntaxRule } from '../../modules/nlp/syntax/SyntaxRule'; 3 | import { SyntaxRuleBuilder } from '../../modules/nlp/syntax/SyntaxRuleBuilder'; 4 | 5 | describe( 'SyntaxRuleBuilder', () => { 6 | 7 | let builder = new SyntaxRuleBuilder(); // under test 8 | 9 | it( 'produces objects with properties of the list object and the default object', () => { 10 | const rules: Array< SyntaxRule > = [ 11 | { minTargets: 1 } 12 | ]; 13 | const defaultRule: SyntaxRule = { 14 | maxTargets: 2, 15 | targets: [ 16 | Entities.VALUE 17 | ] 18 | }; 19 | const r = builder.build( rules, defaultRule ); 20 | expect( r ).toHaveLength( 1 ); 21 | const first = r[ 0 ]; 22 | expect( first ).toHaveProperty( 'minTargets', 1 ); 23 | expect( first ).toHaveProperty( 'maxTargets', 2 ); 24 | expect( first ).toHaveProperty( 'targets' ); 25 | } ); 26 | 27 | it( 'overwrites default properties', () => { 28 | const rules: Array< SyntaxRule > = [ 29 | { minTargets: 2 } 30 | ]; 31 | const defaultRule: SyntaxRule = { 32 | minTargets: 1, 33 | maxTargets: 3, 34 | targets: [ 35 | Entities.VALUE 36 | ] 37 | }; 38 | const r = builder.build( rules, defaultRule ); 39 | expect( r ).toHaveLength( 1 ); 40 | const first = r[ 0 ]; 41 | expect( first ).toHaveProperty( 'minTargets', 2 ); 42 | } ); 43 | 44 | } ); 45 | --------------------------------------------------------------------------------