1import io
2import os
3import os.path
4
5import attr
6import pytest
7import salt.config
8import salt.loader
9from salt.exceptions import SaltRenderError
10
11REQUISITES = ["require", "require_in", "use", "use_in", "watch", "watch_in"]
12
13
14@attr.s
15class Renderer:
16    tmp_path = attr.ib()
17
18    def __call__(
19        self, content, sls="", saltenv="base", argline="-G yaml . jinja", **kws
20    ):
21        root_dir = self.tmp_path
22        state_tree_dir = self.tmp_path / "state_tree"
23        cache_dir = self.tmp_path / "cachedir"
24        state_tree_dir.mkdir()
25        cache_dir.mkdir()
26        config = salt.config.minion_config(None)
27        config["root_dir"] = str(root_dir)
28        config["state_events"] = False
29        config["id"] = "match"
30        config["file_client"] = "local"
31        config["file_roots"] = dict(base=[str(state_tree_dir)])
32        config["cachedir"] = str(cache_dir)
33        config["test"] = False
34        _renderers = salt.loader.render(config, {"config.get": lambda a, b: False})
35        return _renderers["stateconf"](
36            io.StringIO(content),
37            saltenv=saltenv,
38            sls=sls,
39            argline=argline,
40            renderers=salt.loader.render(config, {}),
41            **kws
42        )
43
44
45@pytest.fixture
46def renderer(tmp_path):
47    return Renderer(tmp_path)
48
49
50def test_state_config(renderer):
51    result = renderer(
52        """
53.sls_params:
54  stateconf.set:
55    - name1: value1
56    - name2: value2
57
58.extra:
59  stateconf:
60    - set
61    - name: value
62
63# --- end of state config ---
64
65test:
66  cmd.run:
67    - name: echo name1={{sls_params.name1}} name2={{sls_params.name2}} {{extra.name}}
68    - cwd: /
69""",
70        sls="test",
71    )
72    assert len(result) == 3
73    assert "test::sls_params" in result and "test" in result
74    assert "test::extra" in result
75    assert (
76        result["test"]["cmd.run"][0]["name"] == "echo name1=value1 name2=value2 value"
77    )
78
79
80def test_sls_dir(renderer):
81    result = renderer(
82        """
83test:
84  cmd.run:
85    - name: echo sls_dir={{sls_dir}}
86    - cwd: /
87""",
88        sls="path.to.sls",
89    )
90    assert result["test"]["cmd.run"][0]["name"] == "echo sls_dir=path{}to".format(
91        os.sep
92    )
93
94
95def test_states_declared_with_shorthand_no_args(renderer):
96    result = renderer(
97        """
98test:
99  cmd.run:
100    - name: echo testing
101    - cwd: /
102test1:
103  pkg.installed
104test2:
105  user.present
106"""
107    )
108    assert len(result) == 3
109    for args in (result["test1"]["pkg.installed"], result["test2"]["user.present"]):
110        assert isinstance(args, list)
111        assert len(args) == 0
112    assert result["test"]["cmd.run"][0]["name"] == "echo testing"
113
114
115def test_adding_state_name_arg_for_dot_state_id(renderer):
116    result = renderer(
117        """
118.test:
119  pkg.installed:
120    - cwd: /
121.test2:
122  pkg.installed:
123    - name: vim
124""",
125        sls="test",
126    )
127    assert result["test::test"]["pkg.installed"][0]["name"] == "test"
128    assert result["test::test2"]["pkg.installed"][0]["name"] == "vim"
129
130
131def test_state_prefix(renderer):
132    result = renderer(
133        """
134.test:
135  cmd.run:
136    - name: echo renamed
137    - cwd: /
138
139state_id:
140  cmd:
141    - run
142    - name: echo not renamed
143    - cwd: /
144""",
145        sls="test",
146    )
147    assert len(result) == 2
148    assert "test::test" in result
149    assert "state_id" in result
150
151
152@pytest.mark.parametrize("req", REQUISITES)
153def test_dot_state_id_in_requisites(req, renderer):
154    result = renderer(
155        """
156.test:
157  cmd.run:
158    - name: echo renamed
159    - cwd: /
160
161state_id:
162  cmd.run:
163    - name: echo not renamed
164    - cwd: /
165    - {}:
166      - cmd: .test
167
168""".format(
169            req
170        ),
171        sls="test",
172    )
173    assert len(result) == 2
174    assert "test::test" in result
175    assert "state_id" in result
176    assert result["state_id"]["cmd.run"][2][req][0]["cmd"] == "test::test"
177
178
179@pytest.mark.parametrize("req", REQUISITES)
180def test_relative_include_with_requisites(req, renderer):
181    result = renderer(
182        """
183include:
184  - some.helper
185  - .utils
186
187state_id:
188  cmd.run:
189    - name: echo test
190    - cwd: /
191    - {}:
192      - cmd: .utils::some_state
193""".format(
194            req
195        ),
196        sls="test.work",
197    )
198    assert result["include"][1] == {"base": "test.utils"}
199    assert result["state_id"]["cmd.run"][2][req][0]["cmd"] == "test.utils::some_state"
200
201
202def test_relative_include_and_extend(renderer):
203    result = renderer(
204        """
205include:
206  - some.helper
207  - .utils
208
209extend:
210  .utils::some_state:
211    cmd.run:
212      - name: echo overridden
213    """,
214        sls="test.work",
215    )
216    assert "test.utils::some_state" in result["extend"]
217
218
219@pytest.mark.parametrize("req", REQUISITES)
220def test_multilevel_relative_include_with_requisites(req, renderer):
221    result = renderer(
222        """
223include:
224  - .shared
225  - ..utils
226  - ...helper
227
228state_id:
229  cmd.run:
230    - name: echo test
231    - cwd: /
232    - {}:
233      - cmd: ..utils::some_state
234""".format(
235            req
236        ),
237        sls="test.nested.work",
238    )
239    assert result["include"][0] == {"base": "test.nested.shared"}
240    assert result["include"][1] == {"base": "test.utils"}
241    assert result["include"][2] == {"base": "helper"}
242    assert result["state_id"]["cmd.run"][2][req][0]["cmd"] == "test.utils::some_state"
243
244
245def test_multilevel_relative_include_beyond_top_level(renderer):
246    pytest.raises(
247        SaltRenderError,
248        renderer,
249        """
250include:
251  - ...shared
252""",
253        sls="test.work",
254    )
255
256
257def test_start_state_generation(renderer):
258    result = renderer(
259        """
260A:
261  cmd.run:
262    - name: echo hello
263    - cwd: /
264B:
265  cmd.run:
266    - name: echo world
267    - cwd: /
268""",
269        sls="test",
270        argline="-so yaml . jinja",
271    )
272    assert len(result) == 4
273    assert result["test::start"]["stateconf.set"][0]["require_in"][0]["cmd"] == "A"
274
275
276def test_goal_state_generation(renderer):
277    result = renderer(
278        """
279{% for sid in "ABCDE": %}
280{{sid}}:
281  cmd.run:
282    - name: echo this is {{sid}}
283    - cwd: /
284{% endfor %}
285
286""",
287        sls="test.goalstate",
288        argline="yaml . jinja",
289    )
290    assert len(result) == len("ABCDE") + 1
291
292    reqs = result["test.goalstate::goal"]["stateconf.set"][0]["require"]
293    assert {next(iter(i.values())) for i in reqs} == set("ABCDE")
294
295
296def test_implicit_require_with_goal_state(renderer):
297    result = renderer(
298        """
299{% for sid in "ABCDE": %}
300{{sid}}:
301  cmd.run:
302    - name: echo this is {{sid}}
303    - cwd: /
304{% endfor %}
305
306F:
307  cmd.run:
308    - name: echo this is F
309    - cwd: /
310    - require:
311      - cmd: A
312      - cmd: B
313
314G:
315  cmd.run:
316    - name: echo this is G
317    - cwd: /
318    - require:
319      - cmd: D
320      - cmd: F
321""",
322        sls="test",
323        argline="-o yaml . jinja",
324    )
325
326    sids = "ABCDEFG"[::-1]
327    for i, sid in enumerate(sids):
328        if i < len(sids) - 1:
329            assert result[sid]["cmd.run"][2]["require"][0]["cmd"] == sids[i + 1]
330
331    F_args = result["F"]["cmd.run"]
332    assert len(F_args) == 3
333    F_req = F_args[2]["require"]
334    assert len(F_req) == 3
335    assert F_req[1]["cmd"] == "A"
336    assert F_req[2]["cmd"] == "B"
337
338    G_args = result["G"]["cmd.run"]
339    assert len(G_args) == 3
340    G_req = G_args[2]["require"]
341    assert len(G_req) == 3
342    assert G_req[1]["cmd"] == "D"
343    assert G_req[2]["cmd"] == "F"
344
345    goal_args = result["test::goal"]["stateconf.set"]
346    assert len(goal_args) == 1
347    assert [next(iter(i.values())) for i in goal_args[0]["require"]] == list("ABCDEFG")
348
349
350def test_slsdir(renderer):
351    result = renderer(
352        """
353formula/woot.sls:
354  cmd.run:
355    - name: echo {{ slspath }}
356    - cwd: /
357""",
358        sls="formula.woot",
359        argline="yaml . jinja",
360    )
361
362    r = result["formula/woot.sls"]["cmd.run"][0]["name"]
363    assert r == "echo formula/woot"
364