1# -*- coding -*-
2"""
3Provides step definitions to:
4
5    * run commands, like behave
6    * create textual files within a working directory
7
8TODO:
9  matcher that ignores empty lines and whitespace and has contains comparison
10"""
11
12from __future__ import absolute_import, print_function
13from behave import given, when, then, step, matchers
14from behave4cmd0 import command_shell, command_util, pathutil, textutil
15from behave4cmd0.pathutil import posixpath_normpath
16from behave4cmd0.command_shell_proc import \
17    TextProcessor, BehaveWinCommandOutputProcessor
18import contextlib
19import difflib
20import os
21import shutil
22from hamcrest import assert_that, equal_to, is_not, contains_string
23
24# -----------------------------------------------------------------------------
25# INIT:
26# -----------------------------------------------------------------------------
27matchers.register_type(int=int)
28DEBUG = False
29file_contents_normalizer = None
30if BehaveWinCommandOutputProcessor.enabled:
31    file_contents_normalizer = TextProcessor(BehaveWinCommandOutputProcessor())
32
33
34# -----------------------------------------------------------------------------
35# UTILITIES:
36# -----------------------------------------------------------------------------
37@contextlib.contextmanager
38def on_assert_failed_print_details(actual, expected):
39    """
40    Print text details in case of assertation failed errors.
41
42    .. sourcecode:: python
43
44        with on_assert_failed_print_details(actual_text, expected_text):
45            assert actual == expected
46    """
47    try:
48        yield
49    except AssertionError:
50        # diff = difflib.unified_diff(expected.splitlines(), actual.splitlines(),
51        #                            "expected", "actual")
52        diff = difflib.ndiff(expected.splitlines(), actual.splitlines())
53        diff_text = u"\n".join(diff)
54        print(u"DIFF (+ ACTUAL, - EXPECTED):\n{0}\n".format(diff_text))
55        if DEBUG:
56            print(u"expected:\n{0}\n".format(expected))
57            print(u"actual:\n{0}\n".format(actual))
58        raise
59
60@contextlib.contextmanager
61def on_error_print_details(actual, expected):
62    """
63    Print text details in case of assertation failed errors.
64
65    .. sourcecode:: python
66
67        with on_error_print_details(actual_text, expected_text):
68            ... # Do something
69    """
70    try:
71        yield
72    except Exception:
73        diff = difflib.ndiff(expected.splitlines(), actual.splitlines())
74        diff_text = u"\n".join(diff)
75        print(u"DIFF (+ ACTUAL, - EXPECTED):\n{0}\n".format(diff_text))
76        if DEBUG:
77            print(u"expected:\n{0}\n".format(expected))
78            print(u"actual:\n{0}".format(actual))
79        raise
80
81# -----------------------------------------------------------------------------
82# STEPS: WORKING DIR
83# -----------------------------------------------------------------------------
84@given(u'a new working directory')
85def step_a_new_working_directory(context):
86    """Creates a new, empty working directory."""
87    command_util.ensure_context_attribute_exists(context, "workdir", None)
88    # MAYBE: command_util.ensure_workdir_not_exists(context)
89    command_util.ensure_workdir_exists(context)
90    # OOPS:
91    shutil.rmtree(context.workdir, ignore_errors=True)
92    command_util.ensure_workdir_exists(context)
93
94@given(u'I use the current directory as working directory')
95def step_use_curdir_as_working_directory(context):
96    """
97    Uses the current directory as working directory
98    """
99    context.workdir = os.path.abspath(".")
100    command_util.ensure_workdir_exists(context)
101
102# -----------------------------------------------------------------------------
103# STEPS: Create files with contents
104# -----------------------------------------------------------------------------
105@given(u'a file named "{filename}" and encoding="{encoding}" with')
106def step_a_file_named_filename_and_encoding_with(context, filename, encoding):
107    """Creates a textual file with the content provided as docstring."""
108    __encoding_is_valid = True
109    assert context.text is not None, "ENSURE: multiline text is provided."
110    assert not os.path.isabs(filename)
111    assert __encoding_is_valid
112    command_util.ensure_workdir_exists(context)
113    filename2 = os.path.join(context.workdir, filename)
114    pathutil.create_textfile_with_contents(filename2, context.text, encoding)
115
116
117@given(u'a file named "{filename}" with')
118def step_a_file_named_filename_with(context, filename):
119    """Creates a textual file with the content provided as docstring."""
120    step_a_file_named_filename_and_encoding_with(context, filename, "UTF-8")
121
122    # -- SPECIAL CASE: For usage with behave steps.
123    if filename.endswith(".feature"):
124        command_util.ensure_context_attribute_exists(context, "features", [])
125        context.features.append(filename)
126
127
128@given(u'an empty file named "{filename}"')
129def step_an_empty_file_named_filename(context, filename):
130    """
131    Creates an empty file.
132    """
133    assert not os.path.isabs(filename)
134    command_util.ensure_workdir_exists(context)
135    filename2 = os.path.join(context.workdir, filename)
136    pathutil.create_textfile_with_contents(filename2, "")
137
138
139# -----------------------------------------------------------------------------
140# STEPS: Run commands
141# -----------------------------------------------------------------------------
142@when(u'I run "{command}"')
143@when(u'I run `{command}`')
144def step_i_run_command(context, command):
145    """
146    Run a command as subprocess, collect its output and returncode.
147    """
148    command_util.ensure_workdir_exists(context)
149    context.command_result = command_shell.run(command, cwd=context.workdir)
150    command_util.workdir_save_coverage_files(context.workdir)
151    if False and DEBUG:
152        print(u"run_command: {0}".format(command))
153        print(u"run_command.output {0}".format(context.command_result.output))
154
155@when(u'I successfully run "{command}"')
156@when(u'I successfully run `{command}`')
157def step_i_successfully_run_command(context, command):
158    step_i_run_command(context, command)
159    step_it_should_pass(context)
160
161@then(u'it should fail with result "{result:int}"')
162def step_it_should_fail_with_result(context, result):
163    assert_that(context.command_result.returncode, equal_to(result))
164    assert_that(result, is_not(equal_to(0)))
165
166@then(u'the command should fail with returncode="{result:int}"')
167def step_it_should_fail_with_returncode(context, result):
168    assert_that(context.command_result.returncode, equal_to(result))
169    assert_that(result, is_not(equal_to(0)))
170
171@then(u'the command returncode is "{result:int}"')
172def step_the_command_returncode_is(context, result):
173    assert_that(context.command_result.returncode, equal_to(result))
174
175@then(u'the command returncode is non-zero')
176def step_the_command_returncode_is_nonzero(context):
177    assert_that(context.command_result.returncode, is_not(equal_to(0)))
178
179@then(u'it should pass')
180def step_it_should_pass(context):
181    assert_that(context.command_result.returncode, equal_to(0),
182                context.command_result.output)
183
184@then(u'it should fail')
185def step_it_should_fail(context):
186    assert_that(context.command_result.returncode, is_not(equal_to(0)),
187                context.command_result.output)
188
189@then(u'it should pass with')
190def step_it_should_pass_with(context):
191    '''
192    EXAMPLE:
193        ...
194        when I run "behave ..."
195        then it should pass with:
196            """
197            TEXT
198            """
199    '''
200    assert context.text is not None, "ENSURE: multiline text is provided."
201    step_command_output_should_contain(context)
202    assert_that(context.command_result.returncode, equal_to(0),
203                context.command_result.output)
204
205
206@then(u'it should fail with')
207def step_it_should_fail_with(context):
208    '''
209    EXAMPLE:
210        ...
211        when I run "behave ..."
212        then it should fail with:
213            """
214            TEXT
215            """
216    '''
217    assert context.text is not None, "ENSURE: multiline text is provided."
218    step_command_output_should_contain(context)
219    assert_that(context.command_result.returncode, is_not(equal_to(0)))
220
221
222# -----------------------------------------------------------------------------
223# STEPS FOR: Output Comparison
224# -----------------------------------------------------------------------------
225@then(u'the command output should contain "{text}"')
226def step_command_output_should_contain_text(context, text):
227    '''
228    EXAMPLE:
229        ...
230        Then the command output should contain "TEXT"
231    '''
232    expected_text = text
233    if "{__WORKDIR__}" in expected_text or "{__CWD__}" in expected_text:
234        expected_text = textutil.template_substitute(text,
235             __WORKDIR__ = posixpath_normpath(context.workdir),
236             __CWD__     = posixpath_normpath(os.getcwd())
237        )
238    actual_output = context.command_result.output
239    with on_assert_failed_print_details(actual_output, expected_text):
240        textutil.assert_normtext_should_contain(actual_output, expected_text)
241
242
243@then(u'the command output should not contain "{text}"')
244def step_command_output_should_not_contain_text(context, text):
245    '''
246    EXAMPLE:
247        ...
248        then the command output should not contain "TEXT"
249    '''
250    expected_text = text
251    if "{__WORKDIR__}" in text or "{__CWD__}" in text:
252        expected_text = textutil.template_substitute(text,
253             __WORKDIR__ = posixpath_normpath(context.workdir),
254             __CWD__     = posixpath_normpath(os.getcwd())
255        )
256    actual_output  = context.command_result.output
257    with on_assert_failed_print_details(actual_output, expected_text):
258        textutil.assert_normtext_should_not_contain(actual_output, expected_text)
259
260
261@then(u'the command output should contain "{text}" {count:d} times')
262def step_command_output_should_contain_text_multiple_times(context, text, count):
263    '''
264    EXAMPLE:
265        ...
266        Then the command output should contain "TEXT" 3 times
267    '''
268    assert count >= 0
269    expected_text = text
270    if "{__WORKDIR__}" in expected_text or "{__CWD__}" in expected_text:
271        expected_text = textutil.template_substitute(text,
272             __WORKDIR__ = posixpath_normpath(context.workdir),
273             __CWD__     = posixpath_normpath(os.getcwd())
274        )
275    actual_output = context.command_result.output
276    with on_assert_failed_print_details(actual_output, expected_text):
277        textutil.assert_normtext_should_contain_multiple_times(actual_output,
278                                                               expected_text,
279                                                               count)
280
281@then(u'the command output should contain exactly "{text}"')
282def step_command_output_should_contain_exactly_text(context, text):
283    """
284    Verifies that the command output of the last command contains the
285    expected text.
286
287    .. code-block:: gherkin
288
289        When I run "echo Hello"
290        Then the command output should contain "Hello"
291    """
292    expected_text = text
293    if "{__WORKDIR__}" in text or "{__CWD__}" in text:
294        expected_text = textutil.template_substitute(text,
295             __WORKDIR__ = posixpath_normpath(context.workdir),
296             __CWD__     = posixpath_normpath(os.getcwd())
297        )
298    actual_output  = context.command_result.output
299    textutil.assert_text_should_contain_exactly(actual_output, expected_text)
300
301
302@then(u'the command output should not contain exactly "{text}"')
303def step_command_output_should_not_contain_exactly_text(context, text):
304    expected_text = text
305    if "{__WORKDIR__}" in text or "{__CWD__}" in text:
306        expected_text = textutil.template_substitute(text,
307             __WORKDIR__ = posixpath_normpath(context.workdir),
308             __CWD__     = posixpath_normpath(os.getcwd())
309        )
310    actual_output  = context.command_result.output
311    textutil.assert_text_should_not_contain_exactly(actual_output, expected_text)
312
313
314@then(u'the command output should contain')
315def step_command_output_should_contain(context):
316    '''
317    EXAMPLE:
318        ...
319        when I run "behave ..."
320        then it should pass
321        and  the command output should contain:
322            """
323            TEXT
324            """
325    '''
326    assert context.text is not None, "REQUIRE: multi-line text"
327    step_command_output_should_contain_text(context, context.text)
328
329
330@then(u'the command output should not contain')
331def step_command_output_should_not_contain(context):
332    '''
333    EXAMPLE:
334        ...
335        when I run "behave ..."
336        then it should pass
337        and  the command output should not contain:
338            """
339            TEXT
340            """
341    '''
342    assert context.text is not None, "REQUIRE: multi-line text"
343    step_command_output_should_not_contain_text(context, context.text.strip())
344
345@then(u'the command output should contain {count:d} times')
346def step_command_output_should_contain_multiple_times(context, count):
347    '''
348    EXAMPLE:
349        ...
350        when I run "behave ..."
351        then it should pass
352        and  the command output should contain 2 times:
353            """
354            TEXT
355            """
356    '''
357    assert context.text is not None, "REQUIRE: multi-line text"
358    step_command_output_should_contain_text_multiple_times(context,
359                                                           context.text, count)
360
361@then(u'the command output should contain exactly')
362def step_command_output_should_contain_exactly_with_multiline_text(context):
363    assert context.text is not None, "REQUIRE: multi-line text"
364    step_command_output_should_contain_exactly_text(context, context.text)
365
366
367@then(u'the command output should not contain exactly')
368def step_command_output_should_contain_not_exactly_with_multiline_text(context):
369    assert context.text is not None, "REQUIRE: multi-line text"
370    step_command_output_should_not_contain_exactly_text(context, context.text)
371
372
373# -----------------------------------------------------------------------------
374# STEPS FOR: Directories
375# -----------------------------------------------------------------------------
376@step(u'I remove the directory "{directory}"')
377def step_remove_directory(context, directory):
378    path_ = directory
379    if not os.path.isabs(directory):
380        path_ = os.path.join(context.workdir, os.path.normpath(directory))
381    if os.path.isdir(path_):
382        shutil.rmtree(path_, ignore_errors=True)
383    assert_that(not os.path.isdir(path_))
384
385@given(u'I ensure that the directory "{directory}" does not exist')
386def step_given_the_directory_should_not_exist(context, directory):
387    step_remove_directory(context, directory)
388
389@given(u'a directory named "{path}"')
390def step_directory_named_dirname(context, path):
391    assert context.workdir, "REQUIRE: context.workdir"
392    path_ = os.path.join(context.workdir, os.path.normpath(path))
393    if not os.path.exists(path_):
394        os.makedirs(path_)
395    assert os.path.isdir(path_)
396
397@then(u'the directory "{directory}" should exist')
398def step_the_directory_should_exist(context, directory):
399    path_ = directory
400    if not os.path.isabs(directory):
401        path_ = os.path.join(context.workdir, os.path.normpath(directory))
402    assert_that(os.path.isdir(path_))
403
404@then(u'the directory "{directory}" should not exist')
405def step_the_directory_should_not_exist(context, directory):
406    path_ = directory
407    if not os.path.isabs(directory):
408        path_ = os.path.join(context.workdir, os.path.normpath(directory))
409    assert_that(not os.path.isdir(path_))
410
411@step(u'the directory "{directory}" exists')
412def step_directory_exists(context, directory):
413    """
414    Verifies that a directory exists.
415
416    .. code-block:: gherkin
417
418        Given the directory "abc.txt" exists
419         When the directory "abc.txt" exists
420    """
421    step_the_directory_should_exist(context, directory)
422
423@step(u'the directory "{directory}" does not exist')
424def step_directory_named_does_not_exist(context, directory):
425    """
426    Verifies that a directory does not exist.
427
428    .. code-block:: gherkin
429
430        Given the directory "abc/" does not exist
431         When the directory "abc/" does not exist
432    """
433    step_the_directory_should_not_exist(context, directory)
434
435# -----------------------------------------------------------------------------
436# FILE STEPS:
437# -----------------------------------------------------------------------------
438@step(u'a file named "{filename}" exists')
439def step_file_named_filename_exists(context, filename):
440    """
441    Verifies that a file with this filename exists.
442
443    .. code-block:: gherkin
444
445        Given a file named "abc.txt" exists
446         When a file named "abc.txt" exists
447    """
448    step_file_named_filename_should_exist(context, filename)
449
450@step(u'a file named "{filename}" does not exist')
451def step_file_named_filename_does_not_exist(context, filename):
452    """
453    Verifies that a file with this filename does not exist.
454
455    .. code-block:: gherkin
456
457        Given a file named "abc.txt" does not exist
458         When a file named "abc.txt" does not exist
459    """
460    step_file_named_filename_should_not_exist(context, filename)
461
462@then(u'a file named "{filename}" should exist')
463def step_file_named_filename_should_exist(context, filename):
464    command_util.ensure_workdir_exists(context)
465    filename_ = pathutil.realpath_with_context(filename, context)
466    assert_that(os.path.exists(filename_) and os.path.isfile(filename_))
467
468@then(u'a file named "{filename}" should not exist')
469def step_file_named_filename_should_not_exist(context, filename):
470    command_util.ensure_workdir_exists(context)
471    filename_ = pathutil.realpath_with_context(filename, context)
472    assert_that(not os.path.exists(filename_))
473
474# -----------------------------------------------------------------------------
475# STEPS FOR FILE CONTENTS:
476# -----------------------------------------------------------------------------
477@then(u'the file "{filename}" should contain "{text}"')
478def step_file_should_contain_text(context, filename, text):
479    expected_text = text
480    if "{__WORKDIR__}" in text or "{__CWD__}" in text:
481        expected_text = textutil.template_substitute(text,
482            __WORKDIR__ = posixpath_normpath(context.workdir),
483            __CWD__     = posixpath_normpath(os.getcwd())
484        )
485    file_contents = pathutil.read_file_contents(filename, context=context)
486    file_contents = file_contents.rstrip()
487    if file_contents_normalizer:
488        # -- HACK: Inject TextProcessor as text normalizer
489        file_contents = file_contents_normalizer(file_contents)
490    with on_assert_failed_print_details(file_contents, expected_text):
491        textutil.assert_normtext_should_contain(file_contents, expected_text)
492
493
494@then(u'the file "{filename}" should not contain "{text}"')
495def step_file_should_not_contain_text(context, filename, text):
496    file_contents = pathutil.read_file_contents(filename, context=context)
497    file_contents = file_contents.rstrip()
498    textutil.assert_normtext_should_not_contain(file_contents, text)
499    # XXX assert_that(file_contents, is_not(contains_string(text)))
500
501
502@then(u'the file "{filename}" should contain')
503def step_file_should_contain_multiline_text(context, filename):
504    assert context.text is not None, "REQUIRE: multiline text"
505    step_file_should_contain_text(context, filename, context.text)
506
507
508@then(u'the file "{filename}" should not contain')
509def step_file_should_not_contain_multiline_text(context, filename):
510    assert context.text is not None, "REQUIRE: multiline text"
511    step_file_should_not_contain_text(context, filename, context.text)
512
513
514# -----------------------------------------------------------------------------
515# ENVIRONMENT VARIABLES
516# -----------------------------------------------------------------------------
517@step(u'I set the environment variable "{env_name}" to "{env_value}"')
518def step_I_set_the_environment_variable_to(context, env_name, env_value):
519    if not hasattr(context, "environ"):
520        context.environ = {}
521    context.environ[env_name] = env_value
522    os.environ[env_name] = env_value
523
524@step(u'I remove the environment variable "{env_name}"')
525def step_I_remove_the_environment_variable(context, env_name):
526    if not hasattr(context, "environ"):
527        context.environ = {}
528    context.environ[env_name] = ""
529    os.environ[env_name] = ""
530    del context.environ[env_name]
531    del os.environ[env_name]
532
533