@turri-dev/cli 0.1.27 → 0.2.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 +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +70 -14
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -13
- package/dist/regenerate/conformance.js +119 -82
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +62 -43
- package/dist/regenerate/loop.js +472 -341
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +71 -28
- package/dist/regenerate/test-kinds.js +110 -63
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +87 -52
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +23 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
package/dist/plain/spec.js
CHANGED
|
@@ -1,44 +1,55 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { InvalidFridArgument } from "./errors.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Port of plain_spec.py. The reference keeps its names for a functional spec's id - `frid`, `get_frids` - and so do
|
|
5
|
+
* the ports here: a frid is the functional spec's func-id, the `# <func-id>` heading it is written under.
|
|
6
|
+
*/
|
|
4
7
|
export const DEFINITIONS = "definitions";
|
|
5
8
|
export const NON_FUNCTIONAL_REQUIREMENTS = "implementation reqs";
|
|
6
9
|
export const TEST_REQUIREMENTS = "test reqs";
|
|
7
10
|
export const FUNCTIONAL_REQUIREMENTS = "functional specs";
|
|
11
|
+
export const CONFORMANCE_TESTS = "conformance_tests";
|
|
8
12
|
export const ACCEPTANCE_TESTS = "acceptance_tests";
|
|
13
|
+
export const CONFORMANCE_TEST_HEADING = "conformance tests";
|
|
9
14
|
export const ACCEPTANCE_TEST_HEADING = "acceptance tests";
|
|
10
15
|
/**
|
|
11
16
|
* Where a module's tree carries the functional specs it inherits through `requires`: every
|
|
12
|
-
* functional spec of every module it requires, transitively, bottom of the chain first
|
|
13
|
-
*
|
|
17
|
+
* functional spec of every module it requires, transitively, bottom of the chain first, each
|
|
18
|
+
* with its module. turri's, not the reference parser's.
|
|
14
19
|
*/
|
|
15
20
|
export const REQUIRED_FUNCTIONAL_SPECS = "required functional specs";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
];
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Where a module's tree carries the test plain files bound to its chain, as the reference's parse output has them
|
|
23
|
+
* (`test_plain_files`): each file's path, from the module's folder, to the functional spec it binds to. turri's.
|
|
24
|
+
*/
|
|
25
|
+
export const TEST_PLAIN_FILES = "test plain files";
|
|
26
|
+
/** The sections whose content is a list of `- ` items. */
|
|
27
|
+
export const LIST_SECTION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS];
|
|
28
|
+
export const ALLOWED_SPECIFICATION_HEADINGS = [...LIST_SECTION_HEADINGS, FUNCTIONAL_REQUIREMENTS];
|
|
29
|
+
export const ALLOWED_IMPORT_SPECIFICATION_HEADINGS = LIST_SECTION_HEADINGS;
|
|
30
|
+
/**
|
|
31
|
+
* A func-id or scenario-id: segments of lowercase letters and digits, with one hyphen between segments. So a hyphen
|
|
32
|
+
* never starts or ends an id, two hyphens never touch, and an id is never only hyphens.
|
|
33
|
+
*/
|
|
34
|
+
export const SEMANTIC_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
35
|
+
export function isSemanticId(text) {
|
|
36
|
+
return SEMANTIC_ID_PATTERN.test(text);
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* The functional specs a module inherits through `requires`, rendered: each with its
|
|
30
|
-
* module, its id
|
|
40
|
+
* module, its func-id, and its scenarios.
|
|
31
41
|
*/
|
|
32
42
|
export function getInheritedFunctionalSpecs(tree, replaceCodeVariables = true) {
|
|
33
43
|
const items = tree[REQUIRED_FUNCTIONAL_SPECS] ?? [];
|
|
34
44
|
return items.map((item) => {
|
|
35
45
|
const codeVariables = {};
|
|
36
|
-
const
|
|
46
|
+
const scenarios = (list) => (list ?? []).map((scenario) => ({ id: scenario.id, text: getSpecificationItemMarkdown(scenario, codeVariables, replaceCodeVariables, "text") }));
|
|
37
47
|
return {
|
|
38
48
|
module: item.module,
|
|
39
|
-
|
|
49
|
+
funcId: item.id,
|
|
40
50
|
spec: getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables),
|
|
41
|
-
|
|
51
|
+
conformanceTests: scenarios(item[CONFORMANCE_TESTS]),
|
|
52
|
+
acceptanceTests: scenarios(item[ACCEPTANCE_TESTS]),
|
|
42
53
|
};
|
|
43
54
|
});
|
|
44
55
|
}
|
|
@@ -46,13 +57,14 @@ export function getSpecificationItems(tree, heading) {
|
|
|
46
57
|
const value = tree[heading];
|
|
47
58
|
return Array.isArray(value) ? value : undefined;
|
|
48
59
|
}
|
|
60
|
+
/** The functional specs of a tree, in file order. */
|
|
61
|
+
export function getFunctionalSpecs(tree) {
|
|
62
|
+
const value = tree[FUNCTIONAL_REQUIREMENTS];
|
|
63
|
+
return Array.isArray(value) ? value : [];
|
|
64
|
+
}
|
|
65
|
+
/** True if any functional spec of the tree has acceptance scenarios. */
|
|
49
66
|
export function hasAcceptanceTests(tree) {
|
|
50
|
-
|
|
51
|
-
const tests = requirement[ACCEPTANCE_TESTS];
|
|
52
|
-
if (tests !== undefined && tests.length > 0)
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
67
|
+
return getFunctionalSpecs(tree).some((item) => (item[ACCEPTANCE_TESTS] ?? []).length > 0);
|
|
56
68
|
}
|
|
57
69
|
export function collectSpecificationLinkedResources(specification, specificationHeading, linkedResourcesList) {
|
|
58
70
|
for (const resource of specification.linked_resources ?? []) {
|
|
@@ -86,59 +98,49 @@ export function collectSpecificationLinkedResources(specification, specification
|
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Add the linked resources of the tree to `linkedResourcesList` and sort the list by link text.
|
|
103
|
+
*
|
|
104
|
+
* `specificationsList` limits the sections read; null or empty reads every section. The functional specs are read up
|
|
105
|
+
* to and including `frid`, or all of them when it is null. With `includeTestScenarios` the links of the conformance
|
|
106
|
+
* and acceptance scenarios are added too, for `frid` alone when one is given. Returns true when `frid` was reached.
|
|
107
|
+
*/
|
|
108
|
+
export function collectLinkedResources(tree, linkedResourcesList, specificationsList, includeTestScenarios, frid = null) {
|
|
109
|
+
if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
|
|
110
|
+
throw new TypeError("plain_source_tree must be a dictionary.");
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
collectSpecificationLinkedResources(requirement, heading, linkedResourcesList);
|
|
97
|
-
}
|
|
112
|
+
if (frid !== null && ![...getFrids(tree)].includes(frid)) {
|
|
113
|
+
throw new Error(`frid ${frid} does not exist.`);
|
|
98
114
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const tests = requirement[ACCEPTANCE_TESTS];
|
|
110
|
-
if (tests !== undefined && includeAcceptanceTests && (frid === null || frid === currentFrid)) {
|
|
111
|
-
for (const test of tests) {
|
|
112
|
-
collectSpecificationLinkedResources(test, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
|
|
115
|
+
const sectionsToCollect = ALLOWED_SPECIFICATION_HEADINGS.filter((heading) => heading in tree && (!specificationsList || specificationsList.length === 0 || specificationsList.includes(heading)));
|
|
116
|
+
let fridReached = false;
|
|
117
|
+
for (const heading of sectionsToCollect) {
|
|
118
|
+
for (const specification of tree[heading] ?? []) {
|
|
119
|
+
collectSpecificationLinkedResources(specification, heading, linkedResourcesList);
|
|
120
|
+
if (heading !== FUNCTIONAL_REQUIREMENTS)
|
|
121
|
+
continue;
|
|
122
|
+
if (includeTestScenarios && (frid === null || frid === specification.id)) {
|
|
123
|
+
for (const scenario of [...(specification[CONFORMANCE_TESTS] ?? []), ...(specification[ACCEPTANCE_TESTS] ?? [])]) {
|
|
124
|
+
collectSpecificationLinkedResources(scenario, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
if (specification.id === frid) {
|
|
128
|
+
fridReached = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
export function collectLinkedResources(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid = null) {
|
|
123
|
-
if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
|
|
124
|
-
throw new TypeError("[plain_source_tree must be a dictionary.");
|
|
125
|
-
}
|
|
126
|
-
if (frid !== null && ![...getFrids(tree)].includes(frid)) {
|
|
127
|
-
throw new Error(`frid ${frid} does not exist.`);
|
|
128
|
-
}
|
|
129
|
-
const result = collectLinkedResourcesInSection(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid);
|
|
130
133
|
linkedResourcesList.sort((a, b) => (a.text < b.text ? -1 : a.text > b.text ? 1 : 0));
|
|
131
|
-
return
|
|
134
|
+
return fridReached;
|
|
132
135
|
}
|
|
133
136
|
/**
|
|
134
|
-
* The FRID iterator:
|
|
135
|
-
* Everything that walks the spec one
|
|
137
|
+
* The FRID iterator: the func-ids of the tree's functional specs, in file order.
|
|
138
|
+
* Everything that walks the spec one functional spec at a time - rendering, ranges, context
|
|
136
139
|
* building - is driven by this.
|
|
137
140
|
*/
|
|
138
141
|
export function* getFrids(tree) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
yield String(id);
|
|
142
|
+
for (const functionality of getFunctionalSpecs(tree))
|
|
143
|
+
yield functionality.id;
|
|
142
144
|
}
|
|
143
145
|
export function getFirstFrid(tree) {
|
|
144
146
|
const first = getFrids(tree).next();
|
|
@@ -163,6 +165,7 @@ export function getPreviousFrid(tree, frid) {
|
|
|
163
165
|
}
|
|
164
166
|
throw new Error(`Functionality ${frid} does not exist.`);
|
|
165
167
|
}
|
|
168
|
+
/** The func-ids that come before `targetFrid` in the specification, in order. */
|
|
166
169
|
export function getFridsBefore(tree, targetFrid) {
|
|
167
170
|
const before = [];
|
|
168
171
|
for (const frid of getFrids(tree)) {
|
|
@@ -172,8 +175,12 @@ export function getFridsBefore(tree, targetFrid) {
|
|
|
172
175
|
}
|
|
173
176
|
return before;
|
|
174
177
|
}
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
/**
|
|
179
|
+
* The text under `key` of the item, with its code variables recorded in `codeVariables` and, when
|
|
180
|
+
* `replaceCodeVariables` is set, their values put in place of the `{{ name }}` placeholders.
|
|
181
|
+
*/
|
|
182
|
+
export function getSpecificationItemMarkdown(specificationItem, codeVariables, replaceCodeVariables, key = "markdown") {
|
|
183
|
+
let markdown = String(specificationItem[key]);
|
|
177
184
|
for (const codeVariable of specificationItem.code_variables ?? []) {
|
|
178
185
|
if (codeVariable.name in codeVariables) {
|
|
179
186
|
if (codeVariables[codeVariable.name] !== codeVariable.value) {
|
|
@@ -189,67 +196,41 @@ export function getSpecificationItemMarkdown(specificationItem, codeVariables, r
|
|
|
189
196
|
}
|
|
190
197
|
return markdown;
|
|
191
198
|
}
|
|
192
|
-
function getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables) {
|
|
193
|
-
let returnFrid = null;
|
|
194
|
-
const requirements = getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS);
|
|
195
|
-
if (requirements !== undefined && requirements.length > 0) {
|
|
196
|
-
let count = 0;
|
|
197
|
-
for (const requirement of requirements) {
|
|
198
|
-
count += 1;
|
|
199
|
-
const currentFrid = String(count);
|
|
200
|
-
functionalRequirements.push(getSpecificationItemMarkdown(requirement, codeVariables, replaceCodeVariables));
|
|
201
|
-
if (currentFrid === frid) {
|
|
202
|
-
for (const test of requirement[ACCEPTANCE_TESTS] ?? []) {
|
|
203
|
-
acceptanceTests.push(getSpecificationItemMarkdown(test, codeVariables, replaceCodeVariables));
|
|
204
|
-
}
|
|
205
|
-
returnFrid = currentFrid;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (returnFrid !== null) {
|
|
211
|
-
const prepend = (target, heading) => {
|
|
212
|
-
const source = getSpecificationItems(tree, heading);
|
|
213
|
-
if (source === undefined)
|
|
214
|
-
return;
|
|
215
|
-
target.unshift(...source.map((item) => getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables)));
|
|
216
|
-
};
|
|
217
|
-
prepend(definitions, DEFINITIONS);
|
|
218
|
-
prepend(nonFunctionalRequirements, NON_FUNCTIONAL_REQUIREMENTS);
|
|
219
|
-
prepend(testRequirements, TEST_REQUIREMENTS);
|
|
220
|
-
}
|
|
221
|
-
return returnFrid;
|
|
222
|
-
}
|
|
223
199
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
200
|
+
* The specifications that build `frid`: every list section, the functional specs up to and including `frid`, and
|
|
201
|
+
* the conformance and acceptance scenarios of `frid` as `{id, text}` entries.
|
|
202
|
+
*
|
|
203
|
+
* Returns the specifications and the code variables they use (null when there are none). Throws when `frid` is not
|
|
204
|
+
* in the tree.
|
|
227
205
|
*/
|
|
228
206
|
export function getSpecificationsForFrid(tree, frid, replaceCodeVariables = true) {
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const functionalRequirements = [];
|
|
233
|
-
const acceptanceTests = [];
|
|
234
|
-
const codeVariables = {};
|
|
235
|
-
const result = getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables);
|
|
236
|
-
if (result === null)
|
|
207
|
+
const functionalities = getFunctionalSpecs(tree);
|
|
208
|
+
const position = functionalities.findIndex((item) => item.id === frid);
|
|
209
|
+
if (position === -1)
|
|
237
210
|
throw new Error(`Functionality ${frid} does not exist.`);
|
|
211
|
+
const codeVariables = {};
|
|
212
|
+
const texts = (items, key = "markdown") => items.map((item) => getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables, key));
|
|
213
|
+
const scenarios = (items) => {
|
|
214
|
+
const scenarioTexts = texts(items, "text");
|
|
215
|
+
return items.map((item, index) => ({ id: item.id, text: scenarioTexts[index] }));
|
|
216
|
+
};
|
|
217
|
+
const listOf = (heading) => getSpecificationItems(tree, heading) ?? [];
|
|
218
|
+
const functionality = functionalities[position];
|
|
238
219
|
const specifications = {
|
|
239
|
-
[DEFINITIONS]:
|
|
240
|
-
[NON_FUNCTIONAL_REQUIREMENTS]:
|
|
241
|
-
[TEST_REQUIREMENTS]:
|
|
242
|
-
[FUNCTIONAL_REQUIREMENTS]:
|
|
220
|
+
[DEFINITIONS]: texts(listOf(DEFINITIONS)),
|
|
221
|
+
[NON_FUNCTIONAL_REQUIREMENTS]: texts(listOf(NON_FUNCTIONAL_REQUIREMENTS)),
|
|
222
|
+
[TEST_REQUIREMENTS]: texts(listOf(TEST_REQUIREMENTS)),
|
|
223
|
+
[FUNCTIONAL_REQUIREMENTS]: texts(functionalities.slice(0, position + 1)),
|
|
224
|
+
[CONFORMANCE_TESTS]: scenarios(functionality[CONFORMANCE_TESTS] ?? []),
|
|
225
|
+
[ACCEPTANCE_TESTS]: scenarios(functionality[ACCEPTANCE_TESTS] ?? []),
|
|
243
226
|
};
|
|
244
|
-
if (acceptanceTests.length > 0)
|
|
245
|
-
specifications[ACCEPTANCE_TESTS] = acceptanceTests;
|
|
246
227
|
return [specifications, Object.keys(codeVariables).length > 0 ? codeVariables : null];
|
|
247
228
|
}
|
|
248
|
-
/** Iterate every
|
|
229
|
+
/** Iterate every functional spec with the context that should be rendered for it. turri's, for a host. */
|
|
249
230
|
export function* iterateFunctionalSpecs(tree, replaceCodeVariables = true) {
|
|
250
|
-
for (const
|
|
251
|
-
const [specifications, codeVariables] = getSpecificationsForFrid(tree,
|
|
252
|
-
yield {
|
|
231
|
+
for (const funcId of getFrids(tree)) {
|
|
232
|
+
const [specifications, codeVariables] = getSpecificationsForFrid(tree, funcId, replaceCodeVariables);
|
|
233
|
+
yield { funcId, specifications, codeVariables };
|
|
253
234
|
}
|
|
254
235
|
}
|
|
255
236
|
export function hashText(text) {
|
|
@@ -258,17 +239,18 @@ export function hashText(text) {
|
|
|
258
239
|
export function getHashValue(specifications) {
|
|
259
240
|
return hashText(JSON.stringify(specifications, null, 4));
|
|
260
241
|
}
|
|
261
|
-
|
|
242
|
+
/** The func-ids from `start` through `end` inclusive, or through the last one when `end` is not given. */
|
|
243
|
+
export function getFridsRange(tree, start, end) {
|
|
262
244
|
const frids = [...getFrids(tree)];
|
|
263
|
-
|
|
245
|
+
// Python prints the list with repr.
|
|
246
|
+
const valid = `[${frids.map((frid) => `'${frid}'`).join(", ")}]`;
|
|
264
247
|
if (!frids.includes(start)) {
|
|
265
|
-
throw new InvalidFridArgument(`Invalid start functionality ID: ${start}. Valid IDs are: ${
|
|
248
|
+
throw new InvalidFridArgument(`Invalid start functionality ID: ${start}. Valid IDs are: ${valid}.`);
|
|
266
249
|
}
|
|
267
250
|
let endIdx;
|
|
268
251
|
if (end !== undefined) {
|
|
269
|
-
end = String(end);
|
|
270
252
|
if (!frids.includes(end)) {
|
|
271
|
-
throw new InvalidFridArgument(`Invalid end functionality ID: ${end}. Valid IDs are: ${
|
|
253
|
+
throw new InvalidFridArgument(`Invalid end functionality ID: ${end}. Valid IDs are: ${valid}.`);
|
|
272
254
|
}
|
|
273
255
|
endIdx = frids.indexOf(end) + 1;
|
|
274
256
|
}
|
|
@@ -281,11 +263,3 @@ function getFridsRange(tree, start, end) {
|
|
|
281
263
|
}
|
|
282
264
|
return frids.slice(startIdx, endIdx);
|
|
283
265
|
}
|
|
284
|
-
export function getRenderRange(renderRange, tree) {
|
|
285
|
-
const parts = renderRange.split(",");
|
|
286
|
-
const rangeEnd = parts.length === 2 ? parts[1] : parts[0];
|
|
287
|
-
return getFridsRange(tree, parts[0], rangeEnd);
|
|
288
|
-
}
|
|
289
|
-
export function getRenderRangeFrom(start, tree) {
|
|
290
|
-
return getFridsRange(tree, start);
|
|
291
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type LinkedResource } from "./markdown.ts";
|
|
2
|
+
import { type CodeVariable, type ScenarioItem } from "./spec.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Port of test_plain_file.py: test plain files, the conformance and acceptance scenarios of one functionality.
|
|
5
|
+
*
|
|
6
|
+
* A test plain file binds to a functionality with the frontmatter keys `module` and `func_id`. It holds a
|
|
7
|
+
* `***conformance tests***` section, an `***acceptance tests***` section or both, in that order, each with at least one
|
|
8
|
+
* scenario: a section with none is left out, and a file with none is not written - a functional spec with no scenarios has
|
|
9
|
+
* no test plain file. Each scenario is a `# <scenario-id>` heading followed by free text, which is kept verbatim.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MODULE_KEY = "module";
|
|
12
|
+
export declare const FUNC_ID_KEY = "func_id";
|
|
13
|
+
export declare const FRONTMATTER_KEYS: string[];
|
|
14
|
+
export declare const SECTION_HEADINGS: readonly ["conformance tests", "acceptance tests"];
|
|
15
|
+
export interface Scenario {
|
|
16
|
+
id: string;
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
/** The body as written, without `>` comment lines and without leading and trailing blank lines. */
|
|
19
|
+
text: string;
|
|
20
|
+
linkedResources: LinkedResource[];
|
|
21
|
+
codeVariables: CodeVariable[];
|
|
22
|
+
}
|
|
23
|
+
/** A scenario as the parse gives it: `{id, text, linked_resources}`, and `code_variables` when it uses any. */
|
|
24
|
+
export declare function scenarioItem(scenario: Scenario): ScenarioItem;
|
|
25
|
+
export interface TestPlainFile {
|
|
26
|
+
/** The path of the file as shown in messages. */
|
|
27
|
+
name: string;
|
|
28
|
+
module: string;
|
|
29
|
+
funcId: string;
|
|
30
|
+
conformanceTests: Scenario[];
|
|
31
|
+
acceptanceTests: Scenario[];
|
|
32
|
+
}
|
|
33
|
+
export declare function scenariosOf(file: TestPlainFile): Scenario[];
|
|
34
|
+
/** Whether a frontmatter binds its file to a functionality, which makes the file a test plain file. */
|
|
35
|
+
export declare function isTestPlainFile(metadata: Record<string, unknown>): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The frontmatter and content of a ***plain file, as python-frontmatter reads them: a block between two lines of
|
|
38
|
+
* three or more dashes, loaded as YAML - a block that is not a mapping is no metadata - and the content after it,
|
|
39
|
+
* stripped. Raises the YAML error as it is.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadFrontmatter(text: string): {
|
|
42
|
+
metadata: Record<string, unknown>;
|
|
43
|
+
content: string;
|
|
44
|
+
};
|
|
45
|
+
/** The frontmatter and content of a test plain file; raise unless the keys are exactly module and func_id. */
|
|
46
|
+
export declare function readMetadata(text: string, name: string): {
|
|
47
|
+
metadata: Record<string, unknown>;
|
|
48
|
+
content: string;
|
|
49
|
+
};
|
|
50
|
+
/** The scenarios of the rendered content, keyed by section heading - a section left out, none; raise on the first structural error. */
|
|
51
|
+
export declare function readScenarios(renderedText: string, name: string): Record<string, Scenario[]>;
|
|
52
|
+
export declare function findScenarioLinkedResources(scenario: Scenario, templateDirs: string[], name: string): LinkedResource[];
|
|
53
|
+
/**
|
|
54
|
+
* Parse the text of a test plain file. Liquid runs as on a spec plain file, with the same template folders; every
|
|
55
|
+
* link in a scenario is resolved against them. Concepts are not checked here, see `validateScenarioConcepts`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseTestPlainFileSource(plainSourceText: string, name: string, templateDirs: string[], codeVariables: Record<string, Record<string, string>>): TestPlainFile;
|
|
58
|
+
/** One message per concept used in a scenario that is neither in `concepts` nor predefined. */
|
|
59
|
+
export declare function validateScenarioConcepts(file: TestPlainFile, concepts: Set<string>): string[];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { load as loadYaml } from "js-yaml";
|
|
2
|
+
import { extractConceptsFromSpecText, DEFAULT_CONCEPTS } from "./concepts.js";
|
|
3
|
+
import { PlainSyntaxError } from "./errors.js";
|
|
4
|
+
import { getLoadedTemplates } from "./files.js";
|
|
5
|
+
import { renderPlainSource } from "./liquid.js";
|
|
6
|
+
import { parseMarkdown } from "./markdown.js";
|
|
7
|
+
import { findLinkedResources, normalizeLineEndings, processSectionCodeVariables, restoreStrippedLines } from "./parser.js";
|
|
8
|
+
import { ACCEPTANCE_TEST_HEADING, CONFORMANCE_TEST_HEADING, isSemanticId } from "./spec.js";
|
|
9
|
+
/**
|
|
10
|
+
* Port of test_plain_file.py: test plain files, the conformance and acceptance scenarios of one functionality.
|
|
11
|
+
*
|
|
12
|
+
* A test plain file binds to a functionality with the frontmatter keys `module` and `func_id`. It holds a
|
|
13
|
+
* `***conformance tests***` section, an `***acceptance tests***` section or both, in that order, each with at least one
|
|
14
|
+
* scenario: a section with none is left out, and a file with none is not written - a functional spec with no scenarios has
|
|
15
|
+
* no test plain file. Each scenario is a `# <scenario-id>` heading followed by free text, which is kept verbatim.
|
|
16
|
+
*/
|
|
17
|
+
export const MODULE_KEY = "module";
|
|
18
|
+
export const FUNC_ID_KEY = "func_id";
|
|
19
|
+
export const FRONTMATTER_KEYS = [MODULE_KEY, FUNC_ID_KEY];
|
|
20
|
+
export const SECTION_HEADINGS = [CONFORMANCE_TEST_HEADING, ACCEPTANCE_TEST_HEADING];
|
|
21
|
+
/** A scenario as the parse gives it: `{id, text, linked_resources}`, and `code_variables` when it uses any. */
|
|
22
|
+
export function scenarioItem(scenario) {
|
|
23
|
+
const entry = { id: scenario.id, text: scenario.text, linked_resources: scenario.linkedResources };
|
|
24
|
+
if (scenario.codeVariables.length > 0)
|
|
25
|
+
entry.code_variables = scenario.codeVariables;
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
export function scenariosOf(file) {
|
|
29
|
+
return [...file.conformanceTests, ...file.acceptanceTests];
|
|
30
|
+
}
|
|
31
|
+
/** Whether a frontmatter binds its file to a functionality, which makes the file a test plain file. */
|
|
32
|
+
export function isTestPlainFile(metadata) {
|
|
33
|
+
return FRONTMATTER_KEYS.every((key) => key in metadata);
|
|
34
|
+
}
|
|
35
|
+
const error = (message) => new PlainSyntaxError(`Plain syntax error: ${message}`);
|
|
36
|
+
const at = (lineNumber, name) => `Syntax error at line ${lineNumber} in test plain file '${name}'`;
|
|
37
|
+
/**
|
|
38
|
+
* The frontmatter and content of a ***plain file, as python-frontmatter reads them: a block between two lines of
|
|
39
|
+
* three or more dashes, loaded as YAML - a block that is not a mapping is no metadata - and the content after it,
|
|
40
|
+
* stripped. Raises the YAML error as it is.
|
|
41
|
+
*/
|
|
42
|
+
export function loadFrontmatter(text) {
|
|
43
|
+
const match = /^-{3,}[ \t]*\n(?:([\s\S]*?)\n)?-{3,}[ \t]*(?:\n|$)/.exec(text);
|
|
44
|
+
if (match === null)
|
|
45
|
+
return { metadata: {}, content: text.trim() };
|
|
46
|
+
const block = match[1] ?? "";
|
|
47
|
+
const loaded = block.trim() === "" ? {} : loadYaml(block);
|
|
48
|
+
const metadata = loaded !== null && typeof loaded === "object" && !Array.isArray(loaded) ? loaded : {};
|
|
49
|
+
return { metadata, content: text.slice(match[0].length).trim() };
|
|
50
|
+
}
|
|
51
|
+
/** The frontmatter and content of a test plain file; raise unless the keys are exactly module and func_id. */
|
|
52
|
+
export function readMetadata(text, name) {
|
|
53
|
+
let post;
|
|
54
|
+
try {
|
|
55
|
+
post = loadFrontmatter(text);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
throw error(`Test plain file '${name}' has invalid frontmatter: ${e instanceof Error ? e.message : String(e)}`);
|
|
59
|
+
}
|
|
60
|
+
for (const key of FRONTMATTER_KEYS) {
|
|
61
|
+
if (!(key in post.metadata))
|
|
62
|
+
throw error(`Test plain file '${name}' has no \`${key}:\` key in its frontmatter.`);
|
|
63
|
+
const value = post.metadata[key];
|
|
64
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
65
|
+
throw error(`Test plain file '${name}': the frontmatter key \`${key}:\` must be a non-empty string.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const key of Object.keys(post.metadata)) {
|
|
69
|
+
if (!FRONTMATTER_KEYS.includes(key)) {
|
|
70
|
+
throw error(`Test plain file '${name}' has the frontmatter key \`${key}:\`; only \`module:\` and \`func_id:\` are allowed.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return post;
|
|
74
|
+
}
|
|
75
|
+
function trimBlankLines(lines) {
|
|
76
|
+
let start = 0;
|
|
77
|
+
let end = lines.length;
|
|
78
|
+
while (start < end && lines[start].trim() === "")
|
|
79
|
+
start += 1;
|
|
80
|
+
while (end > start && lines[end - 1].trim() === "")
|
|
81
|
+
end -= 1;
|
|
82
|
+
return lines.slice(start, end);
|
|
83
|
+
}
|
|
84
|
+
/** The scenarios of the rendered content, keyed by section heading - a section left out, none; raise on the first structural error. */
|
|
85
|
+
export function readScenarios(renderedText, name) {
|
|
86
|
+
const sections = {};
|
|
87
|
+
// Each section's heading line, for one left with no scenario.
|
|
88
|
+
const sectionLines = new Map();
|
|
89
|
+
let currentSection = null;
|
|
90
|
+
let current = null;
|
|
91
|
+
let body = [];
|
|
92
|
+
// The marker of the open fenced code block (``` or ~~~), null outside one.
|
|
93
|
+
let fence = null;
|
|
94
|
+
// Scenario-id to the line of its heading.
|
|
95
|
+
const headingLines = new Map();
|
|
96
|
+
const closeScenario = () => {
|
|
97
|
+
if (current === null)
|
|
98
|
+
return;
|
|
99
|
+
if (fence !== null)
|
|
100
|
+
throw error(`${at(current.lineNumber, name)}: Scenario \`${current.id}\` has a code fence that is not closed.`);
|
|
101
|
+
const lines = trimBlankLines(body);
|
|
102
|
+
if (lines.length === 0)
|
|
103
|
+
throw error(`${at(current.lineNumber, name)}: Scenario \`${current.id}\` has no text.`);
|
|
104
|
+
current.text = lines.join("\n");
|
|
105
|
+
};
|
|
106
|
+
renderedText.split("\n").forEach((line, index) => {
|
|
107
|
+
const lineNumber = index + 1;
|
|
108
|
+
const where = at(lineNumber, name);
|
|
109
|
+
// Inside a fenced code block every line is body text: a `# ` line is not a heading there, a `***` line is not a
|
|
110
|
+
// section marker, and a `>` line is not a comment.
|
|
111
|
+
const stripped = line.trimStart();
|
|
112
|
+
if (stripped.startsWith("```") || stripped.startsWith("~~~")) {
|
|
113
|
+
const marker = stripped.slice(0, 3);
|
|
114
|
+
if (fence === null)
|
|
115
|
+
fence = marker;
|
|
116
|
+
else if (fence === marker)
|
|
117
|
+
fence = null;
|
|
118
|
+
if (current !== null) {
|
|
119
|
+
body.push(line);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (fence !== null && current !== null) {
|
|
124
|
+
body.push(line);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (line.startsWith("***")) {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
const heading = trimmed.endsWith("***") && trimmed.length > 6 ? trimmed.slice(3, -3) : null;
|
|
130
|
+
if (heading === null || !SECTION_HEADINGS.includes(heading)) {
|
|
131
|
+
throw error(`${where}: Invalid section (\`${trimmed}\`). A test plain file has the sections ` +
|
|
132
|
+
`\`***${SECTION_HEADINGS[0]}***\` and \`***${SECTION_HEADINGS[1]}***\`, in this order.`);
|
|
133
|
+
}
|
|
134
|
+
if (heading in sections)
|
|
135
|
+
throw error(`${where}: Duplicate section \`***${heading}***\`.`);
|
|
136
|
+
if (heading === SECTION_HEADINGS[0] && SECTION_HEADINGS[1] in sections) {
|
|
137
|
+
throw error(`${where}: The \`***${heading}***\` section must come before \`***${SECTION_HEADINGS[1]}***\`.`);
|
|
138
|
+
}
|
|
139
|
+
closeScenario();
|
|
140
|
+
current = null;
|
|
141
|
+
currentSection = heading;
|
|
142
|
+
sections[heading] = [];
|
|
143
|
+
sectionLines.set(heading, lineNumber);
|
|
144
|
+
}
|
|
145
|
+
else if (line.startsWith("# ") || line === "#") {
|
|
146
|
+
const scenarioId = line.slice(1).trim();
|
|
147
|
+
if (currentSection === null) {
|
|
148
|
+
throw error(`${where}: Scenario \`${scenarioId}\` comes before the first section; a test plain file starts ` +
|
|
149
|
+
`with \`***${SECTION_HEADINGS[0]}***\`.`);
|
|
150
|
+
}
|
|
151
|
+
if (!isSemanticId(scenarioId)) {
|
|
152
|
+
throw error(`${where}: Invalid scenario-id (\`${scenarioId}\`). A scenario-id is lowercase letters and digits, ` +
|
|
153
|
+
"with single hyphens between them.");
|
|
154
|
+
}
|
|
155
|
+
if (headingLines.has(scenarioId)) {
|
|
156
|
+
throw error(`${where}: Duplicate scenario-id \`${scenarioId}\`; it is first used at line ${headingLines.get(scenarioId)}.`);
|
|
157
|
+
}
|
|
158
|
+
closeScenario();
|
|
159
|
+
current = { id: scenarioId, lineNumber, text: "", linkedResources: [], codeVariables: [] };
|
|
160
|
+
body = [];
|
|
161
|
+
headingLines.set(scenarioId, lineNumber);
|
|
162
|
+
sections[currentSection].push(current);
|
|
163
|
+
}
|
|
164
|
+
else if (line.trimStart().startsWith(">")) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
else if (current !== null) {
|
|
168
|
+
body.push(line);
|
|
169
|
+
}
|
|
170
|
+
else if (line.trim() !== "") {
|
|
171
|
+
if (currentSection === null) {
|
|
172
|
+
throw error(`${where}: Content before the first section (\`${line.trim()}\`); a test plain file starts with ` +
|
|
173
|
+
`\`***${SECTION_HEADINGS[0]}***\`.`);
|
|
174
|
+
}
|
|
175
|
+
throw error(`${where}: Content before the first \`# <scenario-id>\` heading in the \`${currentSection}\` section (\`${line.trim()}\`).`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
closeScenario();
|
|
179
|
+
for (const heading of SECTION_HEADINGS) {
|
|
180
|
+
if (sections[heading]?.length === 0) {
|
|
181
|
+
throw error(`${at(sectionLines.get(heading), name)}: The \`***${heading}***\` section has no scenarios. Leave out a section with none.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (Object.keys(sections).length === 0) {
|
|
185
|
+
throw error(`Test plain file '${name}' has no scenarios. A functional spec with none has no test plain file: delete it.`);
|
|
186
|
+
}
|
|
187
|
+
for (const heading of SECTION_HEADINGS)
|
|
188
|
+
sections[heading] ??= [];
|
|
189
|
+
return sections;
|
|
190
|
+
}
|
|
191
|
+
export function findScenarioLinkedResources(scenario, templateDirs, name) {
|
|
192
|
+
const document = parseMarkdown(scenario.text);
|
|
193
|
+
return findLinkedResources(document.children ?? [], templateDirs, ` in test plain file '${name}', scenario \`${scenario.id}\``);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Parse the text of a test plain file. Liquid runs as on a spec plain file, with the same template folders; every
|
|
197
|
+
* link in a scenario is resolved against them. Concepts are not checked here, see `validateScenarioConcepts`.
|
|
198
|
+
*/
|
|
199
|
+
export function parseTestPlainFileSource(plainSourceText, name, templateDirs, codeVariables) {
|
|
200
|
+
plainSourceText = normalizeLineEndings(plainSourceText);
|
|
201
|
+
const post = readMetadata(plainSourceText, name);
|
|
202
|
+
const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText, name);
|
|
203
|
+
const content = restoreStrippedLines(plainSourceText, post.content);
|
|
204
|
+
const renderedText = renderPlainSource(content, loadedTemplates, codeVariables);
|
|
205
|
+
const sections = readScenarios(renderedText, name);
|
|
206
|
+
for (const scenario of [...sections[SECTION_HEADINGS[0]], ...sections[SECTION_HEADINGS[1]]]) {
|
|
207
|
+
scenario.linkedResources = findScenarioLinkedResources(scenario, templateDirs, name);
|
|
208
|
+
const entry = { text: scenario.text };
|
|
209
|
+
processSectionCodeVariables(entry, codeVariables, "text");
|
|
210
|
+
scenario.text = entry.text;
|
|
211
|
+
scenario.codeVariables = entry.code_variables ?? [];
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
name,
|
|
215
|
+
module: String(post.metadata[MODULE_KEY]),
|
|
216
|
+
funcId: String(post.metadata[FUNC_ID_KEY]),
|
|
217
|
+
conformanceTests: sections[SECTION_HEADINGS[0]],
|
|
218
|
+
acceptanceTests: sections[SECTION_HEADINGS[1]],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** One message per concept used in a scenario that is neither in `concepts` nor predefined. */
|
|
222
|
+
export function validateScenarioConcepts(file, concepts) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
for (const scenario of scenariosOf(file)) {
|
|
225
|
+
for (const concept of new Set(extractConceptsFromSpecText(scenario.text))) {
|
|
226
|
+
if (!concepts.has(concept) && !DEFAULT_CONCEPTS.has(concept)) {
|
|
227
|
+
errors.push(`${at(scenario.lineNumber, file.name)}: Concept ${concept} used in scenario \`${scenario.id}\` is not defined in module '${file.module}'.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return errors;
|
|
232
|
+
}
|