harmonyc 0.1.1 → 0.4.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/README.md CHANGED
@@ -1,84 +1,61 @@
1
1
  # Harmony Code
2
2
 
3
- A simple model-based test design tool that generates Cucumber feature files.
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
4
 
5
- ## Setup
5
+ (**Note**: There have been big changes since v0.2.)
6
6
 
7
- You need to have Node.js installed. Then you can install Harmony Code in your project folder by:
7
+ ## Usage
8
8
 
9
- ```bash
10
- npm install harmonyc
11
- ```
9
+ 1. Have Node.js installed.
10
+ 2. You can compile your `*.md` files in the `src` folder by running
12
11
 
13
- And then run it for all `.harmony` files in your `src` folder:
12
+ ```bash script
13
+ npx harmonyc src/**/*.md
14
+ ```
14
15
 
15
- ```bash
16
- harmonyc src/**/*.harmony
17
- ```
16
+ 3. Then you can run the generated tests with Vitest by running
18
17
 
19
- This will generate `.feature` files next to the `.harmony` files.
18
+ ```bash script
19
+ npx vitest
20
+ ```
20
21
 
21
22
  ## Syntax
22
23
 
23
- A `.harmony` file is a text file with a syntax that looks like this:
24
+ A Harmony Code file is a Markdown file with a syntax that looks like this:
24
25
 
25
- ```
26
- Products API:
27
- + Create:
28
- + Anonymous:
26
+ ```markdown
27
+ # Products API
28
+
29
+ - **Create**:
30
+ - **Anonymous:**
29
31
  - create product => error: unauthorized
30
- + Admin:
31
- - authenticate admin
32
- - create product => product created
33
- - Delete:
34
- - delete product => product deleted
32
+ - **Admin**:
33
+ 1. authenticate admin
34
+ 2. create product => product created
35
+ 3. **Delete:**
36
+ - delete product => product deleted
35
37
  ```
36
38
 
37
- Compiling this with `harmonyc` will generate this `.feature` file:
38
-
39
- ```gherkin
40
- Feature: change_password
41
-
42
- Scenario: T1 - Products API - Create - Anonymous
43
- When create product
44
- Then error: unauthorized
45
-
46
- Scenario: T2 - Products API - Create - Admin - Delete
47
- When authenticate admin
48
- When create product
49
- Then product created
50
- When delete product
51
- Then product deleted
52
- ```
39
+ ### Actions and responses
53
40
 
54
- ### Indentation
41
+ List items (either in ordered or bulleted lists) consist of an **action** and zero or more **response**s, separated by a `=>`.
55
42
 
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.
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.
57
44
 
58
45
  ### Sequences and forks
59
46
 
60
- `-` means a sequence: the node follows the previous sibling node and its descendants.
47
+ An ordered list means a **sequence**: the list items are included int the tests in order.
61
48
 
62
- `+` means a fork: the node directly follows its parent node. All siblings with `+` are separate branches, they will generate separate scenarios.
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.
63
50
 
64
51
  ### Labels
65
52
 
66
53
  Label are nodes that end with `:`. You can use them to structure your test design.
67
54
  They are not included in the test case, but the test case name is generated from the labels.
68
55
 
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
56
+ ### Text
80
57
 
81
- `harmonyc` only compiles `.harmony` files to `.feature` files. To run the tests, you need to set up Cucumber or Specflow, depending on your environment.
58
+ Paragraphs outside of lists are for humans only, they are ignored in the automated tests.
82
59
 
83
60
  ## License
84
61
 
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';
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,18 @@
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) {
6
- console.error("Usage: harmonyc <input files glob pattern>");
2
+ import { compileFiles } from './compiler';
3
+ import { parseArgs } from 'node:util';
4
+ import { watchFiles } from './watch';
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
+ watchFiles(args.positionals);
18
+ void compileFiles(args.positionals);
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);
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';
2
+ import { OutFile } from './outFile';
3
+ import { parse } from './syntax';
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';
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,6 +24,11 @@ class Branch {
14
24
  this.isFork = isFork;
15
25
  return this;
16
26
  }
27
+ setFeature(feature) {
28
+ for (const child of this.children)
29
+ child.setFeature(feature);
30
+ return this;
31
+ }
17
32
  addChild(child, index = this.children.length) {
18
33
  this.children.splice(index, 0, child);
19
34
  child.parent = this;
@@ -52,8 +67,7 @@ class Branch {
52
67
  return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
53
68
  }
54
69
  }
55
- exports.Branch = Branch;
56
- class Step extends Branch {
70
+ export class Step extends Branch {
57
71
  constructor(action = '', responses = [], children, isFork = false) {
58
72
  super(children);
59
73
  this.action = new Action(action);
@@ -63,64 +77,97 @@ class Step extends Branch {
63
77
  get phrases() {
64
78
  return [this.action, ...this.responses];
65
79
  }
66
- toGherkin() {
67
- return this.phrases.flatMap((phrase) => phrase.toGherkin());
80
+ toCode(cg) {
81
+ for (const phrase of this.phrases) {
82
+ phrase.toCode(cg);
83
+ }
84
+ }
85
+ setFeature(feature) {
86
+ this.action.setFeature(feature);
87
+ for (const response of this.responses)
88
+ response.setFeature(feature);
89
+ return super.setFeature(feature);
68
90
  }
69
91
  }
70
- exports.Step = Step;
71
- class State {
92
+ export class State {
72
93
  constructor(text = '') {
73
94
  this.text = text;
74
95
  }
75
96
  }
76
- exports.State = State;
77
- class Label {
97
+ export class Label {
78
98
  constructor(text = '') {
79
99
  this.text = text;
80
100
  }
81
101
  }
82
- exports.Label = Label;
83
- class Section extends Branch {
102
+ export class Section extends Branch {
84
103
  constructor(label = '', children, isFork = false) {
85
104
  super(children);
86
105
  this.label = new Label(label);
87
106
  this.isFork = isFork;
88
107
  }
89
108
  }
90
- exports.Section = Section;
91
- class Phrase {
109
+ export class Phrase {
92
110
  constructor(text = '') {
93
111
  this.text = text;
94
112
  }
95
- toGherkin() {
96
- return [`${this.kind === 'action' ? 'When' : 'Then'} ${this.text}`];
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
+ });
97
148
  }
98
149
  }
99
- exports.Phrase = Phrase;
100
- class Action extends Phrase {
150
+ export class Action extends Phrase {
101
151
  constructor() {
102
152
  super(...arguments);
103
153
  this.kind = 'action';
104
154
  }
105
155
  }
106
- exports.Action = Action;
107
- class Response extends Phrase {
156
+ export class Response extends Phrase {
108
157
  constructor() {
109
158
  super(...arguments);
110
159
  this.kind = 'response';
111
160
  }
112
161
  }
113
- exports.Response = Response;
114
- class Precondition extends Branch {
162
+ export class Precondition extends Branch {
115
163
  constructor(state = '') {
116
164
  super();
117
165
  this.state = new State();
118
166
  this.state.text = state;
119
167
  }
120
168
  }
121
- exports.Precondition = Precondition;
122
- function makeTests(root) {
123
- const routers = new Router_1.Routers(root);
169
+ export function makeTests(root) {
170
+ const routers = new Routers(root);
124
171
  const tests = [];
125
172
  let ic = routers.getIncompleteCount();
126
173
  let newIc;
@@ -144,8 +191,7 @@ function makeTests(root) {
144
191
  tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
145
192
  return tests;
146
193
  }
147
- exports.makeTests = makeTests;
148
- class Test {
194
+ export class Test {
149
195
  constructor(root, branches) {
150
196
  this.root = root;
151
197
  this.branches = branches;
@@ -161,12 +207,10 @@ class Test {
161
207
  .filter((b) => b instanceof Section)
162
208
  .map((s) => s.label.text);
163
209
  }
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
- ];
210
+ get name() {
211
+ return `${this.testNumber}${this.labels.length > 0 ? ' - ' : ''}${this.labels.join(' - ')}`;
212
+ }
213
+ toCode(cg) {
214
+ cg.test(this);
170
215
  }
171
216
  }
172
- 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,18 +1,19 @@
1
1
  {
2
2
  "name": "harmonyc",
3
- "description": "Harmony Code - Test design compiler from high-level model to Gherkin",
4
- "version": "0.1.1",
3
+ "description": "Harmony Code - model-driven BDD for Vitest",
4
+ "version": "0.4.0",
5
5
  "author": "Bernát Kalló",
6
+ "type": "module",
6
7
  "bin": {
7
8
  "harmonyc": "./cli.js"
8
9
  },
9
- "homepage": "https://github.com/harmony-ac/harmony-code#readme",
10
+ "homepage": "https://github.com/harmony-ac/code#readme",
10
11
  "repository": {
11
12
  "type": "git",
12
- "url": "git+https://github.com/harmony-ac/harmony-code.git"
13
+ "url": "git+https://github.com/harmony-ac/code.git"
13
14
  },
14
15
  "bugs": {
15
- "url": "https://github.com/harmony-ac/harmony-code/issues"
16
+ "url": "https://github.com/harmony-ac/code/issues"
16
17
  },
17
18
  "dependencies": {
18
19
  "fast-glob": "^3.3.2",
@@ -22,20 +23,17 @@
22
23
  "keywords": [
23
24
  "unit test",
24
25
  "unit testing",
25
- "cucumber",
26
26
  "bdd",
27
27
  "behavior-driven",
28
28
  "test design",
29
29
  "test design automation",
30
- "gherkin",
31
- "cucumber",
32
- "specflow",
33
30
  "harmony.ac",
34
31
  "test framework",
35
32
  "model-based test",
36
33
  "model-based testing",
37
34
  "test model",
38
35
  "test modeling",
39
- "test modelling"
36
+ "test modelling",
37
+ "vitest"
40
38
  ]
41
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';
4
+ import { definitions } from './definitions';
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';
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/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 harmony-ac
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
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
- }