wiliot-certificate 4.4.2__py3-none-any.whl → 4.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. {brg_certificate → certificate}/ag/wlt_cmd_if.html +10 -4
  2. {brg_certificate → certificate}/ag/wlt_types_ag.py +1878 -519
  3. certificate/cert_common.py +1488 -0
  4. certificate/cert_config.py +480 -0
  5. {brg_certificate → certificate}/cert_data_sim.py +134 -46
  6. {brg_certificate → certificate}/cert_defines.py +129 -128
  7. {brg_certificate → certificate}/cert_gw_sim.py +183 -53
  8. {brg_certificate → certificate}/cert_mqtt.py +179 -64
  9. {brg_certificate → certificate}/cert_prints.py +35 -33
  10. {brg_certificate → certificate}/cert_protobuf.py +15 -6
  11. {brg_certificate → certificate}/cert_results.py +240 -64
  12. certificate/cert_utils.py +634 -0
  13. certificate/certificate.py +205 -0
  14. certificate/certificate_cli.py +76 -0
  15. certificate/certificate_eth_test_list.txt +76 -0
  16. certificate/certificate_sanity_test_list.txt +66 -0
  17. certificate/certificate_test_list.txt +76 -0
  18. {brg_certificate → certificate}/tests/calibration/interval_test/interval_test.json +3 -2
  19. {brg_certificate → certificate}/tests/calibration/interval_test/interval_test.py +7 -6
  20. certificate/tests/calibration/output_power_test/output_power_test.json +23 -0
  21. certificate/tests/calibration/output_power_test/output_power_test.py +39 -0
  22. {brg_certificate → certificate}/tests/calibration/pattern_test/pattern_test.json +2 -1
  23. {brg_certificate → certificate}/tests/calibration/pattern_test/pattern_test.py +20 -15
  24. certificate/tests/cloud_connectivity/acl_ext_adv_test/acl_ext_adv_test.json +15 -0
  25. certificate/tests/cloud_connectivity/acl_ext_adv_test/acl_ext_adv_test.py +140 -0
  26. certificate/tests/cloud_connectivity/acl_test/acl_test.json +15 -0
  27. certificate/tests/cloud_connectivity/acl_test/acl_test.py +96 -0
  28. certificate/tests/cloud_connectivity/brg_ota_test/brg_ota_test.json +19 -0
  29. certificate/tests/cloud_connectivity/brg_ota_test/brg_ota_test.py +41 -0
  30. certificate/tests/cloud_connectivity/channel_scan_behaviour_test/channel_scan_behaviour_test.json +19 -0
  31. certificate/tests/cloud_connectivity/channel_scan_behaviour_test/channel_scan_behaviour_test.py +215 -0
  32. certificate/tests/cloud_connectivity/connection_test/connection_test.json +18 -0
  33. certificate/tests/cloud_connectivity/connection_test/connection_test.py +67 -0
  34. certificate/tests/cloud_connectivity/deduplication_test/deduplication_test.json +15 -0
  35. certificate/tests/cloud_connectivity/deduplication_test/deduplication_test.py +80 -0
  36. certificate/tests/cloud_connectivity/downlink_test/downlink_test.json +21 -0
  37. certificate/tests/cloud_connectivity/downlink_test/downlink_test.py +201 -0
  38. certificate/tests/cloud_connectivity/ext_adv_stress_test/ext_adv_stress_test.json +17 -0
  39. certificate/tests/cloud_connectivity/ext_adv_stress_test/ext_adv_stress_test.py +104 -0
  40. certificate/tests/cloud_connectivity/reboot_test/reboot_test.json +18 -0
  41. certificate/tests/cloud_connectivity/reboot_test/reboot_test.py +59 -0
  42. certificate/tests/cloud_connectivity/registration_test/registration_test.json +20 -0
  43. certificate/tests/cloud_connectivity/registration_test/registration_test.py +384 -0
  44. certificate/tests/cloud_connectivity/registration_test/registration_test_cli.py +90 -0
  45. certificate/tests/cloud_connectivity/stress_test/stress_test.json +17 -0
  46. certificate/tests/cloud_connectivity/stress_test/stress_test.py +101 -0
  47. certificate/tests/cloud_connectivity/uplink_ext_adv_test/uplink_ext_adv_test.json +25 -0
  48. certificate/tests/cloud_connectivity/uplink_ext_adv_test/uplink_ext_adv_test.py +92 -0
  49. certificate/tests/cloud_connectivity/uplink_test/uplink_test.json +20 -0
  50. certificate/tests/cloud_connectivity/uplink_test/uplink_test.py +169 -0
  51. {brg_certificate → certificate}/tests/datapath/aging_test/aging_test.json +2 -1
  52. certificate/tests/datapath/aging_test/aging_test.py +142 -0
  53. certificate/tests/datapath/event_ble5_test/event_ble5_test.json +17 -0
  54. certificate/tests/datapath/event_ble5_test/event_ble5_test.py +89 -0
  55. certificate/tests/datapath/event_test/event_test.json +17 -0
  56. certificate/tests/datapath/event_test/event_test.py +80 -0
  57. {brg_certificate → certificate}/tests/datapath/num_of_tags_test/num_of_tags_test.json +4 -3
  58. {brg_certificate → certificate}/tests/datapath/num_of_tags_test/num_of_tags_test.py +19 -13
  59. certificate/tests/datapath/output_power_test/output_power_test.json +23 -0
  60. {brg_certificate → certificate}/tests/datapath/output_power_test/output_power_test.py +17 -6
  61. {brg_certificate → certificate}/tests/datapath/pacer_interval_ble5_test/pacer_interval_ble5_test.json +2 -1
  62. {brg_certificate → certificate}/tests/datapath/pacer_interval_ble5_test/pacer_interval_ble5_test.py +13 -11
  63. {brg_certificate → certificate}/tests/datapath/pacer_interval_test/pacer_interval_test.json +2 -1
  64. {brg_certificate → certificate}/tests/datapath/pacer_interval_test/pacer_interval_test.py +9 -7
  65. {brg_certificate → certificate}/tests/datapath/pattern_test/pattern_test.json +3 -2
  66. {brg_certificate → certificate}/tests/datapath/pattern_test/pattern_test.py +18 -6
  67. certificate/tests/datapath/pkt_filter_ble5_chl21_test/pkt_filter_ble5_chl21_test.json +20 -0
  68. certificate/tests/datapath/pkt_filter_ble5_chl21_test/pkt_filter_ble5_chl21_test.py +61 -0
  69. {brg_certificate → certificate}/tests/datapath/pkt_filter_ble5_test/pkt_filter_ble5_test.json +2 -1
  70. {brg_certificate → certificate}/tests/datapath/pkt_filter_ble5_test/pkt_filter_ble5_test.py +15 -14
  71. certificate/tests/datapath/pkt_filter_brg2gw_ext_adv_test/pkt_filter_brg2gw_ext_adv_test.json +19 -0
  72. certificate/tests/datapath/pkt_filter_brg2gw_ext_adv_test/pkt_filter_brg2gw_ext_adv_test.py +85 -0
  73. {brg_certificate → certificate}/tests/datapath/pkt_filter_gen3_test/pkt_filter_gen3_test.json +2 -1
  74. {brg_certificate → certificate}/tests/datapath/pkt_filter_gen3_test/pkt_filter_gen3_test.py +10 -9
  75. {brg_certificate → certificate}/tests/datapath/pkt_filter_test/pkt_filter_test.json +2 -1
  76. {brg_certificate → certificate}/tests/datapath/pkt_filter_test/pkt_filter_test.py +10 -9
  77. {brg_certificate → certificate}/tests/datapath/rssi_threshold_test/rssi_threshold_test.json +3 -2
  78. {brg_certificate → certificate}/tests/datapath/rssi_threshold_test/rssi_threshold_test.py +9 -8
  79. brg_certificate/tests/datapath/output_power_test/output_power_test.json → certificate/tests/datapath/rx_channel_hopping_test/rx_channel_hopping_test.json +6 -4
  80. certificate/tests/datapath/rx_channel_hopping_test/rx_channel_hopping_test.py +77 -0
  81. {brg_certificate → certificate}/tests/datapath/rx_channel_test/rx_channel_test.json +3 -2
  82. {brg_certificate → certificate}/tests/datapath/rx_channel_test/rx_channel_test.py +7 -6
  83. {brg_certificate → certificate}/tests/datapath/rx_rate_gen2_test/rx_rate_gen2_test.json +8 -7
  84. {brg_certificate → certificate}/tests/datapath/rx_rate_gen2_test/rx_rate_gen2_test.py +113 -73
  85. {brg_certificate → certificate}/tests/datapath/rx_rate_gen3_test/rx_rate_gen3_test.json +8 -7
  86. {brg_certificate → certificate}/tests/datapath/rx_rate_gen3_test/rx_rate_gen3_test.py +112 -72
  87. {brg_certificate → certificate}/tests/datapath/stress_gen3_test/stress_gen3_test.json +4 -3
  88. {brg_certificate → certificate}/tests/datapath/stress_gen3_test/stress_gen3_test.py +15 -11
  89. {brg_certificate → certificate}/tests/datapath/stress_test/stress_test.json +4 -3
  90. {brg_certificate → certificate}/tests/datapath/stress_test/stress_test.py +15 -11
  91. {brg_certificate → certificate}/tests/datapath/tx_repetition_test/tx_repetition_test.json +3 -1
  92. {brg_certificate → certificate}/tests/datapath/tx_repetition_test/tx_repetition_test.py +14 -13
  93. certificate/tests/edge_mgmt/action_blink_test/action_blink_test.json +15 -0
  94. certificate/tests/edge_mgmt/action_blink_test/action_blink_test.py +24 -0
  95. certificate/tests/edge_mgmt/action_get_battery_sensor_test/action_get_battery_sensor_test.json +15 -0
  96. certificate/tests/edge_mgmt/action_get_battery_sensor_test/action_get_battery_sensor_test.py +43 -0
  97. certificate/tests/edge_mgmt/action_get_module_test/action_get_module_test.json +15 -0
  98. certificate/tests/edge_mgmt/action_get_module_test/action_get_module_test.py +42 -0
  99. certificate/tests/edge_mgmt/action_get_pof_data_test/action_get_pof_data_test.json +15 -0
  100. certificate/tests/edge_mgmt/action_get_pof_data_test/action_get_pof_data_test.py +44 -0
  101. certificate/tests/edge_mgmt/action_gw_hb_test/action_gw_hb_test.json +16 -0
  102. certificate/tests/edge_mgmt/action_gw_hb_test/action_gw_hb_test.py +42 -0
  103. certificate/tests/edge_mgmt/action_reboot_test/action_reboot_test.json +15 -0
  104. certificate/tests/edge_mgmt/action_reboot_test/action_reboot_test.py +49 -0
  105. certificate/tests/edge_mgmt/action_restore_defaults_test/action_restore_defaults_test.json +15 -0
  106. certificate/tests/edge_mgmt/action_restore_defaults_test/action_restore_defaults_test.py +102 -0
  107. certificate/tests/edge_mgmt/action_send_hb_test/action_send_hb_test.json +15 -0
  108. certificate/tests/edge_mgmt/action_send_hb_test/action_send_hb_test.py +45 -0
  109. {brg_certificate → certificate}/tests/edge_mgmt/periodic_msgs_test/periodic_msgs_test.json +3 -2
  110. {brg_certificate → certificate}/tests/edge_mgmt/periodic_msgs_test/periodic_msgs_test.py +22 -11
  111. {brg_certificate → certificate}/tests/energy2400/duty_cycle_test/duty_cycle_test.json +2 -1
  112. {brg_certificate → certificate}/tests/energy2400/duty_cycle_test/duty_cycle_test.py +7 -6
  113. certificate/tests/energy2400/output_power_test/output_power_test.json +23 -0
  114. {brg_certificate → certificate}/tests/energy2400/output_power_test/output_power_test.py +17 -6
  115. {brg_certificate → certificate}/tests/energy2400/pattern_test/pattern_test.json +2 -1
  116. {brg_certificate → certificate}/tests/energy2400/pattern_test/pattern_test.py +7 -6
  117. certificate/tests/energy2400/signal_indicator_ble5_test/signal_indicator_ble5_test.json +26 -0
  118. certificate/tests/energy2400/signal_indicator_ble5_test/signal_indicator_ble5_test.py +379 -0
  119. certificate/tests/energy2400/signal_indicator_ext_adv_test/signal_indicator_ext_adv_test.json +20 -0
  120. certificate/tests/energy2400/signal_indicator_ext_adv_test/signal_indicator_ext_adv_test.py +173 -0
  121. certificate/tests/energy2400/signal_indicator_test/signal_indicator_test.json +24 -0
  122. certificate/tests/energy2400/signal_indicator_test/signal_indicator_test.py +350 -0
  123. {brg_certificate → certificate}/tests/energy_sub1g/duty_cycle_test/duty_cycle_test.json +2 -1
  124. {brg_certificate → certificate}/tests/energy_sub1g/duty_cycle_test/duty_cycle_test.py +7 -6
  125. {brg_certificate → certificate}/tests/energy_sub1g/pattern_test/pattern_test.json +2 -1
  126. {brg_certificate → certificate}/tests/energy_sub1g/pattern_test/pattern_test.py +7 -6
  127. {brg_certificate → certificate}/tests/pwr_mgmt/pwr_mgmt_test/pwr_mgmt_test.json +2 -1
  128. {brg_certificate → certificate}/tests/pwr_mgmt/pwr_mgmt_test/pwr_mgmt_test.py +10 -10
  129. {brg_certificate → certificate}/tests/sensors/ext_sensor_test/ext_sensor_test.json +5 -4
  130. certificate/tests/sensors/ext_sensor_test/ext_sensor_test.py +450 -0
  131. certificate/wlt_types.py +122 -0
  132. {gw_certificate → common}/api_if/202/status.json +6 -0
  133. {gw_certificate → common}/api_if/203/status.json +6 -0
  134. {gw_certificate → common}/api_if/204/status.json +6 -0
  135. common/api_if/206/data.json +85 -0
  136. common/api_if/206/status.json +69 -0
  137. common/api_if/api_validation.py +91 -0
  138. common/web/templates/generator.html +210 -0
  139. common/web/templates/index.html +20 -0
  140. common/web/templates/menu.html +54 -0
  141. common/web/templates/parser.html +53 -0
  142. {brg_certificate/ag → common/web/templates}/wlt_types.html +1216 -191
  143. common/web/web_utils.py +399 -0
  144. {brg_certificate → common}/wltPb_pb2.py +14 -12
  145. {gw_certificate/common → common}/wltPb_pb2.pyi +16 -2
  146. gui_certificate/gui_certificate_cli.py +14 -0
  147. gui_certificate/server.py +1267 -0
  148. gui_certificate/templates/cert_run.html +1273 -0
  149. wiliot_certificate-4.5.0.dist-info/METADATA +99 -0
  150. wiliot_certificate-4.5.0.dist-info/RECORD +168 -0
  151. {wiliot_certificate-4.4.2.dist-info → wiliot_certificate-4.5.0.dist-info}/WHEEL +1 -1
  152. wiliot_certificate-4.5.0.dist-info/entry_points.txt +5 -0
  153. wiliot_certificate-4.5.0.dist-info/top_level.txt +3 -0
  154. brg_certificate/ag/energous_v0_defines.py +0 -925
  155. brg_certificate/ag/energous_v1_defines.py +0 -931
  156. brg_certificate/ag/energous_v2_defines.py +0 -925
  157. brg_certificate/ag/energous_v3_defines.py +0 -925
  158. brg_certificate/ag/energous_v4_defines.py +0 -925
  159. brg_certificate/ag/fanstel_lan_v0_defines.py +0 -925
  160. brg_certificate/ag/fanstel_lte_v0_defines.py +0 -925
  161. brg_certificate/ag/fanstel_wifi_v0_defines.py +0 -925
  162. brg_certificate/ag/minew_lte_v0_defines.py +0 -925
  163. brg_certificate/ag/wlt_types_ag_jsons/brg2brg_ota.json +0 -142
  164. brg_certificate/ag/wlt_types_ag_jsons/brg2gw_hb.json +0 -785
  165. brg_certificate/ag/wlt_types_ag_jsons/brg2gw_hb_sleep.json +0 -139
  166. brg_certificate/ag/wlt_types_ag_jsons/calibration.json +0 -394
  167. brg_certificate/ag/wlt_types_ag_jsons/custom.json +0 -515
  168. brg_certificate/ag/wlt_types_ag_jsons/datapath.json +0 -672
  169. brg_certificate/ag/wlt_types_ag_jsons/energy2400.json +0 -550
  170. brg_certificate/ag/wlt_types_ag_jsons/energySub1g.json +0 -595
  171. brg_certificate/ag/wlt_types_ag_jsons/externalSensor.json +0 -598
  172. brg_certificate/ag/wlt_types_ag_jsons/interface.json +0 -938
  173. brg_certificate/ag/wlt_types_ag_jsons/powerManagement.json +0 -1234
  174. brg_certificate/ag/wlt_types_ag_jsons/side_info_sensor.json +0 -105
  175. brg_certificate/ag/wlt_types_ag_jsons/signal_indicator_data.json +0 -77
  176. brg_certificate/ag/wlt_types_ag_jsons/unified_echo_ext_pkt.json +0 -61
  177. brg_certificate/ag/wlt_types_ag_jsons/unified_echo_pkt.json +0 -110
  178. brg_certificate/brg_certificate.py +0 -225
  179. brg_certificate/brg_certificate_cli.py +0 -63
  180. brg_certificate/cert_common.py +0 -923
  181. brg_certificate/cert_config.py +0 -402
  182. brg_certificate/cert_utils.py +0 -362
  183. brg_certificate/certificate_bcc_sanity_test_list.txt +0 -40
  184. brg_certificate/certificate_bcc_test_list.txt +0 -48
  185. brg_certificate/certificate_sanity_test_list.txt +0 -43
  186. brg_certificate/certificate_test_list.txt +0 -53
  187. brg_certificate/config/eclipse.json +0 -10
  188. brg_certificate/config/hivemq.json +0 -10
  189. brg_certificate/config/mosquitto.json +0 -10
  190. brg_certificate/config/mosquitto.md +0 -95
  191. brg_certificate/config/wiliot-dev.json +0 -10
  192. brg_certificate/restore_brg.py +0 -61
  193. brg_certificate/tests/calibration/output_power_test/output_power_test.json +0 -16
  194. brg_certificate/tests/calibration/output_power_test/output_power_test.py +0 -28
  195. brg_certificate/tests/datapath/aging_test/aging_test.py +0 -143
  196. brg_certificate/tests/datapath/pacer_interval_tags_count_test/pacer_interval_tags_count_test.json +0 -16
  197. brg_certificate/tests/datapath/pacer_interval_tags_count_test/pacer_interval_tags_count_test.py +0 -73
  198. brg_certificate/tests/datapath/tx_repetition_algo_test/tx_repetition_algo_test.json +0 -17
  199. brg_certificate/tests/datapath/tx_repetition_algo_test/tx_repetition_algo_test.py +0 -118
  200. brg_certificate/tests/edge_mgmt/actions_test/actions_test.json +0 -14
  201. brg_certificate/tests/edge_mgmt/actions_test/actions_test.py +0 -396
  202. brg_certificate/tests/edge_mgmt/brg2brg_ota_ble5_test/brg2brg_ota_ble5_test.json +0 -20
  203. brg_certificate/tests/edge_mgmt/brg2brg_ota_ble5_test/brg2brg_ota_ble5_test.py +0 -94
  204. brg_certificate/tests/edge_mgmt/brg2brg_ota_test/brg2brg_ota_test.json +0 -19
  205. brg_certificate/tests/edge_mgmt/brg2brg_ota_test/brg2brg_ota_test.py +0 -87
  206. brg_certificate/tests/edge_mgmt/leds_test/leds_test.json +0 -17
  207. brg_certificate/tests/edge_mgmt/leds_test/leds_test.py +0 -223
  208. brg_certificate/tests/edge_mgmt/ota_test/ota_test.json +0 -17
  209. brg_certificate/tests/edge_mgmt/ota_test/ota_test.py +0 -128
  210. brg_certificate/tests/energy2400/output_power_test/output_power_test.json +0 -16
  211. brg_certificate/tests/energy2400/signal_indicator_ble5_10_250k_test/signal_indicator_ble5_10_250k_test.json +0 -20
  212. brg_certificate/tests/energy2400/signal_indicator_ble5_10_250k_test/signal_indicator_ble5_10_250k_test.py +0 -321
  213. brg_certificate/tests/energy2400/signal_indicator_ble5_10_500k_test/signal_indicator_ble5_10_500k_test.json +0 -20
  214. brg_certificate/tests/energy2400/signal_indicator_ble5_10_500k_test/signal_indicator_ble5_10_500k_test.py +0 -321
  215. brg_certificate/tests/energy2400/signal_indicator_sub1g_2_4_test/signal_indicator_sub1g_2_4_test.json +0 -20
  216. brg_certificate/tests/energy2400/signal_indicator_sub1g_2_4_test/signal_indicator_sub1g_2_4_test.py +0 -141
  217. brg_certificate/tests/energy2400/signal_indicator_test/signal_indicator_test.json +0 -20
  218. brg_certificate/tests/energy2400/signal_indicator_test/signal_indicator_test.py +0 -276
  219. brg_certificate/tests/energy_sub1g/signal_indicator_functionality_test/signal_indicator_functionality_test.json +0 -20
  220. brg_certificate/tests/energy_sub1g/signal_indicator_functionality_test/signal_indicator_functionality_test.py +0 -390
  221. brg_certificate/tests/energy_sub1g/signal_indicator_test/signal_indicator_test.json +0 -16
  222. brg_certificate/tests/energy_sub1g/signal_indicator_test/signal_indicator_test.py +0 -28
  223. brg_certificate/tests/sensors/ext_sensor_test/ext_sensor_test.py +0 -305
  224. brg_certificate/wltPb_pb2.pyi +0 -234
  225. brg_certificate/wlt_types.py +0 -113
  226. gw_certificate/ag/ut_defines.py +0 -364
  227. gw_certificate/ag/wlt_types.py +0 -85
  228. gw_certificate/ag/wlt_types_ag.py +0 -5310
  229. gw_certificate/ag/wlt_types_data.py +0 -64
  230. gw_certificate/api/extended_api.py +0 -23
  231. gw_certificate/api_if/200/data.json +0 -106
  232. gw_certificate/api_if/200/status.json +0 -47
  233. gw_certificate/api_if/201/data.json +0 -98
  234. gw_certificate/api_if/201/status.json +0 -53
  235. gw_certificate/api_if/205/logs.json +0 -12
  236. gw_certificate/api_if/api_validation.py +0 -38
  237. gw_certificate/api_if/gw_capabilities.py +0 -54
  238. gw_certificate/cert_results.py +0 -145
  239. gw_certificate/common/analysis_data_bricks.py +0 -60
  240. gw_certificate/common/debug.py +0 -42
  241. gw_certificate/common/serialization_formatter.py +0 -93
  242. gw_certificate/common/utils.py +0 -8
  243. gw_certificate/common/utils_defines.py +0 -15
  244. gw_certificate/common/wltPb_pb2.py +0 -84
  245. gw_certificate/gw_certificate.py +0 -154
  246. gw_certificate/gw_certificate_cli.py +0 -87
  247. gw_certificate/interface/4.4.91_app.zip +0 -0
  248. gw_certificate/interface/4.4.91_sd_bl_app.zip +0 -0
  249. gw_certificate/interface/ble_simulator.py +0 -61
  250. gw_certificate/interface/ble_sniffer.py +0 -189
  251. gw_certificate/interface/flash_fw.py +0 -90
  252. gw_certificate/interface/if_defines.py +0 -36
  253. gw_certificate/interface/mqtt.py +0 -563
  254. gw_certificate/interface/nrfutil-linux +0 -0
  255. gw_certificate/interface/nrfutil-mac +0 -0
  256. gw_certificate/interface/nrfutil.exe +0 -0
  257. gw_certificate/interface/pkt_generator.py +0 -594
  258. gw_certificate/interface/uart_if.py +0 -236
  259. gw_certificate/interface/uart_ports.py +0 -20
  260. gw_certificate/templates/results.html +0 -241
  261. gw_certificate/templates/stage.html +0 -22
  262. gw_certificate/templates/table.html +0 -6
  263. gw_certificate/templates/test.html +0 -38
  264. gw_certificate/tests/__init__.py +0 -10
  265. gw_certificate/tests/actions.py +0 -289
  266. gw_certificate/tests/bad_crc_to_PER_quantization.csv +0 -51
  267. gw_certificate/tests/connection.py +0 -188
  268. gw_certificate/tests/downlink.py +0 -172
  269. gw_certificate/tests/generic.py +0 -238
  270. gw_certificate/tests/registration.py +0 -340
  271. gw_certificate/tests/static/__init__.py +0 -0
  272. gw_certificate/tests/static/connection_defines.py +0 -9
  273. gw_certificate/tests/static/downlink_defines.py +0 -9
  274. gw_certificate/tests/static/generated_packet_table.py +0 -195
  275. gw_certificate/tests/static/packet_table.csv +0 -10067
  276. gw_certificate/tests/static/references.py +0 -5
  277. gw_certificate/tests/static/uplink_defines.py +0 -14
  278. gw_certificate/tests/throughput.py +0 -240
  279. gw_certificate/tests/uplink.py +0 -853
  280. wiliot_certificate-4.4.2.dist-info/METADATA +0 -211
  281. wiliot_certificate-4.4.2.dist-info/RECORD +0 -210
  282. wiliot_certificate-4.4.2.dist-info/entry_points.txt +0 -3
  283. wiliot_certificate-4.4.2.dist-info/top_level.txt +0 -3
  284. {brg_certificate → certificate}/__init__.py +0 -0
  285. {gw_certificate → common}/api_if/202/data.json +0 -0
  286. {gw_certificate/api_if/200 → common/api_if/202}/logs.json +0 -0
  287. {gw_certificate → common}/api_if/203/data.json +0 -0
  288. {gw_certificate/api_if/201 → common/api_if/203}/logs.json +0 -0
  289. {gw_certificate → common}/api_if/204/data.json +0 -0
  290. {gw_certificate/api_if/202 → common/api_if/204}/logs.json +0 -0
  291. {gw_certificate → common}/api_if/205/data.json +0 -0
  292. {gw_certificate/api_if/203 → common/api_if/205}/logs.json +0 -0
  293. {gw_certificate → common}/api_if/205/status.json +0 -0
  294. {gw_certificate/api_if/204 → common/api_if/206}/logs.json +0 -0
  295. {gw_certificate → common/api_if}/__init__.py +0 -0
  296. {gw_certificate/api_if → gui_certificate}/__init__.py +0 -0
  297. {wiliot_certificate-4.4.2.dist-info → wiliot_certificate-4.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1267 @@
1
+ import os
2
+ import json
3
+ import tempfile
4
+ import datetime
5
+ from flask import Flask, render_template, request, session, redirect, url_for, flash, send_file
6
+ from jinja2 import ChoiceLoader, PackageLoader
7
+ from werkzeug.utils import secure_filename
8
+ from certificate.certificate_cli import CertificateCLI
9
+ from certificate.cert_defines import *
10
+ import certificate.cert_utils as cert_utils
11
+ import common.web.web_utils as web_utils
12
+ import serial.tools.list_ports
13
+
14
+ # Certificate Run Defines
15
+ DATA_REAL_TAGS = "REAL"
16
+ DATA_SIMULATION = "SIM"
17
+ TEST_LIST_DEFAULT_FILE = "certificate_sanity_test_list.txt"
18
+ PREFERRED_TERMINAL = os.getenv("CLI_TERMINAL", "auto")
19
+ CERT_TESTS_ROOT = os.path.join(BASE_DIR, "tests")
20
+
21
+ # Module Names
22
+ MODULE_CLOUD_CONNECTIVITY = "cloud_connectivity"
23
+ MODULE_EDGE_MGMT = "edge_mgmt"
24
+
25
+ # Flavor constants
26
+ FLAVOR_GW_ONLY = "gw_only"
27
+ FLAVOR_BRIDGE_ONLY = "bridge_only"
28
+ FLAVOR_COMBO = "combo"
29
+
30
+ # Upload configuration - use system temp directory
31
+ TEMP_BASE = tempfile.gettempdir()
32
+ UPLOAD_FOLDER = os.path.join(TEMP_BASE, "wiliot_certificate_uploaded_schemas")
33
+ CONFIG_FOLDER = os.path.join(TEMP_BASE, "wiliot_certificate_saved_configs")
34
+ TEST_LIST_FOLDER = os.path.join(TEMP_BASE, "wiliot_certificate_test_lists")
35
+ CUSTOM_BROKER_FOLDER = os.path.join(TEMP_BASE, "wiliot_certificate_custom_brokers")
36
+ ALLOWED_EXTENSIONS = {'json'}
37
+ MAX_UPLOAD_SIZE = 16 * 1024 * 1024 # 16MB
38
+
39
+ # Default custom broker configuration (from hivemq.json)
40
+ DEFAULT_CUSTOM_BROKER = {
41
+ "port": 8883,
42
+ "brokerUrl": "mqtts://broker.hivemq.com",
43
+ "username": "",
44
+ "password": "",
45
+ "ownerId": "wiliot"
46
+ }
47
+
48
+ CUSTOM_BROKER_HELP = {
49
+ "ownerId": "Used to construct topics, e.g.: data-v2/<ownerId>/<gatewayId>"
50
+ }
51
+
52
+ app = Flask(__name__)
53
+ app.secret_key = os.urandom(24) # Required for sessions
54
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
55
+ app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
56
+
57
+ # Create folders if they don't exist
58
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
59
+ os.makedirs(CONFIG_FOLDER, exist_ok=True)
60
+ os.makedirs(TEST_LIST_FOLDER, exist_ok=True)
61
+ os.makedirs(CUSTOM_BROKER_FOLDER, exist_ok=True)
62
+
63
+ # extend Jinja search path to include shared dir
64
+ app.jinja_loader = ChoiceLoader([
65
+ app.jinja_loader,
66
+ PackageLoader("common.web", "templates"),
67
+ ])
68
+ # Update jinja_env globals
69
+ app.jinja_env.globals.update(
70
+ FDM=web_utils.FDM,
71
+ CERT_WEB=web_utils.CERT_WEB,
72
+ **web_utils.GLOBAL_DEFINES_TO_UPDATE
73
+ )
74
+
75
+ def allowed_file(filename):
76
+ """Check if file extension is allowed."""
77
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
78
+
79
+ def get_mandatory_modules_for_flavor(flavor):
80
+ """Get mandatory modules for a given flavor."""
81
+ if flavor == FLAVOR_GW_ONLY:
82
+ return [MODULE_CLOUD_CONNECTIVITY]
83
+ elif flavor == FLAVOR_BRIDGE_ONLY:
84
+ return [MODULE_EDGE_MGMT] # Interface module
85
+ elif flavor == FLAVOR_COMBO:
86
+ return [MODULE_CLOUD_CONNECTIVITY, MODULE_EDGE_MGMT]
87
+ return []
88
+
89
+ def get_mandatory_modules_by_schema(schema_data, flavor):
90
+ """Get modules that are mandatory by schema (modules declared in schema)."""
91
+ if not schema_data or "modules" not in schema_data:
92
+ return []
93
+
94
+ # Only apply to bridge flavors (bridge_only and combo)
95
+ if flavor not in (FLAVOR_BRIDGE_ONLY, FLAVOR_COMBO):
96
+ return []
97
+
98
+ schema_modules = schema_data.get("modules", {})
99
+ if not isinstance(schema_modules, dict):
100
+ return []
101
+
102
+ # Map schema module names to test module names
103
+ mandatory_by_schema = []
104
+ for schema_mod_name in schema_modules.keys():
105
+ # Map schema module name to test module name
106
+ test_mod_name = test_module_and_schema_module_to_other(schema_mod_name)
107
+ if test_mod_name not in mandatory_by_schema and test_mod_name != 'custom':
108
+ mandatory_by_schema.append(test_mod_name)
109
+
110
+ return mandatory_by_schema
111
+
112
+ # TODO: This is a temporary mapping to map test module names to schema module names - remove this
113
+ def test_module_and_schema_module_to_other(module_name):
114
+ """Map test module names and schema module names to one another."""
115
+ mapping = {
116
+ "calibration": "calibration",
117
+ "datapath": "datapath",
118
+ "energy2400": "energy2400",
119
+ "energy_sub1g": "energySub1g", # snake_case to camelCase
120
+ "pwr_mgmt": "powerManagement", # abbreviated to full camelCase
121
+ "sensors": "externalSensor", # different name
122
+ "custom": "custom"
123
+ }
124
+ for k, v in mapping.items():
125
+ if module_name == k:
126
+ return v
127
+ if module_name == v:
128
+ return k
129
+ return module_name # Return as-is if not in mapping
130
+
131
+ def filter_modules_by_flavor(tests_schema, flavor, schema_data=None):
132
+ """Filter modules and tests based on selected flavor."""
133
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor)
134
+ filtered_modules = []
135
+
136
+ # Get schema module order if available
137
+ schema_module_order = []
138
+ if schema_data and "modules" in schema_data:
139
+ schema_module_order = list(schema_data["modules"].keys())
140
+
141
+ for mod in tests_schema.get("modules", []):
142
+ mod_name = mod.get("name", "")
143
+ filtered_tests = []
144
+
145
+ for test in mod.get("tests", []):
146
+ test_meta = test.get("meta", {})
147
+ gw_only_test = test_meta.get("gwOnlyTest", 0)
148
+
149
+ # Filter tests based on flavor
150
+ if flavor == FLAVOR_GW_ONLY:
151
+ # For GW only, include tests marked as gwOnlyTest or from cloud_connectivity module
152
+ if gw_only_test == 1 or mod_name == MODULE_CLOUD_CONNECTIVITY:
153
+ filtered_tests.append(test)
154
+ elif flavor == FLAVOR_BRIDGE_ONLY:
155
+ # For Bridge only, exclude GW-only tests (but include all others)
156
+ if gw_only_test == 0:
157
+ filtered_tests.append(test)
158
+ elif flavor == FLAVOR_COMBO:
159
+ # Include all tests for combo
160
+ filtered_tests.append(test)
161
+
162
+ # Get modules mandatory by schema
163
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor) if schema_data else []
164
+ is_mandatory_by_schema = mod_name in mandatory_by_schema
165
+
166
+ # Only include module if it has tests or is mandatory (by flavor or schema)
167
+ # For mandatory modules, include even if no tests match (they'll be shown but empty)
168
+ if filtered_tests or mod_name in mandatory_modules or is_mandatory_by_schema:
169
+ mod_copy = mod.copy()
170
+ mod_copy["tests"] = filtered_tests
171
+ mod_copy["is_mandatory"] = mod_name in mandatory_modules
172
+ mod_copy["is_mandatory_by_schema"] = is_mandatory_by_schema
173
+ filtered_modules.append(mod_copy)
174
+
175
+ # Sort modules: cloud_connectivity first, then edge_mgmt, then by schema order
176
+ def module_sort_key(mod):
177
+ mod_name = mod.get("name", "")
178
+ # cloud_connectivity always first
179
+ if mod_name == MODULE_CLOUD_CONNECTIVITY:
180
+ return (0, "")
181
+ # edge_mgmt always first among bridge modules
182
+ if mod_name == MODULE_EDGE_MGMT:
183
+ return (1, "")
184
+ # Then modules in schema order
185
+ if schema_module_order:
186
+ try:
187
+ # Map test module name to schema module name
188
+ schema_mod_name = test_module_and_schema_module_to_other(mod_name)
189
+ idx = schema_module_order.index(schema_mod_name)
190
+ return (2, idx)
191
+ except ValueError:
192
+ pass
193
+ return (3, mod_name)
194
+
195
+ filtered_modules.sort(key=module_sort_key)
196
+
197
+ return filtered_modules
198
+
199
+ def get_mandatory_tests_for_modules(modules):
200
+ """Get list of mandatory test IDs from selected modules."""
201
+ mandatory_tests = []
202
+ for mod in modules:
203
+ for test in mod.get("tests", []):
204
+ if test.get("meta", {}).get("mandatory", 0) == 1:
205
+ mandatory_tests.append(test.get("id"))
206
+ return mandatory_tests
207
+
208
+ def _check_missing_defaults(properties, path_prefix=""):
209
+ """
210
+ Recursively check if all properties have default values.
211
+ Returns list of field paths missing defaults.
212
+ Properties with nested 'properties' (object types) don't need defaults themselves,
213
+ only their nested properties need defaults.
214
+ Array types with 'items' field also don't need defaults.
215
+ """
216
+ missing_defaults = []
217
+
218
+ if not isinstance(properties, dict):
219
+ return missing_defaults
220
+
221
+ for field_name, field_def in properties.items():
222
+ if not isinstance(field_def, dict):
223
+ continue
224
+
225
+ current_path = f"{path_prefix}.{field_name}" if path_prefix else field_name
226
+
227
+ # Check if this field has nested properties
228
+ has_nested_properties = "properties" in field_def and isinstance(field_def["properties"], dict)
229
+
230
+ # Check if this is an array type with items
231
+ is_array_with_items = field_def.get("type") == "array" and "items" in field_def
232
+
233
+ # Only check for default if this property doesn't have nested properties and isn't an array with items
234
+ # (Objects with nested properties and arrays with items don't need defaults themselves)
235
+ if not has_nested_properties and not is_array_with_items and "default" not in field_def:
236
+ missing_defaults.append(current_path)
237
+
238
+ # Recursively check nested properties if they exist
239
+ if has_nested_properties:
240
+ nested_missing = _check_missing_defaults(field_def["properties"], current_path)
241
+ missing_defaults.extend(nested_missing)
242
+
243
+ return missing_defaults
244
+
245
+ def validate_schema_file(filepath):
246
+ """Validate the uploaded schema file."""
247
+ try:
248
+ with open(filepath, "r", encoding="utf-8") as f:
249
+ data = json.load(f)
250
+
251
+ # Basic validation
252
+ if "properties" not in data and "modules" not in data:
253
+ return False, "Schema missing both 'properties' and 'modules' keys"
254
+
255
+ # Check for cloud_connectivity_flag or brg_flag
256
+ has_cloud = cert_utils.cloud_connectivity_flag(data)
257
+ has_brg = cert_utils.brg_flag(data)
258
+
259
+ if not (has_cloud or has_brg):
260
+ return False, "Schema must support either cloud connectivity or bridge device"
261
+
262
+ # Check for missing defaults in all fields
263
+ missing_defaults = []
264
+
265
+ # Check top-level properties (gateway properties)
266
+ if "properties" in data and isinstance(data["properties"], dict):
267
+ missing_defaults.extend(_check_missing_defaults(data["properties"], ""))
268
+
269
+ # Check module properties (bridge modules)
270
+ if "modules" in data and isinstance(data["modules"], dict):
271
+ for module_name, module_def in data["modules"].items():
272
+ if isinstance(module_def, dict) and "properties" in module_def:
273
+ module_missing = _check_missing_defaults(module_def["properties"], f"modules.{module_name}")
274
+ missing_defaults.extend(module_missing)
275
+
276
+ if missing_defaults:
277
+ # Format error message with clear list of missing fields
278
+ if len(missing_defaults) == 1:
279
+ return False, f"Field '{missing_defaults[0]}' is missing a 'default' value"
280
+ else:
281
+ missing_list = ", ".join(missing_defaults)
282
+ return False, f"The following {len(missing_defaults)} fields are missing 'default' values: {missing_list}"
283
+
284
+ return True, data
285
+ except json.JSONDecodeError as e:
286
+ return False, f"Invalid JSON: {e}"
287
+ except Exception as e:
288
+ return False, f"Error reading file: {e}"
289
+
290
+ def load_schema_data(schema_path):
291
+ """Load schema data from file path. Returns None if file doesn't exist or can't be read."""
292
+ if not schema_path:
293
+ return None
294
+ try:
295
+ schema_path = os.path.normpath(str(schema_path).strip().strip('"').strip("'"))
296
+ if os.path.exists(schema_path):
297
+ with open(schema_path, "r", encoding="utf-8") as f:
298
+ return json.load(f)
299
+ except Exception:
300
+ pass
301
+ return None
302
+
303
+ def verify_schema_matches_selection(schema_data, flavor, selected_modules, selected_tests):
304
+ """Verify that the validation schema matches the selected flavor/modules/tests."""
305
+ errors = []
306
+ warnings = []
307
+
308
+ has_cloud = cert_utils.cloud_connectivity_flag(schema_data)
309
+ has_brg = cert_utils.brg_flag(schema_data)
310
+
311
+ # Check flavor compatibility
312
+ if flavor == FLAVOR_GW_ONLY and not has_cloud:
313
+ errors.append("Schema does not support Gateway (cloud connectivity) but GW only flavor selected")
314
+ elif flavor == FLAVOR_BRIDGE_ONLY and not has_brg:
315
+ errors.append("Schema does not support Bridge but Bridge only flavor selected")
316
+ elif flavor == FLAVOR_COMBO:
317
+ if not has_cloud and not has_brg:
318
+ errors.append("Schema does not support Gateway or Bridge for Combo flavor")
319
+ elif not has_cloud:
320
+ warnings.append("Schema missing Gateway support for Combo flavor")
321
+ elif not has_brg:
322
+ warnings.append("Schema missing Bridge support for Combo flavor")
323
+
324
+ # Check module support (if schema has modules field)
325
+ # The schema modules dict contains the bridge modules that the device supports
326
+ # Note: cloud_connectivity is a gateway feature, not a bridge module, so it won't be in schema modules
327
+ # Note: edge_mgmt is not a configurable module in schema
328
+ # Only check bridge modules that are configurable in the schema
329
+ if has_brg and "modules" in schema_data:
330
+ schema_modules = schema_data.get("modules", {})
331
+ if isinstance(schema_modules, dict):
332
+ schema_module_names = set(schema_modules.keys())
333
+ # Only check bridge modules that are configurable (exclude edge_mgmt and cloud_connectivity)
334
+ bridge_modules_to_check = [m for m in selected_modules if m not in (MODULE_CLOUD_CONNECTIVITY, MODULE_EDGE_MGMT)]
335
+ for test_mod_name in bridge_modules_to_check:
336
+ # Map test module name to schema module name
337
+ schema_mod_name = test_module_and_schema_module_to_other(test_mod_name)
338
+ if schema_mod_name not in schema_module_names:
339
+ warnings.append(f"Module '{test_mod_name}' may not be supported according to the uploaded schema")
340
+
341
+ return len(errors) == 0, errors, warnings
342
+
343
+ def check_certification_status(flavor, selected_modules, selected_tests, tests_schema, schema_data=None, unsterile_run=False):
344
+ """Check if the selection qualifies for certification or is test-only."""
345
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor)
346
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor) if schema_data else []
347
+ all_mandatory_tests = []
348
+
349
+ # Get selected module names (handle both dict and string formats)
350
+ selected_module_names = [m.get("name") if isinstance(m, dict) else m for m in selected_modules]
351
+
352
+ # Get all mandatory tests from ALL selected modules (not just mandatory modules)
353
+ # When a module is selected, its mandatory tests become mandatory
354
+ for mod in tests_schema.get("modules", []):
355
+ mod_name = mod.get("name", "")
356
+ if mod_name in selected_module_names:
357
+ for test in mod.get("tests", []):
358
+ if test.get("meta", {}).get("mandatory", 0) == 1:
359
+ all_mandatory_tests.append(test.get("id"))
360
+
361
+ # Check if all mandatory modules are selected
362
+ missing_modules = [m for m in mandatory_modules if m not in selected_module_names]
363
+
364
+ # Check if all schema-mandatory modules are selected
365
+ missing_modules_by_schema = [m for m in mandatory_by_schema if m not in selected_module_names]
366
+
367
+ # Check if all mandatory tests (from selected modules) are selected
368
+ missing_tests = [t for t in all_mandatory_tests if t not in selected_tests]
369
+
370
+ # Check for "at least one additional module" requirement for Bridge Only and Combo
371
+ missing_additional_module = False
372
+ if flavor in (FLAVOR_BRIDGE_ONLY, FLAVOR_COMBO):
373
+ # Count non-mandatory selected modules (exclude cloud_connectivity and edge_mgmt)
374
+ additional_modules = [m for m in selected_module_names if m not in (MODULE_CLOUD_CONNECTIVITY, MODULE_EDGE_MGMT)]
375
+ if len(additional_modules) == 0:
376
+ missing_additional_module = True
377
+
378
+ # A certifying run must be sterile - if unsterile_run is set, force non-certifying
379
+ is_certified = (len(missing_modules) == 0 and len(missing_modules_by_schema) == 0 and
380
+ len(missing_tests) == 0 and not missing_additional_module and not unsterile_run)
381
+
382
+ return is_certified, missing_modules, missing_tests, missing_additional_module, missing_modules_by_schema
383
+
384
+ def _prepare_form_data_for_template(session, cert_schema):
385
+ """Prepare form_data for template, ensuring custom_broker_path is included if available."""
386
+ form_data = session.get('form_data', {}).copy()
387
+ custom_broker_path = session.get('custom_broker_path')
388
+ if custom_broker_path:
389
+ custom_broker_field_name = f"{cert_schema['title']}_custom_broker"
390
+ if custom_broker_field_name not in form_data:
391
+ form_data[custom_broker_field_name] = custom_broker_path
392
+ return form_data
393
+
394
+ def form_to_argv(form, parser, title) -> list[str]:
395
+ """
396
+ Map POSTed form fields back into argv tokens the parser expects.
397
+ - For flags (store_true/false): include the flag when checked.
398
+ - For text/select: include '--opt value' only if non-empty (keeps defaults otherwise).
399
+ - Special-case overwrite_defaults: accept textarea of key=value lines -> join to key=value,... string.
400
+ """
401
+ argv: list[str] = []
402
+ for a in parser._actions:
403
+ from argparse import _HelpAction, _VersionAction, _StoreTrueAction, _StoreFalseAction
404
+ if isinstance(a, (_HelpAction, _VersionAction)) or a.dest in ('tl', 'validation_schema'):
405
+ continue
406
+
407
+ dest = f'{title}_{a.dest}'
408
+ opt = a.option_strings and max(a.option_strings, key=len) # prefer long option
409
+ val = form.get(dest, "")
410
+
411
+ # Special handling: overwrite_defaults textarea to csv string
412
+ if "overwrite_defaults" in dest:
413
+ # allow either CSV or line-per-pair; normalize to CSV for parser type fn
414
+ lines = [ln.strip().replace(" ", "") for ln in form.get(dest, "").splitlines() if ln.strip()]
415
+ if lines:
416
+ val = ",".join(lines)
417
+
418
+ if getattr(a, "choices", None) and a.nargs in ("+", "*"):
419
+ vals = form.getlist(dest + "[]")
420
+ if vals:
421
+ argv += [opt] + vals
422
+ continue
423
+
424
+ # store_true / store_false
425
+ if isinstance(a, _StoreTrueAction):
426
+ if form.get(dest): # checkbox "on"
427
+ argv += [opt]
428
+ continue
429
+ if isinstance(a, _StoreFalseAction):
430
+ if form.get(dest):
431
+ argv += [opt]
432
+ continue
433
+
434
+ # Positionals (you have none) vs optionals (you have only optionals)
435
+ if a.option_strings:
436
+ if val not in ("", None):
437
+ argv += [opt, val]
438
+ else:
439
+ if val not in ("", None):
440
+ argv += [val]
441
+ return argv
442
+
443
+ def _sort_tests_by_module_priority(test_ids: list[str]) -> list[str]:
444
+ """
445
+ Sort test IDs to ensure edge_mgmt tests come first (after cloud_connectivity if present).
446
+ Test ID format: 'module/test_name'
447
+ """
448
+ def test_sort_key(test_id: str):
449
+ module, _, test_name = test_id.partition('/')
450
+ # Connection test first (requires manual unplug and replug)
451
+ if module == MODULE_CLOUD_CONNECTIVITY and "connection" in test_name.lower():
452
+ return (0, test_id)
453
+ # other cloud_connectivity tests
454
+ if module == MODULE_CLOUD_CONNECTIVITY:
455
+ return (1, test_id)
456
+ # edge_mgmt second
457
+ if module == MODULE_EDGE_MGMT:
458
+ return (2, test_id)
459
+ # Others last
460
+ return (3, test_id)
461
+
462
+ return sorted(test_ids, key=test_sort_key)
463
+
464
+ def _write_testlist(selected_ids: list[str], form_data=None, is_certified=False) -> str:
465
+ """
466
+ Create a temp test list file in the temp directory. If your CLI expects script paths, write id_to_py[tid].
467
+ If it expects logical names, write 'module/test'. Adjust once here.
468
+ """
469
+ # Sort tests to ensure edge_mgmt comes first
470
+ sorted_test_ids = _sort_tests_by_module_priority(selected_ids)
471
+
472
+ test_list = f"custom_tl_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.txt"
473
+ path = os.path.join(TEST_LIST_FOLDER, test_list)
474
+ with open(path, 'w') as f:
475
+ # Add comment about certification status
476
+ if not is_certified:
477
+ f.write("# NON-CERTIFYING RUN (mandatory tests/modules missing)\n")
478
+ for test_id in sorted_test_ids:
479
+ tid_safe = test_id.replace('/', '_')
480
+ # Check for dynamic parameter first (non-array format)
481
+ dynamic_param_key = f"params_{tid_safe}_dynamic"
482
+ dynamic_param = form_data.get(dynamic_param_key) if form_data else None
483
+
484
+ if dynamic_param is not None and dynamic_param != "":
485
+ # Use dynamic parameter value
486
+ params = [str(dynamic_param)]
487
+ else:
488
+ # Use regular parameter array
489
+ param_key = f"params_{tid_safe}[]"
490
+ params = form_data.getlist(param_key) if form_data else [] # list[str]
491
+ # Example line format: "<module/test> <param1> <param2> ..."
492
+ line = " ".join([test_id] + params)
493
+ f.write(line + "\n")
494
+ print(f"Custom test list saved in {path}")
495
+ return path
496
+
497
+ def _write_custom_broker_config(form_data, cert_schema_title="cert_run") -> str:
498
+ """
499
+ Generate a custom broker JSON configuration file from form data.
500
+ Extracts broker field values and creates a JSON file matching hivemq.json format.
501
+ """
502
+ # Extract broker configuration from form_data
503
+ broker_config = {}
504
+
505
+ # Field names follow pattern: {schema_title}_custom_broker_{field_name}
506
+ field_prefix = f"{cert_schema_title}_custom_broker_"
507
+
508
+ # Extract values from form_data, using defaults if not provided
509
+ for field in DEFAULT_CUSTOM_BROKER.keys():
510
+ field_name = f"{field_prefix}{field}"
511
+ value = form_data.get(field_name, "")
512
+
513
+ if field == "port":
514
+ # Convert port to int if it's a number
515
+ try:
516
+ broker_config[field] = int(value) if value else DEFAULT_CUSTOM_BROKER[field]
517
+ except (ValueError, TypeError):
518
+ broker_config[field] = DEFAULT_CUSTOM_BROKER[field]
519
+ elif field == "ownerId":
520
+ # Convert ownerId to topics
521
+ broker_config[CUSTOM_BROKER_UPDATE_TOPIC] = f'update/{value}/<{GW_ID}>'
522
+ broker_config[CUSTOM_BROKER_DATA_TOPIC] = f'data/{value}/<{GW_ID}>'
523
+ broker_config[CUSTOM_BROKER_STATUS_TOPIC] = f'status/{value}/<{GW_ID}>'
524
+ else:
525
+ broker_config[field] = value
526
+
527
+ # Generate filename with timestamp
528
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
529
+ filename = f"custom_broker_{timestamp}.json"
530
+ filepath = os.path.join(CUSTOM_BROKER_FOLDER, filename)
531
+
532
+ # Write JSON file
533
+ with open(filepath, 'w', encoding='utf-8') as f:
534
+ json.dump(broker_config, f, indent=4)
535
+
536
+ print(f"Custom broker config saved in {filepath}")
537
+ return filepath
538
+
539
+ @app.route("/")
540
+ def index():
541
+ """Redirect to step 1 or show current step."""
542
+ if 'current_step' not in session:
543
+ session['current_step'] = 1
544
+ session['flavor'] = None
545
+ session['schema_path'] = None
546
+ session['selected_modules'] = []
547
+ session['selected_tests'] = []
548
+ session['form_data'] = {}
549
+ return redirect(url_for('step', step_num=session.get('current_step', 1)))
550
+
551
+ @app.route("/step/<int:step_num>", methods=['GET', 'POST'])
552
+ def step(step_num):
553
+ """Handle multi-step wizard navigation."""
554
+ if step_num < 1 or step_num > 5:
555
+ flash("Invalid step number", "error")
556
+ return redirect(url_for('index'))
557
+
558
+ # Initialize session if needed
559
+ if 'current_step' not in session:
560
+ session['current_step'] = 1
561
+ session['flavor'] = None
562
+ session['schema_path'] = None
563
+ session['selected_modules'] = []
564
+ session['selected_tests'] = []
565
+ session['form_data'] = {}
566
+ session['schema_mandatory_initialized'] = False
567
+
568
+ title = "Run Certificate"
569
+ tests_schema = web_utils.scan_tests_dir(CERT_TESTS_ROOT)
570
+ error = None
571
+ warning = None
572
+
573
+ if request.method == "POST":
574
+ action = request.form.get('action', 'next')
575
+
576
+ if action == 'back':
577
+ # For GW only, skip step 3 when going back from step 4
578
+ if step_num == 4 and session.get('flavor') == FLAVOR_GW_ONLY:
579
+ session['current_step'] = 2
580
+ else:
581
+ session['current_step'] = max(1, step_num - 1)
582
+ return redirect(url_for('step', step_num=session['current_step']))
583
+
584
+ # Step 1: Flavor selection
585
+ if step_num == 1:
586
+ flavor = request.form.get('flavor')
587
+ if flavor in [FLAVOR_GW_ONLY, FLAVOR_BRIDGE_ONLY, FLAVOR_COMBO]:
588
+ session['flavor'] = flavor
589
+ # Initialize mandatory modules only if not already set (preserves user selections)
590
+ if 'selected_modules' not in session or not session.get('selected_modules'):
591
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor)
592
+ session['selected_modules'] = mandatory_modules
593
+ session['current_step'] = 2
594
+ return redirect(url_for('step', step_num=2))
595
+ else:
596
+ error = "Please select a flavor"
597
+
598
+ # Step 2: Validation schema upload
599
+ elif step_num == 2:
600
+ # Only validate file if moving forward (not going back)
601
+ if action == 'next':
602
+ if 'schema_file' in request.files:
603
+ file = request.files['schema_file']
604
+ if file and file.filename and allowed_file(file.filename):
605
+ filename = secure_filename(file.filename)
606
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
607
+ filename = f"{timestamp}_{filename}"
608
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
609
+ file.save(filepath)
610
+
611
+ is_valid, result = validate_schema_file(filepath)
612
+ if is_valid:
613
+ # Store normalized path without quotes (don't store full schema_data to avoid cookie size issues)
614
+ normalized_path = os.path.normpath(filepath)
615
+ session['schema_path'] = normalized_path
616
+ # Don't store schema_data in session - load from file when needed
617
+ # Initialize mandatory modules only if not already set (preserves user selections)
618
+ flavor = session.get('flavor')
619
+ if 'selected_modules' not in session or not session.get('selected_modules'):
620
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor)
621
+ # Add schema-mandatory modules
622
+ schema_data = result # result is the validated schema data
623
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor)
624
+ all_mandatory = list(set(mandatory_modules + mandatory_by_schema))
625
+ session['selected_modules'] = all_mandatory
626
+ # Mark schema-mandatory modules as initialized
627
+ session['schema_mandatory_initialized'] = True
628
+ else:
629
+ # If modules already exist, add schema-mandatory modules and mark as initialized
630
+ schema_data = result
631
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor)
632
+ current_modules = session.get('selected_modules', [])
633
+ updated_modules = list(set(current_modules + mandatory_by_schema))
634
+ if len(updated_modules) > len(current_modules):
635
+ session['selected_modules'] = updated_modules
636
+ session['schema_mandatory_initialized'] = True
637
+ # For GW only, skip step 3 (modules)
638
+ if session.get('flavor') == FLAVOR_GW_ONLY:
639
+ session['current_step'] = 4
640
+ return redirect(url_for('step', step_num=4))
641
+ else:
642
+ session['current_step'] = 3
643
+ return redirect(url_for('step', step_num=3))
644
+ else:
645
+ error = f"Schema validation failed: {result}"
646
+ os.remove(filepath)
647
+ else:
648
+ error = "Please upload a valid JSON schema file"
649
+ else:
650
+ error = "Please select a validation schema file"
651
+
652
+ # Step 3: Module selection
653
+ elif step_num == 3:
654
+ # Skip step 3 for GW only (shouldn't reach here, but handle gracefully)
655
+ if session.get('flavor') == FLAVOR_GW_ONLY:
656
+ mandatory_modules = get_mandatory_modules_for_flavor(FLAVOR_GW_ONLY)
657
+ session['selected_modules'] = mandatory_modules
658
+ session['current_step'] = 4
659
+ return redirect(url_for('step', step_num=4))
660
+
661
+ selected_modules = request.form.getlist('modules[]')
662
+
663
+ # Get mandatory modules for this flavor
664
+ flavor = session.get('flavor')
665
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor) if flavor else []
666
+
667
+ # Use submitted modules directly (mandatory modules can now be deselected)
668
+ all_selected_modules = selected_modules
669
+
670
+ # Allow moving forward even if requirements aren't met (warnings shown via JavaScript)
671
+ if all_selected_modules:
672
+ session['selected_modules'] = all_selected_modules
673
+ session['current_step'] = 4
674
+ return redirect(url_for('step', step_num=4))
675
+
676
+ # Step 4: Test selection
677
+ elif step_num == 4:
678
+ selected_tests = request.form.getlist('tests[]')
679
+
680
+ # Get mandatory tests from selected modules
681
+ flavor = session.get('flavor')
682
+ selected_modules = session.get('selected_modules', [])
683
+ schema_path = session.get('schema_path')
684
+ schema_data = load_schema_data(schema_path) if schema_path else None
685
+ filtered_modules = filter_modules_by_flavor(tests_schema, flavor, schema_data) if flavor else []
686
+
687
+ # Find all mandatory tests from selected modules
688
+ mandatory_tests = []
689
+ for mod in filtered_modules:
690
+ if mod.get('name') in selected_modules:
691
+ for test in mod.get('tests', []):
692
+ if test.get('meta', {}).get('mandatory', 0) == 1:
693
+ mandatory_tests.append(test.get('id'))
694
+
695
+ # Use submitted tests directly (mandatory tests can now be deselected)
696
+ all_selected_tests = selected_tests
697
+
698
+ # Store all form data including CLI parameters and test parameters
699
+ form_data_dict = {}
700
+ for key in request.form:
701
+ if key.endswith('[]'):
702
+ form_data_dict[key] = request.form.getlist(key)
703
+ else:
704
+ form_data_dict[key] = request.form.get(key)
705
+
706
+ # Handle custom_broker configuration from form fields
707
+ cert_cli = CertificateCLI()
708
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
709
+ custom_broker_field_name = f"{cert_schema['title']}_custom_broker"
710
+
711
+ # Generate custom broker JSON file from form fields
712
+ try:
713
+ broker_config_path = _write_custom_broker_config(form_data_dict, cert_schema['title'])
714
+ normalized_path = os.path.normpath(broker_config_path)
715
+ session['custom_broker_path'] = normalized_path
716
+ form_data_dict[custom_broker_field_name] = normalized_path
717
+ except Exception as e:
718
+ error = f"Error generating custom broker configuration: {e}"
719
+
720
+ # Validate all required fields (required for moving forward, but allow going back)
721
+ if action == 'next':
722
+ # Get required fields from CLI schema
723
+ cert_cli = CertificateCLI()
724
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
725
+ required_fields = []
726
+ missing_required = []
727
+
728
+ # Find all required fields (excluding validation_schema and tl which are handled separately)
729
+ for field in cert_schema.get('fields', []):
730
+ if field.get('required') and field.get('name') not in ('validation_schema', 'tl'):
731
+ required_fields.append(field)
732
+
733
+ # Check each required field
734
+ for field in required_fields:
735
+ field_name = f"{cert_schema['title']}_{field['name']}"
736
+ field_value = form_data_dict.get(field_name, '')
737
+
738
+ # Special handling for custom_broker - check if all required broker fields are filled
739
+ if field['name'] == 'custom_broker':
740
+ # Check all broker fields except username and password are filled
741
+ broker_field_prefix = f"{cert_schema['title']}_custom_broker_"
742
+ optional_fields = {'username', 'password'}
743
+ for broker_field_key in DEFAULT_CUSTOM_BROKER.keys():
744
+ if broker_field_key not in optional_fields:
745
+ broker_field_name = f"{broker_field_prefix}{broker_field_key}"
746
+ broker_field_value = form_data_dict.get(broker_field_name, '')
747
+ if not broker_field_value or (isinstance(broker_field_value, str) and not broker_field_value.strip()):
748
+ missing_required.append(f"custom_broker.{broker_field_key}")
749
+ break
750
+ # Also check if path was generated (should be generated above if fields are valid)
751
+ if not field_value and not session.get('custom_broker_path'):
752
+ # Only add if we haven't already added a broker field error
753
+ if not any('custom_broker.' in req for req in missing_required):
754
+ missing_required.append(field.get('name', field['label']))
755
+ else:
756
+ # For other fields, check if value is provided
757
+ if not field_value or (isinstance(field_value, str) and not field_value.strip()):
758
+ missing_required.append(field.get('name', field['label']))
759
+
760
+ if missing_required:
761
+ if len(missing_required) == 1:
762
+ error = f"Please provide {missing_required[0]} to continue"
763
+ else:
764
+ error = f"Please provide the following required fields to continue: {', '.join(missing_required)}"
765
+ else:
766
+ # Only save and move forward if all required fields are provided
767
+ if all_selected_tests:
768
+ session['selected_tests'] = all_selected_tests
769
+ session['form_data'] = form_data_dict
770
+ session['current_step'] = 5
771
+ return redirect(url_for('step', step_num=5))
772
+ else:
773
+ # Going back - save form data but don't validate required fields
774
+ if all_selected_tests:
775
+ session['selected_tests'] = all_selected_tests
776
+ session['form_data'] = form_data_dict
777
+
778
+ # Step 5: Review and run
779
+ elif step_num == 5:
780
+ if request.form.get('action') == 'run':
781
+ return execute_certificate()
782
+ elif request.form.get('action') == 'edit':
783
+ edit_step = int(request.form.get('edit_step', 1))
784
+ session['current_step'] = edit_step
785
+ return redirect(url_for('step', step_num=edit_step))
786
+
787
+ # Prepare data for template
788
+ flavor = session.get('flavor')
789
+ schema_path = session.get('schema_path')
790
+ selected_modules = session.get('selected_modules', [])
791
+ selected_tests = session.get('selected_tests', [])
792
+
793
+ # Initialize mandatory items only if not already in session (new session or after "Start New Run")
794
+ # This ensures mandatory items are only pre-checked at the beginning
795
+ if flavor and not selected_modules:
796
+ mandatory_modules = get_mandatory_modules_for_flavor(flavor)
797
+ # Add schema-mandatory modules if schema is available
798
+ schema_data = load_schema_data(schema_path) if schema_path else None
799
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor) if schema_data else []
800
+ all_mandatory = list(set(mandatory_modules + mandatory_by_schema))
801
+ selected_modules = all_mandatory
802
+ session['selected_modules'] = selected_modules
803
+
804
+ # Ensure schema-mandatory modules are added when first reaching step 3 (only if not already initialized)
805
+ # Use a session flag to track if schema-mandatory modules have been initialized
806
+ if step_num == 3 and flavor and schema_path:
807
+ schema_mandatory_initialized = session.get('schema_mandatory_initialized', False)
808
+ if not schema_mandatory_initialized:
809
+ schema_data = load_schema_data(schema_path) if schema_path else None
810
+ if schema_data:
811
+ mandatory_by_schema = get_mandatory_modules_by_schema(schema_data, flavor)
812
+ # Add schema-mandatory modules that aren't already selected
813
+ current_modules = session.get('selected_modules', [])
814
+ updated_modules = list(set(current_modules + mandatory_by_schema))
815
+ if len(updated_modules) > len(current_modules):
816
+ selected_modules = updated_modules
817
+ session['selected_modules'] = selected_modules
818
+ # Mark as initialized so we don't re-add them when going back
819
+ session['schema_mandatory_initialized'] = True
820
+
821
+ # Initialize mandatory tests only if not already in session and we have selected modules
822
+ # Only initialize when first reaching step 4 (not on every GET request)
823
+ if flavor and selected_modules and not selected_tests and step_num == 4:
824
+ schema_path = session.get('schema_path')
825
+ schema_data = load_schema_data(schema_path) if schema_path else None
826
+ filtered_modules = filter_modules_by_flavor(tests_schema, flavor, schema_data) if flavor else []
827
+ mandatory_tests = []
828
+ for mod in filtered_modules:
829
+ if mod.get('name') in selected_modules:
830
+ for test in mod.get('tests', []):
831
+ if test.get('meta', {}).get('mandatory', 0) == 1:
832
+ mandatory_tests.append(test.get('id'))
833
+ if mandatory_tests:
834
+ selected_tests = mandatory_tests
835
+ session['selected_tests'] = selected_tests
836
+
837
+ # Skip step 3 for GW only - redirect to step 4 if trying to access step 3
838
+ if step_num == 3 and flavor == FLAVOR_GW_ONLY:
839
+ if not selected_modules:
840
+ mandatory_modules = get_mandatory_modules_for_flavor(FLAVOR_GW_ONLY)
841
+ session['selected_modules'] = mandatory_modules
842
+ return redirect(url_for('step', step_num=4))
843
+
844
+ # Filter modules/tests based on flavor
845
+ filtered_modules = []
846
+ if flavor:
847
+ schema_path = session.get('schema_path')
848
+ schema_data = load_schema_data(schema_path) if schema_path else None
849
+ filtered_modules = filter_modules_by_flavor(tests_schema, flavor, schema_data)
850
+
851
+ # Get CLI schemas for form fields - use certificate_cli for all flavors (needed early for unsterile_run check)
852
+ cert_cli = CertificateCLI()
853
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
854
+
855
+ # Check if unsterile_run is set in form_data
856
+ form_data = _prepare_form_data_for_template(session, cert_schema)
857
+ unsterile_run_field = f"{cert_schema['title']}_unsterile_run"
858
+ unsterile_run = bool(form_data.get(unsterile_run_field))
859
+
860
+ # Get certification status
861
+ is_certified = True
862
+ missing_modules = []
863
+ missing_tests = []
864
+ missing_tests_details = [] # List of dicts with test id and label
865
+ missing_additional_module = False
866
+ missing_modules_by_schema = []
867
+ if step_num >= 4 and flavor and selected_modules and selected_tests:
868
+ # Convert selected_modules to module dicts for check_certification_status
869
+ module_dicts = [m for m in filtered_modules if m.get('name') in selected_modules]
870
+ schema_data = load_schema_data(schema_path) if schema_path else None
871
+ is_certified, missing_modules, missing_tests, missing_additional_module, missing_modules_by_schema = check_certification_status(
872
+ flavor, module_dicts, selected_tests, tests_schema, schema_data, unsterile_run
873
+ )
874
+
875
+ # Get test details (labels) for missing tests
876
+ if missing_tests:
877
+ for mod in filtered_modules:
878
+ for test in mod.get('tests', []):
879
+ test_id = test.get('id')
880
+ if test_id in missing_tests:
881
+ missing_tests_details.append({
882
+ 'id': test_id,
883
+ 'label': test.get('label', test_id),
884
+ 'module': mod.get('name', '')
885
+ })
886
+
887
+ # Schema verification
888
+ schema_errors = []
889
+ schema_warnings = []
890
+ if step_num == 5 and schema_path and flavor:
891
+ schema_data = load_schema_data(schema_path)
892
+ if schema_data:
893
+ module_dicts = [m for m in filtered_modules if m.get('name') in selected_modules]
894
+ is_valid, errors, warnings = verify_schema_matches_selection(
895
+ schema_data, flavor, selected_modules, selected_tests
896
+ )
897
+ schema_errors = errors
898
+ schema_warnings = warnings
899
+ # Get missing schema-mandatory modules for display (recalculate if not already set)
900
+ if not missing_modules_by_schema and step_num >= 4 and flavor and selected_modules:
901
+ _, _, _, _, missing_modules_by_schema = check_certification_status(
902
+ flavor, module_dicts, selected_tests, tests_schema, schema_data, unsterile_run
903
+ )
904
+
905
+
906
+ # Calculate total test count including mandatory tests from selected modules
907
+ total_test_count = len(selected_tests)
908
+ if step_num >= 4 and flavor and selected_modules:
909
+ # Find all mandatory tests from selected modules that might not be in selected_tests
910
+ for mod in filtered_modules:
911
+ if mod.get('name') in selected_modules:
912
+ for test in mod.get('tests', []):
913
+ test_id = test.get('id')
914
+ if test.get('meta', {}).get('mandatory', 0) == 1 and test_id not in selected_tests:
915
+ total_test_count += 1
916
+
917
+ # Prepare data for JavaScript validation
918
+ # Always initialize these to avoid Undefined errors in template
919
+ mandatory_modules_for_js = get_mandatory_modules_for_flavor(flavor) if flavor else []
920
+ schema_data_for_js = load_schema_data(schema_path) if schema_path else None
921
+ mandatory_modules_by_schema_for_js = get_mandatory_modules_by_schema(schema_data_for_js, flavor) if (schema_data_for_js and flavor) else []
922
+ mandatory_tests_for_js = []
923
+
924
+ if step_num in [3, 4] and flavor:
925
+ if step_num == 4 and selected_modules:
926
+ # For step 4, only include mandatory tests from selected modules
927
+ for mod in filtered_modules:
928
+ if mod.get('name') in selected_modules:
929
+ for test in mod.get('tests', []):
930
+ if test.get('meta', {}).get('mandatory', 0) == 1:
931
+ mandatory_tests_for_js.append({
932
+ 'id': test.get('id'),
933
+ 'label': test.get('label', test.get('id')),
934
+ 'module': mod.get('name', '')
935
+ })
936
+ elif step_num == 3:
937
+ # For step 3, include all mandatory tests from all modules (for validation)
938
+ for mod in filtered_modules:
939
+ for test in mod.get('tests', []):
940
+ if test.get('meta', {}).get('mandatory', 0) == 1:
941
+ mandatory_tests_for_js.append({
942
+ 'id': test.get('id'),
943
+ 'label': test.get('label', test.get('id')),
944
+ 'module': mod.get('name', '')
945
+ })
946
+
947
+ # Get run completion info before clearing (for modal display)
948
+ run_completed = session.get('run_completed', False)
949
+ run_terminal = session.get('run_terminal')
950
+ run_pid = session.get('run_pid')
951
+ run_is_certified = session.get('run_is_certified')
952
+
953
+ # Clear run flags after getting them (so modal only shows once)
954
+ if run_completed:
955
+ session.pop('run_completed', None)
956
+ session.pop('run_terminal', None)
957
+ session.pop('run_pid', None)
958
+ session.pop('run_is_certified', None)
959
+
960
+ # Get available COM ports for port dropdown
961
+ available_ports = []
962
+ try:
963
+ for port, desc, hwid in sorted(serial.tools.list_ports.comports()):
964
+ available_ports.append({
965
+ 'value': port,
966
+ 'label': f"{port}: {desc} [{hwid}]"
967
+ })
968
+ except Exception:
969
+ # If serial tools are not available, leave empty list
970
+ pass
971
+
972
+ return render_template('cert_run.html',
973
+ app=web_utils.CERT_WEB,
974
+ title=title,
975
+ step_num=step_num,
976
+ flavor=flavor,
977
+ schema_path=schema_path,
978
+ selected_modules=selected_modules,
979
+ selected_tests=selected_tests,
980
+ total_test_count=total_test_count,
981
+ filtered_modules=filtered_modules,
982
+ tests_schema=tests_schema,
983
+ cert_schema=cert_schema,
984
+ is_certified=is_certified,
985
+ missing_modules=missing_modules,
986
+ missing_tests=missing_tests,
987
+ missing_tests_details=missing_tests_details,
988
+ missing_additional_module=missing_additional_module,
989
+ schema_errors=schema_errors,
990
+ schema_warnings=schema_warnings,
991
+ missing_modules_by_schema=missing_modules_by_schema,
992
+ mandatory_modules_for_js=mandatory_modules_for_js,
993
+ mandatory_modules_by_schema_for_js=mandatory_modules_by_schema_for_js,
994
+ mandatory_tests_for_js=mandatory_tests_for_js,
995
+ error=error,
996
+ warning=warning,
997
+ form_data=_prepare_form_data_for_template(session, cert_schema),
998
+ custom_broker_defaults=DEFAULT_CUSTOM_BROKER,
999
+ custom_broker_help=CUSTOM_BROKER_HELP,
1000
+ custom_broker_field_keys=list(DEFAULT_CUSTOM_BROKER.keys()),
1001
+ run_completed=run_completed,
1002
+ run_terminal=run_terminal,
1003
+ run_pid=run_pid,
1004
+ run_is_certified=run_is_certified,
1005
+ available_ports=available_ports,
1006
+ unsterile_run=unsterile_run)
1007
+
1008
+ def execute_certificate():
1009
+ """Execute the certificate run based on session data."""
1010
+ flavor = session.get('flavor')
1011
+ schema_path = session.get('schema_path')
1012
+ selected_tests = session.get('selected_tests', [])
1013
+ form_data = session.get('form_data', {})
1014
+
1015
+ if not flavor or not schema_path or not selected_tests:
1016
+ flash("Missing required information. Please start over.", "error")
1017
+ return redirect(url_for('step', step_num=1))
1018
+
1019
+ # Strip any quotes that might have been added and normalize path
1020
+ schema_path = str(schema_path).strip().strip('"').strip("'")
1021
+ schema_path = os.path.normpath(schema_path)
1022
+
1023
+ # Verify schema file exists
1024
+ if not os.path.exists(schema_path):
1025
+ flash(f"Validation schema file not found: {schema_path}", "error")
1026
+ return redirect(url_for('step', step_num=2))
1027
+
1028
+ # Check certification status
1029
+ tests_schema = web_utils.scan_tests_dir(CERT_TESTS_ROOT)
1030
+ filtered_modules = filter_modules_by_flavor(tests_schema, flavor)
1031
+ module_dicts = [m for m in filtered_modules if m.get('name') in session.get('selected_modules', [])]
1032
+ schema_data = load_schema_data(schema_path) if schema_path else None
1033
+ # Check if unsterile_run is set in form_data
1034
+ cert_cli = CertificateCLI()
1035
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1036
+ unsterile_run_field = f"{cert_schema['title']}_unsterile_run"
1037
+ unsterile_run = bool(form_data.get(unsterile_run_field))
1038
+ is_certified, _, _, _, _ = check_certification_status(flavor, module_dicts, selected_tests, tests_schema, schema_data, unsterile_run)
1039
+
1040
+ full_cmd = []
1041
+
1042
+ # Create a request-like object for form_to_argv
1043
+ class FormDict:
1044
+ def __init__(self, data):
1045
+ self.data = data
1046
+ def get(self, key, default=""):
1047
+ return self.data.get(key, default)
1048
+ def getlist(self, key):
1049
+ val = self.data.get(key)
1050
+ if isinstance(val, list):
1051
+ return val
1052
+ elif val:
1053
+ return [val]
1054
+ else:
1055
+ return []
1056
+
1057
+ # Ensure custom_broker_path from session is in form_data if available
1058
+ custom_broker_path = session.get('custom_broker_path')
1059
+ if custom_broker_path and custom_broker_path not in form_data.values():
1060
+ cert_cli = CertificateCLI()
1061
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1062
+ custom_broker_field_name = f"{cert_schema['title']}_custom_broker"
1063
+ # Only add if not already present
1064
+ if custom_broker_field_name not in form_data:
1065
+ form_data[custom_broker_field_name] = custom_broker_path
1066
+
1067
+ form_dict = FormDict(form_data)
1068
+
1069
+ cert_cli = CertificateCLI()
1070
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1071
+
1072
+ # Create test list
1073
+ tl = _write_testlist(selected_tests, form_dict, is_certified)
1074
+
1075
+ argv = form_to_argv(form_dict, parser=cert_cli.parser, title=cert_schema['title'])
1076
+ # Use absolute path and ensure it's properly formatted (already normalized above)
1077
+ schema_path_abs = os.path.abspath(schema_path)
1078
+ full_cmd = ["wlt-cert-cli", "--tl", tl, "--validation_schema", schema_path_abs] + argv
1079
+ # Add --non_cert_run flag if not certified
1080
+ if not is_certified:
1081
+ full_cmd.append("--non_cert_run")
1082
+
1083
+ if full_cmd:
1084
+ p = web_utils.open_in_terminal(full_cmd, preferred=PREFERRED_TERMINAL)
1085
+ pid = getattr(p, "pid", None)
1086
+
1087
+ # Store run information for popup display
1088
+ session['run_completed'] = True
1089
+ session['run_terminal'] = PREFERRED_TERMINAL
1090
+ session['run_pid'] = str(pid) if pid else None
1091
+ session['run_is_certified'] = is_certified
1092
+ return redirect(url_for('step', step_num=5))
1093
+
1094
+ @app.route("/export_config", methods=['GET'])
1095
+ def export_config():
1096
+ """Export current run configuration to a JSON file."""
1097
+ if not session.get('flavor') or not session.get('schema_path'):
1098
+ flash("No configuration to export. Please complete at least step 1 and 2.", "error")
1099
+ return redirect(url_for('step', step_num=session.get('current_step', 1)))
1100
+
1101
+ # Extract custom broker form field values from form_data
1102
+ form_data = session.get('form_data', {})
1103
+ cert_cli = CertificateCLI()
1104
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1105
+ custom_broker_fields = {}
1106
+ field_prefix = f"{cert_schema['title']}_custom_broker_"
1107
+ for field_name in DEFAULT_CUSTOM_BROKER.keys():
1108
+ full_field_name = f"{field_prefix}{field_name}"
1109
+ if full_field_name in form_data:
1110
+ custom_broker_fields[field_name] = form_data[full_field_name]
1111
+
1112
+ # Prepare configuration data
1113
+ config = {
1114
+ 'version': '1.0',
1115
+ 'exported_at': datetime.datetime.now().isoformat(),
1116
+ 'flavor': session.get('flavor'),
1117
+ 'schema_path': session.get('schema_path'),
1118
+ 'selected_modules': session.get('selected_modules', []),
1119
+ 'selected_tests': session.get('selected_tests', []),
1120
+ 'form_data': form_data,
1121
+ 'schema_data': load_schema_data(session.get('schema_path')), # Load schema data from file for export
1122
+ 'custom_broker_path': session.get('custom_broker_path'), # Include custom broker file path for backward compatibility
1123
+ 'custom_broker_fields': custom_broker_fields # Include custom broker form field values
1124
+ }
1125
+
1126
+ # Create filename with timestamp
1127
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1128
+ flavor_name = session.get('flavor', 'unknown').replace('_', '-')
1129
+ filename = f"certificate_run_{flavor_name}_{timestamp}.json"
1130
+ filepath = os.path.join(CONFIG_FOLDER, filename)
1131
+
1132
+ # Save configuration
1133
+ with open(filepath, 'w', encoding='utf-8') as f:
1134
+ json.dump(config, f, indent=2, ensure_ascii=False)
1135
+
1136
+ # Send file for download
1137
+ return send_file(filepath, as_attachment=True, download_name=filename, mimetype='application/json')
1138
+
1139
+ @app.route("/import_config", methods=['POST'])
1140
+ def import_config():
1141
+ """Import a previously saved run configuration."""
1142
+ if 'config_file' not in request.files:
1143
+ flash("No file selected", "error")
1144
+ return redirect(url_for('step', step_num=1))
1145
+
1146
+ file = request.files['config_file']
1147
+ if not file or not file.filename:
1148
+ flash("No file selected", "error")
1149
+ return redirect(url_for('step', step_num=1))
1150
+
1151
+ if not allowed_file(file.filename):
1152
+ flash("Invalid file type. Please upload a JSON file.", "error")
1153
+ return redirect(url_for('step', step_num=1))
1154
+
1155
+ try:
1156
+ # Read and parse JSON
1157
+ config_data = json.load(file)
1158
+
1159
+ # Validate configuration structure
1160
+ if 'version' not in config_data or 'flavor' not in config_data:
1161
+ flash("Invalid configuration file format", "error")
1162
+ return redirect(url_for('step', step_num=1))
1163
+
1164
+ # Restore session data
1165
+ session['flavor'] = config_data.get('flavor')
1166
+ session['schema_path'] = config_data.get('schema_path')
1167
+ session['selected_modules'] = config_data.get('selected_modules', [])
1168
+ session['selected_tests'] = config_data.get('selected_tests', [])
1169
+ session['form_data'] = config_data.get('form_data', {})
1170
+ # Don't restore schema_data to session (load from file when needed to avoid cookie size issues)
1171
+ # If schema_data is in config, save it back to the schema_path file if the path exists
1172
+ schema_path = config_data.get('schema_path')
1173
+ schema_data = config_data.get('schema_data')
1174
+ if schema_path and schema_data and os.path.exists(schema_path):
1175
+ # Schema file exists, no need to restore schema_data to session
1176
+ pass
1177
+ elif schema_path and schema_data:
1178
+ # Schema file doesn't exist but we have the data - save it
1179
+ try:
1180
+ with open(schema_path, 'w', encoding='utf-8') as f:
1181
+ json.dump(schema_data, f, indent=2)
1182
+ except Exception:
1183
+ pass # If we can't save, user will need to re-upload
1184
+ # Handle custom broker configuration
1185
+ custom_broker_fields = config_data.get('custom_broker_fields', {})
1186
+ if custom_broker_fields:
1187
+ # Restore broker form field values to form_data
1188
+ cert_cli = CertificateCLI()
1189
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1190
+ field_prefix = f"{cert_schema['title']}_custom_broker_"
1191
+ for field_name, field_value in custom_broker_fields.items():
1192
+ full_field_name = f"{field_prefix}{field_name}"
1193
+ session['form_data'][full_field_name] = field_value
1194
+ # Regenerate broker config file from form fields
1195
+ try:
1196
+ broker_config_path = _write_custom_broker_config(session['form_data'], cert_schema['title'])
1197
+ session['custom_broker_path'] = os.path.normpath(broker_config_path)
1198
+ except Exception:
1199
+ pass # If generation fails, user can reconfigure in step 4
1200
+ else:
1201
+ # Backward compatibility: try to use existing path
1202
+ session['custom_broker_path'] = config_data.get('custom_broker_path')
1203
+
1204
+ # Validate schema file still exists
1205
+ if session['schema_path'] and not os.path.exists(session['schema_path']):
1206
+ flash(f"Warning: Schema file not found at {session['schema_path']}. Please re-upload it in step 2.", "warning")
1207
+ session['current_step'] = 2
1208
+ # Validate custom_broker file still exists if present (or regenerate if we have fields)
1209
+ elif session.get('custom_broker_path') and not os.path.exists(session['custom_broker_path']):
1210
+ if custom_broker_fields:
1211
+ # Regenerate from fields if file doesn't exist
1212
+ try:
1213
+ cert_cli = CertificateCLI()
1214
+ cert_schema = web_utils.parser_to_schema(cert_cli.parser)
1215
+ broker_config_path = _write_custom_broker_config(session['form_data'], cert_schema['title'])
1216
+ session['custom_broker_path'] = os.path.normpath(broker_config_path)
1217
+ except Exception:
1218
+ flash(f"Warning: Custom broker configuration will be regenerated in step 4.", "warning")
1219
+ if session.get('current_step', 0) < 4:
1220
+ session['current_step'] = 4
1221
+ else:
1222
+ flash(f"Warning: Custom broker file not found at {session['custom_broker_path']}. Please reconfigure it in step 4.", "warning")
1223
+ if session.get('current_step', 0) < 4:
1224
+ session['current_step'] = 4
1225
+ else:
1226
+ # Determine appropriate step based on what's configured
1227
+ if session.get('selected_tests'):
1228
+ session['current_step'] = 5
1229
+ elif session.get('selected_modules'):
1230
+ session['current_step'] = 4
1231
+ elif session.get('schema_path'):
1232
+ if session.get('flavor') == FLAVOR_GW_ONLY:
1233
+ session['current_step'] = 4
1234
+ else:
1235
+ session['current_step'] = 3
1236
+ else:
1237
+ session['current_step'] = 2
1238
+
1239
+ flash("Configuration loaded successfully!", "success")
1240
+ return redirect(url_for('step', step_num=session['current_step']))
1241
+
1242
+ except json.JSONDecodeError:
1243
+ flash("Invalid JSON file", "error")
1244
+ return redirect(url_for('step', step_num=1))
1245
+ except Exception as e:
1246
+ flash(f"Error loading configuration: {str(e)}", "error")
1247
+ return redirect(url_for('step', step_num=1))
1248
+
1249
+ @app.route("/clear")
1250
+ def clear_session():
1251
+ """Clear session and start fresh."""
1252
+ session.clear()
1253
+ # Initialize session flags
1254
+ session['schema_mandatory_initialized'] = False
1255
+ return redirect(url_for('step', step_num=1))
1256
+
1257
+ @app.route("/parser")
1258
+ def parser():
1259
+ return web_utils.utils_parser(web_utils.CERT_WEB)
1260
+
1261
+ @app.route("/generator")
1262
+ def generator():
1263
+ return web_utils.utils_generator(web_utils.CERT_WEB)
1264
+
1265
+ @app.route("/data_structs", methods=['GET', 'POST'])
1266
+ def data_structs():
1267
+ return web_utils.utils_data_structs(web_utils.CERT_WEB)