mirror of
https://github.com/esphome/esphome.git
synced 2026-05-18 01:32:27 +08:00
[substitutions] speed up config loading: substitutions pass and !include redesign (package refactor part 4) (#12126)
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -989,7 +989,11 @@ def validate_config(
|
||||
|
||||
result.add_output_path([CONF_PACKAGES], CONF_PACKAGES)
|
||||
try:
|
||||
config = do_packages_pass(config, skip_update=skip_external_update)
|
||||
config = do_packages_pass(
|
||||
config,
|
||||
command_line_substitutions=command_line_substitutions,
|
||||
skip_update=skip_external_update,
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_packages_skip_update_false(
|
||||
}
|
||||
|
||||
# Call with skip_update=False (default)
|
||||
do_packages_pass(config, skip_update=False)
|
||||
do_packages_pass(config, command_line_substitutions={}, skip_update=False)
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
@@ -104,7 +104,7 @@ def test_packages_default_no_skip(
|
||||
}
|
||||
|
||||
# Call without skip_update parameter
|
||||
do_packages_pass(config)
|
||||
do_packages_pass(config, command_line_substitutions={})
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
|
||||
@@ -37,6 +37,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.util import OrderedDict
|
||||
from esphome.yaml_util import add_context
|
||||
|
||||
# Test strings
|
||||
TEST_DEVICE_NAME = "test_device_name"
|
||||
@@ -70,7 +71,7 @@ def fixture_basic_esphome():
|
||||
|
||||
|
||||
def packages_pass(config):
|
||||
"""Wrapper around packages_pass that also resolves Extend and Remove."""
|
||||
"""Passes the config through the packages processing steps."""
|
||||
config = do_packages_pass(config)
|
||||
config = do_substitution_pass(config)
|
||||
config = merge_packages(config)
|
||||
@@ -705,6 +706,85 @@ def test_remote_packages_with_files_list(
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@patch("esphome.yaml_util.load_yaml")
|
||||
@patch("pathlib.Path.is_file")
|
||||
@patch("esphome.git.clone_or_update")
|
||||
def test_remote_packages_with_files_list_and_substitutions(
|
||||
mock_clone_or_update, mock_is_file, mock_load_yaml
|
||||
) -> None:
|
||||
"""
|
||||
Ensures that packages are loaded as mixed list of dictionary and strings
|
||||
"""
|
||||
# Mock the response from git.clone_or_update
|
||||
mock_revert = MagicMock()
|
||||
mock_clone_or_update.return_value = (Path("/tmp/noexists"), mock_revert)
|
||||
|
||||
# Mock the response from pathlib.Path.is_file
|
||||
mock_is_file.return_value = True
|
||||
|
||||
# Mock the response from esphome.yaml_util.load_yaml
|
||||
mock_load_yaml.side_effect = [
|
||||
OrderedDict(
|
||||
{
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
OrderedDict(
|
||||
{
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
# Define the input config
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"package1": add_context(
|
||||
{
|
||||
CONF_URL: r"${url}",
|
||||
CONF_REF: r"${branch}",
|
||||
CONF_FILES: [
|
||||
{CONF_PATH: r"$file"},
|
||||
"sensor2.yaml",
|
||||
],
|
||||
CONF_REFRESH: "1d",
|
||||
},
|
||||
{
|
||||
"branch": "main",
|
||||
"file": TEST_YAML_FILENAME,
|
||||
"url": "https://github.com/esphome/non-existant-repo",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
expected = {
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_2,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
actual = packages_pass(config)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@patch("esphome.yaml_util.load_yaml")
|
||||
@patch("pathlib.Path.is_file")
|
||||
@patch("esphome.git.clone_or_update")
|
||||
@@ -906,7 +986,7 @@ def test_packages_merge_substitutions() -> None:
|
||||
},
|
||||
}
|
||||
|
||||
actual = do_packages_pass(config)
|
||||
actual = do_packages_pass(config, command_line_substitutions={})
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@@ -970,33 +1050,107 @@ def test_package_merge() -> None:
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_packages_invalid_type_raises() -> None:
|
||||
"""Packages that are not a dict or list raise cv.Invalid."""
|
||||
config = {
|
||||
CONF_PACKAGES: "not_a_dict_or_list",
|
||||
}
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="Packages must be a key to value mapping or list"
|
||||
):
|
||||
do_packages_pass(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_package",
|
||||
[
|
||||
6,
|
||||
"some string",
|
||||
["some string"],
|
||||
None,
|
||||
True,
|
||||
{"some_component": 8},
|
||||
{3: 2},
|
||||
{"some_component": r"${unevaluated expression}"},
|
||||
],
|
||||
)
|
||||
def test_package_merge_invalid(invalid_package) -> None:
|
||||
"""
|
||||
Tests that trying to merge an invalid package raises an error.
|
||||
"""
|
||||
def test_invalid_package_contents_rejected(invalid_package: object) -> None:
|
||||
"""Invalid package contents are rejected by PACKAGE_SCHEMA during do_packages_pass."""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"some_package": invalid_package,
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
do_packages_pass(config)
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="Deprecated single-package fallback swallows these errors. "
|
||||
"Remove xfail when single-package deprecation is removed (2026.7.0).",
|
||||
strict=True,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_package",
|
||||
[
|
||||
None,
|
||||
["some string"],
|
||||
{"some_component": 8},
|
||||
{3: 2},
|
||||
],
|
||||
)
|
||||
def test_invalid_package_contents_masked_by_deprecation(
|
||||
invalid_package: object,
|
||||
) -> None:
|
||||
"""These invalid packages are swallowed by the deprecated single-package fallback."""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"some_package": invalid_package,
|
||||
},
|
||||
}
|
||||
with pytest.raises(cv.Invalid):
|
||||
do_packages_pass(config)
|
||||
|
||||
|
||||
def test_merge_packages_invalid_nested_type_raises() -> None:
|
||||
"""Invalid nested packages type during merge raises cv.Invalid."""
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"pkg": {
|
||||
CONF_PACKAGES: "invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="Packages must be a key to value mapping or list"
|
||||
):
|
||||
merge_packages(config)
|
||||
|
||||
|
||||
@patch("esphome.yaml_util.load_yaml")
|
||||
@patch("pathlib.Path.is_file")
|
||||
@patch("esphome.git.clone_or_update")
|
||||
def test_remote_packages_no_revert(
|
||||
mock_clone_or_update, mock_is_file, mock_load_yaml
|
||||
) -> None:
|
||||
"""Remote packages with revert=None load without retry logic."""
|
||||
mock_clone_or_update.return_value = (Path("/tmp/noexists"), None)
|
||||
mock_is_file.return_value = True
|
||||
mock_load_yaml.return_value = OrderedDict(
|
||||
{CONF_SENSOR: [{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}]}
|
||||
)
|
||||
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"pkg": {
|
||||
CONF_URL: "https://github.com/esphome/repo",
|
||||
CONF_REF: "main",
|
||||
CONF_FILES: [{CONF_PATH: "file.yaml"}],
|
||||
CONF_REFRESH: "1d",
|
||||
}
|
||||
}
|
||||
}
|
||||
actual = packages_pass(config)
|
||||
assert actual[CONF_SENSOR] == [
|
||||
{CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: "test"}
|
||||
]
|
||||
|
||||
|
||||
def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None:
|
||||
"""Test that CORE.raw_config contains esphome section from merged package.
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
substitutions:
|
||||
x: 10
|
||||
y: 20
|
||||
z: 30
|
||||
values_from_repo1_main:
|
||||
- package_name: package1
|
||||
x: 3
|
||||
@@ -28,3 +24,20 @@ values_from_repo1_main:
|
||||
y: 20
|
||||
z: 5
|
||||
volume: 1000
|
||||
- package_name: package6
|
||||
x: 12
|
||||
y: 13
|
||||
z: 5
|
||||
volume: 780
|
||||
- package_name: default
|
||||
x: 10
|
||||
y: 20
|
||||
z: 5
|
||||
volume: 1000
|
||||
substitutions:
|
||||
x: 10
|
||||
y: 20
|
||||
z: 30
|
||||
my_repo: repo1
|
||||
my_file: file1
|
||||
my_ref: main
|
||||
|
||||
@@ -2,16 +2,26 @@ substitutions:
|
||||
x: 10
|
||||
y: 20
|
||||
z: 30
|
||||
my_repo: default_repo
|
||||
my_file: default_file
|
||||
my_ref: main
|
||||
|
||||
# The following key is only used by the test framework
|
||||
# to simulate command line substitutions
|
||||
command_line_substitutions:
|
||||
my_repo: repo1
|
||||
my_file: file1
|
||||
|
||||
packages:
|
||||
package1:
|
||||
url: https://github.com/esphome/repo1
|
||||
ref: main
|
||||
files:
|
||||
- path: file1.yaml
|
||||
vars:
|
||||
package_name: package1
|
||||
x: 3
|
||||
y: 4
|
||||
ref: main
|
||||
package2: !include # a package that just includes the given remote package
|
||||
file: remote_package_proxy.yaml
|
||||
vars:
|
||||
@@ -41,3 +51,13 @@ packages:
|
||||
repo: repo1
|
||||
file: file1.yaml
|
||||
ref: main
|
||||
package6:
|
||||
url: https://github.com/esphome/${my_repo}
|
||||
ref: ${my_ref}
|
||||
files:
|
||||
- path: ${my_file + ".yaml"}
|
||||
vars:
|
||||
package_name: package6
|
||||
x: 12
|
||||
y: 13
|
||||
package7: github://esphome/${my_repo}/${my_file + ".yaml"}@${my_ref}
|
||||
|
||||
@@ -37,8 +37,6 @@ substitutions:
|
||||
- id: component8
|
||||
value: 8
|
||||
fancy_package:
|
||||
substitutions:
|
||||
fancy_subst: 42
|
||||
fancy_component: *id001
|
||||
pin: 12
|
||||
some_switches: *id002
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
substitutions:
|
||||
a: 10
|
||||
b: 20
|
||||
x: 79
|
||||
test_list:
|
||||
- level1:
|
||||
a: 10
|
||||
b: 20
|
||||
c: 10
|
||||
d: 20
|
||||
e: ${e}
|
||||
f: ${f}
|
||||
g: ${g}
|
||||
h: ${h}
|
||||
i: ${i}
|
||||
j: ${j}
|
||||
x: 80
|
||||
y: 40
|
||||
level2:
|
||||
- level2:
|
||||
a: 10
|
||||
b: 20
|
||||
c: 10
|
||||
d: 20
|
||||
e: 20
|
||||
f: 40
|
||||
g: ${g}
|
||||
h: ${h}
|
||||
i: ${i}
|
||||
j: ${j}
|
||||
x: 81
|
||||
y: 40
|
||||
level3:
|
||||
- level3:
|
||||
a: 10
|
||||
b: 20
|
||||
c: 10
|
||||
d: 20
|
||||
e: 20
|
||||
f: 40
|
||||
g: 100
|
||||
h: 200
|
||||
i: 30
|
||||
j: ${undefined_variable}
|
||||
x: 82
|
||||
y: 40
|
||||
- a: 10
|
||||
b: 20
|
||||
x: 79
|
||||
@@ -0,0 +1,16 @@
|
||||
substitutions:
|
||||
a: 10
|
||||
b: 20
|
||||
x: 79
|
||||
|
||||
test_list:
|
||||
- !include
|
||||
file: level1_package.yaml
|
||||
vars:
|
||||
x: ${x+1}
|
||||
y: ${d*2}
|
||||
c: ${a}
|
||||
d: ${b}
|
||||
- a: ${a}
|
||||
b: ${b}
|
||||
x: ${x}
|
||||
@@ -0,0 +1,69 @@
|
||||
substitutions:
|
||||
a: from base config
|
||||
b: from package3
|
||||
c: from nested package4
|
||||
nested_package:
|
||||
nested_package_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package1:
|
||||
package1_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package2:
|
||||
package2_test_list:
|
||||
- a: from package2 vars
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package3:
|
||||
package3_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package4:
|
||||
packages:
|
||||
- nested_package_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package_map:
|
||||
package1:
|
||||
package1_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package2:
|
||||
package2_test_list:
|
||||
- a: from package2 vars
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package3: &id001
|
||||
package3_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
selected_package_number: 3
|
||||
selected_package_name: package3
|
||||
selected_package: *id001
|
||||
base_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package1_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package2_test_list:
|
||||
- a: from package2 vars
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
package3_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
nested_package_test_list:
|
||||
- a: from base config
|
||||
- b: from package3
|
||||
- c: from nested package4
|
||||
@@ -0,0 +1,62 @@
|
||||
command_line_substitutions:
|
||||
selected_package_number: 3
|
||||
|
||||
substitutions:
|
||||
a: from base config
|
||||
|
||||
package1: &p1
|
||||
substitutions:
|
||||
a: from package1
|
||||
b: from package1
|
||||
c: from package1
|
||||
package1_test_list:
|
||||
- a: ${ a }
|
||||
- b: ${ b }
|
||||
- c: ${ c }
|
||||
|
||||
package2: &p2 !include
|
||||
file: package2.yaml
|
||||
vars:
|
||||
a: from package2 vars
|
||||
|
||||
package3: &p3
|
||||
substitutions:
|
||||
a: from package3
|
||||
b: from package3
|
||||
c: from package3
|
||||
package3_test_list:
|
||||
- a: ${ a }
|
||||
- b: ${ b }
|
||||
- c: ${ c }
|
||||
|
||||
package4:
|
||||
substitutions:
|
||||
nested_package:
|
||||
substitutions:
|
||||
c: from nested package4
|
||||
nested_package_test_list:
|
||||
- a: ${ a }
|
||||
- b: ${ b }
|
||||
- c: ${ c }
|
||||
packages:
|
||||
- ${ nested_package }
|
||||
|
||||
package_map:
|
||||
package1: *p1
|
||||
package2: *p2
|
||||
package3: *p3
|
||||
|
||||
selected_package_number: 2 # will be overridden by command line substitutions
|
||||
selected_package_name: package${ selected_package_number }
|
||||
selected_package: ${ package_map[selected_package_name] }
|
||||
|
||||
packages:
|
||||
- ${ package1 }
|
||||
- ${ package2 }
|
||||
- ${ selected_package }
|
||||
- ${ package4 }
|
||||
|
||||
base_test_list:
|
||||
- a: ${ a }
|
||||
- b: ${ b }
|
||||
- c: ${ c }
|
||||
@@ -0,0 +1,21 @@
|
||||
# this file is included by 07-include_hierarchy.input.yaml
|
||||
level1:
|
||||
a: ${a} # top-level substitution
|
||||
b: ${b} # top-level substitution
|
||||
c: ${c} # from vars when including
|
||||
d: ${d} # from vars when including
|
||||
e: ${e} # undefined at this level
|
||||
f: ${f} # undefined at this level
|
||||
g: ${g} # undefined at this level
|
||||
h: ${h} # undefined at this level
|
||||
i: ${i} # undefined at this level
|
||||
j: ${j} # undefined at this level
|
||||
x: ${x} # from vars when including, calculated
|
||||
y: ${y} # from vars when including, calculated
|
||||
level2:
|
||||
- !include
|
||||
file: level2_package.yaml
|
||||
vars:
|
||||
e: ${c*2}
|
||||
f: ${d*2}
|
||||
x: ${x+1}
|
||||
@@ -0,0 +1,21 @@
|
||||
# this file is included by level1_package.yaml
|
||||
level2:
|
||||
a: ${a} # top-level substitution
|
||||
b: ${b} # top-level substitution
|
||||
c: ${c} # visible from level1 vars
|
||||
d: ${d} # visible from level1 vars
|
||||
e: ${e} # from vars when including
|
||||
f: ${f} # from vars when including
|
||||
g: ${g} # undefined at this level
|
||||
h: ${h} # undefined at this level
|
||||
i: ${i} # undefined at this level
|
||||
j: ${j} # undefined at this level
|
||||
x: ${x} # from vars when including, calculated
|
||||
y: ${y} # from vars when including, calculated
|
||||
level3:
|
||||
- !include
|
||||
file: level3_package.yaml
|
||||
vars:
|
||||
g: ${e*5}
|
||||
h: ${f*5}
|
||||
x: ${x+1}
|
||||
@@ -0,0 +1,16 @@
|
||||
# this file is included by level2_package.yaml
|
||||
defaults:
|
||||
i: 30
|
||||
level3:
|
||||
a: ${a} # top-level substitution
|
||||
b: ${b} # top-level substitution
|
||||
c: ${c} # visible from level1 vars
|
||||
d: ${d} # visible from level1 vars
|
||||
e: ${e} # visible from level2 vars
|
||||
f: ${f} # visible from level2 vars
|
||||
g: ${g} # from vars when including
|
||||
h: ${h} # from vars when including
|
||||
i: ${i} # Should take the default value of 30
|
||||
j: ${undefined_variable} # Does not exist, should be output as-is
|
||||
x: ${x} # from vars when including, calculated
|
||||
y: ${y} # from vars when including, calculated
|
||||
@@ -0,0 +1,10 @@
|
||||
# included from 10-dynamic_packages.input.yaml
|
||||
substitutions:
|
||||
a: from package2 # must not override base config's a
|
||||
# b not defined here, won't override package1's b
|
||||
c: from package2 # will override package1's c
|
||||
|
||||
package2_test_list:
|
||||
- a: ${ a }
|
||||
- b: ${ b }
|
||||
- c: ${ c }
|
||||
@@ -143,7 +143,9 @@ def test_substitutions_fixtures(
|
||||
|
||||
command_line_substitutions = config.pop("command_line_substitutions", None)
|
||||
|
||||
config = do_packages_pass(config)
|
||||
config = do_packages_pass(
|
||||
config, command_line_substitutions=command_line_substitutions
|
||||
)
|
||||
|
||||
config = substitutions.do_substitution_pass(config, command_line_substitutions)
|
||||
|
||||
|
||||
@@ -98,13 +98,15 @@ def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None:
|
||||
"""Test that missing secrets raise proper errors."""
|
||||
# Create a YAML file with a secret that doesn't exist
|
||||
test_yaml = tmp_path / "test.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
wifi:
|
||||
password: !secret nonexistent_secret
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Create an empty secrets file
|
||||
secrets_yaml = tmp_path / "secrets.yaml"
|
||||
@@ -118,10 +120,12 @@ def test_construct_secret_no_secrets_file(tmp_path: Path) -> None:
|
||||
"""Test that missing secrets.yaml file raises proper error."""
|
||||
# Create a YAML file with a secret but no secrets.yaml
|
||||
test_yaml = tmp_path / "test.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
wifi:
|
||||
password: !secret some_secret
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Mock CORE.config_path to avoid NoneType error
|
||||
with (
|
||||
@@ -140,10 +144,12 @@ def test_construct_secret_fallback_to_main_config_dir(
|
||||
subdir.mkdir()
|
||||
|
||||
test_yaml = subdir / "test.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
wifi:
|
||||
password: !secret test_secret
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Create secrets.yaml in the main directory
|
||||
main_secrets = tmp_path / "secrets.yaml"
|
||||
@@ -164,9 +170,11 @@ def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None
|
||||
|
||||
# Create test YAML that uses include_dir_named
|
||||
test_yaml = dst_dir / "test_include_named.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
sensor: !include_dir_named named_dir
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
actual = yaml_util.load_yaml(test_yaml)
|
||||
actual_sensor = actual["sensor"]
|
||||
@@ -199,9 +207,11 @@ def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None:
|
||||
empty_dir.mkdir()
|
||||
|
||||
test_yaml = tmp_path / "test.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
sensor: !include_dir_named empty_dir
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
actual = yaml_util.load_yaml(test_yaml)
|
||||
|
||||
@@ -231,9 +241,11 @@ def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None:
|
||||
hidden_subfile.write_text("key: hidden_subfile_value")
|
||||
|
||||
test_yaml = tmp_path / "test.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
test: !include_dir_named test_dir
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
actual = yaml_util.load_yaml(test_yaml)
|
||||
|
||||
@@ -255,9 +267,11 @@ def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None:
|
||||
|
||||
# This indirectly tests _find_files by using include_dir_named
|
||||
test_yaml = dst_dir / "test_include_recursive.yaml"
|
||||
test_yaml.write_text("""
|
||||
test_yaml.write_text(
|
||||
"""
|
||||
all_sensors: !include_dir_named named_dir
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
actual = yaml_util.load_yaml(test_yaml)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user