@kensio/yulin 1.21.32 → 1.21.33
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/dist/service/glue/expression/sim-glue-partition-comparisons.js +1 -1
- package/dist/service/iam/authorize/match/condition/null/sim-iam-null.js +4 -1
- package/dist/service/lambda/command/create-function/create-function.handler.js +1 -0
- package/dist/service/lambda/function/environment/sim-lambda-environment-conflict.d.ts +22 -0
- package/dist/service/lambda/function/environment/sim-lambda-environment-conflict.js +33 -0
- package/dist/service/lambda/function/environment/sim-lambda-environment-conflicts.d.ts +53 -11
- package/dist/service/lambda/function/environment/sim-lambda-environment-conflicts.js +78 -24
- package/dist/service/lambda/function/sim-lambda-function.js +1 -0
- package/dist/service/lambda/function/sim-lambda-function.type.d.ts +8 -0
- package/dist/service/lambda/sim-lambda-commands.js +3 -1
- package/dist/util/process/sim-process-environment-reads.d.ts +52 -0
- package/dist/util/process/sim-process-environment-reads.js +94 -0
- package/dist/util/process/sim-process-environment.d.ts +32 -9
- package/dist/util/process/sim-process-environment.js +43 -17
- package/docs/services/lambda/README.md +6 -0
- package/docs/services/s3/README.md +16 -0
- package/package.json +3 -3
|
@@ -3,7 +3,7 @@ import { simGlueAtLeast, simGlueAtMost, simGlueColumnValue, simGlueEqual, simGlu
|
|
|
3
3
|
export function simGlueComparisonFilter(column, holds, literal) {
|
|
4
4
|
return (values) => {
|
|
5
5
|
const placed = simGluePartitionOrder(column, simGlueColumnValue(column, values), literal);
|
|
6
|
-
return placed
|
|
6
|
+
return placed !== undefined && holds(placed);
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
9
|
/** A filter holding for a key between two literals, both ends included. */
|
|
@@ -13,7 +13,10 @@ function expectsAbsentKey(expected) {
|
|
|
13
13
|
if (expected === "true") {
|
|
14
14
|
return true;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
if (expected === "false") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Whether a request value counts as present for a `Null` check.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
|
|
2
|
+
/**
|
|
3
|
+
* A difference between declared values that matters once the name is read
|
|
4
|
+
* outside an invocation.
|
|
5
|
+
*
|
|
6
|
+
* The key says which kind of difference it is and for which name, and is
|
|
7
|
+
* what a warning is printed once for.
|
|
8
|
+
*/
|
|
9
|
+
export interface SimLambdaEnvironmentConflict {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The conflict of a function declaring a name the host process also sets,
|
|
16
|
+
* with a different value.
|
|
17
|
+
*/
|
|
18
|
+
export declare function simLambdaHostConflict(environment: SimLambdaEnvironment, name: string): SimLambdaEnvironmentConflict;
|
|
19
|
+
/**
|
|
20
|
+
* The conflict of two functions declaring different values for one name.
|
|
21
|
+
*/
|
|
22
|
+
export declare function simLambdaFunctionConflict(existing: SimLambdaEnvironment, environment: SimLambdaEnvironment, name: string): SimLambdaEnvironmentConflict;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advice that follows each warning, given the name that was read early.
|
|
3
|
+
*/
|
|
4
|
+
function moveTheReadAdvice(name) {
|
|
5
|
+
return (`Code read process.env.${name} outside any invocation, as a read at ` +
|
|
6
|
+
"module scope does when the module is imported, and saw the host value " +
|
|
7
|
+
"instead. Move the read inside the handler function.");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The conflict of a function declaring a name the host process also sets,
|
|
11
|
+
* with a different value.
|
|
12
|
+
*/
|
|
13
|
+
export function simLambdaHostConflict(environment, name) {
|
|
14
|
+
return {
|
|
15
|
+
key: `host:${name}`,
|
|
16
|
+
name,
|
|
17
|
+
message: `Sim Lambda function "${environment.functionName}" declares ` +
|
|
18
|
+
`environment variable ${name}, which the host process also sets ` +
|
|
19
|
+
`with a different value. ${moveTheReadAdvice(name)}`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The conflict of two functions declaring different values for one name.
|
|
24
|
+
*/
|
|
25
|
+
export function simLambdaFunctionConflict(existing, environment, name) {
|
|
26
|
+
return {
|
|
27
|
+
key: `function:${name}`,
|
|
28
|
+
name,
|
|
29
|
+
message: `Sim Lambda functions "${existing.functionName}" and ` +
|
|
30
|
+
`"${environment.functionName}" declare different values for ` +
|
|
31
|
+
`environment variable ${name}. ${moveTheReadAdvice(name)}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
|
|
2
|
+
interface SimLambdaEnvironmentConflictsProperties {
|
|
3
|
+
/**
|
|
4
|
+
* The environments of the functions the simulated Lambda holds when it is
|
|
5
|
+
* asked, which a held name is checked against again once it is read. A
|
|
6
|
+
* check built standalone has none.
|
|
7
|
+
*/
|
|
8
|
+
readonly currentEnvironments?: () => Iterable<SimLambdaEnvironment>;
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Warns about declared environment variables whose value a handler module
|
|
4
|
-
*
|
|
12
|
+
* did not actually see.
|
|
5
13
|
*
|
|
6
14
|
* A sim Lambda function backed by a real in-process handler only gets its own
|
|
7
15
|
* process.env while the handler runs, so a variable read at module scope is
|
|
8
16
|
* read too early (see SimLambdaProcessEnv). That only changes what the code
|
|
9
|
-
* sees when the value read at module scope differs from the declared one
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* sees when the value read at module scope differs from the declared one.
|
|
18
|
+
* There are two ways for it to differ. The host process sets the same name
|
|
19
|
+
* with a different value, or another simulated function declares the same
|
|
20
|
+
* name with a different value.
|
|
21
|
+
*
|
|
22
|
+
* A difference is reported only once code has read the name from process.env
|
|
23
|
+
* outside any invocation, which SimProcessEnvironment records. The name of a
|
|
24
|
+
* difference with no such read yet is held. If the read turns up before a
|
|
25
|
+
* later invocation (a handler module imported after the function was
|
|
26
|
+
* created), the name is checked again against the functions that exist then,
|
|
27
|
+
* so a function deleted or reconfigured in between leaves nothing stale to
|
|
28
|
+
* report. A read made before Yulin was imported is not on record, and leaves
|
|
29
|
+
* the difference unreported.
|
|
13
30
|
*
|
|
14
31
|
* Staying quiet when the values agree matters as much as warning when they do
|
|
15
32
|
* not. Test suites commonly export the same variables they configure their
|
|
@@ -18,30 +35,55 @@ import type { SimLambdaEnvironment } from "./sim-lambda-environment.js";
|
|
|
18
35
|
*/
|
|
19
36
|
export declare class SimLambdaEnvironmentConflicts {
|
|
20
37
|
/**
|
|
21
|
-
*
|
|
38
|
+
* The environments of the functions that exist now.
|
|
39
|
+
*/
|
|
40
|
+
private readonly currentEnvironments;
|
|
41
|
+
/**
|
|
42
|
+
* Conflict keys already reported, so repeated function creation does not
|
|
22
43
|
* repeat the same warning.
|
|
23
44
|
*/
|
|
24
45
|
private readonly warned;
|
|
46
|
+
/**
|
|
47
|
+
* Names in conflict that had not been read outside an invocation when the
|
|
48
|
+
* conflict was found.
|
|
49
|
+
*/
|
|
50
|
+
private readonly held;
|
|
51
|
+
constructor(properties?: SimLambdaEnvironmentConflictsProperties);
|
|
25
52
|
/**
|
|
26
53
|
* Check a new function's environment against the host process and against
|
|
27
54
|
* the functions that already exist.
|
|
28
55
|
*/
|
|
29
56
|
check(environment: SimLambdaEnvironment, existingEnvironments: Iterable<SimLambdaEnvironment>): void;
|
|
30
57
|
/**
|
|
31
|
-
*
|
|
58
|
+
* Check the held names that have since been read outside an invocation
|
|
59
|
+
* again, against the functions that exist now, and report what conflicts.
|
|
60
|
+
*
|
|
61
|
+
* Called as a function is invoked, before the handler runs.
|
|
32
62
|
*/
|
|
33
|
-
|
|
63
|
+
reportReads(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Check one name across the given environments, each against the host
|
|
66
|
+
* process and against the environments before it.
|
|
67
|
+
*/
|
|
68
|
+
private checkName;
|
|
34
69
|
/**
|
|
35
|
-
*
|
|
70
|
+
* Find a conflict when the host process sets the same name with a
|
|
36
71
|
* different value.
|
|
37
72
|
*/
|
|
73
|
+
private checkHostProcess;
|
|
74
|
+
/**
|
|
75
|
+
* Find a conflict when another simulated function declares the same name
|
|
76
|
+
* with a different value.
|
|
77
|
+
*/
|
|
38
78
|
private checkOtherFunctions;
|
|
39
79
|
/**
|
|
40
|
-
*
|
|
80
|
+
* Warn about a conflict once per key if its name has been read outside an
|
|
81
|
+
* invocation, and hold the name otherwise.
|
|
41
82
|
*
|
|
42
83
|
* Warnings go to the console because that is where a test runner surfaces
|
|
43
84
|
* them next to the failing expectation they explain; the simulator has no
|
|
44
85
|
* logger of its own to route them through.
|
|
45
86
|
*/
|
|
46
|
-
private
|
|
87
|
+
private report;
|
|
47
88
|
}
|
|
89
|
+
export {};
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import { simLambdaFunctionConflict, simLambdaHostConflict, } from "./sim-lambda-environment-conflict.js";
|
|
1
2
|
import { simProcessEnvironment } from "../../../../util/process/sim-process-environment.js";
|
|
2
|
-
const moveTheReadAdvice = "A read of process.env at module scope happens when the module is " +
|
|
3
|
-
"imported, before any invocation, so it sees the host value instead. " +
|
|
4
|
-
"Move the read inside the handler function.";
|
|
5
3
|
/**
|
|
6
4
|
* Warns about declared environment variables whose value a handler module
|
|
7
|
-
*
|
|
5
|
+
* did not actually see.
|
|
8
6
|
*
|
|
9
7
|
* A sim Lambda function backed by a real in-process handler only gets its own
|
|
10
8
|
* process.env while the handler runs, so a variable read at module scope is
|
|
11
9
|
* read too early (see SimLambdaProcessEnv). That only changes what the code
|
|
12
|
-
* sees when the value read at module scope differs from the declared one
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* sees when the value read at module scope differs from the declared one.
|
|
11
|
+
* There are two ways for it to differ. The host process sets the same name
|
|
12
|
+
* with a different value, or another simulated function declares the same
|
|
13
|
+
* name with a different value.
|
|
14
|
+
*
|
|
15
|
+
* A difference is reported only once code has read the name from process.env
|
|
16
|
+
* outside any invocation, which SimProcessEnvironment records. The name of a
|
|
17
|
+
* difference with no such read yet is held. If the read turns up before a
|
|
18
|
+
* later invocation (a handler module imported after the function was
|
|
19
|
+
* created), the name is checked again against the functions that exist then,
|
|
20
|
+
* so a function deleted or reconfigured in between leaves nothing stale to
|
|
21
|
+
* report. A read made before Yulin was imported is not on record, and leaves
|
|
22
|
+
* the difference unreported.
|
|
16
23
|
*
|
|
17
24
|
* Staying quiet when the values agree matters as much as warning when they do
|
|
18
25
|
* not. Test suites commonly export the same variables they configure their
|
|
@@ -21,22 +28,68 @@ const moveTheReadAdvice = "A read of process.env at module scope happens when th
|
|
|
21
28
|
*/
|
|
22
29
|
export class SimLambdaEnvironmentConflicts {
|
|
23
30
|
/**
|
|
24
|
-
*
|
|
31
|
+
* The environments of the functions that exist now.
|
|
32
|
+
*/
|
|
33
|
+
currentEnvironments;
|
|
34
|
+
/**
|
|
35
|
+
* Conflict keys already reported, so repeated function creation does not
|
|
25
36
|
* repeat the same warning.
|
|
26
37
|
*/
|
|
27
38
|
warned = new Set();
|
|
39
|
+
/**
|
|
40
|
+
* Names in conflict that had not been read outside an invocation when the
|
|
41
|
+
* conflict was found.
|
|
42
|
+
*/
|
|
43
|
+
held = new Set();
|
|
44
|
+
constructor(properties = {}) {
|
|
45
|
+
this.currentEnvironments =
|
|
46
|
+
properties.currentEnvironments ?? (() => []);
|
|
47
|
+
}
|
|
28
48
|
/**
|
|
29
49
|
* Check a new function's environment against the host process and against
|
|
30
50
|
* the functions that already exist.
|
|
31
51
|
*/
|
|
32
52
|
check(environment, existingEnvironments) {
|
|
53
|
+
const existing = [...existingEnvironments];
|
|
33
54
|
for (const [name, value] of environment.declaredVariables) {
|
|
34
55
|
this.checkHostProcess(environment, name, value);
|
|
35
|
-
this.checkOtherFunctions(environment, name, value,
|
|
56
|
+
this.checkOtherFunctions(environment, name, value, existing);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check the held names that have since been read outside an invocation
|
|
61
|
+
* again, against the functions that exist now, and report what conflicts.
|
|
62
|
+
*
|
|
63
|
+
* Called as a function is invoked, before the handler runs.
|
|
64
|
+
*/
|
|
65
|
+
reportReads() {
|
|
66
|
+
const read = [...this.held].filter((name) => simProcessEnvironment.wasReadOutsideRun(name));
|
|
67
|
+
if (read.length === 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const environments = [...this.currentEnvironments()];
|
|
71
|
+
for (const name of read) {
|
|
72
|
+
this.held.delete(name);
|
|
73
|
+
this.checkName(name, environments);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check one name across the given environments, each against the host
|
|
78
|
+
* process and against the environments before it.
|
|
79
|
+
*/
|
|
80
|
+
checkName(name, environments) {
|
|
81
|
+
for (const [index, environment] of environments.entries()) {
|
|
82
|
+
const value = environment.declaredVariables.get(name);
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.checkHostProcess(environment, name, value);
|
|
87
|
+
this.checkOtherFunctions(environment, name, value, environments.slice(0, index));
|
|
36
88
|
}
|
|
37
89
|
}
|
|
38
90
|
/**
|
|
39
|
-
*
|
|
91
|
+
* Find a conflict when the host process sets the same name with a
|
|
92
|
+
* different value.
|
|
40
93
|
*/
|
|
41
94
|
checkHostProcess(environment, name, value) {
|
|
42
95
|
// Read-only lookup of a name the function itself declared.
|
|
@@ -45,13 +98,11 @@ export class SimLambdaEnvironmentConflicts {
|
|
|
45
98
|
if (hostValue === undefined || hostValue === value) {
|
|
46
99
|
return;
|
|
47
100
|
}
|
|
48
|
-
this.
|
|
49
|
-
`environment variable ${name}, which the host process also sets ` +
|
|
50
|
-
`with a different value. ${moveTheReadAdvice}`);
|
|
101
|
+
this.report(simLambdaHostConflict(environment, name));
|
|
51
102
|
}
|
|
52
103
|
/**
|
|
53
|
-
*
|
|
54
|
-
* different value.
|
|
104
|
+
* Find a conflict when another simulated function declares the same name
|
|
105
|
+
* with a different value.
|
|
55
106
|
*/
|
|
56
107
|
checkOtherFunctions(environment, name, value, existingEnvironments) {
|
|
57
108
|
for (const existing of existingEnvironments) {
|
|
@@ -59,25 +110,28 @@ export class SimLambdaEnvironmentConflicts {
|
|
|
59
110
|
if (existingValue === undefined || existingValue === value) {
|
|
60
111
|
continue;
|
|
61
112
|
}
|
|
62
|
-
this.
|
|
63
|
-
`"${environment.functionName}" declare different values for ` +
|
|
64
|
-
`environment variable ${name}. ${moveTheReadAdvice}`);
|
|
113
|
+
this.report(simLambdaFunctionConflict(existing, environment, name));
|
|
65
114
|
return;
|
|
66
115
|
}
|
|
67
116
|
}
|
|
68
117
|
/**
|
|
69
|
-
*
|
|
118
|
+
* Warn about a conflict once per key if its name has been read outside an
|
|
119
|
+
* invocation, and hold the name otherwise.
|
|
70
120
|
*
|
|
71
121
|
* Warnings go to the console because that is where a test runner surfaces
|
|
72
122
|
* them next to the failing expectation they explain; the simulator has no
|
|
73
123
|
* logger of its own to route them through.
|
|
74
124
|
*/
|
|
75
|
-
|
|
76
|
-
if (this.warned.has(key)) {
|
|
125
|
+
report(conflict) {
|
|
126
|
+
if (this.warned.has(conflict.key)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!simProcessEnvironment.wasReadOutsideRun(conflict.name)) {
|
|
130
|
+
this.held.add(conflict.name);
|
|
77
131
|
return;
|
|
78
132
|
}
|
|
79
|
-
this.warned.add(key);
|
|
133
|
+
this.warned.add(conflict.key);
|
|
80
134
|
// oxlint-disable-next-line no-console
|
|
81
|
-
console.warn(message);
|
|
135
|
+
console.warn(conflict.message);
|
|
82
136
|
}
|
|
83
137
|
}
|
|
@@ -194,6 +194,7 @@ export class SimLambdaFunction {
|
|
|
194
194
|
logStreamName: this.logging.logStreamName(),
|
|
195
195
|
});
|
|
196
196
|
this.logging.recordFrom(this.#code);
|
|
197
|
+
this.properties.environmentConflicts?.reportReads();
|
|
197
198
|
return await this.metrics.around(String(this.name), async () => simAwsRunAsContext.run(this.runAsOwner, { kind: "arn", arn: this.roleArn }, async () => await this.environment.runWith(async () => await this.runInHostScope(async () => await this.logging.around(async () => await invocation.run(this.#code.handlerFunction(), event))))));
|
|
198
199
|
}
|
|
199
200
|
/**
|
|
@@ -8,6 +8,7 @@ import type { SimLambdaOutput } from "./logging/sim-lambda-output.js";
|
|
|
8
8
|
import type { SimLogsServiceWriter } from "../../logs/write/sim-logs-service-writer.js";
|
|
9
9
|
import type { SimLambdaExecutableCode } from "./code/sim-lambda-executable-code.js";
|
|
10
10
|
import type { SimLambdaEnvironment } from "./environment/sim-lambda-environment.js";
|
|
11
|
+
import type { SimLambdaEnvironmentConflicts } from "./environment/sim-lambda-environment-conflicts.js";
|
|
11
12
|
import type { SimLambdaOutboundHttp } from "./outbound/sim-lambda-outbound-http.js";
|
|
12
13
|
import type { SimLambdaFunctionState } from "./sim-lambda-function-configuration.js";
|
|
13
14
|
import type { SimLambdaHandler } from "./sim-lambda-handler.type.js";
|
|
@@ -27,6 +28,13 @@ export interface SimLambdaFunctionProperties {
|
|
|
27
28
|
timeoutSeconds?: number | undefined;
|
|
28
29
|
memorySizeMb?: number | undefined;
|
|
29
30
|
environment?: SimLambdaEnvironment | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The check that warns about declared variables read too early, shared with
|
|
33
|
+
* every other function of the same simulated Lambda. It reports, as this
|
|
34
|
+
* function is invoked, a difference found earlier whose name has since been
|
|
35
|
+
* read outside an invocation. A function built standalone has none.
|
|
36
|
+
*/
|
|
37
|
+
environmentConflicts?: SimLambdaEnvironmentConflicts | undefined;
|
|
30
38
|
/**
|
|
31
39
|
* The queue or topic this function's abandoned asynchronous events are sent
|
|
32
40
|
* to, which is what `DeadLetterConfig.TargetArn` names.
|
|
@@ -146,7 +146,9 @@ export class SimLambdaCommands {
|
|
|
146
146
|
runAsOwner: properties.runAsOwner,
|
|
147
147
|
// Shared across function creations so a conflicting environment variable
|
|
148
148
|
// is only reported once for this simulated Lambda.
|
|
149
|
-
environmentConflicts: new SimLambdaEnvironmentConflicts(
|
|
149
|
+
environmentConflicts: new SimLambdaEnvironmentConflicts({
|
|
150
|
+
currentEnvironments: () => properties.functions.values().map((existing) => existing.environment),
|
|
151
|
+
}),
|
|
150
152
|
eventInvokeConfigs: this.eventInvokeConfigStore,
|
|
151
153
|
destinations,
|
|
152
154
|
codeStore,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records the names code reads from the host process.env.
|
|
3
|
+
*
|
|
4
|
+
* SimLambdaEnvironmentConflicts warns that a module-scope read sees the host
|
|
5
|
+
* value of a variable instead of the function's own. Whether that matters
|
|
6
|
+
* depends on a read having happened at all, which configuration cannot tell.
|
|
7
|
+
* This keeps the evidence: the host environment is handed out behind a Proxy
|
|
8
|
+
* that notes each name read through it.
|
|
9
|
+
*
|
|
10
|
+
* Enumerating the whole environment is not counted as reading every name in
|
|
11
|
+
* it. Node.js copies process.env key by key when it spawns a child process,
|
|
12
|
+
* and so does any tool that snapshots it, and none of that is handler code
|
|
13
|
+
* capturing a value it later depends on. An enumeration is recognised by its
|
|
14
|
+
* shape: the property reads that follow a listing of the keys, in the order
|
|
15
|
+
* the listing gave them. Its reads are recorded as they happen and taken off
|
|
16
|
+
* the record again once every key has been read, so a listing that is not
|
|
17
|
+
* followed by a complete copy hides nothing. A module copying the environment
|
|
18
|
+
* with a spread at module scope is missed as a result.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SimProcessEnvironmentReads {
|
|
21
|
+
private readonly names;
|
|
22
|
+
/**
|
|
23
|
+
* The environment objects each watching Proxy stands in front of, so that
|
|
24
|
+
* assigning one back to process.env does not stack a Proxy on a Proxy.
|
|
25
|
+
*/
|
|
26
|
+
private readonly targets;
|
|
27
|
+
/**
|
|
28
|
+
* The keys of an enumeration in progress, and how far its reads have got.
|
|
29
|
+
*/
|
|
30
|
+
private enumeration;
|
|
31
|
+
/**
|
|
32
|
+
* Whether code has read the name through a watched environment.
|
|
33
|
+
*/
|
|
34
|
+
wasRead(name: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* A view of the environment that records the names read through it.
|
|
37
|
+
*
|
|
38
|
+
* Writes and deletes go straight to the environment, so a value assigned
|
|
39
|
+
* through the view is coerced to a string as process.env would coerce it.
|
|
40
|
+
*/
|
|
41
|
+
watch(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
42
|
+
/**
|
|
43
|
+
* The environment behind a watching view, or the environment itself when it
|
|
44
|
+
* is not one.
|
|
45
|
+
*/
|
|
46
|
+
unwrap(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
47
|
+
/**
|
|
48
|
+
* Note a read, and take an enumeration's reads off the record once it has
|
|
49
|
+
* read every key.
|
|
50
|
+
*/
|
|
51
|
+
private recordRead;
|
|
52
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records the names code reads from the host process.env.
|
|
3
|
+
*
|
|
4
|
+
* SimLambdaEnvironmentConflicts warns that a module-scope read sees the host
|
|
5
|
+
* value of a variable instead of the function's own. Whether that matters
|
|
6
|
+
* depends on a read having happened at all, which configuration cannot tell.
|
|
7
|
+
* This keeps the evidence: the host environment is handed out behind a Proxy
|
|
8
|
+
* that notes each name read through it.
|
|
9
|
+
*
|
|
10
|
+
* Enumerating the whole environment is not counted as reading every name in
|
|
11
|
+
* it. Node.js copies process.env key by key when it spawns a child process,
|
|
12
|
+
* and so does any tool that snapshots it, and none of that is handler code
|
|
13
|
+
* capturing a value it later depends on. An enumeration is recognised by its
|
|
14
|
+
* shape: the property reads that follow a listing of the keys, in the order
|
|
15
|
+
* the listing gave them. Its reads are recorded as they happen and taken off
|
|
16
|
+
* the record again once every key has been read, so a listing that is not
|
|
17
|
+
* followed by a complete copy hides nothing. A module copying the environment
|
|
18
|
+
* with a spread at module scope is missed as a result.
|
|
19
|
+
*/
|
|
20
|
+
export class SimProcessEnvironmentReads {
|
|
21
|
+
names = new Set();
|
|
22
|
+
/**
|
|
23
|
+
* The environment objects each watching Proxy stands in front of, so that
|
|
24
|
+
* assigning one back to process.env does not stack a Proxy on a Proxy.
|
|
25
|
+
*/
|
|
26
|
+
targets = new WeakMap();
|
|
27
|
+
/**
|
|
28
|
+
* The keys of an enumeration in progress, and how far its reads have got.
|
|
29
|
+
*/
|
|
30
|
+
enumeration;
|
|
31
|
+
/**
|
|
32
|
+
* Whether code has read the name through a watched environment.
|
|
33
|
+
*/
|
|
34
|
+
wasRead(name) {
|
|
35
|
+
return this.names.has(name);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A view of the environment that records the names read through it.
|
|
39
|
+
*
|
|
40
|
+
* Writes and deletes go straight to the environment, so a value assigned
|
|
41
|
+
* through the view is coerced to a string as process.env would coerce it.
|
|
42
|
+
*/
|
|
43
|
+
watch(environment) {
|
|
44
|
+
const target = this.unwrap(environment);
|
|
45
|
+
const view = new Proxy(target, {
|
|
46
|
+
get: (watched, key) => {
|
|
47
|
+
if (typeof key === "string") {
|
|
48
|
+
this.recordRead(key);
|
|
49
|
+
}
|
|
50
|
+
return Reflect.get(watched, key);
|
|
51
|
+
},
|
|
52
|
+
set: (watched, key, value) => Reflect.set(watched, key, value),
|
|
53
|
+
ownKeys: (watched) => {
|
|
54
|
+
const keys = Reflect.ownKeys(watched);
|
|
55
|
+
this.enumeration = { keys, next: 0, added: [] };
|
|
56
|
+
return keys;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
this.targets.set(view, target);
|
|
60
|
+
return view;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The environment behind a watching view, or the environment itself when it
|
|
64
|
+
* is not one.
|
|
65
|
+
*/
|
|
66
|
+
unwrap(environment) {
|
|
67
|
+
return this.targets.get(environment) ?? environment;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Note a read, and take an enumeration's reads off the record once it has
|
|
71
|
+
* read every key.
|
|
72
|
+
*/
|
|
73
|
+
recordRead(name) {
|
|
74
|
+
const enumeration = this.enumeration;
|
|
75
|
+
if (enumeration === undefined ||
|
|
76
|
+
enumeration.keys[enumeration.next] !== name) {
|
|
77
|
+
this.enumeration = undefined;
|
|
78
|
+
this.names.add(name);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
enumeration.next += 1;
|
|
82
|
+
if (!this.names.has(name)) {
|
|
83
|
+
this.names.add(name);
|
|
84
|
+
enumeration.added.push(name);
|
|
85
|
+
}
|
|
86
|
+
if (enumeration.next < enumeration.keys.length) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const added of enumeration.added) {
|
|
90
|
+
this.names.delete(added);
|
|
91
|
+
}
|
|
92
|
+
this.enumeration = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -20,19 +20,33 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The one thing this cannot reach is a read that already happened. A handler
|
|
22
22
|
* module doing `const TABLE = process.env.TABLE_NAME` at module scope is
|
|
23
|
-
* evaluated when the test file imports it, long before any run,
|
|
23
|
+
* evaluated when the test file imports it, long before any run, and captures
|
|
24
24
|
* the host value. Reads inside the handler body see the run's own variables.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* Outside a run, process.env is the host environment behind a
|
|
27
|
+
* SimProcessEnvironmentReads view, which records the names read. The patch is
|
|
28
|
+
* installed when this module loads, which is when Yulin is imported, so a
|
|
29
|
+
* module-scope read in a handler imported after Yulin is on record.
|
|
30
|
+
* SimLambdaEnvironmentConflicts warns from that record, and only for a name
|
|
31
|
+
* whose early read changed what the code saw. A read made before Yulin was
|
|
32
|
+
* imported is invisible to it.
|
|
27
33
|
*/
|
|
28
34
|
declare class SimProcessEnvironment {
|
|
29
35
|
private readonly storage;
|
|
36
|
+
private readonly reads;
|
|
30
37
|
private hostEnvironment;
|
|
31
|
-
private
|
|
38
|
+
private hostView;
|
|
39
|
+
constructor();
|
|
32
40
|
/**
|
|
33
41
|
* Run something with the given variables as its process.env.
|
|
34
42
|
*/
|
|
35
43
|
run<T>(variables: Record<string, string>, run: () => Promise<T>): Promise<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether code read the name from process.env while no run was active.
|
|
46
|
+
*
|
|
47
|
+
* Reads made before this module loaded are not on record.
|
|
48
|
+
*/
|
|
49
|
+
wasReadOutsideRun(name: string): boolean;
|
|
36
50
|
/**
|
|
37
51
|
* The host process environment, ignoring any run in progress.
|
|
38
52
|
*/
|
|
@@ -51,14 +65,23 @@ declare class SimProcessEnvironment {
|
|
|
51
65
|
*
|
|
52
66
|
* process.env is a plain configurable data property on process, so it can
|
|
53
67
|
* be replaced with a getter. Outside a simulated run the getter hands back
|
|
54
|
-
* the
|
|
55
|
-
*
|
|
68
|
+
* the host environment behind a view that records the names read, and
|
|
69
|
+
* passes every read, write and delete through to it. Anything that reads,
|
|
70
|
+
* writes, or stubs process.env normally is unaffected.
|
|
56
71
|
*
|
|
57
|
-
* Installed once and never removed. Removing it would be
|
|
58
|
-
* another run is in flight, and there is nothing to gain
|
|
59
|
-
*
|
|
72
|
+
* Installed once, at construction, and never removed. Removing it would be
|
|
73
|
+
* unsafe while another run is in flight, and there is nothing to gain. With
|
|
74
|
+
* no run in progress, the patch passes everything through to the host
|
|
75
|
+
* environment.
|
|
60
76
|
*/
|
|
61
77
|
private install;
|
|
78
|
+
/**
|
|
79
|
+
* Make the environment the host environment, and watch reads of it.
|
|
80
|
+
*
|
|
81
|
+
* Code saving process.env and assigning it back hands over the watching
|
|
82
|
+
* view, which is unwrapped so that views do not nest.
|
|
83
|
+
*/
|
|
84
|
+
private useHostEnvironment;
|
|
62
85
|
/**
|
|
63
86
|
* Apply an assignment to process.env as a whole.
|
|
64
87
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { SimProcessEnvironmentReads } from "./sim-process-environment-reads.js";
|
|
2
3
|
/**
|
|
3
4
|
* The process.env in-process handler code sees while a simulated run lasts.
|
|
4
5
|
*
|
|
@@ -21,22 +22,39 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
21
22
|
*
|
|
22
23
|
* The one thing this cannot reach is a read that already happened. A handler
|
|
23
24
|
* module doing `const TABLE = process.env.TABLE_NAME` at module scope is
|
|
24
|
-
* evaluated when the test file imports it, long before any run,
|
|
25
|
+
* evaluated when the test file imports it, long before any run, and captures
|
|
25
26
|
* the host value. Reads inside the handler body see the run's own variables.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* Outside a run, process.env is the host environment behind a
|
|
29
|
+
* SimProcessEnvironmentReads view, which records the names read. The patch is
|
|
30
|
+
* installed when this module loads, which is when Yulin is imported, so a
|
|
31
|
+
* module-scope read in a handler imported after Yulin is on record.
|
|
32
|
+
* SimLambdaEnvironmentConflicts warns from that record, and only for a name
|
|
33
|
+
* whose early read changed what the code saw. A read made before Yulin was
|
|
34
|
+
* imported is invisible to it.
|
|
28
35
|
*/
|
|
29
36
|
class SimProcessEnvironment {
|
|
30
37
|
storage = new AsyncLocalStorage();
|
|
38
|
+
reads = new SimProcessEnvironmentReads();
|
|
31
39
|
hostEnvironment = process.env;
|
|
32
|
-
|
|
40
|
+
hostView = process.env;
|
|
41
|
+
constructor() {
|
|
42
|
+
this.install();
|
|
43
|
+
}
|
|
33
44
|
/**
|
|
34
45
|
* Run something with the given variables as its process.env.
|
|
35
46
|
*/
|
|
36
47
|
async run(variables, run) {
|
|
37
|
-
this.install();
|
|
38
48
|
return await this.storage.run(variables, run);
|
|
39
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether code read the name from process.env while no run was active.
|
|
52
|
+
*
|
|
53
|
+
* Reads made before this module loaded are not on record.
|
|
54
|
+
*/
|
|
55
|
+
wasReadOutsideRun(name) {
|
|
56
|
+
return this.reads.wasRead(name);
|
|
57
|
+
}
|
|
40
58
|
/**
|
|
41
59
|
* The host process environment, ignoring any run in progress.
|
|
42
60
|
*/
|
|
@@ -65,27 +83,35 @@ class SimProcessEnvironment {
|
|
|
65
83
|
*
|
|
66
84
|
* process.env is a plain configurable data property on process, so it can
|
|
67
85
|
* be replaced with a getter. Outside a simulated run the getter hands back
|
|
68
|
-
* the
|
|
69
|
-
*
|
|
86
|
+
* the host environment behind a view that records the names read, and
|
|
87
|
+
* passes every read, write and delete through to it. Anything that reads,
|
|
88
|
+
* writes, or stubs process.env normally is unaffected.
|
|
70
89
|
*
|
|
71
|
-
* Installed once and never removed. Removing it would be
|
|
72
|
-
* another run is in flight, and there is nothing to gain
|
|
73
|
-
*
|
|
90
|
+
* Installed once, at construction, and never removed. Removing it would be
|
|
91
|
+
* unsafe while another run is in flight, and there is nothing to gain. With
|
|
92
|
+
* no run in progress, the patch passes everything through to the host
|
|
93
|
+
* environment.
|
|
74
94
|
*/
|
|
75
95
|
install() {
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
this.hostEnvironment = process.env;
|
|
96
|
+
this.useHostEnvironment(process.env);
|
|
80
97
|
Object.defineProperty(process, "env", {
|
|
81
98
|
configurable: true,
|
|
82
99
|
enumerable: true,
|
|
83
|
-
get: () => this.storage.getStore() ?? this.
|
|
100
|
+
get: () => this.storage.getStore() ?? this.hostView,
|
|
84
101
|
set: (replacement) => {
|
|
85
102
|
this.replaceVariables(replacement);
|
|
86
103
|
},
|
|
87
104
|
});
|
|
88
|
-
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Make the environment the host environment, and watch reads of it.
|
|
108
|
+
*
|
|
109
|
+
* Code saving process.env and assigning it back hands over the watching
|
|
110
|
+
* view, which is unwrapped so that views do not nest.
|
|
111
|
+
*/
|
|
112
|
+
useHostEnvironment(environment) {
|
|
113
|
+
this.hostEnvironment = this.reads.unwrap(environment);
|
|
114
|
+
this.hostView = this.reads.watch(this.hostEnvironment);
|
|
89
115
|
}
|
|
90
116
|
/**
|
|
91
117
|
* Apply an assignment to process.env as a whole.
|
|
@@ -104,7 +130,7 @@ class SimProcessEnvironment {
|
|
|
104
130
|
replaceVariables(replacement) {
|
|
105
131
|
const store = this.storage.getStore();
|
|
106
132
|
if (store === undefined) {
|
|
107
|
-
this.
|
|
133
|
+
this.useHostEnvironment(replacement);
|
|
108
134
|
return;
|
|
109
135
|
}
|
|
110
136
|
const replacing = Object.entries(replacement);
|
|
@@ -3355,6 +3355,12 @@ console in the two situations where the difference changes what your code sees:
|
|
|
3355
3355
|
- a declared variable whose name the host process also sets, with a different value
|
|
3356
3356
|
- two simulated functions declaring the same variable name with different values
|
|
3357
3357
|
|
|
3358
|
+
Either one is reported only once code has read that name from `process.env` with no invocation
|
|
3359
|
+
running. Yulin records those reads from the moment it is imported. A read made earlier is invisible
|
|
3360
|
+
to it (a handler module imported ahead of Yulin, or a setup file that loads first), and so is a copy
|
|
3361
|
+
of the whole environment such as `{ ...process.env }`. Neither produces a warning. A handler that
|
|
3362
|
+
reads its variables inside the handler function produces none either.
|
|
3363
|
+
|
|
3358
3364
|
## The time inside a handler
|
|
3359
3365
|
|
|
3360
3366
|
A function runs on its simulation's clock, and so does the function code. `Date.now()` and
|
|
@@ -3317,6 +3317,22 @@ The `getBucketWebsiteUrl(...)` method returns the simulated S3 website URL for t
|
|
|
3317
3317
|
`localUrl(...)` method on the localhost server adapts that URL so the request is sent to the local
|
|
3318
3318
|
server while preserving the simulated S3 website hostname.
|
|
3319
3319
|
|
|
3320
|
+
### AWS CLI uploads
|
|
3321
|
+
|
|
3322
|
+
Set `preferred_transfer_client` to `classic` in the AWS config file used for the simulated
|
|
3323
|
+
endpoint:
|
|
3324
|
+
|
|
3325
|
+
```ini
|
|
3326
|
+
[default]
|
|
3327
|
+
s3 =
|
|
3328
|
+
preferred_transfer_client = classic
|
|
3329
|
+
```
|
|
3330
|
+
|
|
3331
|
+
The CLI's [automatic transfer-client selection](https://docs.aws.amazon.com/cli/latest/topic/s3-config.html#preferred-transfer-client)
|
|
3332
|
+
can choose CRT. Its uploads use streaming payload trailers, which Yulin's signature verifier
|
|
3333
|
+
does not support. Those requests fail with `403`. The classic client supports the single-file
|
|
3334
|
+
and multipart uploads exercised by Yulin's CLI tests.
|
|
3335
|
+
|
|
3320
3336
|
## Presigned URLs
|
|
3321
3337
|
|
|
3322
3338
|
Sim S3 serves a REST API endpoint alongside the website endpoint, and it accepts presigned URLs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.33",
|
|
4
4
|
"description": "AWS system behaviour simulation for isolated unit testing",
|
|
5
5
|
"repository": "https://github.com/KensioSoftware/yulin",
|
|
6
6
|
"homepage": "https://yulinsim.dev/",
|
|
@@ -293,13 +293,13 @@
|
|
|
293
293
|
"eslint-plugin-jsdoc": "^64.0.1",
|
|
294
294
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
295
295
|
"eslint-plugin-security": "^4.0.1",
|
|
296
|
-
"eslint-plugin-unicorn": "^
|
|
296
|
+
"eslint-plugin-unicorn": "^76.0.0",
|
|
297
297
|
"execa": "^10.0.0",
|
|
298
298
|
"fta-cli": "^3.0.0",
|
|
299
299
|
"hyparquet": "^1.29.2",
|
|
300
300
|
"hyparquet-writer": "^0.16.9",
|
|
301
301
|
"node-sql-parser": "^5.4.0",
|
|
302
|
-
"oxfmt": "^0.
|
|
302
|
+
"oxfmt": "^0.73.0",
|
|
303
303
|
"oxlint": "^1.77.0",
|
|
304
304
|
"oxlint-tsgolint": "^7.0.2001",
|
|
305
305
|
"semantic-release": "25.0.9",
|