firestarter 3.2.5__tar.gz → 3.2.6__tar.gz

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 (212) hide show
  1. {firestarter-3.2.5/firestarter.egg-info → firestarter-3.2.6}/PKG-INFO +1 -1
  2. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/_version.py +3 -3
  3. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/cli_handlers.py +4 -2
  4. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/codec.py +3 -2
  5. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/firmware.py +8 -6
  6. firestarter-3.2.6/firestarter/flash4_erase_gate.py +104 -0
  7. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/ic_layout.py +7 -0
  8. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/serial_comm.py +10 -1
  9. {firestarter-3.2.5 → firestarter-3.2.6/firestarter.egg-info}/PKG-INFO +1 -1
  10. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/scm_version.json +2 -2
  11. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_codec.py +2 -2
  12. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_codec_format_message.py +2 -2
  13. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_decoder.py +3 -3
  14. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_firmware_install.py +32 -28
  15. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_flash4_erase_gate.py +98 -102
  16. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_info_vpp_and_erase.py +3 -2
  17. firestarter-3.2.5/firestarter/flash4_erase_gate.py +0 -79
  18. {firestarter-3.2.5 → firestarter-3.2.6}/LICENSE +0 -0
  19. {firestarter-3.2.5 → firestarter-3.2.6}/MANIFEST.in +0 -0
  20. {firestarter-3.2.5 → firestarter-3.2.6}/README.md +0 -0
  21. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/__init__.py +0 -0
  22. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/address_parser.py +0 -0
  23. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/avr_tool.py +0 -0
  24. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/channel.py +0 -0
  25. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/chip_resolver.py +0 -0
  26. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/chip_test.py +0 -0
  27. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/compare.py +0 -0
  28. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/config.py +0 -0
  29. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/constants.py +0 -0
  30. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/data/chip_database.json +0 -0
  31. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/data/pinouts.json +0 -0
  32. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/database.py +0 -0
  33. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/diagnostic_report.py +0 -0
  34. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/dip24_2816_erase_gate.py +0 -0
  35. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/eprom_info.py +0 -0
  36. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/eprom_operations.py +0 -0
  37. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/erase_support.py +0 -0
  38. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/exceptions.py +0 -0
  39. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/frame_parser.py +0 -0
  40. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/fw_release_gate.py +0 -0
  41. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/hardware.py +0 -0
  42. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/jp5_gate.py +0 -0
  43. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/jumper_table.py +0 -0
  44. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/lock_status.py +0 -0
  45. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/log_capture.py +0 -0
  46. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/logging_utils.py +0 -0
  47. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/main.py +0 -0
  48. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/messages.py +0 -0
  49. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/page_size_gate.py +0 -0
  50. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/protection_readability.py +0 -0
  51. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/py32_dfu.py +0 -0
  52. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/sdp_capability.py +0 -0
  53. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/sdp_honesty.py +0 -0
  54. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/submit.py +0 -0
  55. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/timestamps.py +0 -0
  56. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/transport_counters.py +0 -0
  57. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/utils.py +0 -0
  58. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/vpp_display.py +0 -0
  59. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter/write_blank_guard.py +0 -0
  60. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/SOURCES.txt +0 -0
  61. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/dependency_links.txt +0 -0
  62. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/entry_points.txt +0 -0
  63. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/requires.txt +0 -0
  64. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/scm_file_list.json +0 -0
  65. {firestarter-3.2.5 → firestarter-3.2.6}/firestarter.egg-info/top_level.txt +0 -0
  66. {firestarter-3.2.5 → firestarter-3.2.6}/pyproject.toml +0 -0
  67. {firestarter-3.2.5 → firestarter-3.2.6}/setup.cfg +0 -0
  68. {firestarter-3.2.5 → firestarter-3.2.6}/tests/__init__.py +0 -0
  69. {firestarter-3.2.5 → firestarter-3.2.6}/tests/__snapshots__/test_characterization.ambr +0 -0
  70. {firestarter-3.2.5 → firestarter-3.2.6}/tests/conftest.py +0 -0
  71. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fake_chip.py +0 -0
  72. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/part_number_delta.json +0 -0
  73. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/report_shapes.py +0 -0
  74. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/at28c256-full-all-ok-sdp.json +0 -0
  75. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/attr01-status-axis-transport-fault.json +0 -0
  76. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh20-at28c256-fail.json +0 -0
  77. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh23-w27e257-fail.json +0 -0
  78. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh28-m27c512-fail.json +0 -0
  79. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/gh47-sst27sf512-pass.json +0 -0
  80. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-all-ok.json +0 -0
  81. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-blank-check-bad.json +0 -0
  82. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-canonical-name.json +0 -0
  83. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-comma-joined-name.json +0 -0
  84. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/m27c512-full-runs-1.json +0 -0
  85. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/prune03-synthesized-fingerprint-match.json +0 -0
  86. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-full-all-ok.json +0 -0
  87. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-six-step-readback-gated.json +0 -0
  88. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/sst27sf512-six-step.json +0 -0
  89. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/synthetic-arm4-empty-results.json +0 -0
  90. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/synthetic-arm4-no-ok.json +0 -0
  91. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/uv-slot-write-pass.json +0 -0
  92. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/reports/w27e257-full-all-ok.json +0 -0
  93. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/shape_ids.json +0 -0
  94. {firestarter-3.2.5 → firestarter-3.2.6}/tests/fixtures/synthetic_nonzero_chip_id.py +0 -0
  95. {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/build_db_part_specific_constants.json +0 -0
  96. {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/chip_database_field_inventory.json +0 -0
  97. {firestarter-3.2.5 → firestarter-3.2.6}/tests/golden/wire_dict.json +0 -0
  98. {firestarter-3.2.5 → firestarter-3.2.6}/tests/plan_corpus.py +0 -0
  99. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_address_parser.py +0 -0
  100. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_b15_page_size_corroboration.py +0 -0
  101. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_blast_radius_invariance.py +0 -0
  102. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_boot_block_hint.py +0 -0
  103. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_budget_failure_render.py +0 -0
  104. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_bug_characterization.py +0 -0
  105. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_constant_census.py +0 -0
  106. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_inclusion.py +0 -0
  107. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_interpret_timing.py +0 -0
  108. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_build_db_pinout_fork.py +0 -0
  109. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_canonical_part_number.py +0 -0
  110. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_characterization.py +0 -0
  111. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_database_field_inventory.py +0 -0
  112. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_name_resolution.py +0 -0
  113. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_resolver.py +0 -0
  114. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test.py +0 -0
  115. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_blank_check_order.py +0 -0
  116. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_cycle.py +0 -0
  117. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_sdp_leg.py +0 -0
  118. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_timing.py +0 -0
  119. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_chip_test_uv_slot_write.py +0 -0
  120. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_cli_handlers.py +0 -0
  121. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_click_group_gate_hook.py +0 -0
  122. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_cobs.py +0 -0
  123. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_compare.py +0 -0
  124. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_config.py +0 -0
  125. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_config_band_refusal.py +0 -0
  126. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_connect_cost_harness.py +0 -0
  127. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_consistency_check.py +0 -0
  128. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_coverage_floor_v18.py +0 -0
  129. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_database_conversion.py +0 -0
  130. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_datasheet_overrides.py +0 -0
  131. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_derive_plan_no_drop_sweep.py +0 -0
  132. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_derive_plan_structural_sentinel.py +0 -0
  133. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_group_channel_gating.py +0 -0
  134. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_test_cmd.py +0 -0
  135. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dev_tools_channel_gate.py +0 -0
  136. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_devtest_firmware_error_propagation.py +0 -0
  137. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dfu_opcode_anchors.py +0 -0
  138. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_diagnostic_report.py +0 -0
  139. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_dip24_2816_erase_gate.py +0 -0
  140. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_endpoint_constants.py +0 -0
  141. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_database.py +0 -0
  142. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_info.py +0 -0
  143. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_eprom_operations.py +0 -0
  144. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_erase_blank_step_nonregression.py +0 -0
  145. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_erase_flag_invariants.py +0 -0
  146. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_error_code_seam.py +0 -0
  147. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_even_block.py +0 -0
  148. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_extra_chips_supplement.py +0 -0
  149. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_list_failure_vs_empty.py +0 -0
  150. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_port_targeting_and_blind_install.py +0 -0
  151. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_release_gate.py +0 -0
  152. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_update_dead_endpoint.py +0 -0
  153. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_update_path_gate.py +0 -0
  154. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fw_version_guard.py +0 -0
  155. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_fwguard.py +0 -0
  156. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_hardware.py +0 -0
  157. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_hw_revision_gate.py +0 -0
  158. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_ic_layout.py +0 -0
  159. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_jp5_gate.py +0 -0
  160. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_jumper_table.py +0 -0
  161. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_class_partition.py +0 -0
  162. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_cli.py +0 -0
  163. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_lock_status_wire.py +0 -0
  164. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_log_capture.py +0 -0
  165. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_logging_utils.py +0 -0
  166. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_matrix_artifact.py +0 -0
  167. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_alignment_refusal.py +0 -0
  168. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_invariants.py +0 -0
  169. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_page_size_write_refusal.py +0 -0
  170. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_parse_devtest_issue.py +0 -0
  171. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_probe_spurious_setup_ack.py +0 -0
  172. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_programming_vcc_census.py +0 -0
  173. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protect_flags_doc_measurements.py +0 -0
  174. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_resolution.py +0 -0
  175. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_status_catalog.py +0 -0
  176. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protection_table_citations.py +0 -0
  177. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protocol_not_implemented.py +0 -0
  178. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_protocol_not_implemented_production_path.py +0 -0
  179. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_provenance.py +0 -0
  180. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_pulse_us_override.py +0 -0
  181. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_channel_gating.py +0 -0
  182. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_dfu.py +0 -0
  183. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_flash_map_host.py +0 -0
  184. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_py32_pyusb_absent.py +0 -0
  185. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_pyusb_api_surface.py +0 -0
  186. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_capability.py +0 -0
  187. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_db_invariant.py +0 -0
  188. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_sdp_honesty.py +0 -0
  189. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_serial_characterization.py +0 -0
  190. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_serial_comm.py +0 -0
  191. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_session_lease.py +0 -0
  192. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_setup_refusals.py +0 -0
  193. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_submit.py +0 -0
  194. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_timestamps.py +0 -0
  195. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_transport_counters.py +0 -0
  196. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_utils.py +0 -0
  197. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_uv_mask.py +0 -0
  198. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validate_family_cmd.py +0 -0
  199. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validate_oracle.py +0 -0
  200. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_validated_parts_regression_surface.py +0 -0
  201. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vcc_margin_rail.py +0 -0
  202. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vpp_decode_table.py +0 -0
  203. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_vpp_rail_classification.py +0 -0
  204. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_wire_dict_equivalence.py +0 -0
  205. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_blank_guard.py +0 -0
  206. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_blank_guard_pinning.py +0 -0
  207. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_guard_port_pinning.py +0 -0
  208. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_progress.py +0 -0
  209. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_response_budget.py +0 -0
  210. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_skip_erase_0x0d.py +0 -0
  211. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_skip_sdp_unlock.py +0 -0
  212. {firestarter-3.2.5 → firestarter-3.2.6}/tests/test_write_verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firestarter
3
- Version: 3.2.5
3
+ Version: 3.2.6
4
4
  Summary: EPROM programmer software for the Relatively Universal ROM Programmer
5
5
  Author-email: Henrik Olsson <henols@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/henols/firestarter_app
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.2.5'
22
- __version_tuple__ = version_tuple = (3, 2, 5)
21
+ __version__ = version = '3.2.6'
22
+ __version_tuple__ = version_tuple = (3, 2, 6)
23
23
 
24
- __commit_id__ = commit_id = 'g75046d7'
24
+ __commit_id__ = commit_id = 'g6a02a12'
@@ -1443,9 +1443,11 @@ def erase(
1443
1443
 
1444
1444
  eprom_data = resolve_chip(eprom, db=app.db)
1445
1445
 
1446
+ # The database leaves FLAG_CAN_ERASE clear for flash4, so `write` never
1447
+ # carries it. The erase sets it for this command only; setup_command
1448
+ # refuses it on firmware without the 5 V software chip erase.
1446
1449
  if flash4_erase_gate.is_flash4(eprom_data):
1447
- click.echo(flash4_erase_gate.refusal_text(eprom))
1448
- sys.exit(0 if ignore_unsupported else 1)
1450
+ eprom_data = flash4_erase_gate.with_erase_flag(eprom_data)
1449
1451
 
1450
1452
  full_record = app.db.get_eprom(eprom) or {}
1451
1453
  if dip24_2816_erase_gate.is_affected(full_record.get("pin-map")):
@@ -74,7 +74,8 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
74
74
 
75
75
  P-02 MSG_OK_REV — params[0]=physical u8, params[1]=effective u8
76
76
  effective==0xFF → "Rev{physical}" (no override)
77
- effective!=0xFF → "Rev{effective}, Override HW: Rev{physical}"
77
+ effective!=0xFF → "Rev{effective}, Detected HW: Rev{physical}"
78
+ (the override is in effect; the second field is what the board detected)
78
79
 
79
80
  P-03 MSG_OK_CFG — params[0]=r1 u32, params[1]=r2 u32, params[2]=override u8,
80
81
  params[3]=bandgap_mv u16 (absent on firmware before 3.1.0b4)
@@ -104,7 +105,7 @@ def format_message(msg_id: int, params: List[Any], entry: MessageDef) -> str | N
104
105
  if effective == 0xFF:
105
106
  return phys_str
106
107
  eff_str = _REVISION_SILKSCREEN.get(effective, f"Rev{effective}")
107
- return f"{eff_str}, Override HW: {phys_str}"
108
+ return f"{eff_str}, Detected HW: {phys_str}"
108
109
 
109
110
  if msg_id == MSG_OK_CFG and len(params) in (3, 4):
110
111
  r1, r2, override = params[0], params[1], params[2]
@@ -335,8 +335,8 @@ class FirmwareManager:
335
335
  """Router: returns (resolved_version, download_url) or (None, None) on failure.
336
336
 
337
337
  channel='stable' → delegates to fetch_latest_release_info (back-compat shim).
338
- channel='pre' → paginates /releases, filters prerelease=True, sorts by PEP 440
339
- descending, takes highest; falls back to stable if none.
338
+ channel='pre' → paginates /releases, takes the highest non-draft release by
339
+ PEP 440, stable or pre-release; falls back to stable if none.
340
340
  channel='pinned' → fetches /releases/tags/{version} directly.
341
341
  """ # noqa: E501
342
342
  if channel == "stable":
@@ -374,10 +374,12 @@ class FirmwareManager:
374
374
  logger.error(f"Failed to fetch releases for pre-release channel: {e}")
375
375
  return None, None
376
376
 
377
- # Filter: prerelease=True AND draft=False, skip unparseable tags.
377
+ # Every non-draft release, stable or pre-release, as `pip --pre`
378
+ # does: a stable 3.2.2 must win over an older 3.1.1b2. Skip
379
+ # unparseable tags.
378
380
  candidates = []
379
381
  for r in all_releases:
380
- if not r.get("prerelease") or r.get("draft"):
382
+ if r.get("draft"):
381
383
  continue
382
384
  tag = r.get("tag_name", "")
383
385
  try:
@@ -387,7 +389,7 @@ class FirmwareManager:
387
389
 
388
390
  if not candidates:
389
391
  logger.info(
390
- "No pre-release firmware available — falling back to stable "
392
+ "No firmware release with a parseable tag — falling back to stable "
391
393
  "(matches pip --pre semantics)."
392
394
  )
393
395
  return self.fetch_latest_release_info(board=board)
@@ -787,7 +789,7 @@ class FirmwareManager:
787
789
  Returns True if an operation (check or install) was successful in some sense, False on major failure.
788
790
 
789
791
  channel='stable' → uses /releases/latest (default, INST-01 non-regression).
790
- channel='pre' → selects highest pre-release.
792
+ channel='pre' → selects the highest release, stable or pre-release.
791
793
  channel='pinned' → uses exact tag from pinned_version.
792
794
  """ # noqa: E501
793
795
  connected_port, current_version, current_board = self.check_current_firmware(
@@ -0,0 +1,104 @@
1
+ """
2
+ Project Name: Firestarter
3
+ Copyright (c) 2024 Henrik Olsson
4
+
5
+ Permission is hereby granted under MIT license.
6
+
7
+ Flash4 (protocol 0x05) erase policy.
8
+
9
+ Firmware 3.2.2 added a software chip erase for protocol 0x05 (the six-cycle
10
+ AA/55/80/AA/55/10 sequence, 5 V only). Older firmware is not safe to send
11
+ that erase to:
12
+
13
+ - 3.0.0b8 to 3.0.0b26 erase a 0x05 part with the VPP regulator and VPE
14
+ switched on: 12 V on a 5 V part.
15
+ - 3.0.0b27 to 3.2.1 answer "not supported".
16
+
17
+ So `FLAG_CAN_ERASE` stays clear for algorithm 5 in the database
18
+ (`erase_support.erase_accepted`), and `write` never carries it. Only the
19
+ `erase` command sets it, for that one command
20
+ (`with_erase_flag`), and `SerialCommunicator.setup_command` refuses the
21
+ command before the operation starts when the firmware is older than
22
+ `ERASE_MIN_FIRMWARE` (`needs_newer_firmware`).
23
+
24
+ Like `jp5_gate.py` and `sdp_capability.py`, this is a pure predicate module:
25
+ no I/O, no environment reads, no serial access.
26
+
27
+ `is_flash4` FAILS OPEN on absent evidence (a wire dict with no `algorithm`
28
+ key is "not flash4"), unlike `jp5_gate` and `sdp_capability`. Refusing every
29
+ chip this predicate cannot classify would break `erase` for chips that are
30
+ not flash4 at all.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ from typing import Any, Mapping # noqa: UP035
37
+
38
+ from firestarter.constants import COMMAND_ERASE, FLAG_CAN_ERASE
39
+
40
+ FLASH4_PROTOCOL_ID = 5
41
+
42
+ # The first firmware with the software chip erase for protocol 0x05.
43
+ ERASE_MIN_FIRMWARE = (3, 2, 2)
44
+
45
+ _FIRMWARE_REFUSAL_FORMAT = (
46
+ "Erasing this chip needs firmware {minimum} or later; this board runs "
47
+ "{version}. Update it with 'firestarter fw --install'."
48
+ )
49
+
50
+
51
+ def is_flash4(programmer_data: Mapping[str, Any] | None) -> bool:
52
+ """True when the wire dict's `algorithm` value is the flash4 protocol id.
53
+
54
+ The comparison is against `algorithm`, not `flags & FLAG_CAN_ERASE`: the
55
+ flag is also clear for UV-EPROM and SRAM parts, so a flag-based
56
+ predicate would widen this past its flash4-only scope.
57
+
58
+ Returns False for a missing, `None`, or empty `programmer_data`, and for
59
+ a dict with no `algorithm` key.
60
+ """
61
+ if not programmer_data:
62
+ return False
63
+ return programmer_data.get("algorithm") == FLASH4_PROTOCOL_ID
64
+
65
+
66
+ def with_erase_flag(programmer_data: Mapping[str, Any]) -> dict[str, Any]:
67
+ """A copy of `programmer_data` with `FLAG_CAN_ERASE` set, for `erase` only."""
68
+ data = dict(programmer_data)
69
+ data["flags"] = int(data.get("flags") or 0) | FLAG_CAN_ERASE
70
+ return data
71
+
72
+
73
+ def _version_tuple(version: str | None) -> tuple[int, int, int] | None:
74
+ match = re.match(r"(\d+)\.(\d+)\.(\d+)", version or "")
75
+ if not match:
76
+ return None
77
+ return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
78
+
79
+
80
+ def needs_newer_firmware(
81
+ command: Mapping[str, Any], firmware_version: str | None
82
+ ) -> bool:
83
+ """True when `command` is a flash4 erase that `firmware_version` cannot run safely.
84
+
85
+ Only an erase command that carries `FLAG_CAN_ERASE` for a flash4 part is
86
+ affected; every other command returns False. A version that cannot be
87
+ parsed counts as too old, because the 3.0.0 pre-releases with the 12 V
88
+ erase are older than every version this check accepts. A pre-release
89
+ or development build of 3.2.2 (`3.2.2rc1`, `3.2.2.dev5`) counts as 3.2.2.
90
+ """
91
+ if command.get("cmd") != COMMAND_ERASE or not is_flash4(command):
92
+ return False
93
+ if not int(command.get("flags") or 0) & FLAG_CAN_ERASE:
94
+ return False
95
+ parsed = _version_tuple(firmware_version)
96
+ return parsed is None or parsed < ERASE_MIN_FIRMWARE
97
+
98
+
99
+ def firmware_refusal_text(firmware_version: str | None) -> str:
100
+ """The one-line refusal for a flash4 erase on firmware that is too old."""
101
+ return _FIRMWARE_REFUSAL_FORMAT.format(
102
+ minimum=".".join(str(n) for n in ERASE_MIN_FIRMWARE),
103
+ version=firmware_version or "an unknown version",
104
+ )
@@ -12,6 +12,7 @@ from typing import Dict, List # noqa: UP035
12
12
  from firestarter import jumper_table
13
13
  from firestarter.database import EpromDatabase, format_mv
14
14
  from firestarter.erase_support import erase_accepted, is_electrically_erasable
15
+ from firestarter.flash4_erase_gate import FLASH4_PROTOCOL_ID
15
16
  from firestarter.vpp_display import shows_programming_vpp
16
17
 
17
18
  logger = logging.getLogger("EpromSpecBuilder")
@@ -19,6 +20,7 @@ logger = logging.getLogger("EpromSpecBuilder")
19
20
  CAN_ERASE_YES = "yes (electrically erasable)"
20
21
  CAN_ERASE_UV_ONLY = "no (UV erase only)"
21
22
  CAN_ERASE_NOT_SUPPORTED = "no (erase is not supported for this chip)"
23
+ CAN_ERASE_FLASH4 = "yes (needs firmware 3.2.2 or later)"
22
24
 
23
25
 
24
26
  class EpromSpecBuilder:
@@ -524,6 +526,11 @@ class EpromSpecBuilder:
524
526
  etype, eprom_data.get("protocol-id"), eprom_data.get("pin-map")
525
527
  ):
526
528
  output_data["can_erase_str"] = CAN_ERASE_YES
529
+ elif is_electrically_erasable(etype) and (
530
+ eprom_data.get("protocol-id") == FLASH4_PROTOCOL_ID
531
+ ):
532
+ # `erase` sets FLAG_CAN_ERASE for flash4 itself (flash4_erase_gate).
533
+ output_data["can_erase_str"] = CAN_ERASE_FLASH4
527
534
  elif is_electrically_erasable(etype):
528
535
  output_data["can_erase_str"] = CAN_ERASE_NOT_SUPPORTED
529
536
  elif etype == "UV-EPROM":
@@ -20,6 +20,7 @@ import serial.serialutil
20
20
  import serial.tools.list_ports
21
21
 
22
22
  import firestarter.codec as codec
23
+ import firestarter.flash4_erase_gate as flash4_erase_gate
23
24
  import firestarter.transport_counters as transport_counters
24
25
  from firestarter.config import ConfigManager # Assuming ConfigManager is refactored
25
26
  from firestarter.constants import (
@@ -957,9 +958,17 @@ class SerialCommunicator:
957
958
  # to major=3 later. Set FIRESTARTER_DEV_ALLOW_PRE_V12=1 to bypass when # noqa: E501
958
959
  # bench-testing a current host against a historical (v2.x) firmware build. # noqa: E501
959
960
  SerialCommunicator._validate_firmware_version(
960
- version_match.group(0),
961
+ version_match.group(0).rstrip("."),
961
962
  allow_pre_v12=os.environ.get("FIRESTARTER_DEV_ALLOW_PRE_V12") == "1",
962
963
  )
964
+ # A flash4 erase on firmware older than 3.2.2 reaches either a
965
+ # 12 V erase (3.0.0b8-b26) or a "not supported" answer.
966
+ if flash4_erase_gate.needs_newer_firmware(command_to_send, identity):
967
+ raise FirmwareOutdatedError(
968
+ flash4_erase_gate.firmware_refusal_text(
969
+ identity.split(":")[0] if identity else None
970
+ )
971
+ )
963
972
 
964
973
  # Shield-revision gate — ordered after the version check because
965
974
  # firmware old enough to fail that check cannot be trusted to have
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firestarter
3
- Version: 3.2.5
3
+ Version: 3.2.6
4
4
  Summary: EPROM programmer software for the Relatively Universal ROM Programmer
5
5
  Author-email: Henrik Olsson <henols@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/henols/firestarter_app
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "3.2.5",
2
+ "tag": "3.2.6",
3
3
  "distance": 0,
4
- "node": "g75046d7",
4
+ "node": "g6a02a12",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-27"
@@ -40,10 +40,10 @@ class TestFormatMessageRevision:
40
40
  assert result == "Rev 0"
41
41
 
42
42
  def test_msg_ok_rev_with_override(self):
43
- """MSG_OK_REV: effective!=0xFF → '{eff_str}, Override HW: {phys_str}'."""
43
+ """MSG_OK_REV: effective!=0xFF → '{eff_str}, Detected HW: {phys_str}'."""
44
44
  entry = CATALOG[MSG_OK_REV]
45
45
  result = format_message(MSG_OK_REV, [0x02, 0x04], entry)
46
- assert result == "Rev 2.2 (override), Override HW: Rev 2.0-class"
46
+ assert result == "Rev 2.2 (override), Detected HW: Rev 2.0-class"
47
47
 
48
48
  def test_msg_ok_cfg_no_override(self):
49
49
  """MSG_OK_CFG: override==0xFF → 'R1: {r1}, R2: {r2}'."""
@@ -32,8 +32,8 @@ def test_format_msg_ok_rev_with_override_includes_both() -> None:
32
32
  MSG_OK_REV, [REVISION_2_2, REVISION_UNKNOWN], CATALOG[MSG_OK_REV]
33
33
  )
34
34
  assert out is not None
35
- # The format is "{eff}, Override HW: {phys}"
36
- assert "Override HW" in out
35
+ # The format is "{eff}, Detected HW: {phys}"
36
+ assert "Detected HW" in out
37
37
 
38
38
 
39
39
  def test_format_msg_ok_cfg_no_override() -> None:
@@ -363,8 +363,8 @@ class TestIdFrameDecoder:
363
363
 
364
364
  def test_ok_rev_p02_with_override_decodes(self, fake_serial, make_comm):
365
365
  """P-02: MSG_OK_REV with physical=0x01, effective=0x02 (override active)
366
- renders 'Rev 2.0-class, Override HW: Rev 1' per Phase 34 D-05 Path A
367
- silkscreen-string mapping (was 'Rev2, Override HW: Rev1' pre-Phase-34)."""
366
+ renders 'Rev 2.0-class, Detected HW: Rev 1': the override, then the
367
+ revision the board detected."""
368
368
  comm = make_comm()
369
369
  # params: u8 physical=0x01, u8 effective=0x02
370
370
  params = bytes([0x01, 0x02])
@@ -374,7 +374,7 @@ class TestIdFrameDecoder:
374
374
  response = _drive_one_response(comm)
375
375
  assert response is not None
376
376
  assert response.type == "OK"
377
- assert response.message == "Rev 2.0-class, Override HW: Rev 1"
377
+ assert response.message == "Rev 2.0-class, Detected HW: Rev 1"
378
378
 
379
379
  def test_ok_rev_p02_no_override_decodes(self, fake_serial, make_comm):
380
380
  """P-02: MSG_OK_REV with physical=0x01, effective=0xFF sentinel renders
@@ -370,38 +370,42 @@ class TestFirmwareInstallPreRelease:
370
370
  assert version == "3.1.0rc1"
371
371
  assert "uno_rc1.hex" in url
372
372
 
373
- def test_pre_falls_back_to_stable_when_no_prerelease(self, monkeypatch, caplog):
374
- """INST-02 / D-05 — fallback to stable when no pre-release exists.
375
-
376
- If the paginated releases contain no prerelease=True entries, the method
377
- must silently fall back to /releases/latest and log INFO about the fallback.
378
-
379
- RED today: FirmwareManager has no fetch_release_info — AttributeError.
380
- """
381
- stable_only = mock_releases_factory([_STABLE_RELEASE_UNO])
382
- stable_latest_mock = mock_releases_factory([])
383
- stable_latest_mock.json.return_value = _STABLE_RELEASE_UNO
373
+ def test_pre_takes_stable_when_no_prerelease(self, monkeypatch):
374
+ """With no pre-release in the list, the pre channel takes the stable one."""
375
+ monkeypatch.setattr(
376
+ firmware.requests,
377
+ "get",
378
+ lambda url, **kw: mock_releases_factory([_STABLE_RELEASE_UNO]),
379
+ )
380
+ fm = FirmwareManager(config_manager=MagicMock())
381
+ version, url = fm.fetch_release_info(channel="pre", board="uno")
382
+ assert version == "3.0.0"
383
+ assert url == "https://example.com/uno_stable.hex"
384
384
 
385
- call_count = [0]
385
+ def test_pre_takes_a_newer_stable_over_an_older_prerelease(self, monkeypatch):
386
+ """Like `pip --pre`: stable 3.2.2 wins over pre-release 3.1.1b2."""
386
387
 
387
- def multi_response_get(url, **kw):
388
- call_count[0] += 1
389
- if "latest" in url:
390
- return stable_latest_mock
391
- return stable_only
388
+ def release(tag, prerelease):
389
+ return {
390
+ "tag_name": tag,
391
+ "prerelease": prerelease,
392
+ "draft": False,
393
+ "assets": [
394
+ {
395
+ "name": "firestarter_uno.hex",
396
+ "browser_download_url": f"https://example.com/{tag}.hex",
397
+ }
398
+ ],
399
+ }
392
400
 
393
- monkeypatch.setattr(firmware.requests, "get", multi_response_get)
401
+ releases = [release("3.2.2", False), release("3.1.1b2", True)]
402
+ monkeypatch.setattr(
403
+ firmware.requests, "get", lambda url, **kw: mock_releases_factory(releases)
404
+ )
394
405
  fm = FirmwareManager(config_manager=MagicMock())
395
- with caplog.at_level(logging.INFO):
396
- version, url = fm.fetch_release_info(channel="pre", board="uno")
397
- # Falls back to stable release
398
- assert version == "3.0.0"
399
- assert url is not None
400
- # Must log something about falling back to stable
401
- lower_records = [r.message.lower() for r in caplog.records]
402
- assert any(
403
- "fall" in m or "stable" in m or "no pre" in m for m in lower_records
404
- ), f"Expected a fallback log record; got: {[r.message for r in caplog.records]}"
406
+ version, url = fm.fetch_release_info(channel="pre", board="uno")
407
+ assert version == "3.2.2"
408
+ assert url == "https://example.com/3.2.2.hex"
405
409
 
406
410
  def test_pre_filters_draft_releases(self, monkeypatch):
407
411
  """INST-02 / D-03 — draft releases are excluded from pre-release candidates.
@@ -4,38 +4,25 @@ Copyright (c) 2024 Henrik Olsson
4
4
 
5
5
  Permission is hereby granted under MIT license.
6
6
 
7
- Flash4 (protocol 0x05) erase-refusal gate.
7
+ Flash4 (protocol 0x05) erase gate.
8
+
9
+ Firmware 3.2.2 erases a 0x05 part with a 5 V software chip erase. Older
10
+ firmware either answers "not supported" or, in 3.0.0b8 to 3.0.0b26, erases
11
+ with VPP and VPE on: 12 V on a 5 V part.
8
12
 
9
13
  Coverage:
10
- 1. THE PURE PREDICATE -- `is_flash4` parametrized over every `algorithm`
11
- value the shipped database actually contains: True for 5, False for
12
- every other value present.
13
- 2. FAIL-OPEN -- a missing, `None`, or empty wire dict, or one with no
14
- `algorithm` key, returns False rather than raising or refusing. This
15
- polarity is deliberately inverted against `jp5_gate` and
16
- `sdp_capability`, which both fail CLOSED on absent evidence.
17
- 3. COUPLING TO THE REAL DATABASE -- the set of
18
- part numbers for which the predicate fires, driven through
19
- `resolve_chip` against `EpromDatabase(skip_local_override=True)`, is
20
- exactly the set of shipped rows whose `programming.algorithm` is 5.
21
- 4. NO PART-NUMBER LITERAL IN THE MODULE -- no shipped `part_number` string
22
- occurs anywhere in `flash4_erase_gate.py`'s source, docstrings
23
- included, proving the predicate is database-derived rather than a
24
- hand-kept list.
25
- 5. THE MESSAGE SHAPE -- the rendered refusal line is exactly one
26
- line, names the chip, and carries none of a forbidden-substring list
27
- covering the cause, the alternative, and the `--force` workaround.
28
- 6. NOT A BLANKET REFUSAL -- a part whose algorithm is not 5 still
29
- reaches `EpromOperator.erase_eprom`.
30
-
31
- End-to-end proof that `firestarter erase` on a flash4 part refuses before
32
- the serial port opens, printing exactly one line and never reaching
33
- `EpromOperator.erase_eprom` -- the pre-connect guarantee.
14
+ 1. `erase` on a flash4 part reaches the operator with FLAG_CAN_ERASE set,
15
+ and the database still leaves the flag clear, so `write` never has it.
16
+ 2. `needs_newer_firmware`: refused below 3.2.2 and for an unparseable
17
+ version, accepted from 3.2.2 (and its pre-release and dev builds), and
18
+ never applied to another command or another protocol.
19
+ 3. `setup_command` raises before the operation starts when a flash4 erase
20
+ meets firmware older than 3.2.2.
21
+ 4. `is_flash4` over every shipped `algorithm` value, its fail-open polarity,
22
+ and its coupling to the real database.
34
23
  """
35
24
 
36
- import inspect
37
- from pathlib import Path
38
- from unittest.mock import Mock, patch
25
+ from unittest.mock import MagicMock, Mock, patch
39
26
 
40
27
  import pytest
41
28
  from click.testing import CliRunner
@@ -44,13 +31,26 @@ from firestarter import cli_handlers
44
31
  from firestarter.chip_resolver import resolve_chip
45
32
  from firestarter.cli_handlers import AppContext, cli
46
33
  from firestarter.config import ConfigManager
34
+ from firestarter.constants import COMMAND_ERASE, COMMAND_WRITE, FLAG_CAN_ERASE
47
35
  from firestarter.database import EpromDatabase
48
36
  from firestarter.eprom_info import EpromConsolePresenter
49
37
  from firestarter.eprom_operations import EpromOperator
50
- from firestarter.exceptions import ChipNotFoundError, ChipNotImplementedError
38
+ from firestarter.exceptions import (
39
+ ChipNotFoundError,
40
+ ChipNotImplementedError,
41
+ FirmwareOutdatedError,
42
+ )
51
43
  from firestarter.firmware import FirmwareManager
52
- from firestarter.flash4_erase_gate import FLASH4_PROTOCOL_ID, is_flash4, refusal_text
44
+ from firestarter.flash4_erase_gate import (
45
+ FLASH4_PROTOCOL_ID,
46
+ firmware_refusal_text,
47
+ is_flash4,
48
+ needs_newer_firmware,
49
+ )
53
50
  from firestarter.hardware import HardwareManager
51
+ from firestarter.messages import MSG_OK_READY
52
+
53
+ from .conftest import build_frame
54
54
 
55
55
  NO_PIN1_BUS_CONFIG = {"bus": [0, 1, 2]}
56
56
 
@@ -66,51 +66,88 @@ def _cli_app_context(eprom_operator):
66
66
  )
67
67
 
68
68
 
69
- def test_cli_erase_on_flash4_part_refuses_pre_connect_with_one_line():
69
+ def test_cli_erase_on_flash4_part_sends_can_erase():
70
70
  runner = CliRunner()
71
71
  eprom_operator = Mock(spec=EpromOperator)
72
+ eprom_operator.erase_eprom.return_value = True
72
73
  app = _cli_app_context(eprom_operator)
73
74
  with patch.object(
74
75
  cli_handlers,
75
76
  "resolve_chip",
76
- return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
77
+ return_value={"algorithm": 5, "flags": 0, "bus-config": NO_PIN1_BUS_CONFIG},
77
78
  ):
78
79
  result = runner.invoke(cli, ["erase", "AE29F2008"], obj=app)
79
80
 
80
- assert result.exit_code == 1
81
- lines = result.output.splitlines()
82
- assert lines == [refusal_text("AE29F2008")]
83
- eprom_operator.erase_eprom.assert_not_called()
81
+ assert result.exit_code == 0, result.output
82
+ eprom_operator.erase_eprom.assert_called_once()
83
+ sent = eprom_operator.erase_eprom.call_args.args[1]
84
+ assert sent["flags"] & FLAG_CAN_ERASE
84
85
 
85
86
 
86
- def test_cli_erase_ignore_unsupported_exits_0_with_byte_identical_output():
87
- runner = CliRunner()
87
+ def test_database_leaves_can_erase_clear_for_flash4_writes():
88
+ """`write` must never carry FLAG_CAN_ERASE for a flash4 part."""
89
+ db = EpromDatabase(skip_local_override=True)
90
+ data = resolve_chip("AE29F2008", db=db)
91
+ assert is_flash4(data)
92
+ assert not data["flags"] & FLAG_CAN_ERASE
88
93
 
89
- exit1_operator = Mock(spec=EpromOperator)
90
- exit1_app = _cli_app_context(exit1_operator)
91
- with patch.object(
92
- cli_handlers,
93
- "resolve_chip",
94
- return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
95
- ):
96
- exit1_result = runner.invoke(cli, ["erase", "AE29F2008"], obj=exit1_app)
97
94
 
98
- exit0_operator = Mock(spec=EpromOperator)
99
- exit0_app = _cli_app_context(exit0_operator)
100
- with patch.object(
101
- cli_handlers,
102
- "resolve_chip",
103
- return_value={"algorithm": 5, "bus-config": NO_PIN1_BUS_CONFIG},
104
- ):
105
- exit0_result = runner.invoke(
106
- cli, ["erase", "AE29F2008", "--ignore-unsupported"], obj=exit0_app
107
- )
95
+ def _erase(flags=FLAG_CAN_ERASE, algorithm=FLASH4_PROTOCOL_ID, cmd=COMMAND_ERASE):
96
+ return {"cmd": cmd, "algorithm": algorithm, "flags": flags}
97
+
108
98
 
109
- assert exit1_result.exit_code == 1
110
- assert exit0_result.exit_code == 0
111
- assert exit0_result.output == exit1_result.output
112
- exit1_operator.erase_eprom.assert_not_called()
113
- exit0_operator.erase_eprom.assert_not_called()
99
+ @pytest.mark.parametrize(
100
+ "version",
101
+ [None, "", "garbage", "3.0.0", "3.0.0b20", "3.1.1b2", "3.2.0", "3.2.1"],
102
+ )
103
+ def test_flash4_erase_needs_firmware_3_2_2(version):
104
+ assert needs_newer_firmware(_erase(), version) is True
105
+
106
+
107
+ @pytest.mark.parametrize(
108
+ "version",
109
+ ["3.2.2", "3.2.2:uno", "3.2.2rc1:leonardo", "3.2.2.dev48", "3.2.10", "3.3.0"],
110
+ )
111
+ def test_flash4_erase_runs_on_firmware_3_2_2_and_later(version):
112
+ assert needs_newer_firmware(_erase(), version) is False
113
+
114
+
115
+ @pytest.mark.parametrize(
116
+ "command",
117
+ [
118
+ _erase(algorithm=6),
119
+ _erase(cmd=COMMAND_WRITE),
120
+ _erase(flags=0),
121
+ {"state": 13},
122
+ ],
123
+ )
124
+ def test_firmware_gate_applies_only_to_a_flagged_flash4_erase(command):
125
+ assert needs_newer_firmware(command, "3.0.0b20") is False
126
+
127
+
128
+ def test_firmware_refusal_text_is_one_line_and_names_the_versions():
129
+ text = firmware_refusal_text("3.2.1")
130
+ assert "\n" not in text
131
+ assert "3.2.2" in text and "3.2.1" in text
132
+
133
+
134
+ def _ready_frame(version: bytes) -> bytes:
135
+ # [buffer_size u16][hw_revision u8][ver_len u8][version bytes]
136
+ params = b"\x02\x00" + bytes([0]) + bytes([len(version)]) + version
137
+ return build_frame(MSG_OK_READY, params)
138
+
139
+
140
+ def test_setup_command_refuses_flash4_erase_on_old_firmware(make_comm, fake_serial):
141
+ fake_serial.feed(_ready_frame(b"3.2.1:uno"))
142
+ comm = make_comm()
143
+ with pytest.raises(FirmwareOutdatedError, match="3.2.2"):
144
+ comm.setup_command(_erase(), MagicMock())
145
+
146
+
147
+ def test_setup_command_accepts_flash4_erase_on_3_2_2(make_comm, fake_serial):
148
+ fake_serial.feed(_ready_frame(b"3.2.2:uno"))
149
+ comm = make_comm()
150
+ assert comm.setup_command(_erase(), MagicMock()) is True
114
151
 
115
152
 
116
153
  def _shipped_algorithm_values(db: EpromDatabase) -> set:
@@ -188,47 +225,6 @@ def test_predicate_matches_real_database_algorithm_5_rows_exactly():
188
225
  )
189
226
 
190
227
 
191
- def _module_source(module) -> str:
192
- path = inspect.getsourcefile(module)
193
- assert path is not None, f"could not resolve a source file for {module!r}"
194
- return Path(path).read_text(encoding="utf-8")
195
-
196
-
197
- FORBIDDEN_REFUSAL_SUBSTRINGS = (
198
- "force",
199
- "firestarter write",
200
- "write",
201
- "workaround",
202
- "route around",
203
- "because",
204
- "reason",
205
- "cause",
206
- "alternative",
207
- "instead",
208
- "try",
209
- )
210
-
211
-
212
- def test_refusal_text_is_one_line_and_carries_no_forbidden_content():
213
- """THE MESSAGE SHAPE: exactly one line, names the chip, and
214
- carries none of a forbidden-substring list covering the cause, the
215
- alternative, and the `--force` forged-identity workaround. A NEGATIVE
216
- assertion on purpose -- it is what stops a later executor quietly
217
- re-adding the cause clause and the alternative that were deliberately
218
- left out of the tool's output.
219
- """
220
- text = refusal_text("ae29f2008")
221
-
222
- assert "\n" not in text
223
- assert text == "Erase not supported for AE29F2008"
224
-
225
- lowered = text.lower()
226
- for forbidden in FORBIDDEN_REFUSAL_SUBSTRINGS:
227
- assert forbidden not in lowered, (
228
- f"refusal text {text!r} contains forbidden substring {forbidden!r}"
229
- )
230
-
231
-
232
228
  def test_cli_erase_on_non_flash4_part_still_reaches_operator():
233
229
  """NOT A BLANKET REFUSAL: a part whose algorithm is not 5 still
234
230
  reaches `EpromOperator.erase_eprom`. Without this leg, a fail-closed