harmonyc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Router.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Routers = exports.Router = void 0;
4
+ const xmur3_1 = require("./util/xmur3");
5
+ class Router {
6
+ constructor(outs, seed = 'TODO') {
7
+ this.outs = outs;
8
+ this.index = 0;
9
+ this.started = new Set();
10
+ this.covered = new Set();
11
+ this.random = (0, xmur3_1.xmur3)(seed);
12
+ }
13
+ next() {
14
+ if (this.outs.length === 0)
15
+ throw new Error('internal error: no outs');
16
+ if (this.index < this.outs.length)
17
+ return this.outs[this.index++];
18
+ else
19
+ return this.outs[this.random() % this.outs.length];
20
+ }
21
+ get incompleteCount() {
22
+ return this.outs.length - this.index;
23
+ }
24
+ }
25
+ exports.Router = Router;
26
+ class Routers {
27
+ constructor(root) {
28
+ this.root = root;
29
+ this.routers = new Map();
30
+ this.discover(root);
31
+ }
32
+ discover(branch) {
33
+ const successors = branch.successors;
34
+ if (!this.routers.has(branch)) {
35
+ this.routers.set(branch, new Router(successors));
36
+ }
37
+ for (const s of successors)
38
+ this.discover(s);
39
+ }
40
+ get(branch) {
41
+ if (!this.routers.has(branch)) {
42
+ this.routers.set(branch, new Router(branch.successors));
43
+ }
44
+ return this.routers.get(branch);
45
+ }
46
+ nextWalk() {
47
+ const walk = [];
48
+ let current = this.root;
49
+ while (current.successors.length) {
50
+ current = this.get(current).next();
51
+ walk.push(current);
52
+ }
53
+ return walk;
54
+ }
55
+ getIncompleteCount() {
56
+ return Array.from(this.routers.values()).reduce((sum, r) => sum + r.incompleteCount, 0);
57
+ }
58
+ }
59
+ exports.Routers = Routers;
package/cli.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const compiler_1 = require("./compiler");
5
+ if (process.argv.length < 3) {
6
+ console.error("Usage: harmonyc <input files glob pattern>");
7
+ process.exit(1);
8
+ }
9
+ const args = process.argv.slice(2);
10
+ void (0, compiler_1.compileFiles)(args);
package/compile.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.compileFeature = void 0;
5
+ const indent_1 = require("./util/indent");
6
+ const model_1 = require("./model");
7
+ const parser_1 = require("./parser");
8
+ function compileFeature(name, src) {
9
+ const root = (0, parser_1.parse)(src);
10
+ const tests = (0, model_1.makeTests)(root);
11
+ const lines = [
12
+ `Feature: ${name}`,
13
+ '',
14
+ ...(0, indent_1.indent)(tests.flatMap((test) => test.toGherkin())),
15
+ ];
16
+ lines[0] = `Feature: ${name}`;
17
+ return lines.join('\n');
18
+ }
19
+ exports.compileFeature = compileFeature;
package/compiler.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.compileFile = exports.compileFiles = void 0;
16
+ const fast_glob_1 = __importDefault(require("fast-glob"));
17
+ const fs_1 = require("fs");
18
+ const path_1 = require("path");
19
+ const compile_1 = require("./compile");
20
+ function compileFiles(pattern) {
21
+ return __awaiter(this, void 0, void 0, function* () {
22
+ const fns = yield (0, fast_glob_1.default)(pattern);
23
+ if (!fns.length)
24
+ throw new Error(`No files found for pattern: ${String(pattern)}`);
25
+ yield Promise.all(fns.map((fn) => compileFile(fn)));
26
+ });
27
+ }
28
+ exports.compileFiles = compileFiles;
29
+ function compileFile(fn) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ const src = (0, fs_1.readFileSync)(fn, 'utf8').toString();
32
+ const name = (0, path_1.basename)(fn).replace(/\.[a-z]+$/i, '');
33
+ const outFn = fn.replace(/\.[a-z]+$/i, '.feature');
34
+ (0, fs_1.writeFileSync)(outFn, (0, compile_1.compileFeature)(name, src));
35
+ });
36
+ }
37
+ exports.compileFile = compileFile;
package/lexer.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lexer = exports.T = void 0;
4
+ const typescript_parsec_1 = require("typescript-parsec");
5
+ var T;
6
+ (function (T) {
7
+ T[T["Newline"] = 0] = "Newline";
8
+ T[T["EOF"] = 1] = "EOF";
9
+ T[T["Comment"] = 2] = "Comment";
10
+ T[T["Dent"] = 3] = "Dent";
11
+ T[T["Seq"] = 4] = "Seq";
12
+ T[T["Fork"] = 5] = "Fork";
13
+ T[T["State"] = 6] = "State";
14
+ T[T["Label"] = 7] = "Label";
15
+ T[T["ResponseMark"] = 8] = "ResponseMark";
16
+ T[T["Phrase"] = 9] = "Phrase";
17
+ T[T["Space"] = 10] = "Space";
18
+ })(T || (exports.T = T = {}));
19
+ exports.lexer = (0, typescript_parsec_1.buildLexer)([
20
+ [false, /^\s*\n/g, T.Newline],
21
+ [false, /^\s*$/g, T.EOF],
22
+ [false, /^#.*?(?=\n|$)/g, T.Comment],
23
+ [false, /^\/\/.*?(?=\n|$)/g, T.Comment],
24
+ [true, /^ {2}/g, T.Dent],
25
+ [true, /^- /g, T.Seq],
26
+ [true, /^\+ /g, T.Fork],
27
+ [true, /^\[\s*.*?\s*\]/g, T.State],
28
+ [true, /^[^-+\s[][^\n[]*?\s*:(?=\s*(?:\n|$))/g, T.Label],
29
+ [true, /^\s*=>\s*/g, T.ResponseMark],
30
+ [true, /^[^-+\s[][^\n[]*?(?=\s*(?:\n|$|\[|=>))/g, T.Phrase],
31
+ ]);
package/model.js ADDED
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Test = exports.makeTests = exports.Precondition = exports.Response = exports.Action = exports.Phrase = exports.Section = exports.Label = exports.State = exports.Step = exports.Branch = void 0;
4
+ const Router_1 = require("./Router");
5
+ const indent_1 = require("./util/indent");
6
+ class Branch {
7
+ constructor(children = []) {
8
+ this.isFork = false;
9
+ this.isEnd = false;
10
+ this.children = children;
11
+ children.forEach((child) => (child.parent = this));
12
+ }
13
+ setFork(isFork) {
14
+ this.isFork = isFork;
15
+ return this;
16
+ }
17
+ addChild(child, index = this.children.length) {
18
+ this.children.splice(index, 0, child);
19
+ child.parent = this;
20
+ return child;
21
+ }
22
+ get isLeaf() {
23
+ return this.children.length === 0;
24
+ }
25
+ get successors() {
26
+ if (!this.isLeaf)
27
+ return this.children.filter((c, i) => i === 0 || c.isFork);
28
+ else {
29
+ if (this.isEnd)
30
+ return [];
31
+ const next = this.nextNonForkAncestorSibling;
32
+ if (next)
33
+ return [next];
34
+ return [];
35
+ }
36
+ }
37
+ get nextNonForkAncestorSibling() {
38
+ if (!this.parent)
39
+ return undefined;
40
+ const { nextSibling } = this;
41
+ if (nextSibling && !nextSibling.isFork)
42
+ return nextSibling;
43
+ return this.parent.nextNonForkAncestorSibling;
44
+ }
45
+ get nextSibling() {
46
+ if (!this.parent)
47
+ return undefined;
48
+ return this.parent.children[this.siblingIndex + 1];
49
+ }
50
+ get siblingIndex() {
51
+ var _a, _b;
52
+ return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
53
+ }
54
+ }
55
+ exports.Branch = Branch;
56
+ class Step extends Branch {
57
+ constructor(action = '', responses = [], children, isFork = false) {
58
+ super(children);
59
+ this.action = new Action(action);
60
+ this.responses = responses.map((response) => new Response(response));
61
+ this.isFork = isFork;
62
+ }
63
+ get phrases() {
64
+ return [this.action, ...this.responses];
65
+ }
66
+ toGherkin() {
67
+ return this.phrases.flatMap((phrase) => phrase.toGherkin());
68
+ }
69
+ }
70
+ exports.Step = Step;
71
+ class State {
72
+ constructor(text = '') {
73
+ this.text = text;
74
+ }
75
+ }
76
+ exports.State = State;
77
+ class Label {
78
+ constructor(text = '') {
79
+ this.text = text;
80
+ }
81
+ }
82
+ exports.Label = Label;
83
+ class Section extends Branch {
84
+ constructor(label = '', children, isFork = false) {
85
+ super(children);
86
+ this.label = new Label(label);
87
+ this.isFork = isFork;
88
+ }
89
+ }
90
+ exports.Section = Section;
91
+ class Phrase {
92
+ constructor(text = '') {
93
+ this.text = text;
94
+ }
95
+ toGherkin() {
96
+ return [`${this.kind === 'action' ? 'When' : 'Then'} ${this.text}`];
97
+ }
98
+ }
99
+ exports.Phrase = Phrase;
100
+ class Action extends Phrase {
101
+ constructor() {
102
+ super(...arguments);
103
+ this.kind = 'action';
104
+ }
105
+ }
106
+ exports.Action = Action;
107
+ class Response extends Phrase {
108
+ constructor() {
109
+ super(...arguments);
110
+ this.kind = 'response';
111
+ }
112
+ }
113
+ exports.Response = Response;
114
+ class Precondition extends Branch {
115
+ constructor(state = '') {
116
+ super();
117
+ this.state = new State();
118
+ this.state.text = state;
119
+ }
120
+ }
121
+ exports.Precondition = Precondition;
122
+ function makeTests(root) {
123
+ const routers = new Router_1.Routers(root);
124
+ const tests = [];
125
+ let ic = routers.getIncompleteCount();
126
+ let newIc;
127
+ do {
128
+ const newTest = new Test(root, routers.nextWalk());
129
+ newIc = routers.getIncompleteCount();
130
+ if (newIc < ic)
131
+ tests.push(newTest);
132
+ ic = newIc;
133
+ } while (ic > 0);
134
+ // sort by order of appearance of the last branch
135
+ const branchIndex = new Map();
136
+ let i = 0;
137
+ function walk(branch) {
138
+ branchIndex.set(branch, i++);
139
+ for (const child of branch.children)
140
+ walk(child);
141
+ }
142
+ walk(root);
143
+ tests.sort((a, b) => branchIndex.get(a.last) - branchIndex.get(b.last));
144
+ tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
145
+ return tests;
146
+ }
147
+ exports.makeTests = makeTests;
148
+ class Test {
149
+ constructor(root, branches) {
150
+ this.root = root;
151
+ this.branches = branches;
152
+ }
153
+ get steps() {
154
+ return this.branches.filter((b) => b instanceof Step);
155
+ }
156
+ get last() {
157
+ return this.steps[this.steps.length - 1];
158
+ }
159
+ get labels() {
160
+ return this.branches
161
+ .filter((b) => b instanceof Section)
162
+ .map((s) => s.label.text);
163
+ }
164
+ toGherkin() {
165
+ return [
166
+ `Scenario: ${this.testNumber}${this.labels.length > 0 ? ' - ' : ''}${this.labels.join(' - ')}`,
167
+ ...(0, indent_1.indent)(this.steps.flatMap((b) => b.toGherkin())),
168
+ '',
169
+ ];
170
+ }
171
+ }
172
+ exports.Test = Test;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "harmonyc",
3
+ "description": "Harmony Code - Test design compiler from high-level model to Gherkin",
4
+ "version": "0.1.0",
5
+ "author": "Bernát Kalló",
6
+ "bin": {
7
+ "harmonyc": "./cli.js"
8
+ },
9
+ "homepage": "https://github.com/harmony-ac/code#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/harmony-ac/code.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/harmony-ac/code/issues"
16
+ },
17
+ "dependencies": {
18
+ "fast-glob": "^3.3.2",
19
+ "typescript-parsec": "0.3.4"
20
+ },
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "unit test", "unit testing", "cucumber", "bdd", "behavior-driven",
24
+ "test design", "test design automation", "gherkin", "cucumber",
25
+ "specflow", "harmony.ac", "test framework", "model-based test",
26
+ "model-based testing", "test model", "test modeling", "test modelling"
27
+ ]
28
+ }
package/parser.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parse = void 0;
4
+ const typescript_parsec_1 = require("typescript-parsec");
5
+ const lexer_1 = require("./lexer");
6
+ const model_1 = require("./model");
7
+ function parse(input) {
8
+ const tokens = lexer_1.lexer.parse(input);
9
+ return (0, typescript_parsec_1.expectSingleResult)((0, typescript_parsec_1.expectEOF)(TEST_DESIGN.parse(tokens)));
10
+ }
11
+ exports.parse = parse;
12
+ const PHRASE = (0, typescript_parsec_1.rule)();
13
+ PHRASE.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.tok)(lexer_1.T.Phrase), ({ text }) => text));
14
+ const STEP = (0, typescript_parsec_1.rule)();
15
+ STEP.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.seq)(PHRASE, (0, typescript_parsec_1.rep_sc)((0, typescript_parsec_1.kright)((0, typescript_parsec_1.tok)(lexer_1.T.ResponseMark), PHRASE))), ([action, responses]) => new model_1.Step(action, responses).setFork(true)));
16
+ const SECTION = (0, typescript_parsec_1.rule)();
17
+ SECTION.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.tok)(lexer_1.T.Label), ({ text }) => new model_1.Section(text.slice(0, -1))));
18
+ const BRANCH = (0, typescript_parsec_1.rule)();
19
+ BRANCH.setPattern((0, typescript_parsec_1.alt_sc)(STEP, SECTION));
20
+ const DENTS = (0, typescript_parsec_1.rule)();
21
+ DENTS.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.opt_sc)((0, typescript_parsec_1.seq)((0, typescript_parsec_1.rep_sc)((0, typescript_parsec_1.tok)(lexer_1.T.Dent)), (0, typescript_parsec_1.alt_sc)((0, typescript_parsec_1.tok)(lexer_1.T.Seq), (0, typescript_parsec_1.tok)(lexer_1.T.Fork)))), (lineHead) => {
22
+ if (!lineHead)
23
+ return { dent: 0, isFork: true };
24
+ const [dents, seqOrFork] = lineHead;
25
+ return { dent: dents.length + 1, isFork: seqOrFork.kind === lexer_1.T.Fork };
26
+ }));
27
+ const LINE = (0, typescript_parsec_1.rule)();
28
+ LINE.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.seq)(DENTS, BRANCH), ([{ dent, isFork }, branch], [start, end]) => ({
29
+ dent,
30
+ branch: branch.setFork(isFork),
31
+ })));
32
+ const TEST_DESIGN = (0, typescript_parsec_1.rule)();
33
+ TEST_DESIGN.setPattern((0, typescript_parsec_1.apply)((0, typescript_parsec_1.rep_sc)(LINE), (lines) => {
34
+ const startDent = 0;
35
+ let dent = startDent;
36
+ const root = new model_1.Section();
37
+ let parent = root;
38
+ let lineNo = 0;
39
+ for (const { dent: d, branch } of lines) {
40
+ ++lineNo;
41
+ if (d > dent + 1) {
42
+ throw new Error(`invalid indent ${d} at line ${lineNo}`);
43
+ }
44
+ else if (d === dent + 1) {
45
+ parent = parent.children[parent.children.length - 1];
46
+ ++dent;
47
+ }
48
+ else if (d < startDent) {
49
+ throw new Error(`invalid indent at line ${lineNo}`);
50
+ }
51
+ else
52
+ while (d < dent) {
53
+ parent = parent.parent;
54
+ --dent;
55
+ }
56
+ parent.addChild(branch);
57
+ }
58
+ return root;
59
+ }));
60
+ function inputText(start, end) {
61
+ let text = '';
62
+ let t = start;
63
+ while (t && t !== end) {
64
+ text += t.text;
65
+ t = t.next;
66
+ }
67
+ return text;
68
+ }
package/util/indent.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.indent = void 0;
4
+ function indent(lines) {
5
+ return lines.map((line) => ` ${line}`);
6
+ }
7
+ exports.indent = indent;
package/util/xmur3.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.xmur3 = void 0;
4
+ // https://stackoverflow.com/a/47593316
5
+ function xmur3(str) {
6
+ let h = 1779033703 ^ str.length;
7
+ for (let i = 0; i < str.length; i++)
8
+ (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)),
9
+ (h = (h << 13) | (h >>> 19));
10
+ return function () {
11
+ h = Math.imul(h ^ (h >>> 16), 2246822507);
12
+ h = Math.imul(h ^ (h >>> 13), 3266489909);
13
+ return (h ^= h >>> 16) >>> 0;
14
+ };
15
+ }
16
+ exports.xmur3 = xmur3;