DCCL v5
Loading...
Searching...
No Matches
test_dccl.py
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2026:
4# GobySoft, LLC (2013-)
5# Community contributors (see AUTHORS file)
6#
7# This file is part of the Dynamic Compact Control Language Library ("DCCL").
8#
9# DCCL is free software: you can redistribute it and/or modify it under the
10# terms of the GNU Lesser General Public License as published by the Free
11# Software Foundation, either version 2.1 of the License, or (at your option)
12# any later version.
13#
14# DCCL is distributed in the hope that it will be useful, but WITHOUT ANY
15# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
17# details.
18#
19# You should have received a copy of the GNU Lesser General Public License
20# along with DCCL. If not, see <http://www.gnu.org/licenses/>.
21
22"""Unit tests for the DCCL Python bindings.
23
24These tests cover:
25 - Basic encode/decode round-trip for a message with an ID field.
26 - decode_with_full_name for a normal (ID-bearing) message.
27 - decode_with_full_name for a message with omit_id=true.
28 - Error cases: unknown type, mismatched type.
29
30The tests require the following environment variables (set by CMake):
31 DCCL_TEST_PROTO_PATH - directory containing test.proto
32 DCCL_INC_PATH - directory containing dccl/option_extensions.proto
33"""
34
35import os
36import sys
37import unittest
38
39# Allow running from the source tree without installation: if the DCCL Python
40# source directory is on PYTHONPATH the module can be imported directly.
41try:
42 import dccl
43except ImportError as exc:
44 sys.exit("Could not import dccl Python module: {}".format(exc))
45
46# Resolve paths from environment (set by CTest / CMake) or fall back to
47# guessing relative to this file for developer convenience.
48_this_dir = os.path.dirname(os.path.abspath(__file__))
49PROTO_PATH = os.environ.get("DCCL_TEST_PROTO_PATH", _this_dir)
50INC_PATH = os.environ.get("DCCL_INC_PATH", "")
51
52
54 """Load test.proto into DCCL's DynamicProtobufManager and import the
55 generated *_pb2 module. Returns the pb2 module."""
56 if INC_PATH:
57 dccl.addProtoIncludePath(INC_PATH)
58 dccl.addProtoIncludePath(PROTO_PATH)
59 dccl.loadProtoFile(os.path.join(PROTO_PATH, "test.proto"))
60
61 # The pb2 file is generated alongside the test script by CMake.
62 try:
63 import test_pb2 # noqa: PLC0415 (import not at top level is intentional)
64 return test_pb2
65 except ImportError:
66 return None
67
68
69# Load the proto once for the whole test module.
71
72
74 """Return a freshly constructed dccl.Codec with all test types loaded."""
75 c = dccl.Codec()
76 c.load("dccl.test.python.NormalMsg")
77 c.load("dccl.test.python.AnotherMsg")
78 c.load("dccl.test.python.OmitIdMsg")
79 return c
80
81
82@unittest.skipIf(_pb2 is None, "test_pb2 not available (run via CMake)")
83class TestEncodeDecodeNormalMsg(unittest.TestCase):
84 """Tests for basic encode/decode of a message that carries a DCCL ID."""
85
86 def setUp(self):
87 self.codec = _make_codec()
88
89 def _make_msg(self, d=3.14, i=42):
90 msg = _pb2.NormalMsg()
91 msg.d = d
92 msg.i = i
93 return msg
94
95 def test_encode_returns_bytes(self):
96 msg = self._make_msg()
97 encoded = self.codec.encode(msg)
98 self.assertIsInstance(encoded, bytes)
99 self.assertGreater(len(encoded), 0)
100
101 def test_decode_round_trip(self):
102 original = self._make_msg(d=1.23, i=100)
103 encoded = self.codec.encode(original)
104 decoded = self.codec.decode(encoded)
105 self.assertAlmostEqual(decoded.d, 1.23, places=2)
106 self.assertEqual(decoded.i, 100)
107
108 def test_encode_decode_boundary_values(self):
109 msg = self._make_msg(d=-100.0, i=-20)
110 encoded = self.codec.encode(msg)
111 decoded = self.codec.decode(encoded)
112 self.assertAlmostEqual(decoded.d, -100.0, places=2)
113 self.assertEqual(decoded.i, -20)
114
115 def test_id_returns_correct_id(self):
116 msg = self._make_msg()
117 encoded = self.codec.encode(msg)
118 self.assertEqual(self.codec.id(encoded), 201)
119
120 def test_size_returns_positive_integer(self):
121 msg = self._make_msg()
122 self.assertGreater(self.codec.size(msg), 0)
123
124
125@unittest.skipIf(_pb2 is None, "test_pb2 not available (run via CMake)")
126class TestDecodeWithFullNameNormalMsg(unittest.TestCase):
127 """Tests for decode_with_full_name on a message that carries a DCCL ID."""
128
129 def setUp(self):
130 self.codec = _make_codec()
131
132 def _make_msg(self, d=7.5, i=10):
133 msg = _pb2.NormalMsg()
134 msg.d = d
135 msg.i = i
136 return msg
137
138 def test_decode_with_full_name_round_trip(self):
139 original = self._make_msg(d=7.5, i=10)
140 encoded = self.codec.encode(original)
141 decoded = self.codec.decode_with_full_name(
142 encoded, "dccl.test.python.NormalMsg"
143 )
144 self.assertAlmostEqual(decoded.d, 7.5, places=2)
145 self.assertEqual(decoded.i, 10)
146
147 def test_decode_with_full_name_matches_decode(self):
148 original = self._make_msg(d=50.0, i=500)
149 encoded = self.codec.encode(original)
150 via_decode = self.codec.decode(encoded)
151 via_full_name = self.codec.decode_with_full_name(
152 encoded, "dccl.test.python.NormalMsg"
153 )
154 self.assertAlmostEqual(via_decode.d, via_full_name.d, places=5)
155 self.assertEqual(via_decode.i, via_full_name.i)
156
157 def test_decode_with_full_name_unloaded_type_raises(self):
158 original = self._make_msg()
159 encoded = self.codec.encode(original)
160 with self.assertRaises(dccl.DcclException):
161 self.codec.decode_with_full_name(encoded, "dccl.test.python.NoSuchMsg")
162
164 """Encoding NormalMsg (ID=201) then requesting decode as AnotherMsg
165 (ID=202) should raise because the embedded ID does not match."""
166 original = self._make_msg()
167 encoded = self.codec.encode(original)
168 with self.assertRaises(dccl.DcclException):
169 self.codec.decode_with_full_name(encoded, "dccl.test.python.AnotherMsg")
170
171
172@unittest.skipIf(_pb2 is None, "test_pb2 not available (run via CMake)")
173class TestDecodeWithFullNameOmitId(unittest.TestCase):
174 """Tests for decode_with_full_name on a message with omit_id=true."""
175
176 def setUp(self):
177 self.codec = _make_codec()
178
179 def _make_omit_msg(self, d=5.0, i=1):
180 msg = _pb2.OmitIdMsg()
181 msg.d = d
182 msg.i = i
183 return msg
184
185 def test_encode_omit_id_returns_bytes(self):
186 msg = self._make_omit_msg()
187 encoded = self.codec.encode(msg)
188 self.assertIsInstance(encoded, bytes)
189 self.assertGreater(len(encoded), 0)
190
191 def test_decode_with_full_name_omit_id_round_trip(self):
192 original = self._make_omit_msg(d=5.0, i=1)
193 encoded = self.codec.encode(original)
194 decoded = self.codec.decode_with_full_name(
195 encoded, "dccl.test.python.OmitIdMsg"
196 )
197 self.assertAlmostEqual(decoded.d, 5.0, places=2)
198 self.assertEqual(decoded.i, 1)
199
200 def test_decode_with_full_name_omit_id_various_values(self):
201 for d_val, i_val in [(-99.99, -20), (0.0, 0), (99.99, 3000)]:
202 with self.subTest(d=d_val, i=i_val):
203 original = self._make_omit_msg(d=d_val, i=i_val)
204 encoded = self.codec.encode(original)
205 decoded = self.codec.decode_with_full_name(
206 encoded, "dccl.test.python.OmitIdMsg"
207 )
208 self.assertAlmostEqual(decoded.d, d_val, places=2)
209 self.assertEqual(decoded.i, i_val)
210
212 """codec.decode() should raise for an omit_id message because there is
213 no embedded ID to look up."""
214 msg = self._make_omit_msg()
215 encoded = self.codec.encode(msg)
216 with self.assertRaises(dccl.DcclException):
217 self.codec.decode(encoded)
218
219
220@unittest.skipIf(_pb2 is None, "test_pb2 not available (run via CMake)")
221class TestMiscCodecMethods(unittest.TestCase):
222 """Tests for miscellaneous Codec methods."""
223
224 def setUp(self):
225 self.codec = _make_codec()
226
227 def test_id_from_descriptor_string(self):
228 self.assertEqual(self.codec.id("dccl.test.python.NormalMsg"), 201)
229
230 def test_set_strict_does_not_raise(self):
231 self.codec.set_strict(1)
232 self.codec.set_strict(0)
233
234 def test_encode_out_of_range_raises_with_strict(self):
235 self.codec.set_strict(1)
236 msg = _pb2.NormalMsg()
237 msg.d = 200.0 # out of [-100, 100]
238 msg.i = 0
239 with self.assertRaises(Exception):
240 self.codec.encode(msg)
241
242
243if __name__ == "__main__":
244 unittest.main()
The Dynamic CCL enCODer/DECoder. This is the main class you will use to load, encode and decode DCCL ...
Definition codec.h:61
_make_msg(self, d=3.14, i=42)
Definition test_dccl.py:89
_load_proto_and_pb2()
Definition test_dccl.py:53
_make_codec()
Definition test_dccl.py:73