Skip to content

Commit 47e1d01

Browse files
committed
[#25501] Updated Controller 'partitions_match' and test.py for Windows CI
Signed-off-by: danipiza <dpizarrogallego@gmail.com>
1 parent 4aa5596 commit 47e1d01

3 files changed

Lines changed: 148 additions & 72 deletions

File tree

fastddsspy_tool/src/cpp/tool/Controller.cpp

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ using nlohmann::json;
4141
namespace eprosima {
4242
namespace spy {
4343

44+
namespace {
45+
46+
bool partitions_match(
47+
const std::string& filter_partition,
48+
const std::string& endpoint_partition) noexcept
49+
{
50+
// Keep the empty/default DDS partition semantics consistent across
51+
// platforms: it should only match another empty partition
52+
if (filter_partition.empty() || endpoint_partition.empty())
53+
{
54+
return filter_partition == endpoint_partition;
55+
}
56+
57+
return utils::match_pattern(filter_partition, endpoint_partition) ||
58+
utils::match_pattern(endpoint_partition, filter_partition);
59+
}
60+
61+
} // namespace
62+
4463
// Braces + indentation, arrays single-line
4564
static void print_json_arrays_inline(
4665
const json& j,
@@ -145,9 +164,13 @@ void Controller::run()
145164
while (command.command != CommandValue::exit)
146165
{
147166
command = input_.wait_next_command();
148-
// refresh the database if a filter partition is active.
149-
// this checks if there is a new endpoint that does not
150-
// pass the filter and disable it
167+
// Refresh endpoint activity before each command when partition filters are
168+
// active so late-discovered endpoints also honor the current filter set
169+
if (!partition_filter_set_.empty())
170+
{
171+
update_endpoints();
172+
}
173+
151174
run_command_(command);
152175
}
153176
}
@@ -1084,16 +1107,15 @@ void Controller::update_endpoints()
10841107
// Iterate in the partition set
10851108
while (i < n)
10861109
{
1087-
if (guid_partition_pair.second[i] == '|')
1088-
{
1089-
for (const std::string& filter_p: partition_filter_set_)
1110+
if (guid_partition_pair.second[i] == '|')
10901111
{
1091-
if (utils::match_pattern(filter_p, curr_partition) ||
1092-
utils::match_pattern(curr_partition, filter_p))
1112+
for (const std::string& filter_p: partition_filter_set_)
10931113
{
1094-
// The current partition matches with a partition
1095-
// from the filter, the endpoint is active
1096-
endpoint_active = true;
1114+
if (partitions_match(filter_p, curr_partition))
1115+
{
1116+
// The current partition matches with a partition
1117+
// from the filter, the endpoint is active
1118+
endpoint_active = true;
10971119
break;
10981120
}
10991121
}
@@ -1111,8 +1133,7 @@ void Controller::update_endpoints()
11111133
// Empty or last partition
11121134
for (const std::string& filter_p: partition_filter_set_)
11131135
{
1114-
if (utils::match_pattern(filter_p, curr_partition) ||
1115-
utils::match_pattern(curr_partition, filter_p))
1136+
if (partitions_match(filter_p, curr_partition))
11161137
{
11171138
// The current partition matches with a partition
11181139
// from the filter, the endpoint is active

fastddsspy_tool/test/application/test.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import argparse
2727
import importlib
2828
import os
29+
import re
2930
import sys
31+
import time
32+
import traceback
3033

3134

3235
DESCRIPTION = """Script to execute Fast DDS Spy executable test"""
@@ -130,6 +133,40 @@ def get_config_path_spy(arguments_spy, exec_spy, config):
130133
return arguments_spy
131134

132135

136+
def has_explicit_domain(arguments_spy) -> bool:
137+
"""Return whether the test already sets a domain from the CLI."""
138+
return '--domain' in arguments_spy
139+
140+
141+
def config_path_from_arguments(arguments_spy) -> str:
142+
"""Return the resolved config path if the test uses --config-path."""
143+
if '--config-path' not in arguments_spy:
144+
return ''
145+
146+
config_index = arguments_spy.index('--config-path') + 1
147+
if config_index >= len(arguments_spy):
148+
return ''
149+
150+
return arguments_spy[config_index]
151+
152+
153+
def config_has_domain(config_path) -> bool:
154+
"""Return whether the referenced yaml config already sets a DDS domain."""
155+
if not config_path or not os.path.isfile(config_path):
156+
return False
157+
158+
with open(config_path, encoding='utf-8') as file:
159+
return re.search(r'^\s*domain\s*:', file.read(), flags=re.MULTILINE) is not None
160+
161+
162+
def isolated_test_domain() -> str:
163+
"""
164+
Pick a non-default domain for this test process to avoid cross-test discovery
165+
residue and ambient DDS traffic on shared runners.
166+
"""
167+
return str(30 + ((os.getpid() ^ time.time_ns()) % 170))
168+
169+
133170
def main():
134171
"""@brief The main entry point of the program."""
135172
args = parse_options()
@@ -145,30 +182,43 @@ def main():
145182
test_class.exec_spy,
146183
test_class.config)
147184

148-
dds = test_class.run_dds()
149-
spy = test_class.run_tool()
150-
151-
if spy is None:
152-
print('ERROR: Wrong output')
153-
test_class.stop_dds(dds)
154-
sys.exit(1)
155-
156-
if not test_class.one_shot:
157-
output = test_class.send_commands_tool(spy)
158-
159-
if not test_class.valid_output(output):
160-
test_class.stop_tool(spy)
161-
test_class.stop_dds(dds)
162-
print('ERROR: Output command not valid')
163-
sys.exit(1)
164-
165-
if not test_class.stop_dds(dds):
166-
sys.exit(1)
185+
if not has_explicit_domain(test_class.arguments_spy):
186+
config_path = config_path_from_arguments(test_class.arguments_spy)
187+
if not config_has_domain(config_path):
188+
test_domain = isolated_test_domain()
189+
test_class.arguments_spy = ['--domain', test_domain] + test_class.arguments_spy
190+
if test_class.dds:
191+
test_class.arguments_dds = test_class.arguments_dds + ['--domain', test_domain]
192+
193+
dds = None
194+
spy = None
195+
exit_code = 1
196+
197+
try:
198+
dds = test_class.run_dds()
199+
spy = test_class.run_tool()
200+
201+
if spy is None:
202+
print('ERROR: Wrong output')
203+
elif not test_class.one_shot:
204+
output = test_class.send_commands_tool(spy)
205+
206+
if not test_class.valid_output(output):
207+
print('ERROR: Output command not valid')
208+
else:
209+
exit_code = 0
210+
else:
211+
exit_code = 0
212+
except Exception:
213+
traceback.print_exc()
214+
finally:
215+
if dds is not None and not test_class.stop_dds(dds):
216+
exit_code = 1
167217

168-
if not test_class.one_shot:
169-
test_class.stop_tool(spy)
218+
if spy is not None and not test_class.one_shot and not test_class.stop_tool(spy):
219+
exit_code = 1
170220

171-
sys.exit(0)
221+
sys.exit(exit_code)
172222

173223

174224
if __name__ == '__main__':

fastddsspy_tool/test/application/test_class.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
SLEEP_TIME = 0.2
25-
DDS_STARTUP_TIME = 1.0
25+
DDS_STARTUP_TIME = 1.0 if os.name == 'nt' else 0.2
2626

2727

2828
def safe_interrupt(p):
@@ -74,16 +74,25 @@ def run_dds(self):
7474
@return Returns a subprocess object representing the running DDS publisher.
7575
"""
7676
if self.dds:
77-
self.command = [self.exec_dds, 'publisher'] + self.arguments_dds
77+
self.command = [self.exec_dds, 'publisher']
78+
env = os.environ.copy()
79+
80+
# Windows CI is flaky when the helper publisher uses default SHM transport.
81+
# Force plain UDP there, but keep the normal participant construction path so
82+
# tests still observe the expected participant name ("Participant_pub").
83+
if os.name == 'nt':
84+
self.command.append('--transport=udp')
85+
86+
self.command.extend(self.arguments_dds)
7887

7988
proc = subprocess.Popen(self.command,
80-
stdin=subprocess.PIPE,
81-
stdout=subprocess.PIPE,
82-
stderr=subprocess.PIPE)
89+
stdin=subprocess.DEVNULL,
90+
stdout=subprocess.DEVNULL,
91+
stderr=subprocess.DEVNULL,
92+
env=env)
8393

84-
# Give the publisher time to initialize its participant and writer
85-
# before the spy starts discovery. Windows CI is noticeably slower
86-
# than local runs, which makes the one-shot discovery tests flaky.
94+
# Give the helper publisher time to create its participant before the Spy starts
95+
# measuring discovery on slower Windows CI runners.
8796
time.sleep(DDS_STARTUP_TIME)
8897

8998
return proc
@@ -114,18 +123,10 @@ def run_tool(self):
114123
if self.arguments_spy[:2] == ['show', 'all']:
115124
safe_interrupt(proc)
116125
try:
117-
output, error = proc.communicate(timeout=10)
126+
output = proc.communicate(timeout=10)[0]
118127
except subprocess.TimeoutExpired:
119128
proc.kill()
120-
output, error = proc.communicate()
121-
print('ERROR: Spy process timed out')
122-
if error:
123-
print(error)
124-
return None
125-
126-
if error:
127-
print(error)
128-
129+
output = ''
129130
if not self.valid_output(output):
130131
return None
131132

@@ -188,6 +189,9 @@ def read_command_output(self, proc):
188189

189190
line = proc.stdout.readline()
190191

192+
if line == '' and proc.poll() is not None:
193+
break
194+
191195
if ('Insert a command for Fast DDS Spy:' in line):
192196
break
193197

@@ -295,27 +299,27 @@ def valid_output(self, output) -> bool:
295299
"""
296300
clean_output = self.extract_cli_output(output)
297301
expected_output = self.output_command()
298-
299-
# Empty outputs can be represented as '' or '\n' depending on the
300-
# platform/runtime. Treat both as equivalent to avoid CI-only flakes.
301-
if not clean_output.strip() and not expected_output.strip():
302-
return True
303-
304302
if expected_output == clean_output:
305303
return True
306304

307305
lines_expected_output = expected_output.splitlines()
308306
lines_output = clean_output.splitlines()
309307

308+
while lines_expected_output and lines_expected_output[-1] == '':
309+
lines_expected_output.pop()
310+
311+
while lines_output and lines_output[-1] == '':
312+
lines_output.pop()
313+
314+
if len(lines_output) < len(lines_expected_output):
315+
print('Output: ')
316+
print(clean_output)
317+
print('Expected output: ')
318+
print(expected_output)
319+
return False
320+
310321
# TODO (Raul): If guid and rate are on the same line this will not work.
311322
for i in range(len(lines_expected_output)):
312-
if i >= len(lines_output):
313-
print('Output: ')
314-
print(clean_output)
315-
print('Expected output: ')
316-
print(expected_output)
317-
return False
318-
319323
if '%%guid%%' in lines_expected_output[i]:
320324
start_guid_position = lines_expected_output[i].find('%%guid%%')
321325

@@ -335,12 +339,13 @@ def valid_output(self, output) -> bool:
335339
print(expected_output)
336340
return False
337341

338-
if len(lines_output) != len(lines_expected_output):
339-
print('Output: ')
340-
print(clean_output)
341-
print('Expected output: ')
342-
print(expected_output)
343-
return False
342+
for extra_line in lines_output[len(lines_expected_output):]:
343+
if extra_line != '':
344+
print('Output: ')
345+
print(clean_output)
346+
print('Expected output: ')
347+
print(expected_output)
348+
return False
344349

345350
return True
346351

0 commit comments

Comments
 (0)