summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:06:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:06:49 +0000
commit2fe34b6444502079dc0b84365ce82dbc92de308e (patch)
tree8fedcab52bbbc3db6c5aa909a88a7a7b81685018 /test
parentInitial commit. (diff)
downloadansible-lint-2fe34b6444502079dc0b84365ce82dbc92de308e.tar.xz
ansible-lint-2fe34b6444502079dc0b84365ce82dbc92de308e.zip
Adding upstream version 6.17.2.upstream/6.17.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test')
-rw-r--r--test/__init__.py1
-rw-r--r--test/bar.txt1
-rw-r--r--test/conftest.py91
-rw-r--r--test/custom_rules/__init__.py1
-rw-r--r--test/custom_rules/example_com/__init__.py1
-rw-r--r--test/custom_rules/example_com/example_com_rule.py28
-rw-r--r--test/custom_rules/example_inc/__init__.py1
-rw-r--r--test/custom_rules/example_inc/custom_rule.py28
-rw-r--r--test/fixtures/__init__.py1
-rw-r--r--test/fixtures/ansible-config-invalid.yml4
-rw-r--r--test/fixtures/ansible-config.yml3
-rw-r--r--test/fixtures/config-with-extra-vars.yml4
-rw-r--r--test/fixtures/config-with-relative-path.yml4
-rw-r--r--test/fixtures/config-with-write-all.yml3
-rw-r--r--test/fixtures/config-with-write-none.yml3
-rw-r--r--test/fixtures/config-with-write-subset.yml4
-rw-r--r--test/fixtures/exclude-paths-with-expands.yml5
-rw-r--r--test/fixtures/exclude-paths.yml4
-rw-r--r--test/fixtures/formatting-after/fmt-1.yml47
-rw-r--r--test/fixtures/formatting-after/fmt-2.yml24
-rw-r--r--test/fixtures/formatting-after/fmt-3.yml21
-rw-r--r--test/fixtures/formatting-before/fmt-1.yml53
-rw-r--r--test/fixtures/formatting-before/fmt-2.yml24
-rw-r--r--test/fixtures/formatting-before/fmt-3.yml21
-rw-r--r--test/fixtures/formatting-prettier/fmt-1.yml48
-rw-r--r--test/fixtures/formatting-prettier/fmt-2.yml24
-rw-r--r--test/fixtures/formatting-prettier/fmt-3.yml21
-rw-r--r--test/fixtures/list-rules-tests/.yamllint2
-rw-r--r--test/fixtures/parseable.yml3
-rw-r--r--test/fixtures/quiet.yml3
-rw-r--r--test/fixtures/rulesdir-defaults.yml5
-rw-r--r--test/fixtures/rulesdir.yml4
-rw-r--r--test/fixtures/show-abspath.yml3
-rw-r--r--test/fixtures/show-relpath.yml3
-rw-r--r--test/fixtures/skip-tags.yml4
-rw-r--r--test/fixtures/strict.yml3
-rw-r--r--test/fixtures/tags.yml4
-rw-r--r--test/fixtures/unknown-type.yml2
-rw-r--r--test/fixtures/verbosity-tests/.yamllint2
-rw-r--r--test/fixtures/verbosity-tests/tasks/main.yml0
-rw-r--r--test/fixtures/verbosity.yml3
-rw-r--r--test/foo.txt1
-rw-r--r--test/local-content/README.md6
-rw-r--r--test/local-content/collections/ansible_collections/testns/test_collection/galaxy.yml4
-rw-r--r--test/local-content/collections/ansible_collections/testns/test_collection/plugins/filter/test_filter.py17
-rwxr-xr-xtest/local-content/collections/ansible_collections/testns/test_collection/plugins/modules/test_module_2.py14
-rw-r--r--test/local-content/test-collection.yml10
-rwxr-xr-xtest/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py14
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py19
-rwxr-xr-xtest/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py14
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml3
-rwxr-xr-xtest/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py14
-rw-r--r--test/local-content/test-roles-failed/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py18
-rwxr-xr-xtest/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py14
-rw-r--r--test/local-content/test-roles-failed/roles/role3/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed/test.yml7
-rwxr-xr-xtest/local-content/test-roles-success/roles/role1/library/test_module_1_success.py14
-rw-r--r--test/local-content/test-roles-success/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-success/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py18
-rwxr-xr-xtest/local-content/test-roles-success/roles/role3/library/test_module_3_success.py14
-rw-r--r--test/local-content/test-roles-success/roles/role3/tasks/main.yml3
-rw-r--r--test/rules/__init__.py1
-rw-r--r--test/rules/fixtures/__init__.py3
-rw-r--r--test/rules/fixtures/ematcher.py15
-rw-r--r--test/rules/fixtures/raw_task.md3
-rw-r--r--test/rules/fixtures/raw_task.py30
-rw-r--r--test/rules/fixtures/unset_variable_matcher.py15
-rw-r--r--test/rules/test_deprecated_module.py27
-rw-r--r--test/rules/test_inline_env_var.py90
-rw-r--r--test/rules/test_no_changed_when.py23
-rw-r--r--test/rules/test_package_latest.py23
-rw-r--r--test/rules/test_role_names.py91
-rw-r--r--test/rules/test_syntax_check.py70
-rw-r--r--test/schemas/.mocharc.json7
l---------test/schemas/f1
-rw-r--r--test/schemas/negative_test/.ansible-lint4
-rw-r--r--test/schemas/negative_test/.ansible-lint.md139
-rw-r--r--test/schemas/negative_test/.config/ansible-lint.yml3
-rw-r--r--test/schemas/negative_test/.config/ansible-lint.yml.md42
-rw-r--r--test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml4
-rw-r--r--test/schemas/negative_test/changelogs/invalid-date/changelogs/changelog.yaml.md40
-rw-r--r--test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml8
-rw-r--r--test/schemas/negative_test/changelogs/invalid-plugin-namespace/changelogs/changelog.yaml.md34
-rw-r--r--test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml4
-rw-r--r--test/schemas/negative_test/changelogs/list/changelogs/changelog.yaml.md34
-rw-r--r--test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml2
-rw-r--r--test/schemas/negative_test/changelogs/no-semver/changelogs/changelog.yaml.md34
-rw-r--r--test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml2
-rw-r--r--test/schemas/negative_test/changelogs/unknown-keys/changelogs/changelog.yaml.md34
-rw-r--r--test/schemas/negative_test/galaxy_1/galaxy.yml12
-rw-r--r--test/schemas/negative_test/galaxy_1/galaxy.yml.md34
-rw-r--r--test/schemas/negative_test/inventory/broken_dev_inventory.yml10
-rw-r--r--test/schemas/negative_test/inventory/broken_dev_inventory.yml.md34
-rw-r--r--test/schemas/negative_test/meta/runtime.yml1
-rw-r--r--test/schemas/negative_test/meta/runtime.yml.md34
-rw-r--r--test/schemas/negative_test/molecule/platforms_children/molecule.yml5
-rw-r--r--test/schemas/negative_test/molecule/platforms_children/molecule.yml.md34
-rw-r--r--test/schemas/negative_test/molecule/platforms_networks/molecule.yml7
-rw-r--r--test/schemas/negative_test/molecule/platforms_networks/molecule.yml.md49
-rw-r--r--test/schemas/negative_test/playbooks/environment.yml3
-rw-r--r--test/schemas/negative_test/playbooks/environment.yml.md138
-rw-r--r--test/schemas/negative_test/playbooks/failed_when.yml6
-rw-r--r--test/schemas/negative_test/playbooks/failed_when.yml.md177
-rw-r--r--test/schemas/negative_test/playbooks/gather_facts.yml6
-rw-r--r--test/schemas/negative_test/playbooks/gather_facts.yml.md123
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset.yml6
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset.yml.md123
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset2.yml7
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset2.yml.md277
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset3.yml7
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset3.yml.md303
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset4.yml6
-rw-r--r--test/schemas/negative_test/playbooks/gather_subset4.yml.md123
-rw-r--r--test/schemas/negative_test/playbooks/ignore_errors.yml6
-rw-r--r--test/schemas/negative_test/playbooks/ignore_errors.yml.md203
-rw-r--r--test/schemas/negative_test/playbooks/import_playbook.yml1
-rw-r--r--test/schemas/negative_test/playbooks/import_playbook.yml.md90
-rw-r--r--test/schemas/negative_test/playbooks/import_playbook_exclusive.yml4
-rw-r--r--test/schemas/negative_test/playbooks/import_playbook_exclusive.yml.md132
-rw-r--r--test/schemas/negative_test/playbooks/invalid-failed-when.yml15
-rw-r--r--test/schemas/negative_test/playbooks/invalid-failed-when.yml.md253
-rw-r--r--test/schemas/negative_test/playbooks/invalid-serial.yml2
-rw-r--r--test/schemas/negative_test/playbooks/invalid-serial.yml.md177
-rw-r--r--test/schemas/negative_test/playbooks/invalid.yml3
-rw-r--r--test/schemas/negative_test/playbooks/invalid.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/invalid_become.yml3
-rw-r--r--test/schemas/negative_test/playbooks/invalid_become.yml.md140
-rw-r--r--test/schemas/negative_test/playbooks/local_action.yml3
-rw-r--r--test/schemas/negative_test/playbooks/local_action.yml.md141
-rw-r--r--test/schemas/negative_test/playbooks/loop.yml7
-rw-r--r--test/schemas/negative_test/playbooks/loop.yml.md141
-rw-r--r--test/schemas/negative_test/playbooks/loop2.yml7
-rw-r--r--test/schemas/negative_test/playbooks/loop2.yml.md141
-rw-r--r--test/schemas/negative_test/playbooks/no_log_partial_template.yml7
-rw-r--r--test/schemas/negative_test/playbooks/no_log_partial_template.yml.md203
-rw-r--r--test/schemas/negative_test/playbooks/no_log_string.yml7
-rw-r--r--test/schemas/negative_test/playbooks/no_log_string.yml.md203
-rw-r--r--test/schemas/negative_test/playbooks/roles.yml2
-rw-r--r--test/schemas/negative_test/playbooks/roles.yml.md114
-rw-r--r--test/schemas/negative_test/playbooks/run_once_list.yml8
-rw-r--r--test/schemas/negative_test/playbooks/run_once_list.yml.md221
-rw-r--r--test/schemas/negative_test/playbooks/tags-mapping.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tags-mapping.yml.md166
-rw-r--r--test/schemas/negative_test/playbooks/tags-number.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tags-number.yml.md166
-rw-r--r--test/schemas/negative_test/playbooks/tasks.yml5
-rw-r--r--test/schemas/negative_test/playbooks/tasks.yml.md192
-rw-r--r--test/schemas/negative_test/playbooks/tasks/args_integer.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/args_integer.yml.md99
-rw-r--r--test/schemas/negative_test/playbooks/tasks/args_string.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/args_string.yml.md90
-rw-r--r--test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml4
-rw-r--r--test/schemas/negative_test/playbooks/tasks/become_method_invalid.yml.md203
-rw-r--r--test/schemas/negative_test/playbooks/tasks/become_method_untemplated.yml.md181
-rw-r--r--test/schemas/negative_test/playbooks/tasks/ignore_errors.yml4
-rw-r--r--test/schemas/negative_test/playbooks/tasks/ignore_errors.yml.md129
-rw-r--r--test/schemas/negative_test/playbooks/tasks/invalid_block.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/invalid_block.yml.md62
-rw-r--r--test/schemas/negative_test/playbooks/tasks/local_action.yml1
-rw-r--r--test/schemas/negative_test/playbooks/tasks/local_action.yml.md67
-rw-r--r--test/schemas/negative_test/playbooks/tasks/loop.yml3
-rw-r--r--test/schemas/negative_test/playbooks/tasks/loop.yml.md67
-rw-r--r--test/schemas/negative_test/playbooks/tasks/loop2.yml3
-rw-r--r--test/schemas/negative_test/playbooks/tasks/loop2.yml.md67
-rw-r--r--test/schemas/negative_test/playbooks/tasks/no_log_number.yml3
-rw-r--r--test/schemas/negative_test/playbooks/tasks/no_log_number.yml.md147
-rw-r--r--test/schemas/negative_test/playbooks/tasks/no_log_string.yml5
-rw-r--r--test/schemas/negative_test/playbooks/tasks/no_log_string.yml.md129
-rw-r--r--test/schemas/negative_test/playbooks/tasks/tags-mapping.yml3
-rw-r--r--test/schemas/negative_test/playbooks/tasks/tags-mapping.yml.md125
-rw-r--r--test/schemas/negative_test/playbooks/tasks/tags-string.yml3
-rw-r--r--test/schemas/negative_test/playbooks/tasks/tags-string.yml.md125
-rw-r--r--test/schemas/negative_test/playbooks/tasks/when_integer.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/when_integer.yml.md155
-rw-r--r--test/schemas/negative_test/playbooks/tasks/when_object.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/when_object.yml.md155
-rw-r--r--test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/with_items_boolean.yml.md88
-rw-r--r--test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml2
-rw-r--r--test/schemas/negative_test/playbooks/tasks/with_items_untemplated_string.yml.md88
-rw-r--r--test/schemas/negative_test/playbooks/var_files_list_number.yml5
-rw-r--r--test/schemas/negative_test/playbooks/var_files_list_number.yml.md144
-rw-r--r--test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml5
-rw-r--r--test/schemas/negative_test/playbooks/var_files_list_of_list_number.yml.md157
-rw-r--r--test/schemas/negative_test/playbooks/var_files_number.yml4
-rw-r--r--test/schemas/negative_test/playbooks/var_files_number.yml.md122
-rw-r--r--test/schemas/negative_test/playbooks/vars/asterisk.yml2
-rw-r--r--test/schemas/negative_test/playbooks/vars/asterisk.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml2
-rw-r--r--test/schemas/negative_test/playbooks/vars/dash-in-var-name.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vars/list.yml3
-rw-r--r--test/schemas/negative_test/playbooks/vars/list.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vars/numeric-var-name.yml2
-rw-r--r--test/schemas/negative_test/playbooks/vars/numeric-var-name.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vars/play-keyword.yml2
-rw-r--r--test/schemas/negative_test/playbooks/vars/play-keyword.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vars/python-keyword.yml3
-rw-r--r--test/schemas/negative_test/playbooks/vars/python-keyword.yml.md86
-rw-r--r--test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml2
-rw-r--r--test/schemas/negative_test/playbooks/vars/varname-numeric-prefix.yml.md77
-rw-r--r--test/schemas/negative_test/playbooks/vas_prompt.yml7
-rw-r--r--test/schemas/negative_test/playbooks/vas_prompt.yml.md118
-rw-r--r--test/schemas/negative_test/playbooks/when.yml11
-rw-r--r--test/schemas/negative_test/playbooks/when.yml.md286
-rw-r--r--test/schemas/negative_test/reqs3/meta/requirements.yml2
-rw-r--r--test/schemas/negative_test/reqs3/meta/requirements.yml.md101
-rw-r--r--test/schemas/negative_test/roles/meta/argument_specs.yml5
-rw-r--r--test/schemas/negative_test/roles/meta/argument_specs.yml.md34
-rw-r--r--test/schemas/negative_test/roles/meta/main.yml10
-rw-r--r--test/schemas/negative_test/roles/meta/main.yml.md58
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml10
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_collection/meta/main.yml.md34
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml11
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_collections/meta/main.yml.md34
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml12
-rw-r--r--test/schemas/negative_test/roles/meta_invalid_role_namespace/meta/main.yml.md58
-rw-r--r--test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml13
-rw-r--r--test/schemas/negative_test/roles/role_with_bad_deps_in_meta/meta/main.yml.md101
-rw-r--r--test/schemas/package-lock.json2290
-rw-r--r--test/schemas/package.json28
-rw-r--r--test/schemas/src/rebuild.py140
-rw-r--r--test/schemas/src/schema.spec.ts184
-rw-r--r--test/schemas/test/.config/ansible-lint.yml9
-rw-r--r--test/schemas/test/ansible-navigator.yml85
-rw-r--r--test/schemas/test/changelog.yml47
-rw-r--r--test/schemas/test/changelogs/maximal/changelog.yaml61
-rw-r--r--test/schemas/test/changelogs/minimal/changelog.yaml3
-rw-r--r--test/schemas/test/execution-environment-v3.yml19
-rw-r--r--test/schemas/test/execution-environment.yml21
-rw-r--r--test/schemas/test/galaxy.yml17
-rw-r--r--test/schemas/test/inventory.yml13
-rw-r--r--test/schemas/test/inventory/inventory.yml31
-rw-r--r--test/schemas/test/inventory/production.yml37
-rw-r--r--test/schemas/test/meta/requirements.yml3
-rw-r--r--test/schemas/test/meta/runtime.yml1
-rw-r--r--test/schemas/test/molecule/cluster/base.yml0
-rw-r--r--test/schemas/test/molecule/cluster/converge.yml0
-rw-r--r--test/schemas/test/molecule/cluster/foobar.yml0
-rw-r--r--test/schemas/test/molecule/cluster/molecule.yml76
-rw-r--r--test/schemas/test/molecule/default/molecule.yml117
-rw-r--r--test/schemas/test/molecule/vagrant/molecule.yml46
-rw-r--r--test/schemas/test/playbooks/block.yml10
-rw-r--r--test/schemas/test/playbooks/defaults/foo.yml3
-rw-r--r--test/schemas/test/playbooks/environment.yml7
-rw-r--r--test/schemas/test/playbooks/failed_when.yml18
-rw-r--r--test/schemas/test/playbooks/full-jinja.yml16
-rw-r--r--test/schemas/test/playbooks/gather_facts.yml6
-rw-r--r--test/schemas/test/playbooks/gather_subset.yml15
-rw-r--r--test/schemas/test/playbooks/ignore_errors..yml9
-rw-r--r--test/schemas/test/playbooks/import_playbook.yml9
-rw-r--r--test/schemas/test/playbooks/included.yml1
-rw-r--r--test/schemas/test/playbooks/integers.yml23
-rw-r--r--test/schemas/test/playbooks/local_action_dict.yml5
-rw-r--r--test/schemas/test/playbooks/local_action_string.yml3
-rw-r--r--test/schemas/test/playbooks/loop.yml9
-rw-r--r--test/schemas/test/playbooks/no_log.yml11
-rw-r--r--test/schemas/test/playbooks/roles.yml13
-rw-r--r--test/schemas/test/playbooks/run.yml42
-rw-r--r--test/schemas/test/playbooks/run_once.yml6
-rw-r--r--test/schemas/test/playbooks/tags.yml23
-rw-r--r--test/schemas/test/playbooks/tasks.yml5
-rw-r--r--test/schemas/test/playbooks/tasks/args.yml4
-rw-r--r--test/schemas/test/playbooks/tasks/become_method.yml7
-rw-r--r--test/schemas/test/playbooks/tasks/changed_when.yml10
-rw-r--r--test/schemas/test/playbooks/tasks/diff.yml4
-rw-r--r--test/schemas/test/playbooks/tasks/empty_tasks.yml2
-rw-r--r--test/schemas/test/playbooks/tasks/ignore_errors.yml7
-rw-r--r--test/schemas/test/playbooks/tasks/local_action_dict.yml3
-rw-r--r--test/schemas/test/playbooks/tasks/local_action_string.yml1
-rw-r--r--test/schemas/test/playbooks/tasks/loop.yml6
-rw-r--r--test/schemas/test/playbooks/tasks/no_log.yml11
-rw-r--r--test/schemas/test/playbooks/tasks/notify.yml11
-rw-r--r--test/schemas/test/playbooks/tasks/run_once.yml9
-rw-r--r--test/schemas/test/playbooks/tasks/some_tasks.yml8
-rw-r--r--test/schemas/test/playbooks/tasks/tags.yml29
-rw-r--r--test/schemas/test/playbooks/tasks/templated_become.yml12
-rw-r--r--test/schemas/test/playbooks/tasks/templated_integers.yml5
-rw-r--r--test/schemas/test/playbooks/tasks/throttled.yml5
-rw-r--r--test/schemas/test/playbooks/tasks/until.yml14
-rw-r--r--test/schemas/test/playbooks/tasks/when.yml10
-rw-r--r--test/schemas/test/playbooks/tasks/with_items.yml16
-rw-r--r--test/schemas/test/playbooks/templated_become.yml16
-rw-r--r--test/schemas/test/playbooks/user_valid.yml3
-rw-r--r--test/schemas/test/playbooks/var_files.yml18
-rw-r--r--test/schemas/test/playbooks/vars/empty_vars.yml2
-rw-r--r--test/schemas/test/playbooks/vars/encrypted.yml6
-rw-r--r--test/schemas/test/playbooks/vars/myvars.yml9
-rw-r--r--test/schemas/test/playbooks/vars_prompt.yml11
-rw-r--r--test/schemas/test/playbooks/when.yml11
-rw-r--r--test/schemas/test/playbooks/with_.yml34
-rw-r--r--test/schemas/test/reqs2/meta/requirements.yml7
-rw-r--r--test/schemas/test/reqs4/meta/requirements.yml6
-rw-r--r--test/schemas/test/reqs5/meta/requirements.yml3
-rw-r--r--test/schemas/test/roles/empty-meta/meta/main.yml1
-rw-r--r--test/schemas/test/roles/foo/meta/argument_specs.yml74
-rw-r--r--test/schemas/test/roles/foo/meta/main.yml46
-rw-r--r--test/schemas/test/roles/foo/meta/runtime.yml39
-rw-r--r--test/schemas/test/roles/maximum/meta/main.yml20
-rw-r--r--test/schemas/test/roles/meta-tags/meta/main.yml25
-rw-r--r--test/schemas/test/roles/ns/meta/main.yml13
-rw-r--r--test/schemas/test/roles/v1_role/meta/main.yml12
-rw-r--r--test/schemas/test/tests/integration/rom_role/meta/main.yml5
-rw-r--r--test/schemas/tsconfig.json17
-rw-r--r--test/test_ansiblelintrule.py31
-rw-r--r--test/test_ansiblesyntax.py19
-rw-r--r--test/test_app.py30
-rw-r--r--test/test_cli.py215
-rw-r--r--test/test_cli_role_paths.py194
-rw-r--r--test/test_config.py16
-rw-r--r--test/test_constants.py9
-rw-r--r--test/test_dependencies_in_meta.py10
-rw-r--r--test/test_examples.py102
-rw-r--r--test/test_file_path_evaluation.py130
-rw-r--r--test/test_file_utils.py538
-rw-r--r--test/test_formatter.py68
-rw-r--r--test/test_formatter_base.py74
-rw-r--r--test/test_formatter_json.py138
-rw-r--r--test/test_formatter_sarif.py192
-rw-r--r--test/test_import_include_role.py157
-rw-r--r--test/test_import_playbook.py18
-rw-r--r--test/test_import_tasks.py29
-rw-r--r--test/test_include_miss_file_with_role.py43
-rw-r--r--test/test_internal_rules.py8
-rw-r--r--test/test_lint_rule.py46
-rw-r--r--test/test_list_rules.py76
-rw-r--r--test/test_load_failure.py25
-rw-r--r--test/test_loaders.py121
-rw-r--r--test/test_local_content.py13
-rw-r--r--test/test_main.py84
-rw-r--r--test/test_matcherrror.py208
-rw-r--r--test/test_mockings.py18
-rw-r--r--test/test_profiles.py60
-rw-r--r--test/test_rule_properties.py16
-rw-r--r--test/test_rules_collection.py175
-rw-r--r--test/test_runner.py210
-rw-r--r--test/test_schemas.py109
-rw-r--r--test/test_skip_import_playbook.py49
-rw-r--r--test/test_skip_inside_yaml.py41
-rw-r--r--test/test_skip_playbook_items.py121
-rw-r--r--test/test_skiputils.py252
-rw-r--r--test/test_strict.py30
-rw-r--r--test/test_task_includes.py47
-rw-r--r--test/test_text.py75
-rw-r--r--test/test_transform_mixin.py134
-rw-r--r--test/test_transformer.py175
-rw-r--r--test/test_utils.py449
-rw-r--r--test/test_verbosity.py90
-rw-r--r--test/test_with_skip_tagid.py58
-rw-r--r--test/test_yaml_utils.py955
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