testplan: Check Bluetooth test plan according to core spec.

This CL checks more detail about the test plan arguments arccording to the
packet type. A result limit interval should be contained by the interval
from the core specification.

BUG=None
TEST=manually run with test plans
TEST=make test PYTHON=python2

Cq-Depend: chromium:1886573
Change-Id: I8acaeb59af1ced04eba042568223369131b58f4c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/graphyte/+/1895109
Tested-by: Cheng Yueh <cyueh@chromium.org>
Reviewed-by: Yong Hong <yhong@chromium.org>
Commit-Queue: Cheng Yueh <cyueh@chromium.org>
diff --git a/graphyte/testplan.py b/graphyte/testplan.py
index 0d5ad00..51d92be 100644
--- a/graphyte/testplan.py
+++ b/graphyte/testplan.py
@@ -28,7 +28,6 @@
 
 import csv
 import itertools
-import logging
 
 import graphyte_common  # pylint: disable=unused-import
 from graphyte.data_parser import ListChecker
@@ -483,6 +482,13 @@
 
 class BluetoothTestCaseFactory(TestCaseFactory):
   RF_TYPE = 'BLUETOOTH'
+
+  # The standard test limits are from
+  # https://www.bluetooth.com/specifications/qualification-test-requirements/.
+  # Current reference revisions are RF.TS.5.1.0 and RF-PHY.TS.5.1.1.
+  # RF.TS.5.1.0 describes tests for BDR or EDR.
+  # RF-PHY.TS.5.1.1 describes tests for LE.
+  # Each test is named by something like RF-PHY/TRM/BV-03-C.
   REQUIRED_ARG_CHECKER = {
       'rf_type':        ListChecker([RF_TYPE]),
       'component_name': ListChecker([str]),
@@ -495,9 +501,9 @@
       'rx_num_packets': ListChecker([int])}
   BT_RX_ARG_CHECKER = {
       'rx_num_bits': ListChecker([int])}
-  COMMON_RESULTS = [
-      'avg_power', 'acp_5', 'acp_4', 'acp_3', 'acp_2',
-      'acp_1', 'acp0', 'acp1', 'acp2', 'acp3', 'acp4', 'acp5']
+  ADJACENT_CHANNELS = ['acp_5', 'acp_4', 'acp_3', 'acp_2', 'acp_1',
+                       'acp0', 'acp1', 'acp2', 'acp3', 'acp4', 'acp5']
+  COMMON_RESULTS = ['avg_power'] + ADJACENT_CHANNELS
   FORMAT_RESULT_CHECKER = {
       'BDR': ['bandwidth_20db', 'freq_deviation', 'freq_drift', 'delta_f1_avg',
               'delta_f2_avg', 'delta_f2_max', 'delta_f2_f1_avg_ratio'],
@@ -517,9 +523,8 @@
   # NOTE2: delta_f2_f1_avg_ratio is the ratio of delta_f2_avg and delta_f1_avg,
   # so it needs 'F0' and 'AA' patterns.
   BIT_PATTERN_RESULTS = {
-      'PRBS9': ['avg_power', 'bandwidth_20db', 'acp_5', 'acp_4', 'acp_3',
-                'acp_2', 'acp_1', 'acp0', 'acp1', 'acp2', 'acp3', 'acp4',
-                'acp5', 'freq_offset', 'rx_ber', 'rx_per'],
+      'PRBS9': ['avg_power', 'bandwidth_20db', 'freq_offset', 'rx_ber',
+                'rx_per'] + ADJACENT_CHANNELS,
       'F0': ['freq_deviation', 'delta_f1_avg', 'delta_f2_f1_avg_ratio'],
       'AA': ['freq_drift', 'delta_f2_avg', 'delta_f2_max', 'delta_f1_f0',
              'delta_f2_f1_avg_ratio', 'delta_f0_fn_max', 'delta_fn_fn5_max']}
@@ -538,6 +543,19 @@
       '3DH3': 552,
       '3DH5': 1021}
 
+  # The limits of adjacent channel power are defined in RF-PHY/TRM/BV-03-C and
+  # RF/TRM/CA/BV-06-C.
+  ADJACENT_CHANNEL_POWER = {
+      'LE': dict(zip(
+          ADJACENT_CHANNELS,
+          [-30, -30, -30, -20, None, None, None, -20, -30, -30, -30])),
+      'BDR': dict(zip(
+          ADJACENT_CHANNELS,
+          [-40, -40, -40, -20, None, None, None, -20, -40, -40, -40])),
+      'EDR': dict(zip(
+          ADJACENT_CHANNELS,
+          [-40, -40, -40, -20, None, None, None, -20, -40, -40, -40]))}
+
   def _DetermineFormat(self, packet_type):
     """Determines the bluetooth format of the packet type.
 
@@ -557,28 +575,111 @@
       return 'EDR'
     return 'BDR'
 
+  def _VerifyArgAndResultStandardRX(self, packet_format, args, result_limit):
+    """Check if Bluetooth RX test limits are better than standard."""
+    AssertRangeContain('all packets', 'power_level',
+                       (None, -70), (None, args['power_level']))
+    if packet_format == 'LE':
+      # The limits of power_level, rx_num_packets, and rx_per for LE are defined
+      # in RF-PHY/RCV/BV-01-C.
+      AssertRangeContain(packet_format, 'rx_num_packets',
+                         (1500, None), (args['rx_num_packets'], None))
+      AssertRangeContain(packet_format, 'rx_per',
+                         (0, 30.8), result_limit['rx_per'])
+    elif packet_format == 'BDR':
+      # The limits of power_level, rx_num_bits, and rx_ber for BDR are defined
+      # in RF/RCV/CA/BV-02-C.
+      AssertRangeContain(packet_format, 'rx_num_bits',
+                         (1600000, None), (args['rx_num_bits'], None))
+      AssertRangeContain(packet_format, 'rx_ber',
+                         (0, 0.1), result_limit['rx_ber'])
+    else:  # packet_format == EDR
+      # The limits of power_level, rx_num_bits, and rx_ber for EDR are defined
+      # in RF/RCV/CA/BV-07-C.
+      standard_ber_level_0 = 0.007
+      standard_ber_level_1 = 0.01
+      AssertRangeContain(packet_format, 'rx_ber',
+                         (0, standard_ber_level_1), result_limit['rx_ber'])
+      if result_limit['rx_ber'][1] <= standard_ber_level_0:
+        message = 'EDR with rx_ber in [0, %f]' % standard_ber_level_0
+        standard_bits = (1600000, None)
+      else:
+        message = 'EDR with rx_ber in (%f, %f]' % (standard_ber_level_0,
+                                                   standard_ber_level_1)
+        standard_bits = (16000000, None)
+      AssertRangeContain(message, 'rx_num_bits', standard_bits,
+                         (args['rx_num_bits'], None))
+
+  def _VerifyArgAndResultStandardTX(self, packet_format, packet_type,
+                                    result_limit):
+    """Check if Bluetooth TX test limits are better than standard."""
+    standard_acp_list = self.ADJACENT_CHANNEL_POWER[packet_format]
+    for channel, standard_acp in standard_acp_list.items():
+      if channel in result_limit:
+        AssertRangeContain(
+            packet_format, channel, (None, standard_acp), result_limit[channel])
+    STANDARD_BT_TX = {
+        # The limits of delta_f1_avg, delta_f2_max, and delta_f2_f1_avg_ratio
+        # are defined in RF-PHY/TRM/BV-05-C and RF/TRM/CA/BV-07-C.
+        'delta_f1_avg': (225, 275) if packet_format == 'LE' else (140, 175),
+        'delta_f2_max': (182, None) if packet_format == 'LE' else (115, None),
+        'delta_f2_f1_avg_ratio': (0.8, None),
+        # The limits of delta_f0_fn_max, delta_f1_f0, delta_fn_fn5_max, and
+        # freq_offset are defined in RF-PHY/TRM/BV-06-C.
+        'delta_f0_fn_max': (None, 50),
+        'delta_f1_f0': (None, 23),
+        'delta_fn_fn5_max': (None, 20),
+        'freq_offset': (-150, 150),
+        # The limits of freq_drift are defined in RF/TRM/CA/BV-09-C.
+        'freq_drift': (-25, 25) if packet_type == '1DH1' else (-40, 40),
+        # The limits of edr_power_diff are defined in RF/TRM/CA/BV-10-C.
+        'edr_power_diff': (-4, 1),
+        # The limits of edr_evm_avg, edr_evm_peak, edr_prob_evm_99_pass,
+        # edr_extreme_omega_0, edr_extreme_omega_i0, and edr_omega_i are defined
+        # in RF/TRM/CA/BV-11-C.
+        'edr_evm_avg': (0, 20) if packet_type[0] == '2' else (0, 13),
+        'edr_evm_peak': (0, 35) if packet_type[0] == '2' else (0, 25),
+        'edr_prob_evm_99_pass': (0, 30) if packet_type[0] == '2' else (0, 20),
+        'edr_extreme_omega_0': (-10, 10),
+        'edr_extreme_omega_i0': (-75, 75),
+        'edr_omega_i': (-75, 75),
+        # The limits of bandwidth_20db are defined in RF/TRM/CA/BV-05-C.
+        'bandwidth_20db': (0, 1.0)
+    }
+    for arg_name, limits in STANDARD_BT_TX.items():
+      if arg_name in result_limit:
+        AssertRangeContain(
+            packet_type, arg_name, limits, result_limit[arg_name])
+
   def _VerifyArgAndResult(self, args, result_limit):
     # Check the RF type is valid.
     if self.RF_TYPE != args['rf_type']:
       raise ValueError('%s is not a valid RF type.' % args['rf_type'])
     # Check all required arguments are valid.
     arg_checker = self.REQUIRED_ARG_CHECKER.copy()
-    if args['test_type'] == 'RX':
-      if args['packet_type'] == 'LE':
+    test_type = args['test_type']
+    packet_type = args['packet_type']
+    packet_format = self._DetermineFormat(packet_type)
+    if test_type == 'RX':
+      if packet_format == 'LE':
         arg_checker.update(self.LE_RX_ARG_CHECKER)
       else:
         arg_checker.update(self.BT_RX_ARG_CHECKER)
     TestCaseFactory.VerifyArguments(arg_checker, args)
     # Check there is result and all results are valid.
-    if args['test_type'] == 'TX':
-      result_list = self.COMMON_RESULTS[:]
-      packet_format = self._DetermineFormat(args['packet_type'])
-      result_list += self.FORMAT_RESULT_CHECKER[packet_format]
-    elif args['packet_type'] == 'LE':
+    if test_type == 'TX':
+      result_list = (self.COMMON_RESULTS +
+                     self.FORMAT_RESULT_CHECKER[packet_format])
+    elif packet_format == 'LE':
       result_list = self.LE_RX_RESULTS
     else:
       result_list = self.BT_RX_RESULTS
     TestCaseFactory._VerifyResults(result_list, result_limit)
+    if test_type == 'RX':
+      self._VerifyArgAndResultStandardRX(packet_format, args, result_limit)
+    else:
+      self._VerifyArgAndResultStandardTX(
+          packet_format, packet_type, result_limit)
 
   def _Create(self, args, result_limit):
     """Creates the bluetooth test case.
diff --git a/graphyte/testplan_unittest.py b/graphyte/testplan_unittest.py
index 961daa4..325ab85 100755
--- a/graphyte/testplan_unittest.py
+++ b/graphyte/testplan_unittest.py
@@ -202,7 +202,7 @@
     valid_results = [
         '@result', 'bandwidth_20db', 'delta_f2_f1_avg_ratio']
     valid_result_value = [
-        '', '(16,20)', '(0,20)']
+        '', '(0,1.0)', '(0.8,None)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -220,7 +220,8 @@
         'edr_extreme_omega_i0', 'edr_omega_i', 'edr_power_diff',
         'edr_prob_evm_99_pass']
     valid_result_value = [
-        '', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,2)']
+        '', '(0, 13)', '(0, 25)', '(-10, 10)', '(-75, 75)', '(-75, 75)',
+        '(-4, 1)', '(0,2)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -238,8 +239,8 @@
         'delta_f1_f0', 'delta_f2_f1_avg_ratio', 'delta_f2_avg', 'delta_f2_max',
         'delta_fn_fn5_max']
     valid_result_value = [
-        '', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)']
+        '', '(0,20)', '(0,20)', '(225, 275)', '(0,20)', '(0.8, None)', '(0,20)',
+        '(182, None)', '(0,20)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -308,9 +309,10 @@
         'acp_3', 'acp_2', 'acp_1', 'acp0', 'acp1',
         'acp2', 'acp3', 'acp4', 'acp5']
     valid_result_value = [
-        '', '(16,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)', '(0,20)', '(0,20)']
+        '', '(16,20)', '(0, 1.0)', '(None, -40)', '(None, -40)',
+        '(None, -40)', '(None, -40)', '(None, -40)', '(None, -40)',
+        '(None, -40)', '(None, -40)', '(None, -40)', '(None, -40)',
+        '(None, -40)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -329,8 +331,8 @@
         'edr_extreme_omega_0', 'edr_extreme_omega_i0', 'edr_omega_i',
         'edr_power_diff', 'edr_prob_evm_99_pass']
     valid_result_value = [
-        '', '(16,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)', '(0,20)']
+        '', '(16,20)', '(0,20)', '(0,20)', '(0,20)', '(-10, 10)', '(0,20)',
+        '(0,20)', '(-4, 1)', '(0,20)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -349,9 +351,9 @@
         'acp_3', 'acp_2', 'acp_1', 'acp0', 'acp1',
         'acp2', 'acp3', 'acp4', 'acp5']
     valid_result_value = [
-        '', '(16,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)', '(0,20)', '(0,20)', '(0,20)',
-        '(0,20)', '(0,20)', '(0,20)', '(0,20)']
+        '', '(16,20)', '(0,20)', '(None, -30)', '(None, -30)',
+        '(None, -30)', '(None, -30)', '(0,20)', '(None, -30)', '(None, -30)',
+        '(None, -30)', '(None, -30)', '(None, -30)', '(None, -30)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -368,7 +370,7 @@
     valid_results = [
         '@result', 'delta_f1_avg', 'freq_deviation']
     valid_result_value = [
-        '', '(0,20)', '(0,20)']
+        '', '(140, 175)', '(0,20)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -385,7 +387,7 @@
     valid_results = [
         '@result', 'delta_f1_avg']
     valid_result_value = [
-        '', '(0,20)']
+        '', '(225, 275)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -402,7 +404,7 @@
     valid_results = [
         '@result', 'freq_drift', 'delta_f2_avg', 'delta_f2_max']
     valid_result_value = [
-        '', '(16,20)', '(16,20)', '(16,20)']
+        '', '(16,20)', '(16,20)', '(115, None)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -420,8 +422,8 @@
         '@result', 'delta_f2_avg', 'delta_f2_max', 'delta_f1_f0',
         'delta_f0_fn_max', 'delta_fn_fn5_max']
     valid_result_value = [
-        '', '(16,20)', '(0,20)', '(16,20)',
-        '(0,20)', '(16,20)']
+        '', '(16,20)', '(185, None)', '(None, 23)',
+        '(None, 50)', '(None, 20)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -440,7 +442,7 @@
         'delta_f1_avg',  # bit_pattern is F0
         'delta_f2_avg', 'delta_f2_max']  # bit_pattern is AA
     valid_result_value = [
-        '', '(16,20)', '(16,20)', '(0,20)', '(0,20)']
+        '', '(16,20)', '(225, 275)', '(0,20)', '(185, None)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -458,7 +460,7 @@
     valid_results = [
         '@result', 'delta_f2_f1_avg_ratio']
     valid_result_value = [
-        '', '(16,20)']
+        '', '(0.8,None)']
 
     self._WriteCSVContent([
         self.valid_args + valid_results,
@@ -473,7 +475,7 @@
     rx_args = ['rx_num_packets']
     valid_args_value = [
         '', 'BT', 'BLUETOOTH', 'RX', '2412',
-        '18', 'LE', '10000']
+        '-70', 'LE', '10000']
     valid_results = [
         '@result', 'rx_per']
     valid_result_value = [
@@ -490,11 +492,11 @@
     rx_args = ['rx_num_bits']
     valid_args_value = [
         '', 'BT', 'BLUETOOTH', 'RX', '2412',
-        '18', '1DH1', '10000']
+        '-70', '1DH1', '1600000']
     valid_results = [
         '@result', 'rx_ber']
     valid_result_value = [
-        '', '(0,20)']
+        '', '(0,0.1)']
 
     self._WriteCSVContent([
         self.valid_args + rx_args + valid_results,