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

RobotWebTools / rclnodejs / 17610628993

10 Sep 2025 10:16AM UTC coverage: 84.306% (-0.2%) from 84.555%
17610628993

push

github

minggangw
Generate missing messages at runtime (#1257)

This PR implements runtime message generation capabilities for rclnodejs, allowing custom ROS2 message types to be dynamically discovered, built, and loaded during execution rather than requiring pre-generation.

- Adds runtime message discovery and generation functionality to the interface loader
- Implements synchronous message generation using worker processes to avoid blocking the main thread
- Includes comprehensive test infrastructure with a custom message package for validation

Fix: #1257

787 of 1031 branches covered (76.33%)

Branch coverage included in aggregate %.

31 of 36 new or added lines in 1 file covered. (86.11%)

2 existing lines in 1 file now uncovered.

1942 of 2206 relevant lines covered (88.03%)

433.73 hits per line

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

87.2
/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

21
let interfaceLoader = {
26✔
22
  loadInterfaceByObject(obj) {
23
    //
24
    // `obj` param structure
25
    //
26
    // {
27
    //   package: 'std_msgs',
28
    //   type: 'msg',
29
    //   name: 'String',
30
    // }
31
    //
32
    if (
704✔
33
      typeof obj !== 'object' ||
3,464✔
34
      !obj ||
35
      !obj.package ||
36
      !obj.type ||
37
      !obj.name
38
    ) {
39
      throw new TypeError(
24✔
40
        'Should provide an object argument to get ROS message class'
41
      );
42
    }
43
    return this.loadInterface(obj.package, obj.type, obj.name);
680✔
44
  },
45

46
  loadInterfaceByString(name) {
47
    if (typeof name !== 'string') {
8,125!
48
      throw new TypeError(
×
49
        'Should provide a string argument to get ROS message class'
50
      );
51
    }
52

53
    // TODO(Kenny): more checks of the string argument
54
    if (name.indexOf('/') !== -1) {
8,125✔
55
      let [packageName, type, messageName] = name.split('/');
8,073✔
56
      return this.loadInterface(packageName, type, messageName);
8,073✔
57
    }
58

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

62
    let interfaces = fs.readdirSync(packagePath);
52✔
63
    if (interfaces.length > 0) {
52!
64
      return this.loadInterfaceByPath(packagePath, interfaces);
52✔
65
    }
66

67
    throw new TypeError(
×
68
      'A string argument in expected in "package/type/message" format'
69
    );
70
  },
71

72
  loadInterfaceByPath(packagePath, interfaces) {
73
    let interfaceInfos = [];
52✔
74

75
    interfaces.forEach((file) => {
52✔
76
      let results = file.match(/\w+__(\w+)__(\w+).js$/);
1,499✔
77
      let type = results[1];
1,499✔
78
      let name = results[2];
1,499✔
79
      let filePath = path.join(packagePath, file).normalize();
1,499✔
80
      interfaceInfos.push({ name, type, filePath });
1,499✔
81
    });
82

83
    let pkg = { srv: {}, msg: {}, action: {} };
52✔
84
    interfaceInfos.forEach((info) => {
52✔
85
      Object.defineProperty(pkg[info.type], info.name, {
1,499✔
86
        value: require(info.filePath),
87
      });
88
    });
89

90
    return pkg;
52✔
91
  },
92

93
  _isRos2InstallationPath(pkgPath) {
94
    // Use "which ros2" to dynamically find the ROS2 installation root
95
    try {
17✔
96
      const whichResult = require('child_process').spawnSync(
17✔
97
        'which',
98
        ['ros2'],
99
        {
100
          encoding: 'utf8',
101
          timeout: 5000,
102
        }
103
      );
104

105
      if (whichResult.status === 0 && whichResult.stdout) {
17!
106
        const ros2BinPath = whichResult.stdout.trim();
17✔
107
        // Get the ROS2 installation root (typically /opt/ros/<distro> or similar)
108
        const ros2Root = path.dirname(path.dirname(ros2BinPath));
17✔
109

110
        return pkgPath.includes(ros2Root);
17✔
111
      }
112
    } catch (err) {
NEW
113
      console.error('Error running which ros2:', err.message);
×
114
      // If "which ros2" fails, fall back to hardcoded check
NEW
115
      return pkgPath.includes('ros2-linux');
×
116
    }
117

NEW
118
    return false;
×
119
  },
120

121
  _searchAndGenerateInterface(packageName, type, messageName, filePath) {
122
    // Check if it's a valid package
123
    for (const pkgPath of generator.getInstalledPackagePaths()) {
17✔
124
      // We are going to ignore the path where ROS2 is installed.
125
      if (this._isRos2InstallationPath(pkgPath)) {
17✔
126
        continue;
16✔
127
      }
128

129
      // Recursively search for files named messageName.* under pkgPath/
130
      if (fs.existsSync(pkgPath)) {
1!
131
        // Recursive function to search for files
132
        function searchForFile(dir) {
133
          try {
5✔
134
            const items = fs.readdirSync(dir, { withFileTypes: true });
5✔
135
            for (const item of items) {
5✔
136
              const fullPath = path.join(dir, item.name);
52✔
137

138
              if (item.isFile()) {
52✔
139
                const baseName = path.parse(item.name).name;
48✔
140
                // Check if the base filename matches messageName
141
                if (baseName === messageName) {
48✔
142
                  return fullPath;
1✔
143
                }
144
              } else if (item.isDirectory()) {
4!
145
                // Recursively search subdirectories
146
                const result = searchForFile(fullPath);
4✔
147
                if (result) {
4✔
148
                  return result;
1✔
149
                }
150
              }
151
            }
152
          } catch (err) {
153
            // Skip directories we can't read
NEW
154
            console.error('Error reading directory:', dir, err.message);
×
155
          }
156
          return null;
3✔
157
        }
158

159
        const foundFilePath = searchForFile(
1✔
160
          path.join(pkgPath, 'share', packageName)
161
        );
162

163
        if (foundFilePath && foundFilePath.length > 0) {
1!
164
          // Use worker thread to generate interfaces synchronously
165
          try {
1✔
166
            generator.generateInPathSyncWorker(pkgPath);
1✔
167
            // Now try to load the interface again from the generated files
168
            if (fs.existsSync(filePath)) {
1!
169
              return require(filePath);
1✔
170
            }
171
          } catch (err) {
NEW
172
            console.error('Error in interface generation:', err);
×
173
          }
174
        }
175
      }
176
    }
177
    throw new Error(
16✔
178
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`
179
    );
180
  },
181

182
  loadInterface(packageName, type, messageName) {
183
    if (arguments.length === 1) {
17,582✔
184
      const type = arguments[0];
8,829✔
185
      if (typeof type === 'object') {
8,829✔
186
        return this.loadInterfaceByObject(type);
704✔
187
      } else if (typeof type === 'string') {
8,125!
188
        return this.loadInterfaceByString(type);
8,125✔
189
      }
190
      throw new Error(`The message required does not exist: ${type}`);
×
191
    }
192
    if (packageName && type && messageName) {
8,753✔
193
      let filePath = path.join(
8,741✔
194
        generator.generatedRoot,
195
        packageName,
196
        packageName + '__' + type + '__' + messageName + '.js'
197
      );
198

199
      if (fs.existsSync(filePath)) {
8,741✔
200
        return require(filePath);
8,724✔
201
      } else {
202
        return this._searchAndGenerateInterface(
17✔
203
          packageName,
204
          type,
205
          messageName,
206
          filePath
207
        );
208
      }
209
    }
210
    // We cannot parse `packageName`, `type` and `messageName` from the string passed.
211
    throw new Error(
12✔
212
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`
213
    );
214
  },
215
};
216

217
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

© 2025 Coveralls, Inc