Skip to content

Implement the Test Kitchen 4 driver APIs and modernize the driver - #164

Merged
tas50 merged 1 commit into
mainfrom
driver-modernization
Aug 23, 2026
Merged

Implement the Test Kitchen 4 driver APIs and modernize the driver#164
tas50 merged 1 commit into
mainfrom
driver-modernization

Conversation

@tas50

@tas50 tas50 commented Aug 22, 2026

Copy link
Copy Markdown
Member

What this does

Audits the driver against test-kitchen 4.1.1 — the current release — and closes the gaps.

The dependency ceilings were already correct (< 5 for test-kitchen, train 3.16.5 and train-winrm 0.4.3 are both current). What was missing was the driver's side of three Test Kitchen APIs, plus two real bugs.

Test Kitchen APIs

pre_create_command was silently ignored

Kitchen::Driver::Base#create is not empty — it runs config[:pre_create_command]:

def create(state)
  pre_create_command
end

Hyperv#create overrode it and never called super, so anyone setting the option in kitchen.yml got silence, not an error. It now runs before any PowerShell reaches the host.

status(state) — backs kitchen list --probe

Test Kitchen 4 added this with an explicit note: "Drivers that can ask their provider should override this method." This driver can. Today it reports Kitchen::Driver::Hyperv does not support status checks.

It is deliberately read-only. Reusing the existing Assert-VmRunning would have been less code, but that helper starts a stopped VM — so probing would have had the side effect of booting instances. A new Get-VmStatus helper reads power state without changing it, and reports a stopped instance as stopped.

doctor(state) — backs kitchen doctor

Reports a missing Hyper-V module, an unreachable host, and a missing or unreadable parent VHD. Returns every problem it finds rather than stopping at the first, since they're usually related.

Bugs

Bug Impact
kitchen diagnose crashed diagnose evaluates every lazy default, and the disk_type block called File.extname on a nil parent_vhd_nameTypeError: no implicit conversion of nil into String. Reproduced before fixing.
Windows on ARM could not launch PowerShell at all The width checks matched only AMD64. On ARM64 both is_64bit? and is_32bit? were false, so powershell_64_bit fell through to the Sysnative path — which does not exist for a native 64-bit process. Every run_ps call failed.

Architecture detection now recognizes ARM64 and IA64, and reads Ruby's pointer width from RbConfig::SIZEOF rather than ["foo"].pack("p").size. Sysnative is reached only in the genuine WOW64 case it was written for — a 32-bit Ruby on 64-bit Windows — which now has its own test.

PowerShell: WMI → CIM

Set-VMNetworkConfiguration used Get-WmiObject and the [WMI] type accelerator, both removed in PowerShell 6. Behaviour on Windows PowerShell 5.1 is unchanged, but the static-IP path is no longer the thing that would hard-block running under pwsh.

  • Get-WmiObjectGet-CimInstance
  • .GetRelated(...)Get-CimAssociatedInstance -ResultClassName
  • $Service.SetGuestNetworkAdapterConfiguration($vm, $cfg.GetText(1))Invoke-CimMethod

On that last one: the documented MOF signature declares NetworkConfiguration as string[] of embedded instances, which the CIM layer serializes from the CimInstance — that's what replaces GetText(1).

Two behaviour changes worth calling out:

  • A failed configuration job now raises. It previously called $job.GetError(), which emits the error and carries on — hiding a failed address assignment behind an apparently successful create.
  • Fixed a latent typo: the trailing lookup read .NetworkAdapter (singular), which is always $null, rather than .NetworkAdapters. Harmless today because the driver discards that return value.

Modernization

  • copy_vm_files and dry_run are now declared via default_config. Both are documented in the README but were invisible to kitchen diagnose.
  • Dropped the unused require "mixlib/shellout" from both lib files — run_command comes from Kitchen::ShellOut via Driver::Base.
  • Added frozen_string_literal magic comments.
  • Renamed is_64bit?/is_32bit?sixty_four_bit?/thirty_two_bit?, keeping the old names as @deprecated aliases — this is a published mixin, so public methods don't just disappear.
  • Raised the test-kitchen floor to >= 3.0. The previous >= 1.4 predates pre_create_command, where the new super call would be a silent no-op.

Verification

rspec    120 examples, 0 failures
pester    45 passed, 0 failed      (pwsh 7.5.4 / Pester 6.1.0)
yard     100.00% documented, no warnings
rubocop  12 files inspected, no offenses
rake     exit 0
gem build succeeds

Mutation-tested, so the new tests demonstrably can fail:

Mutation Result
Drop super from create 2 RSpec failures
Hardcode status to running 1 RSpec failure
Revert ARM64 → AMD64-only 2 RSpec failures
Swallow the CIM job failure again 1 Pester failure

Notes for review

  • The CIM port is the one thing I could not verify. Pester proves the call shape — which cmdlets get invoked with which arguments — not that Hyper-V accepts the marshalled embedded instance. It wants a manual smoke test with ip_address set against a real host before release. Everything else is exercised by the suites.
  • >= 3.0 is the only user-visible dependency change. Test Kitchen 2 is EOL and Ruby >= 3.1 was already required, so this should affect nobody in practice.
  • hyperv_insecure still defaults to true. Flipping it would break every remote user relying on WinRM's self-signed certificate, so per discussion it keeps its default and gains a README note explaining exactly what it disables and when to turn it off.
  • Also documented the three diagnostics (kitchen list --probe, kitchen doctor, kitchen diagnose --all) in the README, including that probing is read-only.

🤖 Generated with Claude Code

@tas50
tas50 force-pushed the driver-modernization branch from 8c852c2 to 0d96d56 Compare August 22, 2026 20:48
Audits the driver against test-kitchen 4.1.1 (the current release) and
closes the gaps. The dependency ceilings were already right; what was
missing was the driver's side of three APIs, plus two real bugs.

Test Kitchen APIs

- pre_create_command was silently ignored. Kitchen::Driver::Base#create
  is not empty -- it runs config[:pre_create_command] -- and Hyperv#create
  overrode it without calling super, so anyone setting the option got
  nothing and no error. It now runs before any PowerShell.
- status(state) is implemented, backing `kitchen list --probe`. Test
  Kitchen 4 added it with an explicit note that drivers able to ask their
  provider should override it; this driver can. It is deliberately
  read-only and uses a new Get-VmStatus helper rather than
  Assert-VmRunning, so probing a stopped instance reports it as stopped
  instead of booting it.
- doctor(state) is implemented, backing `kitchen doctor`. It reports a
  missing Hyper-V module, an unreachable host, and a missing or
  unreadable parent VHD, and returns every problem it finds rather than
  stopping at the first.

Bugs

- `kitchen diagnose` raised TypeError: no implicit conversion of nil into
  String. diagnose evaluates every lazy default, and the disk_type block
  called File.extname on a nil parent_vhd_name.
- Windows on ARM hosts could not launch PowerShell at all. The width
  checks matched only AMD64, so on ARM64 both were false and the driver
  fell through to the Sysnative path, which does not exist for a native
  64-bit process. Architecture detection now recognizes ARM64 and IA64,
  and reads Ruby's pointer width from RbConfig::SIZEOF rather than
  ["foo"].pack("p").size. Sysnative is reached only in the genuine WOW64
  case it was written for.

PowerShell

Set-VMNetworkConfiguration is ported from Get-WmiObject and the [WMI]
type accelerator, both removed in PowerShell 6, to Get-CimInstance and
Invoke-CimMethod. Behaviour on Windows PowerShell 5.1 is unchanged; the
static IP path is no longer what would block running under pwsh. The
NetworkConfiguration parameter is declared string[] of embedded
instances, which the CIM layer serializes from the CimInstance, replacing
GetText(1). A failed configuration job now raises instead of emitting the
error and carrying on, which previously hid a failed address assignment
behind a successful create. Also fixes a latent typo: the trailing lookup
read .NetworkAdapter, which is always nil, rather than .NetworkAdapters.

Modernization

- copy_vm_files and dry_run are declared via default_config. Both are
  documented in the README but were invisible to `kitchen diagnose`.
- Dropped the unused mixlib/shellout requires from both lib files;
  run_command comes from Kitchen::ShellOut via Driver::Base.
- Added frozen_string_literal magic comments.
- Renamed is_64bit?/is_32bit? to sixty_four_bit?/thirty_two_bit?, keeping
  the old names as deprecated aliases since this is a published mixin.
- Raised the test-kitchen floor to >= 3.0. The previous >= 1.4 predates
  pre_create_command, where the new super call would be a no-op.

Testing

120 RSpec examples and 45 Pester tests, all green. New coverage for
status, doctor, pre_create_command ordering, the diagnose crash, ARM64
and genuine-32-bit path selection, Get-VmStatus, and the CIM port.
Mutation-tested: dropping super, hardcoding status to running, reverting
ARM64, and swallowing the CIM job failure each fail their tests.

The CIM port cannot be verified here -- Pester proves the call shape, not
that Hyper-V accepts it -- so it wants a manual smoke test with
ip_address set against a real host before release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tas50
tas50 force-pushed the driver-modernization branch from 0d96d56 to 124e872 Compare August 22, 2026 21:14
@tas50

tas50 commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

CI fix

Cookstyle failed with Style/RedundantFreeze on hyperv_version.rb:

lib/kitchen/driver/hyperv_version.rb:29:22: C: [Correctable] Style/RedundantFreeze
    HYPERV_VERSION = "0.11.0".freeze

Cause: rebasing this branch onto main after release-please shipped 0.11.0 (#163) resolved that line in favour of main — restoring .freeze while keeping the # frozen_string_literal: true comment this PR adds. With the magic comment the literal is already frozen, so .freeze became redundant. Dropped it.

Unit Test with Ruby showed as skipping rather than failing — that's a cascade, not a second problem. The shared workflow declares needs: [yamllint, cookstyle] on the test job, so one style offense gated all five Ruby versions.

Verified locally with the exact commands CI runs:

bundle exec cookstyle --chefstyle   12 files inspected, no offenses detected
bundle exec rake test               120 examples, 0 failures
bundle exec rake pester             45 passed, 0 failed
gem build                           Version: 0.11.0

On the size of the support/hyperv.ps1 diff

It reports ~497 changed lines for what is a ~40-line change. That's line endings, and it is not avoidable:

  • The blob on main is CRLF — last committed in f658583, back in 2020.
  • .gitattributes (added later, in 3669e28) sets * text=auto eol=lf.
  • git check-attr confirms eol: lf applies to this file, so git rewrites it to LF in the index on commit no matter what the working tree holds. I verified this in a scratch repo: staging a CRLF file under that attribute stores LF.

So the file was simply stale relative to the repo's own line-ending policy, and touching it applies that policy. Reviewing with ?w=1 or git diff -w shows only the real changes.

@tas50
tas50 merged commit 5b773f6 into main Aug 23, 2026
8 checks passed
@tas50
tas50 deleted the driver-modernization branch August 23, 2026 02:48
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