diff --git a/kepler/pyproject.toml b/kepler/pyproject.toml index 3b3da6f..65920e2 100644 --- a/kepler/pyproject.toml +++ b/kepler/pyproject.toml @@ -42,7 +42,7 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant>=1.8,<2", + "jubilant==1.12.0", "pytest", "pytest-jubilant>=2.0.1,<3", "PyYAML", @@ -59,6 +59,16 @@ show_missing = true minversion = "6.0" log_cli_level = "INFO" +# Retain INFO logs in the "Captured log call" section when run interactively. +# Otherwise, that section will have DEBUG logs (coming from log_file_level). +log_level = "INFO" + +log_cli_format = "%(levelname)s %(name)s %(message)s" + +log_file_level = "DEBUG" +log_file_format = "%(asctime)s %(levelname)s %(name)s %(message)s" +log_file_date_format = "%Y-%m-%dT%H:%M:%SZ" + # Linting tools configuration [tool.ruff] line-length = 99 diff --git a/kepler/tests/integration/test_log_level.py b/kepler/tests/integration/test_log_level.py new file mode 100644 index 0000000..737d674 --- /dev/null +++ b/kepler/tests/integration/test_log_level.py @@ -0,0 +1,40 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Differential test: does log_level = INFO filter DEBUG logs from caplog? + +The same test runs in two charms. One charm (kepler) has log_level = "INFO" +set in pyproject.toml alongside log_file_level = "DEBUG"; the other (kosmos) +has only log_file_level = "DEBUG". The test emits a DEBUG log via the +jubilant.wait logger and asserts it appears in caplog records. + +Without log_level, pytest's log_file_level = DEBUG lowers the root logger to +DEBUG and the caplog handler (left at NOTSET) captures DEBUG records. With +log_level = INFO, the caplog handler is raised to INFO and filters them out. +""" + +import logging + +import pytest + +_WAIT_LOGGER = logging.getLogger("jubilant.wait") +_DEBUG_MESSAGE = "differential test: debug log from jubilant.wait" + + +@pytest.mark.xfail(strict=True, reason="log_level=INFO filters DEBUG from caplog") +def test_debug_log_from_jubilant_wait_is_captured(caplog: pytest.LogCaptureFixture): + """Assert that a DEBUG log from jubilant.wait appears in caplog records.""" + _WAIT_LOGGER.debug(_DEBUG_MESSAGE) + captured = [record.getMessage() for record in caplog.records] + assert _DEBUG_MESSAGE in captured diff --git a/kepler/uv.lock b/kepler/uv.lock index 9714747..2486874 100644 --- a/kepler/uv.lock +++ b/kepler/uv.lock @@ -98,13 +98,46 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/c3/13eb3579d021346c95d0622ebbcce7d3326ed6412d0b51d4d3df2461443d/jubilant-1.12.0.tar.gz", hash = "sha256:7d796d5c19f0b74a041f50aa68919731c7c83da4fb8a8258c8861524532b6997", size = 35291, upload-time = "2026-07-30T03:45:32.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/17/0569153f6aca13716e019243d01895f26061233bae7e21a459cc128e7120/jubilant-1.12.0-py3-none-any.whl", hash = "sha256:fa64922156d861149666c8ce08cb93b1fd787172fdd12175a4d0563955a67544", size = 37077, upload-time = "2026-07-30T03:45:31.764Z" }, +] + [[package]] name = "kosmos" version = "0.0.1" @@ -136,7 +169,7 @@ requires-dist = [{ name = "ops", specifier = "~=3.7" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "jubilant", specifier = "==1.12.0" }, { name = "pytest" }, { name = "pytest-jubilant", specifier = ">=2.0.1,<3" }, { name = "pyyaml" }, @@ -152,39 +185,6 @@ unit = [ { name = "pytest" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1" diff --git a/kosmos/pyproject.toml b/kosmos/pyproject.toml index 7fcdddd..52550f0 100644 --- a/kosmos/pyproject.toml +++ b/kosmos/pyproject.toml @@ -42,7 +42,7 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant>=1.8,<2", + "jubilant==1.12.0", "pytest", "pytest-jubilant>=2.0.1,<3", "PyYAML", @@ -59,6 +59,12 @@ show_missing = true minversion = "6.0" log_cli_level = "INFO" +log_cli_format = "%(levelname)s %(name)s %(message)s" + +log_file_level = "DEBUG" +log_file_format = "%(asctime)s %(levelname)s %(name)s %(message)s" +log_file_date_format = "%Y-%m-%dT%H:%M:%SZ" + # Linting tools configuration [tool.ruff] line-length = 99 diff --git a/kosmos/tests/integration/test_log_level.py b/kosmos/tests/integration/test_log_level.py new file mode 100644 index 0000000..22a2578 --- /dev/null +++ b/kosmos/tests/integration/test_log_level.py @@ -0,0 +1,39 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Differential test: does log_level = INFO filter DEBUG logs from caplog? + +The same test runs in two charms. One charm (kepler) has log_level = "INFO" +set in pyproject.toml alongside log_file_level = "DEBUG"; the other (kosmos) +has only log_file_level = "DEBUG". The test emits a DEBUG log via the +jubilant.wait logger and asserts it appears in caplog records. + +Without log_level, pytest's log_file_level = DEBUG lowers the root logger to +DEBUG and the caplog handler (left at NOTSET) captures DEBUG records. With +log_level = INFO, the caplog handler is raised to INFO and filters them out. +""" + +import logging + +import pytest + +_WAIT_LOGGER = logging.getLogger("jubilant.wait") +_DEBUG_MESSAGE = "differential test: debug log from jubilant.wait" + + +def test_debug_log_from_jubilant_wait_is_captured(caplog: pytest.LogCaptureFixture): + """Assert that a DEBUG log from jubilant.wait appears in caplog records.""" + _WAIT_LOGGER.debug(_DEBUG_MESSAGE) + captured = [record.getMessage() for record in caplog.records] + assert _DEBUG_MESSAGE in captured diff --git a/kosmos/uv.lock b/kosmos/uv.lock index 13d7a4a..97e869f 100644 --- a/kosmos/uv.lock +++ b/kosmos/uv.lock @@ -98,13 +98,46 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/c3/13eb3579d021346c95d0622ebbcce7d3326ed6412d0b51d4d3df2461443d/jubilant-1.12.0.tar.gz", hash = "sha256:7d796d5c19f0b74a041f50aa68919731c7c83da4fb8a8258c8861524532b6997", size = 35291, upload-time = "2026-07-30T03:45:32.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/17/0569153f6aca13716e019243d01895f26061233bae7e21a459cc128e7120/jubilant-1.12.0-py3-none-any.whl", hash = "sha256:fa64922156d861149666c8ce08cb93b1fd787172fdd12175a4d0563955a67544", size = 37077, upload-time = "2026-07-30T03:45:31.764Z" }, +] + [[package]] name = "kepler" version = "0.0.1" @@ -136,7 +169,7 @@ requires-dist = [{ name = "ops", specifier = "~=3.7" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "jubilant", specifier = "==1.12.0" }, { name = "pytest" }, { name = "pytest-jubilant", specifier = ">=2.0.1,<3" }, { name = "pyyaml" }, @@ -152,39 +185,6 @@ unit = [ { name = "pytest" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1"