Skip to content

Cover vm_manager_cluster and get the main gate back to green - #97

Open
insatomcat wants to merge 9 commits into
mainfrom
cluster-unit-coverage
Open

Cover vm_manager_cluster and get the main gate back to green#97
insatomcat wants to merge 9 commits into
mainfrom
cluster-unit-coverage

Conversation

@insatomcat

@insatomcat insatomcat commented Aug 7, 2026

Copy link
Copy Markdown
Member

After #94 and #96, one condition stayed red on main: new_coverage at 21.3% for a threshold of 80. Of the 408 uncovered units the new code period counts, all 408 were in vm_manager_cluster.py, a module at 5.6%.

This branch covers it: 228 unit tests, no cluster needed. RbdManager, Pacemaker, LibVirtManager and the subprocess calls are replaced by recording fakes that expose only what the code is expected to call, so an unexpected call fails loudly instead of returning a Mock. The end-to-end suite in test_vm_manager_cluster.py is untouched and CI still ignores it.

The module goes from 5.6% to 86% and owes the period nothing. Projected new_coverage on main: 21.3% to 87.0%.

Three bugs turned up while writing the tests, each fixed in its own commit:

  • clone rolled back over src_additional_count before it was bound, so a failed system disk copy raised UnboundLocalError and hid the real error.
  • create validated the three pacemaker options only when metadata was passed too, so a string went through and the next clone of that VM failed on reading it back.
  • _configure_vm had a "disk_bus" in vm_options guard that could never be false.

The last commit adds one line to sonar-project.properties. The hand-run integration scripts under vm_manager/helpers/tests, plus conf.py and setup.py, account for 49 uncovered units no test will ever reach, and pyproject.toml already omits the scripts from the coverage run for the same reason. sonar.coverage.exclusions rather than sonar.exclusions or sonar.tests on purpose: the files stay analysed, so their issues keep being reported.

Fourteen functions in the module are still uncovered. Their lines predate the new code period, so they weigh nothing on the gate, but they stand between the project and the OpenSSF gold target. The fakes are in place to deal with them in a next PR.

vm_manager_cluster.py was at 5.6%, and clone() alone accounted for 112
of the 408 uncovered units the SonarCloud new code period counts on that
module. Cover it whole, lines and branches.

The suite replaces every cluster access: RbdManager by a recording fake
usable as a context manager, Pacemaker by a stub host validator, and the
three functions clone() delegates to by recorders, so a failure points
at clone() and nowhere else. The fake only exposes the methods clone()
is expected to call, so an unexpected one fails with an AttributeError
instead of returning a Mock. A copy carries the source metadata over,
because clone() relies on that to know what to strip from the clone.

One test is a strict xfail: the rollback loops over
src_additional_count, which is only assigned after the system disk copy,
so any failure before that raises UnboundLocalError from the except
branch and hides the error that caused the rollback. The fix belongs to
a change of its own.

The module reaches 24%, and new_coverage would go from 21.3% to 37.8%.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
Next two functions of the module by uncovered weight, 63 and 41 of the
408 units the new code period counts there. Both are covered whole,
lines and branches, which brings the module to 40% and the three
functions done so far to 217 of those 408 units.

create() needs two collaborators clone() did not: _create_xml, which
shells out to ceph orch and reads the libvirt secrets, and
check_uuid_conflict, which walks every group in Ceph. Both become
recorders. The file existence checks run for real against files in
tmp_path, with CEPH_CONF pointed at one of them, rather than against a
patched os.path.

add_to_cluster() reads the source domain from libvirt, so the fake
LibVirtManager here also exposes the _conn.lookupByName path the
function uses directly, and a fake domain with the three methods it
calls on it.

One more strict xfail: create() validates pacemaker_meta,
pacemaker_params and pacemaker_utilization inside the 'if "metadata" in
vm_options' block, so a non-dict goes through unchecked whenever no
metadata is passed. _configure_vm then stores the string as JSON and the
next clone of that VM fails on it. clone() validates the same three
options unconditionally.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
Two more functions of the module, 31 units each. The module reaches 63%
and the five functions done so far clear 278 of the 408 units the new
code period counts there.

Both are reached through the recorders the earlier tests installed, so
each gets a fixture putting the real function back: the clone and create
tests need _configure_vm mocked, and _configure_vm itself needs
enable_vm mocked. The Pacemaker stub grows from a bare host validator
into a recording context manager, since enable_vm drives it through
eleven calls, and the cluster resources live on the shared collaborators
because the code opens a new Pacemaker per operation.

enable_vm reads fifteen metadata keys, each in its own try/except. Rather
than one test per key, one test asserts every default when the disk
carries nothing and one asserts every value when it carries all of them,
which covers both sides of all fifteen.

One branch in _configure_vm stays uncovered because it cannot be
reached: the "disk_bus" in vm_options guard is dead code, the
unconditional vm_options["disk_bus"] earlier in the same function
already requires the key. Removing the guard belongs to a change of its
own.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
src_additional_count was assigned after the system disk copy, but the
except branch loops over it to put the source additional disks back in
their group. Any failure before that assignment therefore left it
unbound: image_exists, remove_image or copy_image raising, or a copy
that reports success and creates nothing. The rollback then raised
UnboundLocalError, and the caller saw that instead of the real error.

Bind it before the try. The test that documented the behaviour as a
strict xfail becomes a plain test.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
The three isinstance checks on pacemaker_meta, pacemaker_params and
pacemaker_utilization sat inside the "if metadata in vm_options" block,
so they only ran when the caller also passed metadata. Without it a
string went through unchecked, _configure_vm stored it as JSON, and the
next clone of that VM failed on reading it back with "pacemaker_meta
metadata must be a dictionary", far from the call that caused it.

Dedent the loop, so create() validates the three options the way clone()
already does. The strict xfail that documented the hole becomes a plain
test, parametrized over the three options.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
The "disk_bus" in vm_options check could never be false: the same
function reads vm_options["disk_bus"] unconditionally when it builds the
XML, forty lines above, so a missing key raises KeyError long before.
The guard only made the branch impossible to cover.

Keep the body, drop the condition. The five functions covered so far now
have no uncovered line and no uncovered branch left.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
Three more functions, plus _get_all_disk_names which the purge tests
reach on the way. The module goes to 75%, and only 60 of the 408 units
the new code period counts on it are left, all in seven functions that
have no test yet.

The fake Ceph grows the group and snapshot side it had not needed so
far: list_groups, group_exists, list_group_images, remove_group, the
three snapshot calls and purge_image, plus two injection points for
objects that survive their own removal, which is the only way to reach
the two "Could not remove" guards in remove().

purge_image has three modes that share nothing, so they get three
classes. The number mode realigns disks that hold more snapshots than
the others, from a purge that failed halfway, and that is worth its own
test since the arithmetic is not obvious.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
The last seven functions the new code period still counted: _create_xml,
rollback_snapshot, console, create_snapshot, remove_snapshot,
_get_remote_nodes and _get_ceph_hosts_xml, plus _get_observer_host which
carries no weight but sat next to them. The module reaches 86% and owes
the gate nothing.

Three of them shell out, so the fake replaces the subprocess module
inside vm_manager_cluster rather than patching the real one. _create_xml
also reads the libvirt secrets and the Ceph monitor list, both of which
the fakes now serve.

_get_observer_host reads a hardcoded /etc/cluster.conf, so open is the
only seam. It is replaced by a wrapper that redirects that single path
to a temporary file and delegates every other one to the real open.

What is left uncovered in the module is fourteen functions whose lines
predate the new code period: disable_vm, start, stop, status, list_vms,
the metadata accessors and the pacemaker_remote pair. They weigh nothing
on the gate but still stand between the project and the OpenSSF target.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
Three paths could never be covered by the pytest suite and only dragged
the coverage down: vm_manager/helpers/tests holds the integration
scripts driven by hand against a real cluster, and conf.py and setup.py
are build entry points. Together they account for 49 of the units the
new code period counts, all of them uncovered, and no test will ever
touch them.

pyproject.toml already omits the integration scripts from the coverage
run, with the same reasoning written next to it since the suite was
introduced. This only puts SonarCloud in step with a decision the
repository had already made and documented.

sonar.coverage.exclusions rather than sonar.exclusions or sonar.tests on
purpose: the files stay analysed, so the issues they carry keep being
reported. Only the coverage expectation goes away.

Signed-off-by: Florent Carli <florent.carli@rte-france.com>
@insatomcat
insatomcat force-pushed the cluster-unit-coverage branch from cc65cfe to f6a3157 Compare August 7, 2026 20:26
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant