esp-test-utils 0.2.0__tar.gz → 0.2.2__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 (112) hide show
  1. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/.gitignore +3 -0
  2. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/.pre-commit-config.yaml +7 -0
  3. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/CHANGELOG.md +15 -0
  4. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/PKG-INFO +3 -1
  5. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/PKG-INFO +3 -1
  6. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/SOURCES.txt +9 -0
  7. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/requires.txt +2 -0
  8. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/dut_base.py +18 -7
  9. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/esp_mixin.py +6 -1
  10. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/config/env_config.py +37 -7
  11. esp_test_utils-0.2.2/esptest/db/runners.py +199 -0
  12. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/devices/esp_serial.py +11 -0
  13. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/scripts/downbin.py +20 -8
  14. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/tools/download_bin.py +54 -5
  15. esp_test_utils-0.2.2/esptest/tools/http_download.py +61 -0
  16. esp_test_utils-0.2.2/esptest/utility/gen_esp32part.py +808 -0
  17. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/utility/parse_bin_path.py +132 -43
  18. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/pyproject.toml +7 -0
  19. esp_test_utils-0.2.2/tests/conftest.py +0 -0
  20. esp_test_utils-0.2.2/tests/db/test_db_runners.py +66 -0
  21. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/test_Dut.py +1 -1
  22. esp_test_utils-0.2.2/tests/tools/test_download_file.py +44 -0
  23. esp_test_utils-0.2.2/tests/utility/_files/test-bin.zip +0 -0
  24. esp_test_utils-0.2.2/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +80 -0
  25. esp_test_utils-0.2.2/tests/utility/test_parse_bin_path.py +149 -0
  26. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/.github/.gitkeep +0 -0
  27. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/.github/workflows/pypi-publish.yml +0 -0
  28. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/.gitlab-ci.yml +0 -0
  29. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/CONTRIBUTING.md +0 -0
  30. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/LICENSE +0 -0
  31. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/README.md +0 -0
  32. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/docs/Makefile +0 -0
  33. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/docs/conf.py +0 -0
  34. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/docs/index.rst +0 -0
  35. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/docs/make.bat +0 -0
  36. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  37. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/entry_points.txt +0 -0
  38. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esp_test_utils.egg-info/top_level.txt +0 -0
  39. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/__init__.py +0 -0
  40. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/__main__.py +0 -0
  41. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/__init__.py +0 -0
  42. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/__init__.py +0 -0
  43. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/create_dut.py +0 -0
  44. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/esp_dut.py +0 -0
  45. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/esp_port.py +0 -0
  46. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/mac_mixin.py +0 -0
  47. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/dut/wrapper.py +0 -0
  48. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/port/__init__.py +0 -0
  49. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/port/base_port.py +0 -0
  50. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/adapter/port/serial_port.py +0 -0
  51. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/all.py +0 -0
  52. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/__init__.py +0 -0
  53. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/compat_typing.py +0 -0
  54. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/data_monitor.py +0 -0
  55. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/decorators.py +0 -0
  56. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/encoding.py +0 -0
  57. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/generator.py +0 -0
  58. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/shell.py +0 -0
  59. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/common/timestamp.py +0 -0
  60. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/config/__init__.py +0 -0
  61. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/config/default_config.py +0 -0
  62. {esp_test_utils-0.2.0/esptest/devices → esp_test_utils-0.2.2/esptest/db}/__init__.py +0 -0
  63. {esp_test_utils-0.2.0/esptest/env → esp_test_utils-0.2.2/esptest/devices}/__init__.py +0 -0
  64. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/devices/attenuator.py +0 -0
  65. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/devices/serial_dut.py +0 -0
  66. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/devices/serial_tools.py +0 -0
  67. {esp_test_utils-0.2.0/esptest/interface → esp_test_utils-0.2.2/esptest/env}/__init__.py +0 -0
  68. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/env/base_env.py +0 -0
  69. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/env/wifi_env.py +0 -0
  70. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/esp_console/__init__.py +0 -0
  71. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/esp_console/wifi_cmd.py +0 -0
  72. {esp_test_utils-0.2.0/esptest/network → esp_test_utils-0.2.2/esptest/interface}/__init__.py +0 -0
  73. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/interface/dut.py +0 -0
  74. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/interface/port.py +0 -0
  75. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/iperf_utility/__init__.py +0 -0
  76. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/iperf_utility/iperf_results.py +0 -0
  77. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/iperf_utility/iperf_test.py +0 -0
  78. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/iperf_utility/iperf_test.test.py +0 -0
  79. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/iperf_utility/line_chart.py +0 -0
  80. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/logger/__init__.py +0 -0
  81. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/logger/logger.py +0 -0
  82. {esp_test_utils-0.2.0/tests → esp_test_utils-0.2.2/esptest/network}/__init__.py +0 -0
  83. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/network/mac.py +0 -0
  84. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/network/netif.py +0 -0
  85. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/network/nic.py +0 -0
  86. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/scripts/list_ports.py +0 -0
  87. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/scripts/monitor.py +0 -0
  88. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/scripts/set_att.py +0 -0
  89. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/tools/copy_bin.py +0 -0
  90. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/esptest/tools/pip_check.py +0 -0
  91. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/example/jap_test.py +0 -0
  92. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/example/restart_test.py +0 -0
  93. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/setup.cfg +0 -0
  94. /esp_test_utils-0.2.0/tests/conftest.py → /esp_test_utils-0.2.2/tests/__init__.py +0 -0
  95. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/basic/test_decorators.py +0 -0
  96. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/basic/test_network.py +0 -0
  97. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  98. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  99. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/esp_console/conftest.py +0 -0
  100. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/esp_console/test_WifiCmd.py +0 -0
  101. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  102. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  103. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  104. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  105. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/test_chart.py +0 -0
  106. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/test_iperf_results.py +0 -0
  107. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/iperf_utility/test_iperf_util.py +0 -0
  108. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/test_EnvConfig.py +0 -0
  109. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/test_common.py +0 -0
  110. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/test_import.py +0 -0
  111. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tests/tools/test_pip_check.py +0 -0
  112. {esp_test_utils-0.2.0 → esp_test_utils-0.2.2}/tools/ci/check_dev_version.py +0 -0
@@ -14,10 +14,13 @@ gl-codequality.json
14
14
  /dist/
15
15
  *.egg-info/
16
16
 
17
+ # tests
17
18
  /log/
18
19
  /logs/
19
20
  dut_logs/
20
21
  dut.log
22
+ tests/utility/_files/test-bin
23
+
21
24
 
22
25
  # docs
23
26
  /docs/*.rst
@@ -35,6 +35,7 @@ repos:
35
35
  - 'types-psutil'
36
36
  - 'types-PyYAML'
37
37
  - 'typing_extensions'
38
+ - 'sqlalchemy'
38
39
 
39
40
  - repo: https://github.com/espressif/conventional-precommit-linter
40
41
  rev: v1.10.0
@@ -48,3 +49,9 @@ repos:
48
49
  - id: codespell
49
50
  args: ["--write-changes"]
50
51
  additional_dependencies: [tomli]
52
+
53
+ exclude: |
54
+ (
55
+ ^docs/.*
56
+ | gen_esp32part\.py$
57
+ )
@@ -1,3 +1,18 @@
1
+ ## v0.2.2 (2025-10-29)
2
+
3
+
4
+ - feat: get test variables from shell env
5
+ - fix: flash esp32 option no-stub
6
+
7
+ ## v0.2.1 (2025-09-24)
8
+
9
+
10
+ - feat(utility): support to flash bin to encrypted device
11
+ - feat: add runners database
12
+ - feat: add default gen part tool
13
+ - feat(utility): support to check flash enc from bin path
14
+ - fix: get baud from bin path sdkconfig file
15
+
1
16
  ## v0.2.0 (2025-07-16)
2
17
 
3
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,7 +228,9 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: esptool
231
232
  Requires-Dist: packaging
233
+ Requires-Dist: sqlalchemy
232
234
  Requires-Dist: typing_extensions; python_version < "3.11"
233
235
  Provides-Extra: idfci
234
236
  Requires-Dist: pyecharts; extra == "idfci"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,7 +228,9 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: esptool
231
232
  Requires-Dist: packaging
233
+ Requires-Dist: sqlalchemy
232
234
  Requires-Dist: typing_extensions; python_version < "3.11"
233
235
  Provides-Extra: idfci
234
236
  Requires-Dist: pyecharts; extra == "idfci"
@@ -44,6 +44,8 @@ esptest/common/timestamp.py
44
44
  esptest/config/__init__.py
45
45
  esptest/config/default_config.py
46
46
  esptest/config/env_config.py
47
+ esptest/db/__init__.py
48
+ esptest/db/runners.py
47
49
  esptest/devices/__init__.py
48
50
  esptest/devices/attenuator.py
49
51
  esptest/devices/esp_serial.py
@@ -74,7 +76,9 @@ esptest/scripts/monitor.py
74
76
  esptest/scripts/set_att.py
75
77
  esptest/tools/copy_bin.py
76
78
  esptest/tools/download_bin.py
79
+ esptest/tools/http_download.py
77
80
  esptest/tools/pip_check.py
81
+ esptest/utility/gen_esp32part.py
78
82
  esptest/utility/parse_bin_path.py
79
83
  example/jap_test.py
80
84
  example/restart_test.py
@@ -86,6 +90,7 @@ tests/test_common.py
86
90
  tests/test_import.py
87
91
  tests/basic/test_decorators.py
88
92
  tests/basic/test_network.py
93
+ tests/db/test_db_runners.py
89
94
  tests/esp_console/conftest.py
90
95
  tests/esp_console/test_WifiCmd.py
91
96
  tests/esp_console/_files/wifi_cmd_connected_1.log
@@ -97,5 +102,9 @@ tests/iperf_utility/_files/dut_iperf_rx1.log
97
102
  tests/iperf_utility/_files/dut_iperf_rx2.log
98
103
  tests/iperf_utility/_files/pc_iperf_rx.log
99
104
  tests/iperf_utility/_files/pc_iperf_rx2.log
105
+ tests/tools/test_download_file.py
100
106
  tests/tools/test_pip_check.py
107
+ tests/utility/test_parse_bin_path.py
108
+ tests/utility/_files/test-bin.zip
109
+ tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig
101
110
  tools/ci/check_dev_version.py
@@ -3,7 +3,9 @@ pyserial
3
3
  PyYAML
4
4
  pexpect
5
5
  pyusb
6
+ esptool
6
7
  packaging
8
+ sqlalchemy
7
9
 
8
10
  [:python_version < "3.11"]
9
11
  typing_extensions
@@ -11,7 +11,7 @@ from esptool.loader import ESPLoader
11
11
 
12
12
  import esptest.common.compat_typing as t
13
13
 
14
- from ...common.timestamp import timestamp_str
14
+ from ...common.timestamp import timestamp_slug
15
15
  from ...interface.dut import DutInterface
16
16
  from ...interface.port import PortInterface
17
17
  from ...logger import get_logger
@@ -27,7 +27,8 @@ DEFAULT_SERIAL_CONFIGS = {'timeout': 0.005}
27
27
  class DutConfig:
28
28
  name: str = '' # default = dut name / port name
29
29
  device: str = '' # log serial device, eg: '/dev/ttyUSB0', 'COM3', etc.
30
- baudrate: int = 0 # 0: get from bin path or 115200
30
+ baudrate: int = 0 # console baudrate, 0: get from bin path or 115200
31
+ baudrate_from_bin_path: bool = True # always get baudrate from bin path if bin_path is set
31
32
  serial_configs: t.Optional[t.Dict[str, t.Any]] = None # serial configs, eg: {'bytesize': 8, 'timeout': 0.1}
32
33
  # capabilities
33
34
  support_esptool: bool = False # esp port or serial port
@@ -61,7 +62,7 @@ class DutConfig:
61
62
  self._auto_gen_name()
62
63
  if not self.log_file:
63
64
  _log_path = self.log_path or './dut_logs'
64
- _file_name = f'{self.name}_{timestamp_str()}.log'.replace(':', '-')
65
+ _file_name = f'{self.name}_{timestamp_slug()}.log'.replace(':', '-')
65
66
  self.log_file = str(Path(_log_path) / _file_name)
66
67
  # serial configs
67
68
  _serial_configs = DEFAULT_SERIAL_CONFIGS.copy()
@@ -71,10 +72,14 @@ class DutConfig:
71
72
  # bin_path and get variables from bin path
72
73
  if self.bin_path:
73
74
  self.bin_path = Path(self.bin_path).expanduser().resolve()
74
- self.esptool_stub = ParseBinPath(self.bin_path).stub
75
- self.esptool_chip = ParseBinPath(self.bin_path).chip
76
- if not self.baudrate:
77
- self.baudrate = get_baud_from_bin_path(self.bin_path) or 115200
75
+ parsed_bin = ParseBinPath(self.bin_path)
76
+ self.esptool_stub = parsed_bin.stub
77
+ self.esptool_chip = parsed_bin.chip
78
+ if self.baudrate_from_bin_path:
79
+ # baudrate from bin path is much reliable than the specified one for uart0
80
+ self.baudrate = get_baud_from_bin_path(self.bin_path) or self.baudrate or 115200
81
+ if not self.baudrate:
82
+ self.baudrate = 115200 # set default baudrate to 115200
78
83
  # download device
79
84
  if not self.download_device:
80
85
  self.download_device = self.device
@@ -224,6 +229,12 @@ class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-
224
229
  return self._base_port_proxy.raw_port
225
230
  raise NotImplementedError()
226
231
 
232
+ @property
233
+ def log_file(self) -> t.Any:
234
+ if self._base_port_proxy:
235
+ return self._base_port_proxy.log_file
236
+ return None
237
+
227
238
  @property
228
239
  def name(self) -> t.Any:
229
240
  return self.dut_config.name
@@ -95,7 +95,7 @@ class EspMixin(BaseProtocol):
95
95
  esptool=self.dut_config.use_esptool,
96
96
  erase_nvs=erase_nvs,
97
97
  )
98
- if not self.esp.IS_STUB:
98
+ if not self.esp.IS_STUB and self.esp.CHIP_NAME not in ['ESP32']:
99
99
  # preview or dev targets
100
100
  down_bin_tool.force_no_stub = True
101
101
  with self.disable_redirect_thread():
@@ -105,6 +105,11 @@ class EspMixin(BaseProtocol):
105
105
  def start_redirect_thread(self) -> None:
106
106
  if self.esp:
107
107
  self.esp._port.open() # pylint: disable=protected-access
108
+ if self.log_file:
109
+ with open(self.log_file, 'a', encoding='utf-8') as log_f:
110
+ log_f.write(
111
+ f'------------ reopen port: {self.esp._port.port} {self.esp._port.baudrate} --------------- \n' # pylint: disable=protected-access
112
+ )
108
113
  super().start_redirect_thread()
109
114
 
110
115
  def stop_redirect_thread(self) -> bool:
@@ -5,6 +5,30 @@ from typing import Any, List, Optional
5
5
 
6
6
  import yaml
7
7
 
8
+ VAR_NAME_MAPPING = {
9
+ 'ap_ssid': ['RUNNER_WIFI_SSID', 'RUNNER_AP_SSID'],
10
+ 'ap_password': ['RUNNER_WIFI_PASSWORD', 'RUNNER_AP_PASSWORD'],
11
+ 'pc_nic': ['RUNNER_PC_NIC'],
12
+ 'dut1': ['ESPPORT1'],
13
+ 'dut2': ['ESPPORT2'],
14
+ 'dut3': ['ESPPORT3'],
15
+ }
16
+
17
+
18
+ def get_variable_from_env(key: str) -> Any:
19
+ """Get test variable from shell environment
20
+
21
+ Args:
22
+ key (str): which variable to get
23
+ """
24
+ if key in VAR_NAME_MAPPING:
25
+ for var_name in VAR_NAME_MAPPING[key]:
26
+ var = os.getenv(var_name)
27
+ if var is not None:
28
+ logging.debug(f'Got env variable from shell env {var_name}: {var}')
29
+ return var
30
+ return None
31
+
8
32
 
9
33
  class EnvConfig:
10
34
  """Get test environment variables from config file.
@@ -32,6 +56,8 @@ class EnvConfig:
32
56
  # Find env config file from project root path
33
57
  # CI_PROJECT_DIR was set by gitlab CI
34
58
  PROJECT_ROOT_DIR = os.getenv('PROJECT_ROOT_DIR') or os.getenv('CI_PROJECT_DIR', '')
59
+ # allow EnvConfig load shell env variables, default enabled
60
+ DISABLE_LOAD_SHELL_ENV = os.getenv('ESPTEST_DISABLE_LOAD_SHELL_ENV', '').lower() in ('true', '1', 'yes', 'y')
35
61
 
36
62
  # Allow input variables from terminal during local debugging
37
63
  ALLOW_INPUT = not os.getenv('CI')
@@ -115,14 +141,18 @@ class EnvConfig:
115
141
  # do not use dict.get because we can input the variable for local tests
116
142
  if key in self.config_data:
117
143
  var = self.config_data[key]
118
- elif default:
144
+ elif default is not None:
119
145
  var = default
120
146
  else:
121
- logging.warning(f'Failed to get env variable {self.env_tag}/{key}.')
122
- logging.info(self.__doc__)
123
- if not self.ALLOW_INPUT:
124
- raise ValueError(f'Env variable not found: {self.env_tag}/{key}')
125
- # For local test, support input the variable from console
126
- var = input('You can input the variable now:')
147
+ if not self.DISABLE_LOAD_SHELL_ENV:
148
+ # Try to get from shell environment variables
149
+ var = get_variable_from_env(key)
150
+ if var is None:
151
+ logging.warning(f'Failed to get env variable {self.env_tag}/{key}.')
152
+ logging.info(self.__doc__)
153
+ if not self.ALLOW_INPUT:
154
+ raise ValueError(f'Env variable not found: {self.env_tag}/{key}')
155
+ # For local test, support input the variable from console
156
+ var = input('You can input the variable now:')
127
157
  logging.debug(f'Got env variable {self.env_tag}/{key}: {var}')
128
158
  return var
@@ -0,0 +1,199 @@
1
+ import contextlib
2
+ import csv
3
+ from typing import Any, Dict, Generator, List, Optional
4
+
5
+ from sqlalchemy import JSON, Engine, ForeignKey, Integer, String
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+
13
+ class User(Base):
14
+ __tablename__ = 'user'
15
+
16
+ id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
17
+ name: Mapped[str] = mapped_column(String(50), unique=True)
18
+ email: Mapped[Optional[str]] = mapped_column(String(50))
19
+ runners: Mapped[List['Runner']] = relationship(back_populates='user')
20
+
21
+
22
+ class Runner(Base):
23
+ __tablename__ = 'runner'
24
+
25
+ id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True)
26
+ mac: Mapped[str] = mapped_column(String(50), unique=True)
27
+ ip: Mapped[str] = mapped_column(String(100), unique=True)
28
+ vlan: Mapped[Optional[int]] = mapped_column(Integer)
29
+ switch_info: Mapped[Optional[str]] = mapped_column(String) # ip-port
30
+ name: Mapped[str] = mapped_column(String(50))
31
+ owner: Mapped[Optional[str]] = mapped_column(ForeignKey('user.name'), nullable=True)
32
+ tags: Mapped[Optional[List[str]]] = mapped_column(JSON) # only PostgreSQL supports ARRAY(String)
33
+ raw_data: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON)
34
+ description: Mapped[Optional[str]] = mapped_column(String)
35
+ # Link to user table
36
+ user: Mapped[User] = relationship(back_populates='runners')
37
+
38
+ def __str__(self) -> str:
39
+ tags_str = ','.join(self.tags) if self.tags else ''
40
+ return (
41
+ f'Runner('
42
+ f'id={self.id},'
43
+ f'mac={self.mac},'
44
+ f'ip={self.ip},'
45
+ f'vlan={self.vlan},'
46
+ f'switch_info={self.switch_info},'
47
+ f'name={self.name},'
48
+ f'owner={self.owner},'
49
+ f'tags="{tags_str}",'
50
+ f'raw_data={self.raw_data},'
51
+ f'description="{self.description}"'
52
+ ')'
53
+ )
54
+
55
+
56
+ class RunnerDB:
57
+ def __init__(self, engine: Engine) -> None:
58
+ self.engine = engine
59
+ self._session: Optional[Session] = None
60
+ self.__init_db()
61
+
62
+ def __init_db(self) -> None:
63
+ Base.metadata.create_all(self.engine)
64
+
65
+ @contextlib.contextmanager
66
+ def open_session(self, *args: Any, **kwargs: Any) -> Generator['DBSession', None, None]:
67
+ session = DBSession(self, *args, **kwargs)
68
+ yield session
69
+ try:
70
+ session.commit()
71
+ except:
72
+ session.rollback()
73
+ raise
74
+ finally:
75
+ session.close()
76
+
77
+ def to_csv(self, csv_file: str) -> None:
78
+ """
79
+ Export the runners to a CSV file.
80
+
81
+ Args:
82
+ csv_file: The path to the CSV file.
83
+ session: The session to use. If None, use the default session.
84
+ """
85
+ with self.open_session() as session:
86
+ runners = session.all_runners()
87
+ fields = [c.name for c in Runner.__table__.columns]
88
+ with open(csv_file, 'w', encoding='utf-8') as f:
89
+ writer = csv.writer(f)
90
+ writer.writerow(fields)
91
+ for runner in runners:
92
+ writer.writerow([getattr(runner, f) for f in fields])
93
+
94
+
95
+ class DBSession(Session):
96
+ def __init__(self, runner_db: RunnerDB, *args: Any, **kwargs: Any) -> None:
97
+ self.runner_db = runner_db
98
+ super().__init__(self.runner_db.engine, *args, **kwargs)
99
+
100
+ def all_runners(self) -> List[Runner]:
101
+ return self.query(Runner).outerjoin(Runner.user).all()
102
+
103
+ def all_users(self) -> List[User]:
104
+ return self.query(User).outerjoin(User.runners).all()
105
+
106
+ def get_runners_by_mac(self, mac: str) -> List[Runner]:
107
+ return self.query(Runner).filter(Runner.mac == mac).all()
108
+
109
+ def get_runners_by_ip(self, ip: str) -> List[Runner]:
110
+ return self.query(Runner).filter(Runner.ip == ip).all()
111
+
112
+ def get_users_by_name(self, name: str) -> List[User]:
113
+ return self.query(User).outerjoin(Runner.user).filter(User.name == name).all()
114
+
115
+ def get_users_by_email(self, email: str) -> List[User]:
116
+ return self.query(User).outerjoin(Runner.user).filter(User.email == email).all()
117
+
118
+ def remove_runner(self, runner: Runner) -> None:
119
+ """
120
+ Remove a runner.
121
+
122
+ Args:
123
+ runner: The runner to remove.
124
+ session: The session to use. If None, use the default session.
125
+ """
126
+ # session.merge(runner)
127
+ self.delete(runner)
128
+ self.flush()
129
+
130
+ def add_or_update_runner(self, runner: Runner) -> int:
131
+ """
132
+ Add or update a runner.
133
+
134
+ Args:
135
+ runner: The runner to add or update.
136
+ session: The session to use. If None, use the default session.
137
+ """
138
+
139
+ if runner.id is not None:
140
+ self.merge(runner)
141
+ else:
142
+ if self.get_runners_by_mac(runner.mac) or self.get_runners_by_ip(runner.ip):
143
+ raise ValueError(f'Runner with mac {runner.mac} or ip {runner.ip} already exists')
144
+ self.add(runner)
145
+ self.flush()
146
+ return runner.id
147
+
148
+ def add_or_update_user(self, user: User) -> int:
149
+ """
150
+ Add or update an user.
151
+
152
+ Args:
153
+ user: The user to add or update.
154
+ session: The session to use. If None, use the default session.
155
+ """
156
+ if user.id is not None:
157
+ self.merge(user)
158
+ else:
159
+ if self.get_users_by_name(user.name):
160
+ raise ValueError(f'User with name {user.name} already exists')
161
+ self.add(user)
162
+ self.flush()
163
+ return user.id
164
+
165
+
166
+ if __name__ == '__main__':
167
+ from sqlalchemy import create_engine
168
+
169
+ engine1 = create_engine('sqlite:///test.db')
170
+ # engine = create_engine("mysql+pymysql://user:pass@localhost/testdb")
171
+ db1 = RunnerDB(engine1)
172
+ import logging
173
+
174
+ logging.basicConfig(level=logging.DEBUG)
175
+
176
+ with db1.open_session() as session1:
177
+ for i in range(5):
178
+ runner1 = Runner(
179
+ # id=i+1,
180
+ mac=f'11:22:33:44:55:6{i}',
181
+ ip=f'192.168.1.{i}',
182
+ owner=f'test{i // 2}',
183
+ name=f'runner00{i}',
184
+ description=f'test runner 00{i} update 2',
185
+ tags=['esp32', 'generic', 'eco4'],
186
+ raw_data={'envs': {'esp32': [f'wifi_iperf_{i}']}, 'os': 'linux'},
187
+ )
188
+ session1.add_or_update_runner(runner1)
189
+ new_user = User(
190
+ # id=1,
191
+ name='test0'
192
+ )
193
+ session1.add_or_update_user(new_user)
194
+ session1.commit()
195
+ users = session1.query(User).outerjoin(User.runners).all()
196
+ print(users)
197
+ all_runners = session1.all_runners()
198
+ print(','.join([str(r) for r in all_runners]))
199
+ db1.to_csv('runners.csv')
@@ -84,3 +84,14 @@ def list_all_esp_ports() -> t.List[EspPortInfo]:
84
84
  for port in get_all_serial_ports():
85
85
  esp_ports.append(detect_one_port(port))
86
86
  return esp_ports
87
+
88
+
89
+ def get_available_ports(target: str, max_num: int = 0) -> t.List[EspPortInfo]:
90
+ detect_ports = []
91
+ for port in get_all_serial_ports():
92
+ esp_port = detect_one_port(port)
93
+ if esp_port.target == target:
94
+ detect_ports.append(esp_port)
95
+ if max_num > 0 and len(detect_ports) >= max_num: # pylint: disable=chained-comparison
96
+ return detect_ports
97
+ return detect_ports
@@ -2,26 +2,34 @@ import argparse
2
2
  import logging
3
3
  import os
4
4
  import re
5
+ import sys
5
6
 
6
7
  from esptest.devices.serial_tools import get_all_serial_ports
7
- from esptest.tools.download_bin import download_bin_to_ports
8
+ from esptest.tools.download_bin import bin_path_to_dir, download_bin_to_ports
8
9
 
9
10
 
10
11
  def main() -> None:
11
- parser = argparse.ArgumentParser(description='Download bin')
12
+ usage_string = '%(prog)s [bin_path] [options]'
13
+ parser = argparse.ArgumentParser(description='Download bin', usage=usage_string)
12
14
  parser.add_argument('bin_path', type=str, nargs='?', help='esp bin path, default ./build')
13
15
  parser.add_argument('-p', '--ports', type=str, nargs='*', help='download port list')
14
16
  parser.add_argument(
15
17
  '--range', type=str, help='port list from range (linux), eg: "0-10" equals to "-p ttyUSB0 ttyUSB1 ... ttyUSB10"'
16
18
  )
17
- parser.add_argument('--all', action='store_true', help='download to all serial ports.')
18
- parser.add_argument('--erase-nvs', type=bool, default=True, help='use --erase-nvs=n to skip erase nvs')
19
- parser.add_argument('--max-workers', type=int, default=True, help='max download threads')
19
+ parser.add_argument(
20
+ '--all', action='store_true', help='download to all serial ports, ignored if "-p/--ports" is specified.'
21
+ )
22
+ parser.add_argument('--no-erase-nvs', dest='erase_nvs', action='store_false', help='skip erase nvs')
23
+ parser.add_argument('--max-workers', type=int, default=0, help='max download threads')
20
24
  args = parser.parse_args()
21
25
 
22
26
  bin_path = args.bin_path or './build'
23
27
  if not os.path.isdir(bin_path):
24
- raise ValueError(f'Can not find bin_path: {bin_path}')
28
+ try:
29
+ bin_path = bin_path_to_dir(bin_path)
30
+ except Exception as e: # pylint: disable=broad-except
31
+ logging.exception(f'Invalid bin path {bin_path} : {str(e)}')
32
+ sys.exit(1)
25
33
 
26
34
  ports = []
27
35
  if args.ports:
@@ -37,9 +45,13 @@ def main() -> None:
37
45
  ports = [os.getenv('ESPPORT') or '/dev/ttyUSB0']
38
46
  assert isinstance(ports, list)
39
47
 
40
- logging.critical(f'Download {bin_path} to {ports}')
41
- download_bin_to_ports(bin_path, ports, args.erase_nvs, args.max_workers)
48
+ try:
49
+ download_bin_to_ports(bin_path, ports, args.erase_nvs, args.max_workers)
50
+ except RuntimeError as e:
51
+ logging.error(str(e))
52
+ sys.exit(1)
42
53
 
43
54
 
44
55
  if __name__ == '__main__':
56
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
45
57
  main()
@@ -1,13 +1,18 @@
1
1
  import asyncio
2
2
  import concurrent
3
3
  import concurrent.futures
4
+ import os
5
+ import re
4
6
  import subprocess
7
+ import tempfile
8
+ import zipfile
5
9
  from asyncio.events import AbstractEventLoop
6
10
  from functools import lru_cache, partial
7
11
 
8
12
  import esptest.common.compat_typing as t
9
13
  from esptest.devices.serial_tools import compute_serial_port
10
14
  from esptest.logger import get_logger
15
+ from esptest.tools.http_download import download_file
11
16
  from esptest.utility.parse_bin_path import ParseBinPath
12
17
 
13
18
  logger = get_logger('download_bin')
@@ -18,6 +23,32 @@ def _get_bin_parser(bin_path: str, parttool: str) -> ParseBinPath:
18
23
  return ParseBinPath(bin_path, parttool)
19
24
 
20
25
 
26
+ @lru_cache()
27
+ def _tmp_dir() -> str:
28
+ return tempfile.mkdtemp()
29
+
30
+
31
+ @lru_cache()
32
+ def bin_path_to_dir(bin_path: str) -> str:
33
+ bin_hash = hash(bin_path)
34
+ bin_hash_name = os.path.basename(bin_path)
35
+ if bin_path.startswith('http'):
36
+ assert bin_path.endswith('.zip') # for now only support zip from url
37
+ new_bin_path = os.path.join(_tmp_dir(), f'{bin_hash}', bin_hash_name)
38
+ os.makedirs(os.path.dirname(new_bin_path), exist_ok=True)
39
+ download_file(bin_path, new_bin_path)
40
+ bin_path = new_bin_path
41
+ if bin_path.endswith('.zip'):
42
+ new_bin_path = os.path.join(_tmp_dir(), f'{bin_hash}', bin_hash_name.removesuffix('.zip'))
43
+ os.makedirs(new_bin_path, exist_ok=True)
44
+ with zipfile.ZipFile(bin_path, 'r') as zip_ref:
45
+ zip_ref.extractall(new_bin_path)
46
+ bin_path = new_bin_path
47
+ if 'partition_table' not in os.listdir(bin_path):
48
+ logger.warning('Can not find partition_table from bin_path, maybe invalid!')
49
+ return bin_path
50
+
51
+
21
52
  def _filter_esptool_log(log: str) -> str:
22
53
  lines = log.splitlines(keepends=True)
23
54
  new_log = ''
@@ -36,6 +67,7 @@ def _filter_esptool_log(log: str) -> str:
36
67
  class DownBinTool:
37
68
  # RETRY_CNT = 2
38
69
  DEFAULT_BAUD_LIST = [921600, 460800]
70
+ FLASH_CRYPT_CNT_PATTERN = re.compile(r'(?:FLASH_CRYPT_CNT|SPI_BOOT_CRYPT_CNT).*\(0b([01]+)')
39
71
 
40
72
  def __init__(
41
73
  self,
@@ -45,7 +77,7 @@ class DownBinTool:
45
77
  parttool: str = '',
46
78
  esptool: str = '',
47
79
  erase_nvs: bool = True,
48
- force_no_stub: bool = True,
80
+ force_no_stub: bool = False,
49
81
  ): # pylint: disable=too-many-positional-arguments,too-many-arguments
50
82
  self.bin_path = bin_path
51
83
  self.port = compute_serial_port(port, strict=True)
@@ -54,11 +86,29 @@ class DownBinTool:
54
86
  else:
55
87
  self.baud_list = baud
56
88
  self.esptool = esptool or 'python -m esptool'
89
+ self.espefuse = self.esptool.replace('esptool', 'espefuse')
57
90
  self.erase_nvs = erase_nvs
58
91
  self.bin_parser = _get_bin_parser(bin_path, parttool)
59
92
  self.force_no_stub = force_no_stub
60
93
 
94
+ def check_flash_encrypted(self, efuse_summary: str) -> bool:
95
+ match = self.FLASH_CRYPT_CNT_PATTERN.search(efuse_summary)
96
+ if match:
97
+ return match.group(1).count('1') % 2 == 1
98
+ return False
99
+
61
100
  def download(self) -> None:
101
+ efuse_cmd = self.espefuse.split()
102
+ try:
103
+ summary = subprocess.check_output(
104
+ efuse_cmd + ['--port', self.port, 'summary'], stderr=subprocess.STDOUT, text=True
105
+ )
106
+ except subprocess.CalledProcessError as err:
107
+ logger.error(err.output)
108
+ raise RuntimeError(f'Failed to get efuse information from {self.port}') from err
109
+
110
+ enc_indicator = ' [encrypted]' if self.check_flash_encrypted(summary) else ''
111
+
62
112
  download_log = ''
63
113
  for baud in self.baud_list:
64
114
  args = self.esptool.split()
@@ -66,19 +116,18 @@ class DownBinTool:
66
116
  args += ['--no-stub']
67
117
  args += ['-p', self.port]
68
118
  args += ['-b', f'{baud}']
69
- args += self.bin_parser.flash_bin_args(erase_nvs=self.erase_nvs)
119
+ args += self.bin_parser.flash_bin_args(erase_nvs=self.erase_nvs, encrypted=bool(enc_indicator))
70
120
 
71
- logger.critical(f'Downloading {self.port}@{baud}: {self.bin_path}')
121
+ logger.info(f'Downloading {self.port}@{baud}{enc_indicator}: {self.bin_path}')
72
122
  # get return code rather than check
73
123
  ret = subprocess.run(args, capture_output=True, text=True, check=False)
74
124
  if ret.returncode == 0:
75
125
  return # succeed
76
126
  # failed
77
- download_log = f'esptool cmd failed ({ret.returncode}): ' + ' '.join(args)
127
+ download_log += f'esptool cmd failed ({ret.returncode}): ' + ' '.join(args)
78
128
  download_log += f'\nDownload failed: [{self.port}@{baud}]\n'
79
129
  esptool_msg = ret.stdout + ret.stderr
80
130
  download_log += f'esptool output: {_filter_esptool_log(esptool_msg)}'
81
- logger.debug(download_log)
82
131
  logger.error(download_log)
83
132
  raise RuntimeError(f'Failed to download Bin to {self.port}')
84
133