esphome/tests/unit_tests/test_wizard.py

451 lines
12 KiB
Python
Raw Permalink Normal View History

"""Tests for the wizard.py file."""
import os
2020-07-23 23:51:14 +02:00
import esphome.wizard as wz
import pytest
from esphome.core import CORE
ESP-IDF support and generic target platforms (#2303) * Socket refactor and SSL * esp-idf temp * Fixes * Echo component and noise * Add noise API transport support * Updates * ESP-IDF * Complete * Fixes * Fixes * Versions update * New i2c APIs * Complete i2c refactor * SPI migration * Revert ESP Preferences migration, too complex for now * OTA support * Remove echo again * Remove ssl again * GPIOFlags updates * Rename esphal and ICACHE_RAM_ATTR * Make ESP32 arduino compilable again * Fix GPIO flags * Complete pin registry refactor and fixes * Fixes to make test1 compile * Remove sdkconfig file * Ignore sdkconfig file * Fixes in reviewing * Make test2 compile * Make test4 compile * Make test5 compile * Run clang-format * Fix lint errors * Use esp-idf APIs instead of btStart * Another round of fixes * Start implementing ESP8266 * Make test3 compile * Guard esp8266 code * Lint * Reformat * Fixes * Fixes v2 * more fixes * ESP-IDF tidy target * Convert ARDUINO_ARCH_ESPxx * Update WiFiSignalSensor * Update time ifdefs * OTA needs millis from hal * RestartSwitch needs delay from hal * ESP-IDF Uart * Fix OTA blank password * Allow setting sdkconfig * Fix idf partitions and allow setting sdkconfig from yaml * Re-add read/write compat APIs and fix esp8266 uart * Fix esp8266 store log strings in flash * Fix ESP32 arduino preferences not initialized * Update ifdefs * Change how sdkconfig change is detected * Add checks to ci-custom and fix them * Run clang-format * Add esp-idf clang-tidy target and fix errors * Fixes from clang-tidy idf round 2 * Fixes from compiling tests with esp-idf * Run clang-format * Switch test5.yaml to esp-idf * Implement ESP8266 Preferences * Lint * Re-do PIO package version selection a bit * Fix arduinoespressif32 package version * Fix unit tests * Lint * Lint fixes * Fix readv/writev not defined * Fix graphing component * Re-add all old options from core/config.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2021-09-20 11:47:51 +02:00
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
from esphome.components.esp32.boards import ESP32_BOARD_PINS
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
from unittest.mock import MagicMock
2020-07-23 23:51:14 +02:00
@pytest.fixture
def default_config():
return {
2021-04-08 14:26:01 +02:00
"name": "test-name",
"platform": "ESP8266",
"board": "esp01_1m",
2020-07-23 23:51:14 +02:00
"ssid": "test_ssid",
"psk": "test_psk",
"password": "",
2020-07-23 23:51:14 +02:00
}
@pytest.fixture
def wizard_answers():
return [
2021-04-08 14:26:01 +02:00
"test-node", # Name of the node
2020-07-23 23:51:14 +02:00
"ESP8266", # platform
"nodemcuv2", # board
"SSID", # ssid
"psk", # wifi password
"ota_pass", # ota password
]
def test_sanitize_quotes_replaces_with_escaped_char():
"""
The sanitize_quotes function should replace double quotes with their escaped equivalents
"""
# Given
input_str = '"key": "value"'
2020-07-23 23:51:14 +02:00
# When
output_str = wz.sanitize_double_quotes(input_str)
# Then
assert output_str == '\\"key\\": \\"value\\"'
2020-07-23 23:51:14 +02:00
def test_config_file_fallback_ap_includes_descriptive_name(default_config):
"""
The fallback AP should include the node and a descriptive name
"""
# Given
default_config["name"] = "test_node"
# When
config = wz.wizard_file(**default_config)
# Then
assert 'ssid: "Test Node Fallback Hotspot"' in config
2020-07-23 23:51:14 +02:00
def test_config_file_fallback_ap_name_less_than_32_chars(default_config):
"""
The fallback AP name must be less than 32 chars.
Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating
"""
# Given
default_config["name"] = "a_very_long_name_for_this_node"
# When
config = wz.wizard_file(**default_config)
# Then
assert 'ssid: "A Very Long Name For This Node"' in config
2020-07-23 23:51:14 +02:00
def test_config_file_should_include_ota(default_config):
"""
The Over-The-Air update should be enabled by default
"""
# Given
# When
config = wz.wizard_file(**default_config)
# Then
assert "ota:" in config
def test_config_file_should_include_ota_when_password_set(default_config):
"""
The Over-The-Air update should be enabled when a password is set
"""
# Given
default_config["password"] = "foo"
# When
config = wz.wizard_file(**default_config)
# Then
assert "ota:" in config
def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
"""
If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards
"""
# Given
del default_config["platform"]
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
2020-07-23 23:51:14 +02:00
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "esp8266:" in generated_config
2020-07-23 23:51:14 +02:00
def test_wizard_write_defaults_platform_from_board_esp8266(
default_config, tmp_path, monkeypatch
):
2020-07-23 23:51:14 +02:00
"""
If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards
"""
# Given
del default_config["platform"]
default_config["board"] = [*ESP8266_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
2020-07-23 23:51:14 +02:00
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "esp8266:" in generated_config
2020-07-23 23:51:14 +02:00
def test_wizard_write_defaults_platform_from_board_esp32(
default_config, tmp_path, monkeypatch
):
2020-07-23 23:51:14 +02:00
"""
If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards
2020-07-23 23:51:14 +02:00
"""
# Given
del default_config["platform"]
default_config["board"] = [*ESP32_BOARD_PINS][0]
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
2020-07-23 23:51:14 +02:00
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "esp32:" in generated_config
2020-07-23 23:51:14 +02:00
def test_wizard_write_defaults_platform_from_board_bk72xx(
default_config, tmp_path, monkeypatch
):
"""
If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards
"""
# Given
del default_config["platform"]
default_config["board"] = [*BK72XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "bk72xx:" in generated_config
def test_wizard_write_defaults_platform_from_board_rtl87xx(
default_config, tmp_path, monkeypatch
):
"""
If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards
"""
# Given
del default_config["platform"]
default_config["board"] = [*RTL87XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "rtl87xx:" in generated_config
2020-07-23 23:51:14 +02:00
def test_safe_print_step_prints_step_number_and_description(monkeypatch):
"""
The safe_print_step function prints the step number and the passed description
"""
# Given
monkeypatch.setattr(wz, "safe_print", MagicMock())
monkeypatch.setattr(wz, "sleep", lambda time: 0)
step_num = 22
step_desc = "foobartest"
# When
wz.safe_print_step(step_num, step_desc)
# Then
# Collect arguments to all safe_print() calls (substituting "" for any empty ones)
all_args = [
call.args[0] if len(call.args) else "" for call in wz.safe_print.call_args_list
]
2020-07-23 23:51:14 +02:00
assert any(step_desc == arg for arg in all_args)
assert any(f"STEP {step_num}" in arg for arg in all_args)
def test_default_input_uses_default_if_no_input_supplied(monkeypatch):
"""
The default_input() function should return the supplied default value if the user doesn't enter anything
"""
# Given
monkeypatch.setattr("builtins.input", lambda _=None: "")
2020-07-23 23:51:14 +02:00
default_string = "foobar"
# When
retval = wz.default_input("", default_string)
# Then
assert retval == default_string
def test_default_input_uses_user_supplied_value(monkeypatch):
"""
The default_input() function should return the value that the user enters
"""
# Given
user_input = "A value"
monkeypatch.setattr("builtins.input", lambda _=None: user_input)
2020-07-23 23:51:14 +02:00
default_string = "foobar"
# When
retval = wz.default_input("", default_string)
# Then
assert retval == user_input
def test_strip_accents_removes_diacritics():
"""
The strip_accents() function should remove diacritics (umlauts)
"""
# Given
input_str = "Kühne"
2020-07-23 23:51:14 +02:00
expected_str = "Kuhne"
# When
output_str = wz.strip_accents(input_str)
# Then
assert output_str == expected_str
def test_wizard_rejects_path_with_invalid_extension():
"""
The wizard should reject config files that are not yaml
"""
# Given
config_file = "test.json"
# When
retval = wz.wizard(config_file)
# Then
assert retval == 1
def test_wizard_rejects_existing_files(tmpdir):
"""
The wizard should reject any configuration file that already exists
"""
# Given
config_file = tmpdir.join("test.yaml")
config_file.write("")
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 2
def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers):
"""
The wizard should accept the given default answers for esp8266
"""
# Given
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0
def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers):
"""
The wizard should accept the given default answers for esp32
"""
# Given
wizard_answers[1] = "ESP32"
wizard_answers[2] = "nodemcu-32s"
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0
def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers):
"""
When the node name does not conform, a better alternative is offered
* Removes special chars
2021-04-08 14:26:01 +02:00
* Replaces spaces with hyphens
* Replaces underscores with hyphens
2020-07-23 23:51:14 +02:00
* Converts all uppercase letters to lowercase
"""
# Given
2021-04-08 14:26:01 +02:00
wizard_answers[0] = "Küche_Unten #2"
expected_name = "kuche-unten-2"
monkeypatch.setattr(
wz, "default_input", MagicMock(side_effect=lambda _, default: default)
)
2020-07-23 23:51:14 +02:00
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0
assert wz.default_input.call_args.args[1] == expected_name
def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers):
"""
When the platform is not either esp32 or esp8266, the wizard should reject it
"""
# Given
wizard_answers.insert(1, "foobar") # add invalid entry for platform
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0
def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers):
"""
When the board is not a valid esp8266 board, the wizard should reject it
"""
# Given
wizard_answers.insert(2, "foobar") # add an invalid entry for board
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0
def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers):
"""
When the board is not a valid esp8266 board, the wizard should reject it
"""
# Given
wizard_answers.insert(3, "") # add an invalid entry for ssid
config_file = tmpdir.join("test.yaml")
input_mock = MagicMock(side_effect=wizard_answers)
monkeypatch.setattr("builtins.input", input_mock)
monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0)
2020-07-23 23:51:14 +02:00
monkeypatch.setattr(wz, "sleep", lambda _: 0)
monkeypatch.setattr(wz, "wizard_write", MagicMock())
2020-07-23 23:51:14 +02:00
# When
retval = wz.wizard(str(config_file))
# Then
assert retval == 0