Skip to content

Commit 3606ed4

Browse files
Polish tests
Signed-off-by: Eugenio Collado <eugeniocollado@eprosima.com>
1 parent 42a69dc commit 3606ed4

18 files changed

Lines changed: 826 additions & 117 deletions

File tree

ddsenabler/examples/action/goals/1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"order": 3
2+
"order": 5
33
}

ddsenabler/examples/action/goals/2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"order": 5
2+
"order": 6
33
}

ddsenabler/examples/service/main.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,24 @@ bool server_specific_logic(
367367
uint64_t request_id)
368368
{
369369
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simulate processing time
370-
std::string json = "{\"sum\": 3}"; // Example response, replace with actual logic
370+
371+
// Add_two_ints service logic
372+
int a = 1; // Default values in case of parsing failure
373+
int b = 2; // Default values in case of parsing failure
374+
try
375+
{
376+
auto request_json = nlohmann::json::parse(request);
377+
a = request_json["rq/add_two_intsRequest"]["data"]["0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"]["a"].get<int>();
378+
b = request_json["rq/add_two_intsRequest"]["data"]["0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"]["b"].get<int>();
379+
}
380+
catch (const std::exception& e)
381+
{
382+
std::cerr << "Failed to parse request JSON: " << e.what() << " Using default values a=1, b=2." << std::endl;
383+
}
384+
nlohmann::json response_json;
385+
response_json["sum"] = a + b;
386+
std::string json = response_json.dump();
387+
371388
if (!enabler->send_service_reply(service_name, json, request_id))
372389
{
373390
std::cerr << "Failed to send service reply for request ID: " << request_id << std::endl;

ddsenabler/test/DDSEnablerTester.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
// limitations under the License.
1414

1515
#include <mutex>
16+
#include <algorithm>
17+
#include <filesystem>
18+
1619

1720
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
1821
#include <fastdds/dds/publisher/DataWriter.hpp>
@@ -49,8 +52,8 @@ static int write_delay_ms_ = 20;
4952
static int wait_for_ack_ns_ = 1000000000;
5053
static int wait_after_publication_ms_ = 200;
5154

52-
const auto input_file_path = std::filesystem::current_path() / "test_files";
53-
auto persistence_dir = input_file_path.string();
55+
static const auto input_file_path = std::filesystem::current_path() / "test_files";
56+
static std::string persistence_dir = input_file_path.string();
5457
#if defined(_WIN32) // On windows, the path separator is '\'
5558
std::replace(persistence_dir.begin(), persistence_dir.end(), '/', '\\');
5659
#endif // _WIN32
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima).
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+
import argparse
16+
import re
17+
from typing import List, Tuple
18+
19+
import log
20+
21+
import validation
22+
23+
DESCRIPTION = """Script to validate Fibonacci action client output"""
24+
USAGE = ('python3 execute_and_validate_action_client.py '
25+
'[-s <samples>] [-t <timeout>] [-e <exe>] [--delay <sec>] [-d]')
26+
27+
28+
def parse_options():
29+
"""
30+
Parse arguments.
31+
32+
:return: The arguments parsed.
33+
"""
34+
parser = argparse.ArgumentParser(
35+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
36+
add_help=True,
37+
description=(DESCRIPTION),
38+
usage=(USAGE)
39+
)
40+
parser.add_argument(
41+
'-s',
42+
'--samples',
43+
type=int,
44+
default=3,
45+
help='Number of goal requests to send/expect.'
46+
)
47+
parser.add_argument(
48+
'-t',
49+
'--timeout',
50+
type=int,
51+
default=20,
52+
help='Timeout for the action client application.'
53+
)
54+
parser.add_argument(
55+
'-e',
56+
'--exe',
57+
type=str,
58+
default='/scripts/ros2_nodes/node_main.py',
59+
help='Path to the action client executable (python entrypoint).'
60+
)
61+
parser.add_argument(
62+
'--delay',
63+
type=float,
64+
default=0,
65+
help='Time to wait before starting execution.'
66+
)
67+
parser.add_argument(
68+
'-d',
69+
'--debug',
70+
action='store_true',
71+
help='Print test debugging info.'
72+
)
73+
74+
return parser.parse_args()
75+
76+
77+
def _client_command(args):
78+
"""
79+
Build the command to execute the client.
80+
81+
:param args: Arguments parsed
82+
:return: Command to execute the client
83+
"""
84+
command = [
85+
'python3', args.exe,
86+
'--action',
87+
'--client',
88+
'--samples', str(args.samples)
89+
]
90+
91+
return command
92+
93+
94+
# ------------------------- Parsing & Validation ------------------------- #
95+
96+
def _extract_number_sequences_from_line(line: str) -> List[int]:
97+
"""Extract a list of ints from a line, supporting formats like:
98+
- Result { 0,1,1,2 }
99+
- Result: [0, 1, 1, 2]
100+
- result -> 0 1 1 2
101+
The function returns the first reasonable list of ints found.
102+
"""
103+
# Prefer content inside brackets or braces
104+
for opening, closing in [("[", "]"), ("{", "}")]:
105+
if opening in line and closing in line:
106+
content = line.split(opening, 1)[1].split(closing, 1)[0]
107+
nums = re.findall(r"-?\d+", content)
108+
return [int(n) for n in nums]
109+
110+
# Otherwise, extract from the tail of the line
111+
tail = line.split(':', 1)[-1]
112+
nums = re.findall(r"-?\d+", tail)
113+
return [int(n) for n in nums]
114+
115+
116+
def _client_parse_output(stdout: str, stderr: str) -> Tuple[List[List[int]], str]:
117+
"""
118+
Transform the output of the program into a list of Fibonacci sequences.
119+
120+
We look for lines containing the word 'Result' (case-insensitive) and
121+
extract number sequences from them. Each sequence corresponds to one goal.
122+
"""
123+
lines = stdout.splitlines()
124+
125+
sequences: List[List[int]] = []
126+
for line in lines:
127+
if 'result' in line.lower():
128+
seq = _extract_number_sequences_from_line(line)
129+
if len(seq) >= 2: # minimal Fibonacci
130+
sequences.append(seq)
131+
132+
return sequences, stderr
133+
134+
135+
def _is_fibonacci(seq: List[int]) -> bool:
136+
"""Check that a sequence is a valid Fibonacci progression starting at 0, 1."""
137+
if len(seq) < 2:
138+
return False
139+
if not (seq[0] == 0 and seq[1] == 1):
140+
return False
141+
for i in range(2, len(seq)):
142+
if seq[i] != seq[i - 1] + seq[i - 2]:
143+
return False
144+
return True
145+
146+
147+
def _client_validate(stdout_parsed: List[List[int]], stderr_parsed: str):
148+
# First apply the default validator
149+
ret_code = validation.validate_default(stdout_parsed, stderr_parsed)
150+
if ret_code != validation.ReturnCode.SUCCESS:
151+
return ret_code
152+
153+
# Ensure we have at least one sequence
154+
if len(stdout_parsed) == 0:
155+
log.logger.error('No result sequences were found in client output.')
156+
return validation.ReturnCode.NOT_VALID_MESSAGES
157+
158+
# Validate each result sequence as Fibonacci
159+
for seq in stdout_parsed:
160+
if not _is_fibonacci(seq):
161+
log.logger.error(f'Non-Fibonacci sequence detected: {seq}')
162+
return validation.ReturnCode.NOT_VALID_MESSAGES
163+
164+
return ret_code
165+
166+
167+
if __name__ == '__main__':
168+
169+
# Parse arguments
170+
args = parse_options()
171+
172+
# Set log level
173+
if args.debug:
174+
log.activate_debug()
175+
176+
command = _client_command(args)
177+
178+
ret_code = validation.run_and_validate(
179+
command=command,
180+
timeout=args.timeout,
181+
delay=args.delay,
182+
parse_output_function=_client_parse_output,
183+
validate_output_function=_client_validate)
184+
185+
log.logger.info(f'Action client validator exited with code {ret_code}')
186+
187+
exit(ret_code.value)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima).
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+
import argparse
16+
17+
import log
18+
19+
import validation
20+
21+
DESCRIPTION = """Script to validate Fibonacci action server output"""
22+
USAGE = ('python3 execute_and_validate_action_server.py '
23+
'[-s <samples>] [--expect-cancel] [-t <timeout>] [-e <exe>] [--delay <sec>] [-d]')
24+
25+
26+
def parse_options():
27+
"""
28+
Parse arguments.
29+
30+
:return: The arguments parsed.
31+
"""
32+
parser = argparse.ArgumentParser(
33+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
34+
add_help=True,
35+
description=(DESCRIPTION),
36+
usage=(USAGE)
37+
)
38+
parser.add_argument(
39+
'-s',
40+
'--samples',
41+
type=int,
42+
default=3,
43+
help='Number of goals the server must process before exiting.'
44+
)
45+
parser.add_argument(
46+
'--expect-cancel',
47+
action='store_true',
48+
help='Expect goals to be cancelled; count cancellations towards samples.'
49+
)
50+
parser.add_argument(
51+
'-t',
52+
'--timeout',
53+
type=int,
54+
default=20,
55+
help='Timeout for the action server application.'
56+
)
57+
parser.add_argument(
58+
'-e',
59+
'--exe',
60+
type=str,
61+
default='/scripts/ros2_nodes/node_main.py',
62+
help='Path to the action server executable (python entrypoint).'
63+
)
64+
parser.add_argument(
65+
'--delay',
66+
type=float,
67+
default=0,
68+
help='Time to wait before starting execution.'
69+
)
70+
parser.add_argument(
71+
'-d',
72+
'--debug',
73+
action='store_true',
74+
help='Print test debugging info.'
75+
)
76+
77+
return parser.parse_args()
78+
79+
80+
def _server_command(args):
81+
"""
82+
Build the command to execute the server.
83+
84+
:param args: Arguments parsed
85+
:return: Command to execute the server
86+
"""
87+
command = [
88+
'python3', args.exe,
89+
'--action',
90+
'--samples', str(args.samples)
91+
]
92+
if args.expect_cancel:
93+
command.append('--expect-cancel')
94+
95+
return command
96+
97+
98+
if __name__ == '__main__':
99+
100+
# Parse arguments
101+
args = parse_options()
102+
103+
# Set log level
104+
if args.debug:
105+
log.activate_debug()
106+
107+
command = _server_command(args)
108+
109+
# Check process exits after serving the expected goals and stderr is empty
110+
ret_code = validation.run_and_validate(
111+
command=command,
112+
timeout=args.timeout,
113+
delay=args.delay,
114+
parse_output_function=validation.parse_default,
115+
validate_output_function=validation.validate_default,
116+
timeout_as_error=True
117+
)
118+
119+
log.logger.info(f'Action server validator exited with code {ret_code}')
120+
121+
exit(ret_code.value)

0 commit comments

Comments
 (0)