mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +01:00
Feature/wizard tests (#1167)
This commit is contained in:
parent
a5e1f8fe19
commit
ebbfab608c
3 changed files with 394 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -54,6 +54,7 @@ htmlcov/
|
||||||
.esphome
|
.esphome
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
cov.xml
|
||||||
*.cover
|
*.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
|
@ -142,7 +142,7 @@ def wizard(path):
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
safe_print("Uh oh, it seems like {} already exists, please delete that file first "
|
safe_print("Uh oh, it seems like {} already exists, please delete that file first "
|
||||||
"or chose another configuration file.".format(color('cyan', path)))
|
"or chose another configuration file.".format(color('cyan', path)))
|
||||||
return 1
|
return 2
|
||||||
safe_print("Hi there!")
|
safe_print("Hi there!")
|
||||||
sleep(1.5)
|
sleep(1.5)
|
||||||
safe_print("I'm the wizard of ESPHome :)")
|
safe_print("I'm the wizard of ESPHome :)")
|
||||||
|
@ -162,14 +162,15 @@ def wizard(path):
|
||||||
safe_print()
|
safe_print()
|
||||||
sleep(1)
|
sleep(1)
|
||||||
name = input(color("bold_white", "(name): "))
|
name = input(color("bold_white", "(name): "))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
name = cv.valid_name(name)
|
name = cv.valid_name(name)
|
||||||
break
|
break
|
||||||
except vol.Invalid:
|
except vol.Invalid:
|
||||||
safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include "
|
safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include "
|
||||||
"numbers, letters and underscores.".format(name)))
|
"numbers, lower-case letters and underscores.".format(name)))
|
||||||
name = strip_accents(name).replace(' ', '_')
|
name = strip_accents(name).lower().replace(' ', '_')
|
||||||
name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS)
|
name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS)
|
||||||
safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name)))
|
safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name)))
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
|
|
389
tests/unit_tests/test_wizard.py
Normal file
389
tests/unit_tests/test_wizard.py
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
""" Tests for the wizard.py file """
|
||||||
|
|
||||||
|
import esphome.wizard as wz
|
||||||
|
import pytest
|
||||||
|
from esphome.pins import ESP8266_BOARD_PINS
|
||||||
|
from mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def default_config():
|
||||||
|
return {
|
||||||
|
"name": "test_name",
|
||||||
|
"platform": "test_platform",
|
||||||
|
"board": "test_board",
|
||||||
|
"ssid": "test_ssid",
|
||||||
|
"psk": "test_psk",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wizard_answers():
|
||||||
|
return [
|
||||||
|
"test_node", # Name of the node
|
||||||
|
"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\""
|
||||||
|
|
||||||
|
# When
|
||||||
|
output_str = wz.sanitize_double_quotes(input_str)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert output_str == "\\\"key\\\": \\\"value\\\""
|
||||||
|
|
||||||
|
|
||||||
|
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 f"ssid: \"Test Node Fallback Hotspot\"" in config
|
||||||
|
|
||||||
|
|
||||||
|
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 f"ssid: \"A Very Long Name For This Node\"" in config
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
monkeypatch.setattr(wz, "write_file", MagicMock())
|
||||||
|
|
||||||
|
# When
|
||||||
|
wz.wizard_write(tmp_path, **default_config)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
generated_config = wz.write_file.call_args.args[1]
|
||||||
|
assert f"platform: {default_config['platform']}" in generated_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_wizard_write_defaults_platform_from_board_esp8266(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"]
|
||||||
|
default_config["board"] = [*ESP8266_BOARD_PINS][0]
|
||||||
|
|
||||||
|
monkeypatch.setattr(wz, "write_file", MagicMock())
|
||||||
|
|
||||||
|
# When
|
||||||
|
wz.wizard_write(tmp_path, **default_config)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
generated_config = wz.write_file.call_args.args[1]
|
||||||
|
assert "platform: ESP8266" in generated_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_wizard_write_defaults_platform_from_board_esp32(default_config, tmp_path, monkeypatch):
|
||||||
|
"""
|
||||||
|
If the platform is not explicitly set, use "ESP32" if the board is not one of the ESP8266 boards
|
||||||
|
"""
|
||||||
|
# Given
|
||||||
|
del default_config["platform"]
|
||||||
|
default_config["board"] = "foo"
|
||||||
|
|
||||||
|
monkeypatch.setattr(wz, "write_file", MagicMock())
|
||||||
|
|
||||||
|
# When
|
||||||
|
wz.wizard_write(tmp_path, **default_config)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
generated_config = wz.write_file.call_args.args[1]
|
||||||
|
assert "platform: ESP32" in generated_config
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
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 _: "")
|
||||||
|
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 _: user_input)
|
||||||
|
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 = u"Kühne"
|
||||||
|
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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# 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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# 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
|
||||||
|
* Replaces spaces with underscores
|
||||||
|
* Converts all uppercase letters to lowercase
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Given
|
||||||
|
wizard_answers[0] = "Küche #2"
|
||||||
|
expected_name = "kuche_2"
|
||||||
|
monkeypatch.setattr(wz, "default_input", MagicMock(side_effect=lambda _, default: default))
|
||||||
|
|
||||||
|
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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# 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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# 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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# 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: 0)
|
||||||
|
monkeypatch.setattr(wz, "sleep", lambda _: 0)
|
||||||
|
monkeypatch.setattr(wz, "wizard_write", MagicMock())
|
||||||
|
|
||||||
|
# When
|
||||||
|
retval = wz.wizard(str(config_file))
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert retval == 0
|
Loading…
Reference in a new issue