Passed
Push — develop ( c191b3...57b1f3 )
by Shalom
01:47
created

TestInjiCmd.test_template_render_with_internal_vars()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
#!/usr/bin/python3
2
3
# -*- coding: utf-8 -*-
4
5
import atexit
6
import json
7
import os
8
from   os.path import abspath, dirname, join
9
import pytest
10
import re
11
import shutil
12
import subprocess
13
import sys
14
import tempfile
15
import unittest
16
17
sys.path.insert(0, join(dirname(abspath(__file__)), '../..'))
18
sys.path.insert(0, join(dirname(abspath(__file__)), '../../inji'))
19
20
import inji
21
inji = abspath(join(sys.path[0], 'inji'))
22
23
def run(cmd, *args, **kwargs):
24
  os.environ['PYTHONUNBUFFERED'] = "1"
25
  proc = subprocess.Popen( [cmd, *args],
26
    stdout = subprocess.PIPE,
27
    stderr = subprocess.PIPE,
28
  )
29
  stdout, stderr = proc.communicate(**kwargs)
30
  return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
31
32
def check_output(*args, **kwargs):
33
  return subprocess.check_output( [*args], **kwargs ).decode('utf-8')
34
35
def file_from_text(*args, **kwargs):
36
  """ Write args to a tempfile and return the filename """
37
38
  fqdir=kwargs.get('dir', tempfile.tempdir)
39
40
  if kwargs.get('name') is None:
41
    _, filename = tempfile.mkstemp( text=True, dir=fqdir)
42
  else:
43
    filename = join(fqdir, kwargs.get('name'))
44
45
  atexit.register(os.remove, filename)
46
  with open(filename, "a+") as f:
47
    f.write('\n'.join(args))
48
49
  return abspath(filename)
50
51
def dump(obj):
52
53
    def pretty(obj):
54
        j = json.dumps( obj, sort_keys=True, indent=2,
55
                    separators=(', ', ': ') )
56
        try:
57
            from pygments import highlight, lexers, formatters
58
            return highlight( j,
59
                    lexers.JsonLexer(), formatters.TerminalFormatter() )
60
        except ImportError as e:
61
            return j
62
63
    try:
64
        return pretty(obj)
65
    except TypeError as e:
66
        return pretty(obj.__dict__)
67
68
class TestFixtureHelloWorld(unittest.TestCase):
69
70
  def test_hello_world(self):
71
    code, out, err = run('/bin/echo', 'hello', 'world')
72
    assert 'hello' in out
73
    assert 'world' in out
74
    assert '' in err
75
    assert code == 0
76
77
  def test_check_output(self):
78
    out = check_output( 'sed', 's/foo/bar/', input=b'foo' )
79
    assert "bar" in out
80
81
class TestInjiCmd(unittest.TestCase):
82
83
  def test_help(self):
84
    """Test help message is emitted"""
85
    code, out, err = run(inji, '-h')
86
    assert code == 0
87
    assert 'usage:' in out
88
    assert '' == err
89
90
  def test_stdin(self):
91
    """Templates should be read in from STDIN (-) by default"""
92
    assert "Hola world!\n" == \
93
      check_output( inji,
94
                    input=b"{% set foo='world!' %}Hola {{ foo }}"
95
      )
96
97
  def test_stdin_empty_input(self):
98
    """Empty template string should return a newline"""
99
    assert '\n' == check_output( inji, input=b"" )
100
101
  def test_json_config_args(self):
102
    """Config passed as JSON string"""
103
    assert "Hola world!\n" == \
104
      check_output(
105
        inji, '-c', '{"foo": "world!"}',
106
          input=b"Hola {{ foo }}"
107
      )
108
109
  def test_empty_json_config_args(self):
110
    """Empty json config args should cause an error"""
111
    class EmptyJSONConfigArgs(Exception): pass
112
    with pytest.raises(EmptyJSONConfigArgs) as e_info:
113
      try:
114
        check_output( inji, '-c', '',
115
                      stderr=subprocess.STDOUT,
116
        )
117
      except subprocess.CalledProcessError as exc:
118
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
119
        raise EmptyJSONConfigArgs(msg) from exc
120
    e = str(e_info)
121
    assert re.search('JSON config args string is empty', e)
122
    assert "exit_code:1 " in e
123
124
  def test_12factor_config_sourcing(self):
125
    """Test config sourcing precendence should adhere to 12-factor app expectations"""
126
    tmpdir = tempfile.mkdtemp(prefix='param-')
127
    atexit.register(shutil.rmtree, tmpdir)
128
129
    for item in ['dev', 'prod', 'stage']:
130
      cdir = os.path.join(tmpdir, item)
131
      os.mkdir(cdir)
132
      for file in 'a.yml', 'b.yaml', 'c.yaml':
133
        fqn = os.path.join(cdir, file)
134
        file_from_text(f"zoo: {file}\nitem: {item}", dir=cdir, name=fqn)
135
136
    cfg_file = file_from_text(
137
        f"zar: bella\nzoo: cfg",
138
        dir=tmpdir, name='inji.yml' )
139
140
    param_file = file_from_text(
141
        f"zar: zella\nzoo: zorg",
142
        dir=tmpdir, name='vars.yaml' )
143
144
    # needed to source lowest precedence config from ./inji.y?ml
145
    OLDPWD=os.getcwd()
146
    os.chdir(tmpdir)
147
148
    # test we are able to recursively source params
149
    # but also source from the p5 config file relative to PWD
150
    assert re.search('Hola \w+ from bella',
151
      check_output(
152
        inji,
153
          '-o', f"{tmpdir}/dev",
154
          input=b"Hola {{ item }} from {{ zar }}"
155
      )
156
    )
157
158
    # dev/c.yaml should be last file sourced
159
    assert "Hola c.yaml\n" == \
160
      check_output(
161
        inji, '-o', f"{tmpdir}/dev",
162
          input=b"Hola {{ zoo }}"
163
      )
164
165
    # prod/ should be the last overlay sourced
166
    assert "Hola prod\n" == \
167
      check_output(
168
        inji,
169
          '-o', f"{tmpdir}/stage",
170
          '-o', f"{tmpdir}/dev",
171
          '-o', f"{tmpdir}/prod",
172
          input=b"Hola {{ item }}"
173
      )
174
175
    # precedence 4
176
    # named config file trumps overlays, arg position is irrelevant
177
    assert "Hola zella from zorg\n" == \
178
      check_output(
179
        inji,
180
          '-o', f"{tmpdir}/stage",
181
          '-o', f"{tmpdir}/prod",
182
          '-v', param_file,
183
          '-o', f"{tmpdir}/dev",
184
          input=b"Hola {{ zar }} from {{ zoo }}"
185
      )
186
187
    # precedence 3
188
    # env vars trump named config files
189
    os.environ['zoo']='env'
190
    assert "Hola zella from env\n" == \
191
      check_output(
192
        inji,
193
          '-o', f"{tmpdir}/stage",
194
          '-o', f"{tmpdir}/prod",
195
          '-v', param_file,
196
          '-o', f"{tmpdir}/dev",
197
          input=b"Hola {{ zar }} from {{ zoo }}"
198
      )
199
200
    # precedence 2
201
    # cli params passed in as JSON take ultimate precendence
202
    assert "Hola zella from world!\n" == \
203
      check_output(
204
        inji,
205
          '-o', f"{tmpdir}/stage",
206
          '-c', '{"zoo": "world!"}',
207
          '-o', f"{tmpdir}/prod",
208
          '-v', param_file,
209
          '-o', f"{tmpdir}/dev",
210
          input=b"Hola {{ zar }} from {{ zoo }}"
211
      )
212
213
    # precedence 1
214
    # except when params are defined in the templates themselves, off course!
215
    assert "Hola quux from mars\n" == \
216
      check_output(
217
        inji,
218
          '-o', f"{tmpdir}/stage",
219
          '-c', '{"zoo": "mars"}',
220
          '-o', f"{tmpdir}/prod",
221
          '-v', param_file,
222
          '-o', f"{tmpdir}/dev",
223
          input=b"{% set zar='quux' %}Hola {{ zar }} from {{ zoo }}"
224
      )
225
226
    os.environ.pop('zoo')
227
    os.chdir(OLDPWD)
228
229
  def test_strict_undefined_var(self):
230
    """Undefined variables should cause a failure"""
231
    class BarUndefinedException(Exception): pass
232
    with pytest.raises(BarUndefinedException) as e_info:
233
      try:
234
        check_output( inji,
235
                      input=b"{% set foo='world!' %}Hola {{ bar }}",
236
                      stderr=subprocess.STDOUT,
237
        )
238
      except subprocess.CalledProcessError as exc:
239
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
240
        raise BarUndefinedException(msg) from exc
241
    e = str(e_info)
242
    assert "exit_code:1 " in e
243
    assert re.search('jinja2.exceptions.UndefinedError.+bar.+is undefined', e)
244
245
  def test_keep_undefined_var(self):
246
    """Undefined variables in keep mode should be kept"""
247
    assert '[Hola {{ foo }}]\n' == \
248
      check_output( inji, '-s', 'keep',
249
                      input=b"[Hola {{ foo }}]"
250
      )
251
252
  def test_empty_undefined_var(self):
253
    """Undefined variables in empty mode should leave spaces behind placeholders"""
254
    assert '[Hola ]\n' == \
255
      check_output( inji, '-s', 'empty',
256
                      input=b"[Hola {{ foo }}]"
257
      )
258
259
  def test_template_render_with_envvars(self):
260
    """Environment variables should be referenceable as parameters"""
261
    template = file_from_text("Hola {{ foo }}")
262
    os.environ['foo'] = 'world!'
263
    assert 'Hola world!\n' == \
264
      check_output( inji, '-t', template )
265
    os.environ.pop('foo')
266
267
  def test_template_render_with_internal_vars(self):
268
    """
269
    Jinja template should render correctly
270
    referencing those variables set in the templates themselves
271
    """
272
    template = file_from_text("{% set foo='world!' %}Hola {{ foo }}")
273
    assert 'Hola world!\n' == \
274
      check_output( inji, '-t', template )
275
276
  def test_template_missing(self):
277
    """ Missing template files should cause an error"""
278
    class TemplateFileMissingException(Exception): pass
279
    with pytest.raises(TemplateFileMissingException) as e_info:
280
      try:
281
        check_output( inji, '-t', 'nonexistent-template.j2',
282
                      stderr=subprocess.STDOUT,
283
        )
284
      except subprocess.CalledProcessError as exc:
285
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
286
        raise TemplateFileMissingException(msg) from exc
287
    e = str(e_info)
288
    assert "exit_code:2 " in e
289
    assert re.search('nonexistent-template.j2.. does not exist', e)
290
291
  def test_template_directory(self):
292
    """ Using a directory as a template source should cause an error"""
293
    class TemplateAsDirectoryException(Exception): pass
294
    with pytest.raises(TemplateAsDirectoryException) as e_info:
295
      try:
296
        check_output( inji, '-t', '/',
297
                      stderr=subprocess.STDOUT,
298
        )
299
      except subprocess.CalledProcessError as exc:
300
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
301
        raise TemplateAsDirectoryException(msg) from exc
302
    e = str(e_info)
303
    assert "exit_code:2 " in e
304
    assert re.search('/.. is not a file', e)
305
306
  def test_template_render_with_varsfile(self):
307
    """Params from params files should be rendered on the template output"""
308
    template = file_from_text("Hola {{ foo }}")
309
    varsfile = file_from_text("foo: world!")
310
    assert 'Hola world!\n' == \
311
      check_output( inji, '-t', template, '-v', varsfile )
312
313
  def test_template_render_with_multiple_varsfiles(self):
314
    """Params from multiple files should be merged before rendering"""
315
    template = file_from_text("Hola {{ foo }}, Hello {{ bar }}, t{{ t }}")
316
    varsfile1 = file_from_text("foo: world!\nt: quux")
317
    varsfile2 = file_from_text("bar: metaverse\nt: moocow")
318
    assert 'Hola world!, Hello metaverse, tmoocow\n' == \
319
      check_output(
320
          inji, '-t', template,
321
                '-v', varsfile1,
322
                '-v', varsfile2
323
      )
324
325
  def test_error_with_empty_varsfile(self):
326
    """ An empty vars file is an error, we ought to fail early """
327
    """
328
    There may be a case for allowing this to just be a warning and so
329
    we may change this behaviour in the future. For now, it definitely
330
    is something we should fail-early on.
331
    """
332
    class EmptyVarsFileException(Exception): pass
333
    with pytest.raises(EmptyVarsFileException) as e_info:
334
      try:
335
        template = file_from_text("Hola {{ foo }}, Hello {{ bar }}")
336
        varsfile = file_from_text('')
337
        check_output( inji, '-t', template, '-v', varsfile,
338
                      stderr=subprocess.STDOUT,
339
        )
340
      except subprocess.CalledProcessError as exc:
341
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
342
        raise EmptyVarsFileException(msg) from exc
343
    e = str(e_info)
344
    assert re.search('TypeError: .* contains no data', e)
345
    assert "exit_code:1 " in e
346
347
  def test_error_with_malformed_varsfile(self):
348
    """ An invalid varsfile is a fail-early error """
349
    class MalformedVarsFileException(Exception): pass
350
    with pytest.raises(MalformedVarsFileException) as e_info:
351
      try:
352
        varsfile = file_from_text('@')
353
        check_output( inji, '-v', varsfile,
354
                      input=b"Hola {{ foo }}, Hello {{ bar }}",
355
                      stderr=subprocess.STDOUT
356
        )
357
      except subprocess.CalledProcessError as exc:
358
        msg = 'exit_code:{} output:{}'.format(exc.returncode, exc.output)
359
        raise MalformedVarsFileException(msg) from exc
360
    e = str(e_info)
361
    assert re.search('cannot start any token', e)
362
    assert "exit_code:1 " in e
363
364
if __name__ == '__main__':
365
  TestInjiCmd().test_empty_json_config_args()
366
367