diff options
Diffstat (limited to 'test/integration/targets/filter_core')
14 files changed, 993 insertions, 0 deletions
diff --git a/test/integration/targets/filter_core/aliases b/test/integration/targets/filter_core/aliases new file mode 100644 index 0000000..3005e4b --- /dev/null +++ b/test/integration/targets/filter_core/aliases @@ -0,0 +1 @@ +shippable/posix/group4 diff --git a/test/integration/targets/filter_core/files/9851.txt b/test/integration/targets/filter_core/files/9851.txt new file mode 100644 index 0000000..70b1279 --- /dev/null +++ b/test/integration/targets/filter_core/files/9851.txt @@ -0,0 +1,3 @@ + [{ + "k": "Quotes \"'\n" +}] diff --git a/test/integration/targets/filter_core/files/fileglob/one.txt b/test/integration/targets/filter_core/files/fileglob/one.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/integration/targets/filter_core/files/fileglob/one.txt diff --git a/test/integration/targets/filter_core/files/fileglob/two.txt b/test/integration/targets/filter_core/files/fileglob/two.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/integration/targets/filter_core/files/fileglob/two.txt diff --git a/test/integration/targets/filter_core/files/foo.txt b/test/integration/targets/filter_core/files/foo.txt new file mode 100644 index 0000000..9bd9b63 --- /dev/null +++ b/test/integration/targets/filter_core/files/foo.txt @@ -0,0 +1,69 @@ +This is a test of various filter plugins found in Ansible (ex: core.py), and +not so much a test of the core filters in Jinja2. + +Dumping the same structure to YAML + +- this is a list element +- this: is a hash element in a list + warp: 9 + where: endor + + +Dumping the same structure to JSON, but don't pretty print + +["this is a list element", {"this": "is a hash element in a list", "warp": 9, "where": "endor"}] + +Dumping the same structure to YAML, but don't pretty print + +- this is a list element +- {this: is a hash element in a list, warp: 9, where: endor} + + +From a recorded task, the changed, failed, success, and skipped +tests are shortcuts to ask if those tasks produced changes, failed, +succeeded, or skipped (as one might guess). + +Changed = True +Failed = False +Success = True +Skipped = False + +The mandatory filter fails if a variable is not defined and returns the value. +To avoid breaking this test, this variable is already defined. + +a = 1 + +There are various casts available + +int = 1 +bool = True + +String quoting + +quoted = quoted + +The fileglob module returns the list of things matching a pattern. + +fileglob = one.txt, two.txt + +There are also various string operations that work on paths. These do not require +files to exist and are passthrus to the python os.path functions + +/etc/motd with basename = motd +/etc/motd with dirname = /etc + +path_join_simple = /etc/subdir/test +path_join_with_slash = /test +path_join_relative = etc/subdir/test + +TODO: realpath follows symlinks. There isn't a test for this just now. + +TODO: add tests for set theory operations like union + +regex_replace = bar +# Check regex_replace with multiline +#bar +#bart +regex_search = 0001 +regex_findall = ["car", "tar", "bar"] +regex_escape = \^f\.\*o\(\.\*\)\$ diff --git a/test/integration/targets/filter_core/handle_undefined_type_errors.yml b/test/integration/targets/filter_core/handle_undefined_type_errors.yml new file mode 100644 index 0000000..7062880 --- /dev/null +++ b/test/integration/targets/filter_core/handle_undefined_type_errors.yml @@ -0,0 +1,29 @@ +- hosts: localhost + gather_facts: false + tasks: + - debug: msg={{item}} + with_dict: '{{myundef}}' + when: + - myundef is defined + register: shouldskip + + - name: check if skipped + assert: + that: + - shouldskip is skipped + + - debug: msg={{item}} + loop: '{{myundef|dict2items}}' + when: + - myundef is defined + + - debug: msg={{item}} + with_dict: '{{myundef}}' + register: notskipped + ignore_errors: true + + - name: check it failed + assert: + that: + - notskipped is not skipped + - notskipped is failed diff --git a/test/integration/targets/filter_core/host_vars/localhost b/test/integration/targets/filter_core/host_vars/localhost new file mode 100644 index 0000000..a8926a5 --- /dev/null +++ b/test/integration/targets/filter_core/host_vars/localhost @@ -0,0 +1 @@ +a: 1 diff --git a/test/integration/targets/filter_core/meta/main.yml b/test/integration/targets/filter_core/meta/main.yml new file mode 100644 index 0000000..e430ea6 --- /dev/null +++ b/test/integration/targets/filter_core/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - role: setup_passlib + when: ansible_facts.distribution == 'MacOSX' diff --git a/test/integration/targets/filter_core/runme.sh b/test/integration/targets/filter_core/runme.sh new file mode 100755 index 0000000..c055603 --- /dev/null +++ b/test/integration/targets/filter_core/runme.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux + +ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@" +ANSIBLE_ROLES_PATH=../ ansible-playbook handle_undefined_type_errors.yml "$@" diff --git a/test/integration/targets/filter_core/runme.yml b/test/integration/targets/filter_core/runme.yml new file mode 100644 index 0000000..4af4b23 --- /dev/null +++ b/test/integration/targets/filter_core/runme.yml @@ -0,0 +1,3 @@ +- hosts: localhost + roles: + - { role: filter_core } diff --git a/test/integration/targets/filter_core/tasks/main.yml b/test/integration/targets/filter_core/tasks/main.yml new file mode 100644 index 0000000..2d08419 --- /dev/null +++ b/test/integration/targets/filter_core/tasks/main.yml @@ -0,0 +1,708 @@ +# test code for filters +# Copyright: (c) 2014, Michael DeHaan <michael.dehaan@gmail.com> +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Note: |groupby is already tested by the `groupby_filter` target. + +- set_fact: + output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}" + +- name: a dummy task to test the changed and success filters + shell: echo hi + register: some_registered_var + +- debug: + var: some_registered_var + +- name: Verify that we workaround a py26 json bug + template: + src: py26json.j2 + dest: "{{ output_dir }}/py26json.templated" + mode: 0644 + +- name: 9851 - Verify that we don't trigger https://github.com/ansible/ansible/issues/9851 + copy: + content: " [{{ item | to_nice_json }}]" + dest: "{{ output_dir }}/9851.out" + with_items: + - {"k": "Quotes \"'\n"} + +- name: 9851 - copy known good output into place + copy: + src: 9851.txt + dest: "{{ output_dir }}/9851.txt" + +- name: 9851 - Compare generated json to known good + shell: diff -w {{ output_dir }}/9851.out {{ output_dir }}/9851.txt + register: diff_result_9851 + +- name: 9851 - verify generated file matches known good + assert: + that: + - 'diff_result_9851.stdout == ""' + +- name: fill in a basic template + template: + src: foo.j2 + dest: "{{ output_dir }}/foo.templated" + mode: 0644 + register: template_result + +- name: copy known good into place + copy: + src: foo.txt + dest: "{{ output_dir }}/foo.txt" + +- name: compare templated file to known good + shell: diff -w {{ output_dir }}/foo.templated {{ output_dir }}/foo.txt + register: diff_result + +- name: verify templated file matches known good + assert: + that: + - 'diff_result.stdout == ""' + +- name: Test extract + assert: + that: + - '"c" == 2 | extract(["a", "b", "c"])' + - '"b" == 1 | extract(["a", "b", "c"])' + - '"a" == 0 | extract(["a", "b", "c"])' + +- name: Container lookups with extract + assert: + that: + - "'x' == [0]|map('extract',['x','y'])|list|first" + - "'y' == [1]|map('extract',['x','y'])|list|first" + - "42 == ['x']|map('extract',{'x':42,'y':31})|list|first" + - "31 == ['x','y']|map('extract',{'x':42,'y':31})|list|last" + - "'local' == ['localhost']|map('extract',hostvars,'ansible_connection')|list|first" + - "'local' == ['localhost']|map('extract',hostvars,['ansible_connection'])|list|first" + +- name: Test extract filter with defaults + vars: + container: + key: + subkey: value + assert: + that: + - "'key' | extract(badcontainer) | default('a') == 'a'" + - "'key' | extract(badcontainer, 'subkey') | default('a') == 'a'" + - "('key' | extract(badcontainer)).subkey | default('a') == 'a'" + - "'badkey' | extract(container) | default('a') == 'a'" + - "'badkey' | extract(container, 'subkey') | default('a') == 'a'" + - "('badkey' | extract(container)).subsubkey | default('a') == 'a'" + - "'key' | extract(container, 'badsubkey') | default('a') == 'a'" + - "'key' | extract(container, ['badsubkey', 'subsubkey']) | default('a') == 'a'" + - "('key' | extract(container, 'badsubkey')).subsubkey | default('a') == 'a'" + - "'badkey' | extract(hostvars) | default('a') == 'a'" + - "'badkey' | extract(hostvars, 'subkey') | default('a') == 'a'" + - "('badkey' | extract(hostvars)).subsubkey | default('a') == 'a'" + - "'localhost' | extract(hostvars, 'badsubkey') | default('a') == 'a'" + - "'localhost' | extract(hostvars, ['badsubkey', 'subsubkey']) | default('a') == 'a'" + - "('localhost' | extract(hostvars, 'badsubkey')).subsubkey | default('a') == 'a'" + +- name: Test hash filter + assert: + that: + - '"{{ "hash" | hash("sha1") }}" == "2346ad27d7568ba9896f1b7da6b5991251debdf2"' + - '"{{ "café" | hash("sha1") }}" == "f424452a9673918c6f09b0cdd35b20be8e6ae7d7"' + +- name: Test unsupported hash type + debug: + msg: "{{ 'hash' | hash('unsupported_hash_type') }}" + ignore_errors: yes + register: unsupported_hash_type_res + +- assert: + that: + - "unsupported_hash_type_res is failed" + - "'unsupported hash type' in unsupported_hash_type_res.msg" + +- name: Flatten tests + tags: flatten + block: + - name: use flatten + set_fact: + flat_full: '{{orig_list|flatten}}' + flat_one: '{{orig_list|flatten(levels=1)}}' + flat_two: '{{orig_list|flatten(levels=2)}}' + flat_tuples: '{{ [1,3] | zip([2,4]) | list | flatten }}' + flat_full_null: '{{list_with_nulls|flatten(skip_nulls=False)}}' + flat_one_null: '{{list_with_nulls|flatten(levels=1, skip_nulls=False)}}' + flat_two_null: '{{list_with_nulls|flatten(levels=2, skip_nulls=False)}}' + flat_full_nonull: '{{list_with_nulls|flatten(skip_nulls=True)}}' + flat_one_nonull: '{{list_with_nulls|flatten(levels=1, skip_nulls=True)}}' + flat_two_nonull: '{{list_with_nulls|flatten(levels=2, skip_nulls=True)}}' + + - name: Verify flatten filter works as expected + assert: + that: + - flat_full == [1, 2, 3, 4, 5, 6, 7] + - flat_one == [1, 2, 3, [4, [5]], 6, 7] + - flat_two == [1, 2, 3, 4, [5], 6, 7] + - flat_tuples == [1, 2, 3, 4] + - flat_full_null == [1, 'None', 3, 4, 5, 6, 7] + - flat_one_null == [1, 'None', 3, [4, [5]], 6, 7] + - flat_two_null == [1, 'None', 3, 4, [5], 6, 7] + - flat_full_nonull == [1, 3, 4, 5, 6, 7] + - flat_one_nonull == [1, 3, [4, [5]], 6, 7] + - flat_two_nonull == [1, 3, 4, [5], 6, 7] + - list_with_subnulls|flatten(skip_nulls=False) == [1, 2, 'None', 4, 5, 6, 7] + - list_with_subnulls|flatten(skip_nulls=True) == [1, 2, 4, 5, 6, 7] + vars: + orig_list: [1, 2, [3, [4, [5]], 6], 7] + list_with_nulls: [1, None, [3, [4, [5]], 6], 7] + list_with_subnulls: [1, 2, [None, [4, [5]], 6], 7] + +- name: Test base64 filter + assert: + that: + - "'Ansible - くらとみ\n' | b64encode == 'QW5zaWJsZSAtIOOBj+OCieOBqOOBvwo='" + - "'QW5zaWJsZSAtIOOBj+OCieOBqOOBvwo=' | b64decode == 'Ansible - くらとみ\n'" + - "'Ansible - くらとみ\n' | b64encode(encoding='utf-16-le') == 'QQBuAHMAaQBiAGwAZQAgAC0AIABPMIkwaDB/MAoA'" + - "'QQBuAHMAaQBiAGwAZQAgAC0AIABPMIkwaDB/MAoA' | b64decode(encoding='utf-16-le') == 'Ansible - くらとみ\n'" + +- set_fact: + x: + x: x + key: x + y: + y: y + key: y + z: + z: z + key: z + + # Most complicated combine dicts from the documentation + default: + a: + a': + x: default_value + y: default_value + list: + - default_value + b: + - 1 + - 1 + - 2 + - 3 + patch: + a: + a': + y: patch_value + z: patch_value + list: + - patch_value + b: + - 3 + - 4 + - 4 + - key: value + result: + a: + a': + x: default_value + y: patch_value + z: patch_value + list: + - default_value + - patch_value + b: + - 1 + - 1 + - 2 + - 3 + - 4 + - 4 + - key: value + +- name: Verify combine fails with extra kwargs + set_fact: + foo: "{{[1] | combine(foo='bar')}}" + ignore_errors: yes + register: combine_fail + +- name: Verify combine filter + assert: + that: + - "([x] | combine) == x" + - "(x | combine(y)) == {'x': 'x', 'y': 'y', 'key': 'y'}" + - "(x | combine(y, z)) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}" + - "([x, y, z] | combine) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}" + - "([x, y] | combine(z)) == {'x': 'x', 'y': 'y', 'z': 'z', 'key': 'z'}" + - "None|combine == {}" + # more advanced dict combination tests are done in the "merge_hash" function unit tests + # but even though it's redundant with those unit tests, we do at least the most complicated example of the documentation here + - "(default | combine(patch, recursive=True, list_merge='append_rp')) == result" + - combine_fail is failed + - "combine_fail.msg == \"'recursive' and 'list_merge' are the only valid keyword arguments\"" + +- set_fact: + combine: "{{[x, [y]] | combine(z)}}" + ignore_errors: yes + register: result + +- name: Ensure combining objects which aren't dictionaries throws an error + assert: + that: + - "result.msg.startswith(\"failed to combine variables, expected dicts but got\")" + +- name: Ensure combining two dictionaries containing undefined variables provides a helpful error + block: + - set_fact: + foo: + key1: value1 + + - set_fact: + combined: "{{ foo | combine({'key2': undef_variable}) }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.msg.startswith('The task includes an option with an undefined variable')" + + - set_fact: + combined: "{{ foo | combine({'key2': {'nested': [undef_variable]}})}}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.msg.startswith('The task includes an option with an undefined variable')" + +- name: regex_search + set_fact: + match_case: "{{ 'hello' | regex_search('HELLO', ignorecase=false) }}" + ignore_case: "{{ 'hello' | regex_search('HELLO', ignorecase=true) }}" + single_line: "{{ 'hello\nworld' | regex_search('^world', multiline=false) }}" + multi_line: "{{ 'hello\nworld' | regex_search('^world', multiline=true) }}" + named_groups: "{{ 'goodbye' | regex_search('(?P<first>good)(?P<second>bye)', '\\g<second>', '\\g<first>') }}" + numbered_groups: "{{ 'goodbye' | regex_search('(good)(bye)', '\\2', '\\1') }}" + no_match_is_none_inline: "{{ 'hello' | regex_search('world') == none }}" + +- name: regex_search unknown argument (failure expected) + set_fact: + unknown_arg: "{{ 'hello' | regex_search('hello', 'unknown') }}" + ignore_errors: yes + register: failure + +- name: regex_search check + assert: + that: + - match_case == '' + - ignore_case == 'hello' + - single_line == '' + - multi_line == 'world' + - named_groups == ['bye', 'good'] + - numbered_groups == ['bye', 'good'] + - no_match_is_none_inline + - failure is failed + +- name: Verify to_bool + assert: + that: + - 'None|bool == None' + - 'False|bool == False' + - '"TrUe"|bool == True' + - '"FalSe"|bool == False' + - '7|bool == False' + +- name: Verify to_datetime + assert: + that: + - '"1993-03-26 01:23:45"|to_datetime < "1994-03-26 01:23:45"|to_datetime' + +- name: strftime invalid argument (failure expected) + set_fact: + foo: "{{ '%Y' | strftime('foo') }}" + ignore_errors: yes + register: strftime_fail + +- name: Verify strftime + assert: + that: + - '"%Y-%m-%d"|strftime(1585247522) == "2020-03-26"' + - '"%Y-%m-%d"|strftime("1585247522.0") == "2020-03-26"' + - '("%Y"|strftime(None)).startswith("20")' # Current date, can't check much there. + - strftime_fail is failed + - '"Invalid value for epoch value" in strftime_fail.msg' + +- name: Verify case-insensitive regex_replace + assert: + that: + - '"hElLo there"|regex_replace("hello", "hi", ignorecase=True) == "hi there"' + +- name: Verify case-insensitive regex_findall + assert: + that: + - '"hEllo there heLlo haha HELLO there"|regex_findall("h.... ", ignorecase=True)|length == 3' + +- name: Verify ternary + assert: + that: + - 'True|ternary("seven", "eight") == "seven"' + - 'None|ternary("seven", "eight") == "eight"' + - 'None|ternary("seven", "eight", "nine") == "nine"' + - 'False|ternary("seven", "eight") == "eight"' + - '123|ternary("seven", "eight") == "seven"' + - '"haha"|ternary("seven", "eight") == "seven"' + +- name: Verify regex_escape raises on posix_extended (failure expected) + set_fact: + foo: '{{"]]^"|regex_escape(re_type="posix_extended")}}' + ignore_errors: yes + register: regex_escape_fail_1 + +- name: Verify regex_escape raises on other re_type (failure expected) + set_fact: + foo: '{{"]]^"|regex_escape(re_type="haha")}}' + ignore_errors: yes + register: regex_escape_fail_2 + +- name: Verify regex_escape with re_type other than 'python' + assert: + that: + - '"]]^"|regex_escape(re_type="posix_basic") == "\\]\\]\\^"' + - regex_escape_fail_1 is failed + - 'regex_escape_fail_1.msg == "Regex type (posix_extended) not yet implemented"' + - regex_escape_fail_2 is failed + - 'regex_escape_fail_2.msg == "Invalid regex type (haha)"' + +- name: Verify from_yaml and from_yaml_all + assert: + that: + - "'---\nbananas: yellow\napples: red'|from_yaml == {'bananas': 'yellow', 'apples': 'red'}" + - "2|from_yaml == 2" + - "'---\nbananas: yellow\n---\napples: red'|from_yaml_all|list == [{'bananas': 'yellow'}, {'apples': 'red'}]" + - "2|from_yaml_all == 2" + - "unsafe_fruit|from_yaml == {'bananas': 'yellow', 'apples': 'red'}" + - "unsafe_fruit_all|from_yaml_all|list == [{'bananas': 'yellow'}, {'apples': 'red'}]" + vars: + unsafe_fruit: !unsafe | + --- + bananas: yellow + apples: red + unsafe_fruit_all: !unsafe | + --- + bananas: yellow + --- + apples: red + +- name: Verify random raises on non-iterable input (failure expected) + set_fact: + foo: '{{None|random}}' + ignore_errors: yes + register: random_fail_1 + +- name: Verify random raises on iterable input with start (failure expected) + set_fact: + foo: '{{[1,2,3]|random(start=2)}}' + ignore_errors: yes + register: random_fail_2 + +- name: Verify random raises on iterable input with step (failure expected) + set_fact: + foo: '{{[1,2,3]|random(step=2)}}' + ignore_errors: yes + register: random_fail_3 + +- name: Verify random + assert: + that: + - '2|random in [0,1]' + - '2|random(seed=1337) in [0,1]' + - '["a", "b"]|random in ["a", "b"]' + - '20|random(start=10) in range(10, 20)' + - '20|random(start=10, step=2) % 2 == 0' + - random_fail_1 is failure + - '"random can only be used on" in random_fail_1.msg' + - random_fail_2 is failure + - '"start and step can only be used" in random_fail_2.msg' + - random_fail_3 is failure + - '"start and step can only be used" in random_fail_3.msg' + +# It's hard to actually verify much here since the result is, well, random. +- name: Verify randomize_list + assert: + that: + - '[1,3,5,7,9]|shuffle|length == 5' + - '[1,3,5,7,9]|shuffle(seed=1337)|length == 5' + - '22|shuffle == 22' + +- name: Verify password_hash throws on weird salt_size type + set_fact: + foo: '{{"hey"|password_hash(salt_size=[999])}}' + ignore_errors: yes + register: password_hash_1 + +- name: Verify password_hash throws on weird hashtype + set_fact: + foo: '{{"hey"|password_hash(hashtype="supersecurehashtype")}}' + ignore_errors: yes + register: password_hash_2 + +- name: Verify password_hash + assert: + that: + - "'what in the WORLD is up?'|password_hash|length == 120 or 'what in the WORLD is up?'|password_hash|length == 106" + # This throws a vastly different error on py2 vs py3, so we just check + # that it's a failure, not a substring of the exception. + - password_hash_1 is failed + - password_hash_2 is failed + - "'not support' in password_hash_2.msg" + +- name: Verify to_uuid throws on weird namespace + set_fact: + foo: '{{"hey"|to_uuid(namespace=22)}}' + ignore_errors: yes + register: to_uuid_1 + +- name: Verify to_uuid + assert: + that: + - '"monkeys"|to_uuid == "0d03a178-da0f-5b51-934e-cda9c76578c3"' + - to_uuid_1 is failed + - '"Invalid value" in to_uuid_1.msg' + +- name: Verify mandatory throws on undefined variable + set_fact: + foo: '{{hey|mandatory}}' + ignore_errors: yes + register: mandatory_1 + +- name: Verify mandatory throws on undefined variable with custom message + set_fact: + foo: '{{hey|mandatory("You did not give me a variable. I am a sad wolf.")}}' + ignore_errors: yes + register: mandatory_2 + +- name: Set a variable + set_fact: + mandatory_demo: 123 + +- name: Verify mandatory + assert: + that: + - '{{mandatory_demo|mandatory}} == 123' + - mandatory_1 is failed + - "mandatory_1.msg == \"Mandatory variable 'hey' not defined.\"" + - mandatory_2 is failed + - "mandatory_2.msg == 'You did not give me a variable. I am a sad wolf.'" + +- name: Verify undef throws if resolved + set_fact: + foo: '{{ fail_foo }}' + vars: + fail_foo: '{{ undef("Expected failure") }}' + ignore_errors: yes + register: fail_1 + +- name: Setup fail_foo for overriding in test + block: + - name: Verify undef not executed if overridden + set_fact: + foo: '{{ fail_foo }}' + vars: + fail_foo: 'overridden value' + register: fail_2 + vars: + fail_foo: '{{ undef(hint="Expected failure") }}' + +- name: Verify undef is inspectable + debug: + var: fail_foo + vars: + fail_foo: '{{ undef("Expected failure") }}' + register: fail_3 + +- name: Verify undef + assert: + that: + - fail_1 is failed + - not (fail_2 is failed) + - not (fail_3 is failed) + +- name: Verify comment + assert: + that: + - '"boo!"|comment == "#\n# boo!\n#"' + - '"boo!"|comment(decoration="-- ") == "--\n-- boo!\n--"' + - '"boo!"|comment(style="cblock") == "/*\n *\n * boo!\n *\n */"' + - '"boo!"|comment(decoration="") == "boo!\n"' + - '"boo!"|comment(prefix="\n", prefix_count=20) == "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n# boo!\n#"' + +- name: Verify subelements throws on invalid obj + set_fact: + foo: '{{True|subelements("foo")}}' + ignore_errors: yes + register: subelements_1 + +- name: Verify subelements throws on invalid subelements arg + set_fact: + foo: '{{{}|subelements(17)}}' + ignore_errors: yes + register: subelements_2 + +- name: Set demo data for subelements + set_fact: + subelements_demo: '{{ [{"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}] }}' + +- name: Verify subelements throws on bad key + set_fact: + foo: '{{subelements_demo | subelements("does not compute")}}' + ignore_errors: yes + register: subelements_3 + +- name: Verify subelements throws on key pointing to bad value + set_fact: + foo: '{{subelements_demo | subelements("name")}}' + ignore_errors: yes + register: subelements_4 + +- name: Verify subelements throws on list of keys ultimately pointing to bad value + set_fact: + foo: '{{subelements_demo | subelements(["groups", "authorized"])}}' + ignore_errors: yes + register: subelements_5 + +- name: Verify subelements + assert: + that: + - subelements_1 is failed + - 'subelements_1.msg == "obj must be a list of dicts or a nested dict"' + - subelements_2 is failed + - '"subelements must be a list or a string" in subelements_2.msg' + - 'subelements_demo|subelements("does not compute", skip_missing=True) == []' + - subelements_3 is failed + - '"could not find" in subelements_3.msg' + - subelements_4 is failed + - '"should point to a list" in subelements_4.msg' + - subelements_5 is failed + - '"should point to a dictionary" in subelements_5.msg' + - 'subelements_demo|subelements("groups") == [({"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}, "wheel")]' + - 'subelements_demo|subelements(["groups"]) == [({"name": "alice", "groups": ["wheel"], "authorized": ["/tmp/alice/onekey.pub"]}, "wheel")]' + + +- name: Verify dict2items throws on non-Mapping + set_fact: + foo: '{{True|dict2items}}' + ignore_errors: yes + register: dict2items_fail + +- name: Verify dict2items + assert: + that: + - '{"foo": "bar", "banana": "fruit"}|dict2items == [{"key": "foo", "value": "bar"}, {"key": "banana", "value": "fruit"}]' + - dict2items_fail is failed + - '"dict2items requires a dictionary" in dict2items_fail.msg' + +- name: Verify items2dict throws on non-list + set_fact: + foo: '{{True|items2dict}}' + ignore_errors: yes + register: items2dict_fail + +- name: Verify items2dict + assert: + that: + - '[{"key": "foo", "value": "bar"}, {"key": "banana", "value": "fruit"}]|items2dict == {"foo": "bar", "banana": "fruit"}' + - items2dict_fail is failed + - '"items2dict requires a list" in items2dict_fail.msg' + +- name: Verify items2dict throws on list of non-Mapping + set_fact: + foo: '{{[True]|items2dict}}' + ignore_errors: yes + register: items2dict_fail + +- name: Verify items2dict + assert: + that: + - items2dict_fail is failed + - '"items2dict requires a list of dictionaries" in items2dict_fail.msg' + +- name: Verify items2dict throws on missing key + set_fact: + foo: '{{ list_of_dicts | items2dict}}' + vars: + list_of_dicts: [{"key": "foo", "value": "bar"}, {"notkey": "banana", "value": "fruit"}] + ignore_errors: yes + register: items2dict_fail + +- name: Verify items2dict + assert: + that: + - items2dict_fail is failed + - error in items2dict_fail.msg + vars: + error: "items2dict requires each dictionary in the list to contain the keys 'key' and 'value'" + +- name: Verify items2dict throws on missing value + set_fact: + foo: '{{ list_of_dicts | items2dict}}' + vars: + list_of_dicts: [{"key": "foo", "value": "bar"}, {"key": "banana", "notvalue": "fruit"}] + ignore_errors: yes + register: items2dict_fail + +- name: Verify items2dict + assert: + that: + - items2dict_fail is failed + - error in items2dict_fail.msg + vars: + error: "items2dict requires each dictionary in the list to contain the keys 'key' and 'value'" + +- name: Verify path_join throws on non-string and non-sequence + set_fact: + foo: '{{True|path_join}}' + ignore_errors: yes + register: path_join_fail + +- name: Verify path_join + assert: + that: + - '"foo"|path_join == "foo"' + - '["foo", "bar"]|path_join in ["foo/bar", "foo\bar"]' + - path_join_fail is failed + - '"expects string or sequence" in path_join_fail.msg' + +- name: Verify type_debug + assert: + that: + - '"foo"|type_debug == "str"' + +- name: Assert that a jinja2 filter that produces a map is auto unrolled + assert: + that: + - thing|map(attribute="bar")|first == 123 + - thing_result|first == 123 + - thing_items|first|last == 123 + - thing_range == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + vars: + thing: + - bar: 123 + thing_result: '{{ thing|map(attribute="bar") }}' + thing_dict: + bar: 123 + thing_items: '{{ thing_dict.items() }}' + thing_range: '{{ range(10) }}' + +- name: Assert that quote works on None + assert: + that: + - thing|quote == "''" + vars: + thing: null + +- name: split filter + assert: + that: + - splitty|map('split', ',')|flatten|map('int') == [1, 2, 3, 4, 5, 6] + vars: + splitty: + - "1,2,3" + - "4,5,6" diff --git a/test/integration/targets/filter_core/templates/foo.j2 b/test/integration/targets/filter_core/templates/foo.j2 new file mode 100644 index 0000000..a69ba5e --- /dev/null +++ b/test/integration/targets/filter_core/templates/foo.j2 @@ -0,0 +1,62 @@ +This is a test of various filter plugins found in Ansible (ex: core.py), and +not so much a test of the core filters in Jinja2. + +Dumping the same structure to YAML + +{{ some_structure | to_nice_yaml }} + +Dumping the same structure to JSON, but don't pretty print + +{{ some_structure | to_json(sort_keys=true) }} + +Dumping the same structure to YAML, but don't pretty print + +{{ some_structure | to_yaml }} + +From a recorded task, the changed, failed, success, and skipped +tests are shortcuts to ask if those tasks produced changes, failed, +succeeded, or skipped (as one might guess). + +Changed = {{ some_registered_var is changed }} +Failed = {{ some_registered_var is failed }} +Success = {{ some_registered_var is successful }} +Skipped = {{ some_registered_var is skipped }} + +The mandatory filter fails if a variable is not defined and returns the value. +To avoid breaking this test, this variable is already defined. + +a = {{ a | mandatory }} + +There are various casts available + +int = {{ a | int }} +bool = {{ 1 | bool }} + +String quoting + +quoted = {{ 'quoted' | quote }} + +The fileglob module returns the list of things matching a pattern. + +fileglob = {{ (playbook_dir + '/files/fileglob/*') | fileglob | map('basename') | sort | join(', ') }} + +There are also various string operations that work on paths. These do not require +files to exist and are passthrus to the python os.path functions + +/etc/motd with basename = {{ '/etc/motd' | basename }} +/etc/motd with dirname = {{ '/etc/motd' | dirname }} + +path_join_simple = {{ ('/etc', 'subdir', 'test') | path_join }} +path_join_with_slash = {{ ('/etc', 'subdir', '/test') | path_join }} +path_join_relative = {{ ('etc', 'subdir', 'test') | path_join }} + +TODO: realpath follows symlinks. There isn't a test for this just now. + +TODO: add tests for set theory operations like union + +regex_replace = {{ 'foo' | regex_replace('^foo', 'bar') }} +# Check regex_replace with multiline +{{ '#foo\n#foot' | regex_replace('^#foo', '#bar', multiline=True) }} +regex_search = {{ 'test_value_0001' | regex_search('([0-9]+)$')}} +regex_findall = {{ 'car\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True)|to_json }} +regex_escape = {{ '^f.*o(.*)$' | regex_escape() }} diff --git a/test/integration/targets/filter_core/templates/py26json.j2 b/test/integration/targets/filter_core/templates/py26json.j2 new file mode 100644 index 0000000..dba62ad --- /dev/null +++ b/test/integration/targets/filter_core/templates/py26json.j2 @@ -0,0 +1,2 @@ +Provoke a python2.6 json bug +{{ hostvars[inventory_hostname] | to_nice_json }} diff --git a/test/integration/targets/filter_core/vars/main.yml b/test/integration/targets/filter_core/vars/main.yml new file mode 100644 index 0000000..aedecd8 --- /dev/null +++ b/test/integration/targets/filter_core/vars/main.yml @@ -0,0 +1,106 @@ +some_structure: + - "this is a list element" + - + this: "is a hash element in a list" + warp: 9 + where: endor + +other_data: + level1: + foo: bar + blip: baz + nested: + abc: def + ghi: xyz + alist: + - alpha + - beta + - charlie + - delta + level2: + asd: df + xc: dsdfsfsd + nested: + abc: foo + alist: + - zebra + - yellow + - xray + +# from https://github.com/ansible/ansible/issues/20379#issuecomment-280492883 +example_20379: { + "ApplicationVersions": [ + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "test-npm-check-626-1313", + "Description": "bla", + "DateCreated": "2017-01-22T02:02:31.798Z", + "DateUpdated": "2017-01-22T02:02:31.798Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-626-1313.war" + } + }, + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "terminate-611-1289", + "Description": "bla", + "DateCreated": "2017-01-20T00:34:29.864Z", + "DateUpdated": "2017-01-20T00:34:29.864Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-611-1289.war" + } + }, + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "terminate-610-1286", + "Description": "bla", + "DateCreated": "2017-01-20T00:22:02.229Z", + "DateUpdated": "2017-01-20T00:22:02.229Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-610-1286.war" + } + }, + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "master-609-1284", + "Description": "bla", + "DateCreated": "2017-01-19T23:54:32.902Z", + "DateUpdated": "2017-01-19T23:54:32.902Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-609-1284.war" + } + }, + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "master-608-1282", + "Description": "bla", + "DateCreated": "2017-01-19T23:02:44.902Z", + "DateUpdated": "2017-01-19T23:02:44.902Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-608-1282.war" + } + }, + { + "ApplicationName": "gitlab_ci_elasticbeanstalk", + "Status": "UNPROCESSED", + "VersionLabel": "master-606-1278", + "Description": "bla'", + "DateCreated": "2017-01-19T22:47:57.741Z", + "DateUpdated": "2017-01-19T22:47:57.741Z", + "SourceBundle": { + "S3Bucket": "bla", + "S3Key": "ci/beanstalk/gitlab_ci_elasticbeanstalk/gitlab_ci_elasticbeanstalk-606-1278.war" + } + } + ] +} |