• 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

87.3
/lib/interface_loader.js
1
// Copyright (c) 2017 Intel Corporation. All rights reserved.
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
'use strict';
16

17
const path = require('path');
26✔
18
const fs = require('fs');
26✔
19
const generator = require('../rosidl_gen/index.js');
26✔
20
const { TypeValidationError, ValidationError } = require('./errors.js');
26✔
21

22
let interfaceLoader = {
26✔
23
  loadInterfaceByObject(obj) {
24
    //
25
    // `obj` param structure
26
    //
27
    // {
28
    //   package: 'std_msgs',
29
    //   type: 'msg',
30
    //   name: 'String',
31
    // }
32
    //
33
    if (
708✔
34
      typeof obj !== 'object' ||
3,484✔
35
      !obj ||
36
      !obj.package ||
37
      !obj.type ||
38
      !obj.name
39
    ) {
40
      throw new TypeValidationError(
24✔
41
        'interfaceObject',
42
        obj,
43
        'object with package, type, and name properties',
44
        {
45
          entityType: 'interface loader',
46
        }
47
      );
48
    }
49
    return this.loadInterface(obj.package, obj.type, obj.name);
684✔
50
  },
51

52
  loadInterfaceByString(name) {
53
    if (typeof name !== 'string') {
9,641!
NEW
54
      throw new TypeValidationError('name', name, 'string', {
×
55
        entityType: 'interface loader',
56
      });
57
    }
58

59
    // TODO(Kenny): more checks of the string argument
60
    if (name.indexOf('/') !== -1) {
9,641✔
61
      let [packageName, type, messageName] = name.split('/');
9,589✔
62
      return this.loadInterface(packageName, type, messageName);
9,589✔
63
    }
64

65
    // Suppose the name is a package, and traverse the path to collect the IDL files.
66
    let packagePath = path.join(generator.generatedRoot, name);
52✔
67

68
    let interfaces = fs.readdirSync(packagePath);
52✔
69
    if (interfaces.length > 0) {
52!
70
      return this.loadInterfaceByPath(packagePath, interfaces);
52✔
71
    }
72

NEW
73
    throw new ValidationError(
×
74
      'A string argument in expected in "package/type/message" format',
75
      {
76
        code: 'INVALID_INTERFACE_FORMAT',
77
        argumentName: 'name',
78
        providedValue: name,
79
        expectedType: 'string in "package/type/message" format',
80
        entityType: 'interface loader',
81
      }
82
    );
83
  },
84

85
  loadInterfaceByPath(packagePath, interfaces) {
86
    let interfaceInfos = [];
52✔
87

88
    interfaces.forEach((file) => {
52✔
89
      let results = file.match(/\w+__(\w+)__(\w+).js$/);
1,499✔
90
      let type = results[1];
1,499✔
91
      let name = results[2];
1,499✔
92
      let filePath = path.join(packagePath, file).normalize();
1,499✔
93
      interfaceInfos.push({ name, type, filePath });
1,499✔
94
    });
95

96
    let pkg = { srv: {}, msg: {}, action: {} };
52✔
97
    interfaceInfos.forEach((info) => {
52✔
98
      Object.defineProperty(pkg[info.type], info.name, {
1,499✔
99
        value: require(info.filePath),
100
      });
101
    });
102

103
    return pkg;
52✔
104
  },
105

106
  _isRos2InstallationPath(pkgPath) {
107
    // Use "which ros2" to dynamically find the ROS2 installation root
108
    try {
17✔
109
      const whichResult = require('child_process').spawnSync(
17✔
110
        'which',
111
        ['ros2'],
112
        {
113
          encoding: 'utf8',
114
          timeout: 5000,
115
        }
116
      );
117

118
      if (whichResult.status === 0 && whichResult.stdout) {
17!
119
        const ros2BinPath = whichResult.stdout.trim();
17✔
120
        // Get the ROS2 installation root (typically /opt/ros/<distro> or similar)
121
        const ros2Root = path.dirname(path.dirname(ros2BinPath));
17✔
122

123
        return pkgPath.includes(ros2Root);
17✔
124
      }
125
    } catch (err) {
126
      console.error('Error running which ros2:', err.message);
×
127
      // If "which ros2" fails, fall back to hardcoded check
128
      return pkgPath.includes('ros2-linux');
×
129
    }
130

131
    return false;
×
132
  },
133

134
  _searchAndGenerateInterface(packageName, type, messageName, filePath) {
135
    // Check if it's a valid package
136
    for (const pkgPath of generator.getInstalledPackagePaths()) {
17✔
137
      // We are going to ignore the path where ROS2 is installed.
138
      if (this._isRos2InstallationPath(pkgPath)) {
17✔
139
        continue;
16✔
140
      }
141

142
      // Recursively search for files named messageName.* under pkgPath/
143
      if (fs.existsSync(pkgPath)) {
1!
144
        // Recursive function to search for files
145
        function searchForFile(dir) {
146
          try {
5✔
147
            const items = fs.readdirSync(dir, { withFileTypes: true });
5✔
148
            for (const item of items) {
5✔
149
              const fullPath = path.join(dir, item.name);
52✔
150

151
              if (item.isFile()) {
52✔
152
                const baseName = path.parse(item.name).name;
48✔
153
                // Check if the base filename matches messageName
154
                if (baseName === messageName) {
48✔
155
                  return fullPath;
1✔
156
                }
157
              } else if (item.isDirectory()) {
4!
158
                // Recursively search subdirectories
159
                const result = searchForFile(fullPath);
4✔
160
                if (result) {
4✔
161
                  return result;
1✔
162
                }
163
              }
164
            }
165
          } catch (err) {
166
            // Skip directories we can't read
167
            console.error('Error reading directory:', dir, err.message);
×
168
          }
169
          return null;
3✔
170
        }
171

172
        const foundFilePath = searchForFile(
1✔
173
          path.join(pkgPath, 'share', packageName)
174
        );
175

176
        if (foundFilePath && foundFilePath.length > 0) {
1!
177
          // Use worker thread to generate interfaces synchronously
178
          try {
1✔
179
            generator.generateInPathSyncWorker(pkgPath);
1✔
180
            // Now try to load the interface again from the generated files
181
            if (fs.existsSync(filePath)) {
1!
182
              return require(filePath);
1✔
183
            }
184
          } catch (err) {
185
            console.error('Error in interface generation:', err);
×
186
          }
187
        }
188
      }
189
    }
190
    throw new ValidationError(
16✔
191
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`,
192
      {
193
        code: 'MESSAGE_NOT_FOUND',
194
        entityType: 'interface loader',
195
        details: {
196
          packageName: packageName,
197
          type: type,
198
          messageName: messageName,
199
          searchPath: generator.generatedRoot,
200
        },
201
      }
202
    );
203
  },
204

205
  loadInterface(packageName, type, messageName) {
206
    if (arguments.length === 1) {
20,622✔
207
      const type = arguments[0];
10,349✔
208
      if (typeof type === 'object') {
10,349✔
209
        return this.loadInterfaceByObject(type);
708✔
210
      } else if (typeof type === 'string') {
9,641!
211
        return this.loadInterfaceByString(type);
9,641✔
212
      }
NEW
213
      throw new ValidationError(
×
214
        `The message required does not exist: ${type}`,
215
        {
216
          code: 'MESSAGE_NOT_FOUND',
217
          entityType: 'interface loader',
218
          details: { type: type },
219
        }
220
      );
221
    }
222
    if (packageName && type && messageName) {
10,273✔
223
      let filePath = path.join(
10,261✔
224
        generator.generatedRoot,
225
        packageName,
226
        packageName + '__' + type + '__' + messageName + '.js'
227
      );
228

229
      if (fs.existsSync(filePath)) {
10,261✔
230
        return require(filePath);
10,244✔
231
      } else {
232
        return this._searchAndGenerateInterface(
17✔
233
          packageName,
234
          type,
235
          messageName,
236
          filePath
237
        );
238
      }
239
    }
240
    // We cannot parse `packageName`, `type` and `messageName` from the string passed.
241
    throw new ValidationError(
12✔
242
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`,
243
      {
244
        code: 'MESSAGE_NOT_FOUND',
245
        entityType: 'interface loader',
246
        details: {
247
          packageName: packageName,
248
          type: type,
249
          messageName: messageName,
250
          searchPath: generator.generatedRoot,
251
        },
252
      }
253
    );
254
  },
255
};
256

257
module.exports = interfaceLoader;
26✔
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