• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 19071410104

04 Nov 2025 02:08PM UTC coverage: 83.072% (+0.4%) from 82.711%
19071410104

Pull #1320

github

web-flow
Merge 9cad4567e into 3ad842cc4
Pull Request #1320: feat: add structured error handling with class error hierarchy

1032 of 1365 branches covered (75.6%)

Branch coverage included in aggregate %.

161 of 239 new or added lines in 25 files covered. (67.36%)

29 existing lines in 1 file now uncovered.

2354 of 2711 relevant lines covered (86.83%)

459.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.54
/lib/utils.js
1
// Copyright (c) 2025, The Robot Web Tools Contributors
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
const fs = require('fs');
27✔
16
const fsPromises = require('fs/promises');
27✔
17
const path = require('path');
27✔
18
const { ValidationError } = require('./errors.js');
27✔
19

20
/**
21
 * Ensure directory exists, create recursively if needed (async)
22
 * Replaces: fse.ensureDir() / fse.mkdirs()
23
 * @param {string} dirPath - Path to directory
24
 * @returns {Promise<void>}
25
 */
26
async function ensureDir(dirPath) {
27
  try {
2,329✔
28
    await fsPromises.mkdir(dirPath, { recursive: true });
2,329✔
29
  } catch (err) {
30
    // Ignore if directory already exists
31
    if (err.code !== 'EEXIST') throw err;
×
32
  }
33
}
34

35
/**
36
 * Ensure directory exists, create recursively if needed (sync)
37
 * Replaces: fse.mkdirSync()
38
 * @param {string} dirPath - Path to directory
39
 */
40
function ensureDirSync(dirPath) {
41
  try {
93✔
42
    fs.mkdirSync(dirPath, { recursive: true });
93✔
43
  } catch (err) {
44
    // Ignore if directory already exists
45
    if (err.code !== 'EEXIST') throw err;
×
46
  }
47
}
48

49
/**
50
 * Check if path exists (async)
51
 * Replaces: fse.exists()
52
 * @param {string} filePath - Path to check
53
 * @returns {Promise<boolean>}
54
 */
55
async function pathExists(filePath) {
56
  try {
26✔
57
    await fsPromises.access(filePath);
26✔
58
    return true;
23✔
59
  } catch {
60
    return false;
3✔
61
  }
62
}
63

64
/**
65
 * Empty a directory (remove all contents but keep the directory)
66
 * Replaces: fse.emptyDir()
67
 * @param {string} dirPath - Path to directory
68
 * @returns {Promise<void>}
69
 */
70
async function emptyDir(dirPath) {
71
  try {
×
72
    const files = await fsPromises.readdir(dirPath);
×
73
    await Promise.all(
×
74
      files.map((file) =>
75
        fsPromises.rm(path.join(dirPath, file), {
×
76
          recursive: true,
77
          force: true,
78
        })
79
      )
80
    );
81
  } catch (err) {
82
    // Ignore if directory doesn't exist
83
    if (err.code !== 'ENOENT') throw err;
×
84
  }
85
}
86

87
/**
88
 * Copy file or directory recursively
89
 * Replaces: fse.copy()
90
 * @param {string} src - Source path
91
 * @param {string} dest - Destination path
92
 * @param {object} options - Copy options
93
 * @returns {Promise<void>}
94
 */
95
async function copy(src, dest, options = {}) {
3✔
96
  const opts = {
3✔
97
    recursive: true,
98
    force: options.overwrite !== false,
99
    ...options,
100
  };
101
  await fsPromises.cp(src, dest, opts);
3✔
102
}
103

104
/**
105
 * Read and parse JSON file synchronously
106
 * Replaces: fse.readJsonSync()
107
 * @param {string} filePath - Path to JSON file
108
 * @param {object} options - Read options
109
 * @returns {any} Parsed JSON data
110
 */
111
function readJsonSync(filePath, options = {}) {
23✔
112
  const content = fs.readFileSync(filePath, options.encoding || 'utf8');
23✔
113
  return JSON.parse(content);
23✔
114
}
115

116
/**
117
 * Remove file or directory (async)
118
 * Replaces: fse.remove()
119
 * @param {string} filePath - Path to remove
120
 * @returns {Promise<void>}
121
 */
122
async function remove(filePath) {
123
  try {
1✔
124
    await fsPromises.rm(filePath, { recursive: true, force: true });
1✔
125
  } catch (err) {
126
    // Ignore if path doesn't exist
127
    if (err.code !== 'ENOENT') throw err;
×
128
  }
129
}
130

131
/**
132
 * Remove file or directory (sync)
133
 * Replaces: fse.removeSync()
134
 * @param {string} filePath - Path to remove
135
 */
136
function removeSync(filePath) {
137
  try {
248✔
138
    fs.rmSync(filePath, { recursive: true, force: true });
248✔
139
  } catch (err) {
140
    // Ignore if path doesn't exist
141
    if (err.code !== 'ENOENT') throw err;
×
142
  }
143
}
144

145
/**
146
 * Write file with content (async)
147
 * Replaces: fse.writeFile()
148
 * @param {string} filePath - Path to file
149
 * @param {string|Buffer} data - Content to write
150
 * @param {object} options - Write options
151
 * @returns {Promise<void>}
152
 */
153
async function writeFile(filePath, data, options = {}) {
2,325✔
154
  await fsPromises.writeFile(filePath, data, options);
2,325✔
155
}
156

157
/**
158
 * Create directory (async)
159
 * Replaces: fse.mkdir()
160
 * @param {string} dirPath - Path to directory
161
 * @param {object} options - mkdir options
162
 * @returns {Promise<void>}
163
 */
164
async function mkdir(dirPath, options = {}) {
3✔
165
  await fsPromises.mkdir(dirPath, options);
3✔
166
}
167

168
/**
169
 * Detect Ubuntu codename from /etc/os-release
170
 * @returns {string|null} Ubuntu codename (e.g., 'noble', 'jammy') or null if not detectable
171
 */
172
function detectUbuntuCodename() {
173
  if (process.platform !== 'linux') {
52!
174
    return null;
×
175
  }
176

177
  try {
52✔
178
    const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
52✔
179
    const match = osRelease.match(/^VERSION_CODENAME=(.*)$/m);
52✔
180
    return match ? match[1].trim() : null;
52!
181
  } catch {
182
    return null;
×
183
  }
184
}
185

186
/**
187
 * Check if two numbers are equal within a given tolerance.
188
 *
189
 * This function compares two numbers using both relative and absolute tolerance,
190
 * matching the behavior of the 'is-close' npm package.
191
 *
192
 * The comparison uses the formula:
193
 *   abs(a - b) <= max(rtol * max(abs(a), abs(b)), atol)
194
 *
195
 * Implementation checks:
196
 *   1. Absolute tolerance: abs(a - b) <= atol
197
 *   2. Relative tolerance: abs(a - b) / max(abs(a), abs(b)) <= rtol
198
 *
199
 * @param {number} a - The first number to compare
200
 * @param {number} b - The second number to compare
201
 * @param {number} [rtol=1e-9] - The relative tolerance parameter (default: 1e-9)
202
 * @param {number} [atol=0.0] - The absolute tolerance parameter (default: 0.0)
203
 * @returns {boolean} True if the numbers are close within the tolerance
204
 *
205
 * @example
206
 * isClose(1.0, 1.0) // true - exact equality
207
 * isClose(1.0, 1.1, 0.01) // false - relative diff: 0.1/1.1 ≈ 0.091 > 0.01
208
 * isClose(10, 10.00001, 1e-6) // true - relative diff: 0.00001/10 = 1e-6 <= 1e-6
209
 * isClose(0, 0.05, 0, 0.1) // true - absolute diff: 0.05 <= 0.1 (atol)
210
 */
211
function isClose(a, b, rtol = 1e-9, atol = 0.0) {
25!
212
  // Handle exact equality
213
  if (a === b) {
25✔
214
    return true;
6✔
215
  }
216

217
  // Handle non-finite numbers
218
  if (!Number.isFinite(a) || !Number.isFinite(b)) {
19!
219
    return false;
×
220
  }
221

222
  const absDiff = Math.abs(a - b);
19✔
223

224
  // Check absolute tolerance first (optimization)
225
  if (atol >= absDiff) {
19!
226
    return true;
×
227
  }
228

229
  // Check relative tolerance
230
  const relativeScaler = Math.max(Math.abs(a), Math.abs(b));
19✔
231

232
  // Handle division by zero when both values are zero or very close to zero
233
  if (relativeScaler === 0) {
19!
234
    return true; // Both are zero, already handled by absolute tolerance
×
235
  }
236

237
  const relativeDiff = absDiff / relativeScaler;
19✔
238

239
  return rtol >= relativeDiff;
19✔
240
}
241

242
/**
243
 * Compare two semantic version strings.
244
 *
245
 * Supports version strings in the format: x.y.z or x.y.z.w
246
 * where x, y, z, w are integers.
247
 *
248
 * @param {string} version1 - First version string (e.g., '1.2.3')
249
 * @param {string} version2 - Second version string (e.g., '1.2.4')
250
 * @param {string} operator - Comparison operator: '<', '<=', '>', '>=', '==', '!='
251
 * @returns {boolean} Result of the comparison
252
 *
253
 * @example
254
 * compareVersions('1.2.3', '1.2.4', '<')   // true
255
 * compareVersions('2.0.0', '1.9.9', '>')   // true
256
 * compareVersions('1.2.3', '1.2.3', '==')  // true
257
 * compareVersions('1.2.3', '1.2.3', '>=')  // true
258
 */
259
function compareVersions(version1, version2, operator) {
260
  // Parse version strings into arrays of integers
261
  const v1Parts = version1.split('.').map((part) => parseInt(part, 10));
150✔
262
  const v2Parts = version2.split('.').map((part) => parseInt(part, 10));
177✔
263

264
  // Pad arrays to same length with zeros
265
  const maxLength = Math.max(v1Parts.length, v2Parts.length);
50✔
266
  while (v1Parts.length < maxLength) v1Parts.push(0);
50✔
267
  while (v2Parts.length < maxLength) v2Parts.push(0);
50✔
268

269
  // Compare each part
270
  let cmp = 0;
50✔
271
  for (let i = 0; i < maxLength; i++) {
50✔
272
    if (v1Parts[i] > v2Parts[i]) {
96✔
273
      cmp = 1;
27✔
274
      break;
27✔
275
    } else if (v1Parts[i] < v2Parts[i]) {
69!
276
      cmp = -1;
×
277
      break;
×
278
    }
279
  }
280

281
  // Apply operator
282
  switch (operator) {
50!
283
    case '<':
284
      return cmp < 0;
23✔
285
    case '<=':
286
      return cmp <= 0;
×
287
    case '>':
288
      return cmp > 0;
×
289
    case '>=':
290
      return cmp >= 0;
27✔
291
    case '==':
292
    case '===':
293
      return cmp === 0;
×
294
    case '!=':
295
    case '!==':
296
      return cmp !== 0;
×
297
    default:
NEW
298
      throw new ValidationError(`Invalid operator: ${operator}`, {
×
299
        code: 'INVALID_OPERATOR',
300
        argumentName: 'operator',
301
        providedValue: operator,
302
        expectedType: "'eq' | 'ne' | 'lt' | 'lte' | 'gt' | 'gte'",
303
      });
304
  }
305
}
306

307
module.exports = {
27✔
308
  // General utilities
309
  detectUbuntuCodename,
310
  isClose,
311

312
  // File system utilities (async)
313
  ensureDir,
314
  mkdirs: ensureDir, // Alias for fs-extra compatibility
315
  exists: pathExists, // Renamed to avoid conflict with deprecated fs.exists
316
  pathExists,
317
  emptyDir,
318
  copy,
319
  remove,
320
  writeFile,
321
  mkdir,
322

323
  // File system utilities (sync)
324
  ensureDirSync,
325
  mkdirSync: ensureDirSync, // Alias for fs-extra compatibility
326
  removeSync,
327
  readJsonSync,
328

329
  compareVersions,
330
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc