Skip to content

Enable testing with python 3.14 #4637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/requirements-test.in
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pytest-mock
pytest-plus >= 0.6
pytest-sugar # shows failures immediately, even with xdist
pytest-xdist[psutil,setproctitle] >= 2.1.0
ruamel-yaml-clib # needed for mypy
ruamel-yaml-clib; python_version < '3.14' # needed for mypy
ruamel.yaml>=0.18.11
tox >= 4.0.0
tox-extra>=2.1
Expand Down
29 changes: 9 additions & 20 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,12 @@ jobs:
uses: coactions/dynamic-matrix@v4
with:
min_python: "3.10"
max_python: "3.13"
default_python: "3.10"
max_python: "3.14"
default_python: "3.13"
other_names: |
lint
pkg
lint-pkg-schemas-eco:tox -e lint;tox -e pkg;tox -e schemas;tox -e eco
hook
docs
schemas
eco
pre
py311-devel
py310-lower
Expand Down Expand Up @@ -97,25 +94,18 @@ jobs:
fetch-depth: 0 # needed by setuptools-scm
submodules: true

- name: Set pre-commit cache
uses: actions/cache@v4
if: ${{ matrix.name == 'lint' }}
with:
path: |
~/.cache/pre-commit
key: pre-commit-${{ matrix.name }}-${{ hashFiles('.pre-commit-config.yaml') }}

- name: Set ansible cache(s)
- name: Set build cache(s)
uses: actions/cache@v4
with:
path: |
.cache/eco
.tox
examples/playbooks/collections/ansible_collections
~/.cache/ansible-compat
~/.ansible/collections
~/.ansible/roles
.tox
key: ${{ matrix.name }}-${{ matrix.os }}--${{ hashFiles('tools/test-eco.sh', 'requirements.yml', 'examples/playbooks/collections/requirements.yml') }}
~/.cache/ansible-compat
~/.cache/pre-commit
key: ${{ matrix.name }}-${{ matrix.os }}--${{ hashFiles('.pre-commit-config.yaml', 'tools/test-eco.sh', 'requirements.yml', 'examples/playbooks/collections/requirements.yml') }}

- name: Set up Python ${{ matrix.python_version || '3.10' }}
if: "!contains(matrix.shell, 'wsl')"
Expand Down Expand Up @@ -162,7 +152,6 @@ jobs:
include-hidden-files: true
if-no-files-found: ignore
path: |
.tox/**/.coverage*
.tox/**/coverage.xml

- name: Report failure if git reports dirty status
Expand Down Expand Up @@ -253,7 +242,7 @@ jobs:

- name: Check for expected number of coverage.xml reports
run: |
JOBS_PRODUCING_COVERAGE=10
JOBS_PRODUCING_COVERAGE=12
if [ "$(find . -name coverage.xml | wc -l | bc)" -ne "${JOBS_PRODUCING_COVERAGE}" ]; then
echo "::error::Number of coverage.xml files was not the expected one (${JOBS_PRODUCING_COVERAGE}): $(find . -name coverage.xml |xargs echo)"
exit 1
Expand Down
12 changes: 10 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,20 @@ def is_master(config: pytest.Config) -> bool:
stacklevel=1,
)
else:
pytest.fail(
"FATAL: For testing, we require pyyaml to be installed with its native extension, missing it would make testing 3x slower and risk missing essential bugs.",
warnings.warn(
"Some tests are skipped because when pyyaml precompile lib is missing they produce different results. This is also making testing 3x slower.",
category=pytest.PytestWarning,
stacklevel=1,
)


@pytest.fixture(name="project_path")
def fixture_project_path() -> Path:
"""Fixture to linter root folder."""
return Path(__file__).resolve().parent


def pytest_runtest_setup(item: pytest.Item) -> None:
"""Filters some tests if libyaml is not available."""
if not HAS_LIBYAML and list(item.iter_markers("libyaml")):
pytest.skip("skipped because libyaml is not installed")
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python",
"Topic :: System :: Systems Administration",
Expand Down Expand Up @@ -409,6 +410,9 @@ filterwarnings = [
"ignore:Attribute s is deprecated and will be removed in Python 3.14; use value instead:DeprecationWarning"
]
junit_family = "legacy"
markers = [
"libyaml: tests that will pass only with pyyaml libyaml is present."
]
minversion = "4.6.6"
# https://code.visualstudio.com/docs/python/testing
# coverage is re-enabled in `tox.ini`. That approach is safer than
Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/fqcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,12 @@ def transform(

# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
import pytest

from ansiblelint.rules import RulesCollection
from ansiblelint.runner import Runner

@pytest.mark.libyaml
def test_fqcn_builtin_fail() -> None:
"""Test rule matches."""
collection = RulesCollection()
Expand Down
5 changes: 3 additions & 2 deletions src/ansiblelint/rules/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ def blacken(text: str) -> str:
from ansiblelint.runner import Runner
from ansiblelint.transformer import Transformer

@pytest.mark.libyaml
def test_jinja_spacing_playbook() -> None:
"""Ensure that expected error lines are matching found linting error lines."""
# list unexpected error lines or non-matching error lines
Expand Down Expand Up @@ -853,10 +854,10 @@ def test_jinja_invalid() -> None:
assert len(errs) == 2
assert errs[0].tag == "jinja[spacing]"
assert errs[0].rule.id == "jinja"
assert errs[0].lineno == 9
assert errs[0].lineno in [9, 13] # ruamel w/ clib return different numbers
assert errs[1].tag == "jinja[invalid]"
assert errs[1].rule.id == "jinja"
assert errs[1].lineno in [9, 10] # 2.19 has better line identification
assert errs[1].lineno in [9, 10, 13] # 2.19 has better line identification

def test_jinja_valid() -> None:
"""Tests our ability to parse jinja, even when variables may not be defined."""
Expand Down
1 change: 1 addition & 0 deletions src/ansiblelint/rules/no_log_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ def test_password_lock_false(rule_runner: RunFromText) -> None:
results = rule_runner.run_playbook(PASSWORD_LOCK_FALSE)
assert len(results) == 0

@pytest.mark.libyaml
@mock.patch.dict(os.environ, {"ANSIBLE_LINT_WRITE_TMP": "1"}, clear=True)
def test_no_log_password_transform(
config_options: Options,
Expand Down
3 changes: 3 additions & 0 deletions src/ansiblelint/rules/no_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ def matchtask(

# testing code to be loaded only with pytest or when executed the rule file
if "pytest" in sys.modules:
import pytest

from ansiblelint.rules import RulesCollection
from ansiblelint.runner import Runner

@pytest.mark.libyaml
def test_no_tabs_rule(default_rules_collection: RulesCollection) -> None:
"""Test rule matches."""
results = Runner(
Expand Down
1 change: 1 addition & 0 deletions src/ansiblelint/rules/var_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ def test_var_naming_with_role_prefix(
for result in results:
assert result.tag == "var-naming[no-role-prefix]"

@pytest.mark.libyaml
def test_var_naming_with_role_prefix_plays(
default_rules_collection: RulesCollection,
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/ansiblelint/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def run(self) -> list[MatchError]:
warn.category.__name__,
warn.message,
)
return matches
return sorted(matches)

def _run(self) -> list[MatchError]:
"""Run the linting (inner loop)."""
Expand Down
2 changes: 1 addition & 1 deletion test/rules/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_args_module_relative_import(default_rules_collection: RulesCollection)
)
result = Runner(lintable, rules=default_rules_collection).run()
assert len(result) == 1, result
assert result[0].lineno == 5
assert result[0].lineno in [5, 7]
assert result[0].filename == "examples/playbooks/module_relative_import.yml"
assert result[0].tag == "args[module]"
assert result[0].message == "missing required arguments: name"
2 changes: 2 additions & 0 deletions test/test_cli_role_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def test_run_role_three_dir_deep(local_test_dir: Path) -> None:
assert "Use shell only when shell functionality is required" in result.stdout


@pytest.mark.libyaml
def test_run_playbook(local_test_dir: Path) -> None:
"""Call ansible-lint the way molecule does."""
cwd = local_test_dir / "roles" / "test-role"
Expand Down Expand Up @@ -174,6 +175,7 @@ def test_run_single_role_path_with_roles_path_env(local_test_dir: Path) -> None:
assert "Use shell only when shell functionality is required" in result.stdout


@pytest.mark.libyaml
@pytest.mark.parametrize(
("result", "env"),
(
Expand Down
8 changes: 8 additions & 0 deletions test/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def fixture_runner_result(
),
)
@mock.patch.dict(os.environ, {"ANSIBLE_LINT_WRITE_TMP": "1"}, clear=True)
@pytest.mark.libyaml
def test_transformer( # pylint: disable=too-many-arguments,too-many-positional-arguments
config_options: Options,
playbook_str: str,
Expand Down Expand Up @@ -373,6 +374,7 @@ class TestRule(AnsibleLintRule, TransformMixin):
match.rule.transform.assert_not_called() # type: ignore[attr-defined]


@pytest.mark.libyaml
def test_pruned_err_after_fix(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> None:
"""Test that pruned errors are not reported after fixing.

Expand Down Expand Up @@ -464,6 +466,7 @@ def rewrite_part(cls) -> str:
return f"{cls.FILE_NAME} ({cls.FILE_TYPE}), {cls.VERSION_PART}"


@pytest.mark.libyaml
@pytest.fixture(name="test_result")
def fixture_test_result(
config_options: Options,
Expand Down Expand Up @@ -501,6 +504,7 @@ def write(*_args: Any, **_kwargs: Any) -> None:
return result, config_options


@pytest.mark.libyaml
def test_transform_na(
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
Expand Down Expand Up @@ -544,6 +548,7 @@ def mp_isinstance(t_object: Any, classinfo: type) -> bool:
assert logs[1].levelname == "DEBUG"


@pytest.mark.libyaml
def test_transform_no_tb(
caplog: pytest.LogCaptureFixture,
test_result: tuple[LintResult, Options],
Expand Down Expand Up @@ -599,6 +604,7 @@ def transform(*_args: Any, **_kwargs: Any) -> None:
assert logs[4].levelname == "DEBUG"


@pytest.mark.libyaml
def test_transform_applied(
caplog: pytest.LogCaptureFixture,
test_result: tuple[LintResult, Options],
Expand Down Expand Up @@ -631,6 +637,7 @@ def test_transform_applied(
assert logs[2].levelname == "DEBUG"


@pytest.mark.libyaml
def test_transform_not_enabled(
caplog: pytest.LogCaptureFixture,
test_result: tuple[LintResult, Options],
Expand Down Expand Up @@ -660,6 +667,7 @@ def test_transform_not_enabled(
assert logs[1].levelname == "DEBUG"


@pytest.mark.libyaml
def test_transform_not_applied(
caplog: pytest.LogCaptureFixture,
test_result: tuple[LintResult, Options],
Expand Down
1 change: 1 addition & 0 deletions test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def test_logger_debug(caplog: LogCaptureFixture) -> None:
assert expected_info in caplog.record_tuples


@pytest.mark.libyaml
def test_cli_auto_detect(capfd: CaptureFixture[str]) -> None:
"""Test that run without arguments it will detect and lint the entire repository."""
cmd = [
Expand Down
11 changes: 8 additions & 3 deletions tools/report-coverage
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/bin/bash
# cspell: ignore nullglob
set -euo pipefail
coverage combine -q "--data-file=${TOX_ENV_DIR}/.coverage" "${TOX_ENV_DIR}"/.coverage.*
coverage xml "--data-file=${TOX_ENV_DIR}/.coverage" -o "${TOX_ENV_DIR}/coverage.xml" --ignore-errors --fail-under=0
COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage report --fail-under=0 --ignore-errors
shopt -s nullglob
files=("${TOX_ENV_DIR}"/.coverage.*)
if (( ${#files[@]} )); then
coverage combine -q "--data-file=${TOX_ENV_DIR}/.coverage" "${files[@]}"
coverage xml "--data-file=${TOX_ENV_DIR}/.coverage" -o "${TOX_ENV_DIR}/coverage.xml" --ignore-errors --fail-under=0
COVERAGE_FILE="${TOX_ENV_DIR}/.coverage" coverage report --fail-under=0 --ignore-errors
fi
14 changes: 5 additions & 9 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ commands =
--durations=10 \
--junitxml=./junit.xml \
}
commands_post =
{py,py310,py311,py312,py313}: ./tools/report-coverage
{tox_root}/tools/report-coverage
allowlist_externals =
./tools/report-coverage
./tools/test-hook.sh
bash
find
git
npm
pwd
rm
sh
tox
{tox_root}/tools/report-coverage
{tox_root}/tools/test-hook.sh
{work_dir}/.pipx/bin/ansible-lint

[testenv:lint]
Expand Down Expand Up @@ -135,7 +135,7 @@ description = Validate pre-commit hook definition
deps =
pre-commit
commands =
./tools/test-hook.sh
{tox_root}/tools/test-hook.sh

[testenv:docs]
description = Builds docs
Expand All @@ -162,8 +162,6 @@ commands_pre =
npm install
commands =
npm test
allowlist_externals =
npm

[testenv:lower]
description = Install using lower-constraints.txt file for testing oldest versions.
Expand All @@ -176,8 +174,6 @@ extras =
test
commands =
sh -c tools/test-eco.sh
allowlist_externals =
{[testenv]allowlist_externals}

[testenv:deps]
description = Bump all test dependencies
Expand Down
Loading