@featurevisor/core 2.5.0 → 2.6.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 +11 -0
- package/coverage/clover.xml +444 -78
- package/coverage/coverage-final.json +9 -3
- package/coverage/lcov-report/index.html +42 -42
- package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
- package/coverage/lcov-report/lib/builder/buildScopedConditions.js.html +373 -0
- package/coverage/lcov-report/lib/builder/buildScopedDatafile.js.html +403 -0
- package/coverage/lcov-report/lib/builder/buildScopedSegments.js.html +379 -0
- package/coverage/lcov-report/lib/builder/index.html +53 -8
- package/coverage/lcov-report/lib/builder/revision.js.html +1 -1
- package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
- package/coverage/lcov-report/lib/list/index.html +14 -14
- package/coverage/lcov-report/lib/list/matrix.js.html +23 -8
- package/coverage/lcov-report/lib/tester/helpers.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/buildScopedConditions.ts.html +487 -0
- package/coverage/lcov-report/src/builder/buildScopedDatafile.ts.html +604 -0
- package/coverage/lcov-report/src/builder/buildScopedSegments.ts.html +544 -0
- package/coverage/lcov-report/src/builder/index.html +55 -10
- package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/src/builder/traffic.ts.html +3 -3
- package/coverage/lcov-report/src/list/index.html +14 -14
- package/coverage/lcov-report/src/list/matrix.ts.html +33 -12
- package/coverage/lcov-report/src/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/src/tester/index.html +1 -1
- package/coverage/lcov.info +810 -129
- package/jest.config.js +8 -0
- package/lib/builder/buildDatafile.d.ts +10 -0
- package/lib/builder/buildDatafile.js +27 -0
- package/lib/builder/buildDatafile.js.map +1 -1
- package/lib/builder/buildProject.d.ts +2 -0
- package/lib/builder/buildProject.js +38 -4
- package/lib/builder/buildProject.js.map +1 -1
- package/lib/builder/buildScopedConditions.d.ts +5 -0
- package/lib/builder/buildScopedConditions.js +97 -0
- package/lib/builder/buildScopedConditions.js.map +1 -0
- package/lib/builder/buildScopedConditions.spec.d.ts +1 -0
- package/lib/builder/buildScopedConditions.spec.js +2167 -0
- package/lib/builder/buildScopedConditions.spec.js.map +1 -0
- package/lib/builder/buildScopedDatafile.d.ts +2 -0
- package/lib/builder/buildScopedDatafile.js +107 -0
- package/lib/builder/buildScopedDatafile.js.map +1 -0
- package/lib/builder/buildScopedDatafile.spec.d.ts +1 -0
- package/lib/builder/buildScopedDatafile.spec.js +1988 -0
- package/lib/builder/buildScopedDatafile.spec.js.map +1 -0
- package/lib/builder/buildScopedSegments.d.ts +5 -0
- package/lib/builder/buildScopedSegments.js +99 -0
- package/lib/builder/buildScopedSegments.js.map +1 -0
- package/lib/builder/buildScopedSegments.spec.d.ts +1 -0
- package/lib/builder/buildScopedSegments.spec.js +1062 -0
- package/lib/builder/buildScopedSegments.spec.js.map +1 -0
- package/lib/builder/index.d.ts +1 -0
- package/lib/builder/index.js +1 -0
- package/lib/builder/index.js.map +1 -1
- package/lib/config/projectConfig.d.ts +9 -1
- package/lib/config/projectConfig.js +1 -0
- package/lib/config/projectConfig.js.map +1 -1
- package/lib/datasource/adapter.d.ts +3 -1
- package/lib/datasource/adapter.js.map +1 -1
- package/lib/datasource/filesystemAdapter.js +3 -1
- package/lib/datasource/filesystemAdapter.js.map +1 -1
- package/lib/linter/testSchema.d.ts +5 -0
- package/lib/linter/testSchema.js +8 -0
- package/lib/linter/testSchema.js.map +1 -1
- package/lib/list/matrix.js +5 -0
- package/lib/list/matrix.js.map +1 -1
- package/lib/tester/testFeature.d.ts +5 -3
- package/lib/tester/testFeature.js +34 -9
- package/lib/tester/testFeature.js.map +1 -1
- package/lib/tester/testProject.d.ts +3 -2
- package/lib/tester/testProject.js +40 -6
- package/lib/tester/testProject.js.map +1 -1
- package/package.json +5 -5
- package/src/builder/buildDatafile.ts +47 -0
- package/src/builder/buildProject.ts +58 -3
- package/src/builder/buildScopedConditions.spec.ts +2659 -0
- package/src/builder/buildScopedConditions.ts +134 -0
- package/src/builder/buildScopedDatafile.spec.ts +2236 -0
- package/src/builder/buildScopedDatafile.ts +173 -0
- package/src/builder/buildScopedSegments.spec.ts +1573 -0
- package/src/builder/buildScopedSegments.ts +153 -0
- package/src/builder/index.ts +1 -0
- package/src/config/projectConfig.ts +11 -1
- package/src/datasource/adapter.ts +4 -1
- package/src/datasource/filesystemAdapter.ts +4 -1
- package/src/linter/testSchema.ts +12 -0
- package/src/list/matrix.ts +7 -0
- package/src/tester/testFeature.ts +50 -16
- package/src/tester/testProject.ts +68 -8
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GroupSegment,
|
|
3
|
+
AndGroupSegment,
|
|
4
|
+
OrGroupSegment,
|
|
5
|
+
NotGroupSegment,
|
|
6
|
+
Context,
|
|
7
|
+
} from "@featurevisor/types";
|
|
8
|
+
import type { DatafileReader } from "@featurevisor/sdk";
|
|
9
|
+
|
|
10
|
+
export function buildScopedSegments(
|
|
11
|
+
datafileReader: DatafileReader,
|
|
12
|
+
segments: GroupSegment | GroupSegment[],
|
|
13
|
+
context: Context,
|
|
14
|
+
removeSegments: string[] = [],
|
|
15
|
+
): GroupSegment | GroupSegment[] {
|
|
16
|
+
const scoped = buildScopedGroupSegments(datafileReader, segments, context, removeSegments);
|
|
17
|
+
const removed = removeRedundantGroupSegments(scoped);
|
|
18
|
+
|
|
19
|
+
return removed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function removeRedundantGroupSegments(
|
|
23
|
+
groupSegments: GroupSegment | GroupSegment[],
|
|
24
|
+
): GroupSegment | GroupSegment[] {
|
|
25
|
+
if (groupSegments === "*") {
|
|
26
|
+
return groupSegments;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(groupSegments)) {
|
|
30
|
+
// Recursively process each group segment
|
|
31
|
+
const processed = groupSegments.map((gs) => removeRedundantGroupSegments(gs)) as GroupSegment[];
|
|
32
|
+
|
|
33
|
+
// Filter out "*" values
|
|
34
|
+
const filtered = processed.filter((gs) => gs !== "*");
|
|
35
|
+
|
|
36
|
+
// If all were "*", return "*"
|
|
37
|
+
if (filtered.length === 0) {
|
|
38
|
+
return "*";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return filtered;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof groupSegments === "object") {
|
|
45
|
+
if ("and" in groupSegments) {
|
|
46
|
+
const processed = groupSegments.and.map((gs) =>
|
|
47
|
+
removeRedundantGroupSegments(gs),
|
|
48
|
+
) as GroupSegment[];
|
|
49
|
+
const filtered = processed.filter((gs) => gs !== "*");
|
|
50
|
+
|
|
51
|
+
// If all were "*", return "*"
|
|
52
|
+
if (filtered.length === 0) {
|
|
53
|
+
return "*";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
and: filtered,
|
|
58
|
+
} as AndGroupSegment;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ("or" in groupSegments) {
|
|
62
|
+
const processed = groupSegments.or.map((gs) =>
|
|
63
|
+
removeRedundantGroupSegments(gs),
|
|
64
|
+
) as GroupSegment[];
|
|
65
|
+
const filtered = processed.filter((gs) => gs !== "*");
|
|
66
|
+
|
|
67
|
+
// If all were "*", return "*"
|
|
68
|
+
if (filtered.length === 0) {
|
|
69
|
+
return "*";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
or: filtered,
|
|
74
|
+
} as OrGroupSegment;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ("not" in groupSegments) {
|
|
78
|
+
const processed = groupSegments.not.map((gs) =>
|
|
79
|
+
removeRedundantGroupSegments(gs),
|
|
80
|
+
) as GroupSegment[];
|
|
81
|
+
const filtered = processed.filter((gs) => gs !== "*");
|
|
82
|
+
|
|
83
|
+
// If all were "*", return "*"
|
|
84
|
+
if (filtered.length === 0) {
|
|
85
|
+
return "*";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
not: filtered,
|
|
90
|
+
} as NotGroupSegment;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return groupSegments;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildScopedGroupSegments(
|
|
98
|
+
datafileReader: DatafileReader,
|
|
99
|
+
groupSegments: GroupSegment | GroupSegment[],
|
|
100
|
+
context: Context,
|
|
101
|
+
removeSegments: string[] = [],
|
|
102
|
+
): GroupSegment | GroupSegment[] {
|
|
103
|
+
if (groupSegments === "*") {
|
|
104
|
+
return groupSegments;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(groupSegments)) {
|
|
108
|
+
return groupSegments.map((gs) =>
|
|
109
|
+
buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
|
|
110
|
+
) as GroupSegment[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof groupSegments === "string") {
|
|
114
|
+
if (removeSegments.includes(groupSegments)) {
|
|
115
|
+
return "*";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const matched = datafileReader.allSegmentsAreMatched(groupSegments, context);
|
|
119
|
+
|
|
120
|
+
if (matched) {
|
|
121
|
+
return "*";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof groupSegments === "object") {
|
|
126
|
+
// AND, OR, NOT group segments
|
|
127
|
+
if ("and" in groupSegments) {
|
|
128
|
+
return {
|
|
129
|
+
and: groupSegments.and.map((gs) =>
|
|
130
|
+
buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
|
|
131
|
+
) as GroupSegment[],
|
|
132
|
+
} as AndGroupSegment;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if ("or" in groupSegments) {
|
|
136
|
+
return {
|
|
137
|
+
or: groupSegments.or.map((gs) =>
|
|
138
|
+
buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
|
|
139
|
+
) as GroupSegment[],
|
|
140
|
+
} as OrGroupSegment;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if ("not" in groupSegments) {
|
|
144
|
+
return {
|
|
145
|
+
not: groupSegments.not.map((gs) =>
|
|
146
|
+
buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
|
|
147
|
+
) as GroupSegment[],
|
|
148
|
+
} as NotGroupSegment;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return groupSegments;
|
|
153
|
+
}
|
package/src/builder/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
|
|
3
|
-
import type { BucketBy } from "@featurevisor/types";
|
|
3
|
+
import type { BucketBy, Context, Tag } from "@featurevisor/types";
|
|
4
4
|
|
|
5
5
|
import { Parser, parsers } from "./parsers";
|
|
6
6
|
import { FilesystemAdapter } from "../datasource/filesystemAdapter";
|
|
7
7
|
import type { Plugin } from "../cli";
|
|
8
|
+
import type { BuildTags } from "../builder/buildDatafile";
|
|
8
9
|
|
|
9
10
|
export const FEATURES_DIRECTORY_NAME = "features";
|
|
10
11
|
export const SEGMENTS_DIRECTORY_NAME = "segments";
|
|
@@ -31,6 +32,13 @@ export const DEFAULT_PARSER: Parser = "yml";
|
|
|
31
32
|
|
|
32
33
|
export const SCHEMA_VERSION = "2"; // default schema version
|
|
33
34
|
|
|
35
|
+
export interface Scope {
|
|
36
|
+
name: string;
|
|
37
|
+
context: Context;
|
|
38
|
+
tag?: Tag;
|
|
39
|
+
tags?: BuildTags;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export interface ProjectConfig {
|
|
35
43
|
featuresDirectoryPath: string;
|
|
36
44
|
segmentsDirectoryPath: string;
|
|
@@ -45,6 +53,7 @@ export interface ProjectConfig {
|
|
|
45
53
|
|
|
46
54
|
environments: string[] | false;
|
|
47
55
|
tags: string[];
|
|
56
|
+
scopes?: Scope[];
|
|
48
57
|
|
|
49
58
|
adapter: any; // @NOTE: type this properly later
|
|
50
59
|
plugins: Plugin[];
|
|
@@ -68,6 +77,7 @@ export function getProjectConfig(rootDirectoryPath: string): ProjectConfig {
|
|
|
68
77
|
const baseConfig: ProjectConfig = {
|
|
69
78
|
environments: DEFAULT_ENVIRONMENTS,
|
|
70
79
|
tags: DEFAULT_TAGS,
|
|
80
|
+
scopes: [],
|
|
71
81
|
defaultBucketBy: "userId",
|
|
72
82
|
|
|
73
83
|
parser: DEFAULT_PARSER,
|
|
@@ -8,9 +8,12 @@ import type {
|
|
|
8
8
|
EntityType,
|
|
9
9
|
} from "@featurevisor/types";
|
|
10
10
|
|
|
11
|
+
import type { Scope } from "../config";
|
|
12
|
+
|
|
11
13
|
export interface DatafileOptions {
|
|
12
14
|
environment: EnvironmentKey | false;
|
|
13
|
-
tag
|
|
15
|
+
tag?: string;
|
|
16
|
+
scope?: Scope;
|
|
14
17
|
datafilesDir?: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -223,7 +223,10 @@ export class FilesystemAdapter extends Adapter {
|
|
|
223
223
|
getDatafilePath(options: DatafileOptions): string {
|
|
224
224
|
const pattern = this.config.datafileNamePattern || "featurevisor-%s.json";
|
|
225
225
|
|
|
226
|
-
const fileName =
|
|
226
|
+
const fileName = options.scope
|
|
227
|
+
? pattern.replace("%s", `scope-${options.scope.name}`)
|
|
228
|
+
: pattern.replace("%s", `tag-${options.tag}`);
|
|
229
|
+
|
|
227
230
|
const dir = options.datafilesDir || this.config.datafilesDirectoryPath;
|
|
228
231
|
|
|
229
232
|
if (options.environment) {
|
package/src/linter/testSchema.ts
CHANGED
|
@@ -7,6 +7,8 @@ export function getTestsZodSchema(
|
|
|
7
7
|
availableFeatureKeys: [string, ...string[]],
|
|
8
8
|
availableSegmentKeys: [string, ...string[]],
|
|
9
9
|
) {
|
|
10
|
+
const scopeNames = projectConfig.scopes ? projectConfig.scopes.map((scope) => scope.name) : [];
|
|
11
|
+
|
|
10
12
|
const matrixZodSchema = z.record(
|
|
11
13
|
z.array(
|
|
12
14
|
z.union([
|
|
@@ -82,6 +84,16 @@ export function getTestsZodSchema(
|
|
|
82
84
|
}),
|
|
83
85
|
)
|
|
84
86
|
: z.never().optional(),
|
|
87
|
+
// @TODO: add tag later, similar to `scope` below
|
|
88
|
+
scope: z
|
|
89
|
+
.string()
|
|
90
|
+
.refine(
|
|
91
|
+
(value) => scopeNames.includes(value),
|
|
92
|
+
(value) => ({
|
|
93
|
+
message: `Unknown scope "${value}"`,
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
.optional(),
|
|
85
97
|
|
|
86
98
|
// parent
|
|
87
99
|
sticky: z.record(z.record(z.any())).optional(),
|
package/src/list/matrix.ts
CHANGED
|
@@ -96,6 +96,13 @@ export function applyCombinationToFeatureAssertion(
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// scope
|
|
100
|
+
if (flattenedAssertion.scope) {
|
|
101
|
+
flattenedAssertion.scope = applyCombinationToValue(flattenedAssertion.scope, combination);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// @TODO: support `tag` later, similar to `scope` above
|
|
105
|
+
|
|
99
106
|
return flattenedAssertion;
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -17,14 +17,22 @@ import { Datasource } from "../datasource";
|
|
|
17
17
|
import { ProjectConfig } from "../config";
|
|
18
18
|
|
|
19
19
|
import { checkIfArraysAreEqual, checkIfObjectsAreEqual } from "./helpers";
|
|
20
|
-
import type {
|
|
20
|
+
import type { DatafileContentByKey } from "./testProject";
|
|
21
|
+
|
|
22
|
+
export interface TestFeatureOptions {
|
|
23
|
+
verbose?: boolean;
|
|
24
|
+
quiet?: boolean;
|
|
25
|
+
showDatafile?: boolean;
|
|
26
|
+
withScopes?: boolean;
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
export async function testFeature(
|
|
23
31
|
datasource: Datasource,
|
|
24
32
|
projectConfig: ProjectConfig,
|
|
25
33
|
test: TestFeature,
|
|
26
|
-
options:
|
|
27
|
-
|
|
34
|
+
options: TestFeatureOptions = {},
|
|
35
|
+
datafileContentByKey: DatafileContentByKey,
|
|
28
36
|
): Promise<TestResult> {
|
|
29
37
|
const testStartTime = Date.now();
|
|
30
38
|
const featureKey = test.feature;
|
|
@@ -51,7 +59,14 @@ export async function testFeature(
|
|
|
51
59
|
errors: [],
|
|
52
60
|
};
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
let datafileContent = datafileContentByKey.get(assertion.environment || false);
|
|
63
|
+
|
|
64
|
+
const scopedDatafileKey = `${assertion.environment}-scope-${assertion.scope}`;
|
|
65
|
+
if (assertion.scope && datafileContentByKey.has(scopedDatafileKey)) {
|
|
66
|
+
datafileContent = datafileContentByKey.get(scopedDatafileKey);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// @TODO: do similar like `scope`, but for `tag`
|
|
55
70
|
|
|
56
71
|
if (options.showDatafile) {
|
|
57
72
|
console.log("");
|
|
@@ -92,15 +107,39 @@ export async function testFeature(
|
|
|
92
107
|
return testResult;
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
let context = {};
|
|
111
|
+
|
|
112
|
+
if (assertion.scope) {
|
|
113
|
+
if (!options.withScopes) {
|
|
114
|
+
// if not testing with scoped datafiles,
|
|
115
|
+
// then we need to add the scope's context to the context
|
|
116
|
+
const scope = projectConfig.scopes?.find((s) => s.name === assertion.scope);
|
|
117
|
+
|
|
118
|
+
if (scope) {
|
|
119
|
+
context = {
|
|
120
|
+
...(scope.context || {}),
|
|
121
|
+
...context,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
95
127
|
if (assertion.context) {
|
|
96
|
-
|
|
128
|
+
context = {
|
|
129
|
+
...context,
|
|
130
|
+
...assertion.context,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (context) {
|
|
135
|
+
sdk.setContext(context);
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
/**
|
|
100
139
|
* expectedToBeEnabled
|
|
101
140
|
*/
|
|
102
141
|
function testExpectedToBeEnabled(sdk, assertion, details = {}) {
|
|
103
|
-
const isEnabled = sdk.isEnabled(featureKey,
|
|
142
|
+
const isEnabled = sdk.isEnabled(featureKey, context);
|
|
104
143
|
|
|
105
144
|
if (isEnabled !== assertion.expectedToBeEnabled) {
|
|
106
145
|
testResult.passed = false;
|
|
@@ -128,7 +167,7 @@ export async function testFeature(
|
|
|
128
167
|
overrideOptions.defaultVariationValue = assertion.defaultVariationValue;
|
|
129
168
|
}
|
|
130
169
|
|
|
131
|
-
const variation = sdk.getVariation(featureKey,
|
|
170
|
+
const variation = sdk.getVariation(featureKey, context, overrideOptions);
|
|
132
171
|
|
|
133
172
|
if (variation !== assertion.expectedVariation) {
|
|
134
173
|
testResult.passed = false;
|
|
@@ -160,12 +199,7 @@ export async function testFeature(
|
|
|
160
199
|
overrideOptions.defaultVariableValue = assertion.defaultVariableValues[variableKey];
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
const actualValue = sdk.getVariable(
|
|
164
|
-
featureKey,
|
|
165
|
-
variableKey,
|
|
166
|
-
assertion.context || {},
|
|
167
|
-
overrideOptions,
|
|
168
|
-
);
|
|
202
|
+
const actualValue = sdk.getVariable(featureKey, variableKey, context, overrideOptions);
|
|
169
203
|
|
|
170
204
|
let passed;
|
|
171
205
|
|
|
@@ -271,12 +305,12 @@ export async function testFeature(
|
|
|
271
305
|
}
|
|
272
306
|
|
|
273
307
|
if (assertion.expectedEvaluations.flag) {
|
|
274
|
-
const evaluation = sdk.evaluateFlag(featureKey,
|
|
308
|
+
const evaluation = sdk.evaluateFlag(featureKey, context);
|
|
275
309
|
testEvaluation("flag", evaluation, assertion.expectedEvaluations.flag);
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
if (assertion.expectedEvaluations.variation) {
|
|
279
|
-
const evaluation = sdk.evaluateVariation(featureKey,
|
|
313
|
+
const evaluation = sdk.evaluateVariation(featureKey, context);
|
|
280
314
|
testEvaluation("variation", evaluation, assertion.expectedEvaluations.variation);
|
|
281
315
|
}
|
|
282
316
|
|
|
@@ -284,7 +318,7 @@ export async function testFeature(
|
|
|
284
318
|
const variableKeys = Object.keys(assertion.expectedEvaluations.variables);
|
|
285
319
|
|
|
286
320
|
for (const variableKey of variableKeys) {
|
|
287
|
-
const evaluation = sdk.evaluateVariable(featureKey, variableKey,
|
|
321
|
+
const evaluation = sdk.evaluateVariable(featureKey, variableKey, context);
|
|
288
322
|
testEvaluation(
|
|
289
323
|
"variable",
|
|
290
324
|
evaluation,
|
|
@@ -9,7 +9,7 @@ import { Dependencies } from "../dependencies";
|
|
|
9
9
|
import { prettyDuration } from "./prettyDuration";
|
|
10
10
|
import { printTestResult } from "./printTestResult";
|
|
11
11
|
|
|
12
|
-
import { buildDatafile } from "../builder";
|
|
12
|
+
import { buildDatafile, buildScopedDatafile } from "../builder";
|
|
13
13
|
import { SCHEMA_VERSION } from "../config";
|
|
14
14
|
import { Plugin } from "../cli";
|
|
15
15
|
import { listEntities } from "../list";
|
|
@@ -22,6 +22,7 @@ export interface TestProjectOptions {
|
|
|
22
22
|
onlyFailures?: boolean;
|
|
23
23
|
schemaVersion?: string;
|
|
24
24
|
inflate?: number;
|
|
25
|
+
withScopes?: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface ExecutionResult {
|
|
@@ -32,13 +33,14 @@ export interface ExecutionResult {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// the key can be either "<EnvironmentKey>" or "<EnvironmentKey>scope-<Scope>"
|
|
37
|
+
export type DatafileContentByKey = Map<string | false, DatafileContent>;
|
|
36
38
|
|
|
37
39
|
export async function executeTest(
|
|
38
40
|
test: Test,
|
|
39
41
|
deps: Dependencies,
|
|
40
42
|
options: TestProjectOptions,
|
|
41
|
-
|
|
43
|
+
datafileContentByKey: DatafileContentByKey,
|
|
42
44
|
): Promise<ExecutionResult | undefined> {
|
|
43
45
|
const { datasource, projectConfig, rootDirectoryPath } = deps;
|
|
44
46
|
|
|
@@ -74,7 +76,7 @@ export async function executeTest(
|
|
|
74
76
|
projectConfig,
|
|
75
77
|
tAsFeature,
|
|
76
78
|
options,
|
|
77
|
-
|
|
79
|
+
datafileContentByKey,
|
|
78
80
|
);
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -124,7 +126,7 @@ export async function testProject(
|
|
|
124
126
|
let passedAssertionsCount = 0;
|
|
125
127
|
let failedAssertionsCount = 0;
|
|
126
128
|
|
|
127
|
-
const
|
|
129
|
+
const datafileContentByKey: DatafileContentByKey = new Map();
|
|
128
130
|
|
|
129
131
|
// with environments
|
|
130
132
|
if (Array.isArray(projectConfig.environments)) {
|
|
@@ -142,7 +144,36 @@ export async function testProject(
|
|
|
142
144
|
existingState,
|
|
143
145
|
);
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
datafileContentByKey.set(environment, datafileContent as DatafileContent);
|
|
148
|
+
|
|
149
|
+
// by scope
|
|
150
|
+
if (projectConfig.scopes && options.withScopes) {
|
|
151
|
+
for (const scope of projectConfig.scopes) {
|
|
152
|
+
const existingState = await datasource.readState(environment);
|
|
153
|
+
const datafileContent = await buildDatafile(
|
|
154
|
+
projectConfig,
|
|
155
|
+
datasource,
|
|
156
|
+
{
|
|
157
|
+
schemaVersion: options.schemaVersion || SCHEMA_VERSION,
|
|
158
|
+
revision: "include-scoped-features",
|
|
159
|
+
environment: environment,
|
|
160
|
+
inflate: options.inflate,
|
|
161
|
+
tag: scope.tag,
|
|
162
|
+
tags: scope.tags,
|
|
163
|
+
},
|
|
164
|
+
existingState,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const scopedDatafileContent = buildScopedDatafile(
|
|
168
|
+
datafileContent as DatafileContent,
|
|
169
|
+
scope.context,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
datafileContentByKey.set(`${environment}-scope-${scope.name}`, scopedDatafileContent);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// @TODO: by tag
|
|
146
177
|
}
|
|
147
178
|
}
|
|
148
179
|
|
|
@@ -161,7 +192,36 @@ export async function testProject(
|
|
|
161
192
|
existingState,
|
|
162
193
|
);
|
|
163
194
|
|
|
164
|
-
|
|
195
|
+
datafileContentByKey.set(false, datafileContent as DatafileContent);
|
|
196
|
+
|
|
197
|
+
// by scope
|
|
198
|
+
if (projectConfig.scopes && options.withScopes) {
|
|
199
|
+
for (const scope of projectConfig.scopes) {
|
|
200
|
+
const existingState = await datasource.readState(false);
|
|
201
|
+
const datafileContent = await buildDatafile(
|
|
202
|
+
projectConfig,
|
|
203
|
+
datasource,
|
|
204
|
+
{
|
|
205
|
+
schemaVersion: options.schemaVersion || SCHEMA_VERSION,
|
|
206
|
+
revision: "include-scoped-features",
|
|
207
|
+
environment: false,
|
|
208
|
+
inflate: options.inflate,
|
|
209
|
+
tag: scope.tag,
|
|
210
|
+
tags: scope.tags,
|
|
211
|
+
},
|
|
212
|
+
existingState,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const scopedDatafileContent = buildScopedDatafile(
|
|
216
|
+
datafileContent as DatafileContent,
|
|
217
|
+
scope.context,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
datafileContentByKey.set(`scope-${scope.name}`, scopedDatafileContent);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// @TODO: by tag
|
|
165
225
|
}
|
|
166
226
|
|
|
167
227
|
const tests = await listEntities<Test>(
|
|
@@ -178,7 +238,7 @@ export async function testProject(
|
|
|
178
238
|
);
|
|
179
239
|
|
|
180
240
|
for (const test of tests) {
|
|
181
|
-
const executionResult = await executeTest(test, deps, options,
|
|
241
|
+
const executionResult = await executeTest(test, deps, options, datafileContentByKey);
|
|
182
242
|
|
|
183
243
|
if (!executionResult) {
|
|
184
244
|
continue;
|