move local variables
[ieee754fpu.git] / src / ieee754 / part_mul_add / test / test_multiply.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: LGPL-2.1-or-later
3 # See Notices.txt for copyright information
4
5 from ieee754.part_mul_add.multiply import \
6 (PartitionPoints, PartitionedAdder, AddReduce,
7 Mul8_16_32_64, OP_MUL_LOW, OP_MUL_SIGNED_HIGH,
8 OP_MUL_SIGNED_UNSIGNED_HIGH, OP_MUL_UNSIGNED_HIGH)
9 from nmigen import Signal, Module
10 from nmigen.back.pysim import Simulator, Delay, Tick, Passive
11 from nmigen.hdl.ast import Assign, Value
12 from typing import Any, Generator, List, Union, Optional, Tuple, Iterable
13 import unittest
14 from hashlib import sha256
15 import enum
16 import pdb
17
18
19 def create_simulator(module: Any,
20 traces: List[Signal],
21 test_name: str) -> Simulator:
22 return Simulator(module,
23 vcd_file=open(test_name + ".vcd", "w"),
24 gtkw_file=open(test_name + ".gtkw", "w"),
25 traces=traces)
26
27
28 AsyncProcessCommand = Union[Delay, Tick, Passive, Assign, Value]
29 ProcessCommand = Optional[AsyncProcessCommand]
30 AsyncProcessGenerator = Generator[AsyncProcessCommand, Union[int, None], None]
31 ProcessGenerator = Generator[ProcessCommand, Union[int, None], None]
32
33
34 class TestPartitionPoints(unittest.TestCase):
35 def test(self) -> None:
36 module = Module()
37 width = 16
38 mask = Signal(width)
39 partition_point_10 = Signal()
40 partition_points = PartitionPoints({1: True,
41 5: False,
42 10: partition_point_10})
43 module.d.comb += mask.eq(partition_points.as_mask(width))
44 with create_simulator(module,
45 [mask, partition_point_10],
46 "partition_points") as sim:
47 def async_process() -> AsyncProcessGenerator:
48 self.assertEqual((yield partition_points[1]), True)
49 self.assertEqual((yield partition_points[5]), False)
50 yield partition_point_10.eq(0)
51 yield Delay(1e-6)
52 self.assertEqual((yield mask), 0xFFFD)
53 yield partition_point_10.eq(1)
54 yield Delay(1e-6)
55 self.assertEqual((yield mask), 0xFBFD)
56
57 sim.add_process(async_process)
58 sim.run()
59
60
61 class TestPartitionedAdder(unittest.TestCase):
62 def test(self) -> None:
63 width = 16
64 partition_nibbles = Signal()
65 partition_bytes = Signal()
66 module = PartitionedAdder(width,
67 {0x4: partition_nibbles,
68 0x8: partition_bytes | partition_nibbles,
69 0xC: partition_nibbles})
70 with create_simulator(module,
71 [partition_nibbles,
72 partition_bytes,
73 module.a,
74 module.b,
75 module.output],
76 "partitioned_adder") as sim:
77 def async_process() -> AsyncProcessGenerator:
78 def test_add(msg_prefix: str,
79 *mask_list: Tuple[int, ...]) -> Any:
80 for a, b in [(0x0000, 0x0000),
81 (0x1234, 0x1234),
82 (0xABCD, 0xABCD),
83 (0xFFFF, 0x0000),
84 (0x0000, 0x0000),
85 (0xFFFF, 0xFFFF),
86 (0x0000, 0xFFFF)]:
87 yield module.a.eq(a)
88 yield module.b.eq(b)
89 yield Delay(1e-6)
90 y = 0
91 for mask in mask_list:
92 y |= mask & ((a & mask) + (b & mask))
93 output = (yield module.output)
94 msg = f"{msg_prefix}: 0x{a:X} + 0x{b:X}" + \
95 f" => 0x{y:X} != 0x{output:X}"
96 self.assertEqual(y, output, msg)
97 yield partition_nibbles.eq(0)
98 yield partition_bytes.eq(0)
99 yield from test_add("16-bit", 0xFFFF)
100 yield partition_nibbles.eq(0)
101 yield partition_bytes.eq(1)
102 yield from test_add("8-bit", 0xFF00, 0x00FF)
103 yield partition_nibbles.eq(1)
104 yield partition_bytes.eq(0)
105 yield from test_add("4-bit", 0xF000, 0x0F00, 0x00F0, 0x000F)
106
107 sim.add_process(async_process)
108 sim.run()
109
110
111 class GenOrCheck(enum.Enum):
112 Generate = enum.auto()
113 Check = enum.auto()
114
115
116 class TestAddReduce(unittest.TestCase):
117 def calculate_input_values(self,
118 input_count: int,
119 key: int,
120 extra_keys: List[int] = []
121 ) -> (List[int], List[str]):
122 input_values = []
123 input_values_str = []
124 for i in range(input_count):
125 if key == 0:
126 value = 0
127 elif key == 1:
128 value = 0xFFFF
129 elif key == 2:
130 value = 0x0111
131 else:
132 hash_input = f"{input_count} {i} {key} {extra_keys}"
133 hash = sha256(hash_input.encode()).digest()
134 value = int.from_bytes(hash, byteorder="little")
135 value &= 0xFFFF
136 input_values.append(value)
137 input_values_str.append(f"0x{value:04X}")
138 return input_values, input_values_str
139
140 def subtest_value(self,
141 inputs: List[Signal],
142 module: AddReduce,
143 mask_list: List[int],
144 gen_or_check: GenOrCheck,
145 values: List[int]) -> AsyncProcessGenerator:
146 if gen_or_check == GenOrCheck.Generate:
147 for i, v in zip(inputs, values):
148 yield i.eq(v)
149 yield Delay(1e-6)
150 y = 0
151 for mask in mask_list:
152 v = 0
153 for value in values:
154 v += value & mask
155 y |= mask & v
156 output = (yield module.output)
157 if gen_or_check == GenOrCheck.Check:
158 self.assertEqual(y, output, f"0x{y:X} != 0x{output:X}")
159 yield Tick()
160
161 def subtest_key(self,
162 input_count: int,
163 inputs: List[Signal],
164 module: AddReduce,
165 key: int,
166 mask_list: List[int],
167 gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
168 values, values_str = self.calculate_input_values(input_count, key)
169 if gen_or_check == GenOrCheck.Check:
170 with self.subTest(inputs=values_str):
171 yield from self.subtest_value(inputs,
172 module,
173 mask_list,
174 gen_or_check,
175 values)
176 else:
177 yield from self.subtest_value(inputs,
178 module,
179 mask_list,
180 gen_or_check,
181 values)
182
183 def subtest_run_sim(self,
184 input_count: int,
185 sim: Simulator,
186 partition_4: Signal,
187 partition_8: Signal,
188 inputs: List[Signal],
189 module: AddReduce,
190 delay_cycles: int) -> None:
191 def generic_process(gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
192 for partition_4_value, partition_8_value, mask_list in [
193 (0, 0, [0xFFFF]),
194 (0, 1, [0xFF00, 0x00FF]),
195 (1, 0, [0xFFF0, 0x000F]),
196 (1, 1, [0xFF00, 0x00F0, 0x000F])]:
197 key_count = 8
198 if gen_or_check == GenOrCheck.Check:
199 with self.subTest(partition_4=partition_4_value,
200 partition_8=partition_8_value):
201 for key in range(key_count):
202 with self.subTest(key=key):
203 yield from self.subtest_key(input_count,
204 inputs,
205 module,
206 key,
207 mask_list,
208 gen_or_check)
209 else:
210 if gen_or_check == GenOrCheck.Generate:
211 yield partition_4.eq(partition_4_value)
212 yield partition_8.eq(partition_8_value)
213 for key in range(key_count):
214 yield from self.subtest_key(input_count,
215 inputs,
216 module,
217 key,
218 mask_list,
219 gen_or_check)
220
221 def generate_process() -> AsyncProcessGenerator:
222 yield from generic_process(GenOrCheck.Generate)
223
224 def check_process() -> AsyncProcessGenerator:
225 if delay_cycles != 0:
226 for _ in range(delay_cycles):
227 yield Tick()
228 yield from generic_process(GenOrCheck.Check)
229
230 sim.add_clock(2e-6)
231 sim.add_process(generate_process)
232 sim.add_process(check_process)
233 sim.run()
234
235 def subtest_file(self,
236 input_count: int,
237 register_levels: List[int]) -> None:
238 max_level = AddReduce.get_max_level(input_count)
239 for level in register_levels:
240 if level > max_level:
241 return
242 partition_4 = Signal()
243 partition_8 = Signal()
244 partition_points = PartitionPoints()
245 partition_points[4] = partition_4
246 partition_points[8] = partition_8
247 width = 16
248 inputs = [Signal(width, name=f"input_{i}")
249 for i in range(input_count)]
250 module = AddReduce(inputs,
251 width,
252 register_levels,
253 partition_points)
254 file_name = "add_reduce"
255 if len(register_levels) != 0:
256 file_name += f"-{'_'.join(map(repr, register_levels))}"
257 file_name += f"-{input_count:02d}"
258 with create_simulator(module,
259 [partition_4,
260 partition_8,
261 *inputs,
262 module.output],
263 file_name) as sim:
264 self.subtest_run_sim(input_count,
265 sim,
266 partition_4,
267 partition_8,
268 inputs,
269 module,
270 len(register_levels))
271
272 def subtest_register_levels(self, register_levels: List[int]) -> None:
273 for input_count in range(0, 16):
274 with self.subTest(input_count=input_count,
275 register_levels=repr(register_levels)):
276 self.subtest_file(input_count, register_levels)
277
278 def test_empty(self) -> None:
279 self.subtest_register_levels([])
280
281 def test_0(self) -> None:
282 self.subtest_register_levels([0])
283
284 def test_1(self) -> None:
285 self.subtest_register_levels([1])
286
287 def test_2(self) -> None:
288 self.subtest_register_levels([2])
289
290 def test_3(self) -> None:
291 self.subtest_register_levels([3])
292
293 def test_4(self) -> None:
294 self.subtest_register_levels([4])
295
296 def test_5(self) -> None:
297 self.subtest_register_levels([5])
298
299 def test_0(self) -> None:
300 self.subtest_register_levels([0])
301
302 def test_0_1(self) -> None:
303 self.subtest_register_levels([0, 1])
304
305 def test_0_1_2(self) -> None:
306 self.subtest_register_levels([0, 1, 2])
307
308 def test_0_1_2_3(self) -> None:
309 self.subtest_register_levels([0, 1, 2, 3])
310
311 def test_0_1_2_3_4(self) -> None:
312 self.subtest_register_levels([0, 1, 2, 3, 4])
313
314 def test_0_1_2_3_4_5(self) -> None:
315 self.subtest_register_levels([0, 1, 2, 3, 4, 5])
316
317 def test_0_2(self) -> None:
318 self.subtest_register_levels([0, 2])
319
320 def test_0_3(self) -> None:
321 self.subtest_register_levels([0, 3])
322
323 def test_0_4(self) -> None:
324 self.subtest_register_levels([0, 4])
325
326 def test_0_5(self) -> None:
327 self.subtest_register_levels([0, 5])
328
329
330 class SIMDMulLane:
331 def __init__(self,
332 a_signed: bool,
333 b_signed: bool,
334 bit_width: int,
335 high_half: bool):
336 self.a_signed = a_signed
337 self.b_signed = b_signed
338 self.bit_width = bit_width
339 self.high_half = high_half
340
341 def __repr__(self):
342 return f"SIMDMulLane({self.a_signed}, {self.b_signed}, " +\
343 f"{self.bit_width}, {self.high_half})"
344
345
346 class TestMul8_16_32_64(unittest.TestCase):
347 @staticmethod
348 def simd_mul(a: int, b: int, lanes: List[SIMDMulLane]) -> Tuple[int, int]:
349 output = 0
350 intermediate_output = 0
351 shift = 0
352 for lane in lanes:
353 a_signed = lane.a_signed or not lane.high_half
354 b_signed = lane.b_signed or not lane.high_half
355 mask = (1 << lane.bit_width) - 1
356 sign_bit = 1 << (lane.bit_width - 1)
357 a_part = (a >> shift) & mask
358 if a_signed and (a_part & sign_bit) != 0:
359 a_part -= 1 << lane.bit_width
360 b_part = (b >> shift) & mask
361 if b_signed and (b_part & sign_bit) != 0:
362 b_part -= 1 << lane.bit_width
363 value = a_part * b_part
364 value &= (1 << (lane.bit_width * 2)) - 1
365 intermediate_output |= value << (shift * 2)
366 if lane.high_half:
367 value >>= lane.bit_width
368 value &= mask
369 output |= value << shift
370 shift += lane.bit_width
371 return output, intermediate_output
372
373 @staticmethod
374 def get_test_cases(lanes: List[SIMDMulLane],
375 keys: Iterable[int]) -> Iterable[Tuple[int, int]]:
376 mask = (1 << 64) - 1
377 for i in range(8):
378 hash_input = f"{i} {lanes} {list(keys)}"
379 hash = sha256(hash_input.encode()).digest()
380 value = int.from_bytes(hash, byteorder="little")
381 yield (value & mask, value >> 64)
382 a = 0
383 b = 0
384 shift = 0
385 for lane in lanes:
386 a |= 1 << (shift + lane.bit_width - 1)
387 b |= 1 << (shift + lane.bit_width - 1)
388 shift += lane.bit_width
389 yield a, b
390
391 def test_simd_mul_lane(self):
392 self.assertEqual(f"{SIMDMulLane(True, True, 8, False)}",
393 "SIMDMulLane(True, True, 8, False)")
394
395 def test_simd_mul(self):
396 lanes = [SIMDMulLane(True,
397 True,
398 8,
399 True),
400 SIMDMulLane(False,
401 False,
402 8,
403 True),
404 SIMDMulLane(True,
405 True,
406 16,
407 False),
408 SIMDMulLane(True,
409 False,
410 32,
411 True)]
412 a = 0x0123456789ABCDEF
413 b = 0xFEDCBA9876543210
414 output = 0x0121FA00FE1C28FE
415 intermediate_output = 0x0121FA0023E20B28C94DFE1C280AFEF0
416 self.assertEqual(self.simd_mul(a, b, lanes),
417 (output, intermediate_output))
418 a = 0x8123456789ABCDEF
419 b = 0xFEDCBA9876543210
420 output = 0x81B39CB4FE1C28FE
421 intermediate_output = 0x81B39CB423E20B28C94DFE1C280AFEF0
422 self.assertEqual(self.simd_mul(a, b, lanes),
423 (output, intermediate_output))
424
425 def test_signed_mul_from_unsigned(self):
426 for i in range(0, 0x10):
427 for j in range(0, 0x10):
428 si = i if i & 8 else i - 0x10 # signed i
429 sj = j if j & 8 else j - 0x10 # signed j
430 mulu = i * j
431 mulsu = si * j
432 mul = si * sj
433 with self.subTest(i=i, j=j, si=si, sj=sj,
434 mulu=mulu, mulsu=mulsu, mul=mul):
435 mulsu2 = mulu
436 if si < 0:
437 mulsu2 += ~j << 4
438 mulsu2 += 1 << 4
439 self.assertEqual(mulsu & 0xFF, mulsu2 & 0xFF)
440 mul2 = mulsu2
441 if sj < 0:
442 mul2 += ~i << 4
443 mul2 += 1 << 4
444 self.assertEqual(mul & 0xFF, mul2 & 0xFF)
445
446 def subtest_value(self,
447 a: int,
448 b: int,
449 module: Mul8_16_32_64,
450 lanes: List[SIMDMulLane],
451 gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
452 if gen_or_check == GenOrCheck.Generate:
453 yield module.a.eq(a)
454 yield module.b.eq(b)
455 output2, intermediate_output2 = self.simd_mul(a, b, lanes)
456 yield Delay(1e-6)
457 if gen_or_check == GenOrCheck.Check:
458 intermediate_output = (yield module._intermediate_output)
459 self.assertEqual(intermediate_output,
460 intermediate_output2,
461 f"0x{intermediate_output:X} "
462 + f"!= 0x{intermediate_output2:X}")
463 output = (yield module.output)
464 self.assertEqual(output, output2, f"0x{output:X} != 0x{output2:X}")
465 yield Tick()
466
467 def subtest_lanes_2(self,
468 lanes: List[SIMDMulLane],
469 module: Mul8_16_32_64,
470 gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
471 bit_index = 8
472 part_index = 0
473 for lane in lanes:
474 if lane.high_half:
475 if lane.a_signed:
476 if lane.b_signed:
477 op = OP_MUL_SIGNED_HIGH
478 else:
479 op = OP_MUL_SIGNED_UNSIGNED_HIGH
480 else:
481 self.assertFalse(lane.b_signed,
482 "unsigned * signed not supported")
483 op = OP_MUL_UNSIGNED_HIGH
484 else:
485 op = OP_MUL_LOW
486 self.assertEqual(lane.bit_width % 8, 0)
487 for i in range(lane.bit_width // 8):
488 if gen_or_check == GenOrCheck.Generate:
489 yield module.part_ops[part_index].eq(op)
490 part_index += 1
491 for i in range(lane.bit_width // 8 - 1):
492 if gen_or_check == GenOrCheck.Generate:
493 yield module.part_pts[bit_index].eq(0)
494 bit_index += 8
495 if bit_index < 64 and gen_or_check == GenOrCheck.Generate:
496 yield module.part_pts[bit_index].eq(1)
497 bit_index += 8
498 self.assertEqual(part_index, 8)
499 for a, b in self.get_test_cases(lanes, ()):
500 if gen_or_check == GenOrCheck.Check:
501 with self.subTest(a=f"{a:X}", b=f"{b:X}"):
502 yield from self.subtest_value(a, b, module, lanes, gen_or_check)
503 else:
504 yield from self.subtest_value(a, b, module, lanes, gen_or_check)
505
506 def subtest_lanes(self,
507 lanes: List[SIMDMulLane],
508 module: Mul8_16_32_64,
509 gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
510 if gen_or_check == GenOrCheck.Check:
511 with self.subTest(lanes=repr(lanes)):
512 yield from self.subtest_lanes_2(lanes, module, gen_or_check)
513 else:
514 yield from self.subtest_lanes_2(lanes, module, gen_or_check)
515
516 def subtest_file(self,
517 register_levels: List[int]) -> None:
518 module = Mul8_16_32_64(register_levels)
519 file_name = "mul8_16_32_64"
520 if len(register_levels) != 0:
521 file_name += f"-{'_'.join(map(repr, register_levels))}"
522 ports = [module.a,
523 module.b,
524 module._intermediate_output,
525 module.output]
526 ports.extend(module.part_ops)
527 ports.extend(module.part_pts.values())
528 with create_simulator(module, ports, file_name) as sim:
529 def process(gen_or_check: GenOrCheck) -> AsyncProcessGenerator:
530 for a_signed in False, True:
531 for b_signed in False, True:
532 if not a_signed and b_signed:
533 continue
534 for high_half in False, True:
535 if not high_half and not (a_signed and b_signed):
536 continue
537 yield from self.subtest_lanes(
538 [SIMDMulLane(a_signed,
539 b_signed,
540 64,
541 high_half)],
542 module,
543 gen_or_check)
544 yield from self.subtest_lanes(
545 [SIMDMulLane(a_signed,
546 b_signed,
547 32,
548 high_half)] * 2,
549 module,
550 gen_or_check)
551 yield from self.subtest_lanes(
552 [SIMDMulLane(a_signed,
553 b_signed,
554 16,
555 high_half)] * 4,
556 module,
557 gen_or_check)
558 yield from self.subtest_lanes(
559 [SIMDMulLane(a_signed,
560 b_signed,
561 8,
562 high_half)] * 8,
563 module,
564 gen_or_check)
565 yield from self.subtest_lanes([SIMDMulLane(False,
566 False,
567 32,
568 True),
569 SIMDMulLane(False,
570 False,
571 16,
572 True),
573 SIMDMulLane(False,
574 False,
575 8,
576 True),
577 SIMDMulLane(False,
578 False,
579 8,
580 True)],
581 module,
582 gen_or_check)
583 yield from self.subtest_lanes([SIMDMulLane(True,
584 False,
585 32,
586 True),
587 SIMDMulLane(True,
588 True,
589 16,
590 False),
591 SIMDMulLane(True,
592 True,
593 8,
594 True),
595 SIMDMulLane(False,
596 False,
597 8,
598 True)],
599 module,
600 gen_or_check)
601 yield from self.subtest_lanes([SIMDMulLane(True,
602 True,
603 8,
604 True),
605 SIMDMulLane(False,
606 False,
607 8,
608 True),
609 SIMDMulLane(True,
610 True,
611 16,
612 False),
613 SIMDMulLane(True,
614 False,
615 32,
616 True)],
617 module,
618 gen_or_check)
619
620 def generate_process() -> AsyncProcessGenerator:
621 yield from process(GenOrCheck.Generate)
622
623 def check_process() -> AsyncProcessGenerator:
624 if len(register_levels) != 0:
625 for _ in register_levels:
626 yield Tick()
627 yield from process(GenOrCheck.Check)
628
629 sim.add_clock(2e-6)
630 sim.add_process(generate_process)
631 sim.add_process(check_process)
632 sim.run()
633
634 def subtest_register_levels(self, register_levels: List[int]) -> None:
635 with self.subTest(register_levels=repr(register_levels)):
636 self.subtest_file(register_levels)
637
638 def test_empty(self) -> None:
639 self.subtest_register_levels([])
640
641 def test_0(self) -> None:
642 self.subtest_register_levels([0])
643
644 def test_1(self) -> None:
645 self.subtest_register_levels([1])
646
647 def test_2(self) -> None:
648 self.subtest_register_levels([2])
649
650 def test_3(self) -> None:
651 self.subtest_register_levels([3])
652
653 def test_4(self) -> None:
654 self.subtest_register_levels([4])
655
656 def test_5(self) -> None:
657 self.subtest_register_levels([5])
658
659 def test_6(self) -> None:
660 self.subtest_register_levels([6])
661
662 def test_7(self) -> None:
663 self.subtest_register_levels([7])
664
665 def test_8(self) -> None:
666 self.subtest_register_levels([8])
667
668 def test_9(self) -> None:
669 self.subtest_register_levels([9])
670
671 def test_10(self) -> None:
672 self.subtest_register_levels([10])
673
674 def test_0(self) -> None:
675 self.subtest_register_levels([0])
676
677 def test_0_1(self) -> None:
678 self.subtest_register_levels([0, 1])
679
680 def test_0_1_2(self) -> None:
681 self.subtest_register_levels([0, 1, 2])
682
683 def test_0_1_2_3(self) -> None:
684 self.subtest_register_levels([0, 1, 2, 3])
685
686 def test_0_1_2_3_4(self) -> None:
687 self.subtest_register_levels([0, 1, 2, 3, 4])
688
689 def test_0_1_2_3_4_5(self) -> None:
690 self.subtest_register_levels([0, 1, 2, 3, 4, 5])
691
692 def test_0_1_2_3_4_5_6(self) -> None:
693 self.subtest_register_levels([0, 1, 2, 3, 4, 5, 6])
694
695 def test_0_1_2_3_4_5_6_7(self) -> None:
696 self.subtest_register_levels([0, 1, 2, 3, 4, 5, 6, 7])
697
698 def test_0_1_2_3_4_5_6_7_8(self) -> None:
699 self.subtest_register_levels([0, 1, 2, 3, 4, 5, 6, 7, 8])
700
701 def test_0_1_2_3_4_5_6_7_8_9(self) -> None:
702 self.subtest_register_levels([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
703
704 def test_0_1_2_3_4_5_6_7_8_9_10(self) -> None:
705 self.subtest_register_levels([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
706
707 def test_0_2(self) -> None:
708 self.subtest_register_levels([0, 2])
709
710 def test_0_3(self) -> None:
711 self.subtest_register_levels([0, 3])
712
713 def test_0_4(self) -> None:
714 self.subtest_register_levels([0, 4])
715
716 def test_0_5(self) -> None:
717 self.subtest_register_levels([0, 5])
718
719 def test_0_6(self) -> None:
720 self.subtest_register_levels([0, 6])
721
722 def test_0_7(self) -> None:
723 self.subtest_register_levels([0, 7])
724
725 def test_0_8(self) -> None:
726 self.subtest_register_levels([0, 8])
727
728 def test_0_9(self) -> None:
729 self.subtest_register_levels([0, 9])
730
731 def test_0_10(self) -> None:
732 self.subtest_register_levels([0, 10])
733
734 if __name__ == '__main__':
735 unittest.main()