@photostructure/fs-metadata 0.6.0 → 0.7.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 -6
- package/CLAUDE.md +160 -136
- package/CODE_OF_CONDUCT.md +11 -11
- package/CONTRIBUTING.md +2 -2
- package/README.md +34 -84
- package/binding.gyp +98 -23
- package/claude.sh +23 -0
- package/dist/index.cjs +53 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +52 -21
- package/dist/index.mjs.map +1 -1
- package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
- package/doc/GPG_RELEASE_HOWTO.md +505 -0
- package/doc/MACOS_API_REFERENCE.md +469 -0
- package/doc/SECURITY_AUDIT_2025.md +809 -0
- package/doc/SSH_RELEASE_HOWTO.md +207 -0
- package/doc/WINDOWS_API_REFERENCE.md +422 -0
- package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
- package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
- package/doc/examples.md +267 -0
- package/doc/gotchas.md +297 -0
- package/doc/logo.png +0 -0
- package/doc/logo.svg +85 -0
- package/doc/macos-asan-sip-issue.md +71 -0
- package/doc/social.png +0 -0
- package/doc/social.svg +125 -0
- package/doc/windows-build.md +226 -0
- package/doc/windows-clang-tidy.md +72 -0
- package/doc/windows-memory-testing.md +108 -0
- package/doc/windows-prebuildify-arm64.md +232 -0
- package/jest.config.cjs +24 -0
- package/package.json +68 -44
- package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
- package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
- package/scripts/check-memory.ts +186 -0
- package/scripts/clang-tidy.ts +832 -0
- package/scripts/install.cjs +42 -0
- package/scripts/is-platform.mjs +1 -1
- package/scripts/macos-asan.sh +155 -0
- package/scripts/post-build.mjs +3 -3
- package/scripts/prebuild-linux-glibc.sh +119 -0
- package/scripts/prebuildify-wrapper.ts +77 -0
- package/scripts/precommit.ts +70 -0
- package/scripts/sanitizers-test.sh +7 -1
- package/scripts/{configure.mjs → setup-native.mjs} +4 -1
- package/src/binding.cpp +1 -1
- package/src/common/error_utils.h +0 -6
- package/src/common/volume_metadata.h +6 -0
- package/src/darwin/hidden.cpp +73 -25
- package/src/darwin/path_security.h +149 -0
- package/src/darwin/raii_utils.h +104 -4
- package/src/darwin/volume_metadata.cpp +132 -58
- package/src/darwin/volume_mount_points.cpp +80 -47
- package/src/hidden.ts +36 -13
- package/src/linux/gio_mount_points.cpp +17 -18
- package/src/linux/gio_utils.cpp +92 -37
- package/src/linux/gio_utils.h +11 -5
- package/src/linux/gio_volume_metadata.cpp +111 -48
- package/src/linux/volume_metadata.cpp +67 -4
- package/src/object.ts +1 -0
- package/src/options.ts +6 -0
- package/src/path.ts +11 -0
- package/src/platform.ts +25 -0
- package/src/remote_info.ts +5 -3
- package/src/stack_path.ts +8 -6
- package/src/string_enum.ts +1 -0
- package/src/test-utils/benchmark-harness.ts +192 -0
- package/src/test-utils/debuglog-child.ts +30 -2
- package/src/test-utils/debuglog-enabled-child.ts +38 -8
- package/src/test-utils/jest-setup.ts +14 -0
- package/src/test-utils/memory-test-core.ts +336 -0
- package/src/test-utils/memory-test-runner.ts +108 -0
- package/src/test-utils/platform.ts +46 -1
- package/src/test-utils/worker-thread-helper.cjs +157 -26
- package/src/types/native_bindings.ts +1 -1
- package/src/types/options.ts +6 -0
- package/src/windows/drive_status.h +133 -163
- package/src/windows/error_utils.h +54 -3
- package/src/windows/fs_meta.h +1 -1
- package/src/windows/hidden.cpp +60 -43
- package/src/windows/security_utils.h +250 -0
- package/src/windows/string.h +68 -11
- package/src/windows/system_volume.h +1 -1
- package/src/windows/thread_pool.h +206 -0
- package/src/windows/volume_metadata.cpp +11 -6
- package/src/windows/volume_mount_points.cpp +8 -7
- package/src/windows/windows_arch.h +39 -0
- package/scripts/check-memory.mjs +0 -123
- package/scripts/clang-tidy.mjs +0 -73
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { getTimingMultiplier } from "./test-timeout-config";
|
|
2
|
+
|
|
3
|
+
export interface BenchmarkOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Target duration for the benchmark in milliseconds (default: 20000ms / 20 seconds)
|
|
6
|
+
*/
|
|
7
|
+
targetDurationMs?: number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maximum timeout for the entire benchmark in milliseconds (default: 60000ms / 1 minute)
|
|
11
|
+
*/
|
|
12
|
+
maxTimeoutMs?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimum iterations to run regardless of timing (default: 5)
|
|
16
|
+
*/
|
|
17
|
+
minIterations?: number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maximum iterations to run regardless of timing (default: 10000)
|
|
21
|
+
*/
|
|
22
|
+
maxIterations?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Number of warmup iterations before timing (default: 2)
|
|
26
|
+
*/
|
|
27
|
+
warmupIterations?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to log debug information (default: false)
|
|
31
|
+
*/
|
|
32
|
+
debug?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BenchmarkResult {
|
|
36
|
+
/**
|
|
37
|
+
* Number of iterations actually performed
|
|
38
|
+
*/
|
|
39
|
+
iterations: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Total duration in milliseconds
|
|
43
|
+
*/
|
|
44
|
+
totalDurationMs: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Average duration per iteration in milliseconds
|
|
48
|
+
*/
|
|
49
|
+
avgIterationMs: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the benchmark hit the timeout
|
|
53
|
+
*/
|
|
54
|
+
timedOut: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runs a benchmark operation adaptively based on the performance of the test environment.
|
|
59
|
+
*
|
|
60
|
+
* This harness:
|
|
61
|
+
* 1. Runs warmup iterations to estimate operation time
|
|
62
|
+
* 2. Calculates how many iterations can fit within the target duration
|
|
63
|
+
* 3. Runs the calculated number of iterations with a safety timeout
|
|
64
|
+
*
|
|
65
|
+
* @param operation - The async function to benchmark (should be a single iteration)
|
|
66
|
+
* @param options - Configuration options for the benchmark
|
|
67
|
+
* @returns Results of the benchmark run
|
|
68
|
+
*/
|
|
69
|
+
export async function runAdaptiveBenchmark(
|
|
70
|
+
operation: () => Promise<void>,
|
|
71
|
+
options: BenchmarkOptions = {},
|
|
72
|
+
): Promise<BenchmarkResult> {
|
|
73
|
+
const {
|
|
74
|
+
targetDurationMs = 20_000,
|
|
75
|
+
maxTimeoutMs = 60_000,
|
|
76
|
+
minIterations = 5,
|
|
77
|
+
maxIterations = 10_000,
|
|
78
|
+
warmupIterations = 2,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
// Apply timing multiplier based on environment
|
|
82
|
+
const multiplier = getTimingMultiplier();
|
|
83
|
+
const adjustedTargetMs = targetDurationMs * multiplier;
|
|
84
|
+
const adjustedTimeoutMs = maxTimeoutMs * multiplier;
|
|
85
|
+
|
|
86
|
+
// Debug logging removed to prevent 'Cannot log after tests are done' errors
|
|
87
|
+
|
|
88
|
+
// Run warmup iterations
|
|
89
|
+
|
|
90
|
+
const warmupStart = Date.now();
|
|
91
|
+
for (let i = 0; i < warmupIterations; i++) {
|
|
92
|
+
await operation();
|
|
93
|
+
}
|
|
94
|
+
const warmupDuration = Date.now() - warmupStart;
|
|
95
|
+
const avgWarmupTime = warmupDuration / warmupIterations;
|
|
96
|
+
|
|
97
|
+
// Warmup timing debug info removed to prevent console logging issues
|
|
98
|
+
|
|
99
|
+
// Calculate target iterations based on warmup timing
|
|
100
|
+
// Add 10% safety margin to avoid overshooting
|
|
101
|
+
const safetyMargin = 0.9;
|
|
102
|
+
let targetIterations = Math.floor(
|
|
103
|
+
(adjustedTargetMs * safetyMargin) / avgWarmupTime,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Clamp to min/max bounds
|
|
107
|
+
targetIterations = Math.max(
|
|
108
|
+
minIterations,
|
|
109
|
+
Math.min(maxIterations, targetIterations),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Target iterations debug info removed to prevent console logging issues
|
|
113
|
+
|
|
114
|
+
// Set up timeout promise
|
|
115
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
116
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
117
|
+
timeoutHandle = setTimeout(() => {
|
|
118
|
+
reject(new Error(`Benchmark timeout after ${adjustedTimeoutMs}ms`));
|
|
119
|
+
}, adjustedTimeoutMs);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Run the actual benchmark
|
|
123
|
+
const benchmarkStart = Date.now();
|
|
124
|
+
let completedIterations = 0;
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Run iterations with timeout protection
|
|
129
|
+
await Promise.race([
|
|
130
|
+
(async () => {
|
|
131
|
+
for (let i = 0; i < targetIterations; i++) {
|
|
132
|
+
await operation();
|
|
133
|
+
completedIterations++;
|
|
134
|
+
|
|
135
|
+
// Check if we're approaching the timeout
|
|
136
|
+
const elapsed = Date.now() - benchmarkStart;
|
|
137
|
+
if (elapsed > adjustedTimeoutMs * 0.95) {
|
|
138
|
+
// Approaching timeout - stopping early
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
})(),
|
|
143
|
+
timeoutPromise,
|
|
144
|
+
]);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof Error && error.message.includes("timeout")) {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
// Benchmark timed out
|
|
149
|
+
} else {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalDuration = Date.now() - benchmarkStart;
|
|
157
|
+
const avgIterationTime = totalDuration / completedIterations;
|
|
158
|
+
|
|
159
|
+
const result: BenchmarkResult = {
|
|
160
|
+
iterations: completedIterations,
|
|
161
|
+
totalDurationMs: totalDuration,
|
|
162
|
+
avgIterationMs: avgIterationTime,
|
|
163
|
+
timedOut,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Benchmark results debug info removed to prevent console logging issues
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Helper function to run an operation with adaptive iterations and a callback.
|
|
173
|
+
* This is useful for tests that need to process results after each iteration.
|
|
174
|
+
*
|
|
175
|
+
* @param operation - The async function that returns a value
|
|
176
|
+
* @param callback - Function to process each result
|
|
177
|
+
* @param options - Configuration options for the benchmark
|
|
178
|
+
*/
|
|
179
|
+
export async function runAdaptiveBenchmarkWithCallback<T>(
|
|
180
|
+
operation: () => Promise<T>,
|
|
181
|
+
callback: (result: T, iteration: number) => void | Promise<void>,
|
|
182
|
+
options: BenchmarkOptions = {},
|
|
183
|
+
): Promise<BenchmarkResult> {
|
|
184
|
+
let iterationCount = 0;
|
|
185
|
+
|
|
186
|
+
const wrappedOperation = async () => {
|
|
187
|
+
const result = await operation();
|
|
188
|
+
await callback(result, iterationCount++);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return runAdaptiveBenchmark(wrappedOperation, options);
|
|
192
|
+
}
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { debugLogContext, isDebugEnabled } from "../debuglog";
|
|
2
2
|
|
|
3
|
+
// Ensure clean process state on Windows
|
|
4
|
+
process.on("uncaughtException", (err) => {
|
|
5
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
`Uncaught exception in debuglog-child: ${errorMessage}\n`,
|
|
8
|
+
);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
process.on("unhandledRejection", (reason) => {
|
|
13
|
+
const errorMessage =
|
|
14
|
+
reason instanceof Error ? reason.message : String(reason);
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
`Unhandled rejection in debuglog-child: ${errorMessage}\n`,
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
3
21
|
try {
|
|
4
22
|
const result = {
|
|
5
23
|
isDebugEnabled: isDebugEnabled(),
|
|
6
24
|
debugLogContext: debugLogContext(),
|
|
7
25
|
};
|
|
8
|
-
|
|
26
|
+
// Use process.stdout.write to ensure clean output
|
|
27
|
+
process.stdout.write(JSON.stringify(result));
|
|
9
28
|
process.exit(0);
|
|
10
29
|
} catch (err) {
|
|
11
|
-
|
|
30
|
+
// Don't log the error object directly as it might have circular references
|
|
31
|
+
// that cause issues with Jest's message passing on Windows
|
|
32
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
33
|
+
process.stderr.write(`Error in debuglog-child: ${errorMessage}\n`);
|
|
34
|
+
|
|
35
|
+
// Also log stack trace for debugging
|
|
36
|
+
if (err instanceof Error && err.stack) {
|
|
37
|
+
process.stderr.write(`Stack trace:\n${err.stack}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
process.exit(1);
|
|
13
41
|
}
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import { debug } from "../debuglog";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
// Ensure clean process state on Windows
|
|
4
|
+
process.on("uncaughtException", (err) => {
|
|
5
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
`Uncaught exception in debuglog-enabled-child: ${errorMessage}\n`,
|
|
8
|
+
);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
process.on("unhandledRejection", (reason) => {
|
|
13
|
+
const errorMessage =
|
|
14
|
+
reason instanceof Error ? reason.message : String(reason);
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
`Unhandled rejection in debuglog-enabled-child: ${errorMessage}\n`,
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// This will be run with NODE_DEBUG set, so debug should be enabled
|
|
23
|
+
debug("test message %s %d", "hello", 42);
|
|
24
|
+
debug("simple message");
|
|
25
|
+
debug("object %o", { key: "value" });
|
|
26
|
+
|
|
27
|
+
// Signal successful completion using stdout.write for clean output
|
|
28
|
+
process.stdout.write("DONE");
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
32
|
+
process.stderr.write(`Error in debuglog-enabled-child: ${errorMessage}\n`);
|
|
33
|
+
|
|
34
|
+
// Also log stack trace for debugging
|
|
35
|
+
if (err instanceof Error && err.stack) {
|
|
36
|
+
process.stderr.write(`Stack trace:\n${err.stack}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Jest setup file - runs before all tests
|
|
2
|
+
// Configures global test environment and timeouts
|
|
3
|
+
|
|
4
|
+
import { jest } from "@jest/globals";
|
|
5
|
+
import { getTestTimeout } from "./test-timeout-config";
|
|
6
|
+
|
|
7
|
+
// Configure Jest timeout globally for all tests
|
|
8
|
+
jest.setTimeout(getTestTimeout());
|
|
9
|
+
|
|
10
|
+
// Set consistent timezone for tests (similar to PhotoStructure approach)
|
|
11
|
+
process.env["TZ"] = "America/Los_Angeles";
|
|
12
|
+
|
|
13
|
+
// Ensure test environment
|
|
14
|
+
process.env["NODE_ENV"] = "test";
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { env, platform } from "node:process";
|
|
6
|
+
import { delay } from "../async";
|
|
7
|
+
import {
|
|
8
|
+
getAllVolumeMetadata,
|
|
9
|
+
getVolumeMetadata,
|
|
10
|
+
getVolumeMountPoints,
|
|
11
|
+
isHidden,
|
|
12
|
+
setHidden,
|
|
13
|
+
} from "../index";
|
|
14
|
+
import { randomLetters } from "../random";
|
|
15
|
+
import { MiB } from "../units";
|
|
16
|
+
import { runAdaptiveBenchmarkWithCallback } from "./benchmark-harness";
|
|
17
|
+
import { getTimingMultiplier } from "./test-timeout-config";
|
|
18
|
+
|
|
19
|
+
// Core memory testing logic for the standalone memory test runner
|
|
20
|
+
// This module contains all memory leak detection tests, designed to run
|
|
21
|
+
// outside of Jest for more accurate memory measurements and to avoid
|
|
22
|
+
// Jest worker process issues on Windows CI.
|
|
23
|
+
|
|
24
|
+
// Simple assertion helper
|
|
25
|
+
function assert(condition: boolean, message: string): void {
|
|
26
|
+
if (!condition) {
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Enable garbage collection access
|
|
32
|
+
declare const global: {
|
|
33
|
+
gc?: () => void;
|
|
34
|
+
} & typeof globalThis;
|
|
35
|
+
|
|
36
|
+
// Helper function to get a temporary directory that's not hidden
|
|
37
|
+
function tmpDirNotHidden(): string {
|
|
38
|
+
const isMacOS = platform === "darwin";
|
|
39
|
+
const isWindows = platform === "win32";
|
|
40
|
+
|
|
41
|
+
const dir = isMacOS
|
|
42
|
+
? join(homedir(), "tmp")
|
|
43
|
+
: isWindows
|
|
44
|
+
? join(env["SystemDrive"] ?? "C:\\", "tmp")
|
|
45
|
+
: "/tmp";
|
|
46
|
+
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MemoryTestResult {
|
|
52
|
+
testName: string;
|
|
53
|
+
passed: boolean;
|
|
54
|
+
initialMemory: number;
|
|
55
|
+
finalMemory: number;
|
|
56
|
+
memoryIncrease: number;
|
|
57
|
+
slope: number;
|
|
58
|
+
errorMessage?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Helper to get memory usage after GC
|
|
62
|
+
async function getMemoryUsage(): Promise<number> {
|
|
63
|
+
// Give things a bit to fall out of scope
|
|
64
|
+
// Use dynamic delay based on environment
|
|
65
|
+
const delayMs = Math.max(10, 100 * getTimingMultiplier());
|
|
66
|
+
await delay(delayMs);
|
|
67
|
+
if (global.gc) {
|
|
68
|
+
global.gc();
|
|
69
|
+
}
|
|
70
|
+
await delay(delayMs);
|
|
71
|
+
return process.memoryUsage().heapUsed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detects the likelihood of a memory leak based on memory usage values.
|
|
76
|
+
* @param memoryUsages - Array of memory usage values.
|
|
77
|
+
* @returns A boolean indicating if a memory leak is likely.
|
|
78
|
+
*/
|
|
79
|
+
function leastSquaresSlope(memoryUsages: number[]): number {
|
|
80
|
+
const n = memoryUsages.length;
|
|
81
|
+
if (n < 2) return 0;
|
|
82
|
+
|
|
83
|
+
// Calculate the slope using linear regression (least squares method)
|
|
84
|
+
const xSum = (n * (n - 1)) / 2;
|
|
85
|
+
const ySum = memoryUsages.reduce((sum, usage) => sum + usage, 0);
|
|
86
|
+
const xySum = memoryUsages.reduce((sum, usage, i) => sum + i * usage, 0);
|
|
87
|
+
const xSquaredSum = (n * (n - 1) * (2 * n - 1)) / 6;
|
|
88
|
+
|
|
89
|
+
const result = (n * xySum - xSum * ySum) / (n * xSquaredSum - xSum * xSum);
|
|
90
|
+
|
|
91
|
+
// If the slope is positive and significant, it indicates a memory leak
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Helper to check if memory usage is stable
|
|
96
|
+
async function checkMemoryUsage(
|
|
97
|
+
operation: () => Promise<unknown>,
|
|
98
|
+
errorMarginBytes: number = 10 * MiB, // Alpine docker had a 5MB variance
|
|
99
|
+
maxAllowedSlope: number = 0.01,
|
|
100
|
+
): Promise<{
|
|
101
|
+
passed: boolean;
|
|
102
|
+
initialMemory: number;
|
|
103
|
+
finalMemory: number;
|
|
104
|
+
slope: number;
|
|
105
|
+
errorMessage?: string;
|
|
106
|
+
}> {
|
|
107
|
+
// warm up memory consumption:
|
|
108
|
+
for (let i = 0; i < 5; i++) await operation();
|
|
109
|
+
|
|
110
|
+
// __then__ take a snapshot
|
|
111
|
+
const initialMemory = await getMemoryUsage();
|
|
112
|
+
const memoryUsages: number[] = [1];
|
|
113
|
+
|
|
114
|
+
// Run operations using adaptive benchmark
|
|
115
|
+
await runAdaptiveBenchmarkWithCallback(
|
|
116
|
+
operation,
|
|
117
|
+
async (_result, iteration) => {
|
|
118
|
+
// Take memory snapshots approximately 10 times during the benchmark
|
|
119
|
+
// Check every 10 iterations initially, will adjust based on actual runtime
|
|
120
|
+
if (iteration === 0 || iteration % 10 === 0) {
|
|
121
|
+
const currentMemory = await getMemoryUsage();
|
|
122
|
+
memoryUsages.push(currentMemory / initialMemory);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
targetDurationMs: 10_000, // Reduced from 20s to 10s for faster CI
|
|
127
|
+
maxTimeoutMs: 30_000, // Reduced from 60s to 30s
|
|
128
|
+
minIterations: 10,
|
|
129
|
+
debug: !!process.env["DEBUG_BENCHMARK"],
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Final memory check
|
|
134
|
+
const finalMemory = await getMemoryUsage();
|
|
135
|
+
memoryUsages.push(finalMemory / initialMemory);
|
|
136
|
+
const slope = leastSquaresSlope(memoryUsages);
|
|
137
|
+
|
|
138
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
139
|
+
let errorMessage: string | undefined;
|
|
140
|
+
|
|
141
|
+
if (memoryIncrease >= errorMarginBytes) {
|
|
142
|
+
errorMessage = `Memory increased by ${(memoryIncrease / MiB).toFixed(2)} MiB, exceeding limit of ${(errorMarginBytes / MiB).toFixed(2)} MiB`;
|
|
143
|
+
} else if (slope >= maxAllowedSlope) {
|
|
144
|
+
errorMessage = `Memory slope ${slope.toFixed(4)} exceeds limit of ${maxAllowedSlope}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
passed: !errorMessage,
|
|
149
|
+
initialMemory,
|
|
150
|
+
finalMemory,
|
|
151
|
+
slope,
|
|
152
|
+
...(errorMessage && { errorMessage }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Memory test implementations
|
|
157
|
+
export async function testVolumeMountPointsNoLeak(): Promise<MemoryTestResult> {
|
|
158
|
+
const result = await checkMemoryUsage(async () => {
|
|
159
|
+
const mountPoints = await getVolumeMountPoints();
|
|
160
|
+
if (mountPoints.length === 0) {
|
|
161
|
+
throw new Error("Expected at least one mount point");
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
testName: "getVolumeMountPoints - no memory leak",
|
|
167
|
+
passed: result.passed,
|
|
168
|
+
initialMemory: result.initialMemory,
|
|
169
|
+
finalMemory: result.finalMemory,
|
|
170
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
171
|
+
slope: result.slope,
|
|
172
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function testVolumeMountPointsErrorConditions(): Promise<MemoryTestResult> {
|
|
177
|
+
const result = await checkMemoryUsage(async () => {
|
|
178
|
+
try {
|
|
179
|
+
await getVolumeMountPoints({ timeoutMs: 1 });
|
|
180
|
+
} catch {
|
|
181
|
+
// Expected
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
testName: "getVolumeMountPoints - error conditions",
|
|
187
|
+
passed: result.passed,
|
|
188
|
+
initialMemory: result.initialMemory,
|
|
189
|
+
finalMemory: result.finalMemory,
|
|
190
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
191
|
+
slope: result.slope,
|
|
192
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function testGetAllVolumeMetadataNoLeak(): Promise<MemoryTestResult> {
|
|
197
|
+
const result = await checkMemoryUsage(async () => {
|
|
198
|
+
const metadata = await getAllVolumeMetadata();
|
|
199
|
+
if (metadata.length === 0) {
|
|
200
|
+
throw new Error("Expected at least one volume");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
testName: "getAllVolumeMetadata - no memory leak",
|
|
206
|
+
passed: result.passed,
|
|
207
|
+
initialMemory: result.initialMemory,
|
|
208
|
+
finalMemory: result.finalMemory,
|
|
209
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
210
|
+
slope: result.slope,
|
|
211
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function testGetVolumeMetadataErrorConditions(): Promise<MemoryTestResult> {
|
|
216
|
+
const result = await checkMemoryUsage(async () => {
|
|
217
|
+
try {
|
|
218
|
+
await getVolumeMetadata("nonexistent");
|
|
219
|
+
} catch {
|
|
220
|
+
// Expected
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
testName: "getVolumeMetadata - error conditions",
|
|
226
|
+
passed: result.passed,
|
|
227
|
+
initialMemory: result.initialMemory,
|
|
228
|
+
finalMemory: result.finalMemory,
|
|
229
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
230
|
+
slope: result.slope,
|
|
231
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function testIsHiddenSetHiddenNoLeak(): Promise<MemoryTestResult> {
|
|
236
|
+
const testDir = await mkdtemp(join(tmpDirNotHidden(), "memory-tests-"));
|
|
237
|
+
let counter = 0;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await checkMemoryUsage(async () => {
|
|
241
|
+
// Create a unique subdirectory for each iteration to avoid path conflicts
|
|
242
|
+
const iterationDir = join(testDir, `iteration-${counter++}`);
|
|
243
|
+
|
|
244
|
+
// Simple validateHidden implementation without Jest dependencies
|
|
245
|
+
mkdirSync(iterationDir, { recursive: true });
|
|
246
|
+
|
|
247
|
+
// Test isHidden on a regular directory (should be false)
|
|
248
|
+
const isHiddenResult = await isHidden(iterationDir);
|
|
249
|
+
assert(
|
|
250
|
+
isHiddenResult === false,
|
|
251
|
+
`Expected ${iterationDir} to not be hidden`,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Test setHidden to true
|
|
255
|
+
// On Linux, setHidden renames the file/dir by adding a dot prefix,
|
|
256
|
+
// so we need to capture the returned pathname
|
|
257
|
+
const hiddenPath = (await setHidden(iterationDir, true)).pathname;
|
|
258
|
+
const isHiddenAfterSet = await isHidden(hiddenPath);
|
|
259
|
+
assert(
|
|
260
|
+
isHiddenAfterSet === true,
|
|
261
|
+
`Expected ${hiddenPath} to be hidden after setHidden(true)`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Test setHidden to false
|
|
265
|
+
const visiblePath = (await setHidden(hiddenPath, false)).pathname;
|
|
266
|
+
const isHiddenAfterUnset = await isHidden(visiblePath);
|
|
267
|
+
assert(
|
|
268
|
+
isHiddenAfterUnset === false,
|
|
269
|
+
`Expected ${visiblePath} to not be hidden after setHidden(false)`,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
testName: "isHidden/setHidden - no memory leak",
|
|
275
|
+
passed: result.passed,
|
|
276
|
+
initialMemory: result.initialMemory,
|
|
277
|
+
finalMemory: result.finalMemory,
|
|
278
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
279
|
+
slope: result.slope,
|
|
280
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
281
|
+
};
|
|
282
|
+
} finally {
|
|
283
|
+
await rm(testDir, { recursive: true, force: true }).catch(() => null);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function testIsHiddenSetHiddenErrorConditions(): Promise<MemoryTestResult> {
|
|
288
|
+
const testDir = await mkdtemp(join(tmpDirNotHidden(), "memory-tests-"));
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const result = await checkMemoryUsage(async () => {
|
|
292
|
+
const notafile = join(testDir, "nonexistent", "file-" + randomLetters(8));
|
|
293
|
+
try {
|
|
294
|
+
await isHidden(notafile);
|
|
295
|
+
} catch {
|
|
296
|
+
// Expected
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
await setHidden(notafile, true);
|
|
300
|
+
} catch {
|
|
301
|
+
// Expected
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
testName: "isHidden/setHidden - error conditions",
|
|
307
|
+
passed: result.passed,
|
|
308
|
+
initialMemory: result.initialMemory,
|
|
309
|
+
finalMemory: result.finalMemory,
|
|
310
|
+
memoryIncrease: result.finalMemory - result.initialMemory,
|
|
311
|
+
slope: result.slope,
|
|
312
|
+
...(result.errorMessage && { errorMessage: result.errorMessage }),
|
|
313
|
+
};
|
|
314
|
+
} finally {
|
|
315
|
+
await rm(testDir, { recursive: true, force: true }).catch(() => null);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Run all memory tests
|
|
320
|
+
export async function runAllMemoryTests(): Promise<MemoryTestResult[]> {
|
|
321
|
+
if (!global.gc) {
|
|
322
|
+
throw new Error("Garbage collection must be exposed. Run with --expose-gc");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const results: MemoryTestResult[] = [];
|
|
326
|
+
|
|
327
|
+
// Run each test and collect results
|
|
328
|
+
results.push(await testVolumeMountPointsNoLeak());
|
|
329
|
+
results.push(await testVolumeMountPointsErrorConditions());
|
|
330
|
+
results.push(await testGetAllVolumeMetadataNoLeak());
|
|
331
|
+
results.push(await testGetVolumeMetadataErrorConditions());
|
|
332
|
+
results.push(await testIsHiddenSetHiddenNoLeak());
|
|
333
|
+
results.push(await testIsHiddenSetHiddenErrorConditions());
|
|
334
|
+
|
|
335
|
+
return results;
|
|
336
|
+
}
|