harmonyc 0.2.0 → 0.4.1

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/README.md CHANGED
@@ -1,85 +1,62 @@
1
- # Harmony Code
2
-
3
- A simple model-based test design tool that generates Cucumber feature files.
4
-
5
- ## Setup
6
-
7
- You need to have Node.js installed. Then you can install Harmony Code in your project folder by:
8
-
9
- ```bash
10
- npm install harmonyc
11
- ```
12
-
13
- And then run it for all `.harmony` files in your `src` folder:
14
-
15
- ```bash
16
- harmonyc src/**/*.harmony
17
- ```
18
-
19
- This will generate `.feature` files next to the `.harmony` files.
20
-
21
- ## Syntax
22
-
23
- A `.harmony` file is a text file with a syntax that looks like this:
24
-
25
- ```
26
- Products API:
27
- + Create:
28
- + Anonymous:
29
- - create product => error: unauthorized
30
- + Admin:
31
- - authenticate admin
32
- - create product => product created
33
- - Delete:
34
- - delete product => product deleted
35
- ```
36
-
37
- Compiling this with `harmonyc` will generate this `.feature` file:
38
-
39
- ```gherkin
40
- Feature: products
41
-
42
- Scenario: T1 - Products API - Create - Anonymous
43
- When create product -- products
44
- Then error: unauthorized -- products
45
-
46
- Scenario: T2 - Products API - Create - Admin - Delete
47
- When authenticate admin -- products
48
- When create product -- products
49
- Then product created -- products
50
- When delete product -- products
51
- Then product deleted -- products
52
- ```
53
-
54
- ### Indentation
55
-
56
- The lines of a file are nodes of a tree. The tree is specified with the indentation of the lines, which is n times 2 spaces and a `+` or `-` with one more space. The `+` or `-` sign is considered to be part of the indentation.
57
-
58
- ### Sequences and forks
59
-
60
- `-` means a sequence: the node follows the previous sibling node and its descendants.
61
-
62
- `+` means a fork: the node directly follows its parent node. All siblings with `+` are separate branches, they will generate separate scenarios.
63
-
64
- ### Labels
65
-
66
- Label are nodes that end with `:`. You can use them to structure your test design.
67
- They are not included in the test case, but the test case name is generated from the labels.
68
-
69
- ### Comments
70
-
71
- Lines starting with `#` or `//` are comments and are ignored.
72
-
73
- ### Steps
74
-
75
- All other lines are steps. A step consists of an action, and one or more responses denoted by `=>`.
76
- Actions will become `When` steps, and responses will become `Then` steps.
77
-
78
-
79
- ## Running the tests
80
-
81
- Currenlty `harmonyc` only compiles `.harmony` files to `.feature` files. To run the tests, you need to set up Cucumber or Specflow, depending on your environment.
82
-
83
- ## License
84
-
85
- MIT
1
+ # Harmony Code
2
+
3
+ A test design & BDD tool that helps you separate the _what_ to test from the _how_ to automate it. You write test cases in a simple Markdown format, and then automate them with Vitest (and soon with many more frameworks and languages).
4
+
5
+ (**Note**: There have been big changes since v0.2.)
6
+
7
+ ## Usage
8
+
9
+ 1. Have Node.js installed.
10
+ 2. You can compile your `*.md` files in the `src` folder by running
11
+
12
+ ```bash script
13
+ npx harmonyc src/**/*.md
14
+ ```
15
+
16
+ 3. Then you can run the generated tests with Vitest by running
17
+
18
+ ```bash script
19
+ npx vitest
20
+ ```
21
+
22
+ ## Syntax
23
+
24
+ A Harmony Code file is a Markdown file with a syntax that looks like this:
25
+
26
+ ```markdown
27
+ # Products API
28
+
29
+ - **Create**:
30
+ - **Anonymous:**
31
+ - create product => error: unauthorized
32
+ - **Admin**:
33
+ 1. authenticate admin
34
+ 2. create product => product created
35
+ 3. **Delete:**
36
+ - delete product => product deleted
37
+ ```
38
+
39
+ ### Actions and responses
40
+
41
+ List items (either in ordered or bulleted lists) consist of an **action** and zero or more **response**s, separated by a `=>`.
42
+
43
+ The generated steps contain the feature name after a `||`. Cucumber's steps are in one global namespace, but including the feature name makes it easy to scope step defintions by feature.
44
+
45
+ ### Sequences and forks
46
+
47
+ An ordered list means a **sequence**: the list items are included int the tests in order.
48
+
49
+ A bulleted list (`-` or `*` or `+`) means a **fork**: the node directly follows its parent node. All list items are separate branches, they will generate separate scenarios.
50
+
51
+ ### Labels
52
+
53
+ Label are nodes that end with `:`. You can use them to structure your test design.
54
+ They are not included in the test case, but the test case name is generated from the labels.
55
+
56
+ ### Text
57
+
58
+ Paragraphs outside of lists are for humans only, they are ignored in the automated tests.
59
+
60
+ ## License
61
+
62
+ MIT
package/Router.js CHANGED
@@ -1,14 +1,11 @@
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 {
1
+ import { xmur3 } from './util/xmur3.js';
2
+ export class Router {
6
3
  constructor(outs, seed = 'TODO') {
7
4
  this.outs = outs;
8
5
  this.index = 0;
9
6
  this.started = new Set();
10
7
  this.covered = new Set();
11
- this.random = (0, xmur3_1.xmur3)(seed);
8
+ this.random = xmur3(seed);
12
9
  }
13
10
  next() {
14
11
  if (this.outs.length === 0)
@@ -22,8 +19,7 @@ class Router {
22
19
  return this.outs.length - this.index;
23
20
  }
24
21
  }
25
- exports.Router = Router;
26
- class Routers {
22
+ export class Routers {
27
23
  constructor(root) {
28
24
  this.root = root;
29
25
  this.routers = new Map();
@@ -56,4 +52,3 @@ class Routers {
56
52
  return Array.from(this.routers.values()).reduce((sum, r) => sum + r.incompleteCount, 0);
57
53
  }
58
54
  }
59
- exports.Routers = Routers;
package/cli.js CHANGED
@@ -1,10 +1,21 @@
1
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 || process.argv.includes('--help')) {
6
- console.error("Usage: harmonyc <input files glob pattern>");
2
+ import { compileFiles } from './compiler.js';
3
+ import { parseArgs } from 'node:util';
4
+ import { watchFiles } from './watch.js';
5
+ const args = parseArgs({
6
+ options: {
7
+ help: { type: 'boolean' },
8
+ watch: { type: 'boolean' },
9
+ },
10
+ allowPositionals: true,
11
+ });
12
+ if (args.positionals.length === 0 || args.values.help) {
13
+ console.error('Usage: harmonyc <input files glob pattern>');
7
14
  process.exit(1);
8
15
  }
9
- const args = process.argv.slice(2);
10
- void (0, compiler_1.compileFiles)(args);
16
+ if (args.values.watch) {
17
+ void watchFiles(args.positionals);
18
+ }
19
+ else {
20
+ void compileFiles(args.positionals);
21
+ }
package/compile.js CHANGED
@@ -1,19 +1,10 @@
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).setFeatureName(name);
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');
1
+ import { NodeTest } from './languages/JavaScript.js';
2
+ import { OutFile } from './outFile.js';
3
+ import { parse } from './syntax.js';
4
+ export function compileFeature(fileName, src) {
5
+ const feature = parse({ fileName, src });
6
+ const of = new OutFile();
7
+ const cg = new NodeTest(of);
8
+ feature.toCode(cg);
9
+ return of.value;
18
10
  }
19
- exports.compileFeature = compileFeature;
package/compiler.js CHANGED
@@ -1,37 +1,18 @@
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
- });
1
+ import glob from 'fast-glob';
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+ import { basename } from 'path';
4
+ import { compileFeature } from './compile.js';
5
+ export async function compileFiles(pattern) {
6
+ const fns = await glob(pattern);
7
+ if (!fns.length)
8
+ throw new Error(`No files found for pattern: ${String(pattern)}`);
9
+ await Promise.all(fns.map((fn) => compileFile(fn)));
10
+ console.log(`Compiled ${fns.length} file${fns.length === 1 ? '' : 's'}.`);
11
+ return fns;
27
12
  }
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
- });
13
+ export async function compileFile(fn) {
14
+ const src = readFileSync(fn, 'utf8').toString();
15
+ const name = basename(fn).replace(/\.[a-z]+$/i, '');
16
+ const outFn = `${fn.replace(/\.[a-z]+$/i, '')}.mjs`;
17
+ writeFileSync(outFn, compileFeature(fn, src));
36
18
  }
37
- exports.compileFile = compileFile;
package/config.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/definitions.js ADDED
@@ -0,0 +1,22 @@
1
+ import { CucumberExpression, ParameterTypeRegistry, } from '@cucumber/cucumber-expressions';
2
+ const registry = new ParameterTypeRegistry();
3
+ export function definitions({ marker, code, feature, }) {
4
+ var _a, _b;
5
+ const re = new RegExp(`^\s*${q(marker)}(.*?)$`, 'gm');
6
+ let match = re.exec(code);
7
+ const start = (_a = match === null || match === void 0 ? void 0 : match.index) !== null && _a !== void 0 ? _a : code.length;
8
+ feature.prelude += code.slice(0, start);
9
+ while (match) {
10
+ const bodyStart = match.index + match[0].length;
11
+ const head = match[1].trim();
12
+ match = re.exec(code);
13
+ const end = (_b = match === null || match === void 0 ? void 0 : match.index) !== null && _b !== void 0 ? _b : code.length;
14
+ const body = code.slice(bodyStart, end).trim();
15
+ if (body) {
16
+ feature.definitions.set(new CucumberExpression(head, registry), body);
17
+ }
18
+ }
19
+ }
20
+ function q(pattern) {
21
+ return pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
22
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Gherkin = void 0;
4
+ class Gherkin {
5
+ constructor(outFile) {
6
+ this.outFile = outFile;
7
+ }
8
+ feature(name, tests) {
9
+ this.outFile.print(`Feature: ${name}`);
10
+ this.outFile.print('');
11
+ this.outFile.indent(() => {
12
+ for (const test of tests) {
13
+ test.toCode(this);
14
+ }
15
+ });
16
+ }
17
+ test(t) {
18
+ this.outFile.print(`Scenario: ${t.name}`);
19
+ this.outFile.indent(() => {
20
+ for (const step of t.steps) {
21
+ step.toCode(this);
22
+ }
23
+ });
24
+ this.outFile.print('');
25
+ }
26
+ phrase(p) {
27
+ this.outFile.print(`${p.keyword} ${p.text} || ${p.feature}`);
28
+ if (p.docstring !== undefined) {
29
+ this.outFile.indent(() => {
30
+ this.outFile.print(`"""`);
31
+ this.outFile.print(...p.docstring.split('\n'));
32
+ this.outFile.print(`"""`);
33
+ });
34
+ }
35
+ }
36
+ }
37
+ exports.Gherkin = Gherkin;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeTest = void 0;
4
+ class NodeTest {
5
+ constructor(outFile) {
6
+ this.outFile = outFile;
7
+ this.framework = 'vitest';
8
+ }
9
+ feature(feature) {
10
+ if (this.framework === 'vitest') {
11
+ this.outFile.print(`import { test, expect } from 'vitest';`);
12
+ }
13
+ this.outFile.print(feature.prelude);
14
+ for (const test of feature.tests) {
15
+ test.toCode(this);
16
+ }
17
+ }
18
+ test(t) {
19
+ this.outFile.print(`test('${t.name}', async () => {`);
20
+ this.outFile.indent(() => {
21
+ for (const step of t.steps) {
22
+ step.toCode(this);
23
+ }
24
+ });
25
+ this.outFile.print('})');
26
+ this.outFile.print('');
27
+ }
28
+ phrase(p) {
29
+ var _a;
30
+ this.outFile.loc(p).print('/// ' + p.text);
31
+ const code = (_a = p.definition()) !== null && _a !== void 0 ? _a : `throw 'Not defined: ' + ${JSON.stringify(p.text)};`;
32
+ this.outFile.print(...code.split('\n'));
33
+ }
34
+ }
35
+ exports.NodeTest = NodeTest;
@@ -0,0 +1,33 @@
1
+ export class Gherkin {
2
+ constructor(outFile) {
3
+ this.outFile = outFile;
4
+ }
5
+ feature(feature) {
6
+ this.outFile.print(`Feature: ${feature.name}`);
7
+ this.outFile.print('');
8
+ this.outFile.indent(() => {
9
+ for (const test of feature.tests) {
10
+ test.toCode(this);
11
+ }
12
+ });
13
+ }
14
+ test(t) {
15
+ this.outFile.print(`Scenario: ${t.name}`);
16
+ this.outFile.indent(() => {
17
+ for (const step of t.steps) {
18
+ step.toCode(this);
19
+ }
20
+ });
21
+ this.outFile.print('');
22
+ }
23
+ phrase(p) {
24
+ this.outFile.print(`${p.keyword} ${p.text} || ${p.feature}`);
25
+ if (p.docstring !== undefined) {
26
+ this.outFile.indent(() => {
27
+ this.outFile.print(`"""`);
28
+ this.outFile.print(...p.docstring.split('\n'));
29
+ this.outFile.print(`"""`);
30
+ });
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,31 @@
1
+ export class NodeTest {
2
+ constructor(outFile) {
3
+ this.outFile = outFile;
4
+ this.framework = 'vitest';
5
+ }
6
+ feature(feature) {
7
+ if (this.framework === 'vitest') {
8
+ this.outFile.print(`import { test, expect } from 'vitest';`);
9
+ }
10
+ this.outFile.print(feature.prelude);
11
+ for (const test of feature.tests) {
12
+ test.toCode(this);
13
+ }
14
+ }
15
+ test(t) {
16
+ this.outFile.print(`test('${t.name}', async () => {`);
17
+ this.outFile.indent(() => {
18
+ for (const step of t.steps) {
19
+ step.toCode(this);
20
+ }
21
+ });
22
+ this.outFile.print('})');
23
+ this.outFile.print('');
24
+ }
25
+ phrase(p) {
26
+ var _a;
27
+ this.outFile.loc(p).print('/// ' + p.text);
28
+ const code = (_a = p.definition()) !== null && _a !== void 0 ? _a : `throw 'Not defined: ' + ${JSON.stringify(p.text)};`;
29
+ this.outFile.print(...code.split('\n'));
30
+ }
31
+ }
package/model.js CHANGED
@@ -1,9 +1,19 @@
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 {
1
+ import { Routers } from './Router.js';
2
+ export class Feature {
3
+ constructor(name) {
4
+ this.name = name;
5
+ this.root = new Section();
6
+ this.definitions = new Map();
7
+ this.prelude = '';
8
+ }
9
+ get tests() {
10
+ return makeTests(this.root);
11
+ }
12
+ toCode(cg) {
13
+ cg.feature(this);
14
+ }
15
+ }
16
+ export class Branch {
7
17
  constructor(children = []) {
8
18
  this.isFork = false;
9
19
  this.isEnd = false;
@@ -14,9 +24,9 @@ class Branch {
14
24
  this.isFork = isFork;
15
25
  return this;
16
26
  }
17
- setFeatureName(featureName) {
27
+ setFeature(feature) {
18
28
  for (const child of this.children)
19
- child.setFeatureName(featureName);
29
+ child.setFeature(feature);
20
30
  return this;
21
31
  }
22
32
  addChild(child, index = this.children.length) {
@@ -57,85 +67,107 @@ class Branch {
57
67
  return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
58
68
  }
59
69
  }
60
- exports.Branch = Branch;
61
- class Step extends Branch {
62
- constructor(action = '', responses = [], children, isFork = false, featureName = '') {
70
+ export class Step extends Branch {
71
+ constructor(action = '', responses = [], children, isFork = false) {
63
72
  super(children);
64
- this.action = new Action(action, featureName);
65
- this.responses = responses.map((response) => new Response(response, featureName));
73
+ this.action = new Action(action);
74
+ this.responses = responses.map((response) => new Response(response));
66
75
  this.isFork = isFork;
67
76
  }
68
77
  get phrases() {
69
78
  return [this.action, ...this.responses];
70
79
  }
71
- toGherkin() {
72
- return this.phrases.flatMap((phrase) => phrase.toGherkin());
80
+ toCode(cg) {
81
+ for (const phrase of this.phrases) {
82
+ phrase.toCode(cg);
83
+ }
73
84
  }
74
- setFeatureName(featureName) {
75
- this.action.setFeatureName(featureName);
85
+ setFeature(feature) {
86
+ this.action.setFeature(feature);
76
87
  for (const response of this.responses)
77
- response.setFeatureName(featureName);
78
- return super.setFeatureName(featureName);
88
+ response.setFeature(feature);
89
+ return super.setFeature(feature);
79
90
  }
80
91
  }
81
- exports.Step = Step;
82
- class State {
92
+ export class State {
83
93
  constructor(text = '') {
84
94
  this.text = text;
85
95
  }
86
96
  }
87
- exports.State = State;
88
- class Label {
97
+ export class Label {
89
98
  constructor(text = '') {
90
99
  this.text = text;
91
100
  }
92
101
  }
93
- exports.Label = Label;
94
- class Section extends Branch {
102
+ export class Section extends Branch {
95
103
  constructor(label = '', children, isFork = false) {
96
104
  super(children);
97
105
  this.label = new Label(label);
98
106
  this.isFork = isFork;
99
107
  }
100
108
  }
101
- exports.Section = Section;
102
- class Phrase {
103
- constructor(text = '', featureName = '') {
109
+ export class Phrase {
110
+ constructor(text = '') {
104
111
  this.text = text;
105
- this.featureName = featureName;
106
- }
107
- setFeatureName(featureName) {
108
- this.featureName = featureName;
109
112
  }
110
- toGherkin() {
111
- return [`${this.kind === 'action' ? 'When' : 'Then'} ${this.text} -- ${this.featureName}`];
113
+ setFeature(feature) {
114
+ this.feature = feature;
115
+ }
116
+ get keyword() {
117
+ return this.kind === 'action' ? 'When' : 'Then';
118
+ }
119
+ toCode(cg) {
120
+ return cg.phrase(this);
121
+ }
122
+ definition() {
123
+ const key = this.kind === 'action' ? this.text : `=> ${this.text}`;
124
+ let args;
125
+ let code;
126
+ for (const [ce, c] of this.feature.definitions.entries()) {
127
+ const m = ce.match(key);
128
+ if (!m)
129
+ continue;
130
+ if (args !== undefined)
131
+ throw new Error(`Ambiguous definition: ${key}`);
132
+ args = m;
133
+ code = c;
134
+ }
135
+ if (args === undefined)
136
+ return undefined;
137
+ return code.replace(/\$([$_1-9])/g, (s, varName) => {
138
+ var _a;
139
+ if (varName.match(/[1-9]/))
140
+ return JSON.stringify(args[parseInt(varName) - 1].getValue(undefined));
141
+ else if (varName === '_')
142
+ return JSON.stringify((_a = this.docstring) !== null && _a !== void 0 ? _a : '');
143
+ else if (varName === '$')
144
+ return '$';
145
+ else
146
+ return s;
147
+ });
112
148
  }
113
149
  }
114
- exports.Phrase = Phrase;
115
- class Action extends Phrase {
150
+ export class Action extends Phrase {
116
151
  constructor() {
117
152
  super(...arguments);
118
153
  this.kind = 'action';
119
154
  }
120
155
  }
121
- exports.Action = Action;
122
- class Response extends Phrase {
156
+ export class Response extends Phrase {
123
157
  constructor() {
124
158
  super(...arguments);
125
159
  this.kind = 'response';
126
160
  }
127
161
  }
128
- exports.Response = Response;
129
- class Precondition extends Branch {
162
+ export class Precondition extends Branch {
130
163
  constructor(state = '') {
131
164
  super();
132
165
  this.state = new State();
133
166
  this.state.text = state;
134
167
  }
135
168
  }
136
- exports.Precondition = Precondition;
137
- function makeTests(root) {
138
- const routers = new Router_1.Routers(root);
169
+ export function makeTests(root) {
170
+ const routers = new Routers(root);
139
171
  const tests = [];
140
172
  let ic = routers.getIncompleteCount();
141
173
  let newIc;
@@ -159,8 +191,7 @@ function makeTests(root) {
159
191
  tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
160
192
  return tests;
161
193
  }
162
- exports.makeTests = makeTests;
163
- class Test {
194
+ export class Test {
164
195
  constructor(root, branches) {
165
196
  this.root = root;
166
197
  this.branches = branches;
@@ -176,12 +207,10 @@ class Test {
176
207
  .filter((b) => b instanceof Section)
177
208
  .map((s) => s.label.text);
178
209
  }
179
- toGherkin() {
180
- return [
181
- `Scenario: ${this.testNumber}${this.labels.length > 0 ? ' - ' : ''}${this.labels.join(' - ')}`,
182
- ...(0, indent_1.indent)(this.steps.flatMap((b) => b.toGherkin())),
183
- '',
184
- ];
210
+ get name() {
211
+ return `${this.testNumber}${this.labels.length > 0 ? ' - ' : ''}${this.labels.join(' - ')}`;
212
+ }
213
+ toCode(cg) {
214
+ cg.test(this);
185
215
  }
186
216
  }
187
- exports.Test = Test;
package/outFile.js ADDED
@@ -0,0 +1,45 @@
1
+ import { SourceMapGenerator } from 'source-map-js';
2
+ export class OutFile {
3
+ constructor(indentSpaces = 2) {
4
+ this.indentSpaces = indentSpaces;
5
+ this.lines = [];
6
+ this.level = 0;
7
+ this.sm = new SourceMapGenerator();
8
+ }
9
+ indent(fn) {
10
+ this.level++;
11
+ try {
12
+ fn();
13
+ }
14
+ finally {
15
+ this.level--;
16
+ }
17
+ }
18
+ print(...lines) {
19
+ const l = this.lines.length;
20
+ this.lines.push(...lines.map((line) => ' '.repeat(this.level * this.indentSpaces) + line));
21
+ if (this.currentLoc)
22
+ for (const i of lines.keys()) {
23
+ this.sm.addMapping({
24
+ source: this.currentLoc.fileName,
25
+ original: {
26
+ line: this.currentLoc.line,
27
+ column: this.currentLoc.column,
28
+ },
29
+ generated: {
30
+ line: l + i,
31
+ column: this.level * this.indentSpaces,
32
+ },
33
+ });
34
+ }
35
+ return this;
36
+ }
37
+ loc({ location }) {
38
+ this.currentLoc = location;
39
+ return this;
40
+ }
41
+ get value() {
42
+ return (this.lines.join('\n') +
43
+ `\n\n//# sourceMappingURL=data:application/json,${encodeURIComponent(this.sm.toString())}`);
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,41 +1,39 @@
1
- {
2
- "name": "harmonyc",
3
- "description": "Harmony Code - Test design compiler from high-level model to Gherkin",
4
- "version": "0.2.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",
24
- "unit testing",
25
- "cucumber",
26
- "bdd",
27
- "behavior-driven",
28
- "test design",
29
- "test design automation",
30
- "gherkin",
31
- "cucumber",
32
- "specflow",
33
- "harmony.ac",
34
- "test framework",
35
- "model-based test",
36
- "model-based testing",
37
- "test model",
38
- "test modeling",
39
- "test modelling"
40
- ]
41
- }
1
+ {
2
+ "name": "harmonyc",
3
+ "description": "Harmony Code - model-driven BDD for Vitest",
4
+ "version": "0.4.1",
5
+ "author": "Bernát Kalló",
6
+ "type": "module",
7
+ "bin": {
8
+ "harmonyc": "./cli.js"
9
+ },
10
+ "homepage": "https://github.com/harmony-ac/code#readme",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/harmony-ac/code.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/harmony-ac/code/issues"
17
+ },
18
+ "dependencies": {
19
+ "fast-glob": "^3.3.2",
20
+ "typescript-parsec": "0.3.4"
21
+ },
22
+ "license": "MIT",
23
+ "keywords": [
24
+ "unit test",
25
+ "unit testing",
26
+ "bdd",
27
+ "behavior-driven",
28
+ "test design",
29
+ "test design automation",
30
+ "harmony.ac",
31
+ "test framework",
32
+ "model-based test",
33
+ "model-based testing",
34
+ "test model",
35
+ "test modeling",
36
+ "test modelling",
37
+ "vitest"
38
+ ]
39
+ }
package/syntax.js ADDED
@@ -0,0 +1,114 @@
1
+ import remarkParse from 'remark-parse';
2
+ import { unified } from 'unified';
3
+ import { Feature, Section, Step } from './model.js';
4
+ import { definitions } from './definitions.js';
5
+ export function parse({ fileName, src, }) {
6
+ const tree = unified().use(remarkParse).parse(src);
7
+ const rootNodes = tree.children;
8
+ const feature = new Feature('');
9
+ const headings = [feature.root];
10
+ let name;
11
+ for (let i = 0; i < rootNodes.length; i++) {
12
+ const node = rootNodes[i];
13
+ if (node.type === 'heading') {
14
+ if (node.depth === 1 && name === undefined) {
15
+ name = textContent(node);
16
+ }
17
+ const level = node.depth;
18
+ const section = new Section(textContent(node), [], true);
19
+ setLocation(section, node, fileName);
20
+ const last = headings.slice(0, level).reverse().find(Boolean);
21
+ last.addChild(section);
22
+ headings[level] = section;
23
+ headings.length = level + 1;
24
+ }
25
+ else {
26
+ const last = headings[headings.length - 1];
27
+ for (const branch of topLevel(node))
28
+ last.addChild(branch);
29
+ }
30
+ }
31
+ feature.name = name !== null && name !== void 0 ? name : fileName;
32
+ feature.root.setFeature(feature);
33
+ return feature;
34
+ function topLevel(node) {
35
+ if (node.type === 'paragraph')
36
+ return [];
37
+ if (node.type == 'list')
38
+ return list(node);
39
+ if (node.type === 'code')
40
+ return code(node);
41
+ return [];
42
+ }
43
+ function list(node) {
44
+ const isFork = node.ordered === false;
45
+ return node.children.map((item) => listItem(item, isFork));
46
+ }
47
+ function code(node) {
48
+ var _a;
49
+ if (!((_a = node.meta) === null || _a === void 0 ? void 0 : _a.match(/harmony/)))
50
+ return [];
51
+ const code = node.value;
52
+ const marker = '///';
53
+ definitions({ marker, code, feature });
54
+ return [];
55
+ }
56
+ function listItem(node, isFork) {
57
+ const first = node.children[0];
58
+ const text = textContent(first);
59
+ let branch;
60
+ if (first.type === 'heading') {
61
+ branch = new Section(text, [], isFork);
62
+ }
63
+ else {
64
+ const [action, ...responses] = text.split(/(?:^| )=>(?: |$)/);
65
+ const step = (branch = new Step(action, responses.filter(Boolean), [], isFork));
66
+ setLocation(step.action, first, fileName);
67
+ for (const response of step.responses) {
68
+ // todo separate locations along =>'s
69
+ setLocation(response, first, fileName);
70
+ }
71
+ let i = 0;
72
+ for (const child of node.children.slice(1)) {
73
+ if (child.type === 'list')
74
+ break;
75
+ if (child.type === 'code') {
76
+ if (step.phrases[i])
77
+ step.phrases[i].docstring = child.value;
78
+ else
79
+ break;
80
+ ++i;
81
+ }
82
+ }
83
+ }
84
+ for (const child of node.children) {
85
+ if (child.type === 'list') {
86
+ for (const b of list(child))
87
+ branch.addChild(b);
88
+ }
89
+ }
90
+ setLocation(branch, node, fileName);
91
+ return branch;
92
+ }
93
+ }
94
+ function textContent(node) {
95
+ if (!node)
96
+ return '';
97
+ if (node.type === 'text') {
98
+ return node.value.split(/\s+/).filter(Boolean).join(' ');
99
+ }
100
+ if (node.type === 'list')
101
+ return '';
102
+ if (!('children' in node))
103
+ return '';
104
+ return node.children.map(textContent).filter(Boolean).join(' ');
105
+ }
106
+ function setLocation(model, node, fileName) {
107
+ if (!node.position)
108
+ return;
109
+ model.location = {
110
+ line: node.position.start.line,
111
+ column: node.position.start.column,
112
+ fileName,
113
+ };
114
+ }
package/util/indent.js CHANGED
@@ -1,7 +1,5 @@
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;
1
+ export const Indent = (n) => function* indent(lines) {
2
+ for (const line of lines) {
3
+ yield ' '.repeat(n) + line;
4
+ }
5
+ };
@@ -0,0 +1,5 @@
1
+ export function* flatMap(iterable, callback) {
2
+ for (const value of iterable) {
3
+ yield* callback(value);
4
+ }
5
+ }
package/util/xmur3.js CHANGED
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.xmur3 = void 0;
4
1
  // https://stackoverflow.com/revisions/47593316/25 by bryc (github.com/bryc)
5
- function xmur3(str) {
2
+ export function xmur3(str) {
6
3
  let h = 1779033703 ^ str.length;
7
4
  for (let i = 0; i < str.length; i++)
8
5
  (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)),
@@ -13,4 +10,3 @@ function xmur3(str) {
13
10
  return (h ^= h >>> 16) >>> 0;
14
11
  };
15
12
  }
16
- exports.xmur3 = xmur3;
package/watch.js ADDED
@@ -0,0 +1,10 @@
1
+ import { watch } from 'node:fs';
2
+ import { compileFile, compileFiles } from './compiler.js';
3
+ export async function watchFiles(patterns) {
4
+ const fns = await compileFiles(patterns);
5
+ for (const file of fns) {
6
+ watch(file, () => {
7
+ compileFile(file);
8
+ });
9
+ }
10
+ }
package/lexer.js DELETED
@@ -1,31 +0,0 @@
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/parser.js DELETED
@@ -1,68 +0,0 @@
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
- }