@photostructure/fs-metadata 0.5.0 → 0.5.1

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.
Files changed (130) hide show
  1. package/C++_REVIEW_TODO.md +1 -1
  2. package/CHANGELOG.md +11 -2
  3. package/CLAUDE.md +269 -92
  4. package/CONTRIBUTING.md +41 -0
  5. package/README.md +20 -1
  6. package/WINDOWS_DEBUG_GUIDE.md +89 -0
  7. package/binding.gyp +3 -2
  8. package/dist/index.cjs +1440 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +360 -0
  11. package/dist/index.d.mts +360 -0
  12. package/dist/index.d.ts +360 -0
  13. package/dist/index.mjs +1398 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/jest.config.cjs +1 -0
  16. package/package.json +25 -27
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  20. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  22. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  23. package/scripts/check-memory.mjs +53 -173
  24. package/scripts/clang-tidy.ts +241 -0
  25. package/scripts/prebuild-linux-glibc.sh +108 -0
  26. package/scripts/precommit.ts +45 -0
  27. package/scripts/sanitizers-test.sh +157 -0
  28. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  29. package/scripts/valgrind-test.mjs +1 -1
  30. package/scripts/{valgrind.sh → valgrind-test.sh} +15 -0
  31. package/src/binding.cpp +1 -1
  32. package/src/common/error_utils.h +17 -0
  33. package/src/common/metadata_worker.h +4 -1
  34. package/src/darwin/hidden.cpp +13 -6
  35. package/src/darwin/volume_metadata.cpp +2 -2
  36. package/src/darwin/volume_mount_points.cpp +8 -1
  37. package/src/linux/blkid_cache.cpp +8 -2
  38. package/src/linux/volume_metadata.cpp +1 -1
  39. package/src/platform.ts +25 -0
  40. package/src/test-utils/benchmark-harness.ts +192 -0
  41. package/src/test-utils/debuglog-child.ts +30 -2
  42. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  43. package/src/test-utils/jest-setup.ts +14 -0
  44. package/src/test-utils/worker-thread-helper.cjs +92 -0
  45. package/src/windows/hidden.cpp +20 -11
  46. package/coverage/base.css +0 -224
  47. package/coverage/block-navigation.js +0 -87
  48. package/coverage/favicon.png +0 -0
  49. package/coverage/index.html +0 -131
  50. package/coverage/lcov-report/base.css +0 -224
  51. package/coverage/lcov-report/block-navigation.js +0 -87
  52. package/coverage/lcov-report/favicon.png +0 -0
  53. package/coverage/lcov-report/index.html +0 -131
  54. package/coverage/lcov-report/prettify.css +0 -1
  55. package/coverage/lcov-report/prettify.js +0 -2
  56. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  57. package/coverage/lcov-report/sorter.js +0 -196
  58. package/coverage/lcov-report/src/array.ts.html +0 -217
  59. package/coverage/lcov-report/src/async.ts.html +0 -547
  60. package/coverage/lcov-report/src/debuglog.ts.html +0 -187
  61. package/coverage/lcov-report/src/defer.ts.html +0 -175
  62. package/coverage/lcov-report/src/dirname.ts.html +0 -124
  63. package/coverage/lcov-report/src/error.ts.html +0 -322
  64. package/coverage/lcov-report/src/fs.ts.html +0 -316
  65. package/coverage/lcov-report/src/glob.ts.html +0 -472
  66. package/coverage/lcov-report/src/hidden.ts.html +0 -724
  67. package/coverage/lcov-report/src/index.html +0 -521
  68. package/coverage/lcov-report/src/index.ts.html +0 -676
  69. package/coverage/lcov-report/src/linux/dev_disk.ts.html +0 -316
  70. package/coverage/lcov-report/src/linux/index.html +0 -146
  71. package/coverage/lcov-report/src/linux/mount_points.ts.html +0 -364
  72. package/coverage/lcov-report/src/linux/mtab.ts.html +0 -493
  73. package/coverage/lcov-report/src/mount_point.ts.html +0 -106
  74. package/coverage/lcov-report/src/number.ts.html +0 -148
  75. package/coverage/lcov-report/src/object.ts.html +0 -265
  76. package/coverage/lcov-report/src/options.ts.html +0 -475
  77. package/coverage/lcov-report/src/path.ts.html +0 -268
  78. package/coverage/lcov-report/src/platform.ts.html +0 -112
  79. package/coverage/lcov-report/src/random.ts.html +0 -205
  80. package/coverage/lcov-report/src/remote_info.ts.html +0 -553
  81. package/coverage/lcov-report/src/stack_path.ts.html +0 -298
  82. package/coverage/lcov-report/src/string.ts.html +0 -382
  83. package/coverage/lcov-report/src/string_enum.ts.html +0 -208
  84. package/coverage/lcov-report/src/system_volume.ts.html +0 -301
  85. package/coverage/lcov-report/src/unc.ts.html +0 -274
  86. package/coverage/lcov-report/src/units.ts.html +0 -274
  87. package/coverage/lcov-report/src/uuid.ts.html +0 -157
  88. package/coverage/lcov-report/src/volume_health_status.ts.html +0 -259
  89. package/coverage/lcov-report/src/volume_metadata.ts.html +0 -787
  90. package/coverage/lcov-report/src/volume_mount_points.ts.html +0 -388
  91. package/coverage/lcov.info +0 -3581
  92. package/coverage/prettify.css +0 -1
  93. package/coverage/prettify.js +0 -2
  94. package/coverage/sort-arrow-sprite.png +0 -0
  95. package/coverage/sorter.js +0 -196
  96. package/coverage/src/array.ts.html +0 -217
  97. package/coverage/src/async.ts.html +0 -547
  98. package/coverage/src/debuglog.ts.html +0 -187
  99. package/coverage/src/defer.ts.html +0 -175
  100. package/coverage/src/dirname.ts.html +0 -124
  101. package/coverage/src/error.ts.html +0 -322
  102. package/coverage/src/fs.ts.html +0 -316
  103. package/coverage/src/glob.ts.html +0 -472
  104. package/coverage/src/hidden.ts.html +0 -724
  105. package/coverage/src/index.html +0 -521
  106. package/coverage/src/index.ts.html +0 -676
  107. package/coverage/src/linux/dev_disk.ts.html +0 -316
  108. package/coverage/src/linux/index.html +0 -146
  109. package/coverage/src/linux/mount_points.ts.html +0 -364
  110. package/coverage/src/linux/mtab.ts.html +0 -493
  111. package/coverage/src/mount_point.ts.html +0 -106
  112. package/coverage/src/number.ts.html +0 -148
  113. package/coverage/src/object.ts.html +0 -265
  114. package/coverage/src/options.ts.html +0 -475
  115. package/coverage/src/path.ts.html +0 -268
  116. package/coverage/src/platform.ts.html +0 -112
  117. package/coverage/src/random.ts.html +0 -205
  118. package/coverage/src/remote_info.ts.html +0 -553
  119. package/coverage/src/stack_path.ts.html +0 -298
  120. package/coverage/src/string.ts.html +0 -382
  121. package/coverage/src/string_enum.ts.html +0 -208
  122. package/coverage/src/system_volume.ts.html +0 -301
  123. package/coverage/src/unc.ts.html +0 -274
  124. package/coverage/src/units.ts.html +0 -274
  125. package/coverage/src/uuid.ts.html +0 -157
  126. package/coverage/src/volume_health_status.ts.html +0 -259
  127. package/coverage/src/volume_metadata.ts.html +0 -787
  128. package/coverage/src/volume_mount_points.ts.html +0 -388
  129. package/scripts/clang-tidy.mjs +0 -73
  130. package/scripts/run-asan.sh +0 -92
@@ -0,0 +1,157 @@
1
+ #!/bin/bash
2
+ # AddressSanitizer and LeakSanitizer test runner for @photostructure/fs-metadata
3
+ # Runs comprehensive memory safety checks on native code
4
+
5
+ set -euo pipefail
6
+
7
+ # Check if we're on Linux
8
+ if [[ "$OSTYPE" != "linux-gnu"* ]]; then
9
+ echo "AddressSanitizer tests are only supported on Linux"
10
+ exit 0
11
+ fi
12
+
13
+ # Check for clang
14
+ if ! command -v clang &> /dev/null; then
15
+ echo "Error: clang is required for AddressSanitizer tests"
16
+ echo "Install with: sudo apt-get install clang"
17
+ exit 1
18
+ fi
19
+
20
+ # Colors for output
21
+ RED='\033[0;31m'
22
+ GREEN='\033[0;32m'
23
+ YELLOW='\033[1;33m'
24
+ BLUE='\033[0;34m'
25
+ NC='\033[0m' # No Color
26
+
27
+ # Configuration
28
+ CLEAN_BUILD=${CLEAN_BUILD:-1}
29
+ VERBOSE=${VERBOSE:-0}
30
+ OUTPUT_FILE="asan-output.log"
31
+
32
+ echo -e "${YELLOW}Running AddressSanitizer and LeakSanitizer tests...${NC}"
33
+
34
+ # Clean previous builds if requested
35
+ if [[ "$CLEAN_BUILD" == "1" ]]; then
36
+ echo "Cleaning previous builds..."
37
+ rm -rf build/
38
+ fi
39
+ rm -f "$OUTPUT_FILE"
40
+
41
+ # Set up build environment
42
+ export CC=clang
43
+ export CXX=clang++
44
+ export CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
45
+ export CXXFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1"
46
+ export LDFLAGS="-fsanitize=address"
47
+
48
+ # Comprehensive ASAN options combining both implementations
49
+ export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:print_stats=1:check_initialization_order=1:strict_init_order=1:print_module_map=1"
50
+ export LSAN_OPTIONS="suppressions=$(pwd)/.lsan-suppressions.txt:print_suppressions=0"
51
+
52
+ # Increase Node.js heap size for ASan overhead
53
+ export NODE_OPTIONS="--max-old-space-size=8192"
54
+
55
+ # Find and set ASan runtime library
56
+ echo "Detecting ASan runtime library..."
57
+ ASAN_LIB=$(clang -print-file-name=libclang_rt.asan-x86_64.so 2>/dev/null || echo "")
58
+
59
+ if [[ -n "$ASAN_LIB" && "$ASAN_LIB" != *"not found"* && -f "$ASAN_LIB" ]]; then
60
+ export LD_PRELOAD="$ASAN_LIB"
61
+ echo -e "${BLUE}Using ASan library: $ASAN_LIB${NC}"
62
+ else
63
+ # Try common paths as fallback
64
+ for lib in /usr/lib/x86_64-linux-gnu/libasan.so.{8,6} /usr/lib64/libasan.so.{8,6}; do
65
+ if [[ -f "$lib" ]]; then
66
+ export LD_PRELOAD="$lib"
67
+ echo -e "${BLUE}Using ASan library: $lib${NC}"
68
+ break
69
+ fi
70
+ done
71
+ fi
72
+
73
+ if [[ -z "${LD_PRELOAD:-}" ]]; then
74
+ echo -e "${YELLOW}Warning: Could not find ASan runtime library${NC}"
75
+ fi
76
+
77
+ # Build the native module
78
+ echo "Building with AddressSanitizer..."
79
+ npm run setup:native
80
+ npm run clean:native
81
+ node-gyp configure build
82
+
83
+ # Run tests and capture output
84
+ echo -e "${YELLOW}Running tests with AddressSanitizer...${NC}"
85
+ set +e # Don't exit on test failure
86
+ npm test -- --no-coverage 2>&1 | tee "$OUTPUT_FILE"
87
+ TEST_EXIT_CODE=${PIPESTATUS[0]}
88
+ set -e
89
+
90
+ echo -e "${BLUE}\nFull ASAN output saved to: $OUTPUT_FILE${NC}"
91
+
92
+ # Analyze output for errors specific to our code
93
+ echo -e "\n${YELLOW}Analyzing ASAN output...${NC}"
94
+
95
+ # Count different types of issues
96
+ OUR_ERRORS=0
97
+ OUR_LEAKS=0
98
+ INTERNAL_LEAKS=0
99
+
100
+ # Check for ASAN errors in our code (not V8/Node internals)
101
+ if grep -E "(ERROR: AddressSanitizer|ERROR: LeakSanitizer)" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/)" > /dev/null; then
102
+ OUR_ERRORS=1
103
+ fi
104
+
105
+ # Check for direct/indirect leaks in our code with context
106
+ while IFS= read -r line_num; do
107
+ # Get 5 lines before and 10 lines after for context
108
+ start=$((line_num - 5))
109
+ end=$((line_num + 10))
110
+ if sed -n "${start},${end}p" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/|photostructure)" > /dev/null; then
111
+ OUR_LEAKS=1
112
+ break
113
+ fi
114
+ done < <(grep -n "Direct leak\|Indirect leak" "$OUTPUT_FILE" | cut -d: -f1)
115
+
116
+ # Count V8/Node internal leaks for information
117
+ INTERNAL_LEAKS=$(grep -c "leak.*\/usr\/bin\/node" "$OUTPUT_FILE" 2>/dev/null || echo "0")
118
+ INTERNAL_LEAKS="${INTERNAL_LEAKS//[[:space:]]/}" # Remove any whitespace
119
+
120
+ # Report results
121
+ EXIT_CODE=0
122
+
123
+ if [[ "$OUR_ERRORS" -eq 1 ]]; then
124
+ echo -e "${RED}\n✗ AddressSanitizer found errors in fs-metadata code:${NC}"
125
+ grep -E "(ERROR: AddressSanitizer|ERROR: LeakSanitizer)" "$OUTPUT_FILE" | grep -E "(fs_metadata\.node|/src/)" | head -20
126
+ EXIT_CODE=1
127
+ fi
128
+
129
+ if [[ "$OUR_LEAKS" -eq 1 ]]; then
130
+ echo -e "${RED}\n✗ LeakSanitizer found memory leaks in fs-metadata code:${NC}"
131
+ # Show leak summary
132
+ grep -A 5 "SUMMARY: AddressSanitizer" "$OUTPUT_FILE" || true
133
+ EXIT_CODE=1
134
+ fi
135
+
136
+ if [[ "$EXIT_CODE" -eq 0 ]]; then
137
+ echo -e "${GREEN}\n✓ AddressSanitizer and LeakSanitizer tests passed (no issues in fs-metadata code)${NC}"
138
+ if [[ "$INTERNAL_LEAKS" -gt 0 ]]; then
139
+ echo -e "${YELLOW} Note: $INTERNAL_LEAKS V8/Node.js internal leaks detected (suppressed)${NC}"
140
+ fi
141
+ else
142
+ echo -e "${RED}\n✗ Memory safety issues detected!${NC}"
143
+ echo -e "${YELLOW}See $OUTPUT_FILE for full details${NC}"
144
+ fi
145
+
146
+ # Show ASAN statistics if verbose
147
+ if [[ "$VERBOSE" -eq 1 ]] && grep -q "Stats:" "$OUTPUT_FILE"; then
148
+ echo -e "\n${BLUE}ASAN Statistics:${NC}"
149
+ grep -A 20 "Stats:" "$OUTPUT_FILE" | head -20
150
+ fi
151
+
152
+ # Clean build artifacts to ensure no ASAN-compiled code remains
153
+ echo -e "\n${YELLOW}Cleaning build artifacts...${NC}"
154
+ npm run clean:native > /dev/null 2>&1
155
+ echo -e "${GREEN}✓ Build artifacts cleaned${NC}"
156
+
157
+ exit $EXIT_CODE
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // scripts/configure.mjs
3
+ // scripts/setup-native.mjs
4
+
5
+ // This script sets up the config.gypi file to include GIO support when available.
6
+ // It should be run before building native modules with node-gyp.
4
7
 
5
8
  import { execSync } from "node:child_process";
6
9
  import { writeFileSync } from "node:fs";
@@ -15,7 +15,7 @@ import {
15
15
  getVolumeMetadata,
16
16
  getVolumeMountPoints,
17
17
  isHidden,
18
- } from "../dist/index.js";
18
+ } from "../dist/index.mjs";
19
19
 
20
20
  async function runTests() {
21
21
  console.log("Starting valgrind memory leak tests...");
@@ -35,6 +35,21 @@ if [ ! -f "$VALGRIND_TEST" ]; then
35
35
  exit 1
36
36
  fi
37
37
 
38
+ # Check if dist directory exists (indicates build completed)
39
+ if [ ! -d "$ROOT_DIR/dist" ]; then
40
+ echo -e "${RED}Error: dist/ directory not found. Run 'npm run build:dist' first.${NC}"
41
+ exit 1
42
+ fi
43
+
44
+ # Pre-flight check: run the test script without valgrind first
45
+ echo "Running pre-flight check..."
46
+ if ! node "$VALGRIND_TEST" > /dev/null 2>&1; then
47
+ echo -e "${RED}Error: Test script failed to run. Running again to show error:${NC}"
48
+ node "$VALGRIND_TEST"
49
+ exit 1
50
+ fi
51
+ echo -e "${GREEN}✓ Pre-flight check passed${NC}"
52
+
38
53
  # Use the committed suppressions file
39
54
  SUPP_FILE="$ROOT_DIR/.valgrind.supp"
40
55
 
package/src/binding.cpp CHANGED
@@ -93,6 +93,6 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
93
93
  return exports;
94
94
  }
95
95
 
96
- NODE_API_MODULE(node_fs_meta, Init)
96
+ NODE_API_MODULE(fs_metadata, Init)
97
97
 
98
98
  } // namespace
@@ -1,5 +1,6 @@
1
1
  // src/common/error_utils.h
2
2
  #pragma once
3
+ #include <cstring>
3
4
  #include <stdexcept>
4
5
  #include <string>
5
6
 
@@ -11,9 +12,25 @@ public:
11
12
  : std::runtime_error(message) {}
12
13
  };
13
14
 
15
+ // Simple error code only version
14
16
  inline std::string CreateErrorMessage(const char *operation, int error) {
15
17
  return std::string(operation) +
16
18
  " failed with error: " + std::to_string(error);
17
19
  }
18
20
 
21
+ // Convenience function for common pattern: "operation failed for 'path': error"
22
+ inline std::string CreatePathErrorMessage(const char *operation,
23
+ const std::string &path, int error) {
24
+ return std::string(operation) + " failed for '" + path +
25
+ "': " + std::string(strerror(error)) + " (" + std::to_string(error) +
26
+ ")";
27
+ }
28
+
29
+ // For operations without a path context
30
+ inline std::string CreateDetailedErrorMessage(const char *operation,
31
+ int error) {
32
+ return std::string(operation) + " failed: " + std::string(strerror(error)) +
33
+ " (" + std::to_string(error) + ")";
34
+ }
35
+
19
36
  } // namespace FSMeta
@@ -20,7 +20,10 @@ protected:
20
20
  deferred_.Reject(error.Value());
21
21
  }
22
22
 
23
- void OnOK() override { deferred_.Resolve(metadata.ToObject(Env())); }
23
+ void OnOK() override {
24
+ Napi::HandleScope scope(Env());
25
+ deferred_.Resolve(metadata.ToObject(Env()));
26
+ }
24
27
  }; // class MetadataWorkerBase
25
28
 
26
29
  } // namespace FSMeta
@@ -1,6 +1,7 @@
1
1
  // src/darwin/hidden.cpp
2
2
  #include "hidden.h"
3
3
  #include "../common/debug_log.h"
4
+ #include "../common/error_utils.h"
4
5
  #include <sys/stat.h>
5
6
  #include <unistd.h>
6
7
 
@@ -27,11 +28,11 @@ void GetHiddenWorker::Execute() {
27
28
  int error = errno;
28
29
  if (error == ENOENT) {
29
30
  DEBUG_LOG("[GetHiddenWorker] path not found: %s", path_.c_str());
30
- SetError("Path not found");
31
+ SetError("Path not found: '" + path_ + "'");
31
32
  } else {
32
33
  DEBUG_LOG("[GetHiddenWorker] failed to stat path %s: %s (%d)",
33
34
  path_.c_str(), strerror(error), error);
34
- SetError(std::string("Failed to stat path: ") + strerror(error));
35
+ SetError(CreatePathErrorMessage("stat", path_, error));
35
36
  }
36
37
  return;
37
38
  }
@@ -41,11 +42,13 @@ void GetHiddenWorker::Execute() {
41
42
  }
42
43
 
43
44
  void GetHiddenWorker::OnOK() {
45
+ Napi::HandleScope scope(Env());
44
46
  auto env = Env();
45
47
  deferred_.Resolve(Napi::Boolean::New(env, is_hidden_));
46
48
  }
47
49
 
48
50
  void GetHiddenWorker::OnError(const Napi::Error &error) {
51
+ Napi::HandleScope scope(Env());
49
52
  deferred_.Reject(error.Value());
50
53
  }
51
54
 
@@ -92,11 +95,11 @@ void SetHiddenWorker::Execute() {
92
95
  int error = errno;
93
96
  if (error == ENOENT) {
94
97
  DEBUG_LOG("[SetHiddenWorker] path not found: %s", path_.c_str());
95
- SetError("Path not found");
98
+ SetError("Path not found: '" + path_ + "'");
96
99
  } else {
97
100
  DEBUG_LOG("[SetHiddenWorker] failed to stat path %s: %s (%d)",
98
101
  path_.c_str(), strerror(error), error);
99
- SetError(std::string("Failed to stat path: ") + strerror(error));
102
+ SetError(CreatePathErrorMessage("stat", path_, error));
100
103
  }
101
104
  return;
102
105
  }
@@ -109,8 +112,10 @@ void SetHiddenWorker::Execute() {
109
112
  }
110
113
 
111
114
  if (chflags(path_.c_str(), new_flags) != 0) {
112
- DEBUG_LOG("[SetHiddenWorker] failed to set flags for: %s", path_.c_str());
113
- SetError("Failed to set flags");
115
+ int error = errno;
116
+ DEBUG_LOG("[SetHiddenWorker] failed to set flags for %s: %s (%d)",
117
+ path_.c_str(), strerror(error), error);
118
+ SetError(CreatePathErrorMessage("chflags", path_, error));
114
119
  return;
115
120
  }
116
121
  DEBUG_LOG("[SetHiddenWorker] successfully set hidden=%d for: %s", hidden_,
@@ -118,11 +123,13 @@ void SetHiddenWorker::Execute() {
118
123
  }
119
124
 
120
125
  void SetHiddenWorker::OnOK() {
126
+ Napi::HandleScope scope(Env());
121
127
  auto env = Env();
122
128
  deferred_.Resolve(env.Undefined());
123
129
  }
124
130
 
125
131
  void SetHiddenWorker::OnError(const Napi::Error &error) {
132
+ Napi::HandleScope scope(Env());
126
133
  deferred_.Reject(error.Value());
127
134
  }
128
135
 
@@ -70,14 +70,14 @@ private:
70
70
  if (statvfs(mountPoint.c_str(), &vfs) != 0) {
71
71
  DEBUG_LOG("[GetVolumeMetadataWorker] statvfs failed: %s (%d)",
72
72
  strerror(errno), errno);
73
- SetError(CreateErrorMessage("statvfs", errno));
73
+ SetError(CreatePathErrorMessage("statvfs", mountPoint, errno));
74
74
  return false;
75
75
  }
76
76
 
77
77
  if (statfs(mountPoint.c_str(), &fs) != 0) {
78
78
  DEBUG_LOG("[GetVolumeMetadataWorker] statfs failed: %s (%d)",
79
79
  strerror(errno), errno);
80
- SetError(CreateErrorMessage("statfs", errno));
80
+ SetError(CreatePathErrorMessage("statfs", mountPoint, errno));
81
81
  return false;
82
82
  }
83
83
 
@@ -1,6 +1,7 @@
1
1
  // src/darwin/volume_mount_points.cpp
2
2
  #include "../common/volume_mount_points.h"
3
3
  #include "../common/debug_log.h"
4
+ #include "../common/error_utils.h"
4
5
  #include "./fs_meta.h"
5
6
  #include "./raii_utils.h"
6
7
  #include <chrono>
@@ -33,7 +34,13 @@ public:
33
34
  int count = getmntinfo_r_np(mntbuf.ptr(), MNT_NOWAIT);
34
35
 
35
36
  if (count <= 0) {
36
- throw std::runtime_error("Failed to get mount information");
37
+ if (count == 0) {
38
+ throw std::runtime_error("No mount points found");
39
+ } else {
40
+ // getmntinfo_r_np returns -1 on error and sets errno
41
+ throw FSException(
42
+ CreateDetailedErrorMessage("getmntinfo_r_np", errno));
43
+ }
37
44
  }
38
45
 
39
46
  for (int i = 0; i < count; i++) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include "blkid_cache.h"
4
4
  #include "../common/debug_log.h"
5
+ #include "../common/error_utils.h"
5
6
  #include <stdexcept>
6
7
 
7
8
  namespace FSMeta {
@@ -14,8 +15,13 @@ BlkidCache::BlkidCache() : cache_(nullptr) {
14
15
  const std::lock_guard<std::mutex> lock(mutex_);
15
16
  DEBUG_LOG("[BlkidCache] initializing cache");
16
17
  if (blkid_get_cache(&cache_, nullptr) != 0) {
17
- DEBUG_LOG("[BlkidCache] failed to initialize cache");
18
- throw std::runtime_error("Failed to initialize blkid cache");
18
+ int error = errno;
19
+ DEBUG_LOG("[BlkidCache] failed to initialize cache: errno=%d", error);
20
+ if (error != 0) {
21
+ throw FSException(CreateDetailedErrorMessage("blkid_get_cache", error));
22
+ } else {
23
+ throw FSException("Failed to initialize blkid cache (no errno set)");
24
+ }
19
25
  }
20
26
  DEBUG_LOG("[BlkidCache] cache initialized successfully");
21
27
  }
@@ -31,7 +31,7 @@ public:
31
31
  mountPoint.c_str());
32
32
  struct statvfs vfs;
33
33
  if (statvfs(mountPoint.c_str(), &vfs) != 0) {
34
- throw FSException(CreateErrorMessage("statvfs", errno));
34
+ throw FSException(CreatePathErrorMessage("statvfs", mountPoint, errno));
35
35
  }
36
36
 
37
37
  const uint64_t blockSize = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
package/src/platform.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/platform.ts
2
2
 
3
+ import { existsSync, readFileSync } from "node:fs";
3
4
  import { arch, platform } from "node:process";
4
5
 
5
6
  export const isLinux = platform === "linux";
@@ -7,3 +8,27 @@ export const isWindows = platform === "win32";
7
8
  export const isMacOS = platform === "darwin";
8
9
 
9
10
  export const isArm = isLinux && arch.startsWith("arm");
11
+ export const isARM64 = arch === "arm64";
12
+
13
+ /**
14
+ * Detects if we're running on Alpine Linux by checking /etc/os-release
15
+ */
16
+ export function isAlpine(): boolean {
17
+ if (!isLinux) return false;
18
+
19
+ try {
20
+ const osRelease = readFileSync("/etc/os-release", "utf8");
21
+ return (
22
+ osRelease.includes("Alpine Linux") || osRelease.includes("ID=alpine")
23
+ );
24
+ } catch {
25
+ return existsSync("/etc/alpine-release");
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Detects if we're likely running under emulation (as of 202506 there aren't free GHA ARM64 runners)
31
+ */
32
+ export function isEmulated(): boolean {
33
+ return isLinux && isARM64;
34
+ }
@@ -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
- console.log(JSON.stringify(result));
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
- console.error(err);
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
  }