Skip to content

Commit 7d9468a

Browse files
authored
Improve unit test coverage (#161)
- Improve coverage of comparisons and remove unnecessary checks in their evaluate methods (in parsing loop) - Improve coverage of encodings - Improve coverage of packets - Add coverage for minimum dtype functions in xarr module
1 parent bc05878 commit 7d9468a

8 files changed

Lines changed: 334 additions & 82 deletions

File tree

space_packet_parser/xarr.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
def _min_dtype_for_encoding(data_encoding: encodings.DataEncoding):
20-
"""Find the minimum data type capaable of representing an XTCE data encoding.
20+
"""Find the minimum data type capable of representing an XTCE data encoding.
2121
2222
This only works for raw values and does not apply to calibrated or otherwise derived values.
2323
@@ -91,6 +91,10 @@ def _get_minimum_numpy_datatype(
9191
# If we are using raw values, we can determine the minimal dtype from the parameter data encoding
9292
return _min_dtype_for_encoding(data_encoding)
9393

94+
if isinstance(parameter_type, parameter_types.EnumeratedParameterType):
95+
# Enums are always strings in their derived state
96+
return "str"
97+
9498
if isinstance(data_encoding, encodings.NumericDataEncoding):
9599
if not (data_encoding.context_calibrators is not None or data_encoding.default_calibrator is not None):
96100
# If there are no calibrators attached to the encoding, then we can proceed as if we're using
@@ -103,10 +107,6 @@ def _get_minimum_numpy_datatype(
103107
if isinstance(data_encoding, encodings.BinaryDataEncoding):
104108
return "bytes"
105109

106-
if isinstance(parameter_type, parameter_types.EnumeratedParameterType):
107-
# Enums are always strings in their derived state
108-
return "str"
109-
110110
if isinstance(data_encoding, encodings.StringDataEncoding):
111111
return "str"
112112

space_packet_parser/xtce/comparisons.py

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Matching logical objects"""
2-
import warnings
32
from abc import ABCMeta, abstractmethod
43
from collections import namedtuple
54
from typing import Any, Optional, Union
@@ -55,8 +54,13 @@ def evaluate(self,
5554
class Comparison(MatchCriteria):
5655
"""<xtce:Comparison>"""
5756

58-
def __init__(self, required_value: str, referenced_parameter: str,
59-
operator: str = "==", use_calibrated_value: bool = True):
57+
def __init__(
58+
self,
59+
required_value: str,
60+
referenced_parameter: str,
61+
operator: str = "==",
62+
use_calibrated_value: bool = True
63+
):
6064
"""Constructor
6165
6266
Parameters
@@ -176,18 +180,11 @@ def evaluate(self,
176180
if self.referenced_parameter in packet:
177181
if self.use_calibrated_value:
178182
parsed_value = packet[self.referenced_parameter]
179-
if not parsed_value:
180-
raise ComparisonError(f"Comparison {self} was instructed to useCalibratedValue (the default)"
181-
f"but {self.referenced_parameter} does not appear to have a derived value.")
182183
else:
183184
parsed_value = packet[self.referenced_parameter].raw_value
184185
elif current_parsed_value is not None:
185186
# Assume then that the comparison is a reference to its own uncalibrated value
186187
parsed_value = current_parsed_value
187-
if self.use_calibrated_value:
188-
warnings.warn("Performing a comparison against a current value (e.g. a Comparison within a "
189-
"context calibrator contains a reference to its own uncalibrated value but use_"
190-
"calibrated_value is set to true. This is nonsensical. Using the uncalibrated value...")
191188
else:
192189
raise ValueError("Attempting to resolve a Comparison expression but the referenced parameter does not "
193190
"appear in the parsed data so far and no current raw value was passed "
@@ -200,9 +197,6 @@ def evaluate(self,
200197
except ValueError as err:
201198
raise ComparisonError(f"Unable to coerce {self.required_value} of type {type(self.required_value)} to "
202199
f"type {t_comparate} for comparison evaluation.") from err
203-
if required_value is None or parsed_value is None:
204-
raise ValueError(f"Error in Comparison. Cannot compare {required_value} with {parsed_value}. "
205-
"Neither should be None.")
206200

207201
# x.__le__(y) style call
208202
return getattr(parsed_value, operator)(required_value)
@@ -379,7 +373,7 @@ def evaluate(self,
379373
packet : packets.CCSDSPacket
380374
Packet data used to evaluate truthyness of the match criteria.
381375
current_parsed_value : Optional[Union[int, float]]
382-
Current value being parsed. NOTE: This is currently ignored. See the TODO item below.
376+
Ignored.
383377
384378
Returns
385379
-------
@@ -389,21 +383,8 @@ def evaluate(self,
389383

390384
def _get_parsed_value(parameter_name: str, use_calibrated: bool):
391385
"""Retrieves the previously parsed value from the passed in packet"""
392-
try:
393-
return packet[parameter_name] if use_calibrated \
394-
else packet[parameter_name].raw_value
395-
except KeyError as e:
396-
raise ComparisonError(f"Attempting to perform a Condition evaluation on {self.left_param} but "
397-
"the referenced parameter does not appear in the hitherto parsed data passed to "
398-
"the evaluate method. If you intended a comparison against the raw value of the "
399-
"parameter currently being parsed, unfortunately that is not currently supported."
400-
) from e
401-
402-
# TODO: Consider allowing one of the parameters to be the parameter currently being evaluated.
403-
# This isn't explicitly provided for in the XTCE spec but it seems reasonable to be able to
404-
# perform conditionals against the current raw value of a parameter, e.g. while determining if it
405-
# should be calibrated. Note that only one of the parameters can be used this way and it must reference
406-
# an uncalibrated value so the logic and error handling must be done carefully.
386+
return packet[parameter_name] if use_calibrated else packet[parameter_name].raw_value
387+
407388
left_value = _get_parsed_value(self.left_param, self.left_use_calibrated_value)
408389
# Convert XML operator representation to a python-compatible operator (e.g. '&gt;' to '__gt__')
409390
operator = self._valid_operators[self.operator]
@@ -415,8 +396,6 @@ def _get_parsed_value(parameter_name: str, use_calibrated: bool):
415396
right_value = t_left_param(self.right_value)
416397
else:
417398
raise ValueError(f"Error when evaluating condition {self}. Neither right_param nor right_value is set.")
418-
if left_value is None or right_value is None:
419-
raise ComparisonError(f"Error comparing {left_value} and {right_value}. Neither should be None.")
420399

421400
# x.__le__(y) style call
422401
return getattr(left_value, operator)(right_value)
@@ -526,7 +505,7 @@ def evaluate(self,
526505
packet : packets.CCSDSPacket
527506
Packet data used to evaluate truthyness of the match criteria.
528507
current_parsed_value : Optional[Union[int, float]]
529-
Current value being parsed.
508+
Ignored.
530509
531510
Returns
532511
-------

space_packet_parser/xtce/definitions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ def parse_ccsds_packet(self,
423423

424424
def packet_generator(
425425
self,
426-
binary_data: Union[BinaryIO, socket.socket],
426+
binary_data: Union[BinaryIO, socket.socket, bytes],
427427
*,
428428
parse_bad_pkts: bool = True,
429429
root_container_name: Optional[str] = None,

space_packet_parser/xtce/encodings.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,51 @@ def to_xml(self, *, elmaker: ElementMaker) -> ElementTree.Element:
638638

639639
class IntegerDataEncoding(NumericDataEncoding):
640640
"""<xtce:IntegerDataEncoding>"""
641+
_encodings = ("unsigned", "signed", "twosCompliment", "twosComplement")
642+
_byte_orders = ("leastSignificantByteFirst", "mostSignificantByteFirst")
641643
_data_return_class = common.IntParameter
642644

645+
def __init__(self,
646+
size_in_bits: int,
647+
encoding: str,
648+
*,
649+
byte_order: str = "mostSignificantByteFirst",
650+
default_calibrator: Optional[calibrators.Calibrator] = None,
651+
context_calibrators: Optional[list[calibrators.ContextCalibrator]] = None):
652+
"""Constructor
653+
654+
Parameters
655+
----------
656+
size_in_bits : int
657+
Size of the integer
658+
encoding : str
659+
String indicating the type of encoding for the integer. FSW seems to use primarily 'signed' and 'unsigned',
660+
though 'signed' is not actually a valid specifier according to XTCE. 'twosCompliment' [sic] should be used
661+
instead, though we support the unofficial 'signed' specifier here.
662+
For supported specifiers, see XTCE spec 4.3.2.2.5.6.2
663+
byte_order : str
664+
Description of the byte order. Default is 'mostSignficantByteFirst' (big-endian).
665+
default_calibrator : Optional[Calibrator]
666+
Optional Calibrator object, containing information on how to transform the integer-encoded data, e.g. via
667+
a polynomial conversion or spline interpolation.
668+
context_calibrators : Optional[List[ContextCalibrator]]
669+
List of ContextCalibrator objects, containing match criteria and corresponding calibrators to use in
670+
various scenarios, based on other parameters.
671+
"""
672+
if encoding not in self._encodings:
673+
raise ValueError(f"Encoding must be one of {self._encodings}")
674+
675+
if byte_order not in self._byte_orders:
676+
raise ValueError(f"Byte order must be one of {self._byte_orders}")
677+
678+
super().__init__(
679+
size_in_bits,
680+
encoding,
681+
byte_order=byte_order,
682+
default_calibrator=default_calibrator,
683+
context_calibrators=context_calibrators
684+
)
685+
643686
def _get_raw_value(self, packet: packets.CCSDSPacket) -> int:
644687
# Extract the bits from the data in big-endian order from the packet
645688
val = packet.raw_data.read_as_int(self.size_in_bits)
@@ -873,6 +916,11 @@ def __init__(self,
873916
Function that linearly adjusts a size. e.g. if the size reference parameter gives a length in bytes, the
874917
linear adjuster should multiply by 8 to give the size in bits.
875918
"""
919+
if not any([fixed_size_in_bits, size_reference_parameter, size_discrete_lookup_list]):
920+
raise ValueError("Binary data encoding initialized with no way to determine a size. "
921+
"You must provide one of "
922+
"fixed_size_in_bits, size_reference_parameter, size_discrete_lookup_list.")
923+
876924
self.fixed_size_in_bits = fixed_size_in_bits
877925
self.size_reference_parameter = size_reference_parameter
878926
self.use_calibrated_value = use_calibrated_value
@@ -895,17 +943,14 @@ def _calculate_size(self, packet: packets.CCSDSPacket) -> int:
895943
len_bits = packet[field_length_reference]
896944
else:
897945
len_bits = packet[field_length_reference].raw_value
898-
elif self.size_discrete_lookup_list is not None:
946+
else: # self.size_discrete_lookup_list is not None:
899947
for discrete_lookup in self.size_discrete_lookup_list:
900948
len_bits = discrete_lookup.evaluate(packet)
901949
if len_bits is not None:
902950
break
903951
else:
904952
raise ValueError('List of discrete lookup values being used for determining length of '
905953
f'string {self} found no matches based on {packet}.')
906-
else:
907-
raise ValueError("Unable to parse BinaryDataEncoding. "
908-
"No fixed size, dynamic size, or dynamic lookup size were provided.")
909954

910955
if self.linear_adjuster is not None:
911956
# NOTE: This is assumed to be an integer value, represented as a float. If the linear adjuster

tests/unit/test_packets.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Tests for packets"""
2+
import socket
3+
24
import pytest
35

46
from space_packet_parser import packets
@@ -112,7 +114,7 @@ def test_ccsds_packet_data_lookups():
112114
assert packet.user_data == {x: x for x in range(7, 10)}
113115

114116
with pytest.raises(KeyError):
115-
packet[10]
117+
_ = packet[10]
116118

117119

118120
def test_continuation_packets(test_data_dir):
@@ -204,3 +206,36 @@ def test__extract_bits(start, nbits):
204206
data = int(s, 2).to_bytes(2, byteorder="big")
205207

206208
assert packets._extract_bits(data, start, nbits) == int(s[start:start + nbits], 2)
209+
210+
211+
def test_ccsds_generator(jpss_test_data_dir):
212+
"""Test ccsds_generator"""
213+
test_data_file = jpss_test_data_dir / "J01_G011_LZ_2021-04-09T00-00-00Z_V01.DAT1"
214+
test_packet = packets.create_ccsds_packet() # defaults
215+
216+
# From file
217+
with test_data_file.open('rb') as f:
218+
assert next(packets.ccsds_generator(f))
219+
220+
# From socket
221+
send, recv = socket.socketpair()
222+
send.send(test_packet)
223+
assert next(packets.ccsds_generator(recv))
224+
send.close()
225+
recv.close()
226+
227+
# From bytes
228+
# This covers show_progress conditional code and also the end of the iterator
229+
gen_from_bytes = packets.ccsds_generator(test_packet, show_progress=True)
230+
assert next(gen_from_bytes)
231+
with pytest.raises(StopIteration):
232+
next(gen_from_bytes)
233+
234+
# From Text file (error)
235+
with test_data_file.open('rt') as f:
236+
with pytest.raises(OSError, match="Packet data file opened in TextIO mode"):
237+
next(packets.ccsds_generator(f))
238+
239+
# Unrecognized source (error)
240+
with pytest.raises(OSError, match="Unrecognized data source"):
241+
next(packets.ccsds_generator(1))

tests/unit/test_xarr.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Tests for the xarr.py extras module"""
2+
import pytest
3+
4+
from space_packet_parser import xarr
5+
from space_packet_parser.xtce import calibrators, containers, definitions, encodings, parameter_types, parameters
6+
7+
np = pytest.importorskip("numpy", reason="numpy is not available")
8+
9+
10+
@pytest.fixture
11+
def test_xtce():
12+
"""Test definition for testing surmising data types"""
13+
container_set = [
14+
containers.SequenceContainer(
15+
"CONTAINER",
16+
entry_list=[
17+
parameters.Parameter(
18+
"INT32_PARAM",
19+
parameter_type=parameter_types.IntegerParameterType(
20+
"I32_TYPE",
21+
encoding=encodings.IntegerDataEncoding(size_in_bits=32, encoding="twosComplement")
22+
)
23+
),
24+
parameters.Parameter(
25+
"F32_PARAM",
26+
parameter_type=parameter_types.FloatParameterType(
27+
"F32_TYPE",
28+
encoding=encodings.FloatDataEncoding(size_in_bits=32, encoding="IEEE754")
29+
)
30+
),
31+
parameters.Parameter(
32+
"CAL_INT_PARAM",
33+
parameter_type=parameter_types.IntegerParameterType(
34+
"I32_TYPE",
35+
encoding=encodings.IntegerDataEncoding(
36+
size_in_bits=32,
37+
encoding="twosComplement",
38+
default_calibrator=calibrators.PolynomialCalibrator(
39+
coefficients=[
40+
calibrators.PolynomialCoefficient(1, 1)
41+
]
42+
)
43+
)
44+
)
45+
),
46+
parameters.Parameter(
47+
"BIN_PARAM",
48+
parameter_type=parameter_types.BinaryParameterType(
49+
"BIN_TYPE",
50+
encoding=encodings.BinaryDataEncoding(
51+
fixed_size_in_bits=32
52+
)
53+
)
54+
),
55+
parameters.Parameter(
56+
"INT_ENUM_PARAM",
57+
parameter_type=parameter_types.EnumeratedParameterType(
58+
"INT_ENUM_TYPE",
59+
encoding=encodings.IntegerDataEncoding(size_in_bits=8, encoding="unsigned"),
60+
enumeration={
61+
"ONE": 1,
62+
"TWO": 2
63+
}
64+
)
65+
),
66+
parameters.Parameter(
67+
"STR_PARAM",
68+
parameter_type=parameter_types.StringParameterType(
69+
"STR_TYPE",
70+
encoding=encodings.StringDataEncoding(
71+
fixed_raw_length=32
72+
)
73+
)
74+
),
75+
]
76+
)
77+
]
78+
return definitions.XtcePacketDefinition(container_set=container_set)
79+
80+
@pytest.mark.parametrize(
81+
("pname", "use_raw_value", "expected_dtype"),
82+
[
83+
("INT32_PARAM", True, "int32"),
84+
("INT32_PARAM", False, "int32"),
85+
("F32_PARAM", False, "float32"),
86+
("F32_PARAM", True, "float32"),
87+
("CAL_INT_PARAM", True, "int32"),
88+
("CAL_INT_PARAM", False, None),
89+
("BIN_PARAM", True, "bytes"),
90+
("BIN_PARAM", False, "bytes"),
91+
("INT_ENUM_PARAM", True, "uint8"),
92+
("INT_ENUM_PARAM", False, "str"),
93+
("STR_PARAM", True, "str"),
94+
("STR_PARAM", False, "str"),
95+
]
96+
)
97+
def test_minimum_numpy_dtype(test_xtce, pname, use_raw_value, expected_dtype):
98+
"""Test finding the minimum numpy data type for a parameter"""
99+
assert xarr._get_minimum_numpy_datatype(pname, test_xtce, use_raw_value) == expected_dtype

0 commit comments

Comments
 (0)