diff options
Diffstat (limited to 'test')
354 files changed, 19510 insertions, 0 deletions
diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..2cd01f0 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +"""Use ansiblelint.testing instead for reusable tests.""" diff --git a/test/bar.txt b/test/bar.txt new file mode 100644 index 0000000..e22f90b --- /dev/null +++ b/test/bar.txt @@ -0,0 +1 @@ +Bar file diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..8ffa3bd --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,91 @@ +"""PyTest fixtures for testing the project.""" +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +# pylint: disable=wildcard-import,unused-wildcard-import +from ansiblelint.testing.fixtures import * # noqa: F403 +from ansiblelint.yaml_utils import FormattedYAML + +if TYPE_CHECKING: + from _pytest import nodes + from _pytest.config import Config + from _pytest.config.argparsing import Parser + + +def pytest_addoption(parser: Parser) -> None: + """Add --regenerate-formatting-fixtures option to pytest.""" + parser.addoption( + "--regenerate-formatting-fixtures", + action="store_true", + default=False, + help="Regenerate formatting fixtures with prettier and internal formatter", + ) + + +def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None: + """Skip tests based on --regenerate-formatting-fixtures option.""" + do_regenerate = config.getoption("--regenerate-formatting-fixtures") + skip_other = pytest.mark.skip( + reason="not a formatting_fixture test and " + "--regenerate-formatting-fixtures was specified", + ) + skip_formatting_fixture = pytest.mark.skip( + reason="specify --regenerate-formatting-fixtures to " + "only run formatting_fixtures test", + ) + for item in items: + if do_regenerate and "formatting_fixtures" not in item.keywords: + item.add_marker(skip_other) + elif not do_regenerate and "formatting_fixtures" in item.keywords: + item.add_marker(skip_formatting_fixture) + + +def pytest_configure(config: Config) -> None: + """Register custom markers.""" + if config.getoption("--regenerate-formatting-fixtures"): + regenerate_formatting_fixtures() + + +def regenerate_formatting_fixtures() -> None: + """Re-generate formatting fixtures with prettier and internal formatter. + + Pass ``--regenerate-formatting-fixtures`` to run this and skip all other tests. + This is a "test" because once fixtures are regenerated, + we run prettier again to make sure it does not change files formatted + with our internal formatting code. + """ + subprocess.check_call(["which", "prettier"]) + + yaml = FormattedYAML() + + fixtures_dir = Path("test/fixtures/") + fixtures_dir_before = fixtures_dir / "formatting-before" + fixtures_dir_prettier = fixtures_dir / "formatting-prettier" + fixtures_dir_after = fixtures_dir / "formatting-after" + + fixtures_dir_prettier.mkdir(exist_ok=True) + fixtures_dir_after.mkdir(exist_ok=True) + + # Copying before fixtures... + for fixture in fixtures_dir_before.glob("fmt-[0-9].yml"): + shutil.copy(str(fixture), str(fixtures_dir_prettier / fixture.name)) + shutil.copy(str(fixture), str(fixtures_dir_after / fixture.name)) + + # Writing fixtures with prettier... + subprocess.check_call(["prettier", "-w", str(fixtures_dir_prettier)]) + # NB: pre-commit end-of-file-fixer can also modify files. + + # Writing fixtures with ansiblelint.yaml_utils.FormattedYAML() + for fixture in fixtures_dir_after.glob("fmt-[0-9].yml"): + data = yaml.loads(fixture.read_text()) + output = yaml.dumps(data) + fixture.write_text(output) + + # Make sure prettier won't make changes in {fixtures_dir_after} + subprocess.check_call(["prettier", "-c", str(fixtures_dir_after)]) diff --git a/test/custom_rules/__init__.py b/test/custom_rules/__init__.py new file mode 100644 index 0000000..09a0f04 --- /dev/null +++ b/test/custom_rules/__init__.py @@ -0,0 +1 @@ +"""Dummy test module.""" diff --git a/test/custom_rules/example_com/__init__.py b/test/custom_rules/example_com/__init__.py new file mode 100644 index 0000000..a633c75 --- /dev/null +++ b/test/custom_rules/example_com/__init__.py @@ -0,0 +1 @@ +"""A dummy test module #2.""" diff --git a/test/custom_rules/example_com/example_com_rule.py b/test/custom_rules/example_com/example_com_rule.py new file mode 100644 index 0000000..abcb9dd --- /dev/null +++ b/test/custom_rules/example_com/example_com_rule.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020, Ansible Project +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""A dummy custom rule module #2.""" + +from ansiblelint.rules import AnsibleLintRule + + +class ExampleComRule(AnsibleLintRule): + """A dummy custom rule class.""" + + id = "100002" diff --git a/test/custom_rules/example_inc/__init__.py b/test/custom_rules/example_inc/__init__.py new file mode 100644 index 0000000..09a0f04 --- /dev/null +++ b/test/custom_rules/example_inc/__init__.py @@ -0,0 +1 @@ +"""Dummy test module.""" diff --git a/test/custom_rules/example_inc/custom_rule.py b/test/custom_rules/example_inc/custom_rule.py new file mode 100644 index 0000000..15c389f --- /dev/null +++ b/test/custom_rules/example_inc/custom_rule.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020, Ansible Project +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Dummy custom rule module.""" + +from ansiblelint.rules import AnsibleLintRule + + +class CustomRule(AnsibleLintRule): + """Dummy custom rule class.""" + + id = "100001" diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py new file mode 100644 index 0000000..6fc2115 --- /dev/null +++ b/test/fixtures/__init__.py @@ -0,0 +1 @@ +"""Fixtures used in tests.""" diff --git a/test/fixtures/ansible-config-invalid.yml b/test/fixtures/ansible-config-invalid.yml new file mode 100644 index 0000000..9eb8fe7 --- /dev/null +++ b/test/fixtures/ansible-config-invalid.yml @@ -0,0 +1,4 @@ +--- +# invalid .ansible-lint config file +- foo +- bar diff --git a/test/fixtures/ansible-config.yml b/test/fixtures/ansible-config.yml new file mode 100644 index 0000000..673d6f1 --- /dev/null +++ b/test/fixtures/ansible-config.yml @@ -0,0 +1,3 @@ +--- +verbosity: 1 +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/config-with-extra-vars.yml b/test/fixtures/config-with-extra-vars.yml new file mode 100644 index 0000000..5d06b6a --- /dev/null +++ b/test/fixtures/config-with-extra-vars.yml @@ -0,0 +1,4 @@ +--- +extra_vars: + foo: bar + knights_favorite_word: NI diff --git a/test/fixtures/config-with-relative-path.yml b/test/fixtures/config-with-relative-path.yml new file mode 100644 index 0000000..f396347 --- /dev/null +++ b/test/fixtures/config-with-relative-path.yml @@ -0,0 +1,4 @@ +--- +exclude_paths: + - ../../examples/roles/test-role/ +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/config-with-write-all.yml b/test/fixtures/config-with-write-all.yml new file mode 100644 index 0000000..a4242c5 --- /dev/null +++ b/test/fixtures/config-with-write-all.yml @@ -0,0 +1,3 @@ +--- +write_list: + - all diff --git a/test/fixtures/config-with-write-none.yml b/test/fixtures/config-with-write-none.yml new file mode 100644 index 0000000..5dacd38 --- /dev/null +++ b/test/fixtures/config-with-write-none.yml @@ -0,0 +1,3 @@ +--- +write_list: + - none diff --git a/test/fixtures/config-with-write-subset.yml b/test/fixtures/config-with-write-subset.yml new file mode 100644 index 0000000..f83149d --- /dev/null +++ b/test/fixtures/config-with-write-subset.yml @@ -0,0 +1,4 @@ +--- +write_list: + - rule-tag + - rule-id diff --git a/test/fixtures/exclude-paths-with-expands.yml b/test/fixtures/exclude-paths-with-expands.yml new file mode 100644 index 0000000..640563c --- /dev/null +++ b/test/fixtures/exclude-paths-with-expands.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - ~/.ansible/roles + - $HOME/.ansible/roles +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/exclude-paths.yml b/test/fixtures/exclude-paths.yml new file mode 100644 index 0000000..6af079e --- /dev/null +++ b/test/fixtures/exclude-paths.yml @@ -0,0 +1,4 @@ +--- +exclude_paths: + - ../ +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/formatting-after/fmt-1.yml b/test/fixtures/formatting-after/fmt-1.yml new file mode 100644 index 0000000..118a087 --- /dev/null +++ b/test/fixtures/formatting-after/fmt-1.yml @@ -0,0 +1,47 @@ +--- +# ^ too many newlines before +foo: bar # This is a comment has extra spaces preceding it + +fruits: # unindented sequence: + - apple + - orange +vegetables: # indented sequence: + - onion + - carrot + +quoting: + - that should have double quotes + - that should remain in single quotes + - a string with " inside + # next line has some undesired trailing spaces: + - a string with ' inside + - can't be sure! + # next line should be converted to use double quotes: + - [foo, bar] + +inline-dictionary: + - { foo: bar } # should add some spacing between curly braces and content + - { foo2: bar2 } # should reduce spacing between curly braces and content + +# YAML 1.1 Boolean-hell: https://yaml.org/type/bool.html +booleans-true: + preferred: true # YAML 1.2 compatible! + answer-1.1: true + canonical-1.1: true + canonical-upper-1.1: true + logical-1.1: true + option-1.1: true +booleans-false: + preferred: false # YAML 1.2 compatible! + answer-1.1: false + canonical-1.1: false + canonical-upper-1.1: false + logical-1.1: false + option-1.1: false + +# ^ double newline should be removed +overly-indented-vault-value: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 123466303630313 + +# this file also has 3 newlines at end-of-file instead of one diff --git a/test/fixtures/formatting-after/fmt-2.yml b/test/fixtures/formatting-after/fmt-2.yml new file mode 100644 index 0000000..a162721 --- /dev/null +++ b/test/fixtures/formatting-after/fmt-2.yml @@ -0,0 +1,24 @@ +# preamble/header comment +--- +# initial comment +- foo: bar + +- baz: # over indented + - qwerty + - foobar + animals: # under indented + - crow + - pig + - giraffe + +- nothing: # null + +- octal: + - "0o123" # YAML 1.2 octal + - "0123" # YAML 1.1 octal + +- integer: + - 0 # Not an octal. See #2071 + - 10 + - 9999 + zero: 0 # Not an octal. See #2071 diff --git a/test/fixtures/formatting-after/fmt-3.yml b/test/fixtures/formatting-after/fmt-3.yml new file mode 100644 index 0000000..d8106f7 --- /dev/null +++ b/test/fixtures/formatting-after/fmt-3.yml @@ -0,0 +1,21 @@ +--- +dummy_map: # eol comment + # full line comment not indented + something: + # full line comment indented + # next full line comment indented + - or + # 1 full line comments over indented + # 2 full line comments over indented + - other + - | + # this is part of a string not a yaml comment + # also not a comment + +# comment before top-level +second_key: + - {} # should drop the extra space in flow map + # comment before non top-level + - {} + # comment before non top-level + - [] diff --git a/test/fixtures/formatting-before/fmt-1.yml b/test/fixtures/formatting-before/fmt-1.yml new file mode 100644 index 0000000..0678111 --- /dev/null +++ b/test/fixtures/formatting-before/fmt-1.yml @@ -0,0 +1,53 @@ +--- + + + +# ^ too many newlines before +foo: bar # This is a comment has extra spaces preceding it + +fruits: # unindented sequence: +- apple +- orange +vegetables: # indented sequence: + - onion + - carrot + +quoting: + - 'that should have double quotes' + - 'that should remain in single quotes' + - 'a string with " inside' + # next line has some undesired trailing spaces: + - "a string with ' inside" + - can't be sure! + # next line should be converted to use double quotes: + - ['foo', 'bar'] + +inline-dictionary: + - {foo: bar} # should add some spacing between curly braces and content + - { foo2: bar2 } # should reduce spacing between curly braces and content + +# YAML 1.1 Boolean-hell: https://yaml.org/type/bool.html +booleans-true: + preferred: true # YAML 1.2 compatible! + answer-1.1: YES + canonical-1.1: y + canonical-upper-1.1: Y + logical-1.1: True + option-1.1: on +booleans-false: + preferred: false # YAML 1.2 compatible! + answer-1.1: NO + canonical-1.1: n + canonical-upper-1.1: N + logical-1.1: False + option-1.1: off + + +# ^ double newline should be removed +overly-indented-vault-value: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 123466303630313 + +# this file also has 3 newlines at end-of-file instead of one + + diff --git a/test/fixtures/formatting-before/fmt-2.yml b/test/fixtures/formatting-before/fmt-2.yml new file mode 100644 index 0000000..2941663 --- /dev/null +++ b/test/fixtures/formatting-before/fmt-2.yml @@ -0,0 +1,24 @@ +# preamble/header comment +--- +# initial comment + - foo: bar + + - baz: # over indented + - qwerty + - foobar + animals: # under indented + - crow + - pig + - giraffe + + - nothing: null # null + + - octal: + - 0o123 # YAML 1.2 octal + - 0123 # YAML 1.1 octal + + - integer: + - 0 # Not an octal. See #2071 + - 10 + - 9999 + zero: 0 # Not an octal. See #2071 diff --git a/test/fixtures/formatting-before/fmt-3.yml b/test/fixtures/formatting-before/fmt-3.yml new file mode 100644 index 0000000..c862cc4 --- /dev/null +++ b/test/fixtures/formatting-before/fmt-3.yml @@ -0,0 +1,21 @@ +--- +dummy_map: # eol comment +# full line comment not indented + something: + # full line comment indented + # next full line comment indented + - or + # 1 full line comments over indented + # 2 full line comments over indented + - other + - | + # this is part of a string not a yaml comment + # also not a comment + +# comment before top-level +second_key: + - { } # should drop the extra space in flow map +# comment before non top-level + - {} +# comment before non top-level + - [] diff --git a/test/fixtures/formatting-prettier/fmt-1.yml b/test/fixtures/formatting-prettier/fmt-1.yml new file mode 100644 index 0000000..d74c826 --- /dev/null +++ b/test/fixtures/formatting-prettier/fmt-1.yml @@ -0,0 +1,48 @@ +--- +# ^ too many newlines before +foo: bar # This is a comment has extra spaces preceding it + +fruits: # unindented sequence: + - apple + - orange +vegetables: # indented sequence: + - onion + - carrot + +quoting: + - "that should have double quotes" + - "that should remain in single quotes" + - 'a string with " inside' + # next line has some undesired trailing spaces: + - "a string with ' inside" + - can't be sure! + # next line should be converted to use double quotes: + - ["foo", "bar"] + +inline-dictionary: + - { foo: bar } # should add some spacing between curly braces and content + - { foo2: bar2 } # should reduce spacing between curly braces and content + +# YAML 1.1 Boolean-hell: https://yaml.org/type/bool.html +booleans-true: + preferred: true # YAML 1.2 compatible! + answer-1.1: YES + canonical-1.1: y + canonical-upper-1.1: Y + logical-1.1: True + option-1.1: on +booleans-false: + preferred: false # YAML 1.2 compatible! + answer-1.1: NO + canonical-1.1: n + canonical-upper-1.1: N + logical-1.1: False + option-1.1: off + +# ^ double newline should be removed +overly-indented-vault-value: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 123466303630313 + +# this file also has 3 newlines at end-of-file instead of one + diff --git a/test/fixtures/formatting-prettier/fmt-2.yml b/test/fixtures/formatting-prettier/fmt-2.yml new file mode 100644 index 0000000..90ac484 --- /dev/null +++ b/test/fixtures/formatting-prettier/fmt-2.yml @@ -0,0 +1,24 @@ +# preamble/header comment +--- +# initial comment +- foo: bar + +- baz: # over indented + - qwerty + - foobar + animals: # under indented + - crow + - pig + - giraffe + +- nothing: null # null + +- octal: + - "0o123" # YAML 1.2 octal + - "0123" # YAML 1.1 octal + +- integer: + - 0 # Not an octal. See #2071 + - 10 + - 9999 + zero: 0 # Not an octal. See #2071 diff --git a/test/fixtures/formatting-prettier/fmt-3.yml b/test/fixtures/formatting-prettier/fmt-3.yml new file mode 100644 index 0000000..658d550 --- /dev/null +++ b/test/fixtures/formatting-prettier/fmt-3.yml @@ -0,0 +1,21 @@ +--- +dummy_map: # eol comment + # full line comment not indented + something: + # full line comment indented + # next full line comment indented + - or + # 1 full line comments over indented + # 2 full line comments over indented + - other + - | + # this is part of a string not a yaml comment + # also not a comment + +# comment before top-level +second_key: + - {} # should drop the extra space in flow map + # comment before non top-level + - {} + # comment before non top-level + - [] diff --git a/test/fixtures/list-rules-tests/.yamllint b/test/fixtures/list-rules-tests/.yamllint new file mode 100644 index 0000000..d9e1a25 --- /dev/null +++ b/test/fixtures/list-rules-tests/.yamllint @@ -0,0 +1,2 @@ +--- +{} diff --git a/test/fixtures/parseable.yml b/test/fixtures/parseable.yml new file mode 100644 index 0000000..a1f661a --- /dev/null +++ b/test/fixtures/parseable.yml @@ -0,0 +1,3 @@ +--- +parseable: true +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/quiet.yml b/test/fixtures/quiet.yml new file mode 100644 index 0000000..9bacbc6 --- /dev/null +++ b/test/fixtures/quiet.yml @@ -0,0 +1,3 @@ +--- +quiet: true +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/rulesdir-defaults.yml b/test/fixtures/rulesdir-defaults.yml new file mode 100644 index 0000000..c8884bb --- /dev/null +++ b/test/fixtures/rulesdir-defaults.yml @@ -0,0 +1,5 @@ +--- +rulesdir: + - ./rules +use_default_rules: true +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/rulesdir.yml b/test/fixtures/rulesdir.yml new file mode 100644 index 0000000..77c4c3d --- /dev/null +++ b/test/fixtures/rulesdir.yml @@ -0,0 +1,4 @@ +--- +rulesdir: + - ./rules +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/show-abspath.yml b/test/fixtures/show-abspath.yml new file mode 100644 index 0000000..367caff --- /dev/null +++ b/test/fixtures/show-abspath.yml @@ -0,0 +1,3 @@ +--- +display_relative_path: false +# vim: et:sw=2:syntax=2:ts=2: diff --git a/test/fixtures/show-relpath.yml b/test/fixtures/show-relpath.yml new file mode 100644 index 0000000..684f209 --- /dev/null +++ b/test/fixtures/show-relpath.yml @@ -0,0 +1,3 @@ +--- +display_relative_path: true +# vim: et:sw=2:syntax=2:ts=2: diff --git a/test/fixtures/skip-tags.yml b/test/fixtures/skip-tags.yml new file mode 100644 index 0000000..b9c215b --- /dev/null +++ b/test/fixtures/skip-tags.yml @@ -0,0 +1,4 @@ +--- +skip_list: + - bad_tag +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/strict.yml b/test/fixtures/strict.yml new file mode 100644 index 0000000..00e7aad --- /dev/null +++ b/test/fixtures/strict.yml @@ -0,0 +1,3 @@ +--- +strict: true +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml new file mode 100644 index 0000000..70dd1b1 --- /dev/null +++ b/test/fixtures/tags.yml @@ -0,0 +1,4 @@ +--- +tags: + - skip_ansible_lint +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/fixtures/unknown-type.yml b/test/fixtures/unknown-type.yml new file mode 100644 index 0000000..54c6d2b --- /dev/null +++ b/test/fixtures/unknown-type.yml @@ -0,0 +1,2 @@ +--- +some: map diff --git a/test/fixtures/verbosity-tests/.yamllint b/test/fixtures/verbosity-tests/.yamllint new file mode 100644 index 0000000..d9e1a25 --- /dev/null +++ b/test/fixtures/verbosity-tests/.yamllint @@ -0,0 +1,2 @@ +--- +{} diff --git a/test/fixtures/verbosity-tests/tasks/main.yml b/test/fixtures/verbosity-tests/tasks/main.yml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/fixtures/verbosity-tests/tasks/main.yml diff --git a/test/fixtures/verbosity.yml b/test/fixtures/verbosity.yml new file mode 100644 index 0000000..673d6f1 --- /dev/null +++ b/test/fixtures/verbosity.yml @@ -0,0 +1,3 @@ +--- +verbosity: 1 +# vim: et:sw=2:syntax=yaml:ts=2: diff --git a/test/foo.txt b/test/foo.txt new file mode 100644 index 0000000..94f8f1a --- /dev/null +++ b/test/foo.txt @@ -0,0 +1 @@ +Foo file diff --git a/test/local-content/README.md b/test/local-content/README.md new file mode 100644 index 0000000..2b6322a --- /dev/null +++ b/test/local-content/README.md @@ -0,0 +1,6 @@ +The reason that every roles test gets its own directory is that while they +use the same three roles, the way the tests work makes sure that when the +second one runs, the roles and their local plugins from the first test are +still known to Ansible. For that reason, their names reflect the directory +they are in to make sure that tests don't use modules/plugins found by +other tests. diff --git a/test/local-content/collections/ansible_collections/testns/test_collection/galaxy.yml b/test/local-content/collections/ansible_collections/testns/test_collection/galaxy.yml new file mode 100644 index 0000000..4cbaa67 --- /dev/null +++ b/test/local-content/collections/ansible_collections/testns/test_collection/galaxy.yml @@ -0,0 +1,4 @@ +--- +namespace: testns +name: test_collection +version: 0.1.0 diff --git a/test/local-content/collections/ansible_collections/testns/test_collection/plugins/filter/test_filter.py b/test/local-content/collections/ansible_collections/testns/test_collection/plugins/filter/test_filter.py new file mode 100644 index 0000000..58bc269 --- /dev/null +++ b/test/local-content/collections/ansible_collections/testns/test_collection/plugins/filter/test_filter.py @@ -0,0 +1,17 @@ +"""A filter plugin.""" +# pylint: disable=invalid-name + + +def a_test_filter(a, b): + """Return a string containing both a and b.""" + return f"{a}:{b}" + + +# pylint: disable=too-few-public-methods +class FilterModule: + """Filter plugin.""" + + @staticmethod + def filters(): + """Return filters.""" + return {"test_filter": a_test_filter} diff --git a/test/local-content/collections/ansible_collections/testns/test_collection/plugins/modules/test_module_2.py b/test/local-content/collections/ansible_collections/testns/test_collection/plugins/modules/test_module_2.py new file mode 100755 index 0000000..a63d06d --- /dev/null +++ b/test/local-content/collections/ansible_collections/testns/test_collection/plugins/modules/test_module_2.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 2!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-collection.yml b/test/local-content/test-collection.yml new file mode 100644 index 0000000..47b097d --- /dev/null +++ b/test/local-content/test-collection.yml @@ -0,0 +1,10 @@ +--- +- name: Use module and filter plugin from local collection + hosts: localhost + tasks: + - name: Use module from local collection + testns.test_collection.test_module_2: + - name: Use filter from local collection + ansible.builtin.assert: + that: + - 1 | testns.test_collection.test_filter(2) == '1:2' diff --git a/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py new file mode 100755 index 0000000..d9012a7 --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 1!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml new file mode 100644 index 0000000..680dcab --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 1 + test_module_1_failed_complete: diff --git a/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml new file mode 100644 index 0000000..8646f6b --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_1_failed_complete: +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_3_failed_complete: +- name: Use local test plugin + assert: + that: + - "'2' is b_test_failed_complete '12345'" diff --git a/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py new file mode 100644 index 0000000..92bd6e7 --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py @@ -0,0 +1,19 @@ +"""A test plugin.""" +# pylint: disable=invalid-name + + +def compatibility_in_test(a, b): + """Return True when a is contained in b.""" + return a in b + + +# pylint: disable=too-few-public-methods +class TestModule: + """Test plugin.""" + + @staticmethod + def tests(): + """Return tests.""" + return { + "b_test_failed_complete": compatibility_in_test, + } diff --git a/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py new file mode 100755 index 0000000..4d9de0e --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 3!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml new file mode 100644 index 0000000..7a36734 --- /dev/null +++ b/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 3 + test_module_3_failed_complete: diff --git a/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py b/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py new file mode 100755 index 0000000..d9012a7 --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 1!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-failed/roles/role1/tasks/main.yml b/test/local-content/test-roles-failed/roles/role1/tasks/main.yml new file mode 100644 index 0000000..257493a --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role1/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 1 + test_module_1_failed: diff --git a/test/local-content/test-roles-failed/roles/role2/tasks/main.yml b/test/local-content/test-roles-failed/roles/role2/tasks/main.yml new file mode 100644 index 0000000..48daca6 --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role2/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_1_failed: +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_3_failed: +- name: Use local test plugin + assert: + that: + - "'2' is b_test_failed '12345'" diff --git a/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py b/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py new file mode 100644 index 0000000..4bb6167 --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py @@ -0,0 +1,18 @@ +"""A test plugin.""" + + +def compatibility_in_test(element, container): + """Return True when element is contained in container.""" + return element in container + + +# pylint: disable=too-few-public-methods +class TestModule: + """Test plugin.""" + + @staticmethod + def tests(): + """Return tests.""" + return { + "b_test_failed": compatibility_in_test, + } diff --git a/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py b/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py new file mode 100755 index 0000000..4d9de0e --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 3!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-failed/roles/role3/tasks/main.yml b/test/local-content/test-roles-failed/roles/role3/tasks/main.yml new file mode 100644 index 0000000..ad17eb0 --- /dev/null +++ b/test/local-content/test-roles-failed/roles/role3/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 3 + test_module_3_failed: diff --git a/test/local-content/test-roles-failed/test.yml b/test/local-content/test-roles-failed/test.yml new file mode 100644 index 0000000..08ff0f6 --- /dev/null +++ b/test/local-content/test-roles-failed/test.yml @@ -0,0 +1,7 @@ +--- +- name: Use roles with local module in wrong order, so that Ansible fails + hosts: localhost + roles: + - role2 + - role3 + - role1 diff --git a/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py b/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py new file mode 100755 index 0000000..d9012a7 --- /dev/null +++ b/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 1!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-success/roles/role1/tasks/main.yml b/test/local-content/test-roles-success/roles/role1/tasks/main.yml new file mode 100644 index 0000000..ba920af --- /dev/null +++ b/test/local-content/test-roles-success/roles/role1/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 1 + test_module_1_success: diff --git a/test/local-content/test-roles-success/roles/role2/tasks/main.yml b/test/local-content/test-roles-success/roles/role2/tasks/main.yml new file mode 100644 index 0000000..a540cf1 --- /dev/null +++ b/test/local-content/test-roles-success/roles/role2/tasks/main.yml @@ -0,0 +1,11 @@ +--- +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_1_success: +- name: Use local module from other role that has been included before this one + # If it has not been included before, loading this role fails! + test_module_3_success: +- name: Use local test plugin + assert: + that: + - "'2' is b_test_success '12345'" diff --git a/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py b/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py new file mode 100644 index 0000000..6cf2bae --- /dev/null +++ b/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py @@ -0,0 +1,18 @@ +"""A test plugin.""" + + +def compatibility_in_test(element, container): + """Return True when element contained in container.""" + return element in container + + +# pylint: disable=too-few-public-methods +class TestModule: + """Test plugin.""" + + @staticmethod + def tests(): + """Return tests.""" + return { + "b_test_success": compatibility_in_test, + } diff --git a/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py b/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py new file mode 100755 index 0000000..4d9de0e --- /dev/null +++ b/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +"""A module.""" + +from ansible.module_utils.basic import AnsibleModule + + +def main() -> None: + """Execute module.""" + module = AnsibleModule({}) + module.exit_json(msg="Hello 3!") + + +if __name__ == "__main__": + main() diff --git a/test/local-content/test-roles-success/roles/role3/tasks/main.yml b/test/local-content/test-roles-success/roles/role3/tasks/main.yml new file mode 100644 index 0000000..c77a7c8 --- /dev/null +++ b/test/local-content/test-roles-success/roles/role3/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Use local module 3 + test_module_3_success: diff --git a/test/rules/__init__.py b/test/rules/__init__.py new file mode 100644 index 0000000..28b581d --- /dev/null +++ b/test/rules/__init__.py @@ -0,0 +1 @@ +"""Tests for specific rules.""" diff --git a/test/rules/fixtures/__init__.py b/test/rules/fixtures/__init__.py new file mode 100644 index 0000000..d049bf0 --- /dev/null +++ b/test/rules/fixtures/__init__.py @@ -0,0 +1,3 @@ +"""Test rules resources.""" + +__all__ = ["ematcher", "raw_task", "unset_variable_matcher"] diff --git a/test/rules/fixtures/ematcher.py b/test/rules/fixtures/ematcher.py new file mode 100644 index 0000000..1b04b6b --- /dev/null +++ b/test/rules/fixtures/ematcher.py @@ -0,0 +1,15 @@ +"""Custom rule used as fixture.""" +from ansiblelint.rules import AnsibleLintRule + + +class EMatcherRule(AnsibleLintRule): + """BANNED string found.""" + + id = "TEST0001" + description = ( + "This is a test custom rule that looks for lines containing BANNED string" + ) + tags = ["fake", "dummy", "test1"] + + def match(self, line: str) -> bool: + return "BANNED" in line diff --git a/test/rules/fixtures/raw_task.md b/test/rules/fixtures/raw_task.md new file mode 100644 index 0000000..2aa6d22 --- /dev/null +++ b/test/rules/fixtures/raw_task.md @@ -0,0 +1,3 @@ +# raw-task + +This is a test rule that looks in a raw task to flag raw action params. diff --git a/test/rules/fixtures/raw_task.py b/test/rules/fixtures/raw_task.py new file mode 100644 index 0000000..0d5b023 --- /dev/null +++ b/test/rules/fixtures/raw_task.py @@ -0,0 +1,30 @@ +"""Test Rule that needs_raw_task.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ansiblelint.rules import AnsibleLintRule + +if TYPE_CHECKING: + from ansiblelint.file_utils import Lintable + from ansiblelint.utils import Task + + +class RawTaskRule(AnsibleLintRule): + """Test rule that inspects the raw task.""" + + id = "raw-task" + shortdesc = "Test rule that inspects the raw task" + tags = ["fake", "dummy", "test3"] + needs_raw_task = True + + def matchtask( + self, + task: Task, + file: Lintable | None = None, + ) -> bool | str: + """Match a task using __raw_task__ to inspect the module params type.""" + raw_task = task["__raw_task__"] + module = task["action"]["__ansible_module_original__"] + found_raw_task_params = not isinstance(raw_task[module], dict) + return found_raw_task_params diff --git a/test/rules/fixtures/unset_variable_matcher.py b/test/rules/fixtures/unset_variable_matcher.py new file mode 100644 index 0000000..8486009 --- /dev/null +++ b/test/rules/fixtures/unset_variable_matcher.py @@ -0,0 +1,15 @@ +"""Custom linting rule used as test fixture.""" +from ansiblelint.rules import AnsibleLintRule + + +class UnsetVariableMatcherRule(AnsibleLintRule): + """Line contains untemplated variable.""" + + id = "TEST0002" + description = ( + "This is a test rule that looks for lines post templating that still contain {{" + ) + tags = ["fake", "dummy", "test2"] + + def match(self, line: str) -> bool: + return "{{" in line diff --git a/test/rules/test_deprecated_module.py b/test/rules/test_deprecated_module.py new file mode 100644 index 0000000..a57d8db --- /dev/null +++ b/test/rules/test_deprecated_module.py @@ -0,0 +1,27 @@ +"""Tests for deprecated-module rule.""" +from pathlib import Path + +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.deprecated_module import DeprecatedModuleRule +from ansiblelint.testing import RunFromText + +MODULE_DEPRECATED = """ +- name: Task example + docker: + debug: test +""" + + +def test_module_deprecated(tmp_path: Path) -> None: + """Test for deprecated-module.""" + collection = RulesCollection() + collection.register(DeprecatedModuleRule()) + runner = RunFromText(collection) + results = runner.run_role_tasks_main(MODULE_DEPRECATED, tmp_path=tmp_path) + assert len(results) == 1 + # based on version and blend of ansible being used, we may + # get a missing module, so we future proof the test + assert ( + "couldn't resolve module" not in results[0].message + or "Deprecated module" not in results[0].message + ) diff --git a/test/rules/test_inline_env_var.py b/test/rules/test_inline_env_var.py new file mode 100644 index 0000000..98f337e --- /dev/null +++ b/test/rules/test_inline_env_var.py @@ -0,0 +1,90 @@ +"""Tests for inline-env-var rule.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.inline_env_var import EnvVarsInCommandRule +from ansiblelint.testing import RunFromText + +SUCCESS_PLAY_TASKS = """ +- hosts: localhost + + tasks: + - name: Actual use of environment + shell: echo $HELLO + environment: + HELLO: hello + + - name: Use some key-value pairs + command: chdir=/tmp creates=/tmp/bobbins warn=no touch bobbins + + - name: Commands can have flags + command: abc --xyz=def blah + + - name: Commands can have equals in them + command: echo "===========" + + - name: Commands with cmd + command: + cmd: + echo "-------" + + - name: Command with stdin (ansible > 2.4) + command: /bin/cat + args: + stdin: "Hello, world!" + + - name: Use argv to send the command as a list + command: + argv: + - /bin/echo + - Hello + - World + + - name: Another use of argv + command: + args: + argv: + - echo + - testing + + - name: Environment variable with shell + shell: HELLO=hello echo $HELLO + + - name: Command with stdin_add_newline (ansible > 2.8) + command: /bin/cat + args: + stdin: "Hello, world!" + stdin_add_newline: false + + - name: Command with strip_empty_ends (ansible > 2.8) + command: echo + args: + strip_empty_ends: false +""" + +FAIL_PLAY_TASKS = """ +- hosts: localhost + + tasks: + - name: Environment variable with command + command: HELLO=hello echo $HELLO + + - name: Typo some stuff + command: cerates=/tmp/blah warn=no touch /tmp/blah +""" + + +def test_success() -> None: + """Positive test for inline-env-var.""" + collection = RulesCollection() + collection.register(EnvVarsInCommandRule()) + runner = RunFromText(collection) + results = runner.run_playbook(SUCCESS_PLAY_TASKS) + assert len(results) == 0 + + +def test_fail() -> None: + """Negative test for inline-env-var.""" + collection = RulesCollection() + collection.register(EnvVarsInCommandRule()) + runner = RunFromText(collection) + results = runner.run_playbook(FAIL_PLAY_TASKS) + assert len(results) == 2 diff --git a/test/rules/test_no_changed_when.py b/test/rules/test_no_changed_when.py new file mode 100644 index 0000000..c89d8f4 --- /dev/null +++ b/test/rules/test_no_changed_when.py @@ -0,0 +1,23 @@ +"""Tests for no-change-when rule.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.no_changed_when import CommandHasChangesCheckRule +from ansiblelint.runner import Runner + + +def test_command_changes_positive() -> None: + """Positive test for no-changed-when.""" + collection = RulesCollection() + collection.register(CommandHasChangesCheckRule()) + success = "examples/playbooks/command-check-success.yml" + good_runner = Runner(success, rules=collection) + assert [] == good_runner.run() + + +def test_command_changes_negative() -> None: + """Negative test for no-changed-when.""" + collection = RulesCollection() + collection.register(CommandHasChangesCheckRule()) + failure = "examples/playbooks/command-check-failure.yml" + bad_runner = Runner(failure, rules=collection) + errs = bad_runner.run() + assert len(errs) == 2 diff --git a/test/rules/test_package_latest.py b/test/rules/test_package_latest.py new file mode 100644 index 0000000..5631f02 --- /dev/null +++ b/test/rules/test_package_latest.py @@ -0,0 +1,23 @@ +"""Tests for package-latest rule.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.package_latest import PackageIsNotLatestRule +from ansiblelint.runner import Runner + + +def test_package_not_latest_positive() -> None: + """Positive test for package-latest.""" + collection = RulesCollection() + collection.register(PackageIsNotLatestRule()) + success = "examples/playbooks/package-check-success.yml" + good_runner = Runner(success, rules=collection) + assert [] == good_runner.run() + + +def test_package_not_latest_negative() -> None: + """Negative test for package-latest.""" + collection = RulesCollection() + collection.register(PackageIsNotLatestRule()) + failure = "examples/playbooks/package-check-failure.yml" + bad_runner = Runner(failure, rules=collection) + errs = bad_runner.run() + assert len(errs) == 4 diff --git a/test/rules/test_role_names.py b/test/rules/test_role_names.py new file mode 100644 index 0000000..491cf14 --- /dev/null +++ b/test/rules/test_role_names.py @@ -0,0 +1,91 @@ +"""Test the RoleNames rule.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.role_name import RoleNames +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from pathlib import Path + + from _pytest.fixtures import SubRequest + +ROLE_NAME_VALID = "test_role" + +TASK_MINIMAL = """ +- name: Some task + ping: +""" + +ROLE_MINIMAL = {"tasks": {"main.yml": TASK_MINIMAL}} +ROLE_META_EMPTY = {"meta": {"main.yml": ""}} + +ROLE_WITH_EMPTY_META = {**ROLE_MINIMAL, **ROLE_META_EMPTY} + +PLAY_INCLUDE_ROLE = f""" +- hosts: all + roles: + - {ROLE_NAME_VALID} +""" + + +@pytest.fixture(name="test_rules_collection") +def fixture_test_rules_collection() -> RulesCollection: + """Instantiate a roles collection for tests.""" + collection = RulesCollection() + collection.register(RoleNames()) + return collection + + +def dict_to_files(parent_dir: Path, file_dict: dict[str, Any]) -> None: + """Write a nested dict to a file and directory structure below parent_dir.""" + for file, content in file_dict.items(): + if isinstance(content, dict): + directory = parent_dir / file + directory.mkdir() + dict_to_files(directory, content) + else: + (parent_dir / file).write_text(content) + + +@pytest.fixture(name="playbook_path") +def fixture_playbook_path(request: SubRequest, tmp_path: Path) -> str: + """Create a playbook with a role in a temporary directory.""" + playbook_text = request.param[0] + role_name = request.param[1] + role_layout = request.param[2] + role_path = tmp_path / role_name + role_path.mkdir() + dict_to_files(role_path, role_layout) + play_path = tmp_path / "playbook.yml" + play_path.write_text(playbook_text) + return str(play_path) + + +@pytest.mark.parametrize( + ("playbook_path", "messages"), + ( + pytest.param( + (PLAY_INCLUDE_ROLE, ROLE_NAME_VALID, ROLE_WITH_EMPTY_META), + [], + id="ROLE_EMPTY_META", + ), + ), + indirect=("playbook_path",), +) +def test_role_name( + test_rules_collection: RulesCollection, + playbook_path: str, + messages: list[str], +) -> None: + """Lint a playbook and compare the expected messages with the actual messages.""" + runner = Runner(playbook_path, rules=test_rules_collection) + results = runner.run() + assert len(results) == len(messages) + results_text = str(results) + for message in messages: + assert message in results_text diff --git a/test/rules/test_syntax_check.py b/test/rules/test_syntax_check.py new file mode 100644 index 0000000..2fe36a3 --- /dev/null +++ b/test/rules/test_syntax_check.py @@ -0,0 +1,70 @@ +"""Tests for syntax-check rule.""" +from typing import Any + +from ansiblelint.file_utils import Lintable +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_get_ansible_syntax_check_matches( + default_rules_collection: RulesCollection, +) -> None: + """Validate parsing of ansible output.""" + lintable = Lintable( + "examples/playbooks/conflicting_action.yml", + kind="playbook", + ) + + result = Runner(lintable, rules=default_rules_collection).run() + + assert result[0].lineno == 4 + assert result[0].column == 7 + assert ( + result[0].message + == "conflicting action statements: ansible.builtin.debug, ansible.builtin.command" + ) + # We internally convert absolute paths returned by ansible into paths + # relative to current directory. + assert result[0].filename.endswith("/conflicting_action.yml") + assert len(result) == 1 + + +def test_empty_playbook(default_rules_collection: RulesCollection) -> None: + """Validate detection of empty-playbook.""" + lintable = Lintable("examples/playbooks/empty_playbook.yml", kind="playbook") + result = Runner(lintable, rules=default_rules_collection).run() + assert result[0].lineno == 1 + # We internally convert absolute paths returned by ansible into paths + # relative to current directory. + assert result[0].filename.endswith("/empty_playbook.yml") + assert result[0].tag == "syntax-check[empty-playbook]" + assert result[0].message == "Empty playbook, nothing to do" + assert len(result) == 1 + + +def test_extra_vars_passed_to_command( + default_rules_collection: RulesCollection, + config_options: Any, +) -> None: + """Validate `extra-vars` are passed to syntax check command.""" + config_options.extra_vars = { + "foo": "bar", + "complex_variable": ":{;\t$()", + } + lintable = Lintable("examples/playbooks/extra_vars.yml", kind="playbook") + + result = Runner(lintable, rules=default_rules_collection).run() + + assert not result + + +def test_syntax_check_role() -> None: + """Validate syntax check of a broken role.""" + lintable = Lintable("examples/playbooks/roles/invalid_due_syntax", kind="role") + rules = RulesCollection() + result = Runner(lintable, rules=rules).run() + assert len(result) == 1, result + assert result[0].lineno == 2 + assert result[0].filename == "examples/roles/invalid_due_syntax/tasks/main.yml" + assert result[0].tag == "syntax-check[specific]" + assert result[0].message == "no module/action detected in task." diff --git a/test/schemas/.mocharc.json b/test/schemas/.mocharc.json new file mode 100644 index 0000000..0148197 --- /dev/null +++ b/test/schemas/.mocharc.json @@ -0,0 +1,7 @@ +{ + "colors": true, + "extension": ["ts"], + "require": "ts-node/register", + "slow": "500", + "spec": "src/**/*.spec.ts" +} diff --git a/test/schemas/f b/test/schemas/f new file mode 120000 index 0000000..ae8ff29 --- /dev/null +++ b/test/schemas/f @@ -0,0 +1 @@ +../../src/ansiblelint/schemas
\ No newline at end of file diff --git a/test/schemas/negative_test/.ansible-lint b/test/schemas/negative_test/.ansible-lint new file mode 100644 index 0000000..86b5116 --- /dev/null +++ b/test/schemas/negative_test/.ansible-lint @@ -0,0 +1,4 @@ +--- +# .ansible-lint +rules: + Wrong_Rule_name: diff --git a/test/schemas/negative_test/.ansible-lint.md b/test/schemas/negative_test/.ansible-lint.md new file mode 100644 index 0000000..f1f2308 --- /dev/null +++ b/test/schemas/negative_test/.ansible-lint.md @@ -0,0 +1,139 @@ +# ajv errors + +```json +[ + { + "instancePath": "/rules", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "command-instead-of-module", + "command-instead-of-shell", + "deprecated-bare-vars", + "deprecated-local-action", + "deprecated-module", + "empty-string-compare", + "fqcn", + "fqcn[action-core]", + "fqcn[action]", + "fqcn[canonical]", + "fqcn[keyword]", + "galaxy", + "galaxy[no-changelog]", + "galaxy[no-runtime]", + "galaxy[tags]", + "galaxy[version-incorrect]", + "galaxy[version-missing]", + "ignore-errors", + "inline-env-var", + "internal-error", + "jinja", + "jinja[invalid]", + "jinja[spacing]", + "key-order", + "latest", + "literal-compare", + "load-failure", + "load-failure[not-found]", + "loop-var-prefix", + "loop-var-prefix[missing]", + "loop-var-prefix[wrong]", + "meta-incorrect", + "meta-no-tags", + "meta-runtime", + "meta-video-links", + "name", + "name[casing]", + "name[play]", + "name[prefix]", + "name[template]", + "no-changed-when", + "no-handler", + "no-jinja-when", + "no-log-password", + "no-prompting", + "no-relative-paths", + "no-same-owner", + "no-tabs", + "only-builtins", + "package-latest", + "parser-error", + "partial-become", + "playbook-extension", + "risky-file-permissions", + "risky-octal", + "risky-shell-pipe", + "role-name", + "run-once", + "run-once[play]", + "run-once[task]", + "sanity", + "sanity[bad-ignore]", + "sanity[cannot-ignore]", + "schema", + "syntax-check", + "var-naming", + "yaml" + ] + }, + "propertyName": "Wrong_Rule_name", + "schemaPath": "#/properties/rules/propertyNames/oneOf/0/enum" + }, + { + "instancePath": "/rules", + "keyword": "pattern", + "message": "must match pattern \"^[a-z0-9-\\[\\]]+$\"", + "params": { + "pattern": "^[a-z0-9-\\[\\]]+$" + }, + "propertyName": "Wrong_Rule_name", + "schemaPath": "#/properties/rules/propertyNames/oneOf/1/pattern" + }, + { + "instancePath": "/rules", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "propertyName": "Wrong_Rule_name", + "schemaPath": "#/properties/rules/propertyNames/oneOf" + }, + { + "instancePath": "/rules", + "keyword": "propertyNames", + "message": "property name must be valid", + "params": { + "propertyName": "Wrong_Rule_name" + }, + "schemaPath": "#/properties/rules/propertyNames" + }, + { + "instancePath": "/rules/Wrong_Rule_name", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/$defs/rule/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [], + "parse_errors": [ + { + "filename": "negative_test/.ansible-lint", + "message": "Failed to parse negative_test/.ansible-lint" + } + ] +} +``` diff --git a/test/schemas/negative_test/.config/ansible-lint.yml b/test/schemas/negative_test/.config/ansible-lint.yml new file mode 100644 index 0000000..c12a2ef --- /dev/null +++ b/test/schemas/negative_test/.config/ansible-lint.yml @@ -0,0 +1,3 @@ +--- +# .ansible-lint +profile: invalid_profile diff --git a/test/schemas/negative_test/.config/ansible-lint.yml.md b/test/schemas/negative_test/.config/ansible-lint.yml.md new file mode 100644 index 0000000..4fe331e --- /dev/null +++ b/test/schemas/negative_test/.config/ansible-lint.yml.md @@ -0,0 +1,42 @@ +# ajv errors + +```json +[ + { + "instancePath": "/profile", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "min", + "basic", + "moderate", + "safety", + "shared", + "production", + null + ] + }, + "schemaPath": "#/properties/profile/enum" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/.config/ansible-lint.yml", + "path": "$.profile", + "message": "'invalid_profile' is not one of ['min', 'basic', 'moderate', 'safety', 'shared', 'production', None]", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml b/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml new file mode 100644 index 0000000..2639e9a --- /dev/null +++ b/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml @@ -0,0 +1,4 @@ +--- +releases: + 1.0.0: + release_date: 01-01-2020 # invalid date format, must be ISO-8601 ! diff --git a/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml.md b/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml.md new file mode 100644 index 0000000..72b4f96 --- /dev/null +++ b/test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml.md @@ -0,0 +1,40 @@ +# ajv errors + +```json +[ + { + "instancePath": "/releases/1.0.0/release_date", + "keyword": "pattern", + "message": "must match pattern \"\\d\\d\\d\\d-\\d\\d-\\d\\d\"", + "params": { + "pattern": "\\d\\d\\d\\d-\\d\\d-\\d\\d" + }, + "schemaPath": "#/properties/release_date/pattern" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/changelogs/invalid-date/changelogs/changelog.yaml", + "path": "$.releases.1.0.0.release_date", + "message": "'01-01-2020' is not a 'date'", + "has_sub_errors": false + }, + { + "filename": "negative_test/changelogs/invalid-date/changelogs/changelog.yaml", + "path": "$.releases.1.0.0.release_date", + "message": "'01-01-2020' does not match '\\\\d\\\\d\\\\d\\\\d-\\\\d\\\\d-\\\\d\\\\d'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml b/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml new file mode 100644 index 0000000..99632a4 --- /dev/null +++ b/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml @@ -0,0 +1,8 @@ +--- +releases: + 1.0.0: + plugins: + lookup: + - name: reverse + description: Reverse magic + namespace: "foo" # namespace must be null for plugins and objects diff --git a/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml.md b/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml.md new file mode 100644 index 0000000..ef847c3 --- /dev/null +++ b/test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/releases/1.0.0/plugins/lookup/0/namespace", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/$defs/plugin-descriptions/items/properties/namespace/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml", + "path": "$.releases.1.0.0.plugins.lookup[0].namespace", + "message": "'foo' is not of type 'null'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml b/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml new file mode 100644 index 0000000..72def5b --- /dev/null +++ b/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml @@ -0,0 +1,4 @@ +--- +- this is invalid +- as changelog must be object (mapping) +- not an array (sequence) diff --git a/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml.md b/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml.md new file mode 100644 index 0000000..5938944 --- /dev/null +++ b/test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/changelogs/list/changelogs/changelog.yaml", + "path": "$", + "message": "['this is invalid', 'as changelog must be object (mapping)', 'not an array (sequence)'] is not of type 'object'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml b/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml new file mode 100644 index 0000000..d08ebd0 --- /dev/null +++ b/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml @@ -0,0 +1,2 @@ +--- +releases: foo # <-- not a semver diff --git a/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml.md b/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml.md new file mode 100644 index 0000000..64c4665 --- /dev/null +++ b/test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/releases", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/properties/releases/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/changelogs/no-semver/changelogs/changelog.yaml", + "path": "$.releases", + "message": "'foo' is not of type 'object'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml b/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml new file mode 100644 index 0000000..a97e4e2 --- /dev/null +++ b/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml @@ -0,0 +1,2 @@ +--- +release: {} # <- unknown key, correct would be releases diff --git a/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml.md b/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml.md new file mode 100644 index 0000000..490bdbe --- /dev/null +++ b/test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "release" + }, + "schemaPath": "#/additionalProperties" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/changelogs/unknown-keys/changelogs/changelog.yaml", + "path": "$", + "message": "Additional properties are not allowed ('release' was unexpected)", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/galaxy_1/galaxy.yml b/test/schemas/negative_test/galaxy_1/galaxy.yml new file mode 100644 index 0000000..914d219 --- /dev/null +++ b/test/schemas/negative_test/galaxy_1/galaxy.yml @@ -0,0 +1,12 @@ +name: foo +namespace: bar +version: 1.2.3 +authors: + - John +readme: ../README.md +description: ... +repository: https://www.github.com/my_org/my_collection +manifest: + directive: # <-- typo, should be "directives" + - "foo" + omit_default_directives: true diff --git a/test/schemas/negative_test/galaxy_1/galaxy.yml.md b/test/schemas/negative_test/galaxy_1/galaxy.yml.md new file mode 100644 index 0000000..bbb79ec --- /dev/null +++ b/test/schemas/negative_test/galaxy_1/galaxy.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/manifest", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "directive" + }, + "schemaPath": "#/properties/manifest/additionalProperties" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/galaxy_1/galaxy.yml", + "path": "$.manifest", + "message": "Additional properties are not allowed ('directive' was unexpected)", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/inventory/broken_dev_inventory.yml b/test/schemas/negative_test/inventory/broken_dev_inventory.yml new file mode 100644 index 0000000..ce84309 --- /dev/null +++ b/test/schemas/negative_test/inventory/broken_dev_inventory.yml @@ -0,0 +1,10 @@ +--- +# See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html +ungrouped: {} +all: + hosts: + mail.example.com: + children: + foo: {} # <-- invalid based on inventory json schema + vars: {} +webservers: {} diff --git a/test/schemas/negative_test/inventory/broken_dev_inventory.yml.md b/test/schemas/negative_test/inventory/broken_dev_inventory.yml.md new file mode 100644 index 0000000..d4fefaf --- /dev/null +++ b/test/schemas/negative_test/inventory/broken_dev_inventory.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/all", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "foo" + }, + "schemaPath": "#/$defs/special-group/additionalProperties" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/inventory/broken_dev_inventory.yml", + "path": "$.all", + "message": "Additional properties are not allowed ('foo' was unexpected)", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/meta/runtime.yml b/test/schemas/negative_test/meta/runtime.yml new file mode 100644 index 0000000..c143dc6 --- /dev/null +++ b/test/schemas/negative_test/meta/runtime.yml @@ -0,0 +1 @@ +requires_ansible: ">= 2.12" # invalid as space is not allowed! diff --git a/test/schemas/negative_test/meta/runtime.yml.md b/test/schemas/negative_test/meta/runtime.yml.md new file mode 100644 index 0000000..761fa6f --- /dev/null +++ b/test/schemas/negative_test/meta/runtime.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/requires_ansible", + "keyword": "pattern", + "message": "must match pattern \"^[^\\s]*$\"", + "params": { + "pattern": "^[^\\s]*$" + }, + "schemaPath": "#/properties/requires_ansible/pattern" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/meta/runtime.yml", + "path": "$.requires_ansible", + "message": "'>= 2.12' does not match '^[^\\\\s]*$'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/molecule/platforms_children/molecule.yml b/test/schemas/negative_test/molecule/platforms_children/molecule.yml new file mode 100644 index 0000000..6800584 --- /dev/null +++ b/test/schemas/negative_test/molecule/platforms_children/molecule.yml @@ -0,0 +1,5 @@ +driver: + name: delegated +platforms: + - name: foo + children: 2 # invalid, must be list of strings diff --git a/test/schemas/negative_test/molecule/platforms_children/molecule.yml.md b/test/schemas/negative_test/molecule/platforms_children/molecule.yml.md new file mode 100644 index 0000000..68e09eb --- /dev/null +++ b/test/schemas/negative_test/molecule/platforms_children/molecule.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/platforms/0/children", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/children/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/molecule/platforms_children/molecule.yml", + "path": "$.platforms[0].children", + "message": "2 is not of type 'array'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/molecule/platforms_networks/molecule.yml b/test/schemas/negative_test/molecule/platforms_networks/molecule.yml new file mode 100644 index 0000000..4ae9799 --- /dev/null +++ b/test/schemas/negative_test/molecule/platforms_networks/molecule.yml @@ -0,0 +1,7 @@ +driver: + name: docker +platforms: + - name: docker + networks: # invalid, must be list of dictionaries + - foo + - bar diff --git a/test/schemas/negative_test/molecule/platforms_networks/molecule.yml.md b/test/schemas/negative_test/molecule/platforms_networks/molecule.yml.md new file mode 100644 index 0000000..74b8de7 --- /dev/null +++ b/test/schemas/negative_test/molecule/platforms_networks/molecule.yml.md @@ -0,0 +1,49 @@ +# ajv errors + +```json +[ + { + "instancePath": "/platforms/0/networks/0", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/$defs/platform-network/type" + }, + { + "instancePath": "/platforms/0/networks/1", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/$defs/platform-network/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/molecule/platforms_networks/molecule.yml", + "path": "$.platforms[0].networks[0]", + "message": "'foo' is not of type 'object'", + "has_sub_errors": false + }, + { + "filename": "negative_test/molecule/platforms_networks/molecule.yml", + "path": "$.platforms[0].networks[1]", + "message": "'bar' is not of type 'object'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/environment.yml b/test/schemas/negative_test/playbooks/environment.yml new file mode 100644 index 0000000..2064aca --- /dev/null +++ b/test/schemas/negative_test/playbooks/environment.yml @@ -0,0 +1,3 @@ +--- +- hosts: localhost + environment: "{{ foo }}-123" # <- invalid only a full jinja string is allowed, or a list of strings diff --git a/test/schemas/negative_test/playbooks/environment.yml.md b/test/schemas/negative_test/playbooks/environment.yml.md new file mode 100644 index 0000000..8923cb3 --- /dev/null +++ b/test/schemas/negative_test/playbooks/environment.yml.md @@ -0,0 +1,138 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "environment" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/environment", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/anyOf/0/type" + }, + { + "instancePath": "/0/environment", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/environment", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/environment.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'environment': '{{ foo }}-123'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'environment', 'hosts' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'environment', 'hosts' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'environment': '{{ foo }}-123'} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].environment", + "message": "'{{ foo }}-123' is not valid under any of the given schemas" + }, + { + "path": "$[0].environment", + "message": "'{{ foo }}-123' is not of type 'object'" + }, + { + "path": "$[0].environment", + "message": "'{{ foo }}-123' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/failed_when.yml b/test/schemas/negative_test/playbooks/failed_when.yml new file mode 100644 index 0000000..59b7272 --- /dev/null +++ b/test/schemas/negative_test/playbooks/failed_when.yml @@ -0,0 +1,6 @@ +- hosts: localhost + tasks: + - name: foo + ansible.builtin.debug: + msg: foo! + failed_when: 123 # <- not ok diff --git a/test/schemas/negative_test/playbooks/failed_when.yml.md b/test/schemas/negative_test/playbooks/failed_when.yml.md new file mode 100644 index 0000000..e843e1f --- /dev/null +++ b/test/schemas/negative_test/playbooks/failed_when.yml.md @@ -0,0 +1,177 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/failed_when.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'foo', 'ansible.builtin.debug': {'msg': 'foo!'}, 'failed_when': 123}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'foo', 'ansible.builtin.debug': {'msg': 'foo!'}, 'failed_when': 123}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'name': 'foo', 'ansible.builtin.debug': {'msg': 'foo!'}, 'failed_when': 123} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/gather_facts.yml b/test/schemas/negative_test/playbooks/gather_facts.yml new file mode 100644 index 0000000..d1b1345 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_facts.yml @@ -0,0 +1,6 @@ +--- +- hosts: localhost + gather_facts: non + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/negative_test/playbooks/gather_facts.yml.md b/test/schemas/negative_test/playbooks/gather_facts.yml.md new file mode 100644 index 0000000..0eb3a4b --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_facts.yml.md @@ -0,0 +1,123 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_facts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/gather_facts", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/properties/gather_facts/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/gather_facts.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_facts': 'non', 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_facts', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_facts', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_facts': 'non', 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].gather_facts", + "message": "'non' is not of type 'boolean'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/gather_subset.yml b/test/schemas/negative_test/playbooks/gather_subset.yml new file mode 100644 index 0000000..455d683 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset.yml @@ -0,0 +1,6 @@ +--- +- hosts: localhost + gather_subset: all + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/negative_test/playbooks/gather_subset.yml.md b/test/schemas/negative_test/playbooks/gather_subset.yml.md new file mode 100644 index 0000000..b426a23 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset.yml.md @@ -0,0 +1,123 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_subset" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/gather_subset", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/gather_subset/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/gather_subset.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': 'all', 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': 'all', 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].gather_subset", + "message": "'all' is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/gather_subset2.yml b/test/schemas/negative_test/playbooks/gather_subset2.yml new file mode 100644 index 0000000..d5a39ae --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset2.yml @@ -0,0 +1,7 @@ +--- +- hosts: localhost + gather_subset: + - invalid + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/negative_test/playbooks/gather_subset2.yml.md b/test/schemas/negative_test/playbooks/gather_subset2.yml.md new file mode 100644 index 0000000..8d6be68 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset2.yml.md @@ -0,0 +1,277 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_subset" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "all", + "min", + "all_ipv4_addresses", + "all_ipv6_addresses", + "apparmor", + "architecture", + "caps", + "chroot,cmdline", + "date_time", + "default_ipv4", + "default_ipv6", + "devices", + "distribution", + "distribution_major_version", + "distribution_release", + "distribution_version", + "dns", + "effective_group_ids", + "effective_user_id", + "env", + "facter", + "fips", + "hardware", + "interfaces", + "is_chroot", + "iscsi", + "kernel", + "local", + "lsb", + "machine", + "machine_id", + "mounts", + "network", + "ohai", + "os_family", + "pkg_mgr", + "platform", + "processor", + "processor_cores", + "processor_count", + "python", + "python_version", + "real_user_id", + "selinux", + "service_mgr", + "ssh_host_key_dsa_public", + "ssh_host_key_ecdsa_public", + "ssh_host_key_ed25519_public", + "ssh_host_key_rsa_public", + "ssh_host_pub_keys", + "ssh_pub_keys", + "system", + "system_capabilities", + "system_capabilities_enforced", + "user", + "user_dir", + "user_gecos", + "user_gid", + "user_id", + "user_shell", + "user_uid", + "virtual", + "virtualization_role", + "virtualization_type" + ] + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/0/enum" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "!all", + "!min", + "!all_ipv4_addresses", + "!all_ipv6_addresses", + "!apparmor", + "!architecture", + "!caps", + "!chroot,cmdline", + "!date_time", + "!default_ipv4", + "!default_ipv6", + "!devices", + "!distribution", + "!distribution_major_version", + "!distribution_release", + "!distribution_version", + "!dns", + "!effective_group_ids", + "!effective_user_id", + "!env", + "!facter", + "!fips", + "!hardware", + "!interfaces", + "!is_chroot", + "!iscsi", + "!kernel", + "!local", + "!lsb", + "!machine", + "!machine_id", + "!mounts", + "!network", + "!ohai", + "!os_family", + "!pkg_mgr", + "!platform", + "!processor", + "!processor_cores", + "!processor_count", + "!python", + "!python_version", + "!real_user_id", + "!selinux", + "!service_mgr", + "!ssh_host_key_dsa_public", + "!ssh_host_key_ecdsa_public", + "!ssh_host_key_ed25519_public", + "!ssh_host_key_rsa_public", + "!ssh_host_pub_keys", + "!ssh_pub_keys", + "!system", + "!system_capabilities", + "!system_capabilities_enforced", + "!user", + "!user_dir", + "!user_gecos", + "!user_gid", + "!user_id", + "!user_shell", + "!user_uid", + "!virtual", + "!virtualization_role", + "!virtualization_type" + ] + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/1/enum" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/properties/gather_subset/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/gather_subset2.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': ['invalid'], 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': ['invalid'], 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].gather_subset[0]", + "message": "'invalid' is not valid under any of the given schemas" + }, + { + "path": "$[0].gather_subset[0]", + "message": "'invalid' is not one of ['all', 'min', 'all_ipv4_addresses', 'all_ipv6_addresses', 'apparmor', 'architecture', 'caps', 'chroot,cmdline', 'date_time', 'default_ipv4', 'default_ipv6', 'devices', 'distribution', 'distribution_major_version', 'distribution_release', 'distribution_version', 'dns', 'effective_group_ids', 'effective_user_id', 'env', 'facter', 'fips', 'hardware', 'interfaces', 'is_chroot', 'iscsi', 'kernel', 'local', 'lsb', 'machine', 'machine_id', 'mounts', 'network', 'ohai', 'os_family', 'pkg_mgr', 'platform', 'processor', 'processor_cores', 'processor_count', 'python', 'python_version', 'real_user_id', 'selinux', 'service_mgr', 'ssh_host_key_dsa_public', 'ssh_host_key_ecdsa_public', 'ssh_host_key_ed25519_public', 'ssh_host_key_rsa_public', 'ssh_host_pub_keys', 'ssh_pub_keys', 'system', 'system_capabilities', 'system_capabilities_enforced', 'user', 'user_dir', 'user_gecos', 'user_gid', 'user_id', 'user_shell', 'user_uid', 'virtual', 'virtualization_role', 'virtualization_type']" + }, + { + "path": "$[0].gather_subset[0]", + "message": "'invalid' is not one of ['!all', '!min', '!all_ipv4_addresses', '!all_ipv6_addresses', '!apparmor', '!architecture', '!caps', '!chroot,cmdline', '!date_time', '!default_ipv4', '!default_ipv6', '!devices', '!distribution', '!distribution_major_version', '!distribution_release', '!distribution_version', '!dns', '!effective_group_ids', '!effective_user_id', '!env', '!facter', '!fips', '!hardware', '!interfaces', '!is_chroot', '!iscsi', '!kernel', '!local', '!lsb', '!machine', '!machine_id', '!mounts', '!network', '!ohai', '!os_family', '!pkg_mgr', '!platform', '!processor', '!processor_cores', '!processor_count', '!python', '!python_version', '!real_user_id', '!selinux', '!service_mgr', '!ssh_host_key_dsa_public', '!ssh_host_key_ecdsa_public', '!ssh_host_key_ed25519_public', '!ssh_host_key_rsa_public', '!ssh_host_pub_keys', '!ssh_pub_keys', '!system', '!system_capabilities', '!system_capabilities_enforced', '!user', '!user_dir', '!user_gecos', '!user_gid', '!user_id', '!user_shell', '!user_uid', '!virtual', '!virtualization_role', '!virtualization_type']" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/gather_subset3.yml b/test/schemas/negative_test/playbooks/gather_subset3.yml new file mode 100644 index 0000000..05e4028 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset3.yml @@ -0,0 +1,7 @@ +--- +- hosts: localhost + gather_subset: + - 1 + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/negative_test/playbooks/gather_subset3.yml.md b/test/schemas/negative_test/playbooks/gather_subset3.yml.md new file mode 100644 index 0000000..7dc1b13 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset3.yml.md @@ -0,0 +1,303 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_subset" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/0/type" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "all", + "min", + "all_ipv4_addresses", + "all_ipv6_addresses", + "apparmor", + "architecture", + "caps", + "chroot,cmdline", + "date_time", + "default_ipv4", + "default_ipv6", + "devices", + "distribution", + "distribution_major_version", + "distribution_release", + "distribution_version", + "dns", + "effective_group_ids", + "effective_user_id", + "env", + "facter", + "fips", + "hardware", + "interfaces", + "is_chroot", + "iscsi", + "kernel", + "local", + "lsb", + "machine", + "machine_id", + "mounts", + "network", + "ohai", + "os_family", + "pkg_mgr", + "platform", + "processor", + "processor_cores", + "processor_count", + "python", + "python_version", + "real_user_id", + "selinux", + "service_mgr", + "ssh_host_key_dsa_public", + "ssh_host_key_ecdsa_public", + "ssh_host_key_ed25519_public", + "ssh_host_key_rsa_public", + "ssh_host_pub_keys", + "ssh_pub_keys", + "system", + "system_capabilities", + "system_capabilities_enforced", + "user", + "user_dir", + "user_gecos", + "user_gid", + "user_id", + "user_shell", + "user_uid", + "virtual", + "virtualization_role", + "virtualization_type" + ] + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/0/enum" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/1/type" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "!all", + "!min", + "!all_ipv4_addresses", + "!all_ipv6_addresses", + "!apparmor", + "!architecture", + "!caps", + "!chroot,cmdline", + "!date_time", + "!default_ipv4", + "!default_ipv6", + "!devices", + "!distribution", + "!distribution_major_version", + "!distribution_release", + "!distribution_version", + "!dns", + "!effective_group_ids", + "!effective_user_id", + "!env", + "!facter", + "!fips", + "!hardware", + "!interfaces", + "!is_chroot", + "!iscsi", + "!kernel", + "!local", + "!lsb", + "!machine", + "!machine_id", + "!mounts", + "!network", + "!ohai", + "!os_family", + "!pkg_mgr", + "!platform", + "!processor", + "!processor_cores", + "!processor_count", + "!python", + "!python_version", + "!real_user_id", + "!selinux", + "!service_mgr", + "!ssh_host_key_dsa_public", + "!ssh_host_key_ecdsa_public", + "!ssh_host_key_ed25519_public", + "!ssh_host_key_rsa_public", + "!ssh_host_pub_keys", + "!ssh_pub_keys", + "!system", + "!system_capabilities", + "!system_capabilities_enforced", + "!user", + "!user_dir", + "!user_gecos", + "!user_gid", + "!user_id", + "!user_shell", + "!user_uid", + "!virtual", + "!virtualization_role", + "!virtualization_type" + ] + }, + "schemaPath": "#/properties/gather_subset/items/anyOf/1/enum" + }, + { + "instancePath": "/0/gather_subset/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/properties/gather_subset/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/gather_subset3.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': [1], 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': [1], 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].gather_subset[0]", + "message": "1 is not valid under any of the given schemas" + }, + { + "path": "$[0].gather_subset[0]", + "message": "1 is not one of ['all', 'min', 'all_ipv4_addresses', 'all_ipv6_addresses', 'apparmor', 'architecture', 'caps', 'chroot,cmdline', 'date_time', 'default_ipv4', 'default_ipv6', 'devices', 'distribution', 'distribution_major_version', 'distribution_release', 'distribution_version', 'dns', 'effective_group_ids', 'effective_user_id', 'env', 'facter', 'fips', 'hardware', 'interfaces', 'is_chroot', 'iscsi', 'kernel', 'local', 'lsb', 'machine', 'machine_id', 'mounts', 'network', 'ohai', 'os_family', 'pkg_mgr', 'platform', 'processor', 'processor_cores', 'processor_count', 'python', 'python_version', 'real_user_id', 'selinux', 'service_mgr', 'ssh_host_key_dsa_public', 'ssh_host_key_ecdsa_public', 'ssh_host_key_ed25519_public', 'ssh_host_key_rsa_public', 'ssh_host_pub_keys', 'ssh_pub_keys', 'system', 'system_capabilities', 'system_capabilities_enforced', 'user', 'user_dir', 'user_gecos', 'user_gid', 'user_id', 'user_shell', 'user_uid', 'virtual', 'virtualization_role', 'virtualization_type']" + }, + { + "path": "$[0].gather_subset[0]", + "message": "1 is not of type 'string'" + }, + { + "path": "$[0].gather_subset[0]", + "message": "1 is not one of ['!all', '!min', '!all_ipv4_addresses', '!all_ipv6_addresses', '!apparmor', '!architecture', '!caps', '!chroot,cmdline', '!date_time', '!default_ipv4', '!default_ipv6', '!devices', '!distribution', '!distribution_major_version', '!distribution_release', '!distribution_version', '!dns', '!effective_group_ids', '!effective_user_id', '!env', '!facter', '!fips', '!hardware', '!interfaces', '!is_chroot', '!iscsi', '!kernel', '!local', '!lsb', '!machine', '!machine_id', '!mounts', '!network', '!ohai', '!os_family', '!pkg_mgr', '!platform', '!processor', '!processor_cores', '!processor_count', '!python', '!python_version', '!real_user_id', '!selinux', '!service_mgr', '!ssh_host_key_dsa_public', '!ssh_host_key_ecdsa_public', '!ssh_host_key_ed25519_public', '!ssh_host_key_rsa_public', '!ssh_host_pub_keys', '!ssh_pub_keys', '!system', '!system_capabilities', '!system_capabilities_enforced', '!user', '!user_dir', '!user_gecos', '!user_gid', '!user_id', '!user_shell', '!user_uid', '!virtual', '!virtualization_role', '!virtualization_type']" + }, + { + "path": "$[0].gather_subset[0]", + "message": "1 is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/gather_subset4.yml b/test/schemas/negative_test/playbooks/gather_subset4.yml new file mode 100644 index 0000000..816e666 --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset4.yml @@ -0,0 +1,6 @@ +--- +- hosts: localhost + gather_subset: 1 + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/negative_test/playbooks/gather_subset4.yml.md b/test/schemas/negative_test/playbooks/gather_subset4.yml.md new file mode 100644 index 0000000..ada01cb --- /dev/null +++ b/test/schemas/negative_test/playbooks/gather_subset4.yml.md @@ -0,0 +1,123 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_subset" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/gather_subset", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/gather_subset/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/gather_subset4.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': 1, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_subset', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'gather_subset': 1, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].gather_subset", + "message": "1 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/ignore_errors.yml b/test/schemas/negative_test/playbooks/ignore_errors.yml new file mode 100644 index 0000000..9da277f --- /dev/null +++ b/test/schemas/negative_test/playbooks/ignore_errors.yml @@ -0,0 +1,6 @@ +- hosts: localhost + tasks: + - command: echo 123 + vars: + should_ignore_errors: true + ignore_errors: should_ignore_errors # invalid due to missing {{ }} diff --git a/test/schemas/negative_test/playbooks/ignore_errors.yml.md b/test/schemas/negative_test/playbooks/ignore_errors.yml.md new file mode 100644 index 0000000..61c3116 --- /dev/null +++ b/test/schemas/negative_test/playbooks/ignore_errors.yml.md @@ -0,0 +1,203 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/ignore_errors", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/ignore_errors.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'command': 'echo 123', 'vars': {'should_ignore_errors': True}, 'ignore_errors': 'should_ignore_errors'}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'command': 'echo 123', 'vars': {'should_ignore_errors': True}, 'ignore_errors': 'should_ignore_errors'}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'command': 'echo 123', 'vars': {'should_ignore_errors': True}, 'ignore_errors': 'should_ignore_errors'} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].ignore_errors", + "message": "'should_ignore_errors' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/import_playbook.yml b/test/schemas/negative_test/playbooks/import_playbook.yml new file mode 100644 index 0000000..b6d8ec2 --- /dev/null +++ b/test/schemas/negative_test/playbooks/import_playbook.yml @@ -0,0 +1 @@ +- ansible.builtin.import_playbook: {} # only freeform/string is allowed diff --git a/test/schemas/negative_test/playbooks/import_playbook.yml.md b/test/schemas/negative_test/playbooks/import_playbook.yml.md new file mode 100644 index 0000000..def3dce --- /dev/null +++ b/test/schemas/negative_test/playbooks/import_playbook.yml.md @@ -0,0 +1,90 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0/ansible.builtin.import_playbook", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/patternProperties/%5E(ansible%5C.builtin%5C.)%3Fimport_playbook%24/type" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/allOf/0/not" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'hosts'", + "params": { + "missingProperty": "hosts" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/import_playbook.yml", + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': {}} should not be valid under {'required': ['ansible.builtin.import_playbook']}" + }, + "sub_errors": [ + { + "path": "$[0].ansible.builtin.import_playbook", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0]", + "message": "Additional properties are not allowed ('ansible.builtin.import_playbook' was unexpected)" + }, + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': {}} should not be valid under {'required': ['ansible.builtin.import_playbook']}" + }, + { + "path": "$[0]", + "message": "'hosts' is a required property" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml b/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml new file mode 100644 index 0000000..ef2b5f6 --- /dev/null +++ b/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml @@ -0,0 +1,4 @@ +--- +# invalid because you cannot have both entries in the same time: +- ansible.builtin.import_playbook: foo.yml + import_playbook: other.yml diff --git a/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml.md b/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml.md new file mode 100644 index 0000000..184a434 --- /dev/null +++ b/test/schemas/negative_test/playbooks/import_playbook_exclusive.yml.md @@ -0,0 +1,132 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/oneOf/0/not" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/oneOf/1/not" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/allOf/0/not" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/allOf/1/not" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'hosts'", + "params": { + "missingProperty": "hosts" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "import_playbook" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/import_playbook_exclusive.yml", + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} should not be valid under {'required': ['ansible.builtin.import_playbook']}" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} should not be valid under {'required': ['import_playbook']}" + }, + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} should not be valid under {'required': ['ansible.builtin.import_playbook']}" + }, + { + "path": "$[0]", + "message": "Additional properties are not allowed ('ansible.builtin.import_playbook', 'import_playbook' were unexpected)" + }, + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} should not be valid under {'required': ['ansible.builtin.import_playbook']}" + }, + { + "path": "$[0]", + "message": "{'ansible.builtin.import_playbook': 'foo.yml', 'import_playbook': 'other.yml'} should not be valid under {'required': ['import_playbook']}" + }, + { + "path": "$[0]", + "message": "'hosts' is a required property" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/invalid-failed-when.yml b/test/schemas/negative_test/playbooks/invalid-failed-when.yml new file mode 100644 index 0000000..075f166 --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid-failed-when.yml @@ -0,0 +1,15 @@ +- hosts: localhost + tasks: + - debug: + msg: "failed_when should not accept numeric" + failed_when: 123 + + - debug: + msg: "failed_when should not accept sequence" + failed_when: + - foo + - bar + + - debug: + msg: "failed_when should not accept map" + failed_when: {} diff --git a/test/schemas/negative_test/playbooks/invalid-failed-when.yml.md b/test/schemas/negative_test/playbooks/invalid-failed-when.yml.md new file mode 100644 index 0000000..3a41059 --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid-failed-when.yml.md @@ -0,0 +1,253 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/tasks/0/failed_when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0/tasks/2", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/2/failed_when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/2/failed_when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/2/failed_when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/tasks/2/failed_when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/tasks/2", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/invalid-failed-when.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'debug': {'msg': 'failed_when should not accept numeric'}, 'failed_when': 123}, {'debug': {'msg': 'failed_when should not accept sequence'}, 'failed_when': ['foo', 'bar']}, {'debug': {'msg': 'failed_when should not accept map'}, 'failed_when': {}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'debug': {'msg': 'failed_when should not accept numeric'}, 'failed_when': 123}, {'debug': {'msg': 'failed_when should not accept sequence'}, 'failed_when': ['foo', 'bar']}, {'debug': {'msg': 'failed_when should not accept map'}, 'failed_when': {}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'debug': {'msg': 'failed_when should not accept numeric'}, 'failed_when': 123} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tasks[0].failed_when", + "message": "123 is not of type 'array'" + }, + { + "path": "$[0].tasks[2]", + "message": "{'debug': {'msg': 'failed_when should not accept map'}, 'failed_when': {}} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[2]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[2].failed_when", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[2].failed_when", + "message": "{} is not of type 'boolean'" + }, + { + "path": "$[0].tasks[2].failed_when", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].tasks[2].failed_when", + "message": "{} is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/invalid-serial.yml b/test/schemas/negative_test/playbooks/invalid-serial.yml new file mode 100644 index 0000000..f2ffd3c --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid-serial.yml @@ -0,0 +1,2 @@ +- hosts: localhost + serial: 10%BAD diff --git a/test/schemas/negative_test/playbooks/invalid-serial.yml.md b/test/schemas/negative_test/playbooks/invalid-serial.yml.md new file mode 100644 index 0000000..5c48b21 --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid-serial.yml.md @@ -0,0 +1,177 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "serial" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/serial", + "keyword": "type", + "message": "must be integer", + "params": { + "type": "integer" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/serial", + "keyword": "pattern", + "message": "must match pattern \"^\\d+\\.?\\d*%?$\"", + "params": { + "pattern": "^\\d+\\.?\\d*%?$" + }, + "schemaPath": "#/oneOf/1/pattern" + }, + { + "instancePath": "/0/serial", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/serial", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/serial", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/serial/anyOf/1/type" + }, + { + "instancePath": "/0/serial", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/properties/serial/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/invalid-serial.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'serial': '10%BAD'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'serial' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'serial' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'serial': '10%BAD'} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' is not valid under any of the given schemas" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' is not valid under any of the given schemas" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' is not of type 'integer'" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' does not match '^\\\\d+\\\\.?\\\\d*%?$'" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].serial", + "message": "'10%BAD' is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/invalid.yml b/test/schemas/negative_test/playbooks/invalid.yml new file mode 100644 index 0000000..e34d3c9 --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid.yml @@ -0,0 +1,3 @@ +- name: foo + hosts: localhost # <-- not allowed with import_playbook + import_playbook: included.yml diff --git a/test/schemas/negative_test/playbooks/invalid.yml.md b/test/schemas/negative_test/playbooks/invalid.yml.md new file mode 100644 index 0000000..c3435dd --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/allOf/1/not" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "import_playbook" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/invalid.yml", + "path": "$[0]", + "message": "{'name': 'foo', 'hosts': 'localhost', 'import_playbook': 'included.yml'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "{'name': 'foo', 'hosts': 'localhost', 'import_playbook': 'included.yml'} should not be valid under {'required': ['import_playbook']}" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "Additional properties are not allowed ('import_playbook' was unexpected)" + }, + { + "path": "$[0]", + "message": "{'name': 'foo', 'hosts': 'localhost', 'import_playbook': 'included.yml'} should not be valid under {'required': ['import_playbook']}" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/invalid_become.yml b/test/schemas/negative_test/playbooks/invalid_become.yml new file mode 100644 index 0000000..0cc6721 --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid_become.yml @@ -0,0 +1,3 @@ +--- +- hosts: localhost + become: yes # <- invalid based on json schema diff --git a/test/schemas/negative_test/playbooks/invalid_become.yml.md b/test/schemas/negative_test/playbooks/invalid_become.yml.md new file mode 100644 index 0000000..37d730d --- /dev/null +++ b/test/schemas/negative_test/playbooks/invalid_become.yml.md @@ -0,0 +1,140 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "become" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/become", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/become", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/become", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/invalid_become.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'become': 'yes'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'become', 'hosts' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'become', 'hosts' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'become': 'yes'} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].become", + "message": "'yes' is not valid under any of the given schemas" + }, + { + "path": "$[0].become", + "message": "'yes' is not of type 'boolean'" + }, + { + "path": "$[0].become", + "message": "'yes' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/local_action.yml b/test/schemas/negative_test/playbooks/local_action.yml new file mode 100644 index 0000000..9e01b1d --- /dev/null +++ b/test/schemas/negative_test/playbooks/local_action.yml @@ -0,0 +1,3 @@ +- hosts: localhost + tasks: + - local_action: [] # <-- only string or dict is allowed diff --git a/test/schemas/negative_test/playbooks/local_action.yml.md b/test/schemas/negative_test/playbooks/local_action.yml.md new file mode 100644 index 0000000..17f6244 --- /dev/null +++ b/test/schemas/negative_test/playbooks/local_action.yml.md @@ -0,0 +1,141 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/local_action", + "keyword": "type", + "message": "must be string,object", + "params": { + "type": [ + "string", + "object" + ] + }, + "schemaPath": "#/properties/local_action/type" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/local_action.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'local_action': []}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'local_action': []}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'local_action': []} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].local_action", + "message": "[] is not of type 'string', 'object'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/loop.yml b/test/schemas/negative_test/playbooks/loop.yml new file mode 100644 index 0000000..fd02ec5 --- /dev/null +++ b/test/schemas/negative_test/playbooks/loop.yml @@ -0,0 +1,7 @@ +--- +- hosts: localhost + tasks: + - name: that should pass + ansible.builtin.debug: + var: item + loop: 123 # <-- number is not valid diff --git a/test/schemas/negative_test/playbooks/loop.yml.md b/test/schemas/negative_test/playbooks/loop.yml.md new file mode 100644 index 0000000..88df838 --- /dev/null +++ b/test/schemas/negative_test/playbooks/loop.yml.md @@ -0,0 +1,141 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/loop", + "keyword": "type", + "message": "must be string,array", + "params": { + "type": [ + "string", + "array" + ] + }, + "schemaPath": "#/properties/loop/type" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/loop.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': 123}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': 123}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': 123} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].loop", + "message": "123 is not of type 'string', 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/loop2.yml b/test/schemas/negative_test/playbooks/loop2.yml new file mode 100644 index 0000000..7c9f2db --- /dev/null +++ b/test/schemas/negative_test/playbooks/loop2.yml @@ -0,0 +1,7 @@ +--- +- hosts: localhost + tasks: + - name: that should pass + ansible.builtin.debug: + var: item + loop: {} # <-- map is not valid diff --git a/test/schemas/negative_test/playbooks/loop2.yml.md b/test/schemas/negative_test/playbooks/loop2.yml.md new file mode 100644 index 0000000..df60a41 --- /dev/null +++ b/test/schemas/negative_test/playbooks/loop2.yml.md @@ -0,0 +1,141 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/loop", + "keyword": "type", + "message": "must be string,array", + "params": { + "type": [ + "string", + "array" + ] + }, + "schemaPath": "#/properties/loop/type" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/loop2.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': {}}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': {}}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'name': 'that should pass', 'ansible.builtin.debug': {'var': 'item'}, 'loop': {}} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].loop", + "message": "{} is not of type 'string', 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/no_log_partial_template.yml b/test/schemas/negative_test/playbooks/no_log_partial_template.yml new file mode 100644 index 0000000..224aba8 --- /dev/null +++ b/test/schemas/negative_test/playbooks/no_log_partial_template.yml @@ -0,0 +1,7 @@ +- hosts: localhost + vars: + some_var: true + tasks: + - ansible.builtin.debug: + msg: foo + no_log: "foo-{{ some_var }}" # <-- partial templating not allowed here diff --git a/test/schemas/negative_test/playbooks/no_log_partial_template.yml.md b/test/schemas/negative_test/playbooks/no_log_partial_template.yml.md new file mode 100644 index 0000000..ee73686 --- /dev/null +++ b/test/schemas/negative_test/playbooks/no_log_partial_template.yml.md @@ -0,0 +1,203 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/no_log_partial_template.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars': {'some_var': True}, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'foo-{{ some_var }}'}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars': {'some_var': True}, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'foo-{{ some_var }}'}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'foo-{{ some_var }}'} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'foo-{{ some_var }}' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/no_log_string.yml b/test/schemas/negative_test/playbooks/no_log_string.yml new file mode 100644 index 0000000..caf88e2 --- /dev/null +++ b/test/schemas/negative_test/playbooks/no_log_string.yml @@ -0,0 +1,7 @@ +- hosts: localhost + vars: + some_var: true + tasks: + - ansible.builtin.debug: + msg: foo + no_log: some_var # <-- bad, jinja use must be explicit diff --git a/test/schemas/negative_test/playbooks/no_log_string.yml.md b/test/schemas/negative_test/playbooks/no_log_string.yml.md new file mode 100644 index 0000000..c8213c0 --- /dev/null +++ b/test/schemas/negative_test/playbooks/no_log_string.yml.md @@ -0,0 +1,203 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/tasks/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/no_log_string.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars': {'some_var': True}, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'some_var'}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars': {'some_var': True}, 'tasks': [{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'some_var'}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 'some_var'} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].no_log", + "message": "'some_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/roles.yml b/test/schemas/negative_test/playbooks/roles.yml new file mode 100644 index 0000000..e24445a --- /dev/null +++ b/test/schemas/negative_test/playbooks/roles.yml @@ -0,0 +1,2 @@ +- hosts: localhost + roles: xxx # must be array diff --git a/test/schemas/negative_test/playbooks/roles.yml.md b/test/schemas/negative_test/playbooks/roles.yml.md new file mode 100644 index 0000000..9b4e25a --- /dev/null +++ b/test/schemas/negative_test/playbooks/roles.yml.md @@ -0,0 +1,114 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "roles" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/roles", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/roles/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/roles.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'roles': 'xxx'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'roles' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'roles' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'roles': 'xxx'} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].roles", + "message": "'xxx' is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/run_once_list.yml b/test/schemas/negative_test/playbooks/run_once_list.yml new file mode 100644 index 0000000..0dd2cd5 --- /dev/null +++ b/test/schemas/negative_test/playbooks/run_once_list.yml @@ -0,0 +1,8 @@ +- hosts: localhost + tasks: + - name: foo2 + ansible.builtin.debug: + msg: foo! + run_once: # invalid due to schema, also ansible does not allow lists + - "{{ true }}" + - xxx diff --git a/test/schemas/negative_test/playbooks/run_once_list.yml.md b/test/schemas/negative_test/playbooks/run_once_list.yml.md new file mode 100644 index 0000000..84b7dc1 --- /dev/null +++ b/test/schemas/negative_test/playbooks/run_once_list.yml.md @@ -0,0 +1,221 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/tasks/0/run_once", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/run_once_list.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'foo2', 'ansible.builtin.debug': {'msg': 'foo!'}, 'run_once': ['{{ true }}', 'xxx']}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tasks': [{'name': 'foo2', 'ansible.builtin.debug': {'msg': 'foo!'}, 'run_once': ['{{ true }}', 'xxx']}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'name': 'foo2', 'ansible.builtin.debug': {'msg': 'foo!'}, 'run_once': ['{{ true }}', 'xxx']} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not of type 'string'" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].run_once", + "message": "['{{ true }}', 'xxx'] is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tags-mapping.yml b/test/schemas/negative_test/playbooks/tags-mapping.yml new file mode 100644 index 0000000..8c6da3d --- /dev/null +++ b/test/schemas/negative_test/playbooks/tags-mapping.yml @@ -0,0 +1,2 @@ +- hosts: localhost + tags: {} # <-- not allowed diff --git a/test/schemas/negative_test/playbooks/tags-mapping.yml.md b/test/schemas/negative_test/playbooks/tags-mapping.yml.md new file mode 100644 index 0000000..aada0c6 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tags-mapping.yml.md @@ -0,0 +1,166 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tags-mapping.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tags': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tags': {}} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tags", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'array'" + }, + { + "path": "$[0].tags", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tags-number.yml b/test/schemas/negative_test/playbooks/tags-number.yml new file mode 100644 index 0000000..1872ced --- /dev/null +++ b/test/schemas/negative_test/playbooks/tags-number.yml @@ -0,0 +1,2 @@ +- hosts: localhost + tags: 123 # <-- not allowed diff --git a/test/schemas/negative_test/playbooks/tags-number.yml.md b/test/schemas/negative_test/playbooks/tags-number.yml.md new file mode 100644 index 0000000..3d32737 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tags-number.yml.md @@ -0,0 +1,166 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tags-number.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tags': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'tags': 123} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tags", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'array'" + }, + { + "path": "$[0].tags", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks.yml b/test/schemas/negative_test/playbooks/tasks.yml new file mode 100644 index 0000000..2464a73 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks.yml @@ -0,0 +1,5 @@ +- hosts: localhost + pre_tasks: foo # <-- must be array + post_tasks: {} # <-- must be array + tasks: 1 # <-- must be array + handlers: 1.0 # <-- must be array diff --git a/test/schemas/negative_test/playbooks/tasks.yml.md b/test/schemas/negative_test/playbooks/tasks.yml.md new file mode 100644 index 0000000..309912b --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks.yml.md @@ -0,0 +1,192 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "pre_tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "post_tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "handlers" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/handlers", + "keyword": "type", + "message": "must be array,null", + "params": { + "type": [ + "array", + "null" + ] + }, + "schemaPath": "#/type" + }, + { + "instancePath": "/0/post_tasks", + "keyword": "type", + "message": "must be array,null", + "params": { + "type": [ + "array", + "null" + ] + }, + "schemaPath": "#/type" + }, + { + "instancePath": "/0/pre_tasks", + "keyword": "type", + "message": "must be array,null", + "params": { + "type": [ + "array", + "null" + ] + }, + "schemaPath": "#/type" + }, + { + "instancePath": "/0/tasks", + "keyword": "type", + "message": "must be array,null", + "params": { + "type": [ + "array", + "null" + ] + }, + "schemaPath": "#/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'pre_tasks': 'foo', 'post_tasks': {}, 'tasks': 1, 'handlers': 1.0} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'handlers', 'hosts', 'post_tasks', 'pre_tasks', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'handlers', 'hosts', 'post_tasks', 'pre_tasks', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'pre_tasks': 'foo', 'post_tasks': {}, 'tasks': 1, 'handlers': 1.0} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].handlers", + "message": "1.0 is not of type 'array', 'null'" + }, + { + "path": "$[0].post_tasks", + "message": "{} is not of type 'array', 'null'" + }, + { + "path": "$[0].pre_tasks", + "message": "'foo' is not of type 'array', 'null'" + }, + { + "path": "$[0].tasks", + "message": "1 is not of type 'array', 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/args_integer.yml b/test/schemas/negative_test/playbooks/tasks/args_integer.yml new file mode 100644 index 0000000..b831039 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/args_integer.yml @@ -0,0 +1,2 @@ +- action: foo + args: 123 # invalid diff --git a/test/schemas/negative_test/playbooks/tasks/args_integer.yml.md b/test/schemas/negative_test/playbooks/tasks/args_integer.yml.md new file mode 100644 index 0000000..8820251 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/args_integer.yml.md @@ -0,0 +1,99 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/args", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/args", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/oneOf/1/type" + }, + { + "instancePath": "/0/args", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/args", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/args_integer.yml", + "path": "$[0]", + "message": "{'action': 'foo', 'args': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].args", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].args", + "message": "123 is not of type 'object'" + }, + { + "path": "$[0].args", + "message": "123 is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/args_string.yml b/test/schemas/negative_test/playbooks/tasks/args_string.yml new file mode 100644 index 0000000..121da6d --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/args_string.yml @@ -0,0 +1,2 @@ +- action: foo + args: "{{ }}123" # invalid as only full jinja2 expressions are allowed diff --git a/test/schemas/negative_test/playbooks/tasks/args_string.yml.md b/test/schemas/negative_test/playbooks/tasks/args_string.yml.md new file mode 100644 index 0000000..6359a14 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/args_string.yml.md @@ -0,0 +1,90 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/args", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/args", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/args", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/args_string.yml", + "path": "$[0]", + "message": "{'action': 'foo', 'args': '{{ }}123'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].args", + "message": "'{{ }}123' is not valid under any of the given schemas" + }, + { + "path": "$[0].args", + "message": "'{{ }}123' is not of type 'object'" + }, + { + "path": "$[0].args", + "message": "'{{ }}123' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml b/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml new file mode 100644 index 0000000..9a6bc99 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml @@ -0,0 +1,4 @@ +- command: echo 123 + vars: + sudo_var: doo + become_method: true diff --git a/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml.md b/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml.md new file mode 100644 index 0000000..fc1e692 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml.md @@ -0,0 +1,203 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/0/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "ansible.builtin.sudo", + "ansible.builtin.su", + "community.general.pbrun", + "community.general.pfexec", + "ansible.builtin.runas", + "community.general.dzdo", + "community.general.ksu", + "community.general.doas", + "community.general.machinectl", + "community.general.pmrun", + "community.general.sesu", + "community.general.sudosu" + ] + }, + "schemaPath": "#/anyOf/0/enum" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/0/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "ansible.builtin.sudo", + "ansible.builtin.su", + "community.general.pbrun", + "community.general.pfexec", + "ansible.builtin.runas", + "community.general.dzdo", + "community.general.ksu", + "community.general.doas", + "community.general.machinectl", + "community.general.pmrun", + "community.general.sesu", + "community.general.sudosu" + ] + }, + "schemaPath": "#/anyOf/0/enum" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "/0/become_method", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/become_method_invalid.yml", + "path": "$[0]", + "message": "{'command': 'echo 123', 'vars': {'sudo_var': 'doo'}, 'become_method': True} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].become_method", + "message": "True is not valid under any of the given schemas" + }, + { + "path": "$[0].become_method", + "message": "True is not one of ['ansible.builtin.sudo', 'ansible.builtin.su', 'community.general.pbrun', 'community.general.pfexec', 'ansible.builtin.runas', 'community.general.dzdo', 'community.general.ksu', 'community.general.doas', 'community.general.machinectl', 'community.general.pmrun', 'community.general.sesu', 'community.general.sudosu']" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].become_method", + "message": "True is not valid under any of the given schemas" + }, + { + "path": "$[0].become_method", + "message": "True is not one of ['ansible.builtin.sudo', 'ansible.builtin.su', 'community.general.pbrun', 'community.general.pfexec', 'ansible.builtin.runas', 'community.general.dzdo', 'community.general.ksu', 'community.general.doas', 'community.general.machinectl', 'community.general.pmrun', 'community.general.sesu', 'community.general.sudosu']" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + }, + { + "path": "$[0].become_method", + "message": "True is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/become_method_untemplated.yml.md b/test/schemas/negative_test/playbooks/tasks/become_method_untemplated.yml.md new file mode 100644 index 0000000..47a6554 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/become_method_untemplated.yml.md @@ -0,0 +1,181 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/become_method", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "sudo", + "su", + "pbrun", + "pfexec", + "runas", + "dzdo", + "ksu", + "doas", + "machinectl", + "pmrun", + "sesu", + "sudosu" + ] + }, + "schemaPath": "#/oneOf/0/enum" + }, + { + "instancePath": "/0/become_method", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/become_method", + "keyword": "pattern", + "message": "must match pattern \"^[A-Z][a-z][0-9]._$\"", + "params": { + "pattern": "^[A-Z][a-z][0-9]._$" + }, + "schemaPath": "#/oneOf/2/pattern" + }, + { + "instancePath": "/0/become_method", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/become_method", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": { + "allowedValues": [ + "sudo", + "su", + "pbrun", + "pfexec", + "runas", + "dzdo", + "ksu", + "doas", + "machinectl", + "pmrun", + "sesu", + "sudosu" + ] + }, + "schemaPath": "#/oneOf/0/enum" + }, + { + "instancePath": "/0/become_method", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/become_method", + "keyword": "pattern", + "message": "must match pattern \"^[A-Z][a-z][0-9]._$\"", + "params": { + "pattern": "^[A-Z][a-z][0-9]._$" + }, + "schemaPath": "#/oneOf/2/pattern" + }, + { + "instancePath": "/0/become_method", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/become_method_untemplated.yml", + "path": "$[0]", + "message": "{'command': 'echo 123', 'vars': {'sudo_var': 'doo'}, 'become_method': 'sudo_var'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].become_method", + "message": "'sudo_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' is not one of ['sudo', 'su', 'pbrun', 'pfexec', 'runas', 'dzdo', 'ksu', 'doas', 'machinectl', 'pmrun', 'sesu', 'sudosu']" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' does not match '^[A-Z][a-z][0-9]._$'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' is not one of ['sudo', 'su', 'pbrun', 'pfexec', 'runas', 'dzdo', 'ksu', 'doas', 'machinectl', 'pmrun', 'sesu', 'sudosu']" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].become_method", + "message": "'sudo_var' does not match '^[A-Z][a-z][0-9]._$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml b/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml new file mode 100644 index 0000000..4f8cbb3 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml @@ -0,0 +1,4 @@ +- command: echo 123 + vars: + should_ignore_errors: true + ignore_errors: should_ignore_errors # invalid due to missing {{ }} diff --git a/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml.md b/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml.md new file mode 100644 index 0000000..559a200 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/ignore_errors.yml.md @@ -0,0 +1,129 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/ignore_errors", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/ignore_errors.yml", + "path": "$[0]", + "message": "{'command': 'echo 123', 'vars': {'should_ignore_errors': True}, 'ignore_errors': 'should_ignore_errors'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' is not valid under any of the given schemas" + }, + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' is not of type 'boolean'" + }, + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' is not valid under any of the given schemas" + }, + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' is not of type 'boolean'" + }, + { + "path": "$[0].ignore_errors", + "message": "'should_ignore_errors' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/invalid_block.yml b/test/schemas/negative_test/playbooks/tasks/invalid_block.yml new file mode 100644 index 0000000..6fef6d1 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/invalid_block.yml @@ -0,0 +1,2 @@ +--- +- block: {} # <-- invalid, should be array diff --git a/test/schemas/negative_test/playbooks/tasks/invalid_block.yml.md b/test/schemas/negative_test/playbooks/tasks/invalid_block.yml.md new file mode 100644 index 0000000..bf4b30e --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/invalid_block.yml.md @@ -0,0 +1,62 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0/block", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/block/type" + }, + { + "instancePath": "/0", + "keyword": "not", + "message": "must NOT be valid", + "params": {}, + "schemaPath": "#/allOf/3/not" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/invalid_block.yml", + "path": "$[0]", + "message": "{'block': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "{'block': {}} should not be valid under {'required': ['block']}" + }, + "sub_errors": [ + { + "path": "$[0].block", + "message": "{} is not of type 'array'" + }, + { + "path": "$[0]", + "message": "{'block': {}} should not be valid under {'required': ['block']}" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/local_action.yml b/test/schemas/negative_test/playbooks/tasks/local_action.yml new file mode 100644 index 0000000..d601ff5 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/local_action.yml @@ -0,0 +1 @@ +- local_action: [] # <-- only string or dict is allowed diff --git a/test/schemas/negative_test/playbooks/tasks/local_action.yml.md b/test/schemas/negative_test/playbooks/tasks/local_action.yml.md new file mode 100644 index 0000000..cf67e7b --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/local_action.yml.md @@ -0,0 +1,67 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/local_action", + "keyword": "type", + "message": "must be string,object", + "params": { + "type": [ + "string", + "object" + ] + }, + "schemaPath": "#/properties/local_action/type" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/local_action.yml", + "path": "$[0]", + "message": "{'local_action': []} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].local_action", + "message": "[] is not of type 'string', 'object'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/loop.yml b/test/schemas/negative_test/playbooks/tasks/loop.yml new file mode 100644 index 0000000..651d262 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/loop.yml @@ -0,0 +1,3 @@ +- ansible.builtin.debug: + var: item + loop: {} # <-- map is not valid diff --git a/test/schemas/negative_test/playbooks/tasks/loop.yml.md b/test/schemas/negative_test/playbooks/tasks/loop.yml.md new file mode 100644 index 0000000..de8277f --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/loop.yml.md @@ -0,0 +1,67 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/loop", + "keyword": "type", + "message": "must be string,array", + "params": { + "type": [ + "string", + "array" + ] + }, + "schemaPath": "#/properties/loop/type" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/loop.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'var': 'item'}, 'loop': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].loop", + "message": "{} is not of type 'string', 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/loop2.yml b/test/schemas/negative_test/playbooks/tasks/loop2.yml new file mode 100644 index 0000000..ec2642f --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/loop2.yml @@ -0,0 +1,3 @@ +- ansible.builtin.debug: + var: item + loop: 123 # <-- number is not valid diff --git a/test/schemas/negative_test/playbooks/tasks/loop2.yml.md b/test/schemas/negative_test/playbooks/tasks/loop2.yml.md new file mode 100644 index 0000000..c36d7c9 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/loop2.yml.md @@ -0,0 +1,67 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/loop", + "keyword": "type", + "message": "must be string,array", + "params": { + "type": [ + "string", + "array" + ] + }, + "schemaPath": "#/properties/loop/type" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/loop2.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'var': 'item'}, 'loop': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].loop", + "message": "123 is not of type 'string', 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/no_log_number.yml b/test/schemas/negative_test/playbooks/tasks/no_log_number.yml new file mode 100644 index 0000000..4fa8da2 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/no_log_number.yml @@ -0,0 +1,3 @@ +- ansible.builtin.debug: + msg: foo + no_log: 123 # <-- bad diff --git a/test/schemas/negative_test/playbooks/tasks/no_log_number.yml.md b/test/schemas/negative_test/playbooks/tasks/no_log_number.yml.md new file mode 100644 index 0000000..4b9516c --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/no_log_number.yml.md @@ -0,0 +1,147 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/oneOf/1/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/oneOf/1/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/no_log_number.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'no_log': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].no_log", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].no_log", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].no_log", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].no_log", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].no_log", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].no_log", + "message": "123 is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/no_log_string.yml b/test/schemas/negative_test/playbooks/tasks/no_log_string.yml new file mode 100644 index 0000000..0e0b71a --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/no_log_string.yml @@ -0,0 +1,5 @@ +- ansible.builtin.debug: + msg: foo + vars: + some_var: true + no_log: some_var # <-- bad, jinja use must be explicit diff --git a/test/schemas/negative_test/playbooks/tasks/no_log_string.yml.md b/test/schemas/negative_test/playbooks/tasks/no_log_string.yml.md new file mode 100644 index 0000000..6742175 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/no_log_string.yml.md @@ -0,0 +1,129 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0/no_log", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/oneOf/0/type" + }, + { + "instancePath": "/0/no_log", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/no_log", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/no_log_string.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'vars': {'some_var': True}, 'no_log': 'some_var'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].no_log", + "message": "'some_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].no_log", + "message": "'some_var' is not of type 'boolean'" + }, + { + "path": "$[0].no_log", + "message": "'some_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].no_log", + "message": "'some_var' is not valid under any of the given schemas" + }, + { + "path": "$[0].no_log", + "message": "'some_var' is not of type 'boolean'" + }, + { + "path": "$[0].no_log", + "message": "'some_var' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml b/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml new file mode 100644 index 0000000..39fe8c7 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml @@ -0,0 +1,3 @@ +- ansible.builtin.debug: + msg: foo + tags: {} # <-- not allowed diff --git a/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml.md b/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml.md new file mode 100644 index 0000000..d860605 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/tags-mapping.yml.md @@ -0,0 +1,125 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/tags-mapping.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'tags': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].tags", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'array'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tags", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "{} is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/tags-string.yml b/test/schemas/negative_test/playbooks/tasks/tags-string.yml new file mode 100644 index 0000000..6512fb5 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/tags-string.yml @@ -0,0 +1,3 @@ +- ansible.builtin.debug: + msg: foo + tags: 123 # <-- not allowed diff --git a/test/schemas/negative_test/playbooks/tasks/tags-string.yml.md b/test/schemas/negative_test/playbooks/tasks/tags-string.yml.md new file mode 100644 index 0000000..0bb7ed0 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/tags-string.yml.md @@ -0,0 +1,125 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/tags/anyOf/0/type" + }, + { + "instancePath": "/0/tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/tags/anyOf/1/type" + }, + { + "instancePath": "/0/tags", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/tags/anyOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/tags-string.yml", + "path": "$[0]", + "message": "{'ansible.builtin.debug': {'msg': 'foo'}, 'tags': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].tags", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'array'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tags", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tags", + "message": "123 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/when_integer.yml b/test/schemas/negative_test/playbooks/tasks/when_integer.yml new file mode 100644 index 0000000..7758503 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/when_integer.yml @@ -0,0 +1,2 @@ +- action: foo + when: 123 # invalid, number is not accepted diff --git a/test/schemas/negative_test/playbooks/tasks/when_integer.yml.md b/test/schemas/negative_test/playbooks/tasks/when_integer.yml.md new file mode 100644 index 0000000..bc59cc4 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/when_integer.yml.md @@ -0,0 +1,155 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/when_integer.yml", + "path": "$[0]", + "message": "{'action': 'foo', 'when': 123} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].when", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'array'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].when", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].when", + "message": "123 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/when_object.yml b/test/schemas/negative_test/playbooks/tasks/when_object.yml new file mode 100644 index 0000000..430605d --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/when_object.yml @@ -0,0 +1,2 @@ +- action: foo + when: {} # invalid, object is not accepted diff --git a/test/schemas/negative_test/playbooks/tasks/when_object.yml.md b/test/schemas/negative_test/playbooks/tasks/when_object.yml.md new file mode 100644 index 0000000..6c28d0c --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/when_object.yml.md @@ -0,0 +1,155 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/when", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/type" + }, + { + "instancePath": "/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/when_object.yml", + "path": "$[0]", + "message": "{'action': 'foo', 'when': {}} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0].when", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'boolean'" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'array'" + }, + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].when", + "message": "{} is not valid under any of the given schemas" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'boolean'" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'string'" + }, + { + "path": "$[0].when", + "message": "{} is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml b/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml new file mode 100644 index 0000000..eff6ea0 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml @@ -0,0 +1,2 @@ +- command: echo 123 + with_items: true # invalid, must be a list or templated string diff --git a/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml.md b/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml.md new file mode 100644 index 0000000..ffc8ef8 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml.md @@ -0,0 +1,88 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/with_items", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/full-jinja/type" + }, + { + "instancePath": "/0/with_items", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/with_items/anyOf/1/type" + }, + { + "instancePath": "/0/with_items", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/properties/with_items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/with_items_boolean.yml", + "path": "$[0]", + "message": "{'command': 'echo 123', 'with_items': True} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].with_items", + "message": "True is not valid under any of the given schemas" + }, + { + "path": "$[0].with_items", + "message": "True is not of type 'string'" + }, + { + "path": "$[0].with_items", + "message": "True is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml b/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml new file mode 100644 index 0000000..257ffe2 --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml @@ -0,0 +1,2 @@ +- command: echo 123 + with_items: foobar # invalid, probably user wanted "{{ foobar }}"? diff --git a/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml.md b/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml.md new file mode 100644 index 0000000..158b0ee --- /dev/null +++ b/test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml.md @@ -0,0 +1,88 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/with_items", + "keyword": "pattern", + "message": "must match pattern \"^\\{[\\{%](.|[\r\n])*[\\}%]\\}$\"", + "params": { + "pattern": "^\\{[\\{%](.|[\r\n])*[\\}%]\\}$" + }, + "schemaPath": "#/$defs/full-jinja/pattern" + }, + { + "instancePath": "/0/with_items", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/with_items/anyOf/1/type" + }, + { + "instancePath": "/0/with_items", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/properties/with_items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/tasks/with_items_untemplated_string.yml", + "path": "$[0]", + "message": "{'command': 'echo 123', 'with_items': 'foobar'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'block' is a required property" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].with_items", + "message": "'foobar' is not valid under any of the given schemas" + }, + { + "path": "$[0].with_items", + "message": "'foobar' does not match '^\\\\{[\\\\{%](.|[\\r\\n])*[\\\\}%]\\\\}$'" + }, + { + "path": "$[0].with_items", + "message": "'foobar' is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/var_files_list_number.yml b/test/schemas/negative_test/playbooks/var_files_list_number.yml new file mode 100644 index 0000000..9f3d8dd --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_list_number.yml @@ -0,0 +1,5 @@ +--- +- name: var_files should not accept array[number] + hosts: localhost + vars_files: + - 0 diff --git a/test/schemas/negative_test/playbooks/var_files_list_number.yml.md b/test/schemas/negative_test/playbooks/var_files_list_number.yml.md new file mode 100644 index 0000000..e915593 --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_list_number.yml.md @@ -0,0 +1,144 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/vars_files", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/patternProperties/vars/type" + }, + { + "instancePath": "/0/vars_files/0", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/vars_files/items/oneOf/0/type" + }, + { + "instancePath": "/0/vars_files/0", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/vars_files/items/oneOf/1/type" + }, + { + "instancePath": "/0/vars_files/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/properties/vars_files/items/oneOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/var_files_list_number.yml", + "path": "$[0]", + "message": "{'name': 'var_files should not accept array[number]', 'hosts': 'localhost', 'vars_files': [0]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'name': 'var_files should not accept array[number]', 'hosts': 'localhost', 'vars_files': [0]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].vars_files", + "message": "[0] is not of type 'object'" + }, + { + "path": "$[0].vars_files[0]", + "message": "0 is not valid under any of the given schemas" + }, + { + "path": "$[0].vars_files[0]", + "message": "0 is not of type 'string'" + }, + { + "path": "$[0].vars_files[0]", + "message": "0 is not of type 'array'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml b/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml new file mode 100644 index 0000000..7170010 --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml @@ -0,0 +1,5 @@ +--- +- name: var_files should not accept array[number] + hosts: localhost + vars_files: + - [0, 1] diff --git a/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml.md b/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml.md new file mode 100644 index 0000000..3494498 --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml.md @@ -0,0 +1,157 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/vars_files", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/patternProperties/vars/type" + }, + { + "instancePath": "/0/vars_files/0", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/vars_files/items/oneOf/0/type" + }, + { + "instancePath": "/0/vars_files/0/0", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/vars_files/items/oneOf/1/items/type" + }, + { + "instancePath": "/0/vars_files/0/1", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/properties/vars_files/items/oneOf/1/items/type" + }, + { + "instancePath": "/0/vars_files/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/properties/vars_files/items/oneOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/var_files_list_of_list_number.yml", + "path": "$[0]", + "message": "{'name': 'var_files should not accept array[number]', 'hosts': 'localhost', 'vars_files': [[0, 1]]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'name': 'var_files should not accept array[number]', 'hosts': 'localhost', 'vars_files': [[0, 1]]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].vars_files", + "message": "[[0, 1]] is not of type 'object'" + }, + { + "path": "$[0].vars_files[0]", + "message": "[0, 1] is not valid under any of the given schemas" + }, + { + "path": "$[0].vars_files[0]", + "message": "[0, 1] is not of type 'string'" + }, + { + "path": "$[0].vars_files[0][0]", + "message": "0 is not of type 'string'" + }, + { + "path": "$[0].vars_files[0][1]", + "message": "1 is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/var_files_number.yml b/test/schemas/negative_test/playbooks/var_files_number.yml new file mode 100644 index 0000000..fe26650 --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_number.yml @@ -0,0 +1,4 @@ +--- +- name: var_files should not accept number + hosts: localhost + vars_files: 0 diff --git a/test/schemas/negative_test/playbooks/var_files_number.yml.md b/test/schemas/negative_test/playbooks/var_files_number.yml.md new file mode 100644 index 0000000..fa97e7e --- /dev/null +++ b/test/schemas/negative_test/playbooks/var_files_number.yml.md @@ -0,0 +1,122 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/vars_files", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/patternProperties/vars/type" + }, + { + "instancePath": "/0/vars_files", + "keyword": "type", + "message": "must be array,string,null", + "params": { + "type": [ + "array", + "string", + "null" + ] + }, + "schemaPath": "#/properties/vars_files/type" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/var_files_number.yml", + "path": "$[0]", + "message": "{'name': 'var_files should not accept number', 'hosts': 'localhost', 'vars_files': 0} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'name': 'var_files should not accept number', 'hosts': 'localhost', 'vars_files': 0} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].vars_files", + "message": "0 is not of type 'object'" + }, + { + "path": "$[0].vars_files", + "message": "0 is not of type 'array', 'string', 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/asterisk.yml b/test/schemas/negative_test/playbooks/vars/asterisk.yml new file mode 100644 index 0000000..9dd2200 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/asterisk.yml @@ -0,0 +1,2 @@ +--- +"*foo": ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/asterisk.yml.md b/test/schemas/negative_test/playbooks/vars/asterisk.yml.md new file mode 100644 index 0000000..1ea9a98 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/asterisk.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "*foo" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/asterisk.yml", + "path": "$", + "message": "{'*foo': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'*foo': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'*foo' does not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'*foo': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'*foo': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml b/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml new file mode 100644 index 0000000..216de64 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml @@ -0,0 +1,2 @@ +--- +foo-bar: ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml.md b/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml.md new file mode 100644 index 0000000..b862e69 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "foo-bar" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/dash-in-var-name.yml", + "path": "$", + "message": "{'foo-bar': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'foo-bar': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'foo-bar' does not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'foo-bar': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'foo-bar': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/list.yml b/test/schemas/negative_test/playbooks/vars/list.yml new file mode 100644 index 0000000..909a4d7 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/list.yml @@ -0,0 +1,3 @@ +# invalid vars file, as sequence is not allowed +- foo +- bar diff --git a/test/schemas/negative_test/playbooks/vars/list.yml.md b/test/schemas/negative_test/playbooks/vars/list.yml.md new file mode 100644 index 0000000..e2c9bf5 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/list.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/anyOf/0/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/list.yml", + "path": "$", + "message": "['foo', 'bar'] is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "['foo', 'bar'] is not of type 'object'" + }, + "sub_errors": [ + { + "path": "$", + "message": "['foo', 'bar'] is not of type 'object'" + }, + { + "path": "$", + "message": "['foo', 'bar'] is not of type 'string'" + }, + { + "path": "$", + "message": "['foo', 'bar'] is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml b/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml new file mode 100644 index 0000000..826150d --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml @@ -0,0 +1,2 @@ +--- +12: ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml.md b/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml.md new file mode 100644 index 0000000..7ddcff6 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/numeric-var-name.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "12" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/numeric-var-name.yml", + "path": "$", + "message": "{'12': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'12': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'12' does not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'12': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'12': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/play-keyword.yml b/test/schemas/negative_test/playbooks/vars/play-keyword.yml new file mode 100644 index 0000000..7d277ed --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/play-keyword.yml @@ -0,0 +1,2 @@ +--- +environment: ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/play-keyword.yml.md b/test/schemas/negative_test/playbooks/vars/play-keyword.yml.md new file mode 100644 index 0000000..6b88b2a --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/play-keyword.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "environment" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/play-keyword.yml", + "path": "$", + "message": "{'environment': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'environment': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'environment' does not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'environment': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'environment': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/python-keyword.yml b/test/schemas/negative_test/playbooks/vars/python-keyword.yml new file mode 100644 index 0000000..7b9d01d --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/python-keyword.yml @@ -0,0 +1,3 @@ +--- +async: ... # invalid var name +lambda: ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/python-keyword.yml.md b/test/schemas/negative_test/playbooks/vars/python-keyword.yml.md new file mode 100644 index 0000000..ca42f74 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/python-keyword.yml.md @@ -0,0 +1,86 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "async" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "lambda" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/python-keyword.yml", + "path": "$", + "message": "{'async': '...', 'lambda': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'async': '...', 'lambda': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'async', 'lambda' do not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'async': '...', 'lambda': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'async': '...', 'lambda': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml b/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml new file mode 100644 index 0000000..5f97995 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml @@ -0,0 +1,2 @@ +--- +5foo: ... # invalid var name diff --git a/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml.md b/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml.md new file mode 100644 index 0000000..8b73b0a --- /dev/null +++ b/test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml.md @@ -0,0 +1,77 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "5foo" + }, + "schemaPath": "#/anyOf/0/additionalProperties" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/anyOf/1/type" + }, + { + "instancePath": "", + "keyword": "type", + "message": "must be null", + "params": { + "type": "null" + }, + "schemaPath": "#/anyOf/2/type" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vars/varname-numeric-prefix.yml", + "path": "$", + "message": "{'5foo': '...'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'5foo': '...'} is not of type 'string'" + }, + "sub_errors": [ + { + "path": "$", + "message": "'5foo' does not match any of the regexes: '^(?!(False|None|True|and|any_errors_fatal|as|assert|async|await|become|become_exe|become_flags|become_method|become_user|break|check_mode|class|collections|connection|continue|debugger|def|del|diff|elif|else|environment|except|fact_path|finally|for|force_handlers|from|gather_facts|gather_subset|gather_timeout|global|handlers|hosts|if|ignore_errors|ignore_unreachable|import|in|is|lambda|max_fail_percentage|module_defaults|name|no_log|nonlocal|not|or|order|pass|port|post_tasks|pre_tasks|raise|remote_user|return|roles|run_once|serial|strategy|tags|tasks|throttle|timeout|try|vars|vars_files|vars_prompt|while|with|yield)$)[a-zA-Z_][\\\\w]*$'" + }, + { + "path": "$", + "message": "{'5foo': '...'} is not of type 'string'" + }, + { + "path": "$", + "message": "{'5foo': '...'} is not of type 'null'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/vas_prompt.yml b/test/schemas/negative_test/playbooks/vas_prompt.yml new file mode 100644 index 0000000..a90d131 --- /dev/null +++ b/test/schemas/negative_test/playbooks/vas_prompt.yml @@ -0,0 +1,7 @@ +- hosts: localhost + vars_prompt: + - name: username + prompt: What is your username? + private: false + tags: # tags were never supported, https://github.com/ansible/ansible/issues/1780 + - foo diff --git a/test/schemas/negative_test/playbooks/vas_prompt.yml.md b/test/schemas/negative_test/playbooks/vas_prompt.yml.md new file mode 100644 index 0000000..d2d809d --- /dev/null +++ b/test/schemas/negative_test/playbooks/vas_prompt.yml.md @@ -0,0 +1,118 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/vars_prompt", + "keyword": "type", + "message": "must be object", + "params": { + "type": "object" + }, + "schemaPath": "#/patternProperties/vars/type" + }, + { + "instancePath": "/0/vars_prompt/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tags" + }, + "schemaPath": "#/$defs/vars_prompt/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/vas_prompt.yml", + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars_prompt': [{'name': 'username', 'prompt': 'What is your username?', 'private': False, 'tags': ['foo']}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'hosts' does not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'hosts': 'localhost', 'vars_prompt': [{'name': 'username', 'prompt': 'What is your username?', 'private': False, 'tags': ['foo']}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].vars_prompt", + "message": "[{'name': 'username', 'prompt': 'What is your username?', 'private': False, 'tags': ['foo']}] is not of type 'object'" + }, + { + "path": "$[0].vars_prompt[0]", + "message": "Additional properties are not allowed ('tags' was unexpected)" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/playbooks/when.yml b/test/schemas/negative_test/playbooks/when.yml new file mode 100644 index 0000000..c48bdc1 --- /dev/null +++ b/test/schemas/negative_test/playbooks/when.yml @@ -0,0 +1,11 @@ +--- +- name: Test for when (failure) + hosts: localhost + gather_facts: false + tasks: + - name: Testing for when is passed a list + ansible.builtin.debug: + msg: "this is ok" + when: + - true + - 123 diff --git a/test/schemas/negative_test/playbooks/when.yml.md b/test/schemas/negative_test/playbooks/when.yml.md new file mode 100644 index 0000000..4c23dcb --- /dev/null +++ b/test/schemas/negative_test/playbooks/when.yml.md @@ -0,0 +1,286 @@ +# ajv errors + +```json +[ + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'ansible.builtin.import_playbook'", + "params": { + "missingProperty": "ansible.builtin.import_playbook" + }, + "schemaPath": "#/oneOf/0/required" + }, + { + "instancePath": "/0", + "keyword": "required", + "message": "must have required property 'import_playbook'", + "params": { + "missingProperty": "import_playbook" + }, + "schemaPath": "#/oneOf/1/required" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/oneOf" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "hosts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "gather_facts" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "tasks" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "required", + "message": "must have required property 'block'", + "params": { + "missingProperty": "block" + }, + "schemaPath": "#/required" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf/0/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf/1/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/0/type" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/1/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "type", + "message": "must be boolean", + "params": { + "type": "boolean" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf/0/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "type", + "message": "must be string", + "params": { + "type": "string" + }, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf/1/type" + }, + { + "instancePath": "/0/tasks/0/when/1", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/$defs/complex_conditional/oneOf/2/items/anyOf" + }, + { + "instancePath": "/0/tasks/0/when", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/$defs/complex_conditional/oneOf" + }, + { + "instancePath": "/0/tasks/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/items/anyOf" + }, + { + "instancePath": "/0", + "keyword": "oneOf", + "message": "must match exactly one schema in oneOf", + "params": { + "passingSchemas": null + }, + "schemaPath": "#/items/oneOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/playbooks/when.yml", + "path": "$[0]", + "message": "{'name': 'Test for when (failure)', 'hosts': 'localhost', 'gather_facts': False, 'tasks': [{'name': 'Testing for when is passed a list', 'ansible.builtin.debug': {'msg': 'this is ok'}, 'when': [True, 123]}]} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$[0]", + "message": "'gather_facts', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + "sub_errors": [ + { + "path": "$[0]", + "message": "'gather_facts', 'hosts', 'tasks' do not match any of the regexes: '^(ansible\\\\.builtin\\\\.)?import_playbook$', 'name', 'tags', 'vars', 'when'" + }, + { + "path": "$[0]", + "message": "{'name': 'Test for when (failure)', 'hosts': 'localhost', 'gather_facts': False, 'tasks': [{'name': 'Testing for when is passed a list', 'ansible.builtin.debug': {'msg': 'this is ok'}, 'when': [True, 123]}]} is not valid under any of the given schemas" + }, + { + "path": "$[0]", + "message": "'ansible.builtin.import_playbook' is a required property" + }, + { + "path": "$[0]", + "message": "'import_playbook' is a required property" + }, + { + "path": "$[0].tasks[0]", + "message": "{'name': 'Testing for when is passed a list', 'ansible.builtin.debug': {'msg': 'this is ok'}, 'when': [True, 123]} is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not of type 'string'" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not of type 'string'" + }, + { + "path": "$[0].tasks[0]", + "message": "'block' is a required property" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].when", + "message": "[True, 123] is not of type 'string'" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not valid under any of the given schemas" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not of type 'boolean'" + }, + { + "path": "$[0].tasks[0].when[1]", + "message": "123 is not of type 'string'" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/reqs3/meta/requirements.yml b/test/schemas/negative_test/reqs3/meta/requirements.yml new file mode 100644 index 0000000..f28aebb --- /dev/null +++ b/test/schemas/negative_test/reqs3/meta/requirements.yml @@ -0,0 +1,2 @@ +# this should fail validation +foo: bar diff --git a/test/schemas/negative_test/reqs3/meta/requirements.yml.md b/test/schemas/negative_test/reqs3/meta/requirements.yml.md new file mode 100644 index 0000000..5de6643 --- /dev/null +++ b/test/schemas/negative_test/reqs3/meta/requirements.yml.md @@ -0,0 +1,101 @@ +# ajv errors + +```json +[ + { + "instancePath": "", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/anyOf/0/type" + }, + { + "instancePath": "", + "keyword": "required", + "message": "must have required property 'collections'", + "params": { + "missingProperty": "collections" + }, + "schemaPath": "#/anyOf/0/required" + }, + { + "instancePath": "", + "keyword": "required", + "message": "must have required property 'roles'", + "params": { + "missingProperty": "roles" + }, + "schemaPath": "#/anyOf/1/required" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + }, + { + "instancePath": "", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "foo" + }, + "schemaPath": "#/additionalProperties" + }, + { + "instancePath": "", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/reqs3/meta/requirements.yml", + "path": "$", + "message": "{'foo': 'bar'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$", + "message": "{'foo': 'bar'} is not of type 'array'" + }, + "sub_errors": [ + { + "path": "$", + "message": "{'foo': 'bar'} is not of type 'array'" + }, + { + "path": "$", + "message": "Additional properties are not allowed ('foo' was unexpected)" + }, + { + "path": "$", + "message": "{'foo': 'bar'} is not valid under any of the given schemas" + }, + { + "path": "$", + "message": "'collections' is a required property" + }, + { + "path": "$", + "message": "'roles' is a required property" + } + ] + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/meta/argument_specs.yml b/test/schemas/negative_test/roles/meta/argument_specs.yml new file mode 100644 index 0000000..ddc9862 --- /dev/null +++ b/test/schemas/negative_test/roles/meta/argument_specs.yml @@ -0,0 +1,5 @@ +--- +argument_specs: + main: + foo: bar # <-- invalid based on json schema + options: {} diff --git a/test/schemas/negative_test/roles/meta/argument_specs.yml.md b/test/schemas/negative_test/roles/meta/argument_specs.yml.md new file mode 100644 index 0000000..34da932 --- /dev/null +++ b/test/schemas/negative_test/roles/meta/argument_specs.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/argument_specs/main", + "keyword": "additionalProperties", + "message": "must NOT have additional properties", + "params": { + "additionalProperty": "foo" + }, + "schemaPath": "#/additionalProperties" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/meta/argument_specs.yml", + "path": "$.argument_specs.main", + "message": "Additional properties are not allowed ('foo' was unexpected)", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/meta/main.yml b/test/schemas/negative_test/roles/meta/main.yml new file mode 100644 index 0000000..3ed9a8c --- /dev/null +++ b/test/schemas/negative_test/roles/meta/main.yml @@ -0,0 +1,10 @@ +galaxy_info: + description: bar + min_ansible_version: "2.9" + company: foo + license: MIT + galaxy_tags: database # <-- invalid, must be a list of strings + platforms: + - name: Alpine + versions: + - all diff --git a/test/schemas/negative_test/roles/meta/main.yml.md b/test/schemas/negative_test/roles/meta/main.yml.md new file mode 100644 index 0000000..2c9e99b --- /dev/null +++ b/test/schemas/negative_test/roles/meta/main.yml.md @@ -0,0 +1,58 @@ +# ajv errors + +```json +[ + { + "instancePath": "/galaxy_info", + "keyword": "required", + "message": "must have required property 'author'", + "params": { + "missingProperty": "author" + }, + "schemaPath": "#/allOf/0/then/required" + }, + { + "instancePath": "/galaxy_info", + "keyword": "if", + "message": "must match \"then\" schema", + "params": { + "failingKeyword": "then" + }, + "schemaPath": "#/allOf/0/if" + }, + { + "instancePath": "/galaxy_info/galaxy_tags", + "keyword": "type", + "message": "must be array", + "params": { + "type": "array" + }, + "schemaPath": "#/properties/galaxy_tags/type" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/meta/main.yml", + "path": "$.galaxy_info", + "message": "'author' is a required property", + "has_sub_errors": false + }, + { + "filename": "negative_test/roles/meta/main.yml", + "path": "$.galaxy_info.galaxy_tags", + "message": "'database' is not of type 'array'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml b/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml new file mode 100644 index 0000000..1fa41eb --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml @@ -0,0 +1,10 @@ +collections: + - foo # invalid pattern +galaxy_info: + standalone: false # role inside a collection + description: foo + license: bar + platforms: + - name: Fedora + versions: + - all diff --git a/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml.md b/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml.md new file mode 100644 index 0000000..1b8dcd0 --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/collections/0", + "keyword": "pattern", + "message": "must match pattern \"^[a-z_]+\\.[a-z_]+$\"", + "params": { + "pattern": "^[a-z_]+\\.[a-z_]+$" + }, + "schemaPath": "#/$defs/collections/items/pattern" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/meta_invalid_collection/meta/main.yml", + "path": "$.collections[0]", + "message": "'foo' does not match '^[a-z_]+\\\\.[a-z_]+$'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml b/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml new file mode 100644 index 0000000..488928c --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml @@ -0,0 +1,11 @@ +# role inside a collection +collections: + - FOO.BAR # invalid pattern, need to use lowercase +galaxy_info: + standalone: false + description: foo + license: bar + platforms: + - name: Fedora + versions: + - all diff --git a/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml.md b/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml.md new file mode 100644 index 0000000..5d775f0 --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml.md @@ -0,0 +1,34 @@ +# ajv errors + +```json +[ + { + "instancePath": "/collections/0", + "keyword": "pattern", + "message": "must match pattern \"^[a-z_]+\\.[a-z_]+$\"", + "params": { + "pattern": "^[a-z_]+\\.[a-z_]+$" + }, + "schemaPath": "#/$defs/collections/items/pattern" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/meta_invalid_collections/meta/main.yml", + "path": "$.collections[0]", + "message": "'FOO.BAR' does not match '^[a-z_]+\\\\.[a-z_]+$'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml b/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml new file mode 100644 index 0000000..e50e5b7 --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml @@ -0,0 +1,12 @@ +--- +# old standalone role +galaxy_info: + description: foo + min_ansible_version: "2.9" + namespace: foo-bar + company: foo + license: MIT + platforms: + - name: Alpine + versions: + - all diff --git a/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml.md b/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml.md new file mode 100644 index 0000000..ad7e9d3 --- /dev/null +++ b/test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml.md @@ -0,0 +1,58 @@ +# ajv errors + +```json +[ + { + "instancePath": "/galaxy_info", + "keyword": "required", + "message": "must have required property 'author'", + "params": { + "missingProperty": "author" + }, + "schemaPath": "#/allOf/0/then/required" + }, + { + "instancePath": "/galaxy_info", + "keyword": "if", + "message": "must match \"then\" schema", + "params": { + "failingKeyword": "then" + }, + "schemaPath": "#/allOf/0/if" + }, + { + "instancePath": "/galaxy_info/namespace", + "keyword": "pattern", + "message": "must match pattern \"^[a-z][a-z0-9_]+$\"", + "params": { + "pattern": "^[a-z][a-z0-9_]+$" + }, + "schemaPath": "#/properties/namespace/pattern" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/meta_invalid_role_namespace/meta/main.yml", + "path": "$.galaxy_info", + "message": "'author' is a required property", + "has_sub_errors": false + }, + { + "filename": "negative_test/roles/meta_invalid_role_namespace/meta/main.yml", + "path": "$.galaxy_info.namespace", + "message": "'foo-bar' does not match '^[a-z][a-z0-9_]+$'", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml b/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml new file mode 100644 index 0000000..81d4d3d --- /dev/null +++ b/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml @@ -0,0 +1,13 @@ +# old standalone role +galaxy_info: + description: bar + min_ansible_version: "2.9" + company: foo + license: MIT + platforms: + - name: Alpine + versions: + - all + +dependencies: + - version: foo # invalid, should have at least name, role or src properties diff --git a/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml.md b/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml.md new file mode 100644 index 0000000..f09b1ac --- /dev/null +++ b/test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml.md @@ -0,0 +1,101 @@ +# ajv errors + +```json +[ + { + "instancePath": "/dependencies/0", + "keyword": "required", + "message": "must have required property 'role'", + "params": { + "missingProperty": "role" + }, + "schemaPath": "#/anyOf/0/required" + }, + { + "instancePath": "/dependencies/0", + "keyword": "required", + "message": "must have required property 'src'", + "params": { + "missingProperty": "src" + }, + "schemaPath": "#/anyOf/1/required" + }, + { + "instancePath": "/dependencies/0", + "keyword": "required", + "message": "must have required property 'name'", + "params": { + "missingProperty": "name" + }, + "schemaPath": "#/anyOf/2/required" + }, + { + "instancePath": "/dependencies/0", + "keyword": "anyOf", + "message": "must match a schema in anyOf", + "params": {}, + "schemaPath": "#/anyOf" + }, + { + "instancePath": "/galaxy_info", + "keyword": "required", + "message": "must have required property 'author'", + "params": { + "missingProperty": "author" + }, + "schemaPath": "#/allOf/0/then/required" + }, + { + "instancePath": "/galaxy_info", + "keyword": "if", + "message": "must match \"then\" schema", + "params": { + "failingKeyword": "then" + }, + "schemaPath": "#/allOf/0/if" + } +] +``` + +# check-jsonschema + +stdout: + +```json +{ + "status": "fail", + "errors": [ + { + "filename": "negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml", + "path": "$.dependencies[0]", + "message": "{'version': 'foo'} is not valid under any of the given schemas", + "has_sub_errors": true, + "best_match": { + "path": "$.dependencies[0]", + "message": "'role' is a required property" + }, + "sub_errors": [ + { + "path": "$.dependencies[0]", + "message": "'role' is a required property" + }, + { + "path": "$.dependencies[0]", + "message": "'src' is a required property" + }, + { + "path": "$.dependencies[0]", + "message": "'name' is a required property" + } + ] + }, + { + "filename": "negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml", + "path": "$.galaxy_info", + "message": "'author' is a required property", + "has_sub_errors": false + } + ], + "parse_errors": [] +} +``` diff --git a/test/schemas/package-lock.json b/test/schemas/package-lock.json new file mode 100644 index 0000000..3745a97 --- /dev/null +++ b/test/schemas/package-lock.json @@ -0,0 +1,2290 @@ +{ + "name": "schemas", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "ajv-formats": "^2.1.1", + "js-yaml": "^4.1.0", + "safe-stable-stringify": "^2.4.3", + "ts-node": "^10.9.1", + "vscode-json-languageservice": "^5.3.5" + }, + "devDependencies": { + "@types/chai": "^4.3.5", + "@types/js-yaml": "^4.0.5", + "@types/minimatch": "^5.1.2", + "@types/mocha": "^10.0.1", + "@types/node": "^20.3.1", + "chai": "^4.3.7", + "minimatch": "^9.0.1", + "mocha": "^10.2.0", + "typescript": "^5.1.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.13.tgz", + "integrity": "sha512-A3uY356uOU9nGa+TQIT/i3ziWUgJjVMUrGGXSrtRiTwklyCFjGVWIOHoEIHbJpiyhDkJd9kvIWUOfXK1IkK8XQ==" + }, + "node_modules/acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", + "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.1.tgz", + "integrity": "sha512-reLxBcKUPNBnc/sVtAbxgRVFSegoGeLaSjmphNhcwcolhYLRgtJscn5mRl6YRZNQv40Y7P6JM2YhSIsbL9OB5A==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vscode-json-languageservice": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.5.tgz", + "integrity": "sha512-DasT+bKtpaS2rTPEB4VMROnvO1WES2KD8RZZxXbumnk9sk5wco10VdB6sJgTlsKQN14tHQLZDXuHnSoSAlE8LQ==", + "dependencies": { + "@vscode/l10n": "^0.0.13", + "jsonc-parser": "^3.2.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", + "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, + "node_modules/vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + }, + "@vscode/l10n": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.13.tgz", + "integrity": "sha512-A3uY356uOU9nGa+TQIT/i3ziWUgJjVMUrGGXSrtRiTwklyCFjGVWIOHoEIHbJpiyhDkJd9kvIWUOfXK1IkK8XQ==" + }, + "acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "ajv": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", + "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.1.tgz", + "integrity": "sha512-reLxBcKUPNBnc/sVtAbxgRVFSegoGeLaSjmphNhcwcolhYLRgtJscn5mRl6YRZNQv40Y7P6JM2YhSIsbL9OB5A==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + } + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "vscode-json-languageservice": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.5.tgz", + "integrity": "sha512-DasT+bKtpaS2rTPEB4VMROnvO1WES2KD8RZZxXbumnk9sk5wco10VdB6sJgTlsKQN14tHQLZDXuHnSoSAlE8LQ==", + "requires": { + "@vscode/l10n": "^0.0.13", + "jsonc-parser": "^3.2.0", + "vscode-languageserver-textdocument": "^1.0.8", + "vscode-languageserver-types": "^3.17.3", + "vscode-uri": "^3.0.7" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz", + "integrity": "sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==" + }, + "vscode-languageserver-types": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", + "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + }, + "vscode-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", + "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/test/schemas/package.json b/test/schemas/package.json new file mode 100644 index 0000000..c318ca0 --- /dev/null +++ b/test/schemas/package.json @@ -0,0 +1,28 @@ +{ + "dependencies": { + "ajv-formats": "^2.1.1", + "js-yaml": "^4.1.0", + "safe-stable-stringify": "^2.4.3", + "ts-node": "^10.9.1", + "vscode-json-languageservice": "^5.3.5" + }, + "scripts": { + "compile": "tsc -p ./src", + "deps": "npx --yes npm-check-updates -u && npm install --ignore-scripts", + "test": "python3 src/rebuild.py && mocha" + }, + "devDependencies": { + "@types/chai": "^4.3.5", + "@types/js-yaml": "^4.0.5", + "@types/minimatch": "^5.1.2", + "@types/mocha": "^10.0.1", + "@types/node": "^20.3.1", + "chai": "^4.3.7", + "minimatch": "^9.0.1", + "mocha": "^10.2.0", + "typescript": "^5.1.3" + }, + "directories": { + "test": "./src" + } +} diff --git a/test/schemas/src/rebuild.py b/test/schemas/src/rebuild.py new file mode 100644 index 0000000..2fab8c0 --- /dev/null +++ b/test/schemas/src/rebuild.py @@ -0,0 +1,140 @@ +"""Utility to generate some complex patterns.""" +import copy +import json +import keyword +import sys +from pathlib import Path +from typing import Any + +play_keywords = list( + filter( + None, + """\ +any_errors_fatal +become +become_exe +become_flags +become_method +become_user +check_mode +collections +connection +debugger +diff +environment +fact_path +force_handlers +gather_facts +gather_subset +gather_timeout +handlers +hosts +ignore_errors +ignore_unreachable +max_fail_percentage +module_defaults +name +no_log +order +port +post_tasks +pre_tasks +remote_user +roles +run_once +serial +strategy +tags +tasks +throttle +timeout +vars +vars_files +vars_prompt +""".split(), + ), +) + + +def is_ref_used(obj: Any, ref: str) -> bool: + """Return a reference use from a schema.""" + ref_use = f"#/$defs/{ref}" + if isinstance(obj, dict): + if obj.get("$ref", None) == ref_use: + return True + for _ in obj.values(): + if isinstance(_, (dict, list)) and is_ref_used(_, ref): + return True + elif isinstance(obj, list): + for _ in obj: + if isinstance(_, (dict, list)) and is_ref_used(_, ref): + return True + return False + + +if __name__ == "__main__": + invalid_var_names = sorted(list(keyword.kwlist) + play_keywords) + if "__peg_parser__" in invalid_var_names: + invalid_var_names.remove("__peg_parser__") + print("Updating invalid var names") # noqa: T201 + + with Path("f/vars.json").open("r+", encoding="utf-8") as f: + vars_schema = json.load(f) + vars_schema["anyOf"][0]["patternProperties"] = { + f"^(?!({'|'.join(invalid_var_names)})$)[a-zA-Z_][\\w]*$": {}, + } + f.seek(0) + json.dump(vars_schema, f, indent=2) + f.write("\n") + f.truncate() + + print("Compiling subschemas...") # noqa: T201 + with Path("f/ansible.json").open(encoding="utf-8") as f: + combined_json = json.load(f) + + for subschema in ["tasks", "playbook"]: + sub_json = copy.deepcopy(combined_json) + # remove unsafe keys from root + for key in [ + "$id", + "id", + "title", + "description", + "type", + "default", + "items", + "properties", + "additionalProperties", + "examples", + ]: + if key in sub_json: + del sub_json[key] + for key in sub_json: + if key not in ["$schema", "$defs"]: + print( # noqa: T201 + f"Unexpected key found at combined schema root: ${key}", + ) + sys.exit(2) + # Copy keys from subschema to root + for key, value in combined_json["$defs"][subschema].items(): + sub_json[key] = value + sub_json["$comment"] = "Generated from ansible.json, do not edit." + sub_json[ + "$id" + ] = f"https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/{subschema}.json" + + # Remove all unreferenced ($ref) definitions ($defs) recursively + while True: + spare = [] + for k in sub_json["$defs"]: + if not is_ref_used(sub_json, k): + spare.append(k) + for k in spare: + print(f"{subschema}: deleting unused '{k}' definition") # noqa: T201 + del sub_json["$defs"][k] + if not spare: + break + + with Path(f"f/{subschema}.json").open("w", encoding="utf-8") as f: + json.dump(sub_json, f, indent=2, sort_keys=True) + f.write("\n") diff --git a/test/schemas/src/schema.spec.ts b/test/schemas/src/schema.spec.ts new file mode 100644 index 0000000..b826461 --- /dev/null +++ b/test/schemas/src/schema.spec.ts @@ -0,0 +1,184 @@ +import * as path from "path"; +import Ajv from "ajv"; +import fs from "fs"; +import { minimatch } from "minimatch"; +import yaml from "js-yaml"; +import { assert } from "chai"; +import stringify from "safe-stable-stringify"; +import { integer } from "vscode-languageserver-types"; +import { exec } from "child_process"; +const spawnSync = require("child_process").spawnSync; + +function ansiRegex({ onlyFirst = false } = {}) { + const pattern = [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", + ].join("|"); + + return new RegExp(pattern, onlyFirst ? undefined : "g"); +} + +function stripAnsi(data: string) { + if (typeof data !== "string") { + throw new TypeError( + `Expected a \`string\`, got \`${typeof data}\ = ${data}` + ); + } + return data.replace(ansiRegex(), ""); +} + +const ajv = new Ajv({ + strictTypes: false, + strict: false, + inlineRefs: true, // https://github.com/ajv-validator/ajv/issues/1581#issuecomment-832211568 + allErrors: true, // https://github.com/ajv-validator/ajv/issues/1581#issuecomment-832211568 +}); + +// load whitelist of all test file subjects schemas can reference +const test_files = getAllFiles("./test"); +const negative_test_files = getAllFiles("./negative_test"); + +// load all schemas +const schema_files = fs + .readdirSync("f/") + .filter((el) => path.extname(el) === ".json"); +console.log(`Schemas: ${schema_files}`); + +describe("schemas under f/", function () { + schema_files.forEach((schema_file) => { + if ( + schema_file.startsWith("_") || + ["ansible-navigator-config.json", "rulebook.json"].includes(schema_file) + ) { + return; + } + const schema_json = JSON.parse(fs.readFileSync(`f/${schema_file}`, "utf8")); + ajv.addSchema(schema_json); + const validator = ajv.compile(schema_json); + if (schema_json.examples == undefined) { + console.error( + `Schema file ${schema_file} is missing an examples key that we need for documenting file matching patterns.` + ); + return process.exit(1); + } + describe(schema_file, function () { + getTestFiles(schema_json.examples).forEach( + ({ file: test_file, expect_fail }) => { + it(`linting ${test_file} using ${schema_file}`, function () { + var errors_md = ""; + const result = validator( + yaml.load(fs.readFileSync(test_file, "utf8")) + ); + if (validator.errors) { + errors_md += "# ajv errors\n\n```json\n"; + errors_md += stringify(validator.errors, null, 2); + errors_md += "\n```\n\n"; + } + // validate using check-jsonschema (python-jsonschema): + // const py = exec(); + // Do not use python -m ... calling notation because for some + // reason, nodejs environment lacks some env variables needed + // and breaks usage from inside virtualenvs. + const proc = spawnSync( + `${process.env.VIRTUAL_ENV}/bin/check-jsonschema -v -o json --schemafile f/${schema_file} ${test_file}`, + { shell: true, encoding: "utf-8", stdio: "pipe" } + ); + if (proc.status != 0) { + // real errors are sent to stderr due to https://github.com/python-jsonschema/check-jsonschema/issues/88 + errors_md += "# check-jsonschema\n\nstdout:\n\n```json\n"; + errors_md += stripAnsi(proc.output[1]); + errors_md += "```\n"; + if (proc.output[2]) { + errors_md += "\nstderr:\n\n```\n"; + errors_md += stripAnsi(proc.output[2]); + errors_md += "```\n"; + } + } + + // dump errors to markdown file for manual inspection + const md_filename = `${test_file}.md`; + if (errors_md) { + fs.writeFileSync(md_filename, errors_md); + } else { + // if no error occurs, we should ensure there is no md file present + fs.unlink(md_filename, function (err) { + if (err && err.code != "ENOENT") { + console.error(`Failed to remove ${md_filename}.`); + } + }); + } + assert.equal( + result, + !expect_fail, + `${JSON.stringify(validator.errors)}` + ); + }); + } + ); + // All /$defs/ that have examples property are assumed to be + // subschemas, "tasks" being the primary such case, which is also used + // for validating separated files. + for (var definition in schema_json["$defs"]) { + if (schema_json["$defs"][definition].examples) { + const subschema_uri = `${schema_json["$id"]}#/$defs/${definition}`; + const subschema_validator = ajv.getSchema(subschema_uri); + if (!subschema_validator) { + console.error(`Failed to load subschema ${subschema_uri}`); + return process.exit(1); + } + getTestFiles(schema_json["$defs"][definition].examples).forEach( + ({ file: test_file, expect_fail }) => { + it(`linting ${test_file} using ${subschema_uri}`, function () { + const result = subschema_validator( + yaml.load(fs.readFileSync(test_file, "utf8")) + ); + assert.equal( + result, + !expect_fail, + `${JSON.stringify(validator.errors)}` + ); + }); + } + ); + } + } + }); + }); +}); + +// find all tests for each schema file +function getTestFiles( + globs: string[] +): { file: string; expect_fail: boolean }[] { + const files = Array.from( + new Set( + globs + .map((glob: any) => minimatch.match(test_files, path.join("**", glob))) + .flat() + ) + ); + const negative_files = Array.from( + new Set( + globs + .map((glob: any) => + minimatch.match(negative_test_files, path.join("**", glob)) + ) + .flat() + ) + ); + + // All fails ending with fail, like `foo.fail.yml` are expected to fail validation + let result = files.map((f) => ({ file: f, expect_fail: false })); + result = result.concat( + negative_files.map((f) => ({ file: f, expect_fail: true })) + ); + return result; +} + +function getAllFiles(dir: string): string[] { + return fs.readdirSync(dir).reduce((files: string[], file: string) => { + const name = path.join(dir, file); + const isDirectory = fs.statSync(name).isDirectory(); + return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name]; + }, []); +} diff --git a/test/schemas/test/.config/ansible-lint.yml b/test/schemas/test/.config/ansible-lint.yml new file mode 100644 index 0000000..0e7d05d --- /dev/null +++ b/test/schemas/test/.config/ansible-lint.yml @@ -0,0 +1,9 @@ +--- +# .ansible-lint +profile: basic +rules: + name[missing]: + exclude_paths: [] + custom-inc-rule: + exclude_paths: + - "tests/*.yml" diff --git a/test/schemas/test/ansible-navigator.yml b/test/schemas/test/ansible-navigator.yml new file mode 100644 index 0000000..e627b78 --- /dev/null +++ b/test/schemas/test/ansible-navigator.yml @@ -0,0 +1,85 @@ +--- +ansible-navigator: + ansible: + config: /tmp/ansible.cfg + cmdline: "--forks 15" + inventories: + - /tmp/test_inventory.yml + playbook: /tmp/test_playbook.yml + + ansible-builder: + workdir: /tmp/ + + ansible-runner: + artifact-dir: /tmp/test1 + rotate-artifacts-count: 10 + timeout: 300 + + app: run + + collection-doc-cache-path: /tmp/cache.db + + color: + enable: False + osc4: False + + documentation: + plugin: + name: shell + type: become + + editor: + command: vim_from_setting + console: False + + exec: + shell: False + command: /bin/foo + + execution-environment: + container-engine: podman + enabled: False + environment-variables: + pass: + - ONE + - TWO + - THREE + set: + KEY1: VALUE1 + KEY2: VALUE2 + KEY3: VALUE3 + image: test_image:latest + pull-policy: never + volume-mounts: + - src: "/test1" + dest: "/test1" + label: "Z" + container-options: + - "--net=host" + + help-builder: False + + help-config: True + + help-doc: True + + help-inventory: True + + help-playbook: False + + inventory-columns: + - ansible_network_os + - ansible_network_cli_ssh_type + - ansible_connection + + logging: + level: critical + append: False + file: /tmp/log.txt + + mode: stdout + + playbook-artifact: + enable: True + replay: /tmp/test_artifact.json + save-as: /tmp/test_artifact.json diff --git a/test/schemas/test/changelog.yml b/test/schemas/test/changelog.yml new file mode 100644 index 0000000..99bcb2f --- /dev/null +++ b/test/schemas/test/changelog.yml @@ -0,0 +1,47 @@ +ancestor: 0.5.4 +releases: + 1.0.0-alpha: + release_date: "2020-01-01" + codename: "The first public one" + changes: + release_summary: A bit o markdown text + major_changes: + - Free form text mentioning a major change + minor_changes: + - Free form text mentioning a minor change + breaking_changes: + - Free form text mentioning a breaking change + deprecated_features: + - A list of strings describing features deprecated in this release + removed_features: + - A list of strings describing features removed in this release + security_fixes: + - A list of strings describing security-relevant bugfixes + bugfixes: + - Fixed bug `#1 <https://example.com>` + known_issues: + - A list of strings describing known issues that are currently not fixed or will not be fixed + trivial: + - A list of strings describing changes that are too trivial to show in the changelog + modules: + - name: short_module_name + description: foo + namespace: foo + plugins: + lookup: + - name: reverse + description: Reverse magic + namespace: null + inventory: + - name: docker + description: Inventory plugin for docker containers + namespace: null + objects: + role: + - name: install_reqs + description: Install all requirements of this collection + namespace: null + playbook: + - name: wipe_personal_data + description: Wipes all personal data from the database + namespace: null diff --git a/test/schemas/test/changelogs/maximal/changelog.yaml b/test/schemas/test/changelogs/maximal/changelog.yaml new file mode 100644 index 0000000..8e063c7 --- /dev/null +++ b/test/schemas/test/changelogs/maximal/changelog.yaml @@ -0,0 +1,61 @@ +--- +# Example of minimal changelogs/changelog.yaml that is considered valid +ancestor: null + +releases: + 1.0.0-alpha: + release_date: "1980-01-01" + codename: foo + fragments: [] + changes: + release_summary: This is the initial White Rabbit release. Enjoy! + major_changes: + - The authentication method handling has been rewritten. + minor_changes: + - foo - Module can now reformat hard disks without asking. + - bob lookup - Makes sure Bob isn't there multiple times. + breaking_changes: + - Due to the security bug in the post module, the module no longer accepts the password + option. Please stop using the option and change any password you ever supplied to the + module. + deprecated_features: + - foo - The bar option has been deprecated. Use the username option instead. + - send_request - The quick option has been deprecated. Use the protocol option instead. + removed_features: + - foo - The baz option has been removed. It has never been used anyway. + security_fixes: + - post - The module accidentally sent your password in plaintext to all servers it could find. + bugfixes: + - post - The module made PUT requests instead of POST requests. + - get - The module will no longer crash if it received invalid JSON data + trivial: + - something that is not included in release notes + known_issues: + - som other + xxx: + - we should ignore unknown keys because user can define custom section in changelogs/config.yaml file + modules: + - name: head + description: Make a HEAD request + namespace: "net_tools.rest" + - name: echo + description: Echo params + namespace: "" + plugins: + lookup: + - name: reverse + description: Reverse magic + namespace: null + inventory: + - name: docker + description: Inventory plugin for docker containers + namespace: null + objects: + role: + - name: install_reqs + description: Install all requirements of this collection + namespace: null + playbook: + - name: wipe_personal_data + description: Wipes all personal data from the database + namespace: null diff --git a/test/schemas/test/changelogs/minimal/changelog.yaml b/test/schemas/test/changelogs/minimal/changelog.yaml new file mode 100644 index 0000000..d1618f0 --- /dev/null +++ b/test/schemas/test/changelogs/minimal/changelog.yaml @@ -0,0 +1,3 @@ +--- +# Example of minimal changelogs/changelog.yaml that is considered valid +releases: {} diff --git a/test/schemas/test/execution-environment-v3.yml b/test/schemas/test/execution-environment-v3.yml new file mode 100644 index 0000000..edc4fe2 --- /dev/null +++ b/test/schemas/test/execution-environment-v3.yml @@ -0,0 +1,19 @@ +--- +version: 3 + +images: + base_image: + name: "quay.io/ansible/ansible-runner:stable-2.10-devel" + +dependencies: + galaxy: requirements.yml + python: requirements.txt + system: bindep.txt + +additional_build_steps: + prepend_base: | + RUN whoami + RUN cat /etc/os-release + append_base: + - RUN echo This is a post-install command! + - RUN ls -la /etc diff --git a/test/schemas/test/execution-environment.yml b/test/schemas/test/execution-environment.yml new file mode 100644 index 0000000..e447a9a --- /dev/null +++ b/test/schemas/test/execution-environment.yml @@ -0,0 +1,21 @@ +--- +# Example from https://docs.ansible.com/automation-controller/latest/html/userguide/ee_reference.html +version: 1 + +build_arg_defaults: + EE_BASE_IMAGE: "quay.io/ansible/ansible-runner:stable-2.10-devel" + +ansible_config: "ansible.cfg" + +dependencies: + galaxy: requirements.yml + python: requirements.txt + system: bindep.txt + +additional_build_steps: + prepend: | + RUN whoami + RUN cat /etc/os-release + append: + - RUN echo This is a post-install command! + - RUN ls -la /etc diff --git a/test/schemas/test/galaxy.yml b/test/schemas/test/galaxy.yml new file mode 100644 index 0000000..004344f --- /dev/null +++ b/test/schemas/test/galaxy.yml @@ -0,0 +1,17 @@ +name: foo +namespace: bar +version: 1.2.3 +authors: + - John +readme: ../README.md +description: ... +dependencies: + "other_namespace.collection1": ">=1.0.0" + "other_namespace.collection2": ">=2.0.0,<3.0.0" + "anderson55.my_collection": "*" # note: "*" selects the highest version available +# upload to galaxy will fail if a repository key is not present +repository: https://www.github.com/my_org/my_collection +manifest: + directives: + - "foo" + omit_default_directives: true diff --git a/test/schemas/test/inventory.yml b/test/schemas/test/inventory.yml new file mode 100644 index 0000000..48a0e6a --- /dev/null +++ b/test/schemas/test/inventory.yml @@ -0,0 +1,13 @@ +all: + hosts: + mail.example.com: + children: + webservers: + hosts: + foo.example.com: + bar[01:50:2].example.com: + dbservers: + hosts: + one.example.com: + two.example.com: + three.example.com: diff --git a/test/schemas/test/inventory/inventory.yml b/test/schemas/test/inventory/inventory.yml new file mode 100644 index 0000000..8752d9b --- /dev/null +++ b/test/schemas/test/inventory/inventory.yml @@ -0,0 +1,31 @@ +--- +# https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html +ungrouped: {} +all: + hosts: + mail.example.com: + children: + webservers: + hosts: + foo.example.com: + bar.example.com: + dbservers: + hosts: + one.example.com: + two.example.com: + three.example.com: + east: + hosts: + foo.example.com: + one.example.com: + two.example.com: + west: + hosts: + bar.example.com: + three.example.com: + prod: + children: + east: {} + test: + children: + west: {} diff --git a/test/schemas/test/inventory/production.yml b/test/schemas/test/inventory/production.yml new file mode 100644 index 0000000..6350bda --- /dev/null +++ b/test/schemas/test/inventory/production.yml @@ -0,0 +1,37 @@ +all: + hosts: + mail.example.com: + children: + webservers: + hosts: + foo.example.com: + bar.example.com: + # ranges are supported: + www[01:50].example.com: + www[01:50:2].example.com: + # these are variables: + var_1: value_1 + another_var: 200 + dbservers: + hosts: + one.example.com: + two.example.com: + three.example.com: + east: + hosts: + foo.example.com: + one.example.com: + two.example.com: + west: + hosts: + bar.example.com: + three.example.com: + prod: + children: + east: + test: + children: + west: + # add variables for all hosts + vars: + my_var: 123 diff --git a/test/schemas/test/meta/requirements.yml b/test/schemas/test/meta/requirements.yml new file mode 100644 index 0000000..6b07e4f --- /dev/null +++ b/test/schemas/test/meta/requirements.yml @@ -0,0 +1,3 @@ +# requirements v2 +collections: [] +roles: [] diff --git a/test/schemas/test/meta/runtime.yml b/test/schemas/test/meta/runtime.yml new file mode 100644 index 0000000..6a992c4 --- /dev/null +++ b/test/schemas/test/meta/runtime.yml @@ -0,0 +1 @@ +requires_ansible: ">=2.12,<2.14" diff --git a/test/schemas/test/molecule/cluster/base.yml b/test/schemas/test/molecule/cluster/base.yml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/schemas/test/molecule/cluster/base.yml diff --git a/test/schemas/test/molecule/cluster/converge.yml b/test/schemas/test/molecule/cluster/converge.yml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/schemas/test/molecule/cluster/converge.yml diff --git a/test/schemas/test/molecule/cluster/foobar.yml b/test/schemas/test/molecule/cluster/foobar.yml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/schemas/test/molecule/cluster/foobar.yml diff --git a/test/schemas/test/molecule/cluster/molecule.yml b/test/schemas/test/molecule/cluster/molecule.yml new file mode 100644 index 0000000..f3e586c --- /dev/null +++ b/test/schemas/test/molecule/cluster/molecule.yml @@ -0,0 +1,76 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +lint: | + set -e + yamllint -c molecule/yaml-lint.yml . + ansible-lint + +platforms: + - name: instance-1 + image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: true + groups: + - zookeeper + env: + - Hello: world! + + - name: instance-2 + image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: true + groups: + - zookeeper + env: + - Hello: world! + + - name: instance-3 + image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: true + groups: + - zookeeper + env: + - Hello: world! + +provisioner: + name: ansible + log: false + playbooks: + converge: ${MOLECULE_PLAYBOOK:-converge.yml} + inventory: + host_vars: + instance-1: + zookeeper_id: 0 + instance-2: + zookeeper_id: 1 + instance-3: + zookeeper_id: 2 + +scenario: + name: cluster + test_sequence: + - destroy + - create + - prepare + - converge + - check + - verify + - destroy + +verifier: + name: ansible diff --git a/test/schemas/test/molecule/default/molecule.yml b/test/schemas/test/molecule/default/molecule.yml new file mode 100644 index 0000000..b573e74 --- /dev/null +++ b/test/schemas/test/molecule/default/molecule.yml @@ -0,0 +1,117 @@ +--- +dependency: + name: shell + enabled: true + command: path/to/command --flag1 subcommand --flag2 + options: + ignore-certs: true + ignore-errors: true + env: + FOO: bar + +lint: | + set -e + yamllint . + ansible-lint + +driver: + name: podman + options: + managed: false + login_cmd_template: ... + ansible_connection_options: + ansible_connection: ssh + # vagrant options: + provider: + name: virtualbox + +log: true + +platforms: + - name: ubi8 + hostname: ubi8 + children: [] # list of strings + unknown_property_foo: bar # unknown properties should be allowed for drivers + groups: + - ubi8 + image: ubi8/ubi-init + pre_build_image: true + registry: + url: registry.access.redhat.com + dockerfile: Dockerfile + pkg_extras: python*setuptools + volumes: + - /etc/ci/mirror_info.sh:/etc/ci/mirror_info.sh:ro + - /etc/pki/rpm-gpg:/etc/pki/rpm-gpg + privileged: true + environment: &env + http_proxy: "{{ lookup('env', 'http_proxy') }}" + https_proxy: "{{ lookup('env', 'https_proxy') }}" + ulimits: &ulimit + - host + # vagrant ones + box: foo/bar + memory: 1024 + cpus: 2 + provider_raw_config_args: [] + networks: # used by docker/podman + - name: foo + + - name: ubi7 + hostname: ubi7 + children: ["ubi8"] + groups: + - ubi7 + image: ubi7/ubi-init + registry: + url: registry.access.redhat.com + command: /sbin/init + tmpfs: + - /run + - /tmp + volumes: + - /etc/ci/mirror_info.sh:/etc/ci/mirror_info.sh:ro + - /etc/pki/rpm-gpg:/etc/pki/rpm-gpg + - /sys/fs/cgroup:/sys/fs/cgroup:ro + network_mode: service:vpn + privileged: true + environment: &env + http_proxy: "{{ lookup('env', 'http_proxy') }}" + https_proxy: "{{ lookup('env', 'https_proxy') }}" + ulimits: &ulimit + - host + +provisioner: + playbooks: + prepare: prepare.yml + inventory: + hosts: + all: + hosts: + ubi8: + ansible_python_interpreter: /usr/bin/python3 + ubi7: + selinux: permissive + ubi8: + selinux: enforced + name: ansible + log: true + env: + ANSIBLE_STDOUT_CALLBACK: yaml + config_options: + defaults: + fact_caching: jsonfile + fact_caching_connection: /tmp/molecule/facts + +scenario: + test_sequence: + - destroy + - create + - prepare + - converge + - check + - verify + - destroy + +verifier: + name: testinfra diff --git a/test/schemas/test/molecule/vagrant/molecule.yml b/test/schemas/test/molecule/vagrant/molecule.yml new file mode 100644 index 0000000..dea2c07 --- /dev/null +++ b/test/schemas/test/molecule/vagrant/molecule.yml @@ -0,0 +1,46 @@ +--- +dependency: + name: shell + enabled: false + +lint: | + set -e + yamllint . + ansible-lint + +driver: + name: vagrant + provider: + name: libvirt + provision: false + cachier: machine + parallel: true + default_box: "generic/alpine310" +platforms: + - name: instance + hostname: foo.bar.com + interfaces: + - auto_config: true + network_name: private_network + type: dhcp + instance_raw_config_args: + - 'vm.synced_folder ".", "/vagrant", type: "rsync"' + - 'vm.provision :shell, inline: "uname"' + config_options: + ssh.keep_alive: true + ssh.remote_user: "vagrant" + synced_folder: true + box: fedora/32-cloud-base + box_version: 32.20200422.0 + box_url: "http://127.0.0.1/box.img" + memory: 512 + cpus: 1 + provider_options: + video_type: "vga" + provider_raw_config_args: + - cpuset = '1-4,^3,6' + - name: instance2 + hostname: false + +provisioner: + name: ansible diff --git a/test/schemas/test/playbooks/block.yml b/test/schemas/test/playbooks/block.yml new file mode 100644 index 0000000..631242b --- /dev/null +++ b/test/schemas/test/playbooks/block.yml @@ -0,0 +1,10 @@ +- hosts: localhost + tasks: + - debug: + msg: task under no block + - block: + - debug: + msg: task under one level of block + - block: + - debug: + msg: task under two levels of block diff --git a/test/schemas/test/playbooks/defaults/foo.yml b/test/schemas/test/playbooks/defaults/foo.yml new file mode 100644 index 0000000..47d9438 --- /dev/null +++ b/test/schemas/test/playbooks/defaults/foo.yml @@ -0,0 +1,3 @@ +# defaults have same format as vars +in_is_reserved: ... +ss: ss diff --git a/test/schemas/test/playbooks/environment.yml b/test/schemas/test/playbooks/environment.yml new file mode 100644 index 0000000..d25fd1b --- /dev/null +++ b/test/schemas/test/playbooks/environment.yml @@ -0,0 +1,7 @@ +--- +- hosts: localhost + environment: # <- valid + FOO: BAR + +- hosts: localhost + environment: "{{ foo }}" # <- valid diff --git a/test/schemas/test/playbooks/failed_when.yml b/test/schemas/test/playbooks/failed_when.yml new file mode 100644 index 0000000..14c942a --- /dev/null +++ b/test/schemas/test/playbooks/failed_when.yml @@ -0,0 +1,18 @@ +- hosts: localhost + tasks: + - name: foo + ansible.builtin.debug: + msg: foo! + failed_when: false # <- valid + + - name: foo + ansible.builtin.debug: + msg: foo! + failed_when: "string is valid too" # <- valid + + - name: foo + ansible.builtin.debug: + msg: foo! + failed_when: # <- lists are valid too + - foo + - bar diff --git a/test/schemas/test/playbooks/full-jinja.yml b/test/schemas/test/playbooks/full-jinja.yml new file mode 100644 index 0000000..22eaafe --- /dev/null +++ b/test/schemas/test/playbooks/full-jinja.yml @@ -0,0 +1,16 @@ +--- +- name: Test that schema allows multiline-jinja + hosts: localhost + # https://github.com/ansible/ansible-lint/issues/2772 + become: >- + {{ + true + }} + tasks: + - name: Test more complex jinja is also allowed + ansible.builtin.debug: + msg: "{{ item }}" + # that below is valid and show be allowed: + with_items: >- + {%- set ns = [1, 1, 2] -%} + {{- ns | unique -}} diff --git a/test/schemas/test/playbooks/gather_facts.yml b/test/schemas/test/playbooks/gather_facts.yml new file mode 100644 index 0000000..598188d --- /dev/null +++ b/test/schemas/test/playbooks/gather_facts.yml @@ -0,0 +1,6 @@ +--- +- hosts: localhost + gather_facts: false + tasks: + - ansible.builtin.debug: + msg: foo diff --git a/test/schemas/test/playbooks/gather_subset.yml b/test/schemas/test/playbooks/gather_subset.yml new file mode 100644 index 0000000..de0e689 --- /dev/null +++ b/test/schemas/test/playbooks/gather_subset.yml @@ -0,0 +1,15 @@ +--- +- hosts: localhost + gather_subset: + - all + - "!network" + tasks: + - ansible.builtin.debug: + msg: foo + +- hosts: localhost + gather_subset: + - all + tasks: + - ansible.builtin.debug: + msg: bar diff --git a/test/schemas/test/playbooks/ignore_errors..yml b/test/schemas/test/playbooks/ignore_errors..yml new file mode 100644 index 0000000..6c92046 --- /dev/null +++ b/test/schemas/test/playbooks/ignore_errors..yml @@ -0,0 +1,9 @@ +- hosts: localhost + tasks: + - command: echo 123 + ignore_errors: true + + - command: echo 123 + vars: + should_ignore_errors: true + ignore_errors: "{{ should_ignore_errors }}" diff --git a/test/schemas/test/playbooks/import_playbook.yml b/test/schemas/test/playbooks/import_playbook.yml new file mode 100644 index 0000000..efd8787 --- /dev/null +++ b/test/schemas/test/playbooks/import_playbook.yml @@ -0,0 +1,9 @@ +- ansible.builtin.import_playbook: other.yml + +- import_playbook: other.yml + tags: + - foo + +- import_playbook: other.yml + when: + - foo is true diff --git a/test/schemas/test/playbooks/included.yml b/test/schemas/test/playbooks/included.yml new file mode 100644 index 0000000..468a17c --- /dev/null +++ b/test/schemas/test/playbooks/included.yml @@ -0,0 +1 @@ +- hosts: localhost diff --git a/test/schemas/test/playbooks/integers.yml b/test/schemas/test/playbooks/integers.yml new file mode 100644 index 0000000..861acee --- /dev/null +++ b/test/schemas/test/playbooks/integers.yml @@ -0,0 +1,23 @@ +--- +- hosts: localhost + vars: + some: 0 + gather_timeout: "{{ some }}" + tasks: + - ansible.builtin.debug: + msg: "{{ item }}" + async: 0 + poll: 0 + delay: 0 + timeout: 0 + port: 0 + - ansible.builtin.debug: + msg: "{{ item }}" + async: "{{ some }}" + poll: "{{ some }}" + delay: "{{ some }}" + timeout: "{{ some }}" + port: "{{ some }}" + +- hosts: localhost + gather_timeout: 0 diff --git a/test/schemas/test/playbooks/local_action_dict.yml b/test/schemas/test/playbooks/local_action_dict.yml new file mode 100644 index 0000000..05b3129 --- /dev/null +++ b/test/schemas/test/playbooks/local_action_dict.yml @@ -0,0 +1,5 @@ +- hosts: localhost + tasks: + - local_action: + module: ansible.builtin.debug + msg: hello diff --git a/test/schemas/test/playbooks/local_action_string.yml b/test/schemas/test/playbooks/local_action_string.yml new file mode 100644 index 0000000..e7dacc4 --- /dev/null +++ b/test/schemas/test/playbooks/local_action_string.yml @@ -0,0 +1,3 @@ +- hosts: localhost + tasks: + - local_action: "ansible.builtin.debug msg=hello" diff --git a/test/schemas/test/playbooks/loop.yml b/test/schemas/test/playbooks/loop.yml new file mode 100644 index 0000000..c0e1734 --- /dev/null +++ b/test/schemas/test/playbooks/loop.yml @@ -0,0 +1,9 @@ +--- +- hosts: localhost + tasks: + - name: that should pass + ansible.builtin.debug: + var: item + loop: + - foo + - bar diff --git a/test/schemas/test/playbooks/no_log.yml b/test/schemas/test/playbooks/no_log.yml new file mode 100644 index 0000000..e1944dd --- /dev/null +++ b/test/schemas/test/playbooks/no_log.yml @@ -0,0 +1,11 @@ +- hosts: localhost + vars: + some_var: true + tasks: + - ansible.builtin.debug: + msg: foo + no_log: true + + - ansible.builtin.debug: + msg: foo + no_log: "{{ some_var }}" diff --git a/test/schemas/test/playbooks/roles.yml b/test/schemas/test/playbooks/roles.yml new file mode 100644 index 0000000..a996ce0 --- /dev/null +++ b/test/schemas/test/playbooks/roles.yml @@ -0,0 +1,13 @@ +- hosts: localhost + roles: [] + +- hosts: localhost + roles: + - foo + - role: "path/to/role" + vars: + FOO: bar + tags: + - foo + - role: bar + tags: string_tag diff --git a/test/schemas/test/playbooks/run.yml b/test/schemas/test/playbooks/run.yml new file mode 100644 index 0000000..52e7001 --- /dev/null +++ b/test/schemas/test/playbooks/run.yml @@ -0,0 +1,42 @@ +- name: foo + ansible.builtin.import_playbook: included.yml + +- hosts: # to check if lists are allowed: + - localhost + - webservers + # validate serial allows strings like percentage value + serial: 10% + handlers: + - name: handler 1 + ansible.builtin.debug: + msg: "I am handler 1" + listen: "always handler" + + - name: handler 2 + ansible.builtin.debug: + msg: "I am handler 2" + listen: # to check if lists are allowed: + - "list listening handler" + - "other listening topic" + +- hosts: localhost + serial: 1 # validate serial allows integer + +- hosts: localhost + serial: "{{ 1 }}" # jinja also ok + +- hosts: localhost + serial: # validate serial allows these too: + - 123 + - 10% + - "{{ some }}" # jinja also ok + +- hosts: localhost + tasks: + - debug: + msg: "failed_when should accept booleans" + failed_when: false + + - debug: + msg: "failed_when should allow strings" + failed_when: "'foo' in 'foobar'" diff --git a/test/schemas/test/playbooks/run_once.yml b/test/schemas/test/playbooks/run_once.yml new file mode 100644 index 0000000..be36c8e --- /dev/null +++ b/test/schemas/test/playbooks/run_once.yml @@ -0,0 +1,6 @@ +- hosts: localhost + tasks: + - name: foo2 + ansible.builtin.debug: + msg: foo! + run_once: "{{ true }}" # valid diff --git a/test/schemas/test/playbooks/tags.yml b/test/schemas/test/playbooks/tags.yml new file mode 100644 index 0000000..b758257 --- /dev/null +++ b/test/schemas/test/playbooks/tags.yml @@ -0,0 +1,23 @@ +- hosts: localhost + roles: + - role: foo + tags: foo # <-- allowed + - role: foo + tags: # <-- allowed + - foo + - bar + tags: # <-- allowed + - foo + - bar + tasks: + - ansible.builtin.debug: + msg: "..." + tags: # <-- allowed + - foo + - bar + - ansible.builtin.debug: + msg: "..." + tags: # <-- allowed + - foo +- hosts: localhost + tags: foo # <-- allowed diff --git a/test/schemas/test/playbooks/tasks.yml b/test/schemas/test/playbooks/tasks.yml new file mode 100644 index 0000000..b01cf8c --- /dev/null +++ b/test/schemas/test/playbooks/tasks.yml @@ -0,0 +1,5 @@ +- hosts: localhost + pre_tasks: [] + post_tasks: [] + tasks: [] + handlers: [] diff --git a/test/schemas/test/playbooks/tasks/args.yml b/test/schemas/test/playbooks/tasks/args.yml new file mode 100644 index 0000000..1e25e1d --- /dev/null +++ b/test/schemas/test/playbooks/tasks/args.yml @@ -0,0 +1,4 @@ +- action: foo + args: {} +- action: foo + args: "{{ {} }}" diff --git a/test/schemas/test/playbooks/tasks/become_method.yml b/test/schemas/test/playbooks/tasks/become_method.yml new file mode 100644 index 0000000..9d63a76 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/become_method.yml @@ -0,0 +1,7 @@ +- command: echo 123 + become_method: sudo + +- command: echo 123 + vars: + sudo_var: doo + become_method: "{{ sudo_var }}" # templating is ok diff --git a/test/schemas/test/playbooks/tasks/changed_when.yml b/test/schemas/test/playbooks/tasks/changed_when.yml new file mode 100644 index 0000000..7887ac7 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/changed_when.yml @@ -0,0 +1,10 @@ +- command: echo 123 + changed_when: false + +- command: echo 123 + changed_when: '"1" in ["1", "2", "3"]' + +- command: echo 123 + changed_when: # valid, all items must evaluate as true (AND) + - "foo is defined" + - '"1" in ["1", "2", "3"]' diff --git a/test/schemas/test/playbooks/tasks/diff.yml b/test/schemas/test/playbooks/tasks/diff.yml new file mode 100644 index 0000000..cc0bebc --- /dev/null +++ b/test/schemas/test/playbooks/tasks/diff.yml @@ -0,0 +1,4 @@ +- action: foo + diff: true +- action: foo + diff: "{{ true }}" diff --git a/test/schemas/test/playbooks/tasks/empty_tasks.yml b/test/schemas/test/playbooks/tasks/empty_tasks.yml new file mode 100644 index 0000000..7ee1211 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/empty_tasks.yml @@ -0,0 +1,2 @@ +--- +# this is a valid tasks file, loaded as 'null' document. diff --git a/test/schemas/test/playbooks/tasks/ignore_errors.yml b/test/schemas/test/playbooks/tasks/ignore_errors.yml new file mode 100644 index 0000000..2f253f2 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/ignore_errors.yml @@ -0,0 +1,7 @@ +- command: echo 123 + ignore_errors: true + +- command: echo 123 + vars: + should_ignore_errors: true + ignore_errors: "{{ should_ignore_errors }}" diff --git a/test/schemas/test/playbooks/tasks/local_action_dict.yml b/test/schemas/test/playbooks/tasks/local_action_dict.yml new file mode 100644 index 0000000..5351ab9 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/local_action_dict.yml @@ -0,0 +1,3 @@ +- local_action: + module: ansible.builtin.debug + msg: hello diff --git a/test/schemas/test/playbooks/tasks/local_action_string.yml b/test/schemas/test/playbooks/tasks/local_action_string.yml new file mode 100644 index 0000000..93d98e0 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/local_action_string.yml @@ -0,0 +1 @@ +- local_action: "ansible.builtin.debug msg=hello" diff --git a/test/schemas/test/playbooks/tasks/loop.yml b/test/schemas/test/playbooks/tasks/loop.yml new file mode 100644 index 0000000..33c6130 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/loop.yml @@ -0,0 +1,6 @@ +- name: that should pass + ansible.builtin.debug: + var: item + loop: + - foo + - bar diff --git a/test/schemas/test/playbooks/tasks/no_log.yml b/test/schemas/test/playbooks/tasks/no_log.yml new file mode 100644 index 0000000..83a12d0 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/no_log.yml @@ -0,0 +1,11 @@ +- ansible.builtin.debug: + msg: foo + no_log: true # valid + vars: + some_var: true + +- ansible.builtin.debug: + msg: foo + no_log: "{{ some_var }}" # valid too + vars: + some_var: true diff --git a/test/schemas/test/playbooks/tasks/notify.yml b/test/schemas/test/playbooks/tasks/notify.yml new file mode 100644 index 0000000..88432d9 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/notify.yml @@ -0,0 +1,11 @@ +- name: notify single handler + ansible.builtin.debug: + msg: task with single handler + notify: handler1 + +- name: notify multiple handlers + ansible.builtin.debug: + msg: task with multiple handlers + notify: + - handler1 + - handler2 diff --git a/test/schemas/test/playbooks/tasks/run_once.yml b/test/schemas/test/playbooks/tasks/run_once.yml new file mode 100644 index 0000000..0f3f6f7 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/run_once.yml @@ -0,0 +1,9 @@ +- name: foo + ansible.builtin.debug: + msg: foo! + run_once: true # valid + +- name: foo2 + ansible.builtin.debug: + msg: foo! + run_once: "{{ true }}" # valid diff --git a/test/schemas/test/playbooks/tasks/some_tasks.yml b/test/schemas/test/playbooks/tasks/some_tasks.yml new file mode 100644 index 0000000..2430d52 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/some_tasks.yml @@ -0,0 +1,8 @@ +- name: foo + debug: + msg: bar + delegate_facts: true + +- block: + - debug: + msg: "block under one level of block" diff --git a/test/schemas/test/playbooks/tasks/tags.yml b/test/schemas/test/playbooks/tasks/tags.yml new file mode 100644 index 0000000..a0b7454 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/tags.yml @@ -0,0 +1,29 @@ +- command: echo 123 + tags: + - foo + - bar + +- command: echo 123 + tags: foo + +- block: + - command: echo 123 + tags: + - foo + - bar + + - command: echo 123 + tags: foo + tags: + - foo + - bar + +- block: + - command: echo 123 + tags: + - foo + - bar + + - command: echo 123 + tags: foo + tags: foo diff --git a/test/schemas/test/playbooks/tasks/templated_become.yml b/test/schemas/test/playbooks/tasks/templated_become.yml new file mode 100644 index 0000000..a8cfad3 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/templated_become.yml @@ -0,0 +1,12 @@ +- name: foo + ansible.builtin.debug: + msg: foo! + become: "{{ firewalld_become }}" # <- valid + +- name: foo block + become: "{{ firewalld_become }}" # <- valid + block: + - name: foo + ansible.builtin.debug: + msg: foo! + become: "{{ firewalld_become }}" # <- valid diff --git a/test/schemas/test/playbooks/tasks/templated_integers.yml b/test/schemas/test/playbooks/tasks/templated_integers.yml new file mode 100644 index 0000000..59c4530 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/templated_integers.yml @@ -0,0 +1,5 @@ +- debug: + msg: foo + retries: "{{ 2 }}" # <-- valid + port: "{{ 80 }}" # <-- valid + poll: "{{ 2 }}" # <-- valid diff --git a/test/schemas/test/playbooks/tasks/throttled.yml b/test/schemas/test/playbooks/tasks/throttled.yml new file mode 100644 index 0000000..e1be471 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/throttled.yml @@ -0,0 +1,5 @@ +- action: foo + throttle: 1 # valid + +- action: foo + throttle: "{{ 1 }}" # valid diff --git a/test/schemas/test/playbooks/tasks/until.yml b/test/schemas/test/playbooks/tasks/until.yml new file mode 100644 index 0000000..2146a9d --- /dev/null +++ b/test/schemas/test/playbooks/tasks/until.yml @@ -0,0 +1,14 @@ +- ansible.builtin.debug: + msg: "valid" + until: true + +- ansible.builtin.debug: + msg: "valid" + until: + - "foo not in bar" + +- ansible.builtin.debug: + msg: "valid" + until: + - "'1' in ['1', '2', '3']" + - "foo is not defined" diff --git a/test/schemas/test/playbooks/tasks/when.yml b/test/schemas/test/playbooks/tasks/when.yml new file mode 100644 index 0000000..7874329 --- /dev/null +++ b/test/schemas/test/playbooks/tasks/when.yml @@ -0,0 +1,10 @@ +- action: foo + when: true # valid + +- action: foo 2 + when: foo in bar # valid + +- action: foo 3 + when: # valid + - foo in bar + - apple is orange diff --git a/test/schemas/test/playbooks/tasks/with_items.yml b/test/schemas/test/playbooks/tasks/with_items.yml new file mode 100644 index 0000000..07c72aa --- /dev/null +++ b/test/schemas/test/playbooks/tasks/with_items.yml @@ -0,0 +1,16 @@ +- command: echo 123 + with_items: [] + +- command: echo 123 + with_items: + - 1 + - foo + - {} + - [] + +- command: echo 123 + vars: + my_list: + - 1 + - 2 + with_items: "{{ my_list }}" diff --git a/test/schemas/test/playbooks/templated_become.yml b/test/schemas/test/playbooks/templated_become.yml new file mode 100644 index 0000000..518e46b --- /dev/null +++ b/test/schemas/test/playbooks/templated_become.yml @@ -0,0 +1,16 @@ +--- +- hosts: localhost + become: "{{ firewalld_become }}" # <- valid + tasks: + - name: foo + ansible.builtin.debug: + msg: foo! + become: "{{ firewalld_become }}" # <- valid + + - name: foo block + become: "{{ firewalld_become }}" # <- valid + block: + - name: foo + ansible.builtin.debug: + msg: foo! + become: "{{ firewalld_become }}" # <- valid diff --git a/test/schemas/test/playbooks/user_valid.yml b/test/schemas/test/playbooks/user_valid.yml new file mode 100644 index 0000000..bc6a5e6 --- /dev/null +++ b/test/schemas/test/playbooks/user_valid.yml @@ -0,0 +1,3 @@ +- hosts: localhost + user: foo # <-- allowed, alias to remote_user + tasks: [] diff --git a/test/schemas/test/playbooks/var_files.yml b/test/schemas/test/playbooks/var_files.yml new file mode 100644 index 0000000..2630287 --- /dev/null +++ b/test/schemas/test/playbooks/var_files.yml @@ -0,0 +1,18 @@ +--- +- name: var_files should accept null + hosts: localhost + vars_files: null + +- name: var_files should accept string + hosts: localhost + vars_files: /dev/null + +- name: var_files should accept array[string] + hosts: localhost + vars_files: + - /dev/null + +- name: var_files should accept array of array[string] + hosts: localhost + vars_files: + - ["/dev/null"] diff --git a/test/schemas/test/playbooks/vars/empty_vars.yml b/test/schemas/test/playbooks/vars/empty_vars.yml new file mode 100644 index 0000000..a6e3ce7 --- /dev/null +++ b/test/schemas/test/playbooks/vars/empty_vars.yml @@ -0,0 +1,2 @@ +--- +# Ensure we allow empty var files, matching Ansible behavior diff --git a/test/schemas/test/playbooks/vars/encrypted.yml b/test/schemas/test/playbooks/vars/encrypted.yml new file mode 100644 index 0000000..7808fec --- /dev/null +++ b/test/schemas/test/playbooks/vars/encrypted.yml @@ -0,0 +1,6 @@ +$ANSIBLE_VAULT;1.2;AES256;dev +66373266323161346330626137613862653935343634366636353266323966363665636266363739 +6436363237626633653139636232663131613832336266310a323766643264306436306266663930 +66666238346132373766623932356530333165613835623863653837306130383065323138333034 +6265313861613761620a393663616265633637343534346533366437653839623239396366366330 +3165 diff --git a/test/schemas/test/playbooks/vars/myvars.yml b/test/schemas/test/playbooks/vars/myvars.yml new file mode 100644 index 0000000..8698380 --- /dev/null +++ b/test/schemas/test/playbooks/vars/myvars.yml @@ -0,0 +1,9 @@ +foo: bar +_foo: bar +foo_var_xxx: "{{ sss }}" +in_job: ... +nested: + pear: fruit + apple: fruit +sso_force_handlers: ... +force_handlers_foo: ... diff --git a/test/schemas/test/playbooks/vars_prompt.yml b/test/schemas/test/playbooks/vars_prompt.yml new file mode 100644 index 0000000..1bf65c3 --- /dev/null +++ b/test/schemas/test/playbooks/vars_prompt.yml @@ -0,0 +1,11 @@ +- name: Fixture + hosts: localhost + vars_prompt: + - name: username + prompt: What is your username? + private: false + unsafe: false + + - name: password + prompt: What is your password? + default: "secret" diff --git a/test/schemas/test/playbooks/when.yml b/test/schemas/test/playbooks/when.yml new file mode 100644 index 0000000..93b7781 --- /dev/null +++ b/test/schemas/test/playbooks/when.yml @@ -0,0 +1,11 @@ +--- +- name: Test for when (passing) + hosts: localhost + gather_facts: false + tasks: + - name: Testing for when is passed a list + ansible.builtin.debug: + msg: "this is ok" + when: + - true + - "foo" diff --git a/test/schemas/test/playbooks/with_.yml b/test/schemas/test/playbooks/with_.yml new file mode 100644 index 0000000..b3a3748 --- /dev/null +++ b/test/schemas/test/playbooks/with_.yml @@ -0,0 +1,34 @@ +--- +# https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#with-flattened +- hosts: localhost + tasks: + - ansible.builtin.debug: + msg: "{{ item }}" + with_list: [] # <-- valid + - ansible.builtin.debug: + msg: "{{ item }}" + with_items: [] # <-- valid + - ansible.builtin.debug: + msg: "{{ item }}" + with_indexed_items: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_together: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_dict: {} + - ansible.builtin.debug: + msg: "{{ item }}" + with_sequence: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_subelements: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_nested: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_random_choice: [] + - ansible.builtin.debug: + msg: "{{ item }}" + with_fileglob: [] diff --git a/test/schemas/test/reqs2/meta/requirements.yml b/test/schemas/test/reqs2/meta/requirements.yml new file mode 100644 index 0000000..8d55085 --- /dev/null +++ b/test/schemas/test/reqs2/meta/requirements.yml @@ -0,0 +1,7 @@ +# https://docs.ansible.com/ansible/latest/galaxy/user_guide.html +collections: + - doo.bar + - name: geerlingguy.php_roles + version: 0.9.3 + source: https://galaxy.ansible.com +roles: [] diff --git a/test/schemas/test/reqs4/meta/requirements.yml b/test/schemas/test/reqs4/meta/requirements.yml new file mode 100644 index 0000000..8269128 --- /dev/null +++ b/test/schemas/test/reqs4/meta/requirements.yml @@ -0,0 +1,6 @@ +# requirements v1 format +- src: https://github.com/bennojoy/nginx +- src: git+http://bitbucket.org/willthames/git-ansible-galaxy + version: v1.4 + scm: git +- include: foo.yml diff --git a/test/schemas/test/reqs5/meta/requirements.yml b/test/schemas/test/reqs5/meta/requirements.yml new file mode 100644 index 0000000..cd99e3c --- /dev/null +++ b/test/schemas/test/reqs5/meta/requirements.yml @@ -0,0 +1,3 @@ +# Collection without roles +collections: + - name: kubernetes.core diff --git a/test/schemas/test/roles/empty-meta/meta/main.yml b/test/schemas/test/roles/empty-meta/meta/main.yml new file mode 100644 index 0000000..9b6fe15 --- /dev/null +++ b/test/schemas/test/roles/empty-meta/meta/main.yml @@ -0,0 +1 @@ +# this is meta file without any data, ansible-core accepts it diff --git a/test/schemas/test/roles/foo/meta/argument_specs.yml b/test/schemas/test/roles/foo/meta/argument_specs.yml new file mode 100644 index 0000000..c8d8c68 --- /dev/null +++ b/test/schemas/test/roles/foo/meta/argument_specs.yml @@ -0,0 +1,74 @@ +--- +# https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#role-argument-validation +argument_specs: + main: + short_description: The main entry point for the role. + description: "a longer description" + version_added: 1.2.3 + author: Foobar Baz + options: + my_app_int: + type: "int" + required: false + default: 42 + description: "The integer value, defaulting to 42." + no_log: false + version_added: 1.0.0 + + my_app_str: + type: "str" + required: true + description: + - The string value. + - Has some more text. + choices: + - foo + - bar + - baz + + top_level: + type: dict + description: Contains more content. + options: + sub_option: + type: list + elements: int + description: A list of special integers. + choices: + - 1 + - 2 + - 3 + - 123 + + seealso: + - module: community.foo.bar + - module: community.foo.baz + description: Baz bam! + - plugin: community.foo.bam + plugin_type: lookup + - plugin: community.foo.bar + plugin_type: lookup + description: A lookup plugin. + - ref: developer_guide + description: A link into the Ansible documentation. + - link: https://docs.ansible.com/ + name: The Ansible documentation. + description: A link to the Ansible documentation. + + alternate: + short_description: The alternate entry point for the my_app role. + author: + - Foobar Baz + - Bert Foo + options: + my_app_int: + type: "int" + required: false + default: 1024 + description: "The integer value, defaulting to 1024." + + third: + description: + - First paragraph. + - Second paragraph. + options: {} diff --git a/test/schemas/test/roles/foo/meta/main.yml b/test/schemas/test/roles/foo/meta/main.yml new file mode 100644 index 0000000..b84b10c --- /dev/null +++ b/test/schemas/test/roles/foo/meta/main.yml @@ -0,0 +1,46 @@ +collections: + - foo.bar +dependencies: + - name: ansible-role-foo + version: "1.0" + - name: ansible-role-bar + version: "1.0" + # from Bitbucket + - src: git+http://bitbucket.org/willthames/git-ansible-galaxy + version: v1.4 + + # from Bitbucket, alternative syntax and caveats + - src: http://bitbucket.org/willthames/hg-ansible-galaxy + scm: hg + + # from galaxy + - src: community.molecule + + # from GitHub + - src: https://github.com/bennojoy/nginx + + # from GitHub, overriding the name and specifying a specific tag + - src: https://github.com/bennojoy/nginx + version: master + name: nginx_role + + # from GitLab or other git-based scm + - src: git@gitlab.company.com:my-group/my-repo.git + scm: git + version: "0.1" # quoted, so YAML doesn't parse this as a floating-point value + + # from a web server, where the role is packaged in a tar.gz + - src: https://some.webserver.example.com/files/master.tar.gz + name: http-role + +galaxy_info: + author: John Doe + company: foo + description: foo + license: MIT + min_ansible_version: "2.9" + # standalone: true + platforms: + - name: Alpine + versions: + - all diff --git a/test/schemas/test/roles/foo/meta/runtime.yml b/test/schemas/test/roles/foo/meta/runtime.yml new file mode 100644 index 0000000..561e446 --- /dev/null +++ b/test/schemas/test/roles/foo/meta/runtime.yml @@ -0,0 +1,39 @@ +# Based on https://docs.ansible.com/ansible/devel/dev_guide/developing_collections_structure.html#meta-directory +requires_ansible: ">=2.10,<2.11" +plugin_routing: + inventory: + kubevirt: + redirect: community.general.kubevirt + my_inventory: + tombstone: + removal_version: "2.0.0" + warning_text: my_inventory has been removed. Please use other_inventory instead. + modules: + my_module: + deprecation: + removal_date: "2021-11-30" + warning_text: + my_module will be removed in a future release of this collection. Use + another.collection.new_module instead. + redirect: another.collection.new_module + podman_image: + redirect: containers.podman.podman_image + module_utils: + ec2: + redirect: amazon.aws.ec2 + util_dir.subdir.my_util: + redirect: namespace.name.my_util +import_redirection: + ansible.module_utils.old_utility: + redirect: ansible_collections.namespace_name.collection_name.plugins.module_utils.new_location +action_groups: + groupname: + # The special metadata dictionary. All action/module names should be strings. + - metadata: + extend_group: + - another.collection.groupname + - another_group + - my_action + another_group: + - my_module + - another.collection.another_module diff --git a/test/schemas/test/roles/maximum/meta/main.yml b/test/schemas/test/roles/maximum/meta/main.yml new file mode 100644 index 0000000..10c57b1 --- /dev/null +++ b/test/schemas/test/roles/maximum/meta/main.yml @@ -0,0 +1,20 @@ +allow_duplicates: true +galaxy_info: + author: John Doe + standalone: true # v1 role meta (standalone) + description: maximum + min_ansible_version: "2.9" + company: foo + license: MIT + galaxy_tags: # ensure galaxy_tags is allowed + - database + platforms: + - name: Alpine + versions: + - all +dependencies: + - role: foo + vars: {} + when: + - foo + - bar diff --git a/test/schemas/test/roles/meta-tags/meta/main.yml b/test/schemas/test/roles/meta-tags/meta/main.yml new file mode 100644 index 0000000..4abba23 --- /dev/null +++ b/test/schemas/test/roles/meta-tags/meta/main.yml @@ -0,0 +1,25 @@ +--- +# https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-dependencies +dependencies: + - role: foo + tags: fruit # simple string allowed + - role: bar + tags: # array of strings allowed + - apple + - orange + - role: requires_sudo + become: true + - role: role_with_condition + when: inventory_hostname == "foo" + - role: another_role + # https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#passing-different-parameters + something_that_counts_as_role_parameter: ... + vars: + "foo": bar +galaxy_info: + author: John Doe + standalone: true + description: foo + license: MIT + min_ansible_version: "2.10" + platforms: [] diff --git a/test/schemas/test/roles/ns/meta/main.yml b/test/schemas/test/roles/ns/meta/main.yml new file mode 100644 index 0000000..0ea558c --- /dev/null +++ b/test/schemas/test/roles/ns/meta/main.yml @@ -0,0 +1,13 @@ +--- +galaxy_info: + author: John Doe + standalone: true + description: foo + min_ansible_version: "2.9" + namespace: foo_bar + company: foo + license: MIT + platforms: + - name: Alpine + versions: + - all diff --git a/test/schemas/test/roles/v1_role/meta/main.yml b/test/schemas/test/roles/v1_role/meta/main.yml new file mode 100644 index 0000000..a74eb47 --- /dev/null +++ b/test/schemas/test/roles/v1_role/meta/main.yml @@ -0,0 +1,12 @@ +--- +galaxy_info: + standalone: true + author: foo-bar # <-- that is a valid author name because is a valid github username + description: foo + min_ansible_version: "2.9" + company: foo + license: MIT + platforms: + - name: Alpine + versions: + - all diff --git a/test/schemas/test/tests/integration/rom_role/meta/main.yml b/test/schemas/test/tests/integration/rom_role/meta/main.yml new file mode 100644 index 0000000..c1409c4 --- /dev/null +++ b/test/schemas/test/tests/integration/rom_role/meta/main.yml @@ -0,0 +1,5 @@ +--- +dependencies: [] +galaxy_info: + standalone: false + description: foo diff --git a/test/schemas/tsconfig.json b/test/schemas/tsconfig.json new file mode 100644 index 0000000..fe51c68 --- /dev/null +++ b/test/schemas/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "lib": ["es5", "es2015.promise"], + "module": "commonjs", + "moduleResolution": "node", + "outDir": "../lib/umd", + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "stripInternal": true, + "target": "es5" + }, + "exclude": ["node_modules"], + "include": ["src/**/*"] +} diff --git a/test/test_ansiblelintrule.py b/test/test_ansiblelintrule.py new file mode 100644 index 0000000..c576e0f --- /dev/null +++ b/test/test_ansiblelintrule.py @@ -0,0 +1,31 @@ +"""Generic tests for AnsibleLintRule class.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest + +from ansiblelint.config import options +from ansiblelint.rules import AnsibleLintRule + +if TYPE_CHECKING: + from _pytest.monkeypatch import MonkeyPatch + + +def test_unjinja() -> None: + """Verify that unjinja understands nested mustache.""" + text = "{{ a }} {% b %} {# try to confuse parsing inside a comment { {{}} } #}" + output = "JINJA_EXPRESSION JINJA_STATEMENT JINJA_COMMENT" + assert AnsibleLintRule.unjinja(text) == output + + +@pytest.mark.parametrize("rule_config", ({}, {"foo": True, "bar": 1})) +def test_rule_config(rule_config: dict[str, Any], monkeypatch: MonkeyPatch) -> None: + """Check that a rule config is inherited from options.""" + rule_id = "rule-0" + monkeypatch.setattr(AnsibleLintRule, "id", rule_id) + monkeypatch.setitem(options.rules, rule_id, rule_config) + + rule = AnsibleLintRule() + assert set(rule.rule_config.items()) == set(rule_config.items()) + assert all(rule.get_config(k) == v for k, v in rule_config.items()) diff --git a/test/test_ansiblesyntax.py b/test/test_ansiblesyntax.py new file mode 100644 index 0000000..f71a525 --- /dev/null +++ b/test/test_ansiblesyntax.py @@ -0,0 +1,19 @@ +"""Test Ansible Syntax. + +This module contains tests that validate that linter does not produce errors +when encountering what counts as valid Ansible syntax. +""" +from ansiblelint.testing import RunFromText + +PB_WITH_NULL_TASKS = """\ +--- +- name: Fixture for test_null_tasks + hosts: all + tasks: +""" + + +def test_null_tasks(default_text_runner: RunFromText) -> None: + """Assure we do not fail when encountering null tasks.""" + results = default_text_runner.run_playbook(PB_WITH_NULL_TASKS) + assert not results diff --git a/test/test_app.py b/test/test_app.py new file mode 100644 index 0000000..140f5f6 --- /dev/null +++ b/test/test_app.py @@ -0,0 +1,30 @@ +"""Test for app module.""" +from pathlib import Path + +from ansiblelint.constants import RC +from ansiblelint.file_utils import Lintable +from ansiblelint.testing import run_ansible_lint + + +def test_generate_ignore(tmp_path: Path) -> None: + """Validate that --generate-ignore dumps expected ignore to the file.""" + lintable = Lintable(tmp_path / "vars.yaml") + lintable.content = "foo: bar\nfoo: baz\n" + lintable.write(force=True) + ignore_file = tmp_path / ".ansible-lint-ignore" + assert not ignore_file.exists() + result = run_ansible_lint(lintable.filename, "--generate-ignore", cwd=tmp_path) + assert result.returncode == 2 + + assert ignore_file.exists() + with ignore_file.open(encoding="utf-8") as f: + assert "vars.yaml yaml[key-duplicates]\n" in f.readlines() + # Run again and now we expect to succeed as we have an ignore file. + result = run_ansible_lint(lintable.filename, cwd=tmp_path) + assert result.returncode == 0 + + +def test_app_no_matches(tmp_path: Path) -> None: + """Validate that linter returns special exit code if no files are analyzed.""" + result = run_ansible_lint(cwd=tmp_path) + assert result.returncode == RC.NO_FILES_MATCHED diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..a37a43d --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,215 @@ +"""Test cli arguments and config.""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from ansiblelint import cli + +if TYPE_CHECKING: + from _pytest.monkeypatch import MonkeyPatch + + +@pytest.fixture(name="base_arguments") +def fixture_base_arguments() -> list[str]: + """Define reusable base arguments for tests in current module.""" + return ["../test/skiptasks.yml"] + + +@pytest.mark.parametrize( + ("args", "config_path"), + ( + pytest.param(["-p"], "test/fixtures/parseable.yml", id="1"), + pytest.param(["-q"], "test/fixtures/quiet.yml", id="2"), + pytest.param( + ["-r", "test/fixtures/rules/"], + "test/fixtures/rulesdir.yml", + id="3", + ), + pytest.param( + ["-R", "-r", "test/fixtures/rules/"], + "test/fixtures/rulesdir-defaults.yml", + id="4", + ), + pytest.param(["-s"], "test/fixtures/strict.yml", id="5"), + pytest.param(["-t", "skip_ansible_lint"], "test/fixtures/tags.yml", id="6"), + pytest.param(["-v"], "test/fixtures/verbosity.yml", id="7"), + pytest.param(["-x", "bad_tag"], "test/fixtures/skip-tags.yml", id="8"), + pytest.param(["--exclude", "../"], "test/fixtures/exclude-paths.yml", id="9"), + pytest.param(["--show-relpath"], "test/fixtures/show-abspath.yml", id="10"), + pytest.param([], "test/fixtures/show-relpath.yml", id="11"), + ), +) +def test_ensure_config_are_equal( + base_arguments: list[str], + args: list[str], + config_path: str, +) -> None: + """Check equality of the CLI options to config files.""" + command = base_arguments + args + cli_parser = cli.get_cli_parser() + + options = cli_parser.parse_args(command) + file_config = cli.load_config(config_path)[0] + for key, val in file_config.items(): + # config_file does not make sense in file_config + if key == "config_file": + continue + + if key == "rulesdir": + # this is list of Paths + val = [Path(p) for p in val] + assert val == getattr(options, key), f"Mismatch for {key}" + + +@pytest.mark.parametrize( + ("with_base", "args", "config"), + ( + (True, ["--write"], "test/fixtures/config-with-write-all.yml"), + (True, ["--write=all"], "test/fixtures/config-with-write-all.yml"), + (True, ["--write", "all"], "test/fixtures/config-with-write-all.yml"), + (True, ["--write=none"], "test/fixtures/config-with-write-none.yml"), + (True, ["--write", "none"], "test/fixtures/config-with-write-none.yml"), + ( + True, + ["--write=rule-tag,rule-id"], + "test/fixtures/config-with-write-subset.yml", + ), + ( + True, + ["--write", "rule-tag,rule-id"], + "test/fixtures/config-with-write-subset.yml", + ), + ( + True, + ["--write", "rule-tag", "--write", "rule-id"], + "test/fixtures/config-with-write-subset.yml", + ), + ( + False, + ["--write", "examples/playbooks/example.yml"], + "test/fixtures/config-with-write-all.yml", + ), + ( + False, + ["--write", "examples/playbooks/example.yml", "non-existent.yml"], + "test/fixtures/config-with-write-all.yml", + ), + ), +) +def test_ensure_write_cli_does_not_consume_lintables( + base_arguments: list[str], + with_base: bool, + args: list[str], + config: str, +) -> None: + """Check equality of the CLI --write options to config files.""" + cli_parser = cli.get_cli_parser() + + command = base_arguments + args if with_base else args + options = cli_parser.parse_args(command) + file_config = cli.load_config(config)[0] + + file_value = file_config.get("write_list") + orig_cli_value = options.write_list + cli_value = cli.WriteArgAction.merge_write_list_config( + from_file=[], + from_cli=orig_cli_value, + ) + assert file_value == cli_value + + +def test_config_can_be_overridden(base_arguments: list[str]) -> None: + """Check that config can be overridden from CLI.""" + no_override = cli.get_config([*base_arguments, "-t", "bad_tag"]) + + overridden = cli.get_config( + [*base_arguments, "-t", "bad_tag", "-c", "test/fixtures/tags.yml"], + ) + + assert [*no_override.tags, "skip_ansible_lint"] == overridden.tags + + +def test_different_config_file(base_arguments: list[str]) -> None: + """Ensures an alternate config_file can be used.""" + diff_config = cli.get_config( + [*base_arguments, "-c", "test/fixtures/ansible-config.yml"], + ) + no_config = cli.get_config([*base_arguments, "-v"]) + + assert diff_config.verbosity == no_config.verbosity + + +def test_expand_path_user_and_vars_config_file(base_arguments: list[str]) -> None: + """Ensure user and vars are expanded when specified as exclude_paths.""" + config1 = cli.get_config( + [*base_arguments, "-c", "test/fixtures/exclude-paths-with-expands.yml"], + ) + config2 = cli.get_config( + [ + *base_arguments, + "--exclude", + "~/.ansible/roles", + "--exclude", + "$HOME/.ansible/roles", + ], + ) + + assert str(config1.exclude_paths[0]) == os.path.expanduser( # noqa: PTH111 + "~/.ansible/roles", + ) + assert str(config1.exclude_paths[1]) == os.path.expandvars("$HOME/.ansible/roles") + + # exclude-paths coming in via cli are PosixPath objects; which hold the (canonical) real path (without symlinks) + assert str(config2.exclude_paths[0]) == os.path.realpath( + os.path.expanduser("~/.ansible/roles"), # noqa: PTH111 + ) + assert str(config2.exclude_paths[1]) == os.path.realpath( + os.path.expandvars("$HOME/.ansible/roles"), + ) + + +def test_path_from_config_do_not_depend_on_cwd( + monkeypatch: MonkeyPatch, +) -> None: # Issue 572 + """Check that config-provided paths are decoupled from CWD.""" + config1 = cli.load_config("test/fixtures/config-with-relative-path.yml")[0] + monkeypatch.chdir("test") + config2 = cli.load_config("fixtures/config-with-relative-path.yml")[0] + + assert config1["exclude_paths"].sort() == config2["exclude_paths"].sort() + + +@pytest.mark.parametrize( + "config_file", + ( + pytest.param("test/fixtures/ansible-config-invalid.yml", id="invalid"), + pytest.param("/dev/null/ansible-config-missing.yml", id="missing"), + ), +) +def test_config_failure(base_arguments: list[str], config_file: str) -> None: + """Ensures specific config files produce error code 3.""" + with pytest.raises(SystemExit, match="^3$"): + cli.get_config([*base_arguments, "-c", config_file]) + + +def test_extra_vars_loaded(base_arguments: list[str]) -> None: + """Ensure ``extra_vars`` option is loaded from file config.""" + config = cli.get_config( + [*base_arguments, "-c", "test/fixtures/config-with-extra-vars.yml"], + ) + + assert config.extra_vars == {"foo": "bar", "knights_favorite_word": "NI"} + + +@pytest.mark.parametrize( + "config_file", + (pytest.param("/dev/null", id="dev-null"),), +) +def test_config_dev_null(base_arguments: list[str], config_file: str) -> None: + """Ensures specific config files produce error code 3.""" + cfg = cli.get_config([*base_arguments, "-c", config_file]) + assert cfg.config_file == "/dev/null" diff --git a/test/test_cli_role_paths.py b/test/test_cli_role_paths.py new file mode 100644 index 0000000..148e1ed --- /dev/null +++ b/test/test_cli_role_paths.py @@ -0,0 +1,194 @@ +"""Tests related to role paths.""" +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from ansiblelint.constants import RC +from ansiblelint.testing import run_ansible_lint +from ansiblelint.text import strip_ansi_escape + + +@pytest.fixture(name="local_test_dir") +def fixture_local_test_dir() -> Path: + """Fixture to return local test directory.""" + return Path(__file__).resolve().parent.parent / "examples" + + +def test_run_single_role_path_no_trailing_slash_module(local_test_dir: Path) -> None: + """Test that a role path without a trailing slash is accepted.""" + cwd = local_test_dir + role_path = "roles/test-role" + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_single_role_path_no_trailing_slash_script(local_test_dir: Path) -> None: + """Test that a role path without a trailing slash is accepted.""" + cwd = local_test_dir + role_path = "roles/test-role" + + result = run_ansible_lint(role_path, cwd=cwd, executable="ansible-lint") + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_single_role_path_with_trailing_slash(local_test_dir: Path) -> None: + """Test that a role path with a trailing slash is accepted.""" + cwd = local_test_dir + role_path = "roles/test-role/" + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_multiple_role_path_no_trailing_slash(local_test_dir: Path) -> None: + """Test that multiple roles paths without a trailing slash are accepted.""" + cwd = local_test_dir + role_path = "roles/test-role" + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_multiple_role_path_with_trailing_slash(local_test_dir: Path) -> None: + """Test that multiple roles paths without a trailing slash are accepted.""" + cwd = local_test_dir + role_path = "roles/test-role/" + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_inside_role_dir(local_test_dir: Path) -> None: + """Tests execution from inside a role.""" + cwd = local_test_dir / "roles" / "test-role" + role_path = "." + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_role_three_dir_deep(local_test_dir: Path) -> None: + """Tests execution from deep inside a role.""" + cwd = local_test_dir + role_path = "testproject/roles/test-role" + + result = run_ansible_lint(role_path, cwd=cwd) + assert "Use shell only when shell functionality is required" in result.stdout + + +def test_run_playbook(local_test_dir: Path) -> None: + """Call ansible-lint the way molecule does.""" + cwd = local_test_dir / "roles" / "test-role" + lintable = "molecule/default/include-import-role.yml" + role_path = str(Path(cwd).parent.resolve()) + + env = os.environ.copy() + env["ANSIBLE_ROLES_PATH"] = role_path + env["NO_COLOR"] = "1" + + result = run_ansible_lint("-f", "pep8", lintable, cwd=cwd, env=env) + # All 4 failures are expected to be found inside the included role and not + # from the playbook given as argument. + assert result.returncode == RC.VIOLATIONS_FOUND + assert "tasks/main.yml:2: command-instead-of-shell" in result.stdout + assert "tasks/world.yml:2: name[missing]" in result.stdout + + +@pytest.mark.parametrize( + ("args", "expected_msg"), + ( + pytest.param( + [], + "role-name: Role name invalid-name does not match", + id="normal", + ), + pytest.param(["--skip-list", "role-name"], "", id="skipped"), + ), +) +def test_run_role_name_invalid( + local_test_dir: Path, + args: list[str], + expected_msg: str, +) -> None: + """Test run with a role with invalid name.""" + cwd = local_test_dir + role_path = "roles/invalid-name" + + result = run_ansible_lint(*args, role_path, cwd=cwd) + assert result.returncode == (2 if expected_msg else 0), result + if expected_msg: + assert expected_msg in strip_ansi_escape(result.stdout) + + +def test_run_role_name_with_prefix(local_test_dir: Path) -> None: + """Test run where role path has a prefix.""" + cwd = local_test_dir + role_path = "roles/ansible-role-foo" + + result = run_ansible_lint("-v", role_path, cwd=cwd) + assert len(result.stdout) == 0 + assert result.returncode == 0 + + +def test_run_role_name_from_meta(local_test_dir: Path) -> None: + """Test running from inside meta folder.""" + cwd = local_test_dir + role_path = "roles/valid-due-to-meta" + + result = run_ansible_lint("-v", role_path, cwd=cwd) + assert len(result.stdout) == 0 + assert result.returncode == 0 + + +def test_run_invalid_role_name_from_meta(local_test_dir: Path) -> None: + """Test invalid role from inside meta folder.""" + cwd = local_test_dir + role_path = "roles/invalid_due_to_meta" + + result = run_ansible_lint(role_path, cwd=cwd) + assert ( + "role-name: Role name invalid-due-to-meta does not match" + in strip_ansi_escape(result.stdout) + ) + + +def test_run_single_role_path_with_roles_path_env(local_test_dir: Path) -> None: + """Test for role name collision with ANSIBLE_ROLES_PATH. + + Test if ansible-lint chooses the role in the current directory when the role + specified as parameter exists in the current directory and the ANSIBLE_ROLES_PATH. + """ + cwd = local_test_dir + role_path = "roles/test-role" + + env = os.environ.copy() + env["ANSIBLE_ROLES_PATH"] = os.path.realpath((cwd / "../examples/roles").resolve()) + + result = run_ansible_lint(role_path, cwd=cwd, env=env) + assert "Use shell only when shell functionality is required" in result.stdout + + +@pytest.mark.parametrize( + ("result", "env"), + ((True, {"GITHUB_ACTIONS": "true", "GITHUB_WORKFLOW": "foo"}), (False, None)), + ids=("on", "off"), +) +def test_run_playbook_github(result: bool, env: dict[str, str]) -> None: + """Call ansible-lint simulating GitHub Actions environment.""" + cwd = Path(__file__).parent.parent.resolve() + role_path = "examples/playbooks/example.yml" + + if env is None: + env = {} + env["PATH"] = os.environ["PATH"] + result_gh = run_ansible_lint(role_path, cwd=cwd, env=env) + + expected = ( + "::error file=examples/playbooks/example.yml,line=44,severity=VERY_LOW,title=package-latest::" + "Package installs should not use latest" + ) + assert (expected in result_gh.stderr) is result diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..51a09b0 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,16 @@ +"""Tests for config module.""" +from ansiblelint.config import PROFILES +from ansiblelint.rules import RulesCollection + + +def test_profiles(default_rules_collection: RulesCollection) -> None: + """Test the rules included in profiles are valid.""" + profile_banned_tags = {"opt-in", "experimental"} + for name, data in PROFILES.items(): + for profile_rule_id in data["rules"]: + for rule in default_rules_collection.rules: + if profile_rule_id == rule.id: + forbidden_tags = profile_banned_tags & set(rule.tags) + assert ( + not forbidden_tags + ), f"Rule {profile_rule_id} from {name} profile cannot use {profile_banned_tags & set(rule.tags)} tag." diff --git a/test/test_constants.py b/test/test_constants.py new file mode 100644 index 0000000..52b297a --- /dev/null +++ b/test/test_constants.py @@ -0,0 +1,9 @@ +"""Tests for constants module.""" +from ansiblelint.constants import States + + +def test_states() -> None: + """Test that states are evaluated as boolean false.""" + assert bool(States.NOT_LOADED) is False + assert bool(States.LOAD_FAILED) is False + assert bool(States.UNKNOWN_DATA) is False diff --git a/test/test_dependencies_in_meta.py b/test/test_dependencies_in_meta.py new file mode 100644 index 0000000..44007b7 --- /dev/null +++ b/test/test_dependencies_in_meta.py @@ -0,0 +1,10 @@ +"""Tests about dependencies in meta.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_external_dependency_is_ok(default_rules_collection: RulesCollection) -> None: + """Check that external dep in role meta is not a violation.""" + playbook_path = "examples/roles/dependency_in_meta/meta/main.yml" + good_runner = Runner(playbook_path, rules=default_rules_collection) + assert [] == good_runner.run() diff --git a/test/test_examples.py b/test/test_examples.py new file mode 100644 index 0000000..2842930 --- /dev/null +++ b/test/test_examples.py @@ -0,0 +1,102 @@ +"""Assure samples produced desire outcomes.""" +import pytest + +from ansiblelint.app import get_app +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner +from ansiblelint.testing import run_ansible_lint + + +def test_example(default_rules_collection: RulesCollection) -> None: + """example.yml is expected to have exact number of errors inside.""" + result = Runner( + "examples/playbooks/example.yml", + rules=default_rules_collection, + ).run() + assert len(result) == 22 + + +@pytest.mark.parametrize( + ("filename", "line", "column"), + ( + pytest.param( + "examples/playbooks/syntax-error-string.yml", + 6, + 7, + id="syntax-error", + ), + pytest.param("examples/playbooks/syntax-error.yml", 2, 3, id="syntax-error"), + ), +) +def test_example_syntax_error( + default_rules_collection: RulesCollection, + filename: str, + line: int, + column: int, +) -> None: + """Validates that loading valid YAML string produce error.""" + result = Runner(filename, rules=default_rules_collection).run() + assert len(result) == 1 + assert result[0].rule.id == "syntax-check" + # This also ensures that line and column numbers start at 1, so they + # match what editors will show (or output from other linters) + assert result[0].lineno == line + assert result[0].column == column + + +def test_example_custom_module(default_rules_collection: RulesCollection) -> None: + """custom_module.yml is expected to pass.""" + app = get_app(offline=True) + result = Runner( + "examples/playbooks/custom_module.yml", + rules=default_rules_collection, + ).run() + assert len(result) == 0, f"{app.runtime.cache_dir}" + + +def test_vault_full(default_rules_collection: RulesCollection) -> None: + """Check ability to process fully vaulted files.""" + result = Runner( + "examples/playbooks/vars/vault_full.yml", + rules=default_rules_collection, + ).run() + assert len(result) == 0 + + +def test_vault_partial( + default_rules_collection: RulesCollection, + caplog: pytest.LogCaptureFixture, +) -> None: + """Check ability to precess files that container !vault inside.""" + result = Runner( + "examples/playbooks/vars/vault_partial.yml", + rules=default_rules_collection, + ).run() + assert len(result) == 0 + # Ensure that we do not have side-effect extra logging even if the vault + # content cannot be decrypted. + assert caplog.record_tuples == [] + + +def test_custom_kinds() -> None: + """Check if user defined kinds are used.""" + result = run_ansible_lint("-vv", "--offline", "examples/other/") + assert result.returncode == 0 + # .yaml-too is not a recognized extension and unless is manually defined + # in our ansible-lint config, the test would not identify it as yaml file. + assert "Examining examples/other/some.yaml-too of type yaml" in result.stderr + assert "Examining examples/other/some.j2.yaml of type jinja2" in result.stderr + + +def test_bug_3216(capsys: pytest.CaptureFixture[str]) -> None: + """Check that we hide ansible-core originating warning about fallback on unique filter.""" + result = run_ansible_lint( + "-vv", + "--offline", + "examples/playbooks/bug-core-warning-unique-filter-fallback.yml", + ) + captured = capsys.readouterr() + assert result.returncode == 0 + warn_msg = "Falling back to Ansible unique filter" + assert warn_msg not in captured.err + assert warn_msg not in captured.out diff --git a/test/test_file_path_evaluation.py b/test/test_file_path_evaluation.py new file mode 100644 index 0000000..b31f923 --- /dev/null +++ b/test/test_file_path_evaluation.py @@ -0,0 +1,130 @@ +"""Testing file path evaluation when using import_tasks / include_tasks.""" +from __future__ import annotations + +import textwrap +from typing import TYPE_CHECKING + +import pytest + +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from pathlib import Path + + from ansiblelint.rules import RulesCollection + +LAYOUT_IMPORTS: dict[str, str] = { + "main.yml": textwrap.dedent( + """\ + --- + - name: Fixture + hosts: target + gather_facts: false + tasks: + - name: From main import task 1 + ansible.builtin.import_tasks: tasks/task_1.yml + """, + ), + "tasks/task_1.yml": textwrap.dedent( + """\ + --- + - name: task_1 | From task 1 import task 2 + ansible.builtin.import_tasks: tasks/task_2.yml + """, + ), + "tasks/task_2.yml": textwrap.dedent( + """\ + --- + - name: task_2 | From task 2 import subtask 1 + ansible.builtin.import_tasks: tasks/subtasks/subtask_1.yml + """, + ), + "tasks/subtasks/subtask_1.yml": textwrap.dedent( + """\ + --- + - name: subtask_1 | From subtask 1 import subtask 2 + ansible.builtin.import_tasks: tasks/subtasks/subtask_2.yml + """, + ), + "tasks/subtasks/subtask_2.yml": textwrap.dedent( + """\ + --- + - name: subtask_2 | From subtask 2 do something + debug: # <-- expected to raise fqcn[action-core] + msg: | + Something... + """, + ), +} + +LAYOUT_INCLUDES: dict[str, str] = { + "main.yml": textwrap.dedent( + """\ + --- + - name: Fixture + hosts: target + gather_facts: false + tasks: + - name: From main import task 1 + ansible.builtin.include_tasks: tasks/task_1.yml + """, + ), + "tasks/task_1.yml": textwrap.dedent( + """\ + --- + - name: task_1 | From task 1 import task 2 + ansible.builtin.include_tasks: tasks/task_2.yml + """, + ), + "tasks/task_2.yml": textwrap.dedent( + """\ + --- + - name: task_2 | From task 2 import subtask 1 + ansible.builtin.include_tasks: tasks/subtasks/subtask_1.yml + """, + ), + "tasks/subtasks/subtask_1.yml": textwrap.dedent( + """\ + --- + - name: subtask_1 | From subtask 1 import subtask 2 + ansible.builtin.include_tasks: tasks/subtasks/subtask_2.yml + """, + ), + "tasks/subtasks/subtask_2.yml": textwrap.dedent( + """\ + --- + - name: subtask_2 | From subtask 2 do something + debug: # <-- expected to raise fqcn[action-core] + msg: | + Something... + """, + ), +} + + +@pytest.mark.parametrize( + "ansible_project_layout", + ( + pytest.param(LAYOUT_IMPORTS, id="using only import_tasks"), + pytest.param(LAYOUT_INCLUDES, id="using only include_tasks"), + ), +) +def test_file_path_evaluation( + tmp_path: Path, + default_rules_collection: RulesCollection, + ansible_project_layout: dict[str, str], +) -> None: + """Test file path evaluation when using import_tasks / include_tasks in the project. + + The goal of this test is to verify our ability to find errors from within + nested includes. + """ + for file_path, file_content in ansible_project_layout.items(): + full_path = tmp_path / file_path + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(file_content) + + result = Runner(str(tmp_path), rules=default_rules_collection).run() + + assert len(result) == 1 + assert result[0].rule.id == "fqcn" diff --git a/test/test_file_utils.py b/test/test_file_utils.py new file mode 100644 index 0000000..b7b9115 --- /dev/null +++ b/test/test_file_utils.py @@ -0,0 +1,538 @@ +"""Tests for file utility functions.""" +from __future__ import annotations + +import copy +import logging +import os +import time +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +from ansiblelint import cli, file_utils +from ansiblelint.file_utils import ( + Lintable, + cwd, + expand_path_vars, + expand_paths_vars, + find_project_root, + normpath, + normpath_path, +) +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from _pytest.capture import CaptureFixture + from _pytest.logging import LogCaptureFixture + from _pytest.monkeypatch import MonkeyPatch + + from ansiblelint.constants import FileType + from ansiblelint.rules import RulesCollection + + +@pytest.mark.parametrize( + ("path", "expected"), + ( + pytest.param(Path("a/b/../"), "a", id="pathlib.Path"), + pytest.param("a/b/../", "a", id="str"), + pytest.param("", ".", id="empty"), + pytest.param(".", ".", id="empty"), + ), +) +def test_normpath(path: str, expected: str) -> None: + """Ensure that relative parent dirs are normalized in paths.""" + assert normpath(path) == expected + + +def test_expand_path_vars(monkeypatch: MonkeyPatch) -> None: + """Ensure that tilde and env vars are expanded in paths.""" + test_path = "/test/path" + monkeypatch.setenv("TEST_PATH", test_path) + assert expand_path_vars("~") == os.path.expanduser("~") # noqa: PTH111 + assert expand_path_vars("$TEST_PATH") == test_path + + +@pytest.mark.parametrize( + ("test_path", "expected"), + ( + pytest.param(Path("$TEST_PATH"), "/test/path", id="pathlib.Path"), + pytest.param("$TEST_PATH", "/test/path", id="str"), + pytest.param(" $TEST_PATH ", "/test/path", id="stripped-str"), + pytest.param("~", os.path.expanduser("~"), id="home"), # noqa: PTH:111 + ), +) +def test_expand_paths_vars( + test_path: str | Path, + expected: str, + monkeypatch: MonkeyPatch, +) -> None: + """Ensure that tilde and env vars are expanded in paths lists.""" + monkeypatch.setenv("TEST_PATH", "/test/path") + assert expand_paths_vars([test_path]) == [expected] # type: ignore[list-item] + + +def test_discover_lintables_silent( + monkeypatch: MonkeyPatch, + capsys: CaptureFixture[str], + caplog: LogCaptureFixture, +) -> None: + """Verify that no stderr output is displayed while discovering yaml files. + + (when the verbosity is off, regardless of the Git or Git-repo presence) + + Also checks expected number of files are detected. + """ + caplog.set_level(logging.FATAL) + options = cli.get_config([]) + test_dir = Path(__file__).resolve().parent + lint_path = (test_dir / ".." / "examples" / "roles" / "test-role").resolve() + + yaml_count = len(list(lint_path.glob("**/*.yml"))) + len( + list(lint_path.glob("**/*.yaml")), + ) + + monkeypatch.chdir(str(lint_path)) + my_options = copy.deepcopy(options) + my_options.lintables = [str(lint_path)] + files = file_utils.discover_lintables(my_options) + stderr = capsys.readouterr().err + assert ( + not stderr + ), f"No stderr output is expected when the verbosity is off, got: {stderr}" + assert ( + len(files) == yaml_count + ), "Expected to find {yaml_count} yaml files in {lint_path}".format_map( + locals(), + ) + + +def test_discover_lintables_umlaut(monkeypatch: MonkeyPatch) -> None: + """Verify that filenames containing German umlauts are not garbled by the discover_lintables.""" + options = cli.get_config([]) + test_dir = Path(__file__).resolve().parent + lint_path = (test_dir / ".." / "examples" / "playbooks").resolve() + + monkeypatch.chdir(str(lint_path)) + files = file_utils.discover_lintables(options) + assert '"with-umlaut-\\303\\244.yml"' not in files + assert "with-umlaut-ä.yml" in files + + +@pytest.mark.parametrize( + ("path", "kind"), + ( + pytest.param("tasks/run_test_playbook.yml", "tasks", id="0"), + pytest.param("foo/playbook.yml", "playbook", id="1"), + pytest.param("playbooks/foo.yml", "playbook", id="2"), + pytest.param("examples/roles/foo.yml", "yaml", id="3"), + # the only yml file that is not a playbook inside molecule/ folders + pytest.param( + "examples/.config/molecule/config.yml", + "yaml", + id="4", + ), # molecule shared config + pytest.param( + "test/schemas/test/molecule/cluster/base.yml", + "yaml", + id="5", + ), # molecule scenario base config + pytest.param( + "test/schemas/test/molecule/cluster/molecule.yml", + "yaml", + id="6", + ), # molecule scenario config + pytest.param( + "test/schemas/test/molecule/cluster/foobar.yml", + "playbook", + id="7", + ), # custom playbook name + pytest.param( + "test/schemas/test/molecule/cluster/converge.yml", + "playbook", + id="8", + ), # common playbook name + pytest.param( + "roles/foo/molecule/scenario3/requirements.yml", + "requirements", + id="9", + ), # requirements + pytest.param( + "roles/foo/molecule/scenario3/collections.yml", + "requirements", + id="10", + ), # requirements + pytest.param( + "roles/foo/meta/argument_specs.yml", + "role-arg-spec", + id="11", + ), # role argument specs + # tasks files: + pytest.param("tasks/directory with spaces/main.yml", "tasks", id="12"), # tasks + pytest.param("tasks/requirements.yml", "tasks", id="13"), # tasks + # requirements (we do not support includes yet) + pytest.param( + "requirements.yml", + "requirements", + id="14", + ), # collection requirements + pytest.param( + "roles/foo/meta/requirements.yml", + "requirements", + id="15", + ), # inside role requirements + # Undeterminable files: + pytest.param("test/fixtures/unknown-type.yml", "yaml", id="16"), + pytest.param( + "releasenotes/notes/run-playbooks-refactor.yaml", + "reno", + id="17", + ), # reno + pytest.param("examples/host_vars/localhost.yml", "vars", id="18"), + pytest.param("examples/group_vars/all.yml", "vars", id="19"), + pytest.param("examples/playbooks/vars/other.yml", "vars", id="20"), + pytest.param( + "examples/playbooks/vars/subfolder/settings.yml", + "vars", + id="21", + ), # deep vars + pytest.param( + "molecule/scenario/collections.yml", + "requirements", + id="22", + ), # deprecated 2.8 format + pytest.param( + "../roles/geerlingguy.mysql/tasks/configure.yml", + "tasks", + id="23", + ), # relative path involved + pytest.param("galaxy.yml", "galaxy", id="24"), + pytest.param("foo.j2.yml", "jinja2", id="25"), + pytest.param("foo.yml.j2", "jinja2", id="26"), + pytest.param("foo.j2.yaml", "jinja2", id="27"), + pytest.param("foo.yaml.j2", "jinja2", id="28"), + pytest.param( + "examples/playbooks/rulebook.yml", + "playbook", + id="29", + ), # playbooks folder should determine kind + pytest.param( + "examples/rulebooks/rulebook-pass.yml", + "rulebook", + id="30", + ), # content should determine it as a rulebook + pytest.param( + "examples/yamllint/valid.yml", + "yaml", + id="31", + ), # empty yaml is valid yaml, not assuming anything else + pytest.param( + "examples/other/guess-1.yml", + "playbook", + id="32", + ), # content should determine is as a play + pytest.param( + "examples/playbooks/tasks/passing_task.yml", + "tasks", + id="33", + ), # content should determine is tasks + pytest.param("examples/collection/galaxy.yml", "galaxy", id="34"), + pytest.param("examples/meta/runtime.yml", "meta-runtime", id="35"), + pytest.param("examples/meta/changelogs/changelog.yaml", "changelog", id="36"), + pytest.param("examples/inventory/inventory.yml", "inventory", id="37"), + pytest.param("examples/inventory/production.yml", "inventory", id="38"), + pytest.param("examples/playbooks/vars/empty_vars.yml", "vars", id="39"), + pytest.param( + "examples/playbooks/vars/subfolder/settings.yaml", + "vars", + id="40", + ), + pytest.param( + "examples/sanity_ignores/tests/sanity/ignore-2.14.txt", + "sanity-ignore-file", + id="41", + ), + pytest.param("examples/playbooks/tasks/vars/bug-3289.yml", "vars", id="42"), + pytest.param( + "examples/site.yml", + "playbook", + id="43", + ), # content should determine it as a play + ), +) +def test_kinds(path: str, kind: FileType) -> None: + """Verify auto-detection logic based on DEFAULT_KINDS.""" + # assert Lintable is able to determine file type + lintable_detected = Lintable(path) + lintable_expected = Lintable(path, kind=kind) + assert lintable_detected == lintable_expected + + +def test_find_project_root_1(tmp_path: Path) -> None: + """Verify find_project_root().""" + # this matches black behavior in absence of any config files or .git/.hg folders. + with cwd(tmp_path): + path, method = find_project_root([]) + assert str(path) == "/" + assert method == "file system root" + + +def test_find_project_root_dotconfig() -> None: + """Verify find_project_root().""" + # this expects to return examples folder as project root because this + # folder already has an .config/ansible-lint.yml file inside, which should + # be enough. + with cwd(Path("examples")): + assert Path( + ".config/ansible-lint.yml", + ).exists(), "Test requires config file inside .config folder." + path, method = find_project_root([]) + assert str(path) == str(Path.cwd()) + assert ".config/ansible-lint.yml" in method + + +BASIC_PLAYBOOK = """ +- name: "playbook" + tasks: + - name: Hello + debug: + msg: 'world' +""" + + +@pytest.fixture(name="tmp_updated_lintable") +def fixture_tmp_updated_lintable( + tmp_path: Path, + path: str, + content: str, + updated_content: str, +) -> Lintable: + """Create a temp file Lintable with a content update that is not on disk.""" + lintable = Lintable(tmp_path / path, content) + with lintable.path.open("w", encoding="utf-8") as f: + f.write(content) + # move mtime to a time in the past to avoid race conditions in the test + mtime = time.time() - 60 * 60 # 1hr ago + os.utime(str(lintable.path), (mtime, mtime)) + lintable.content = updated_content + return lintable + + +@pytest.mark.parametrize( + ("path", "content", "updated_content", "updated"), + ( + pytest.param( + "no_change.yaml", + BASIC_PLAYBOOK, + BASIC_PLAYBOOK, + False, + id="no_change", + ), + pytest.param( + "quotes.yaml", + BASIC_PLAYBOOK, + BASIC_PLAYBOOK.replace('"', "'"), + True, + id="updated_quotes", + ), + pytest.param( + "shorten.yaml", + BASIC_PLAYBOOK, + "# short file\n", + True, + id="shorten_file", + ), + ), +) +def test_lintable_updated( + path: str, + content: str, + updated_content: str, + updated: bool, +) -> None: + """Validate ``Lintable.updated`` when setting ``Lintable.content``.""" + lintable = Lintable(path, content) + + assert lintable.content == content + + lintable.content = updated_content + + assert lintable.content == updated_content + + assert lintable.updated is updated + + +@pytest.mark.parametrize( + "updated_content", + ((None,), (b"bytes",)), + ids=("none", "bytes"), +) +def test_lintable_content_setter_with_bad_types(updated_content: Any) -> None: + """Validate ``Lintable.updated`` when setting ``Lintable.content``.""" + lintable = Lintable("bad_type.yaml", BASIC_PLAYBOOK) + assert lintable.content == BASIC_PLAYBOOK + + with pytest.raises(TypeError): + lintable.content = updated_content + + assert not lintable.updated + + +def test_lintable_with_new_file(tmp_path: Path) -> None: + """Validate ``Lintable.updated`` for a new file.""" + lintable = Lintable(tmp_path / "new.yaml") + + lintable.content = BASIC_PLAYBOOK + lintable.content = BASIC_PLAYBOOK + assert lintable.content == BASIC_PLAYBOOK + + assert lintable.updated + + assert not lintable.path.exists() + lintable.write() + assert lintable.path.exists() + assert lintable.path.read_text(encoding="utf-8") == BASIC_PLAYBOOK + + +@pytest.mark.parametrize( + ("path", "force", "content", "updated_content", "updated"), + ( + pytest.param( + "no_change.yaml", + False, + BASIC_PLAYBOOK, + BASIC_PLAYBOOK, + False, + id="no_change", + ), + pytest.param( + "forced.yaml", + True, + BASIC_PLAYBOOK, + BASIC_PLAYBOOK, + False, + id="forced_rewrite", + ), + pytest.param( + "quotes.yaml", + False, + BASIC_PLAYBOOK, + BASIC_PLAYBOOK.replace('"', "'"), + True, + id="updated_quotes", + ), + pytest.param( + "shorten.yaml", + False, + BASIC_PLAYBOOK, + "# short file\n", + True, + id="shorten_file", + ), + pytest.param( + "forced.yaml", + True, + BASIC_PLAYBOOK, + BASIC_PLAYBOOK.replace('"', "'"), + True, + id="forced_and_updated", + ), + ), +) +def test_lintable_write( + tmp_updated_lintable: Lintable, + force: bool, + content: str, + updated_content: str, + updated: bool, +) -> None: + """Validate ``Lintable.write`` writes when it should.""" + pre_updated = tmp_updated_lintable.updated + pre_stat = tmp_updated_lintable.path.stat() + + tmp_updated_lintable.write(force=force) + + post_stat = tmp_updated_lintable.path.stat() + post_updated = tmp_updated_lintable.updated + + # write() should not hide that an update happened + assert pre_updated == post_updated == updated + + if force or updated: + assert pre_stat.st_mtime < post_stat.st_mtime + else: + assert pre_stat.st_mtime == post_stat.st_mtime + + with tmp_updated_lintable.path.open("r", encoding="utf-8") as f: + post_content = f.read() + + if updated: + assert content != post_content + else: + assert content == post_content + assert post_content == updated_content + + +@pytest.mark.parametrize( + ("path", "content", "updated_content"), + ( + pytest.param( + "quotes.yaml", + BASIC_PLAYBOOK, + BASIC_PLAYBOOK.replace('"', "'"), + id="updated_quotes", + ), + ), +) +def test_lintable_content_deleter( + tmp_updated_lintable: Lintable, + content: str, + updated_content: str, +) -> None: + """Ensure that resetting content cache triggers re-reading file.""" + assert content != updated_content + assert tmp_updated_lintable.content == updated_content + del tmp_updated_lintable.content + assert tmp_updated_lintable.content == content + + +@pytest.mark.parametrize( + ("path", "result"), + ( + pytest.param("foo", "foo", id="rel"), + pytest.param( + os.path.expanduser("~/xxx"), # noqa: PTH111 + "~/xxx", + id="rel-to-home", + ), + pytest.param("/a/b/c", "/a/b/c", id="absolute"), + pytest.param( + "examples/playbooks/roles", + "examples/roles", + id="resolve-symlink", + ), + ), +) +def test_normpath_path(path: str, result: str) -> None: + """Tests behavior of normpath.""" + assert normpath_path(path) == Path(result) + + +def test_bug_2513( + tmp_path: Path, + default_rules_collection: RulesCollection, +) -> None: + """Regression test for bug 2513. + + Test that when CWD is outside ~, and argument is like ~/playbook.yml + we will still be able to process the files. + See: https://github.com/ansible/ansible-lint/issues/2513 + """ + filename = Path("~/.cache/ansible-lint/playbook.yml").expanduser() + filename.parent.mkdir(parents=True, exist_ok=True) + lintable = Lintable(filename, content="---\n- hosts: all\n") + lintable.write(force=True) + with cwd(tmp_path): + results = Runner(filename, rules=default_rules_collection).run() + assert len(results) == 1 + assert results[0].rule.id == "name" diff --git a/test/test_formatter.py b/test/test_formatter.py new file mode 100644 index 0000000..68f0508 --- /dev/null +++ b/test/test_formatter.py @@ -0,0 +1,68 @@ +"""Test for output formatter.""" +# Copyright (c) 2016 Will Thames <will@thames.id.au> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +import pathlib + +from ansiblelint.errors import MatchError +from ansiblelint.file_utils import Lintable +from ansiblelint.formatters import Formatter +from ansiblelint.rules import AnsibleLintRule + +rule = AnsibleLintRule() +rule.id = "TCF0001" +formatter = Formatter(pathlib.Path.cwd(), display_relative_path=True) +# These details would generate a rich rendering error if not escaped: +DETAILS = "Some [/tmp/foo] details." + + +def test_format_coloured_string() -> None: + """Test formetting colored.""" + match = MatchError( + message="message", + lineno=1, + details=DETAILS, + lintable=Lintable("filename.yml", content=""), + rule=rule, + ) + formatter.apply(match) + + +def test_unicode_format_string() -> None: + """Test formatting unicode.""" + match = MatchError( + message="\U0001f427", + lineno=1, + details=DETAILS, + lintable=Lintable("filename.yml", content=""), + rule=rule, + ) + formatter.apply(match) + + +def test_dict_format_line() -> None: + """Test formatting dictionary details.""" + match = MatchError( + message="xyz", + lineno=1, + details={"hello": "world"}, # type: ignore[arg-type] + lintable=Lintable("filename.yml", content=""), + rule=rule, + ) + formatter.apply(match) diff --git a/test/test_formatter_base.py b/test/test_formatter_base.py new file mode 100644 index 0000000..5cc86b8 --- /dev/null +++ b/test/test_formatter_base.py @@ -0,0 +1,74 @@ +"""Tests related to base formatter.""" +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest + +from ansiblelint.formatters import BaseFormatter + + +@pytest.mark.parametrize( + ("base_dir", "relative_path"), + ( + (None, True), + ("/whatever", False), + (Path("/whatever"), False), + ), +) +@pytest.mark.parametrize("path", ("/whatever/string", Path("/whatever/string"))) +def test_base_formatter_when_base_dir( + base_dir: Any, + relative_path: bool, + path: str, +) -> None: + """Check that base formatter accepts relative pathlib and str.""" + # Given + base_formatter = BaseFormatter(base_dir, relative_path) # type: ignore[var-annotated] + + # When + output_path = ( + base_formatter._format_path( # pylint: disable=protected-access # noqa: SLF001 + path, + ) + ) + + # Then + assert isinstance(output_path, (str, Path)) + # pylint: disable=protected-access + assert base_formatter.base_dir is None or isinstance( + base_formatter.base_dir, + (str, Path), + ) + assert output_path == path + + +@pytest.mark.parametrize( + "base_dir", + ( + Path("/whatever"), + "/whatever", + ), +) +@pytest.mark.parametrize("path", ("/whatever/string", Path("/whatever/string"))) +def test_base_formatter_when_base_dir_is_given_and_relative_is_true( + path: str | Path, + base_dir: str | Path, +) -> None: + """Check that the base formatter equally accepts pathlib and str.""" + # Given + base_formatter = BaseFormatter(base_dir, True) # type: ignore[var-annotated] + + # When + # pylint: disable=protected-access + output_path = base_formatter._format_path(path) # noqa: SLF001 + + # Then + assert isinstance(output_path, (str, Path)) + # pylint: disable=protected-access + assert isinstance(base_formatter.base_dir, (str, Path)) + assert output_path == Path(path).name + + +# vim: et:sw=4:syntax=python:ts=4: diff --git a/test/test_formatter_json.py b/test/test_formatter_json.py new file mode 100644 index 0000000..25aa5f5 --- /dev/null +++ b/test/test_formatter_json.py @@ -0,0 +1,138 @@ +"""Test the codeclimate JSON formatter.""" +from __future__ import annotations + +import json +import pathlib +import subprocess +import sys + +import pytest + +from ansiblelint.errors import MatchError +from ansiblelint.file_utils import Lintable +from ansiblelint.formatters import CodeclimateJSONFormatter +from ansiblelint.rules import AnsibleLintRule + + +class TestCodeclimateJSONFormatter: + """Unit test for CodeclimateJSONFormatter.""" + + rule = AnsibleLintRule() + matches: list[MatchError] = [] + formatter: CodeclimateJSONFormatter | None = None + + def setup_class(self) -> None: + """Set up few MatchError objects.""" + self.rule = AnsibleLintRule() + self.rule.id = "TCF0001" + self.rule.severity = "VERY_HIGH" + self.matches = [] + self.matches.append( + MatchError( + message="message", + lineno=1, + details="hello", + lintable=Lintable("filename.yml", content=""), + rule=self.rule, + ), + ) + self.matches.append( + MatchError( + message="message", + lineno=2, + details="hello", + lintable=Lintable("filename.yml", content=""), + rule=self.rule, + ignored=True, + ), + ) + self.formatter = CodeclimateJSONFormatter( + pathlib.Path.cwd(), + display_relative_path=True, + ) + + def test_format_list(self) -> None: + """Test if the return value is a string.""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + assert isinstance(self.formatter.format_result(self.matches), str) + + def test_result_is_json(self) -> None: + """Test if returned string value is a JSON.""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + output = self.formatter.format_result(self.matches) + json.loads(output) + # https://github.com/ansible/ansible-navigator/issues/1490 + assert "\n" not in output + + def test_single_match(self) -> None: + """Test negative case. Only lists are allowed. Otherwise a RuntimeError will be raised.""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + with pytest.raises(RuntimeError): + self.formatter.format_result(self.matches[0]) # type: ignore[arg-type] + + def test_result_is_list(self) -> None: + """Test if the return JSON contains a list with a length of 2.""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + result = json.loads(self.formatter.format_result(self.matches)) + assert len(result) == 2 + + def test_validate_codeclimate_schema(self) -> None: + """Test if the returned JSON is a valid codeclimate report.""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + result = json.loads(self.formatter.format_result(self.matches)) + single_match = result[0] + assert "type" in single_match + assert single_match["type"] == "issue" + assert "check_name" in single_match + assert "categories" in single_match + assert isinstance(single_match["categories"], list) + assert "severity" in single_match + assert single_match["severity"] == "major" + assert "description" in single_match + assert "fingerprint" in single_match + assert "location" in single_match + assert "path" in single_match["location"] + assert single_match["location"]["path"] == self.matches[0].filename + assert "lines" in single_match["location"] + assert single_match["location"]["lines"]["begin"] == self.matches[0].lineno + assert "positions" not in single_match["location"] + # check that the 2nd match is marked as 'minor' because it was created with ignored=True + assert result[1]["severity"] == "minor" + + def test_validate_codeclimate_schema_with_positions(self) -> None: + """Test if the returned JSON is a valid codeclimate report (containing 'positions' instead of 'lines').""" + assert isinstance(self.formatter, CodeclimateJSONFormatter) + result = json.loads( + self.formatter.format_result( + [ + MatchError( + message="message", + lineno=1, + column=42, + details="hello", + lintable=Lintable("filename.yml", content=""), + rule=self.rule, + ), + ], + ), + ) + assert result[0]["location"]["positions"]["begin"]["line"] == 1 + assert result[0]["location"]["positions"]["begin"]["column"] == 42 + assert "lines" not in result[0]["location"] + + +def test_code_climate_parsable_ignored() -> None: + """Test that -p option does not alter codeclimate format.""" + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "-v", + "-p", + ] + file = "examples/playbooks/empty_playbook.yml" + result = subprocess.run([*cmd, file], check=False) + result2 = subprocess.run([*cmd, "-p", file], check=False) + + assert result.returncode == result2.returncode + assert result.stdout == result2.stdout diff --git a/test/test_formatter_sarif.py b/test/test_formatter_sarif.py new file mode 100644 index 0000000..026d336 --- /dev/null +++ b/test/test_formatter_sarif.py @@ -0,0 +1,192 @@ +"""Test the codeclimate JSON formatter.""" +from __future__ import annotations + +import json +import os +import pathlib +import subprocess +import sys +from tempfile import NamedTemporaryFile + +import pytest + +from ansiblelint.errors import MatchError +from ansiblelint.file_utils import Lintable +from ansiblelint.formatters import SarifFormatter +from ansiblelint.rules import AnsibleLintRule + + +class TestSarifFormatter: + """Unit test for SarifFormatter.""" + + rule = AnsibleLintRule() + matches: list[MatchError] = [] + formatter: SarifFormatter | None = None + + def setup_class(self) -> None: + """Set up few MatchError objects.""" + self.rule = AnsibleLintRule() + self.rule.id = "TCF0001" + self.rule.severity = "VERY_HIGH" + self.rule.description = "This is the rule description." + self.rule.link = "https://rules/help#TCF0001" + self.rule.tags = ["tag1", "tag2"] + self.matches = [] + self.matches.append( + MatchError( + message="message", + lineno=1, + column=10, + details="details", + lintable=Lintable("filename.yml", content=""), + rule=self.rule, + tag="yaml[test]", + ), + ) + self.matches.append( + MatchError( + message="message", + lineno=2, + details="", + lintable=Lintable("filename.yml", content=""), + rule=self.rule, + tag="yaml[test]", + ), + ) + self.formatter = SarifFormatter(pathlib.Path.cwd(), display_relative_path=True) + + def test_format_list(self) -> None: + """Test if the return value is a string.""" + assert isinstance(self.formatter, SarifFormatter) + assert isinstance(self.formatter.format_result(self.matches), str) + + def test_result_is_json(self) -> None: + """Test if returned string value is a JSON.""" + assert isinstance(self.formatter, SarifFormatter) + output = self.formatter.format_result(self.matches) + json.loads(output) + # https://github.com/ansible/ansible-navigator/issues/1490 + assert "\n" not in output + + def test_single_match(self) -> None: + """Test negative case. Only lists are allowed. Otherwise, a RuntimeError will be raised.""" + assert isinstance(self.formatter, SarifFormatter) + with pytest.raises(RuntimeError): + self.formatter.format_result(self.matches[0]) # type: ignore[arg-type] + + def test_result_is_list(self) -> None: + """Test if the return SARIF object contains the results with length of 2.""" + assert isinstance(self.formatter, SarifFormatter) + sarif = json.loads(self.formatter.format_result(self.matches)) + assert len(sarif["runs"][0]["results"]) == 2 + + def test_validate_sarif_schema(self) -> None: + """Test if the returned JSON is a valid SARIF report.""" + assert isinstance(self.formatter, SarifFormatter) + sarif = json.loads(self.formatter.format_result(self.matches)) + assert sarif["$schema"] == SarifFormatter.SARIF_SCHEMA + assert sarif["version"] == SarifFormatter.SARIF_SCHEMA_VERSION + driver = sarif["runs"][0]["tool"]["driver"] + assert driver["name"] == SarifFormatter.TOOL_NAME + assert driver["informationUri"] == SarifFormatter.TOOL_URL + rules = driver["rules"] + assert len(rules) == 1 + assert rules[0]["id"] == self.matches[0].tag + assert rules[0]["name"] == self.matches[0].tag + assert rules[0]["shortDescription"]["text"] == self.matches[0].message + assert rules[0]["defaultConfiguration"]["level"] == "error" + assert rules[0]["help"]["text"] == self.matches[0].rule.description + assert rules[0]["properties"]["tags"] == self.matches[0].rule.tags + assert rules[0]["helpUri"] == self.matches[0].rule.url + results = sarif["runs"][0]["results"] + assert len(results) == 2 + for i, result in enumerate(results): + assert result["ruleId"] == self.matches[i].tag + assert ( + result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"] + == self.matches[i].filename + ) + assert ( + result["locations"][0]["physicalLocation"]["artifactLocation"][ + "uriBaseId" + ] + == SarifFormatter.BASE_URI_ID + ) + assert ( + result["locations"][0]["physicalLocation"]["region"]["startLine"] + == self.matches[i].lineno + ) + if self.matches[i].column: + assert ( + result["locations"][0]["physicalLocation"]["region"]["startColumn"] + == self.matches[i].column + ) + else: + assert ( + "startColumn" + not in result["locations"][0]["physicalLocation"]["region"] + ) + assert sarif["runs"][0]["originalUriBaseIds"][SarifFormatter.BASE_URI_ID]["uri"] + assert results[0]["message"]["text"] == self.matches[0].details + assert results[1]["message"]["text"] == self.matches[1].message + + +def test_sarif_parsable_ignored() -> None: + """Test that -p option does not alter SARIF format.""" + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "-v", + "-p", + ] + file = "examples/playbooks/empty_playbook.yml" + result = subprocess.run([*cmd, file], check=False) + result2 = subprocess.run([*cmd, "-p", file], check=False) + + assert result.returncode == result2.returncode + assert result.stdout == result2.stdout + + +@pytest.mark.parametrize( + ("file", "return_code"), + ( + pytest.param("examples/playbooks/valid.yml", 0), + pytest.param("playbook.yml", 2), + ), +) +def test_sarif_file(file: str, return_code: int) -> None: + """Test ability to dump sarif file (--sarif-file).""" + with NamedTemporaryFile(mode="w", suffix=".sarif", prefix="output") as output_file: + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "--sarif-file", + str(output_file.name), + ] + result = subprocess.run([*cmd, file], check=False, capture_output=True) + assert result.returncode == return_code + assert os.path.exists(output_file.name) # noqa: PTH110 + assert os.path.getsize(output_file.name) > 0 + + +@pytest.mark.parametrize( + ("file", "return_code"), + (pytest.param("examples/playbooks/valid.yml", 0),), +) +def test_sarif_file_creates_it_if_none_exists(file: str, return_code: int) -> None: + """Test ability to create sarif file if none exists and dump output to it (--sarif-file).""" + sarif_file_name = "test_output.sarif" + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "--sarif-file", + sarif_file_name, + ] + result = subprocess.run([*cmd, file], check=False, capture_output=True) + assert result.returncode == return_code + assert os.path.exists(sarif_file_name) # noqa: PTH110 + assert os.path.getsize(sarif_file_name) > 0 + pathlib.Path.unlink(pathlib.Path(sarif_file_name)) diff --git a/test/test_import_include_role.py b/test/test_import_include_role.py new file mode 100644 index 0000000..bc3fdbe --- /dev/null +++ b/test/test_import_include_role.py @@ -0,0 +1,157 @@ +"""Tests related to role imports.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from pathlib import Path + + from _pytest.fixtures import SubRequest + + from ansiblelint.rules import RulesCollection + +ROLE_TASKS_MAIN = """\ +--- +- name: Shell instead of command + shell: echo hello world # noqa: fqcn no-free-form + changed_when: false +""" + +ROLE_TASKS_WORLD = """\ +--- +- ansible.builtin.debug: + msg: "this is a task without a name" +""" + +PLAY_IMPORT_ROLE = """\ +--- +- name: Test fixture + hosts: all + + tasks: + - name: Some import # noqa: fqcn + import_role: + name: test-role +""" + +PLAY_IMPORT_ROLE_FQCN = """\ +--- +- name: Test fixture + hosts: all + + tasks: + - name: Some import + ansible.builtin.import_role: + name: test-role +""" + +PLAY_IMPORT_ROLE_INLINE = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Some import + import_role: name=test-role # noqa: no-free-form fqcn +""" + +PLAY_INCLUDE_ROLE = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Some import + include_role: + name: test-role + tasks_from: world +""" + +PLAY_INCLUDE_ROLE_FQCN = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Some import + ansible.builtin.include_role: + name: test-role + tasks_from: world +""" + +PLAY_INCLUDE_ROLE_INLINE = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Some import + include_role: name=test-role tasks_from=world # noqa: no-free-form +""" + + +@pytest.fixture(name="playbook_path") +def fixture_playbook_path(request: SubRequest, tmp_path: Path) -> str: + """Create a reusable per-test role skeleton.""" + playbook_text = request.param + role_tasks_dir = tmp_path / "test-role" / "tasks" + role_tasks_dir.mkdir(parents=True) + (role_tasks_dir / "main.yml").write_text(ROLE_TASKS_MAIN) + (role_tasks_dir / "world.yml").write_text(ROLE_TASKS_WORLD) + play_path = tmp_path / "playbook.yml" + play_path.write_text(playbook_text) + return str(play_path) + + +@pytest.mark.parametrize( + ("playbook_path", "messages"), + ( + pytest.param( + PLAY_IMPORT_ROLE, + ["only when shell functionality is required", "All tasks should be named"], + id="IMPORT_ROLE", + ), + pytest.param( + PLAY_IMPORT_ROLE_FQCN, + ["only when shell functionality is required", "All tasks should be named"], + id="IMPORT_ROLE_FQCN", + ), + pytest.param( + PLAY_IMPORT_ROLE_INLINE, + ["only when shell functionality is require", "All tasks should be named"], + id="IMPORT_ROLE_INLINE", + ), + pytest.param( + PLAY_INCLUDE_ROLE, + ["only when shell functionality is require", "All tasks should be named"], + id="INCLUDE_ROLE", + ), + pytest.param( + PLAY_INCLUDE_ROLE_FQCN, + ["only when shell functionality is require", "All tasks should be named"], + id="INCLUDE_ROLE_FQCN", + ), + pytest.param( + PLAY_INCLUDE_ROLE_INLINE, + ["only when shell functionality is require", "All tasks should be named"], + id="INCLUDE_ROLE_INLINE", + ), + ), + indirect=("playbook_path",), +) +def test_import_role2( + default_rules_collection: RulesCollection, + playbook_path: str, + messages: list[str], +) -> None: + """Test that include_role digs deeper than import_role.""" + runner = Runner( + playbook_path, + rules=default_rules_collection, + skip_list=["fqcn[action-core]"], + ) + results = runner.run() + for message in messages: + assert message in str(results) + # Ensure no other unexpected messages are present + assert len(messages) == len(results), results diff --git a/test/test_import_playbook.py b/test/test_import_playbook.py new file mode 100644 index 0000000..66d8763 --- /dev/null +++ b/test/test_import_playbook.py @@ -0,0 +1,18 @@ +"""Test ability to import playbooks.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_task_hook_import_playbook(default_rules_collection: RulesCollection) -> None: + """Assures import_playbook includes are recognized.""" + playbook_path = "examples/playbooks/playbook-parent.yml" + runner = Runner(playbook_path, rules=default_rules_collection) + results = runner.run() + + results_text = str(results) + assert len(runner.lintables) == 2 + assert len(results) == 2 + # Assures we detected the issues from imported playbook + assert "Commands should not change things" in results_text + assert "[name]" in results_text + assert "All tasks should be named" in results_text diff --git a/test/test_import_tasks.py b/test/test_import_tasks.py new file mode 100644 index 0000000..aec1c25 --- /dev/null +++ b/test/test_import_tasks.py @@ -0,0 +1,29 @@ +"""Test related to import of invalid files.""" +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +@pytest.mark.parametrize( + "playbook_path", + ( + pytest.param( + "examples/playbooks/test_import_with_conflicting_action_statements.yml", + id="0", + ), + pytest.param("examples/playbooks/test_import_with_malformed.yml", id="1"), + ), +) +def test_import_tasks( + default_rules_collection: RulesCollection, + playbook_path: str, +) -> None: + """Assures import_playbook includes are recognized.""" + runner = Runner(playbook_path, rules=default_rules_collection) + results = runner.run() + + assert len(runner.lintables) == 1 + assert len(results) == 1 + # Assures we detected the issues from imported file + assert results[0].rule.id == "syntax-check" diff --git a/test/test_include_miss_file_with_role.py b/test/test_include_miss_file_with_role.py new file mode 100644 index 0000000..6834758 --- /dev/null +++ b/test/test_include_miss_file_with_role.py @@ -0,0 +1,43 @@ +"""Tests related to inclusions.""" +import pytest +from _pytest.logging import LogCaptureFixture + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_cases_warning_message(default_rules_collection: RulesCollection) -> None: + """Test that including a non-existing file produces an error.""" + playbook_path = "examples/playbooks/play_miss_include.yml" + runner = Runner(playbook_path, rules=default_rules_collection) + results = runner.run() + + assert len(runner.lintables) == 3 + assert len(results) == 1 + assert "No such file or directory" in results[0].message + + +@pytest.mark.parametrize( + "playbook_path", + ( + pytest.param("examples/playbooks/test_include_inplace.yml", id="inplace"), + pytest.param("examples/playbooks/test_include_relative.yml", id="relative"), + ), +) +def test_cases_that_do_not_report( + playbook_path: str, + default_rules_collection: RulesCollection, + caplog: LogCaptureFixture, +) -> None: + """Test that relative inclusions are properly followed.""" + runner = Runner(playbook_path, rules=default_rules_collection) + result = runner.run() + noexist_message_count = 0 + + for record in caplog.records: + for msg in ("No such file or directory", "Couldn't open"): + if msg in str(record): + noexist_message_count += 1 + + assert noexist_message_count == 0 + assert len(result) == 0 diff --git a/test/test_internal_rules.py b/test/test_internal_rules.py new file mode 100644 index 0000000..b949238 --- /dev/null +++ b/test/test_internal_rules.py @@ -0,0 +1,8 @@ +"""Tests for internal rules.""" +from ansiblelint._internal.rules import BaseRule + + +def test_base_rule_url() -> None: + """Test that rule URL is set to expected value.""" + rule = BaseRule() + assert rule.url == "https://ansible-lint.readthedocs.io/rules/" diff --git a/test/test_lint_rule.py b/test/test_lint_rule.py new file mode 100644 index 0000000..2e13aa2 --- /dev/null +++ b/test/test_lint_rule.py @@ -0,0 +1,46 @@ +"""Tests for lintable.""" +# Copyright (c) 2013-2014 Will Thames <will@thames.id.au> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from test.rules.fixtures import ematcher, raw_task + +import pytest + +from ansiblelint.file_utils import Lintable + + +@pytest.fixture(name="lintable") +def fixture_lintable() -> Lintable: + """Return a playbook Lintable for use in this file's tests.""" + return Lintable("examples/playbooks/ematcher-rule.yml", kind="playbook") + + +def test_rule_matching(lintable: Lintable) -> None: + """Test rule.matchlines() on a playbook.""" + rule = ematcher.EMatcherRule() + matches = rule.matchlines(lintable) + assert len(matches) == 3 + + +def test_raw_rule_matching(lintable: Lintable) -> None: + """Test rule.matchlines() on a playbook.""" + rule = raw_task.RawTaskRule() + matches = rule.matchtasks(lintable) + assert len(matches) == 1 diff --git a/test/test_list_rules.py b/test/test_list_rules.py new file mode 100644 index 0000000..dab16e3 --- /dev/null +++ b/test/test_list_rules.py @@ -0,0 +1,76 @@ +"""Tests related to our logging/verbosity setup.""" + +from pathlib import Path + +import pytest + +from ansiblelint.testing import run_ansible_lint + + +def test_list_rules_includes_opt_in_rules(project_path: Path) -> None: + """Checks that listing rules also includes the opt-in rules.""" + # Piggyback off the .yamllint in the root of the repo, just for testing. + # We'll "override" it with the one in the fixture. + fakerole = Path("test") / "fixtures" / "list-rules-tests" + + result_list_rules = run_ansible_lint("-L", fakerole, cwd=project_path) + + assert ("opt-in" in result_list_rules.stdout) is True + + +@pytest.mark.parametrize( + ("result", "returncode", "format_string"), + ( + (False, 0, "brief"), + (False, 0, "full"), + (False, 0, "md"), + (True, 2, "json"), + (True, 2, "codeclimate"), + (True, 2, "quiet"), + (True, 2, "pep8"), + (True, 2, "foo"), + ), + ids=( + "plain", + "full", + "md", + "json", + "codeclimate", + "quiet", + "pep8", + "foo", + ), +) +def test_list_rules_with_format_option( + result: bool, + returncode: int, + format_string: str, + project_path: Path, +) -> None: + """Checks that listing rules with format options works.""" + # Piggyback off the .yamllint in the root of the repo, just for testing. + # We'll "override" it with the one in the fixture. + fakerole = Path("test") / "fixtures" / "list-rules-tests" + + result_list_rules = run_ansible_lint( + "-f", + format_string, + "-L", + fakerole, + cwd=project_path, + ) + + assert (f"invalid choice: '{format_string}'" in result_list_rules.stderr) is result + assert ("syntax-check" in result_list_rules.stdout) is not result + assert result_list_rules.returncode is returncode + + +def test_list_tags_includes_opt_in_rules(project_path: Path) -> None: + """Checks that listing tags also includes the opt-in rules.""" + # Piggyback off the .yamllint in the root of the repo, just for testing. + # We'll "override" it with the one in the fixture. + fakerole = Path("test") / "fixtures" / "list-rules-tests" + + result_list_tags = run_ansible_lint("-L", str(fakerole), cwd=project_path) + + assert ("opt-in" in result_list_tags.stdout) is True diff --git a/test/test_load_failure.py b/test/test_load_failure.py new file mode 100644 index 0000000..98d178f --- /dev/null +++ b/test/test_load_failure.py @@ -0,0 +1,25 @@ +"""Tests for LoadFailureRule.""" +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +@pytest.mark.parametrize( + "path", + ( + pytest.param("examples/broken/encoding.j2", id="jinja2"), + pytest.param("examples/broken/encoding.yml", id="yaml"), + ), +) +def test_load_failure_encoding( + path: str, + default_rules_collection: RulesCollection, +) -> None: + """Check that we fail when file encoding is wrong.""" + runner = Runner(path, rules=default_rules_collection) + matches = runner.run() + assert len(matches) == 1, matches + assert matches[0].rule.id == "load-failure" + assert "'utf-8' codec can't decode byte" in matches[0].message + assert matches[0].tag == "load-failure[unicodedecodeerror]" diff --git a/test/test_loaders.py b/test/test_loaders.py new file mode 100644 index 0000000..be12cfd --- /dev/null +++ b/test/test_loaders.py @@ -0,0 +1,121 @@ +"""Tests for loaders submodule.""" +import os +import tempfile +import uuid +from pathlib import Path +from textwrap import dedent + +from ansiblelint.loaders import IGNORE_FILE, load_ignore_txt + + +def test_load_ignore_txt_default_empty() -> None: + """Test load_ignore_txt when no ignore-file is present.""" + with tempfile.TemporaryDirectory() as temporary_directory: + cwd = Path.cwd() + + try: + os.chdir(temporary_directory) + result = load_ignore_txt() + finally: + os.chdir(cwd) + + assert not result + + +def test_load_ignore_txt_default_success() -> None: + """Test load_ignore_txt with an existing ignore-file in the default location.""" + with tempfile.TemporaryDirectory() as temporary_directory: + ignore_file = Path(temporary_directory) / IGNORE_FILE.default + + with ignore_file.open("w", encoding="utf-8") as _ignore_file: + _ignore_file.write( + dedent( + """ + # See https://ansible-lint.readthedocs.io/configuring/#ignoring-rules-for-entire-files + playbook2.yml package-latest # comment + playbook2.yml foo-bar + """, + ), + ) + + cwd = Path.cwd() + + try: + os.chdir(temporary_directory) + result = load_ignore_txt() + finally: + os.chdir(cwd) + + assert result == {"playbook2.yml": {"package-latest", "foo-bar"}} + + +def test_load_ignore_txt_default_success_alternative() -> None: + """Test load_ignore_txt with an ignore-file in the alternative location ('.config' subdirectory).""" + with tempfile.TemporaryDirectory() as temporary_directory: + ignore_file = Path(temporary_directory) / IGNORE_FILE.alternative + ignore_file.parent.mkdir(parents=True) + + with ignore_file.open("w", encoding="utf-8") as _ignore_file: + _ignore_file.write( + dedent( + """ + playbook.yml foo-bar + playbook.yml more-foo # what-the-foo? + tasks/main.yml more-bar + """, + ), + ) + + cwd = Path.cwd() + + try: + os.chdir(temporary_directory) + result = load_ignore_txt() + finally: + os.chdir(cwd) + + assert result == { + "playbook.yml": {"more-foo", "foo-bar"}, + "tasks/main.yml": {"more-bar"}, + } + + +def test_load_ignore_txt_custom_success() -> None: + """Test load_ignore_txt with an ignore-file in a user defined location.""" + with tempfile.TemporaryDirectory() as temporary_directory: + ignore_file = Path(temporary_directory) / "subdir" / "my_ignores.txt" + ignore_file.parent.mkdir(parents=True, exist_ok=True) + + with ignore_file.open("w", encoding="utf-8") as _ignore_file: + _ignore_file.write( + dedent( + """ + playbook.yml hector + vars/main.yml tuco + roles/guzman/tasks/main.yml lalo + roles/eduardo/tasks/main.yml lalo + """, + ), + ) + + cwd = Path.cwd() + + try: + os.chdir(temporary_directory) + result = load_ignore_txt(Path(ignore_file)) + finally: + os.chdir(cwd) + + assert result == { + "playbook.yml": {"hector"}, + "roles/eduardo/tasks/main.yml": {"lalo"}, + "roles/guzman/tasks/main.yml": {"lalo"}, + "vars/main.yml": {"tuco"}, + } + + +def test_load_ignore_txt_custom_fail() -> None: + """Test load_ignore_txt with a user defined but invalid ignore-file location.""" + result = load_ignore_txt(Path(str(uuid.uuid4()))) + + assert not result diff --git a/test/test_local_content.py b/test/test_local_content.py new file mode 100644 index 0000000..8455aaf --- /dev/null +++ b/test/test_local_content.py @@ -0,0 +1,13 @@ +"""Test playbooks with local content.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_local_collection(default_rules_collection: RulesCollection) -> None: + """Assures local collections are found.""" + playbook_path = "test/local-content/test-collection.yml" + runner = Runner(playbook_path, rules=default_rules_collection) + results = runner.run() + + assert len(runner.lintables) == 1 + assert len(results) == 0 diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..870926f --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,84 @@ +"""Tests related to ansiblelint.__main__ module.""" +import os +import shutil +import subprocess +import sys +import time +from pathlib import Path + +import pytest +from pytest_mock import MockerFixture + +from ansiblelint.config import get_version_warning + + +@pytest.mark.parametrize( + ("expected_warning"), + (False, True), + ids=("normal", "isolated"), +) +def test_call_from_outside_venv(expected_warning: bool) -> None: + """Asserts ability to be called w/ or w/o venv activation.""" + git_location = shutil.which("git") + if not git_location: + pytest.fail("git not found") + git_path = Path(git_location).parent + + if expected_warning: + env = {"HOME": str(Path.home()), "PATH": str(git_path)} + else: + env = os.environ.copy() + + for v in ("COVERAGE_FILE", "COVERAGE_PROCESS_START"): + if v in os.environ: + env[v] = os.environ[v] + + py_path = Path(sys.executable).parent + # Passing custom env prevents the process from inheriting PATH or other + # environment variables from the current process, so we emulate being + # called from outside the venv. + proc = subprocess.run( + [str(py_path / "ansible-lint"), "--version"], + check=False, + capture_output=True, + text=True, + env=env, + ) + assert proc.returncode == 0, proc + warning_found = "PATH altered to include" in proc.stderr + assert warning_found is expected_warning + + +@pytest.mark.parametrize( + ("ver_diff", "found", "check", "outlen"), + ( + ("v1.2.2", True, "pre-release", 1), + ("v1.2.3", False, "", 1), + ("v1.2.4", True, "new release", 2), + ), +) +def test_get_version_warning( + mocker: MockerFixture, + ver_diff: str, + found: bool, + check: str, + outlen: int, +) -> None: + """Assert get_version_warning working as expected.""" + data = f'{{"html_url": "https://127.0.0.1", "tag_name": "{ver_diff}"}}' + # simulate cache file + mocker.patch("os.path.exists", return_value=True) + mocker.patch("os.path.getmtime", return_value=time.time()) + mocker.patch("builtins.open", mocker.mock_open(read_data=data)) + # overwrite ansible-lint version + mocker.patch("ansiblelint.config.__version__", "1.2.3") + # overwrite install method to custom one. This one will increase msg line count + # to easily detect unwanted call to it. + mocker.patch("ansiblelint.config.guess_install_method", return_value="\n") + msg = get_version_warning() + + if not found: + assert msg == check + else: + assert check in msg + assert len(msg.split("\n")) == outlen diff --git a/test/test_matcherrror.py b/test/test_matcherrror.py new file mode 100644 index 0000000..03d9cbd --- /dev/null +++ b/test/test_matcherrror.py @@ -0,0 +1,208 @@ +"""Tests for MatchError.""" + +import operator +from typing import Any, Callable + +import pytest + +from ansiblelint.errors import MatchError +from ansiblelint.file_utils import Lintable +from ansiblelint.rules.no_changed_when import CommandHasChangesCheckRule +from ansiblelint.rules.partial_become import BecomeUserWithoutBecomeRule + + +class DummyTestObject: + """A dummy object for equality tests.""" + + def __repr__(self) -> str: + """Return a dummy object representation for parametrize.""" + return f"{self.__class__.__name__}()" + + def __eq__(self, other: object) -> bool: + """Report the equality check failure with any object.""" + return False + + def __ne__(self, other: object) -> bool: + """Report the confirmation of inequality with any object.""" + return True + + +class DummySentinelTestObject: + """A dummy object for equality protocol tests with sentinel.""" + + def __eq__(self, other: object) -> bool: + """Return sentinel as result of equality check w/ anything.""" + return "EQ_SENTINEL" # type: ignore[return-value] + + def __ne__(self, other: object) -> bool: + """Return sentinel as result of inequality check w/ anything.""" + return "NE_SENTINEL" # type: ignore[return-value] + + def __lt__(self, other: object) -> bool: + """Return sentinel as result of less than check w/ anything.""" + return "LT_SENTINEL" # type: ignore[return-value] + + def __gt__(self, other: object) -> bool: + """Return sentinel as result of greater than chk w/ anything.""" + return "GT_SENTINEL" # type: ignore[return-value] + + +@pytest.mark.parametrize( + ("left_match_error", "right_match_error"), + ( + (MatchError("foo"), MatchError("foo")), + (MatchError("a", details="foo"), MatchError("a", details="foo")), + ), +) +def test_matcherror_compare( + left_match_error: MatchError, + right_match_error: MatchError, +) -> None: + """Check that MatchError instances with similar attrs are equivalent.""" + assert left_match_error == right_match_error + + +def test_matcherror_invalid() -> None: + """Ensure that MatchError requires message or rule.""" + with pytest.raises(TypeError): + MatchError() # pylint: disable=pointless-exception-statement + + +@pytest.mark.parametrize( + ("left_match_error", "right_match_error"), + ( + # sorting by message + (MatchError("z"), MatchError("a")), + # filenames takes priority in sorting + ( + MatchError("a", lintable=Lintable("b", content="")), + MatchError("a", lintable=Lintable("a", content="")), + ), + # rule id partial-become > rule id no-changed-when + ( + MatchError(rule=BecomeUserWithoutBecomeRule()), + MatchError(rule=CommandHasChangesCheckRule()), + ), + # details are taken into account + (MatchError("a", details="foo"), MatchError("a", details="bar")), + # columns are taken into account + (MatchError("a", column=3), MatchError("a", column=1)), + (MatchError("a", column=3), MatchError("a")), + ), +) +class TestMatchErrorCompare: + """Test the comparison of MatchError instances.""" + + @staticmethod + def test_match_error_less_than( + left_match_error: MatchError, + right_match_error: MatchError, + ) -> None: + """Check 'less than' protocol implementation in MatchError.""" + assert right_match_error < left_match_error + + @staticmethod + def test_match_error_greater_than( + left_match_error: MatchError, + right_match_error: MatchError, + ) -> None: + """Check 'greater than' protocol implementation in MatchError.""" + assert left_match_error > right_match_error + + @staticmethod + def test_match_error_not_equal( + left_match_error: MatchError, + right_match_error: MatchError, + ) -> None: + """Check 'not equals' protocol implementation in MatchError.""" + assert left_match_error != right_match_error + + +@pytest.mark.parametrize( + "other", + ( + None, + "foo", + 42, + Exception("foo"), + ), + ids=repr, +) +@pytest.mark.parametrize( + ("operation", "operator_char"), + ( + pytest.param(operator.le, "<=", id="<="), + pytest.param(operator.gt, ">", id=">"), + ), +) +def test_matcherror_compare_no_other_fallback( + other: Any, + operation: Callable[..., bool], + operator_char: str, +) -> None: + """Check that MatchError comparison with other types causes TypeError.""" + expected_error = ( + r"^(" + r"unsupported operand type\(s\) for {operator!s}:|" + r"'{operator!s}' not supported between instances of" + r") 'MatchError' and '{other_type!s}'$".format( + other_type=type(other).__name__, + operator=operator_char, + ) + ) + with pytest.raises(TypeError, match=expected_error): + operation(MatchError("foo"), other) + + +@pytest.mark.parametrize( + "other", + ( + None, + "foo", + 42, + Exception("foo"), + DummyTestObject(), + ), + ids=repr, +) +@pytest.mark.parametrize( + ("operation", "expected_value"), + ( + (operator.eq, False), + (operator.ne, True), + ), + ids=("==", "!="), +) +def test_matcherror_compare_with_other_fallback( + other: object, + operation: Callable[..., bool], + expected_value: bool, +) -> None: + """Check that MatchError comparison runs other types fallbacks.""" + assert operation(MatchError(message="foo"), other) is expected_value + + +@pytest.mark.parametrize( + ("operation", "expected_value"), + ( + (operator.eq, "EQ_SENTINEL"), + (operator.ne, "NE_SENTINEL"), + # NOTE: these are swapped because when we do `x < y`, and `x.__lt__(y)` + # NOTE: returns `NotImplemented`, Python will reverse the check into + # NOTE: `y > x`, and so `y.__gt__(x) is called. + # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__ + (operator.lt, "GT_SENTINEL"), + (operator.gt, "LT_SENTINEL"), + ), + ids=("==", "!=", "<", ">"), +) +def test_matcherror_compare_with_dummy_sentinel( + operation: Callable[..., bool], + expected_value: str, +) -> None: + """Check that MatchError comparison runs other types fallbacks.""" + dummy_obj = DummySentinelTestObject() + # NOTE: This assertion abuses the CPython property to cache short string + # NOTE: objects because the identity check is more precise and we don't + # NOTE: want extra operator protocol methods to influence the test. + assert operation(MatchError("foo"), dummy_obj) is expected_value # type: ignore[comparison-overlap] diff --git a/test/test_mockings.py b/test/test_mockings.py new file mode 100644 index 0000000..0e8d77a --- /dev/null +++ b/test/test_mockings.py @@ -0,0 +1,18 @@ +"""Test mockings module.""" +from typing import Any + +import pytest + +from ansiblelint._mockings import _make_module_stub +from ansiblelint.config import options +from ansiblelint.constants import RC + + +def test_make_module_stub(mocker: Any) -> None: + """Test make module stub.""" + mocker.patch("ansiblelint.config.options.cache_dir", return_value=".") + assert options.cache_dir is not None + with pytest.raises(SystemExit) as exc: + _make_module_stub(module_name="", options=options) + assert exc.type == SystemExit + assert exc.value.code == RC.INVALID_CONFIG diff --git a/test/test_profiles.py b/test/test_profiles.py new file mode 100644 index 0000000..a40382c --- /dev/null +++ b/test/test_profiles.py @@ -0,0 +1,60 @@ +"""Tests for the --profile feature.""" +import platform +import subprocess +import sys + +from _pytest.capture import CaptureFixture + +from ansiblelint.rules import RulesCollection, filter_rules_with_profile +from ansiblelint.rules.risky_shell_pipe import ShellWithoutPipefail +from ansiblelint.text import strip_ansi_escape + + +def test_profile_min() -> None: + """Asserts our ability to unload rules based on profile.""" + collection = RulesCollection() + assert len(collection.rules) == 4, "Unexpected number of implicit rules." + # register one extra rule that we know not to be part of "min" profile + + collection.register(ShellWithoutPipefail()) + assert len(collection.rules) == 5, "Failed to register new rule." + + filter_rules_with_profile(collection.rules, "min") + assert ( + len(collection.rules) == 3 + ), "Failed to unload rule that is not part of 'min' profile." + + +def test_profile_listing(capfd: CaptureFixture[str]) -> None: + """Test that run without arguments it will detect and lint the entire repository.""" + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "-P", + ] + result = subprocess.run(cmd, check=False).returncode + assert result == 0 + + out, err = capfd.readouterr() + + # Confirmation that it runs in auto-detect mode + assert "command-instead-of-module" in out + # On WSL we might see this warning on stderr: + # [WARNING]: Ansible is being run in a world writable directory + # WSL2 has "WSL2" in platform name but WSL1 has "microsoft": + platform_name = platform.platform().lower() + err_lines = [] + for line in strip_ansi_escape(err).splitlines(): + if "SyntaxWarning:" in line: + continue + if ( + "Skipped installing collection dependencies due to running in offline mode." + in line + ): + continue + err_lines.append(line) + if all(word not in platform_name for word in ["wsl", "microsoft"]) and err_lines: + assert ( + not err_lines + ), f"Unexpected stderr output found while running on {platform_name} platform:\n{err_lines}" diff --git a/test/test_rule_properties.py b/test/test_rule_properties.py new file mode 100644 index 0000000..7db3afd --- /dev/null +++ b/test/test_rule_properties.py @@ -0,0 +1,16 @@ +"""Tests related to rule properties.""" +from ansiblelint.rules import RulesCollection + + +def test_severity_valid(default_rules_collection: RulesCollection) -> None: + """Test that rules collection only has allow-listed severities.""" + valid_severity_values = [ + "VERY_HIGH", + "HIGH", + "MEDIUM", + "LOW", + "VERY_LOW", + "INFO", + ] + for rule in default_rules_collection: + assert rule.severity in valid_severity_values diff --git a/test/test_rules_collection.py b/test/test_rules_collection.py new file mode 100644 index 0000000..66c69ec --- /dev/null +++ b/test/test_rules_collection.py @@ -0,0 +1,175 @@ +"""Tests for rule collection class.""" +# Copyright (c) 2013-2014 Will Thames <will@thames.id.au> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import annotations + +import collections +import re +from pathlib import Path + +import pytest + +from ansiblelint.config import options +from ansiblelint.file_utils import Lintable +from ansiblelint.rules import RulesCollection +from ansiblelint.testing import run_ansible_lint + + +@pytest.fixture(name="test_rules_collection") +def fixture_test_rules_collection() -> RulesCollection: + """Create a shared rules collection test instance.""" + return RulesCollection([Path("./test/rules/fixtures").resolve()]) + + +@pytest.fixture(name="ematchtestfile") +def fixture_ematchtestfile() -> Lintable: + """Produce a test lintable with an id violation.""" + return Lintable("examples/playbooks/ematcher-rule.yml", kind="playbook") + + +@pytest.fixture(name="bracketsmatchtestfile") +def fixture_bracketsmatchtestfile() -> Lintable: + """Produce a test lintable with matching brackets.""" + return Lintable("examples/playbooks/bracketsmatchtest.yml", kind="playbook") + + +def test_load_collection_from_directory(test_rules_collection: RulesCollection) -> None: + """Test that custom rules extend the default ones.""" + # two detected rules plus the internal ones + assert len(test_rules_collection) == 7 + + +def test_run_collection( + test_rules_collection: RulesCollection, + ematchtestfile: Lintable, +) -> None: + """Test that default rules match pre-meditated violations.""" + matches = test_rules_collection.run(ematchtestfile) + assert len(matches) == 4 # 3 occurrences of BANNED using TEST0001 + 1 for raw-task + assert matches[0].lineno == 3 + + +def test_tags( + test_rules_collection: RulesCollection, + ematchtestfile: Lintable, + bracketsmatchtestfile: Lintable, +) -> None: + """Test that tags are treated as skip markers.""" + matches = test_rules_collection.run(ematchtestfile, tags={"test1"}) + assert len(matches) == 3 + matches = test_rules_collection.run(ematchtestfile, tags={"test2"}) + assert len(matches) == 0 + matches = test_rules_collection.run(bracketsmatchtestfile, tags={"test1"}) + assert len(matches) == 0 + matches = test_rules_collection.run(bracketsmatchtestfile, tags={"test2"}) + assert len(matches) == 2 + + +def test_skip_tags( + test_rules_collection: RulesCollection, + ematchtestfile: Lintable, + bracketsmatchtestfile: Lintable, +) -> None: + """Test that tags can be skipped.""" + matches = test_rules_collection.run(ematchtestfile, skip_list=["test1", "test3"]) + assert len(matches) == 0 + matches = test_rules_collection.run(ematchtestfile, skip_list=["test2", "test3"]) + assert len(matches) == 3 + matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=["test1"]) + assert len(matches) == 2 + matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=["test2"]) + assert len(matches) == 0 + + +def test_skip_id( + test_rules_collection: RulesCollection, + ematchtestfile: Lintable, + bracketsmatchtestfile: Lintable, +) -> None: + """Check that skipping valid IDs excludes their violations.""" + matches = test_rules_collection.run( + ematchtestfile, + skip_list=["TEST0001", "raw-task"], + ) + assert len(matches) == 0 + matches = test_rules_collection.run( + ematchtestfile, + skip_list=["TEST0002", "raw-task"], + ) + assert len(matches) == 3 + matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=["TEST0001"]) + assert len(matches) == 2 + matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=["TEST0002"]) + assert len(matches) == 0 + + +def test_skip_non_existent_id( + test_rules_collection: RulesCollection, + ematchtestfile: Lintable, +) -> None: + """Check that skipping invalid IDs changes nothing.""" + matches = test_rules_collection.run(ematchtestfile, skip_list=["DOESNOTEXIST"]) + assert len(matches) == 4 + + +def test_no_duplicate_rule_ids() -> None: + """Check that rules of the collection don't have duplicate IDs.""" + real_rules = RulesCollection([Path("./src/ansiblelint/rules").resolve()]) + rule_ids = [rule.id for rule in real_rules] + assert not any(y > 1 for y in collections.Counter(rule_ids).values()) + + +def test_rich_rule_listing() -> None: + """Test that rich list format output is rendered as a table. + + This check also offers the contract of having rule id, short and long + descriptions in the console output. + """ + rules_path = Path("./test/rules/fixtures").resolve() + result = run_ansible_lint("-r", str(rules_path), "-f", "full", "-L") + assert result.returncode == 0 + + for rule in RulesCollection([rules_path]): + assert rule.id in result.stdout + assert rule.shortdesc in result.stdout + # description could wrap inside table, so we do not check full length + assert rule.description[:30] in result.stdout + + +def test_rules_id_format() -> None: + """Assure all our rules have consistent format.""" + rule_id_re = re.compile("^[a-z-]{4,30}$") + rules = RulesCollection( + [Path("./src/ansiblelint/rules").resolve()], + options=options, + conditional=False, + ) + keys: set[str] = set() + for rule in rules: + assert rule_id_re.match( + rule.id, + ), f"Rule id {rule.id} did not match our required format." + keys.add(rule.id) + assert ( + rule.help or rule.description or rule.__doc__ + ), f"Rule {rule.id} must have at least one of: .help, .description, .__doc__" + assert "yaml" in keys, "yaml rule is missing" + assert len(rules) == 49 # update this number when adding new rules! + assert len(keys) == len(rules), "Duplicate rule ids?" diff --git a/test/test_runner.py b/test/test_runner.py new file mode 100644 index 0000000..e89cee1 --- /dev/null +++ b/test/test_runner.py @@ -0,0 +1,210 @@ +"""Tests for runner submodule.""" +# Copyright (c) 2013-2014 Will Thames <will@thames.id.au> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +from ansiblelint import formatters +from ansiblelint.file_utils import Lintable +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from ansiblelint.rules import RulesCollection + +LOTS_OF_WARNINGS_PLAYBOOK = Path("examples/playbooks/lots_of_warnings.yml").resolve() + + +@pytest.mark.parametrize( + ("playbook", "exclude", "length"), + ( + pytest.param( + Path("examples/playbooks/nomatchestest.yml"), + [], + 0, + id="nomatchestest", + ), + pytest.param(Path("examples/playbooks/unicode.yml"), [], 1, id="unicode"), + pytest.param( + LOTS_OF_WARNINGS_PLAYBOOK, + [LOTS_OF_WARNINGS_PLAYBOOK], + 992, + id="lots_of_warnings", + ), + pytest.param(Path("examples/playbooks/become.yml"), [], 0, id="become"), + pytest.param( + Path("examples/playbooks/contains_secrets.yml"), + [], + 0, + id="contains_secrets", + ), + ), +) +def test_runner( + default_rules_collection: RulesCollection, + playbook: Path, + exclude: list[str], + length: int, +) -> None: + """Test that runner can go through any corner cases.""" + runner = Runner(playbook, rules=default_rules_collection, exclude_paths=exclude) + + matches = runner.run() + + assert len(matches) == length + + +def test_runner_exclude_paths(default_rules_collection: RulesCollection) -> None: + """Test that exclude paths do work.""" + runner = Runner( + "examples/playbooks/deep/", + rules=default_rules_collection, + exclude_paths=["examples/playbooks/deep/empty.yml"], + ) + + matches = runner.run() + assert len(matches) == 0 + + +@pytest.mark.parametrize(("exclude_path"), ("**/playbooks/*.yml",)) +def test_runner_exclude_globs( + default_rules_collection: RulesCollection, + exclude_path: str, +) -> None: + """Test that globs work.""" + runner = Runner( + "examples/playbooks", + rules=default_rules_collection, + exclude_paths=[exclude_path], + ) + + matches = runner.run() + # we expect to find one match from the very few .yaml file we have there (most of them have .yml extension) + assert len(matches) == 1 + + +@pytest.mark.parametrize( + ("formatter_cls"), + ( + pytest.param(formatters.Formatter, id="Formatter-plain"), + pytest.param(formatters.ParseableFormatter, id="ParseableFormatter-colored"), + pytest.param(formatters.QuietFormatter, id="QuietFormatter-colored"), + pytest.param(formatters.Formatter, id="Formatter-colored"), + ), +) +def test_runner_unicode_format( + default_rules_collection: RulesCollection, + formatter_cls: type[formatters.BaseFormatter[Any]], +) -> None: + """Check that all formatters are unicode-friendly.""" + formatter = formatter_cls(Path.cwd(), display_relative_path=True) + runner = Runner( + Lintable("examples/playbooks/unicode.yml", kind="playbook"), + rules=default_rules_collection, + ) + + matches = runner.run() + + formatter.apply(matches[0]) + + +@pytest.mark.parametrize( + "directory_name", + ( + pytest.param(Path("test/fixtures/verbosity-tests"), id="rel"), + pytest.param(Path("test/fixtures/verbosity-tests").resolve(), id="abs"), + ), +) +def test_runner_with_directory( + default_rules_collection: RulesCollection, + directory_name: Path, +) -> None: + """Check that runner detects a directory as role.""" + runner = Runner(directory_name, rules=default_rules_collection) + + expected = Lintable(name=directory_name, kind="role") + assert expected in runner.lintables + + +def test_files_not_scanned_twice(default_rules_collection: RulesCollection) -> None: + """Ensure that lintables aren't double-checked.""" + checked_files: set[Lintable] = set() + + filename = Path("examples/playbooks/common-include-1.yml").resolve() + runner = Runner( + filename, + rules=default_rules_collection, + verbosity=0, + checked_files=checked_files, + ) + run1 = runner.run() + assert len(runner.checked_files) == 2 + assert len(run1) == 1 + + filename = Path("examples/playbooks/common-include-2.yml").resolve() + runner = Runner( + str(filename), + rules=default_rules_collection, + verbosity=0, + checked_files=checked_files, + ) + run2 = runner.run() + assert len(runner.checked_files) == 3 + # this second run should return 0 because the included filed was already + # processed and added to checked_files, which acts like a bypass list. + assert len(run2) == 0 + + +def test_runner_not_found(default_rules_collection: RulesCollection) -> None: + """Ensure that lintables aren't double-checked.""" + checked_files: set[Lintable] = set() + + filename = Path("this/folder/does/not/exist").resolve() + runner = Runner( + filename, + rules=default_rules_collection, + verbosity=0, + checked_files=checked_files, + ) + result = runner.run() + assert len(runner.checked_files) == 1 + assert len(result) == 1 + assert result[0].tag == "load-failure[not-found]" + + +def test_runner_tmp_file( + tmp_path: Path, + default_rules_collection: RulesCollection, +) -> None: + """Ensure we do not ignore an explicit temporary file from linting.""" + # https://github.com/ansible/ansible-lint/issues/2628 + filename = tmp_path / "playbook.yml" + filename.write_text("---\n") + runner = Runner( + filename, + rules=default_rules_collection, + verbosity=0, + ) + result = runner.run() + assert len(result) == 1 + assert result[0].tag == "syntax-check[empty-playbook]" diff --git a/test/test_schemas.py b/test/test_schemas.py new file mode 100644 index 0000000..6392241 --- /dev/null +++ b/test/test_schemas.py @@ -0,0 +1,109 @@ +"""Test schemas modules.""" +import json +import logging +import subprocess +import sys +import urllib +from pathlib import Path +from time import sleep +from typing import Any +from unittest.mock import DEFAULT, MagicMock, patch + +import pytest +import spdx.config + +from ansiblelint.file_utils import Lintable +from ansiblelint.schemas import __file__ as schema_module +from ansiblelint.schemas.__main__ import refresh_schemas +from ansiblelint.schemas.main import validate_file_schema + +schema_path = Path(schema_module).parent +spdx_config_path = Path(spdx.config.__file__).parent + + +def test_refresh_schemas() -> None: + """Test for schema update skip.""" + # This is written as a single test in order to avoid concurrency issues, + # which caused random issues on CI when the two tests run in parallel + # and or in different order. + assert refresh_schemas(min_age_seconds=3600 * 24 * 365 * 10) == 0 + sleep(1) + # this should disable the cache and force an update + assert refresh_schemas(min_age_seconds=0) == 1 + sleep(1) + # should be cached now + assert refresh_schemas(min_age_seconds=10) == 0 + + +def urlopen_side_effect(*_args: Any, **kwargs: Any) -> DEFAULT: + """Actual test that timeout parameter is defined.""" + assert "timeout" in kwargs + assert kwargs["timeout"] > 0 + return DEFAULT + + +@patch("urllib.request") +def test_requests_uses_timeout(mock_request: MagicMock) -> None: + """Test that schema refresh uses timeout.""" + mock_request.urlopen.side_effect = urlopen_side_effect + refresh_schemas(min_age_seconds=0) + mock_request.urlopen.assert_called() + + +@patch("urllib.request") +def test_request_timeouterror_handling( + mock_request: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that schema refresh can handle time out errors.""" + error_msg = "Simulating handshake operation time out." + mock_request.urlopen.side_effect = urllib.error.URLError(TimeoutError(error_msg)) + with caplog.at_level(logging.DEBUG): + assert refresh_schemas(min_age_seconds=0) == 1 + mock_request.urlopen.assert_called() + assert "Skipped schema refresh due to unexpected exception: " in caplog.text + assert error_msg in caplog.text + + +def test_schema_refresh_cli() -> None: + """Ensure that we test the cli schema refresh command.""" + proc = subprocess.run( + [sys.executable, "-m", "ansiblelint.schemas"], + check=False, + capture_output=True, + text=True, + ) + assert proc.returncode == 0 + + +def test_validate_file_schema() -> None: + """Test file schema validation failure on unknown file kind.""" + lintable = Lintable("foo.bar", kind="") + result = validate_file_schema(lintable) + assert len(result) == 1, result + assert "Unable to find JSON Schema" in result[0] + + +def test_spdx() -> None: + """Test that SPDX license identifiers are in sync.""" + _licenses = spdx_config_path / "licenses.json" + + license_ids = set() + with _licenses.open(encoding="utf-8") as license_fh: + licenses = json.load(license_fh) + for lic in licenses["licenses"]: + if lic.get("isDeprecatedLicenseId"): + continue + license_ids.add(lic["licenseId"]) + + galaxy_json = schema_path / "galaxy.json" + with galaxy_json.open(encoding="utf-8") as f: + schema = json.load(f) + spx_enum = schema["$defs"]["SPDXLicenseEnum"]["enum"] + if set(spx_enum) != license_ids: + with galaxy_json.open("w", encoding="utf-8") as f: + schema["$defs"]["SPDXLicenseEnum"]["enum"] = sorted(license_ids) + json.dump(schema, f, indent=2) + pytest.fail( + "SPDX license list inside galaxy.json JSON Schema file was updated.", + ) diff --git a/test/test_skip_import_playbook.py b/test/test_skip_import_playbook.py new file mode 100644 index 0000000..777fec6 --- /dev/null +++ b/test/test_skip_import_playbook.py @@ -0,0 +1,49 @@ +"""Test related to skipping import_playbook.""" +from pathlib import Path + +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + +IMPORTED_PLAYBOOK = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Success # noqa: no-free-form + ansible.builtin.fail: msg="fail" + when: false +""" + +MAIN_PLAYBOOK = """\ +--- +- name: Fixture + hosts: all + + tasks: + - name: Should be shell # noqa: command-instead-of-shell no-changed-when no-free-form + ansible.builtin.shell: echo lol + +- name: Should not be imported + import_playbook: imported_playbook.yml +""" + + +@pytest.fixture(name="playbook") +def fixture_playbook(tmp_path: Path) -> str: + """Create a reusable per-test playbook.""" + playbook_path = tmp_path / "playbook.yml" + playbook_path.write_text(MAIN_PLAYBOOK) + (tmp_path / "imported_playbook.yml").write_text(IMPORTED_PLAYBOOK) + return str(playbook_path) + + +def test_skip_import_playbook( + default_rules_collection: RulesCollection, + playbook: str, +) -> None: + """Verify that a playbook import is skipped after a failure.""" + runner = Runner(playbook, rules=default_rules_collection) + results = runner.run() + assert len(results) == 0 diff --git a/test/test_skip_inside_yaml.py b/test/test_skip_inside_yaml.py new file mode 100644 index 0000000..363734e --- /dev/null +++ b/test/test_skip_inside_yaml.py @@ -0,0 +1,41 @@ +"""Tests related to use of inline noqa.""" +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner +from ansiblelint.testing import run_ansible_lint + + +def test_role_tasks_with_block(default_rules_collection: RulesCollection) -> None: + """Check that blocks in role tasks can contain skips.""" + results = Runner( + "examples/playbooks/roles/fixture_1", + rules=default_rules_collection, + ).run() + assert len(results) == 4 + for result in results: + assert result.tag == "latest[git]" + + +@pytest.mark.parametrize( + ("lintable", "expected"), + (pytest.param("examples/playbooks/test_skip_inside_yaml.yml", 4, id="yaml"),), +) +def test_inline_skips( + default_rules_collection: RulesCollection, + lintable: str, + expected: int, +) -> None: + """Check that playbooks can contain skips.""" + results = Runner(lintable, rules=default_rules_collection).run() + + assert len(results) == expected + + +def test_role_meta() -> None: + """Test running from inside meta folder.""" + role_path = "examples/roles/meta_noqa" + + result = run_ansible_lint("-v", role_path) + assert len(result.stdout) == 0 + assert result.returncode == 0 diff --git a/test/test_skip_playbook_items.py b/test/test_skip_playbook_items.py new file mode 100644 index 0000000..2861c6a --- /dev/null +++ b/test/test_skip_playbook_items.py @@ -0,0 +1,121 @@ +"""Tests related to use of noqa inside playbooks.""" +import pytest + +from ansiblelint.testing import RunFromText + +PLAYBOOK_PRE_TASKS = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Bad git 1 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 2 + action: ansible.builtin.git a=b c=d + pre_tasks: + - name: Bad git 3 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 4 + action: ansible.builtin.git a=b c=d +""" + +PLAYBOOK_POST_TASKS = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Bad git 1 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 2 + action: ansible.builtin.git a=b c=d + post_tasks: + - name: Bad git 3 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 4 + action: ansible.builtin.git a=b c=d +""" + +PLAYBOOK_HANDLERS = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Bad git 1 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 2 + action: ansible.builtin.git a=b c=d + handlers: + - name: Bad git 3 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 4 + action: ansible.builtin.git a=b c=d +""" + +PLAYBOOK_TWO_PLAYS = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Bad git 1 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 2 + action: ansible.builtin.git a=b c=d + +- name: Fixture 2 + hosts: all + tasks: + - name: Bad git 3 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 4 + action: ansible.builtin.git a=b c=d +""" + +PLAYBOOK_WITH_BLOCK = """\ +--- +- name: Fixture + hosts: all + tasks: + - name: Bad git 1 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 2 + action: ansible.builtin.git a=b c=d + - name: Block with rescue and always section + block: + - name: Bad git 3 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 4 + action: ansible.builtin.git a=b c=d + rescue: + - name: Bad git 5 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 6 + action: ansible.builtin.git a=b c=d + always: + - name: Bad git 7 # noqa: latest[git] + action: ansible.builtin.git a=b c=d + - name: Bad git 8 + action: ansible.builtin.git a=b c=d +""" + + +@pytest.mark.parametrize( + ("playbook", "length"), + ( + pytest.param(PLAYBOOK_PRE_TASKS, 6, id="PRE_TASKS"), + pytest.param(PLAYBOOK_POST_TASKS, 6, id="POST_TASKS"), + pytest.param(PLAYBOOK_HANDLERS, 6, id="HANDLERS"), + pytest.param(PLAYBOOK_TWO_PLAYS, 6, id="TWO_PLAYS"), + pytest.param(PLAYBOOK_WITH_BLOCK, 12, id="WITH_BLOCK"), + ), +) +def test_pre_tasks( + default_text_runner: RunFromText, + playbook: str, + length: int, +) -> None: + """Check that skipping is possible in different playbook parts.""" + # When + results = default_text_runner.run_playbook(playbook) + + # Then + assert len(results) == length diff --git a/test/test_skiputils.py b/test/test_skiputils.py new file mode 100644 index 0000000..7e736e7 --- /dev/null +++ b/test/test_skiputils.py @@ -0,0 +1,252 @@ +"""Validate ansiblelint.skip_utils.""" +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +from ansiblelint.constants import SKIPPED_RULES_KEY +from ansiblelint.file_utils import Lintable +from ansiblelint.runner import Runner +from ansiblelint.skip_utils import ( + append_skipped_rules, + get_rule_skips_from_line, + is_nested_task, +) + +if TYPE_CHECKING: + from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject + + from ansiblelint.rules import RulesCollection + from ansiblelint.testing import RunFromText + +PLAYBOOK_WITH_NOQA = """\ +--- +- name: Fixture + hosts: all + vars: + SOME_VAR_NOQA: "Foo" # noqa: var-naming + SOME_VAR: "Bar" + tasks: + - name: "Set the SOME_OTHER_VAR" + ansible.builtin.set_fact: + SOME_OTHER_VAR_NOQA: "Baz" # noqa: var-naming + SOME_OTHER_VAR: "Bat" +""" + + +@pytest.mark.parametrize( + ("line", "expected"), + ( + ("foo # noqa: bar", "bar"), + ("foo # noqa bar", "bar"), + ), +) +def test_get_rule_skips_from_line(line: str, expected: str) -> None: + """Validate get_rule_skips_from_line.""" + v = get_rule_skips_from_line(line, lintable=Lintable("")) + assert v == [expected] + + +def test_playbook_noqa(default_text_runner: RunFromText) -> None: + """Check that noqa is properly taken into account on vars and tasks.""" + results = default_text_runner.run_playbook(PLAYBOOK_WITH_NOQA) + # Should raise error at "SOME_VAR". + assert len(results) == 1 + + +def test_playbook_noqa2(default_text_runner: RunFromText) -> None: + """Check that noqa is properly taken into account on vars and tasks.""" + results = default_text_runner.run_playbook(PLAYBOOK_WITH_NOQA, "test") + # Should raise error at "SOME_VAR". + assert len(results) == 1 + + +@pytest.mark.parametrize( + ("lintable", "yaml", "expected_form"), + ( + pytest.param( + Lintable("examples/playbooks/noqa.yml", kind="playbook"), + [ + { + "hosts": "localhost", + "tasks": [ + { + "name": "This would typically fire latest[git] and partial-become", + "become_user": "alice", + "git": "src=/path/to/git/repo dest=checkout", + "__line__": 4, + "__file__": Path("examples/playbooks/noqa.yml"), + }, + ], + "__line__": 2, + "__file__": Path("examples/playbooks/noqa.yml"), + }, + ], + [ + { + "hosts": "localhost", + "tasks": [ + { + "name": "This would typically fire latest[git] and partial-become", + "become_user": "alice", + "git": "src=/path/to/git/repo dest=checkout", + "__line__": 4, + "__file__": Path("examples/playbooks/noqa.yml"), + SKIPPED_RULES_KEY: ["latest[git]", "partial-become"], + }, + ], + "__line__": 2, + "__file__": Path("examples/playbooks/noqa.yml"), + }, + ], + ), + pytest.param( + Lintable("examples/playbooks/noqa-nested.yml", kind="playbook"), + [ + { + "hosts": "localhost", + "tasks": [ + { + "name": "Example of multi-level block", + "block": [ + { + "name": "2nd level", + "block": [ + { + "ansible.builtin.debug": { + "msg": "Test unnamed task in block", + "__line__": 9, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + }, + "__line__": 8, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + }, + ], + "__line__": 6, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + }, + ], + "__line__": 4, + "__file__": Path("examples/playbooks/noqa-nested.yml"), + }, + ], + "__line__": 2, + "__file__": Path("examples/playbooks/noqa-nested.yml"), + }, + ], + [ + { + "hosts": "localhost", + "tasks": [ + { + "name": "Example of multi-level block", + "block": [ + { + "name": "2nd level", + "block": [ + { + "ansible.builtin.debug": { + "msg": "Test unnamed task in block", + "__line__": 9, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + }, + "__line__": 8, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + SKIPPED_RULES_KEY: ["name[missing]"], + }, + ], + "__line__": 6, + "__file__": Path( + "examples/playbooks/noqa-nested.yml", + ), + SKIPPED_RULES_KEY: ["name[missing]"], + }, + ], + "__line__": 4, + "__file__": Path("examples/playbooks/noqa-nested.yml"), + SKIPPED_RULES_KEY: ["name[missing]"], + }, + ], + "__line__": 2, + "__file__": Path("examples/playbooks/noqa-nested.yml"), + }, + ], + ), + ), +) +def test_append_skipped_rules( + lintable: Lintable, + yaml: AnsibleBaseYAMLObject, + expected_form: AnsibleBaseYAMLObject, +) -> None: + """Check that it appends skipped_rules properly.""" + assert append_skipped_rules(yaml, lintable) == expected_form + + +@pytest.mark.parametrize( + ("task", "expected"), + ( + pytest.param( + { + "name": "ensure apache is at the latest version", + "yum": {"name": "httpd", "state": "latest"}, + }, + False, + ), + pytest.param( + { + "name": "Attempt and graceful roll back", + "block": [ + { + "name": "Force a failure", + "ansible.builtin.command": "/bin/false", + }, + ], + "rescue": [ + { + "name": "Force a failure in middle of recovery!", + "ansible.builtin.command": "/bin/false", + }, + ], + "always": [ + { + "name": "Always do this", + "ansible.builtin.debug": {"msg": "This always executes"}, + }, + ], + }, + True, + ), + ), +) +def test_is_nested_task(task: dict[str, Any], expected: bool) -> None: + """Test is_nested_task() returns expected bool.""" + assert is_nested_task(task) == expected + + +def test_capture_warning_outdated_tag( + default_rules_collection: RulesCollection, +) -> None: + """Test that exclude paths do work.""" + runner = Runner( + "examples/playbooks/capture-warning.yml", + rules=default_rules_collection, + ) + + matches = runner.run() + assert len(matches) == 1 + assert matches[0].rule.id == "warning" + assert matches[0].tag == "warning[outdated-tag]" + assert matches[0].lineno == 8 diff --git a/test/test_strict.py b/test/test_strict.py new file mode 100644 index 0000000..ba93d7c --- /dev/null +++ b/test/test_strict.py @@ -0,0 +1,30 @@ +"""Test strict mode.""" +import os + +import pytest + +from ansiblelint.testing import run_ansible_lint + + +@pytest.mark.parametrize( + ("strict", "returncode", "message"), + ( + pytest.param(True, 2, "Failed", id="on"), + pytest.param(False, 0, "Passed", id="off"), + ), +) +def test_strict(strict: bool, returncode: int, message: str) -> None: + """Test running from inside meta folder.""" + args = ["examples/playbooks/strict-mode.yml"] + env = os.environ.copy() + env["NO_COLOR"] = "1" + if strict: + args.insert(0, "--strict") + result = run_ansible_lint(*args, env=env) + assert result.returncode == returncode + assert "args[module]" in result.stdout + for summary_line in result.stderr.splitlines(): + if summary_line.startswith(message): + break + else: + pytest.fail(f"Failed to find {message} inside stderr output") diff --git a/test/test_task_includes.py b/test/test_task_includes.py new file mode 100644 index 0000000..3b02d00 --- /dev/null +++ b/test/test_task_includes.py @@ -0,0 +1,47 @@ +"""Tests related to task inclusions.""" +import pytest + +from ansiblelint.file_utils import Lintable +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +@pytest.mark.parametrize( + ("filename", "file_count", "match_count"), + ( + pytest.param("examples/playbooks/blockincludes.yml", 4, 3, id="blockincludes"), + pytest.param( + "examples/playbooks/blockincludes2.yml", + 4, + 3, + id="blockincludes2", + ), + pytest.param("examples/playbooks/taskincludes.yml", 3, 6, id="taskincludes"), + pytest.param("examples/playbooks/taskimports.yml", 5, 3, id="taskimports"), + pytest.param( + "examples/playbooks/include-in-block.yml", + 3, + 1, + id="include-in-block", + ), + pytest.param( + "examples/playbooks/include-import-tasks-in-role.yml", + 4, + 2, + id="role_with_task_inclusions", + ), + ), +) +def test_included_tasks( + default_rules_collection: RulesCollection, + filename: str, + file_count: int, + match_count: int, +) -> None: + """Check if number of loaded files is correct.""" + lintable = Lintable(filename) + default_rules_collection.options.enable_list = ["name[prefix]"] + runner = Runner(lintable, rules=default_rules_collection) + result = runner.run() + assert len(runner.lintables) == file_count + assert len(result) == match_count diff --git a/test/test_text.py b/test/test_text.py new file mode 100644 index 0000000..fa91fee --- /dev/null +++ b/test/test_text.py @@ -0,0 +1,75 @@ +"""Tests for text module.""" +from typing import Any + +import pytest + +from ansiblelint.text import has_glob, has_jinja, strip_ansi_escape, toidentifier + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + pytest.param("\x1b[1;31mHello", "Hello", id="0"), + pytest.param("\x1b[2;37;41mExample_file.zip", "Example_file.zip", id="1"), + pytest.param(b"ansible-lint", "ansible-lint", id="2"), + ), +) +def test_strip_ansi_escape(value: Any, expected: str) -> None: + """Tests for strip_ansi_escape().""" + assert strip_ansi_escape(value) == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + pytest.param("foo-bar", "foo_bar", id="0"), + pytest.param("foo--bar", "foo_bar", id="1"), + ), +) +def test_toidentifier(value: Any, expected: str) -> None: + """Tests for toidentifier().""" + assert toidentifier(value) == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + (pytest.param("example_test.zip", "Unable to convert role name", id="0"),), +) +def test_toidentifier_fail(value: Any, expected: str) -> None: + """Tests for toidentifier().""" + with pytest.raises(RuntimeError) as err: + toidentifier(value) + assert str(err.value).find(expected) > -1 + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + pytest.param("", False, id="0"), + pytest.param("{{ }}", True, id="1"), + pytest.param("foo {# #} bar", True, id="2"), + pytest.param("foo \n{% %} bar", True, id="3"), + pytest.param(None, False, id="4"), + pytest.param(42, False, id="5"), + pytest.param(True, False, id="6"), + ), +) +def test_has_jinja(value: Any, expected: bool) -> None: + """Tests for has_jinja().""" + assert has_jinja(value) == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + pytest.param("", False, id="0"), + pytest.param("*", True, id="1"), + pytest.param("foo.*", True, id="2"), + pytest.param(None, False, id="4"), + pytest.param(42, False, id="5"), + pytest.param(True, False, id="6"), + ), +) +def test_has_glob(value: Any, expected: bool) -> None: + """Tests for has_jinja().""" + assert has_glob(value) == expected diff --git a/test/test_transform_mixin.py b/test/test_transform_mixin.py new file mode 100644 index 0000000..d639bff --- /dev/null +++ b/test/test_transform_mixin.py @@ -0,0 +1,134 @@ +"""Tests for TransformMixin.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from ansiblelint.rules import TransformMixin + +if TYPE_CHECKING: + from collections.abc import MutableMapping, MutableSequence + from typing import Any + + +DUMMY_MAP: dict[str, Any] = { + "foo": "text", + "bar": {"some": "text2"}, + "fruits": ["apple", "orange"], + "answer": [{"forty-two": ["life", "universe", "everything"]}], +} +DUMMY_LIST: list[dict[str, Any]] = [ + {"foo": "text"}, + {"bar": {"some": "text2"}, "fruits": ["apple", "orange"]}, + {"answer": [{"forty-two": ["life", "universe", "everything"]}]}, +] + + +@pytest.mark.parametrize( + ("yaml_path", "data", "expected_error"), + ( + ([0], DUMMY_MAP, KeyError), + (["bar", 0], DUMMY_MAP, KeyError), + (["fruits", 100], DUMMY_MAP, IndexError), + (["answer", 1], DUMMY_MAP, IndexError), + (["answer", 0, 42], DUMMY_MAP, KeyError), + (["answer", 0, "42"], DUMMY_MAP, KeyError), + ([100], DUMMY_LIST, IndexError), + ([0, 0], DUMMY_LIST, KeyError), + ([0, "wrong key"], DUMMY_LIST, KeyError), + ([1, "bar", "wrong key"], DUMMY_LIST, KeyError), + ([1, "fruits", "index should be int"], DUMMY_LIST, TypeError), + ([1, "fruits", 100], DUMMY_LIST, IndexError), + ), +) +def test_seek_with_bad_path( + yaml_path: list[int | str], + data: MutableMapping[str, Any] | MutableSequence[Any] | str, + expected_error: type[Exception], +) -> None: + """Verify that TransformMixin.seek() propagates errors.""" + with pytest.raises(expected_error): + TransformMixin.seek(yaml_path, data) + + +@pytest.mark.parametrize( + ("yaml_path", "data", "expected"), + ( + ([], DUMMY_MAP, DUMMY_MAP), + (["foo"], DUMMY_MAP, DUMMY_MAP["foo"]), + (["bar"], DUMMY_MAP, DUMMY_MAP["bar"]), + (["bar", "some"], DUMMY_MAP, DUMMY_MAP["bar"]["some"]), + (["fruits"], DUMMY_MAP, DUMMY_MAP["fruits"]), + (["fruits", 0], DUMMY_MAP, DUMMY_MAP["fruits"][0]), + (["fruits", 1], DUMMY_MAP, DUMMY_MAP["fruits"][1]), + (["answer"], DUMMY_MAP, DUMMY_MAP["answer"]), + (["answer", 0], DUMMY_MAP, DUMMY_MAP["answer"][0]), + (["answer", 0, "forty-two"], DUMMY_MAP, DUMMY_MAP["answer"][0]["forty-two"]), + ( + ["answer", 0, "forty-two", 0], + DUMMY_MAP, + DUMMY_MAP["answer"][0]["forty-two"][0], + ), + ( + ["answer", 0, "forty-two", 1], + DUMMY_MAP, + DUMMY_MAP["answer"][0]["forty-two"][1], + ), + ( + ["answer", 0, "forty-two", 2], + DUMMY_MAP, + DUMMY_MAP["answer"][0]["forty-two"][2], + ), + ([], DUMMY_LIST, DUMMY_LIST), + ([0], DUMMY_LIST, DUMMY_LIST[0]), + ([0, "foo"], DUMMY_LIST, DUMMY_LIST[0]["foo"]), + ([1], DUMMY_LIST, DUMMY_LIST[1]), + ([1, "bar"], DUMMY_LIST, DUMMY_LIST[1]["bar"]), + ([1, "bar", "some"], DUMMY_LIST, DUMMY_LIST[1]["bar"]["some"]), + ([1, "fruits"], DUMMY_LIST, DUMMY_LIST[1]["fruits"]), + ([1, "fruits", 0], DUMMY_LIST, DUMMY_LIST[1]["fruits"][0]), + ([1, "fruits", 1], DUMMY_LIST, DUMMY_LIST[1]["fruits"][1]), + ([2], DUMMY_LIST, DUMMY_LIST[2]), + ([2, "answer"], DUMMY_LIST, DUMMY_LIST[2]["answer"]), + ([2, "answer", 0], DUMMY_LIST, DUMMY_LIST[2]["answer"][0]), + ( + [2, "answer", 0, "forty-two"], + DUMMY_LIST, + DUMMY_LIST[2]["answer"][0]["forty-two"], + ), + ( + [2, "answer", 0, "forty-two", 0], + DUMMY_LIST, + DUMMY_LIST[2]["answer"][0]["forty-two"][0], + ), + ( + [2, "answer", 0, "forty-two", 1], + DUMMY_LIST, + DUMMY_LIST[2]["answer"][0]["forty-two"][1], + ), + ( + [2, "answer", 0, "forty-two", 2], + DUMMY_LIST, + DUMMY_LIST[2]["answer"][0]["forty-two"][2], + ), + ( + [], + "this is a string that should be returned as is, ignoring path.", + "this is a string that should be returned as is, ignoring path.", + ), + ( + [2, "answer", 0, "forty-two", 2], + "this is a string that should be returned as is, ignoring path.", + "this is a string that should be returned as is, ignoring path.", + ), + ), +) +def test_seek( + yaml_path: list[int | str], + data: MutableMapping[str, Any] | MutableSequence[Any] | str, + expected: Any, +) -> None: + """Ensure TransformMixin.seek() retrieves the correct data.""" + actual = TransformMixin.seek(yaml_path, data) + assert actual == expected diff --git a/test/test_transformer.py b/test/test_transformer.py new file mode 100644 index 0000000..78dd121 --- /dev/null +++ b/test/test_transformer.py @@ -0,0 +1,175 @@ +"""Tests for Transformer.""" +from __future__ import annotations + +import os +import shutil +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +# noinspection PyProtectedMember +from ansiblelint.runner import LintResult, _get_matches +from ansiblelint.transformer import Transformer + +if TYPE_CHECKING: + from argparse import Namespace + from collections.abc import Iterator + + from ansiblelint.config import Options + from ansiblelint.rules import RulesCollection + + +@pytest.fixture(name="copy_examples_dir") +def fixture_copy_examples_dir( + tmp_path: Path, + config_options: Namespace, +) -> Iterator[tuple[Path, Path]]: + """Fixture that copies the examples/ dir into a tmpdir.""" + examples_dir = Path("examples") + + shutil.copytree(examples_dir, tmp_path / "examples") + old_cwd = Path.cwd() + try: + os.chdir(tmp_path) + config_options.cwd = tmp_path + yield old_cwd, tmp_path + finally: + os.chdir(old_cwd) + + +@pytest.fixture(name="runner_result") +def fixture_runner_result( + config_options: Options, + default_rules_collection: RulesCollection, + playbook: str, +) -> LintResult: + """Fixture that runs the Runner to populate a LintResult for a given file.""" + config_options.lintables = [playbook] + result = _get_matches(rules=default_rules_collection, options=config_options) + return result + + +@pytest.mark.parametrize( + ("playbook", "matches_count", "transformed"), + ( + # reuse TestRunner::test_runner test cases to ensure transformer does not mangle matches + pytest.param( + "examples/playbooks/nomatchestest.yml", + 0, + False, + id="nomatchestest", + ), + pytest.param("examples/playbooks/unicode.yml", 1, False, id="unicode"), + pytest.param( + "examples/playbooks/lots_of_warnings.yml", + 992, + False, + id="lots_of_warnings", + ), + pytest.param("examples/playbooks/become.yml", 0, False, id="become"), + pytest.param( + "examples/playbooks/contains_secrets.yml", + 0, + False, + id="contains_secrets", + ), + pytest.param( + "examples/playbooks/vars/empty_vars.yml", + 0, + False, + id="empty_vars", + ), + pytest.param("examples/playbooks/vars/strings.yml", 0, True, id="strings"), + pytest.param("examples/playbooks/vars/empty.yml", 1, False, id="empty"), + pytest.param("examples/playbooks/name-case.yml", 1, True, id="name_case"), + pytest.param("examples/playbooks/fqcn.yml", 3, True, id="fqcn"), + ), +) +def test_transformer( # pylint: disable=too-many-arguments, too-many-locals + config_options: Options, + copy_examples_dir: tuple[Path, Path], + playbook: str, + runner_result: LintResult, + transformed: bool, + matches_count: int, +) -> None: + """Test that transformer can go through any corner cases. + + Based on TestRunner::test_runner + """ + config_options.write_list = ["all"] + transformer = Transformer(result=runner_result, options=config_options) + transformer.run() + + matches = runner_result.matches + assert len(matches) == matches_count + + orig_dir, tmp_dir = copy_examples_dir + orig_playbook = orig_dir / playbook + expected_playbook = orig_dir / playbook.replace(".yml", ".transformed.yml") + transformed_playbook = tmp_dir / playbook + + orig_playbook_content = orig_playbook.read_text() + expected_playbook_content = expected_playbook.read_text() + transformed_playbook_content = transformed_playbook.read_text() + + if transformed: + assert orig_playbook_content != transformed_playbook_content + else: + assert orig_playbook_content == transformed_playbook_content + + assert transformed_playbook_content == expected_playbook_content + + +@pytest.mark.parametrize( + ("write_list", "expected"), + ( + # 1 item + (["all"], {"all"}), + (["none"], {"none"}), + (["rule-id"], {"rule-id"}), + # 2 items + (["all", "all"], {"all"}), + (["all", "none"], {"none"}), + (["all", "rule-id"], {"all"}), + (["none", "all"], {"all"}), + (["none", "none"], {"none"}), + (["none", "rule-id"], {"rule-id"}), + (["rule-id", "all"], {"all"}), + (["rule-id", "none"], {"none"}), + (["rule-id", "rule-id"], {"rule-id"}), + # 3 items + (["all", "all", "all"], {"all"}), + (["all", "all", "none"], {"none"}), + (["all", "all", "rule-id"], {"all"}), + (["all", "none", "all"], {"all"}), + (["all", "none", "none"], {"none"}), + (["all", "none", "rule-id"], {"rule-id"}), + (["all", "rule-id", "all"], {"all"}), + (["all", "rule-id", "none"], {"none"}), + (["all", "rule-id", "rule-id"], {"all"}), + (["none", "all", "all"], {"all"}), + (["none", "all", "none"], {"none"}), + (["none", "all", "rule-id"], {"all"}), + (["none", "none", "all"], {"all"}), + (["none", "none", "none"], {"none"}), + (["none", "none", "rule-id"], {"rule-id"}), + (["none", "rule-id", "all"], {"all"}), + (["none", "rule-id", "none"], {"none"}), + (["none", "rule-id", "rule-id"], {"rule-id"}), + (["rule-id", "all", "all"], {"all"}), + (["rule-id", "all", "none"], {"none"}), + (["rule-id", "all", "rule-id"], {"all"}), + (["rule-id", "none", "all"], {"all"}), + (["rule-id", "none", "none"], {"none"}), + (["rule-id", "none", "rule-id"], {"rule-id"}), + (["rule-id", "rule-id", "all"], {"all"}), + (["rule-id", "rule-id", "none"], {"none"}), + (["rule-id", "rule-id", "rule-id"], {"rule-id"}), + ), +) +def test_effective_write_set(write_list: list[str], expected: set[str]) -> None: + """Make sure effective_write_set handles all/none keywords correctly.""" + actual = Transformer.effective_write_set(write_list) + assert actual == expected diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000..1b9a2dc --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,449 @@ +# Copyright (c) 2013-2014 Will Thames <will@thames.id.au> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Tests for generic utility functions.""" +from __future__ import annotations + +import logging +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest +from ansible.utils.sentinel import Sentinel +from ansible_compat.runtime import Runtime + +from ansiblelint import cli, constants, utils +from ansiblelint.__main__ import initialize_logger +from ansiblelint.cli import get_rules_dirs +from ansiblelint.constants import RC +from ansiblelint.file_utils import Lintable, cwd +from ansiblelint.runner import Runner + +if TYPE_CHECKING: + from collections.abc import Sequence + + from _pytest.capture import CaptureFixture + from _pytest.logging import LogCaptureFixture + from _pytest.monkeypatch import MonkeyPatch + + from ansiblelint.rules import RulesCollection + + +runtime = Runtime(require_module=True) + + +@pytest.mark.parametrize( + ("string", "expected_cmd", "expected_args", "expected_kwargs"), + ( + pytest.param("", "", [], {}, id="blank"), + pytest.param("vars:", "vars", [], {}, id="single_word"), + pytest.param("hello: a=1", "hello", [], {"a": "1"}, id="string_module_and_arg"), + pytest.param("action: hello a=1", "hello", [], {"a": "1"}, id="strips_action"), + pytest.param( + "action: whatever bobbins x=y z=x c=3", + "whatever", + ["bobbins", "x=y", "z=x", "c=3"], + {}, + id="more_than_one_arg", + ), + pytest.param( + "action: command chdir=wxy creates=zyx tar xzf zyx.tgz", + "command", + ["tar", "xzf", "zyx.tgz"], + {"chdir": "wxy", "creates": "zyx"}, + id="command_with_args", + ), + ), +) +def test_tokenize( + string: str, + expected_cmd: str, + expected_args: Sequence[str], + expected_kwargs: dict[str, Any], +) -> None: + """Test that tokenize works for different input types.""" + (cmd, args, kwargs) = utils.tokenize(string) + assert cmd == expected_cmd + assert args == expected_args + assert kwargs == expected_kwargs + + +@pytest.mark.parametrize( + ("reference_form", "alternate_forms"), + ( + pytest.param( + {"name": "hello", "action": "command chdir=abc echo hello world"}, + ({"name": "hello", "command": "chdir=abc echo hello world"},), + id="simple_command", + ), + pytest.param( + {"git": {"version": "abc"}, "args": {"repo": "blah", "dest": "xyz"}}, + ( + {"git": {"version": "abc", "repo": "blah", "dest": "xyz"}}, + {"git": "version=abc repo=blah dest=xyz"}, + { + "git": None, + "args": {"repo": "blah", "dest": "xyz", "version": "abc"}, + }, + ), + id="args", + ), + ), +) +def test_normalize( + reference_form: dict[str, Any], + alternate_forms: tuple[dict[str, Any]], +) -> None: + """Test that tasks specified differently are normalized same way.""" + normal_form = utils.normalize_task(reference_form, "tasks.yml") + + for form in alternate_forms: + assert normal_form == utils.normalize_task(form, "tasks.yml") + + +def test_normalize_complex_command() -> None: + """Test that tasks specified differently are normalized same way.""" + task1 = { + "name": "hello", + "action": {"module": "pip", "name": "df", "editable": "false"}, + } + task2 = {"name": "hello", "pip": {"name": "df", "editable": "false"}} + task3 = {"name": "hello", "pip": "name=df editable=false"} + task4 = {"name": "hello", "action": "pip name=df editable=false"} + assert utils.normalize_task(task1, "tasks.yml") == utils.normalize_task( + task2, + "tasks.yml", + ) + assert utils.normalize_task(task2, "tasks.yml") == utils.normalize_task( + task3, + "tasks.yml", + ) + assert utils.normalize_task(task3, "tasks.yml") == utils.normalize_task( + task4, + "tasks.yml", + ) + + +@pytest.mark.parametrize( + ("task", "expected_form"), + ( + pytest.param( + { + "name": "ensure apache is at the latest version", + "yum": {"name": "httpd", "state": "latest"}, + }, + { + "delegate_to": Sentinel, + "name": "ensure apache is at the latest version", + "action": { + "__ansible_module__": "yum", + "__ansible_module_original__": "yum", + "name": "httpd", + "state": "latest", + }, + }, + id="0", + ), + pytest.param( + { + "name": "Attempt and graceful roll back", + "block": [ + { + "name": "Install httpd and memcached", + "ansible.builtin.yum": ["httpd", "memcached"], + "state": "present", + }, + ], + }, + { + "name": "Attempt and graceful roll back", + "block": [ + { + "name": "Install httpd and memcached", + "ansible.builtin.yum": ["httpd", "memcached"], + "state": "present", + }, + ], + "action": { + "__ansible_module__": "block/always/rescue", + "__ansible_module_original__": "block/always/rescue", + }, + }, + id="1", + ), + ), +) +def test_normalize_task_v2(task: dict[str, Any], expected_form: dict[str, Any]) -> None: + """Check that it normalizes task and returns the expected form.""" + assert utils.normalize_task_v2(task) == expected_form + + +def test_extract_from_list() -> None: + """Check that tasks get extracted from blocks if present.""" + block = { + "block": [{"tasks": {"name": "hello", "command": "whoami"}}], + "test_none": None, + "test_string": "foo", + } + blocks = [block] + + test_list = utils.extract_from_list(blocks, ["block"]) + test_none = utils.extract_from_list(blocks, ["test_none"]) + + assert list(block["block"]) == test_list # type: ignore[arg-type] + assert not test_none + with pytest.raises(RuntimeError): + utils.extract_from_list(blocks, ["test_string"]) + + +def test_extract_from_list_recursive() -> None: + """Check that tasks get extracted from blocks if present.""" + block = { + "block": [{"block": [{"name": "hello", "command": "whoami"}]}], + } + blocks = [block] + + test_list = utils.extract_from_list(blocks, ["block"]) + assert list(block["block"]) == test_list + + test_list_recursive = utils.extract_from_list(blocks, ["block"], recursive=True) + assert block["block"] + block["block"][0]["block"] == test_list_recursive + + +@pytest.mark.parametrize( + ("template", "output"), + ( + pytest.param("{{ playbook_dir }}", "/a/b/c", id="simple"), + pytest.param( + "{{ 'hello' | doesnotexist }}", + "hello", # newer implementation ignores unknown filters + id="unknown_filter", + ), + pytest.param( + "{{ hello | to_json }}", + "{{ hello | to_json }}", + id="to_json_filter_on_undefined_variable", + ), + pytest.param( + "{{ hello | to_nice_yaml }}", + "{{ hello | to_nice_yaml }}", + id="to_nice_yaml_filter_on_undefined_variable", + ), + ), +) +def test_template(template: str, output: str) -> None: + """Verify that resolvable template vars and filters get rendered.""" + result = utils.template( + basedir=Path("/base/dir"), + value=template, + variables={"playbook_dir": "/a/b/c"}, + fail_on_error=False, + ) + assert result == output + + +def test_task_to_str_unicode() -> None: + """Ensure that extracting messages from tasks preserves Unicode.""" + task = {"fail": {"msg": "unicode é ô à "}} + result = utils.task_to_str(utils.normalize_task(task, "filename.yml")) + assert result == "fail msg=unicode é ô à " + + +def test_logger_debug(caplog: LogCaptureFixture) -> None: + """Test that the double verbosity arg causes logger to be DEBUG.""" + options = cli.get_config(["-vv"]) + initialize_logger(options.verbosity) + + expected_info = ( + "ansiblelint.__main__", + logging.DEBUG, + "Logging initialized to level 10", + ) + + assert expected_info in caplog.record_tuples + + +def test_cli_auto_detect(capfd: CaptureFixture[str]) -> None: + """Test that run without arguments it will detect and lint the entire repository.""" + cmd = [ + sys.executable, + "-m", + "ansiblelint", + "-x", + "schema", # exclude schema as our test file would fail it + "-v", + "-p", + "--nocolor", + ] + result = subprocess.run(cmd, check=False).returncode + + # We de expect to fail on our own repo due to test examples we have + assert result == RC.VIOLATIONS_FOUND + + out, err = capfd.readouterr() + + # An expected rule match from our examples + assert ( + "examples/playbooks/empty_playbook.yml:1:1: " + "syntax-check[empty-playbook]: Empty playbook, nothing to do" in out + ) + # assures that our ansible-lint config exclude was effective in excluding github files + assert "Identified: .github/" not in out + # assures that we can parse playbooks as playbooks + assert "Identified: test/test/always-run-success.yml" not in err + assert ( + "Executing syntax check on playbook examples/playbooks/mocked_dependency.yml" + in err + ) + + +def test_is_playbook() -> None: + """Verify that we can detect a playbook as a playbook.""" + assert utils.is_playbook("examples/playbooks/always-run-success.yml") + + +@pytest.mark.parametrize( + "exclude", + (pytest.param("foo", id="1"), pytest.param("foo/", id="2")), +) +def test_auto_detect_exclude(tmp_path: Path, exclude: str) -> None: + """Verify that exclude option can be used to narrow down detection.""" + with cwd(tmp_path): + subprocess.check_output( + "git init", + stderr=subprocess.STDOUT, + text=True, + shell=True, + cwd=tmp_path, + ) + (tmp_path / "foo").mkdir() + (tmp_path / "bar").mkdir() + (tmp_path / "foo" / "playbook.yml").touch() + (tmp_path / "bar" / "playbook.yml").touch() + + options = cli.get_config(["--exclude", exclude]) + options.cwd = tmp_path + result = utils.get_lintables(options) + assert result == [Lintable("bar/playbook.yml", kind="playbook")] + + # now we also test with .gitignore exclude approach + (tmp_path / ".gitignore").write_text(f".gitignore\n{exclude}\n") + options = cli.get_config([]) + options.cwd = tmp_path + result = utils.get_lintables(options) + assert result == [Lintable("bar/playbook.yml", kind="playbook")] + + +_DEFAULT_RULEDIRS = [constants.DEFAULT_RULESDIR] +_CUSTOM_RULESDIR = Path(__file__).parent / "custom_rules" +_CUSTOM_RULEDIRS = [ + _CUSTOM_RULESDIR / "example_inc", + _CUSTOM_RULESDIR / "example_com", +] + + +@pytest.mark.parametrize( + ("user_ruledirs", "use_default", "expected"), + ( + ([], True, _DEFAULT_RULEDIRS), + ([], False, _DEFAULT_RULEDIRS), + (_CUSTOM_RULEDIRS, True, _CUSTOM_RULEDIRS + _DEFAULT_RULEDIRS), + (_CUSTOM_RULEDIRS, False, _CUSTOM_RULEDIRS), + ), +) +def test_get_rules_dirs( + user_ruledirs: list[Path], + use_default: bool, + expected: list[Path], +) -> None: + """Test it returns expected dir lists.""" + assert get_rules_dirs(user_ruledirs, use_default=use_default) == expected + + +@pytest.mark.parametrize( + ("user_ruledirs", "use_default", "expected"), + ( + ([], True, sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS), + ([], False, sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS), + ( + _CUSTOM_RULEDIRS, + True, + _CUSTOM_RULEDIRS + sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS, + ), + (_CUSTOM_RULEDIRS, False, _CUSTOM_RULEDIRS), + ), +) +def test_get_rules_dirs_with_custom_rules( + user_ruledirs: list[Path], + use_default: bool, + expected: list[Path], + monkeypatch: MonkeyPatch, +) -> None: + """Test it returns expected dir lists when custom rules exist.""" + monkeypatch.setenv(constants.CUSTOM_RULESDIR_ENVVAR, str(_CUSTOM_RULESDIR)) + assert get_rules_dirs(user_ruledirs, use_default=use_default) == expected + + +def test_find_children(default_rules_collection: RulesCollection) -> None: + """Verify correct function of find_children().""" + Runner( + rules=default_rules_collection, + ).find_children(Lintable("examples/playbooks/find_children.yml")) + + +def test_find_children_in_task(default_rules_collection: RulesCollection) -> None: + """Verify correct function of find_children() in tasks.""" + Runner( + Lintable("examples/playbooks/tasks/bug-2875.yml"), + rules=default_rules_collection, + ).run() + + +@pytest.mark.parametrize( + ("file", "names", "positions"), + ( + pytest.param( + "examples/playbooks/task_in_list-0.yml", + ["A", "B", "C", "D", "E", "F", "G"], + [ + ".[0].tasks[0]", + ".[0].tasks[1]", + ".[0].pre_tasks[0]", + ".[0].post_tasks[0]", + ".[0].post_tasks[0].block[0]", + ".[0].post_tasks[0].rescue[0]", + ".[0].post_tasks[0].always[0]", + ], + id="0", + ), + ), +) +def test_task_in_list(file: str, names: list[str], positions: list[str]) -> None: + """Check that tasks get extracted from blocks if present.""" + lintable = Lintable(file) + assert lintable.kind + tasks = list( + utils.task_in_list(data=lintable.data, file=lintable, kind=lintable.kind), + ) + assert len(tasks) == len(names) + for index, task in enumerate(tasks): + assert task.name == names[index] + assert task.position == positions[index] diff --git a/test/test_verbosity.py b/test/test_verbosity.py new file mode 100644 index 0000000..d3ddb3c --- /dev/null +++ b/test/test_verbosity.py @@ -0,0 +1,90 @@ +"""Tests related to our logging/verbosity setup.""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ansiblelint.testing import run_ansible_lint + + +# substrs is a list of tuples, where: +# component 1 is the substring in question +# component 2 is whether or not to invert ("NOT") the match +@pytest.mark.parametrize( + ("verbosity", "substrs"), + ( + pytest.param( + "", + [ + ("WARNING Listing 1 violation(s) that are fatal", False), + ("DEBUG ", True), + ("INFO ", True), + ], + id="default", + ), + pytest.param( + "-q", + [ + ("WARNING ", True), + ("DEBUG ", True), + ("INFO ", True), + ], + id="q", + ), + pytest.param( + "-qq", + [ + ("WARNING ", True), + ("DEBUG ", True), + ("INFO ", True), + ], + id="qq", + ), + pytest.param( + "-v", + [ + ("WARNING Listing 1 violation(s) that are fatal", False), + ("INFO Set ANSIBLE_LIBRARY=", False), + ("DEBUG ", True), + ], + id="v", + ), + pytest.param( + "-vv", + [ + ("WARNING Listing 1 violation(s) that are fatal", False), + ("INFO Set ANSIBLE_LIBRARY=", False), + ], + id="really-loquacious", + ), + pytest.param( + "-vv", + [ + ("WARNING Listing 1 violation(s) that are fatal", False), + ("INFO Set ANSIBLE_LIBRARY=", False), + ], + id="vv", + ), + ), +) +def test_verbosity( + verbosity: str, + substrs: list[tuple[str, bool]], + project_path: Path, +) -> None: + """Checks that our default verbosity displays (only) warnings.""" + # Piggyback off the .yamllint in the root of the repo, just for testing. + # We'll "override" it with the one in the fixture, to produce a warning. + fakerole = Path() / "test" / "fixtures" / "verbosity-tests" + + if verbosity: + result = run_ansible_lint(verbosity, str(fakerole), cwd=project_path) + else: + result = run_ansible_lint(str(fakerole), cwd=project_path) + + for substr, invert in substrs: + if invert: + assert substr not in result.stderr, result.stderr + else: + assert substr in result.stderr, result.stderr diff --git a/test/test_with_skip_tagid.py b/test/test_with_skip_tagid.py new file mode 100644 index 0000000..5fbea8f --- /dev/null +++ b/test/test_with_skip_tagid.py @@ -0,0 +1,58 @@ +"""Tests related to skip tag id.""" +from ansiblelint.rules import RulesCollection +from ansiblelint.rules.yaml_rule import YamllintRule +from ansiblelint.runner import Runner +from ansiblelint.testing import run_ansible_lint + +FILE = "examples/playbooks/with-skip-tag-id.yml" +collection = RulesCollection() +collection.register(YamllintRule()) + + +def test_negative_no_param() -> None: + """Negative test no param.""" + bad_runner = Runner(FILE, rules=collection) + errs = bad_runner.run() + assert len(errs) > 0 + + +def test_negative_with_id() -> None: + """Negative test with_id.""" + with_id = "yaml" + bad_runner = Runner(FILE, rules=collection, tags=frozenset([with_id])) + errs = bad_runner.run() + assert len(errs) == 1 + + +def test_negative_with_tag() -> None: + """Negative test with_tag.""" + with_tag = "trailing-spaces" + bad_runner = Runner(FILE, rules=collection, tags=frozenset([with_tag])) + errs = bad_runner.run() + assert len(errs) == 1 + + +def test_positive_skip_id() -> None: + """Positive test skip_id.""" + skip_id = "yaml" + good_runner = Runner(FILE, rules=collection, skip_list=[skip_id]) + assert [] == good_runner.run() + + +def test_positive_skip_tag() -> None: + """Positive test skip_tag.""" + skip_tag = "yaml[trailing-spaces]" + good_runner = Runner(FILE, rules=collection, skip_list=[skip_tag]) + assert [] == good_runner.run() + + +def test_run_skip_rule() -> None: + """Test that we can skip a rule with -x.""" + result = run_ansible_lint( + "-x", + "name[casing]", + "examples/playbooks/rule-name-casing.yml", + executable="ansible-lint", + ) + assert result.returncode == 0 + assert not result.stdout diff --git a/test/test_yaml_utils.py b/test/test_yaml_utils.py new file mode 100644 index 0000000..5546e58 --- /dev/null +++ b/test/test_yaml_utils.py @@ -0,0 +1,955 @@ +"""Tests for yaml-related utility functions.""" +from __future__ import annotations + +from io import StringIO +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest +from ruamel.yaml.main import YAML +from yamllint.linter import run as run_yamllint + +import ansiblelint.yaml_utils +from ansiblelint.file_utils import Lintable +from ansiblelint.utils import task_in_list + +if TYPE_CHECKING: + from ruamel.yaml.comments import CommentedMap, CommentedSeq + from ruamel.yaml.emitter import Emitter + +fixtures_dir = Path(__file__).parent / "fixtures" +formatting_before_fixtures_dir = fixtures_dir / "formatting-before" +formatting_prettier_fixtures_dir = fixtures_dir / "formatting-prettier" +formatting_after_fixtures_dir = fixtures_dir / "formatting-after" + + +@pytest.fixture(name="empty_lintable") +def fixture_empty_lintable() -> Lintable: + """Return a Lintable with no contents.""" + lintable = Lintable("__empty_file__.yaml", content="") + return lintable + + +def test_tasks_in_list_empty_file(empty_lintable: Lintable) -> None: + """Make sure that task_in_list returns early when files are empty.""" + assert empty_lintable.kind + assert empty_lintable.path + res = list( + task_in_list( + data=empty_lintable, + file=empty_lintable, + kind=empty_lintable.kind, + ), + ) + assert not res + + +def test_nested_items_path() -> None: + """Verify correct function of nested_items_path().""" + data = { + "foo": "text", + "bar": {"some": "text2"}, + "fruits": ["apple", "orange"], + "answer": [{"forty-two": ["life", "universe", "everything"]}], + } + + items = [ + ("foo", "text", []), + ("bar", {"some": "text2"}, []), + ("some", "text2", ["bar"]), + ("fruits", ["apple", "orange"], []), + (0, "apple", ["fruits"]), + (1, "orange", ["fruits"]), + ("answer", [{"forty-two": ["life", "universe", "everything"]}], []), + (0, {"forty-two": ["life", "universe", "everything"]}, ["answer"]), + ("forty-two", ["life", "universe", "everything"], ["answer", 0]), + (0, "life", ["answer", 0, "forty-two"]), + (1, "universe", ["answer", 0, "forty-two"]), + (2, "everything", ["answer", 0, "forty-two"]), + ] + assert list(ansiblelint.yaml_utils.nested_items_path(data)) == items + + +@pytest.mark.parametrize( + "invalid_data_input", + ( + "string", + 42, + 1.234, + ("tuple",), + {"set"}, + # NoneType is no longer include, as we assume we have to ignore it + ), +) +def test_nested_items_path_raises_typeerror(invalid_data_input: Any) -> None: + """Verify non-dict/non-list types make nested_items_path() raises TypeError.""" + with pytest.raises(TypeError, match=r"Expected a dict or a list.*"): + list(ansiblelint.yaml_utils.nested_items_path(invalid_data_input)) + + +_input_playbook = [ + { + "name": "It's a playbook", # unambiguous; no quotes needed + "tasks": [ + { + "name": '"fun" task', # should be a single-quoted string + "debug": { + # ruamel.yaml default to single-quotes + # our Emitter defaults to double-quotes + "msg": "{{ msg }}", + }, + }, + ], + }, +] +_SINGLE_QUOTE_WITHOUT_INDENTS = """\ +--- +- name: It's a playbook + tasks: + - name: '"fun" task' + debug: + msg: '{{ msg }}' +""" +_SINGLE_QUOTE_WITH_INDENTS = """\ +--- + - name: It's a playbook + tasks: + - name: '"fun" task' + debug: + msg: '{{ msg }}' +""" +_DOUBLE_QUOTE_WITHOUT_INDENTS = """\ +--- +- name: It's a playbook + tasks: + - name: '"fun" task' + debug: + msg: "{{ msg }}" +""" +_DOUBLE_QUOTE_WITH_INDENTS_EXCEPT_ROOT_LEVEL = """\ +--- +- name: It's a playbook + tasks: + - name: '"fun" task' + debug: + msg: "{{ msg }}" +""" + + +@pytest.mark.parametrize( + ( + "map_indent", + "sequence_indent", + "sequence_dash_offset", + "alternate_emitter", + "expected_output", + ), + ( + pytest.param( + 2, + 2, + 0, + None, + _SINGLE_QUOTE_WITHOUT_INDENTS, + id="single_quote_without_indents", + ), + pytest.param( + 2, + 4, + 2, + None, + _SINGLE_QUOTE_WITH_INDENTS, + id="single_quote_with_indents", + ), + pytest.param( + 2, + 2, + 0, + ansiblelint.yaml_utils.FormattedEmitter, + _DOUBLE_QUOTE_WITHOUT_INDENTS, + id="double_quote_without_indents", + ), + pytest.param( + 2, + 4, + 2, + ansiblelint.yaml_utils.FormattedEmitter, + _DOUBLE_QUOTE_WITH_INDENTS_EXCEPT_ROOT_LEVEL, + id="double_quote_with_indents_except_root_level", + ), + ), +) +def test_custom_ruamel_yaml_emitter( + map_indent: int, + sequence_indent: int, + sequence_dash_offset: int, + alternate_emitter: Emitter | None, + expected_output: str, +) -> None: + """Test ``ruamel.yaml.YAML.dump()`` sequence formatting and quotes.""" + yaml = YAML(typ="rt") + # NB: ruamel.yaml does not have typehints, so mypy complains about everything here. + yaml.explicit_start = True + yaml.map_indent = map_indent + yaml.sequence_indent = sequence_indent + yaml.sequence_dash_offset = sequence_dash_offset + if alternate_emitter is not None: + yaml.Emitter = alternate_emitter + # ruamel.yaml only writes to a stream (there is no `dumps` function) + with StringIO() as output_stream: + yaml.dump(_input_playbook, output_stream) + output = output_stream.getvalue() + assert output == expected_output + + +@pytest.fixture(name="yaml_formatting_fixtures") +def fixture_yaml_formatting_fixtures(fixture_filename: str) -> tuple[str, str, str]: + """Get the contents for the formatting fixture files. + + To regenerate these fixtures, please run ``pytest --regenerate-formatting-fixtures``. + + Ideally, prettier should not have to change any ``formatting-after`` fixtures. + """ + before_path = formatting_before_fixtures_dir / fixture_filename + prettier_path = formatting_prettier_fixtures_dir / fixture_filename + after_path = formatting_after_fixtures_dir / fixture_filename + before_content = before_path.read_text() + prettier_content = prettier_path.read_text() + formatted_content = after_path.read_text() + return before_content, prettier_content, formatted_content + + +@pytest.mark.parametrize( + "fixture_filename", + ( + "fmt-1.yml", + "fmt-2.yml", + "fmt-3.yml", + ), +) +def test_formatted_yaml_loader_dumper( + yaml_formatting_fixtures: tuple[str, str, str], + fixture_filename: str, # noqa: ARG001 +) -> None: + """Ensure that FormattedYAML loads/dumps formatting fixtures consistently.""" + # pylint: disable=unused-argument + before_content, prettier_content, after_content = yaml_formatting_fixtures + assert before_content != prettier_content + assert before_content != after_content + + yaml = ansiblelint.yaml_utils.FormattedYAML() + + data_before = yaml.loads(before_content) + dump_from_before = yaml.dumps(data_before) + data_prettier = yaml.loads(prettier_content) + dump_from_prettier = yaml.dumps(data_prettier) + data_after = yaml.loads(after_content) + dump_from_after = yaml.dumps(data_after) + + # comparing data does not work because the Comment objects + # have different IDs even if contents do not match. + + assert dump_from_before == after_content + assert dump_from_prettier == after_content + assert dump_from_after == after_content + + # We can't do this because FormattedYAML is stricter in some cases: + # + # Instead, `pytest --regenerate-formatting-fixtures` will fail if prettier would + # change any files in test/fixtures/formatting-after + + # Running our files through yamllint, after we reformatted them, + # should not yield any problems. + config = ansiblelint.yaml_utils.load_yamllint_config() + assert not list(run_yamllint(after_content, config)) + + +@pytest.fixture(name="lintable") +def fixture_lintable(file_path: str) -> Lintable: + """Return a playbook Lintable for use in ``get_path_to_*`` tests.""" + return Lintable(file_path) + + +@pytest.fixture(name="ruamel_data") +def fixture_ruamel_data(lintable: Lintable) -> CommentedMap | CommentedSeq: + """Return the loaded YAML data for the Lintable.""" + yaml = ansiblelint.yaml_utils.FormattedYAML() + data: CommentedMap | CommentedSeq = yaml.loads(lintable.content) + return data + + +@pytest.mark.parametrize( + ("file_path", "lineno", "expected_path"), + ( + # ignored lintables + pytest.param( + "examples/playbooks/tasks/passing_task.yml", + 2, + [], + id="ignore_tasks_file", + ), + pytest.param( + "examples/roles/more_complex/handlers/main.yml", + 2, + [], + id="ignore_handlers_file", + ), + pytest.param("examples/playbooks/vars/other.yml", 2, [], id="ignore_vars_file"), + pytest.param( + "examples/host_vars/localhost.yml", + 2, + [], + id="ignore_host_vars_file", + ), + pytest.param("examples/group_vars/all.yml", 2, [], id="ignore_group_vars_file"), + pytest.param( + "examples/inventory/inventory.yml", + 2, + [], + id="ignore_inventory_file", + ), + pytest.param( + "examples/roles/dependency_in_meta/meta/main.yml", + 2, + [], + id="ignore_meta_file", + ), + pytest.param( + "examples/reqs_v1/requirements.yml", + 2, + [], + id="ignore_requirements_v1_file", + ), + pytest.param( + "examples/reqs_v2/requirements.yml", + 2, + [], + id="ignore_requirements_v2_file", + ), + # we don't have any release notes examples. Oh well. + pytest.param( + ".pre-commit-config.yaml", + 2, + [], + id="ignore_unrecognized_yaml_file", + ), + # playbook lintables + pytest.param( + "examples/playbooks/become.yml", + 1, + [], + id="1_play_playbook-line_before_play", + ), + pytest.param( + "examples/playbooks/become.yml", + 2, + [0], + id="1_play_playbook-first_line_in_play", + ), + pytest.param( + "examples/playbooks/become.yml", + 10, + [0], + id="1_play_playbook-middle_line_in_play", + ), + pytest.param( + "examples/playbooks/become.yml", + 100, + [0], + id="1_play_playbook-line_after_eof", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 1, + [], + id="4_play_playbook-line_before_play_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 2, + [0], + id="4_play_playbook-first_line_in_play_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 5, + [0], + id="4_play_playbook-middle_line_in_play_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 9, + [0], + id="4_play_playbook-last_line_in_play_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 10, + [1], + id="4_play_playbook-first_line_in_play_2", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 14, + [1], + id="4_play_playbook-middle_line_in_play_2", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 18, + [1], + id="4_play_playbook-last_line_in_play_2", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 19, + [2], + id="4_play_playbook-first_line_in_play_3", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 23, + [2], + id="4_play_playbook-middle_line_in_play_3", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 27, + [2], + id="4_play_playbook-last_line_in_play_3", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 28, + [3], + id="4_play_playbook-first_line_in_play_4", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 31, + [3], + id="4_play_playbook-middle_line_in_play_4", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 35, + [3], + id="4_play_playbook-last_line_in_play_4", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 100, + [3], + id="4_play_playbook-line_after_eof", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 1, + [], + id="import_playbook-line_before_play_1", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 2, + [0], + id="import_playbook-first_line_in_play_1", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 3, + [0], + id="import_playbook-middle_line_in_play_1", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 4, + [0], + id="import_playbook-last_line_in_play_1", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 5, + [1], + id="import_playbook-first_line_in_play_2", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 6, + [1], + id="import_playbook-middle_line_in_play_2", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 7, + [1], + id="import_playbook-last_line_in_play_2", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 8, + [2], + id="import_playbook-first_line_in_play_3", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 9, + [2], + id="import_playbook-last_line_in_play_3", + ), + pytest.param( + "examples/playbooks/playbook-parent.yml", + 15, + [2], + id="import_playbook-line_after_eof", + ), + ), +) +def test_get_path_to_play( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, + expected_path: list[int | str], +) -> None: + """Ensure ``get_path_to_play`` returns the expected path given a file + line.""" + path_to_play = ansiblelint.yaml_utils.get_path_to_play( + lintable, + lineno, + ruamel_data, + ) + assert path_to_play == expected_path + + +@pytest.mark.parametrize( + ("file_path", "lineno", "expected_path"), + ( + # ignored lintables + pytest.param("examples/playbooks/vars/other.yml", 2, [], id="ignore_vars_file"), + pytest.param( + "examples/host_vars/localhost.yml", + 2, + [], + id="ignore_host_vars_file", + ), + pytest.param("examples/group_vars/all.yml", 2, [], id="ignore_group_vars_file"), + pytest.param( + "examples/inventory/inventory.yml", + 2, + [], + id="ignore_inventory_file", + ), + pytest.param( + "examples/roles/dependency_in_meta/meta/main.yml", + 2, + [], + id="ignore_meta_file", + ), + pytest.param( + "examples/reqs_v1/requirements.yml", + 2, + [], + id="ignore_requirements_v1_file", + ), + pytest.param( + "examples/reqs_v2/requirements.yml", + 2, + [], + id="ignore_requirements_v2_file", + ), + # we don't have any release notes examples. Oh well. + pytest.param( + ".pre-commit-config.yaml", + 2, + [], + id="ignore_unrecognized_yaml_file", + ), + # tasks-containing lintables + pytest.param( + "examples/playbooks/become.yml", + 4, + [], + id="1_task_playbook-line_before_tasks", + ), + pytest.param( + "examples/playbooks/become.yml", + 5, + [0, "tasks", 0], + id="1_task_playbook-first_line_in_task_1", + ), + pytest.param( + "examples/playbooks/become.yml", + 10, + [0, "tasks", 0], + id="1_task_playbook-middle_line_in_task_1", + ), + pytest.param( + "examples/playbooks/become.yml", + 15, + [0, "tasks", 0], + id="1_task_playbook-last_line_in_task_1", + ), + pytest.param( + "examples/playbooks/become.yml", + 100, + [0, "tasks", 0], + id="1_task_playbook-line_after_eof_without_anything_after_task", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 1, + [], + id="4_play_playbook-play_1_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 7, + [0, "tasks", 0], + id="4_play_playbook-play_1_first_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 9, + [0, "tasks", 0], + id="4_play_playbook-play_1_last_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 10, + [], + id="4_play_playbook-play_2_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 12, + [], + id="4_play_playbook-play_2_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 13, + [1, "tasks", 0], + id="4_play_playbook-play_2_first_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 18, + [1, "tasks", 0], + id="4_play_playbook-play_2_middle_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 18, + [1, "tasks", 0], + id="4_play_playbook-play_2_last_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 19, + [], + id="4_play_playbook-play_3_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 22, + [], + id="4_play_playbook-play_3_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 23, + [2, "tasks", 0], + id="4_play_playbook-play_3_first_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 25, + [2, "tasks", 0], + id="4_play_playbook-play_3_middle_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 27, + [2, "tasks", 0], + id="4_play_playbook-play_3_last_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 28, + [], + id="4_play_playbook-play_4_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 31, + [], + id="4_play_playbook-play_4_line_before_tasks", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 32, + [3, "tasks", 0], + id="4_play_playbook-play_4_first_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 33, + [3, "tasks", 0], + id="4_play_playbook-play_4_middle_line_task_1", + ), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 35, + [3, "tasks", 0], + id="4_play_playbook-play_4_last_line_task_1", + ), + # playbook with multiple tasks + tasks blocks in a play + pytest.param( + # must have at least one key after one of the tasks blocks + "examples/playbooks/include.yml", + 6, + [0, "pre_tasks", 0], + id="playbook-multi_tasks_blocks-pre_tasks_last_task_before_roles", + ), + pytest.param( + "examples/playbooks/include.yml", + 7, + [], + id="playbook-multi_tasks_blocks-roles_after_pre_tasks", + ), + pytest.param( + "examples/playbooks/include.yml", + 10, + [], + id="playbook-multi_tasks_blocks-roles_before_tasks", + ), + pytest.param( + "examples/playbooks/include.yml", + 12, + [0, "tasks", 0], + id="playbook-multi_tasks_blocks-tasks_first_task", + ), + pytest.param( + "examples/playbooks/include.yml", + 14, + [0, "tasks", 1], + id="playbook-multi_tasks_blocks-tasks_last_task_before_handlers", + ), + pytest.param( + "examples/playbooks/include.yml", + 16, + [0, "handlers", 0], + id="playbook-multi_tasks_blocks-handlers_task", + ), + # playbook with subtasks blocks + pytest.param( + "examples/playbooks/blockincludes.yml", + 14, + [0, "tasks", 0, "block", 1, "block", 0], + id="playbook-deeply_nested_task", + ), + pytest.param( + "examples/playbooks/block.yml", + 12, + [0, "tasks", 0, "block", 1], + id="playbook-subtasks-block_task_2", + ), + pytest.param( + "examples/playbooks/block.yml", + 22, + [0, "tasks", 0, "rescue", 2], + id="playbook-subtasks-rescue_task_3", + ), + pytest.param( + "examples/playbooks/block.yml", + 25, + [0, "tasks", 0, "always", 0], + id="playbook-subtasks-always_task_3", + ), + # tasks files + pytest.param("examples/playbooks/tasks/x.yml", 2, [0], id="tasks-null_task"), + pytest.param( + "examples/playbooks/tasks/x.yml", + 6, + [1], + id="tasks-null_task_next", + ), + pytest.param( + "examples/playbooks/tasks/empty_blocks.yml", + 7, + [0], # this IS part of the first task and "rescue" does not have subtasks. + id="tasks-null_rescue", + ), + pytest.param( + "examples/playbooks/tasks/empty_blocks.yml", + 8, + [0], # this IS part of the first task and "always" does not have subtasks. + id="tasks-empty_always", + ), + pytest.param( + "examples/playbooks/tasks/empty_blocks.yml", + 16, + [1, "always", 0], + id="tasks-task_beyond_empty_blocks", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 1, + [], + id="tasks-line_before_tasks", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 2, + [0], + id="tasks-first_line_in_task_1", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 3, + [0], + id="tasks-middle_line_in_task_1", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 4, + [0], + id="tasks-last_line_in_task_1", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 5, + [1], + id="tasks-first_line_in_task_2", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 6, + [1], + id="tasks-middle_line_in_task_2", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 7, + [1], + id="tasks-last_line_in_task_2", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 8, + [2], + id="tasks-first_line_in_task_3", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 9, + [2], + id="tasks-last_line_in_task_3", + ), + pytest.param( + "examples/roles/more_complex/tasks/main.yml", + 100, + [2], + id="tasks-line_after_eof", + ), + # handlers + pytest.param( + "examples/roles/more_complex/handlers/main.yml", + 1, + [], + id="handlers-line_before_tasks", + ), + pytest.param( + "examples/roles/more_complex/handlers/main.yml", + 2, + [0], + id="handlers-first_line_in_task_1", + ), + pytest.param( + "examples/roles/more_complex/handlers/main.yml", + 3, + [0], + id="handlers-last_line_in_task_1", + ), + pytest.param( + "examples/roles/more_complex/handlers/main.yml", + 100, + [0], + id="handlers-line_after_eof", + ), + ), +) +def test_get_path_to_task( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, + expected_path: list[int | str], +) -> None: + """Ensure ``get_task_to_play`` returns the expected path given a file + line.""" + path_to_task = ansiblelint.yaml_utils.get_path_to_task( + lintable, + lineno, + ruamel_data, + ) + assert path_to_task == expected_path + + +@pytest.mark.parametrize( + ("file_path", "lineno"), + ( + pytest.param("examples/playbooks/become.yml", 0, id="1_play_playbook"), + pytest.param( + "examples/playbooks/rule-partial-become-without-become-pass.yml", + 0, + id="4_play_playbook", + ), + pytest.param("examples/playbooks/playbook-parent.yml", 0, id="import_playbook"), + pytest.param("examples/playbooks/become.yml", 0, id="1_task_playbook"), + ), +) +def test_get_path_to_play_raises_value_error_for_bad_lineno( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, +) -> None: + """Ensure ``get_path_to_play`` raises ValueError for lineno < 1.""" + with pytest.raises( + ValueError, + match=f"expected lineno >= 1, got {lineno}", + ): + ansiblelint.yaml_utils.get_path_to_play(lintable, lineno, ruamel_data) + + +@pytest.mark.parametrize( + ("file_path", "lineno"), + (pytest.param("examples/roles/more_complex/tasks/main.yml", 0, id="tasks"),), +) +def test_get_path_to_task_raises_value_error_for_bad_lineno( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, +) -> None: + """Ensure ``get_task_to_play`` raises ValueError for lineno < 1.""" + with pytest.raises( + ValueError, + match=f"expected lineno >= 1, got {lineno}", + ): + ansiblelint.yaml_utils.get_path_to_task(lintable, lineno, ruamel_data) + + +@pytest.mark.parametrize( + ("before", "after"), + ( + pytest.param(None, None, id="1"), + pytest.param(1, 1, id="2"), + pytest.param({}, {}, id="3"), + pytest.param({"__file__": 1}, {}, id="simple"), + pytest.param({"foo": {"__file__": 1}}, {"foo": {}}, id="nested"), + pytest.param([{"foo": {"__file__": 1}}], [{"foo": {}}], id="nested-in-lint"), + pytest.param({"foo": [{"__file__": 1}]}, {"foo": [{}]}, id="nested-in-lint"), + ), +) +def test_deannotate( + before: Any, + after: Any, +) -> None: + """Ensure deannotate works as intended.""" + assert ansiblelint.yaml_utils.deannotate(before) == after |