Skip to content
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

Track component initialisations for error messages #420

Merged
merged 10 commits into from
Jun 25, 2024
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-----------------------

- batou migrate now writes .batou.json with a newline at the end as `pre-commit` hooks expect (usually).
- Unused Components, that is, Components that are initialized, but not used in the deployment, are now reported as warnings.


## 2.5.0b2 (2024-05-15)
Expand Down
31 changes: 31 additions & 0 deletions src/batou/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,37 @@ def report(self):
)


class UnusedComponentsInitialized(ConfigurationError):
"""Some components were initialized but never used."""

sort_key = (5, "unused")

@classmethod
def from_context(cls, components, root):
self = cls()
self.unused_components = []
self.breadcrumbs = []
for component in components:
self.unused_components.append(repr(component.__class__.__name__))
self.breadcrumbs.append(component._init_breadcrumbs)
self.root_name = root.name
return self

def __str__(self):
out_str = "Unused components: "
for i, component in enumerate(self.unused_components):
out_str += f"\n {component}: {' -> '.join(self.breadcrumbs[i])}"
return out_str

def report(self):
output.error(f"Unused components:")
for i, component in enumerate(self.unused_components):
output.line(
f" {component}: {' -> '.join(self.breadcrumbs[i])}", red=True
)
output.tabular("Root", self.root_name, red=True)


class UnsatisfiedResources(ConfigurationError):
"""Some required resources were never provided."""

Expand Down
32 changes: 32 additions & 0 deletions src/batou/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import types
import weakref
from typing import List

import batou
import batou.c
Expand Down Expand Up @@ -146,6 +147,13 @@ class Component(object):
#: working directory to this.
workdir: str = None

#: A list of all component instances that have been created. When
#: a component is created (__init__) it is added to this list.
#: After the configuration phase, this list is checked for
#: components that have component._prepared == False and
#: warns about them.
instances: List["Component"] = []

@property
def defdir(self):
"""(*readonly*) The definition directory
Expand Down Expand Up @@ -188,6 +196,30 @@ def root(self):
_prepared = False

def __init__(self, namevar=None, **kw):
init_stack = inspect.stack()
init_stack.reverse()
init_breadcrumbs = []
for frame in init_stack:
if (
"self" in frame.frame.f_locals
and isinstance(frame.frame.f_locals["self"], Component)
and frame.frame.f_code.co_name == "configure"
):
component = frame.frame.f_locals["self"]
try:
init_breadcrumbs.append(component._breadcrumb)
except AttributeError:
# some ._breadcrumb are broken
breadcrumb = component.__class__.__name__
if component.namevar:
breadcrumb += (
f"({getattr(component, component.namevar, None)})"
)
init_breadcrumbs.append(breadcrumb)

self._init_breadcrumbs = init_breadcrumbs

Component.instances.append(self)
self.timer = batou.utils.Timer(self.__class__.__name__)
# Are any keyword arguments undefined attributes?
# This is a somewhat rough implementation as it allows overriding
Expand Down
17 changes: 16 additions & 1 deletion src/batou/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
SuperfluousSection,
UnknownComponentConfigurationError,
UnsatisfiedResources,
UnusedComponentsInitialized,
UnusedResources,
)
from batou._output import output
from batou.component import ComponentDefinition, RootComponent
from batou.component import Component, ComponentDefinition, RootComponent
from batou.provision import Provisioner
from batou.repository import Repository
from batou.utils import CycleError, cmd
Expand Down Expand Up @@ -497,6 +498,7 @@ def configure(self):

for root in working_set:
try:
Component.instances.clear()
self.resources.reset_component_resources(root)
root.overrides = self.overrides.get(root.name, {})
root.prepare()
Expand All @@ -516,6 +518,19 @@ def configure(self):
)
)
else:
unprepared_components = []
for component in Component.instances:
if not component._prepared:
unprepared_components.append(component)
if unprepared_components:
# TODO: activate in future
unused_exception = (
UnusedComponentsInitialized.from_context(
unprepared_components, root
)
)
# exceptions.append(unused_exception)
output.warn(str(unused_exception))
# configured this component successfully
# we won't have to retry it later
continue
Expand Down
6 changes: 6 additions & 0 deletions src/batou/remote_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def error(self, message, exc_info=None, debug=False):
out = " " + out.replace("\n", "\n ") + "\n"
self.backend.write(out, red=True)

def warn(self, message, debug=False):
if debug and not self.enable_debug:
return
self.flush_buffer()
self.step("WARN", message, yellow=True)


class ChannelBackend(object):
def __init__(self, channel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ def configure(self):
class Sub(Component):
def configure(self):
self.log("Sub!")


class Unused(Component):
pass


class BadUnused(Component):
def configure(self):
Unused()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[environment]
connect_method = local

[hosts]
localhost = badunused
11 changes: 11 additions & 0 deletions src/batou/tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import batou
import batou.utils
from batou.component import Component, RootComponent
from batou.environment import Config, Environment
from batou.host import Host

Expand Down Expand Up @@ -331,3 +332,13 @@ def test_resolver_overrides_invalid_address(sample_service):
errors = e.configure()
assert len(errors) == 1
assert "thisisinvalid" == errors[0].address


def test_unused_components_get_reported(sample_service, output):
e = Environment("test-unused")
e.load()

output.backend.output = ""
e.configure()

assert "'Unused': BadUnused" in output.backend.output
Loading