├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------