@featurevisor/core 0.60.1 → 0.62.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/.eslintcache +1 -1
- package/CHANGELOG.md +22 -0
- package/coverage/clover.xml +2 -2
- package/coverage/lcov-report/index.html +1 -1
- package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
- package/coverage/lcov-report/lib/builder/index.html +1 -1
- package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
- package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
- package/coverage/lcov-report/lib/tester/index.html +1 -1
- package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/src/builder/index.html +1 -1
- package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
- package/coverage/lcov-report/src/tester/index.html +1 -1
- package/lib/datasource/adapter.d.ts +3 -2
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/datasource.d.ts +5 -2
- package/lib/datasource/datasource.js +10 -2
- package/lib/datasource/datasource.js.map +1 -1
- package/lib/datasource/filesystemAdapter.d.ts +11 -3
- package/lib/datasource/filesystemAdapter.js +127 -1
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/site/exportSite.js +4 -3
- package/lib/site/exportSite.js.map +1 -1
- package/lib/site/generateHistory.d.ts +1 -1
- package/lib/site/generateHistory.js +58 -66
- package/lib/site/generateHistory.js.map +1 -1
- package/lib/tester/testFeature.d.ts +3 -1
- package/lib/tester/testFeature.js +10 -6
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/tester/testProject.d.ts +6 -1
- package/lib/tester/testProject.js +17 -6
- package/lib/tester/testProject.js.map +1 -1
- package/lib/tester/testSegment.d.ts +1 -1
- package/lib/tester/testSegment.js +6 -3
- package/lib/tester/testSegment.js.map +1 -1
- package/lib/{utils.js → utils/extractKeys.js} +1 -1
- package/lib/utils/extractKeys.js.map +1 -0
- package/lib/utils/git.d.ts +6 -0
- package/lib/utils/git.js +121 -0
- package/lib/utils/git.js.map +1 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +19 -0
- package/lib/utils/index.js.map +1 -0
- package/package.json +5 -5
- package/src/datasource/adapter.ts +17 -3
- package/src/datasource/datasource.ts +12 -2
- package/src/datasource/filesystemAdapter.ts +144 -4
- package/src/site/exportSite.ts +1 -3
- package/src/site/generateHistory.ts +3 -77
- package/src/tester/testFeature.ts +14 -5
- package/src/tester/testProject.ts +29 -6
- package/src/tester/testSegment.ts +11 -3
- package/src/utils/git.ts +129 -0
- package/src/utils/index.ts +2 -0
- package/lib/utils.js.map +0 -1
- /package/lib/{utils.d.ts → utils/extractKeys.d.ts} +0 -0
- /package/src/{utils.ts → utils/extractKeys.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featurevisor/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "Core package of Featurevisor for Node.js usage",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@featurevisor/sdk": "^0.
|
|
48
|
-
"@featurevisor/site": "^0.
|
|
49
|
-
"@featurevisor/types": "^0.
|
|
47
|
+
"@featurevisor/sdk": "^0.62.0",
|
|
48
|
+
"@featurevisor/site": "^0.62.0",
|
|
49
|
+
"@featurevisor/types": "^0.62.0",
|
|
50
50
|
"axios": "^1.3.4",
|
|
51
51
|
"joi": "^17.8.3",
|
|
52
52
|
"js-yaml": "^4.1.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"@types/js-yaml": "^4.0.5",
|
|
58
58
|
"@types/tar": "^6.1.4"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "64ff2edd5bbc0deedfc69aa2ada36b36e5e0c13a"
|
|
61
61
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
DatafileContent,
|
|
3
|
+
EnvironmentKey,
|
|
4
|
+
ExistingState,
|
|
5
|
+
HistoryEntry,
|
|
6
|
+
Commit,
|
|
7
|
+
CommitHash,
|
|
8
|
+
EntityType,
|
|
9
|
+
} from "@featurevisor/types";
|
|
4
10
|
|
|
5
11
|
export interface DatafileOptions {
|
|
6
12
|
environment: EnvironmentKey;
|
|
@@ -22,4 +28,12 @@ export abstract class Adapter {
|
|
|
22
28
|
// datafile
|
|
23
29
|
abstract readDatafile(options: DatafileOptions): Promise<DatafileContent>;
|
|
24
30
|
abstract writeDatafile(datafileContent: DatafileContent, options: DatafileOptions): Promise<void>;
|
|
31
|
+
|
|
32
|
+
// history
|
|
33
|
+
abstract listHistoryEntries(entityType?: EntityType, entityKey?: string): Promise<HistoryEntry[]>;
|
|
34
|
+
abstract readCommit(
|
|
35
|
+
commit: CommitHash,
|
|
36
|
+
entityType?: EntityType,
|
|
37
|
+
entityKey?: string,
|
|
38
|
+
): Promise<Commit>;
|
|
25
39
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
SegmentKey,
|
|
11
11
|
AttributeKey,
|
|
12
12
|
DatafileContent,
|
|
13
|
+
EntityType,
|
|
13
14
|
} from "@featurevisor/types";
|
|
14
15
|
|
|
15
16
|
import { ProjectConfig, CustomParser } from "../config";
|
|
@@ -19,8 +20,8 @@ import { Adapter, DatafileOptions } from "./adapter";
|
|
|
19
20
|
export class Datasource {
|
|
20
21
|
private adapter: Adapter;
|
|
21
22
|
|
|
22
|
-
constructor(private config: ProjectConfig) {
|
|
23
|
-
this.adapter = new config.adapter(config);
|
|
23
|
+
constructor(private config: ProjectConfig, private rootDirectoryPath?: string) {
|
|
24
|
+
this.adapter = new config.adapter(config, rootDirectoryPath);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// @TODO: only site generator needs it, find a way to get it out of here later
|
|
@@ -187,4 +188,13 @@ export class Datasource {
|
|
|
187
188
|
getTestSpecName(testKey: string) {
|
|
188
189
|
return `${testKey}.${this.getExtension()}`;
|
|
189
190
|
}
|
|
191
|
+
|
|
192
|
+
// history
|
|
193
|
+
listHistoryEntries(entityType?: EntityType, entityKey?: string) {
|
|
194
|
+
return this.adapter.listHistoryEntries(entityType, entityKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
readCommit(commitHash: string, entityType?: EntityType, entityKey?: string) {
|
|
198
|
+
return this.adapter.readCommit(commitHash, entityType, entityKey);
|
|
199
|
+
}
|
|
190
200
|
}
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
|
|
4
5
|
import * as mkdirp from "mkdirp";
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
ExistingState,
|
|
9
|
+
EnvironmentKey,
|
|
10
|
+
DatafileContent,
|
|
11
|
+
EntityType,
|
|
12
|
+
HistoryEntry,
|
|
13
|
+
Commit,
|
|
14
|
+
CommitHash,
|
|
15
|
+
HistoryEntity,
|
|
16
|
+
} from "@featurevisor/types";
|
|
17
|
+
|
|
18
|
+
import { Adapter, DatafileOptions } from "./adapter";
|
|
9
19
|
import { ProjectConfig, CustomParser } from "../config";
|
|
20
|
+
import { getCommit } from "../utils/git";
|
|
21
|
+
|
|
22
|
+
const commitRegex = /^commit (\w+)\nAuthor: (.+) <(.+)>\nDate: (.+)\n\n(.+)/gm; // eslint-disable-line
|
|
10
23
|
|
|
11
24
|
export function getExistingStateFilePath(
|
|
12
25
|
projectConfig: ProjectConfig,
|
|
@@ -18,7 +31,7 @@ export function getExistingStateFilePath(
|
|
|
18
31
|
export class FilesystemAdapter extends Adapter {
|
|
19
32
|
private parser: CustomParser;
|
|
20
33
|
|
|
21
|
-
constructor(private config: ProjectConfig) {
|
|
34
|
+
constructor(private config: ProjectConfig, private rootDirectoryPath?: string) {
|
|
22
35
|
super();
|
|
23
36
|
|
|
24
37
|
this.parser = config.parser as CustomParser;
|
|
@@ -161,4 +174,131 @@ export class FilesystemAdapter extends Adapter {
|
|
|
161
174
|
const shortPath = outputFilePath.replace(root + path.sep, "");
|
|
162
175
|
console.log(` Datafile generated: ${shortPath}`);
|
|
163
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* History
|
|
180
|
+
*/
|
|
181
|
+
getRawHistory(pathPatterns: string[]) {
|
|
182
|
+
const gitPaths = pathPatterns.join(" ");
|
|
183
|
+
|
|
184
|
+
const logCommand = `git log --name-only --pretty=format:"%h|%an|%aI" --relative --no-merges -- ${gitPaths}`;
|
|
185
|
+
const fullCommand = `(cd ${this.rootDirectoryPath} && ${logCommand})`;
|
|
186
|
+
|
|
187
|
+
return execSync(fullCommand, { encoding: "utf8" }).toString();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getPathPatterns(entityType?: EntityType, entityKey?: string): string[] {
|
|
191
|
+
let pathPatterns: string[] = [];
|
|
192
|
+
|
|
193
|
+
if (entityType && entityKey) {
|
|
194
|
+
pathPatterns = [this.getEntityPath(entityType, entityKey)];
|
|
195
|
+
} else if (entityType) {
|
|
196
|
+
if (entityType === "attribute") {
|
|
197
|
+
pathPatterns = [this.config.attributesDirectoryPath];
|
|
198
|
+
} else if (entityType === "segment") {
|
|
199
|
+
pathPatterns = [this.config.segmentsDirectoryPath];
|
|
200
|
+
} else if (entityType === "feature") {
|
|
201
|
+
pathPatterns = [this.config.featuresDirectoryPath];
|
|
202
|
+
} else if (entityType === "group") {
|
|
203
|
+
pathPatterns = [this.config.groupsDirectoryPath];
|
|
204
|
+
} else if (entityType === "test") {
|
|
205
|
+
pathPatterns = [this.config.testsDirectoryPath];
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
pathPatterns = [
|
|
209
|
+
this.config.featuresDirectoryPath,
|
|
210
|
+
this.config.attributesDirectoryPath,
|
|
211
|
+
this.config.segmentsDirectoryPath,
|
|
212
|
+
this.config.groupsDirectoryPath,
|
|
213
|
+
this.config.testsDirectoryPath,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return pathPatterns.map((p) => p.replace((this.rootDirectoryPath as string) + path.sep, ""));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async listHistoryEntries(entityType?: EntityType, entityKey?: string): Promise<HistoryEntry[]> {
|
|
221
|
+
const pathPatterns = this.getPathPatterns(entityType, entityKey);
|
|
222
|
+
const rawHistory = this.getRawHistory(pathPatterns);
|
|
223
|
+
|
|
224
|
+
const fullHistory: HistoryEntry[] = [];
|
|
225
|
+
const blocks = rawHistory.split("\n\n");
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
228
|
+
const block = blocks[i];
|
|
229
|
+
|
|
230
|
+
if (block.length === 0) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const lines = block.split("\n");
|
|
235
|
+
|
|
236
|
+
const commitLine = lines[0];
|
|
237
|
+
const [commitHash, author, timestamp] = commitLine.split("|");
|
|
238
|
+
|
|
239
|
+
const entities: HistoryEntity[] = [];
|
|
240
|
+
|
|
241
|
+
const filePathLines = lines.slice(1);
|
|
242
|
+
for (let j = 0; j < filePathLines.length; j++) {
|
|
243
|
+
const relativePath = filePathLines[j];
|
|
244
|
+
const absolutePath = path.join(this.rootDirectoryPath as string, relativePath);
|
|
245
|
+
const fileName = absolutePath.split(path.sep).pop() as string;
|
|
246
|
+
const relativeDir = path.dirname(absolutePath);
|
|
247
|
+
|
|
248
|
+
const key = fileName.replace("." + this.parser.extension, "");
|
|
249
|
+
|
|
250
|
+
let type: EntityType = "attribute";
|
|
251
|
+
if (relativeDir === this.config.attributesDirectoryPath) {
|
|
252
|
+
type = "attribute";
|
|
253
|
+
} else if (relativeDir === this.config.segmentsDirectoryPath) {
|
|
254
|
+
type = "segment";
|
|
255
|
+
} else if (relativeDir === this.config.featuresDirectoryPath) {
|
|
256
|
+
type = "feature";
|
|
257
|
+
} else if (relativeDir === this.config.groupsDirectoryPath) {
|
|
258
|
+
type = "group";
|
|
259
|
+
} else if (relativeDir === this.config.testsDirectoryPath) {
|
|
260
|
+
type = "test";
|
|
261
|
+
} else {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
entities.push({
|
|
266
|
+
type,
|
|
267
|
+
key,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (entities.length === 0) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fullHistory.push({
|
|
276
|
+
commit: commitHash,
|
|
277
|
+
author,
|
|
278
|
+
timestamp,
|
|
279
|
+
entities,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return fullHistory;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async readCommit(
|
|
287
|
+
commitHash: CommitHash,
|
|
288
|
+
entityType?: EntityType,
|
|
289
|
+
entityKey?: string,
|
|
290
|
+
): Promise<Commit> {
|
|
291
|
+
const pathPatterns = this.getPathPatterns(entityType, entityKey);
|
|
292
|
+
const gitPaths = pathPatterns.join(" ");
|
|
293
|
+
const logCommand = `git show ${commitHash} --relative -- ${gitPaths}`;
|
|
294
|
+
const fullCommand = `(cd ${this.rootDirectoryPath} && ${logCommand})`;
|
|
295
|
+
|
|
296
|
+
const gitShowOutput = execSync(fullCommand, { encoding: "utf8" }).toString();
|
|
297
|
+
const commit = getCommit(gitShowOutput, {
|
|
298
|
+
rootDirectoryPath: this.rootDirectoryPath as string,
|
|
299
|
+
projectConfig: this.config,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return commit;
|
|
303
|
+
}
|
|
164
304
|
}
|
package/src/site/exportSite.ts
CHANGED
|
@@ -24,7 +24,7 @@ export async function exportSite(deps: Dependencies) {
|
|
|
24
24
|
console.log("Site dist copied to:", projectConfig.siteExportDirectoryPath);
|
|
25
25
|
|
|
26
26
|
// generate history
|
|
27
|
-
const fullHistory = generateHistory(deps);
|
|
27
|
+
const fullHistory = await generateHistory(deps);
|
|
28
28
|
|
|
29
29
|
// site search index
|
|
30
30
|
const repoDetails = getRepoDetails();
|
|
@@ -40,7 +40,5 @@ export async function exportSite(deps: Dependencies) {
|
|
|
40
40
|
{ recursive: true },
|
|
41
41
|
);
|
|
42
42
|
|
|
43
|
-
// @TODO: replace placeoholders in index.html
|
|
44
|
-
|
|
45
43
|
return hasError;
|
|
46
44
|
}
|
|
@@ -1,88 +1,14 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
3
|
|
|
5
4
|
import { HistoryEntry } from "@featurevisor/types";
|
|
6
|
-
|
|
7
|
-
import { getRelativePaths } from "./getRelativePaths";
|
|
8
5
|
import { Dependencies } from "../dependencies";
|
|
9
6
|
|
|
10
|
-
export function generateHistory(deps: Dependencies): HistoryEntry[] {
|
|
11
|
-
const {
|
|
7
|
+
export async function generateHistory(deps: Dependencies): Promise<HistoryEntry[]> {
|
|
8
|
+
const { projectConfig, datasource } = deps;
|
|
12
9
|
|
|
13
10
|
try {
|
|
14
|
-
|
|
15
|
-
const rawHistoryFilePath = path.join(projectConfig.siteExportDirectoryPath, "history-raw.txt");
|
|
16
|
-
|
|
17
|
-
const { relativeFeaturesPath, relativeSegmentsPath, relativeAttributesPath } = getRelativePaths(
|
|
18
|
-
rootDirectoryPath,
|
|
19
|
-
projectConfig,
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
const separator = "|";
|
|
23
|
-
|
|
24
|
-
const cmd = `git log --name-only --pretty=format:"%h${separator}%an${separator}%aI" --no-merges --relative -- ${relativeFeaturesPath} ${relativeSegmentsPath} ${relativeAttributesPath} > ${rawHistoryFilePath}`;
|
|
25
|
-
execSync(cmd);
|
|
26
|
-
|
|
27
|
-
console.log(`History (raw) generated at: ${rawHistoryFilePath}`);
|
|
28
|
-
|
|
29
|
-
// structured history
|
|
30
|
-
const rawHistory = fs.readFileSync(rawHistoryFilePath, "utf8");
|
|
31
|
-
|
|
32
|
-
const fullHistory: HistoryEntry[] = [];
|
|
33
|
-
|
|
34
|
-
let entry: HistoryEntry = {
|
|
35
|
-
commit: "",
|
|
36
|
-
author: "",
|
|
37
|
-
timestamp: "",
|
|
38
|
-
entities: [],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
rawHistory.split("\n").forEach((line, index) => {
|
|
42
|
-
if (index === 0 && line.length === 0) {
|
|
43
|
-
// no history found
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (index > 0 && line.length === 0) {
|
|
48
|
-
// commit finished
|
|
49
|
-
fullHistory.push(entry);
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (line.indexOf(separator) > -1) {
|
|
55
|
-
// commit line
|
|
56
|
-
const parts = line.split("|");
|
|
57
|
-
|
|
58
|
-
entry = {
|
|
59
|
-
commit: parts[0],
|
|
60
|
-
author: parts[1],
|
|
61
|
-
timestamp: parts[2],
|
|
62
|
-
entities: [],
|
|
63
|
-
};
|
|
64
|
-
} else {
|
|
65
|
-
// file line
|
|
66
|
-
const lineSplit = line.split(path.sep);
|
|
67
|
-
const fileName = lineSplit.pop() as string;
|
|
68
|
-
const relativeDir = lineSplit.join(path.sep);
|
|
69
|
-
|
|
70
|
-
const key = fileName.replace("." + datasource.getExtension(), "");
|
|
71
|
-
|
|
72
|
-
let type = "feature" as "attribute" | "segment" | "feature";
|
|
73
|
-
|
|
74
|
-
if (relativeDir === relativeSegmentsPath) {
|
|
75
|
-
type = "segment";
|
|
76
|
-
} else if (relativeDir === relativeAttributesPath) {
|
|
77
|
-
type = "attribute";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
entry.entities.push({
|
|
81
|
-
type,
|
|
82
|
-
key,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
});
|
|
11
|
+
const fullHistory = await datasource.listHistoryEntries();
|
|
86
12
|
|
|
87
13
|
const fullHistoryFilePath = path.join(
|
|
88
14
|
projectConfig.siteExportDirectoryPath,
|
|
@@ -14,6 +14,8 @@ export async function testFeature(
|
|
|
14
14
|
datasource: Datasource,
|
|
15
15
|
projectConfig: ProjectConfig,
|
|
16
16
|
test: TestFeature,
|
|
17
|
+
options: { verbose?: boolean } = {},
|
|
18
|
+
patterns,
|
|
17
19
|
): Promise<boolean> {
|
|
18
20
|
let hasError = false;
|
|
19
21
|
const featureKey = test.feature;
|
|
@@ -22,9 +24,15 @@ export async function testFeature(
|
|
|
22
24
|
|
|
23
25
|
for (let aIndex = 0; aIndex < test.assertions.length; aIndex++) {
|
|
24
26
|
const assertion = test.assertions[aIndex];
|
|
25
|
-
const description =
|
|
27
|
+
const description = ` Assertion #${aIndex + 1}: (${assertion.environment}) ${
|
|
28
|
+
assertion.description || `at ${assertion.at}%`
|
|
29
|
+
}`;
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
if (patterns.assertionPattern && !patterns.assertionPattern.test(description)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(description);
|
|
28
36
|
|
|
29
37
|
const requiredChain = await datasource.getRequiredFeaturesChain(test.feature);
|
|
30
38
|
const featuresToInclude = Array.from(requiredChain);
|
|
@@ -47,11 +55,12 @@ export async function testFeature(
|
|
|
47
55
|
configureBucketValue: () => {
|
|
48
56
|
return assertion.at * (MAX_BUCKETED_NUMBER / 100);
|
|
49
57
|
},
|
|
50
|
-
// logger: createLogger({
|
|
51
|
-
// levels: ["debug", "info", "warn", "error"],
|
|
52
|
-
// }),
|
|
53
58
|
});
|
|
54
59
|
|
|
60
|
+
if (options.verbose) {
|
|
61
|
+
sdk.setLogLevels(["debug", "info", "warn", "error"]);
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
// isEnabled
|
|
56
65
|
if ("expectedToBeEnabled" in assertion) {
|
|
57
66
|
const isEnabled = sdk.isEnabled(featureKey, assertion.context);
|
|
@@ -7,7 +7,16 @@ import { testFeature } from "./testFeature";
|
|
|
7
7
|
import { CLI_FORMAT_BOLD, CLI_FORMAT_GREEN, CLI_FORMAT_RED } from "./cliFormat";
|
|
8
8
|
import { Dependencies } from "../dependencies";
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export interface TestProjectOptions {
|
|
11
|
+
keyPattern?: string;
|
|
12
|
+
assertionPattern?: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function testProject(
|
|
17
|
+
deps: Dependencies,
|
|
18
|
+
options: TestProjectOptions = {},
|
|
19
|
+
): Promise<boolean> {
|
|
11
20
|
const { rootDirectoryPath, projectConfig, datasource } = deps;
|
|
12
21
|
|
|
13
22
|
let hasError = false;
|
|
@@ -28,19 +37,27 @@ export async function testProject(deps: Dependencies): Promise<boolean> {
|
|
|
28
37
|
return hasError;
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
const patterns = {
|
|
41
|
+
keyPattern: options.keyPattern ? new RegExp(options.keyPattern) : undefined,
|
|
42
|
+
assertionPattern: options.assertionPattern ? new RegExp(options.assertionPattern) : undefined,
|
|
43
|
+
};
|
|
44
|
+
|
|
31
45
|
for (const testFile of testFiles) {
|
|
32
46
|
const testFilePath = datasource.getTestSpecName(testFile);
|
|
33
47
|
|
|
34
|
-
console.log("");
|
|
35
|
-
console.log(CLI_FORMAT_BOLD, `Testing: ${testFilePath.replace(rootDirectoryPath, "")}`);
|
|
36
|
-
|
|
37
48
|
const t = await datasource.readTest(testFile);
|
|
38
49
|
|
|
39
50
|
if ((t as TestSegment).segment) {
|
|
40
51
|
// segment testing
|
|
41
52
|
const test = t as TestSegment;
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
if (patterns.keyPattern && !patterns.keyPattern.test(test.segment)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(CLI_FORMAT_BOLD, `\nTesting: ${testFilePath.replace(rootDirectoryPath, "")}`);
|
|
59
|
+
|
|
60
|
+
const segmentHasError = await testSegment(datasource, test, patterns);
|
|
44
61
|
|
|
45
62
|
if (segmentHasError) {
|
|
46
63
|
hasError = true;
|
|
@@ -49,7 +66,13 @@ export async function testProject(deps: Dependencies): Promise<boolean> {
|
|
|
49
66
|
// feature testing
|
|
50
67
|
const test = t as TestFeature;
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
if (patterns.keyPattern && !patterns.keyPattern.test(test.feature)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(CLI_FORMAT_BOLD, `\nTesting: ${testFilePath.replace(rootDirectoryPath, "")}`);
|
|
74
|
+
|
|
75
|
+
const featureHasError = await testFeature(datasource, projectConfig, test, options, patterns);
|
|
53
76
|
|
|
54
77
|
if (featureHasError) {
|
|
55
78
|
hasError = true;
|
|
@@ -5,7 +5,11 @@ import { Datasource } from "../datasource";
|
|
|
5
5
|
|
|
6
6
|
import { CLI_FORMAT_BOLD, CLI_FORMAT_RED } from "./cliFormat";
|
|
7
7
|
|
|
8
|
-
export async function testSegment(
|
|
8
|
+
export async function testSegment(
|
|
9
|
+
datasource: Datasource,
|
|
10
|
+
test: TestSegment,
|
|
11
|
+
patterns,
|
|
12
|
+
): Promise<boolean> {
|
|
9
13
|
let hasError = false;
|
|
10
14
|
|
|
11
15
|
const segmentKey = test.segment;
|
|
@@ -25,9 +29,13 @@ export async function testSegment(datasource: Datasource, test: TestSegment): Pr
|
|
|
25
29
|
const conditions = parsedSegment.conditions as Condition | Condition[];
|
|
26
30
|
|
|
27
31
|
test.assertions.forEach(function (assertion, aIndex) {
|
|
28
|
-
const description = assertion.description || `#${aIndex + 1}`;
|
|
32
|
+
const description = ` Assertion #${aIndex + 1}: ${assertion.description || `#${aIndex + 1}`}`;
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
if (patterns.assertionPattern && !patterns.assertionPattern.test(description)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(description);
|
|
31
39
|
|
|
32
40
|
const expected = assertion.expectedToMatch;
|
|
33
41
|
const actual = allConditionsAreMatched(conditions, assertion.context);
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
|
|
3
|
+
import { Commit, EntityDiff, EntityType } from "@featurevisor/types";
|
|
4
|
+
|
|
5
|
+
import { CustomParser, ProjectConfig } from "../config";
|
|
6
|
+
|
|
7
|
+
function parseGitCommitShowOutput(gitShowOutput: string) {
|
|
8
|
+
const result = {
|
|
9
|
+
hash: "",
|
|
10
|
+
author: "",
|
|
11
|
+
date: "",
|
|
12
|
+
message: "",
|
|
13
|
+
diffs: {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const lines = gitShowOutput.split("\n");
|
|
17
|
+
let currentFile = "";
|
|
18
|
+
let parsingDiffs = false;
|
|
19
|
+
let parsingMessage = false;
|
|
20
|
+
|
|
21
|
+
lines.forEach((line) => {
|
|
22
|
+
if (line.startsWith("commit ")) {
|
|
23
|
+
result.hash = line.replace("commit ", "").trim();
|
|
24
|
+
} else if (line.startsWith("Author:")) {
|
|
25
|
+
result.author = line.replace("Author:", "").trim();
|
|
26
|
+
parsingMessage = false;
|
|
27
|
+
} else if (line.startsWith("Date:")) {
|
|
28
|
+
result.date = line.replace("Date:", "").trim();
|
|
29
|
+
parsingMessage = true; // Start parsing commit message
|
|
30
|
+
} else if (line.startsWith("diff --git")) {
|
|
31
|
+
if (currentFile) {
|
|
32
|
+
// Finish capturing the previous file's diff
|
|
33
|
+
parsingDiffs = false;
|
|
34
|
+
}
|
|
35
|
+
currentFile = line.split(" ")[2].substring(2);
|
|
36
|
+
result.diffs[currentFile] = line + "\n"; // Include the diff --git line
|
|
37
|
+
parsingDiffs = true;
|
|
38
|
+
parsingMessage = false; // Stop parsing commit message
|
|
39
|
+
} else if (parsingDiffs) {
|
|
40
|
+
result.diffs[currentFile] += line + "\n";
|
|
41
|
+
} else if (parsingMessage && line.trim() !== "") {
|
|
42
|
+
result.message += line.trim() + "\n"; // Capture commit message
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function analyzeFileChange(diff) {
|
|
50
|
+
let status = "updated"; // Default to 'updated'
|
|
51
|
+
|
|
52
|
+
// Check for file creation or deletion
|
|
53
|
+
if (diff.includes("new file mode")) {
|
|
54
|
+
status = "created";
|
|
55
|
+
} else if (diff.includes("deleted file mode")) {
|
|
56
|
+
status = "deleted";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCommit(
|
|
63
|
+
gitShowOutput: string,
|
|
64
|
+
options: { rootDirectoryPath: string; projectConfig: ProjectConfig },
|
|
65
|
+
): Commit {
|
|
66
|
+
const parsed = parseGitCommitShowOutput(gitShowOutput);
|
|
67
|
+
const { rootDirectoryPath, projectConfig } = options;
|
|
68
|
+
|
|
69
|
+
const commit: Commit = {
|
|
70
|
+
hash: parsed.hash,
|
|
71
|
+
author: parsed.author,
|
|
72
|
+
timestamp: parsed.date,
|
|
73
|
+
entities: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
Object.keys(parsed.diffs).forEach((file) => {
|
|
77
|
+
const diff = parsed.diffs[file];
|
|
78
|
+
const status = analyzeFileChange(diff);
|
|
79
|
+
|
|
80
|
+
const absolutePath = path.join(rootDirectoryPath, file);
|
|
81
|
+
const relativeDir = path.dirname(absolutePath);
|
|
82
|
+
|
|
83
|
+
// get entity type
|
|
84
|
+
let type: EntityType = "attribute";
|
|
85
|
+
if (relativeDir === projectConfig.attributesDirectoryPath) {
|
|
86
|
+
type = "attribute";
|
|
87
|
+
} else if (relativeDir === projectConfig.segmentsDirectoryPath) {
|
|
88
|
+
type = "segment";
|
|
89
|
+
} else if (relativeDir === projectConfig.featuresDirectoryPath) {
|
|
90
|
+
type = "feature";
|
|
91
|
+
} else if (relativeDir === projectConfig.groupsDirectoryPath) {
|
|
92
|
+
type = "group";
|
|
93
|
+
} else if (relativeDir === projectConfig.testsDirectoryPath) {
|
|
94
|
+
type = "test";
|
|
95
|
+
} else {
|
|
96
|
+
// unknown type
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// get entity key
|
|
101
|
+
const fileName = absolutePath.split(path.sep).pop() as string;
|
|
102
|
+
const extensionWithDot = "." + (projectConfig.parser as CustomParser).extension;
|
|
103
|
+
|
|
104
|
+
if (!fileName.endsWith(extensionWithDot)) {
|
|
105
|
+
// unknown extension
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const key = fileName.replace(extensionWithDot, "");
|
|
110
|
+
|
|
111
|
+
const entityDiff: EntityDiff = {
|
|
112
|
+
type,
|
|
113
|
+
key,
|
|
114
|
+
content: diff,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (status === "created") {
|
|
118
|
+
entityDiff.created = true;
|
|
119
|
+
} else if (status === "deleted") {
|
|
120
|
+
entityDiff.deleted = true;
|
|
121
|
+
} else {
|
|
122
|
+
entityDiff.updated = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
commit.entities.push(entityDiff);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return commit;
|
|
129
|
+
}
|
package/lib/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AAEA,SAAgB,mCAAmC,CACjD,QAAuC;IAEvC,IAAM,MAAM,GAAG,IAAI,GAAG,EAAc,CAAC;IAErC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;QAC3B,QAAQ,CAAC,OAAO,CAAC,UAAC,OAAO;YACvB,mCAAmC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,UAAC,UAAU;gBAC9D,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;KACJ;IAED,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE;QAChC,IAAI,KAAK,IAAI,QAAQ,EAAE;YACrB,mCAAmC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAC,UAAU;gBACnE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;SACJ;QAED,IAAI,IAAI,IAAI,QAAQ,EAAE;YACpB,mCAAmC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAC,UAAU;gBAClE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;SACJ;QAED,IAAI,KAAK,IAAI,QAAQ,EAAE;YACrB,mCAAmC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAC,UAAU;gBACnE,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;SACJ;KACF;IAED,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE;QAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;KACtB;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAtCD,kFAsCC;AAED,SAAgB,kCAAkC,CAChD,UAAmC;IAEnC,IAAM,MAAM,GAAG,IAAI,GAAG,EAAgB,CAAC;IAEvC,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;QAC7B,UAAU,CAAC,OAAO,CAAC,UAAC,SAAS;YAC3B,kCAAkC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,UAAC,YAAY;gBACjE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC3B,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;KACJ;IAED,IAAI,WAAW,IAAI,UAAU,EAAE;QAC7B,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;KAClC;IAED,IAAI,KAAK,IAAI,UAAU,EAAE;QACvB,kCAAkC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAC,YAAY;YACtE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;KACJ;IAED,IAAI,IAAI,IAAI,UAAU,EAAE;QACtB,kCAAkC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAC,YAAY;YACrE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;KACJ;IAED,IAAI,KAAK,IAAI,UAAU,EAAE;QACvB,kCAAkC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAC,YAAY;YACtE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;KACJ;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AApCD,gFAoCC"}
|
|
File without changes
|
|
File without changes
|