@application-kit/acceptance 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cjs/bin/application-scenarios.d.ts +2 -0
- package/dist/cjs/bin/application-scenarios.js +15 -0
- package/dist/cjs/bin/index.d.ts +1 -0
- package/dist/cjs/bin/index.js +17 -0
- package/dist/cjs/cadence/index.d.ts +1 -0
- package/dist/cjs/cadence/index.js +17 -0
- package/dist/cjs/cadence/resolve-cadence.d.ts +8 -0
- package/dist/cjs/cadence/resolve-cadence.js +18 -0
- package/dist/cjs/environments/environment.interface.d.ts +5 -0
- package/dist/cjs/environments/environment.interface.js +2 -0
- package/dist/cjs/environments/index.d.ts +3 -0
- package/dist/cjs/environments/index.js +19 -0
- package/dist/cjs/environments/resolve-environment.d.ts +10 -0
- package/dist/cjs/environments/resolve-environment.js +20 -0
- package/dist/cjs/environments/secrets.d.ts +4 -0
- package/dist/cjs/environments/secrets.js +14 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +18 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/scenarios/acceptance-class.d.ts +26 -0
- package/dist/cjs/scenarios/acceptance-class.js +100 -0
- package/dist/cjs/scenarios/board.d.ts +6 -0
- package/dist/cjs/scenarios/board.js +98 -0
- package/dist/cjs/scenarios/classes.d.ts +16 -0
- package/dist/cjs/scenarios/classes.js +48 -0
- package/dist/cjs/scenarios/classify.d.ts +35 -0
- package/dist/cjs/scenarios/classify.js +108 -0
- package/dist/cjs/scenarios/config.d.ts +17 -0
- package/dist/cjs/scenarios/config.js +67 -0
- package/dist/cjs/scenarios/coverage-data.d.ts +53 -0
- package/dist/cjs/scenarios/coverage-data.js +118 -0
- package/dist/cjs/scenarios/coverage-json.d.ts +2 -0
- package/dist/cjs/scenarios/coverage-json.js +94 -0
- package/dist/cjs/scenarios/coverage-open.d.ts +16 -0
- package/dist/cjs/scenarios/coverage-open.js +76 -0
- package/dist/cjs/scenarios/coverage-page.d.ts +2 -0
- package/dist/cjs/scenarios/coverage-page.js +108 -0
- package/dist/cjs/scenarios/coverage-removed.d.ts +13 -0
- package/dist/cjs/scenarios/coverage-removed.js +30 -0
- package/dist/cjs/scenarios/coverage.d.ts +26 -0
- package/dist/cjs/scenarios/coverage.js +91 -0
- package/dist/cjs/scenarios/dates.d.ts +8 -0
- package/dist/cjs/scenarios/dates.js +93 -0
- package/dist/cjs/scenarios/declared.d.ts +18 -0
- package/dist/cjs/scenarios/declared.js +69 -0
- package/dist/cjs/scenarios/features.d.ts +13 -0
- package/dist/cjs/scenarios/features.js +112 -0
- package/dist/cjs/scenarios/gherkin.d.ts +10 -0
- package/dist/cjs/scenarios/gherkin.js +75 -0
- package/dist/cjs/scenarios/git.d.ts +15 -0
- package/dist/cjs/scenarios/git.js +83 -0
- package/dist/cjs/scenarios/index.d.ts +27 -0
- package/dist/cjs/scenarios/index.js +43 -0
- package/dist/cjs/scenarios/inventory.d.ts +8 -0
- package/dist/cjs/scenarios/inventory.js +55 -0
- package/dist/cjs/scenarios/links.d.ts +4 -0
- package/dist/cjs/scenarios/links.js +99 -0
- package/dist/cjs/scenarios/lint-inputs.d.ts +12 -0
- package/dist/cjs/scenarios/lint-inputs.js +118 -0
- package/dist/cjs/scenarios/lint.d.ts +8 -0
- package/dist/cjs/scenarios/lint.js +94 -0
- package/dist/cjs/scenarios/projects.d.ts +21 -0
- package/dist/cjs/scenarios/projects.js +104 -0
- package/dist/cjs/scenarios/python-text.d.ts +11 -0
- package/dist/cjs/scenarios/python-text.js +95 -0
- package/dist/cjs/scenarios/remote.d.ts +6 -0
- package/dist/cjs/scenarios/remote.js +29 -0
- package/dist/cjs/scenarios/removed-list.d.ts +15 -0
- package/dist/cjs/scenarios/removed-list.js +62 -0
- package/dist/cjs/scenarios/report.d.ts +6 -0
- package/dist/cjs/scenarios/report.js +10 -0
- package/dist/cjs/scenarios/run.d.ts +10 -0
- package/dist/cjs/scenarios/run.js +145 -0
- package/dist/cjs/scenarios/task-links.d.ts +15 -0
- package/dist/cjs/scenarios/task-links.js +54 -0
- package/dist/esm/bin/application-scenarios.d.ts +2 -0
- package/dist/esm/bin/application-scenarios.js +10 -0
- package/dist/esm/bin/index.d.ts +1 -0
- package/dist/esm/bin/index.js +1 -0
- package/dist/esm/cadence/index.d.ts +1 -0
- package/dist/esm/cadence/index.js +1 -0
- package/dist/esm/cadence/resolve-cadence.d.ts +8 -0
- package/dist/esm/cadence/resolve-cadence.js +14 -0
- package/dist/esm/environments/environment.interface.d.ts +5 -0
- package/dist/esm/environments/environment.interface.js +1 -0
- package/dist/esm/environments/index.d.ts +3 -0
- package/dist/esm/environments/index.js +3 -0
- package/dist/esm/environments/resolve-environment.d.ts +10 -0
- package/dist/esm/environments/resolve-environment.js +16 -0
- package/dist/esm/environments/secrets.d.ts +4 -0
- package/dist/esm/environments/secrets.js +9 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/scenarios/acceptance-class.d.ts +26 -0
- package/dist/esm/scenarios/acceptance-class.js +93 -0
- package/dist/esm/scenarios/board.d.ts +6 -0
- package/dist/esm/scenarios/board.js +92 -0
- package/dist/esm/scenarios/classes.d.ts +16 -0
- package/dist/esm/scenarios/classes.js +42 -0
- package/dist/esm/scenarios/classify.d.ts +35 -0
- package/dist/esm/scenarios/classify.js +104 -0
- package/dist/esm/scenarios/config.d.ts +17 -0
- package/dist/esm/scenarios/config.js +58 -0
- package/dist/esm/scenarios/coverage-data.d.ts +53 -0
- package/dist/esm/scenarios/coverage-data.js +109 -0
- package/dist/esm/scenarios/coverage-json.d.ts +2 -0
- package/dist/esm/scenarios/coverage-json.js +90 -0
- package/dist/esm/scenarios/coverage-open.d.ts +16 -0
- package/dist/esm/scenarios/coverage-open.js +71 -0
- package/dist/esm/scenarios/coverage-page.d.ts +2 -0
- package/dist/esm/scenarios/coverage-page.js +104 -0
- package/dist/esm/scenarios/coverage-removed.d.ts +13 -0
- package/dist/esm/scenarios/coverage-removed.js +26 -0
- package/dist/esm/scenarios/coverage.d.ts +26 -0
- package/dist/esm/scenarios/coverage.js +84 -0
- package/dist/esm/scenarios/dates.d.ts +8 -0
- package/dist/esm/scenarios/dates.js +87 -0
- package/dist/esm/scenarios/declared.d.ts +18 -0
- package/dist/esm/scenarios/declared.js +62 -0
- package/dist/esm/scenarios/features.d.ts +13 -0
- package/dist/esm/scenarios/features.js +96 -0
- package/dist/esm/scenarios/gherkin.d.ts +10 -0
- package/dist/esm/scenarios/gherkin.js +68 -0
- package/dist/esm/scenarios/git.d.ts +15 -0
- package/dist/esm/scenarios/git.js +72 -0
- package/dist/esm/scenarios/index.d.ts +27 -0
- package/dist/esm/scenarios/index.js +27 -0
- package/dist/esm/scenarios/inventory.d.ts +8 -0
- package/dist/esm/scenarios/inventory.js +48 -0
- package/dist/esm/scenarios/links.d.ts +4 -0
- package/dist/esm/scenarios/links.js +95 -0
- package/dist/esm/scenarios/lint-inputs.d.ts +12 -0
- package/dist/esm/scenarios/lint-inputs.js +110 -0
- package/dist/esm/scenarios/lint.d.ts +8 -0
- package/dist/esm/scenarios/lint.js +89 -0
- package/dist/esm/scenarios/projects.d.ts +21 -0
- package/dist/esm/scenarios/projects.js +93 -0
- package/dist/esm/scenarios/python-text.d.ts +11 -0
- package/dist/esm/scenarios/python-text.js +81 -0
- package/dist/esm/scenarios/remote.d.ts +6 -0
- package/dist/esm/scenarios/remote.js +24 -0
- package/dist/esm/scenarios/removed-list.d.ts +15 -0
- package/dist/esm/scenarios/removed-list.js +57 -0
- package/dist/esm/scenarios/report.d.ts +6 -0
- package/dist/esm/scenarios/report.js +6 -0
- package/dist/esm/scenarios/run.d.ts +10 -0
- package/dist/esm/scenarios/run.js +141 -0
- package/dist/esm/scenarios/task-links.d.ts +15 -0
- package/dist/esm/scenarios/task-links.js +50 -0
- package/package.json +65 -3
- package/README.md +0 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classify = void 0;
|
|
4
|
+
const acceptance_class_js_1 = require("./acceptance-class.js");
|
|
5
|
+
const config_js_1 = require("./config.js");
|
|
6
|
+
const git_js_1 = require("./git.js");
|
|
7
|
+
const projects_js_1 = require("./projects.js");
|
|
8
|
+
const python_text_js_1 = require("./python-text.js");
|
|
9
|
+
const storyNumber = /^(?<epic>\d+[a-z]*)-(?<story>\d+[a-z]*)(?:-|$)/u;
|
|
10
|
+
const revisionStoryFiles = async (cwd, project, revision) => {
|
|
11
|
+
const listed = await (0, git_js_1.git)(cwd, project.repo, ['ls-tree', '-r', '-z', '--name-only', revision, '--', 'tasks']);
|
|
12
|
+
if (listed === undefined) {
|
|
13
|
+
throw new config_js_1.ConfigError(`git ls-tree в ${project.repo} на ${project.revision} не прошёл`);
|
|
14
|
+
}
|
|
15
|
+
const files = (0, python_text_js_1.sortedText)(listed.split('\0')).filter((file) => file.endsWith('.md'))
|
|
16
|
+
.toReversed();
|
|
17
|
+
return new Map(files.map((file) => [(0, projects_js_1.storyKey)(file), file]));
|
|
18
|
+
};
|
|
19
|
+
const storyPrefix = (story) => {
|
|
20
|
+
const groups = storyNumber.exec(story.key)?.groups;
|
|
21
|
+
return groups === undefined ? undefined : `${story.project}:AC-${groups.epic}-${groups.story}.`;
|
|
22
|
+
};
|
|
23
|
+
const storyIds = (scenarios, story) => {
|
|
24
|
+
const prefix = storyPrefix(story);
|
|
25
|
+
const linked = (0, acceptance_class_js_1.storyLinks)(story.text, scenarios.config).map(([, tag]) => tag);
|
|
26
|
+
const known = [...scenarios.ids.keys(), ...scenarios.removed.keys()];
|
|
27
|
+
const prefixed = prefix === undefined ? [] : known.filter((id) => id.startsWith(prefix));
|
|
28
|
+
return (0, python_text_js_1.sortedText)(new Set([...linked, ...prefixed]));
|
|
29
|
+
};
|
|
30
|
+
const scenarioOutcome = (scenarios, story, found) => {
|
|
31
|
+
const ids = storyIds(scenarios, story);
|
|
32
|
+
const state = (0, acceptance_class_js_1.scenarioState)(ids, scenarios.ids, scenarios.removed);
|
|
33
|
+
const { defects, kinds } = found;
|
|
34
|
+
const scenarioStory = { file: story.file, ids, key: story.key, state };
|
|
35
|
+
return { counted: [...kinds, state], defects, story: scenarioStory };
|
|
36
|
+
};
|
|
37
|
+
const fileOutcome = (scenarios, story) => {
|
|
38
|
+
const location = `${story.project}/${story.file}`;
|
|
39
|
+
const found = (0, acceptance_class_js_1.storyClass)({ config: scenarios.config, location, text: story.text });
|
|
40
|
+
const { defects, kinds } = found;
|
|
41
|
+
return kinds?.includes('e2e') === true
|
|
42
|
+
? scenarioOutcome(scenarios, story, { defects, kinds })
|
|
43
|
+
: { counted: kinds ?? ['без класса'], defects };
|
|
44
|
+
};
|
|
45
|
+
const isOpen = (status) => status !== 'done' && status !== 'rejected';
|
|
46
|
+
const openStory = (scenarios, story, status) => {
|
|
47
|
+
const { kinds } = (0, acceptance_class_js_1.storyClass)({ config: scenarios.config, location: story.file, text: story.text });
|
|
48
|
+
const ids = kinds?.includes('e2e') === true ? storyIds(scenarios, story) : [];
|
|
49
|
+
return { file: story.file, ids, key: story.key, kinds, status };
|
|
50
|
+
};
|
|
51
|
+
const missingOutcome = (project, key) => ({
|
|
52
|
+
counted: ['без класса'],
|
|
53
|
+
defects: [`закрытая story без файла в tasks/ на ${project.revision}: ${project.name}:${key}`],
|
|
54
|
+
});
|
|
55
|
+
const countOf = (outcomes) => {
|
|
56
|
+
const counted = outcomes.flatMap((outcome) => outcome.counted);
|
|
57
|
+
const count = (name) => counted.filter((item) => item === name).length;
|
|
58
|
+
return {
|
|
59
|
+
'code-tests': count('code-tests'),
|
|
60
|
+
e2e: count('e2e'),
|
|
61
|
+
none: count('none'),
|
|
62
|
+
'без класса': count('без класса'),
|
|
63
|
+
'без сценария': count('без сценария'),
|
|
64
|
+
'все сняты': count('все сняты'),
|
|
65
|
+
'с тестом': count('с тестом'),
|
|
66
|
+
'только долг': count('только долг'),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
const transferLines = (project, stories) => stories
|
|
70
|
+
.filter((story) => story.state === 'без сценария')
|
|
71
|
+
.map((story) => `перенести: ${project.name}:${story.key} — ${story.file}`);
|
|
72
|
+
const locateStories = async (cwd, project, revision, keys) => {
|
|
73
|
+
const files = await revisionStoryFiles(cwd, project, revision);
|
|
74
|
+
const paths = keys.flatMap((key) => files.get(key) ?? []);
|
|
75
|
+
const texts = await (0, git_js_1.readAtRevision)({ cwd, repo: project.repo }, revision, paths);
|
|
76
|
+
return new Map([...texts].map(([file, text]) => [
|
|
77
|
+
(0, projects_js_1.storyKey)(file),
|
|
78
|
+
{ file, key: (0, projects_js_1.storyKey)(file), project: project.name, text },
|
|
79
|
+
]));
|
|
80
|
+
};
|
|
81
|
+
const classify = async (scenarios, project) => {
|
|
82
|
+
const statuses = await (0, projects_js_1.projectStatuses)(scenarios.cwd, project);
|
|
83
|
+
const revision = String(project.revision);
|
|
84
|
+
const sorted = [...statuses].toSorted(([left], [right]) => (0, python_text_js_1.compareText)(left, right));
|
|
85
|
+
const closed = sorted.filter(([, status]) => status === 'done').map(([key]) => key);
|
|
86
|
+
const open = sorted.filter(([, status]) => isOpen(status));
|
|
87
|
+
const located = await locateStories(scenarios.cwd, project, revision, [...closed, ...open.map(([key]) => key)]);
|
|
88
|
+
const outcomes = closed.map((key) => {
|
|
89
|
+
const story = located.get(key);
|
|
90
|
+
return story === undefined ? missingOutcome(project, key) : fileOutcome(scenarios, story);
|
|
91
|
+
});
|
|
92
|
+
const stories = outcomes.flatMap((outcome) => outcome.story ?? []);
|
|
93
|
+
const openStories = open.map(([key, status]) => {
|
|
94
|
+
const story = located.get(key);
|
|
95
|
+
return story === undefined
|
|
96
|
+
? { file: undefined, ids: [], key, kinds: undefined, status }
|
|
97
|
+
: openStory(scenarios, story, status);
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
closed: closed.length,
|
|
101
|
+
counts: countOf(outcomes),
|
|
102
|
+
defects: outcomes.flatMap((outcome) => outcome.defects),
|
|
103
|
+
open: openStories,
|
|
104
|
+
stories,
|
|
105
|
+
transfers: transferLines(project, stories),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
exports.classify = classify;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ConfigInterface {
|
|
2
|
+
readonly blocking: readonly string[];
|
|
3
|
+
readonly features: string;
|
|
4
|
+
readonly groups: readonly string[];
|
|
5
|
+
readonly id: RegExp;
|
|
6
|
+
readonly pending: string | undefined;
|
|
7
|
+
readonly removed: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const markPending = "not-implemented";
|
|
10
|
+
export declare const markQuarantine = "quarantine";
|
|
11
|
+
export declare const markTodo = "todo";
|
|
12
|
+
export declare const marks: readonly string[];
|
|
13
|
+
export declare class ConfigError extends Error {
|
|
14
|
+
readonly name = "ConfigError";
|
|
15
|
+
}
|
|
16
|
+
export declare const loadConfig: (file: string, cwd: string) => Promise<ConfigInterface>;
|
|
17
|
+
export declare const isProjectCovered: (config: ConfigInterface, project: string) => boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isProjectCovered = exports.loadConfig = exports.ConfigError = exports.marks = exports.markTodo = exports.markQuarantine = exports.markPending = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const python_text_js_1 = require("./python-text.js");
|
|
10
|
+
exports.markPending = 'not-implemented';
|
|
11
|
+
exports.markQuarantine = 'quarantine';
|
|
12
|
+
exports.markTodo = 'todo';
|
|
13
|
+
exports.marks = [exports.markPending, exports.markQuarantine, exports.markTodo];
|
|
14
|
+
class ConfigError extends Error {
|
|
15
|
+
name = 'ConfigError';
|
|
16
|
+
}
|
|
17
|
+
exports.ConfigError = ConfigError;
|
|
18
|
+
const emptyValues = new Set(['', 0, false, null, undefined]);
|
|
19
|
+
const isFilled = (value) => (Array.isArray(value) ? value.length > 0 : !emptyValues.has(value));
|
|
20
|
+
const isRawConfig = (value) => {
|
|
21
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
22
|
+
return isObject && !Array.isArray(value);
|
|
23
|
+
};
|
|
24
|
+
const parseRaw = (text) => {
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
if (!isRawConfig(parsed)) {
|
|
27
|
+
throw new TypeError('ждём объект JSON');
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
};
|
|
31
|
+
const readRaw = async (file, cwd) => {
|
|
32
|
+
try {
|
|
33
|
+
return parseRaw(await (0, promises_1.readFile)(node_path_1.default.resolve(cwd, file), 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new ConfigError(`конфиг ${file} не читается: ${(0, python_text_js_1.messageOf)(error)}`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const compileId = (source) => {
|
|
40
|
+
try {
|
|
41
|
+
return new RegExp(source, 'u');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new ConfigError(`«id» конфига — не regex: ${(0, python_text_js_1.messageOf)(error)}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const textList = (value) => (Array.isArray(value) ? value.map(String) : []);
|
|
48
|
+
const textOr = (value, fallback) => (typeof value === 'string' ? value : fallback);
|
|
49
|
+
const pendingOf = (value) => (isFilled(value) ? String(value) : undefined);
|
|
50
|
+
const loadConfig = async (file, cwd) => {
|
|
51
|
+
const raw = await readRaw(file, cwd);
|
|
52
|
+
const missing = ['id', 'groups'].find((key) => !isFilled(raw[key]));
|
|
53
|
+
if (missing !== undefined) {
|
|
54
|
+
throw new ConfigError(`в конфиге ${file} нет ключа «${missing}»`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
blocking: textList(raw.blocking),
|
|
58
|
+
features: textOr(raw.features, 'features'),
|
|
59
|
+
groups: textList(raw.groups),
|
|
60
|
+
id: compileId(String(raw.id)),
|
|
61
|
+
pending: pendingOf(raw.pending),
|
|
62
|
+
removed: textOr(raw.removed, 'removed-scenarios.md'),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
exports.loadConfig = loadConfig;
|
|
66
|
+
const isProjectCovered = (config, project) => config.id.test(`${project}:AC-1-1.1`);
|
|
67
|
+
exports.isProjectCovered = isProjectCovered;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ClassifiedInterface } from './classes.js';
|
|
2
|
+
import type { ScenariosInterface } from './classify.js';
|
|
3
|
+
import type { FilesType } from './features.js';
|
|
4
|
+
import type { ScenarioInterface } from './gherkin.js';
|
|
5
|
+
import type { LinkRowInterface } from './task-links.js';
|
|
6
|
+
export interface CoverageInterface {
|
|
7
|
+
readonly projects: readonly CoverageProjectInterface[];
|
|
8
|
+
readonly uncovered: readonly UncoveredStoryInterface[];
|
|
9
|
+
}
|
|
10
|
+
export interface CoverageProjectInterface {
|
|
11
|
+
readonly allRemoved: number;
|
|
12
|
+
readonly closed: number;
|
|
13
|
+
readonly codeTests: number;
|
|
14
|
+
readonly commit: string;
|
|
15
|
+
readonly covered: number;
|
|
16
|
+
readonly denominator: number;
|
|
17
|
+
readonly e2e: number;
|
|
18
|
+
readonly none: number;
|
|
19
|
+
readonly project: string;
|
|
20
|
+
readonly revision: string;
|
|
21
|
+
readonly share: number | undefined;
|
|
22
|
+
readonly unclassified: number;
|
|
23
|
+
readonly uncovered: Readonly<Record<UncoveredType, number>>;
|
|
24
|
+
}
|
|
25
|
+
export interface PendingInterface {
|
|
26
|
+
readonly file: string;
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly line: number;
|
|
29
|
+
readonly stories: readonly ReferrerInterface[];
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly url: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface UncoveredStoryInterface {
|
|
34
|
+
readonly file: string;
|
|
35
|
+
readonly project: string;
|
|
36
|
+
readonly scenarios: readonly string[];
|
|
37
|
+
readonly state: UncoveredType;
|
|
38
|
+
readonly story: string;
|
|
39
|
+
readonly url: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
export type UncoveredType = (typeof uncoveredOrder)[number];
|
|
42
|
+
interface ReferrerInterface {
|
|
43
|
+
readonly file: string;
|
|
44
|
+
readonly line: number;
|
|
45
|
+
readonly project: string;
|
|
46
|
+
readonly status: string;
|
|
47
|
+
}
|
|
48
|
+
export declare const uncoveredOrder: readonly ["off-run", "todo-only", "no-scenario"];
|
|
49
|
+
export declare const uncoveredLabels: Readonly<Record<UncoveredType, string>>;
|
|
50
|
+
export declare const liveScenarios: (ids: readonly string[], scenarios: ScenariosInterface) => ScenarioInterface[];
|
|
51
|
+
export declare const coverageOf: (classified: readonly ClassifiedInterface[], scenarios: ScenariosInterface) => Promise<CoverageInterface>;
|
|
52
|
+
export declare const pendingScenarios: (scenarios: ScenariosInterface, files: FilesType, rows: readonly LinkRowInterface[]) => Promise<PendingInterface[]>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.pendingScenarios = exports.coverageOf = exports.liveScenarios = exports.uncoveredLabels = exports.uncoveredOrder = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
const gherkin_js_1 = require("./gherkin.js");
|
|
10
|
+
const git_js_1 = require("./git.js");
|
|
11
|
+
const python_text_js_1 = require("./python-text.js");
|
|
12
|
+
const remote_js_1 = require("./remote.js");
|
|
13
|
+
const task_links_js_1 = require("./task-links.js");
|
|
14
|
+
exports.uncoveredOrder = ['off-run', 'todo-only', 'no-scenario'];
|
|
15
|
+
exports.uncoveredLabels = {
|
|
16
|
+
'no-scenario': 'без сценария',
|
|
17
|
+
'off-run': 'вне прогона',
|
|
18
|
+
'todo-only': `только долг @${config_js_1.markTodo}`,
|
|
19
|
+
};
|
|
20
|
+
const uncoveredOf = {
|
|
21
|
+
'без сценария': 'no-scenario',
|
|
22
|
+
'все сняты': 'no-scenario',
|
|
23
|
+
'с тестом': 'off-run',
|
|
24
|
+
'только долг': 'todo-only',
|
|
25
|
+
};
|
|
26
|
+
const repositoryRoot = '.';
|
|
27
|
+
const liveScenarios = (ids, scenarios) => ids
|
|
28
|
+
.filter((id) => !scenarios.removed.has(id))
|
|
29
|
+
.flatMap((id) => scenarios.ids.get(id)?.slice(0, 1) ?? []);
|
|
30
|
+
exports.liveScenarios = liveScenarios;
|
|
31
|
+
const isInRun = (story, scenarios) => (0, exports.liveScenarios)(story.ids, scenarios).some((scenario) => scenario.tags.every((tag) => !config_js_1.marks.includes(tag)));
|
|
32
|
+
const uncoveredStory = (item, location, story) => ({
|
|
33
|
+
file: story.file,
|
|
34
|
+
project: item.project.name,
|
|
35
|
+
scenarios: story.ids,
|
|
36
|
+
state: uncoveredOf[story.state],
|
|
37
|
+
story: story.key,
|
|
38
|
+
url: (0, remote_js_1.blobUrl)(location, story.file),
|
|
39
|
+
});
|
|
40
|
+
const uncoveredStories = async (item, scenarios) => {
|
|
41
|
+
const location = { commit: item.project.commit, web: await (0, remote_js_1.webUrl)(scenarios.cwd, item.project.repo) };
|
|
42
|
+
return item.stories
|
|
43
|
+
.filter((story) => story.state !== 'все сняты' && !isInRun(story, scenarios))
|
|
44
|
+
.map((story) => uncoveredStory(item, location, story));
|
|
45
|
+
};
|
|
46
|
+
const stateCounts = (uncovered) => {
|
|
47
|
+
const count = (state) => uncovered.filter((story) => story.state === state).length;
|
|
48
|
+
return { 'no-scenario': count('no-scenario'), 'off-run': count('off-run'), 'todo-only': count('todo-only') };
|
|
49
|
+
};
|
|
50
|
+
const shareOf = (covered, denominator) => (denominator === 0 ? undefined : (0, python_text_js_1.roundTenth)((100 * covered) / denominator));
|
|
51
|
+
const projectOf = (item, uncovered) => {
|
|
52
|
+
const { counts } = item;
|
|
53
|
+
const denominator = counts.e2e - counts['все сняты'];
|
|
54
|
+
const covered = denominator - uncovered.length;
|
|
55
|
+
return {
|
|
56
|
+
allRemoved: counts['все сняты'],
|
|
57
|
+
closed: item.closed,
|
|
58
|
+
codeTests: counts['code-tests'],
|
|
59
|
+
commit: item.project.commit ?? '',
|
|
60
|
+
covered,
|
|
61
|
+
denominator,
|
|
62
|
+
e2e: counts.e2e,
|
|
63
|
+
none: counts.none,
|
|
64
|
+
project: item.project.name,
|
|
65
|
+
revision: item.project.revision ?? '',
|
|
66
|
+
share: shareOf(covered, denominator),
|
|
67
|
+
unclassified: counts['без класса'],
|
|
68
|
+
uncovered: stateCounts(uncovered),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const coverageOf = async (classified, scenarios) => {
|
|
72
|
+
const rows = await Promise.all(classified.map(async (item) => ({
|
|
73
|
+
item,
|
|
74
|
+
uncovered: await uncoveredStories(item, scenarios),
|
|
75
|
+
})));
|
|
76
|
+
return {
|
|
77
|
+
projects: rows.map(({ item, uncovered }) => projectOf(item, uncovered)),
|
|
78
|
+
uncovered: rows.flatMap(({ uncovered }) => uncovered),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
exports.coverageOf = coverageOf;
|
|
82
|
+
const referrerOf = (row) => ({
|
|
83
|
+
file: row.file,
|
|
84
|
+
line: row.line,
|
|
85
|
+
project: row.project,
|
|
86
|
+
status: row.status,
|
|
87
|
+
});
|
|
88
|
+
const referrersOf = (rows) => {
|
|
89
|
+
const found = rows.flatMap((row) => [...row.text.matchAll(task_links_js_1.linkToken)]
|
|
90
|
+
.map((match) => ({ row, tag: match[0].slice(1) })));
|
|
91
|
+
const grouped = Map.groupBy(found, ({ tag }) => tag);
|
|
92
|
+
return new Map([...grouped].map(([tag, items]) => [tag, items.map(({ row }) => referrerOf(row))]));
|
|
93
|
+
};
|
|
94
|
+
const pendingIds = (context, scenario) => scenario.tags
|
|
95
|
+
.filter((tag) => context.scenarios.config.id.test(tag))
|
|
96
|
+
.slice(0, 1)
|
|
97
|
+
.filter((id) => scenario.tags.includes(config_js_1.markPending) && !context.scenarios.removed.has(id));
|
|
98
|
+
const pendingOf = (context, scenario) => {
|
|
99
|
+
const { file, location, referrers } = context;
|
|
100
|
+
const { line, title } = scenario;
|
|
101
|
+
return pendingIds(context, scenario)
|
|
102
|
+
.map((id) => ({ file, id, line, stories: referrers.get(id) ?? [], title, url: (0, remote_js_1.blobUrl)(location, file, line) }));
|
|
103
|
+
};
|
|
104
|
+
const headLocation = async (scenarios) => {
|
|
105
|
+
const head = await (0, git_js_1.git)(scenarios.cwd, repositoryRoot, ['rev-parse', 'HEAD']);
|
|
106
|
+
return { commit: (head ?? '').trim(), web: await (0, remote_js_1.webUrl)(scenarios.cwd, repositoryRoot) };
|
|
107
|
+
};
|
|
108
|
+
const pendingScenarios = async (scenarios, files, rows) => {
|
|
109
|
+
const referrers = referrersOf(rows);
|
|
110
|
+
const location = await headLocation(scenarios);
|
|
111
|
+
const pending = [...files].flatMap(([raw, text]) => {
|
|
112
|
+
const file = node_path_1.default.posix.normalize(raw);
|
|
113
|
+
const context = { file, location, referrers, scenarios };
|
|
114
|
+
return (0, gherkin_js_1.parseFeature)(text, file).flatMap((scenario) => pendingOf(context, scenario));
|
|
115
|
+
});
|
|
116
|
+
return pending.toSorted((left, right) => (0, python_text_js_1.compareText)(left.id, right.id));
|
|
117
|
+
};
|
|
118
|
+
exports.pendingScenarios = pendingScenarios;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coverageJson = void 0;
|
|
4
|
+
const coverage_data_js_1 = require("./coverage-data.js");
|
|
5
|
+
class PyFloat {
|
|
6
|
+
value;
|
|
7
|
+
constructor(value) {
|
|
8
|
+
this.value = value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const ordered = (fields) => Object.fromEntries(fields);
|
|
12
|
+
const projectJson = (item) => ordered([
|
|
13
|
+
['project', item.project],
|
|
14
|
+
['ref', item.revision],
|
|
15
|
+
['commit', item.commit],
|
|
16
|
+
['closed', item.closed],
|
|
17
|
+
['e2e', item.e2e],
|
|
18
|
+
['all_removed', item.allRemoved],
|
|
19
|
+
['code_tests', item.codeTests],
|
|
20
|
+
['none', item.none],
|
|
21
|
+
['unclassified', item.unclassified],
|
|
22
|
+
['denominator', item.denominator],
|
|
23
|
+
['covered', item.covered],
|
|
24
|
+
['share', item.share === undefined ? null : new PyFloat(item.share)],
|
|
25
|
+
['uncovered', ordered(coverage_data_js_1.uncoveredOrder.map((state) => [state, item.uncovered[state]]))],
|
|
26
|
+
]);
|
|
27
|
+
const uncoveredJson = (story) => ordered([
|
|
28
|
+
['project', story.project],
|
|
29
|
+
['story', story.story],
|
|
30
|
+
['path', story.file],
|
|
31
|
+
['url', story.url ?? null],
|
|
32
|
+
['state', story.state],
|
|
33
|
+
['scenarios', story.scenarios],
|
|
34
|
+
]);
|
|
35
|
+
const openJson = (story) => ordered([
|
|
36
|
+
['project', story.project],
|
|
37
|
+
['story', story.story],
|
|
38
|
+
['status', story.status],
|
|
39
|
+
['group', story.group],
|
|
40
|
+
['path', story.file ?? null],
|
|
41
|
+
['url', story.url ?? null],
|
|
42
|
+
['scenarios', story.scenarios],
|
|
43
|
+
]);
|
|
44
|
+
const referrerJson = (story) => ordered([
|
|
45
|
+
['project', story.project],
|
|
46
|
+
['path', story.file],
|
|
47
|
+
['line', story.line],
|
|
48
|
+
['status', story.status],
|
|
49
|
+
]);
|
|
50
|
+
const pendingJson = (scenario) => ordered([
|
|
51
|
+
['id', scenario.id],
|
|
52
|
+
['title', scenario.title],
|
|
53
|
+
['file', scenario.file],
|
|
54
|
+
['line', scenario.line],
|
|
55
|
+
['url', scenario.url ?? null],
|
|
56
|
+
['stories', scenario.stories.map((story) => referrerJson(story))],
|
|
57
|
+
]);
|
|
58
|
+
const removedJson = (row) => ordered([
|
|
59
|
+
['id', row.id],
|
|
60
|
+
['date', row.date],
|
|
61
|
+
['reason', row.reason],
|
|
62
|
+
['story', row.story],
|
|
63
|
+
['replaced_by', row.replacedBy],
|
|
64
|
+
['commit', row.commit],
|
|
65
|
+
]);
|
|
66
|
+
const pythonShape = (data) => ordered([
|
|
67
|
+
['date', data.date],
|
|
68
|
+
['period', ordered([['since', data.period.since], ['until', data.period.until]])],
|
|
69
|
+
['projects', data.projects.map((item) => projectJson(item))],
|
|
70
|
+
['uncovered', data.uncovered.map((story) => uncoveredJson(story))],
|
|
71
|
+
['open', data.open.map((story) => openJson(story))],
|
|
72
|
+
['pending', data.pending.map((scenario) => pendingJson(scenario))],
|
|
73
|
+
['removed', data.removed.map((row) => removedJson(row))],
|
|
74
|
+
['notes', data.notes],
|
|
75
|
+
['defects', data.defects],
|
|
76
|
+
]);
|
|
77
|
+
const isPlainObject = (value) => {
|
|
78
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
79
|
+
return isObject && !Array.isArray(value) && !(value instanceof PyFloat);
|
|
80
|
+
};
|
|
81
|
+
const floatText = (value) => (Number.isSafeInteger(value) ? `${value}.0` : String(value));
|
|
82
|
+
const block = (open, close, entries, indent) => (entries.length === 0
|
|
83
|
+
? `${open}${close}`
|
|
84
|
+
: `${open}\n${entries.join(',\n')}\n${indent}${close}`);
|
|
85
|
+
const scalarJson = (value) => (value instanceof PyFloat ? floatText(value.value) : JSON.stringify(value));
|
|
86
|
+
const dump = (value, indent) => {
|
|
87
|
+
const inner = `${indent} `;
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return block('[', ']', value.map((item) => `${inner}${dump(item, inner)}`), indent);
|
|
90
|
+
}
|
|
91
|
+
return isPlainObject(value) ? block('{', '}', Object.entries(value).map(([key, item]) => `${inner}${JSON.stringify(key)}: ${dump(item, inner)}`), indent) : scalarJson(value);
|
|
92
|
+
};
|
|
93
|
+
const coverageJson = (data) => `${dump(pythonShape(data), '')}\n`;
|
|
94
|
+
exports.coverageJson = coverageJson;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClassifiedInterface } from './classes.js';
|
|
2
|
+
import type { ScenariosInterface } from './classify.js';
|
|
3
|
+
export type OpenGroupType = (typeof openOrder)[number];
|
|
4
|
+
export interface OpenRowInterface {
|
|
5
|
+
readonly file: string | undefined;
|
|
6
|
+
readonly group: OpenGroupType;
|
|
7
|
+
readonly project: string;
|
|
8
|
+
readonly scenarios: readonly string[];
|
|
9
|
+
readonly status: string;
|
|
10
|
+
readonly story: string;
|
|
11
|
+
readonly url: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare const openOrder: readonly ["tested", "pending", "todo-only", "no-scenario", "code-tests", "none", "unclassified"];
|
|
14
|
+
export declare const openLabels: Readonly<Record<OpenGroupType, string>>;
|
|
15
|
+
export declare const openRows: (classified: readonly ClassifiedInterface[], scenarios: ScenariosInterface) => Promise<OpenRowInterface[]>;
|
|
16
|
+
export declare const openCounts: (rows: readonly OpenRowInterface[]) => Record<OpenGroupType, number>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openCounts = exports.openRows = exports.openLabels = exports.openOrder = void 0;
|
|
4
|
+
const acceptance_class_js_1 = require("./acceptance-class.js");
|
|
5
|
+
const config_js_1 = require("./config.js");
|
|
6
|
+
const coverage_data_js_1 = require("./coverage-data.js");
|
|
7
|
+
const remote_js_1 = require("./remote.js");
|
|
8
|
+
exports.openOrder = [
|
|
9
|
+
'tested',
|
|
10
|
+
'pending',
|
|
11
|
+
'todo-only',
|
|
12
|
+
'no-scenario',
|
|
13
|
+
'code-tests',
|
|
14
|
+
'none',
|
|
15
|
+
'unclassified',
|
|
16
|
+
];
|
|
17
|
+
exports.openLabels = {
|
|
18
|
+
'code-tests': 'проверка — тестами кода',
|
|
19
|
+
'no-scenario': 'сценария ещё нет',
|
|
20
|
+
none: 'проверять нечего',
|
|
21
|
+
pending: 'тест готов раньше реализации',
|
|
22
|
+
tested: 'тест есть',
|
|
23
|
+
'todo-only': `критерий перенесён, шагов нет (@${config_js_1.markTodo})`,
|
|
24
|
+
unclassified: 'класс не задан',
|
|
25
|
+
};
|
|
26
|
+
const e2eGroup = (story, scenarios) => {
|
|
27
|
+
const state = (0, acceptance_class_js_1.scenarioState)(story.ids, scenarios.ids, scenarios.removed);
|
|
28
|
+
if (state === 'только долг') {
|
|
29
|
+
return 'todo-only';
|
|
30
|
+
}
|
|
31
|
+
if (state !== 'с тестом') {
|
|
32
|
+
return 'no-scenario';
|
|
33
|
+
}
|
|
34
|
+
const tested = (0, coverage_data_js_1.liveScenarios)(story.ids, scenarios).filter((scenario) => !scenario.tags.includes(config_js_1.markTodo));
|
|
35
|
+
return tested.every((scenario) => scenario.tags.includes(config_js_1.markPending)) ? 'pending' : 'tested';
|
|
36
|
+
};
|
|
37
|
+
const groupOf = (story, scenarios) => {
|
|
38
|
+
const { kinds } = story;
|
|
39
|
+
if (kinds === undefined) {
|
|
40
|
+
return 'unclassified';
|
|
41
|
+
}
|
|
42
|
+
if (kinds.includes('e2e')) {
|
|
43
|
+
return e2eGroup(story, scenarios);
|
|
44
|
+
}
|
|
45
|
+
return kinds.includes('code-tests') ? 'code-tests' : 'none';
|
|
46
|
+
};
|
|
47
|
+
const projectRows = async (item, scenarios) => {
|
|
48
|
+
const location = { commit: item.project.commit, web: await (0, remote_js_1.webUrl)(scenarios.cwd, item.project.repo) };
|
|
49
|
+
return item.open.map((story) => ({
|
|
50
|
+
file: story.file,
|
|
51
|
+
group: groupOf(story, scenarios),
|
|
52
|
+
project: item.project.name,
|
|
53
|
+
scenarios: story.ids,
|
|
54
|
+
status: story.status,
|
|
55
|
+
story: story.key,
|
|
56
|
+
url: story.file === undefined ? undefined : (0, remote_js_1.blobUrl)(location, story.file),
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
const openRows = async (classified, scenarios) => {
|
|
60
|
+
const rows = await Promise.all(classified.map(async (item) => projectRows(item, scenarios)));
|
|
61
|
+
return rows.flat().toSorted((left, right) => exports.openOrder.indexOf(left.group) - exports.openOrder.indexOf(right.group));
|
|
62
|
+
};
|
|
63
|
+
exports.openRows = openRows;
|
|
64
|
+
const openCounts = (rows) => {
|
|
65
|
+
const count = (group) => rows.filter((row) => row.group === group).length;
|
|
66
|
+
return {
|
|
67
|
+
'code-tests': count('code-tests'),
|
|
68
|
+
'no-scenario': count('no-scenario'),
|
|
69
|
+
none: count('none'),
|
|
70
|
+
pending: count('pending'),
|
|
71
|
+
tested: count('tested'),
|
|
72
|
+
'todo-only': count('todo-only'),
|
|
73
|
+
unclassified: count('unclassified'),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
exports.openCounts = openCounts;
|