Patch Drivers
Below are listed the Patch Drivers (shortened to “drivers”) available at time of docs generation. Note that drivers are expected to be coming from third parties, and are exposed as “plugins” (also known as python package entry_points).
Making your own driver
The simplest way to make something custom is to reconfigure an existing Driver.
See for instance the driver mass_driver.drivers.counter.Counter
, which
takes as configuration a counter_file
parameter, and a target_count
.
But if you want to do something more elaborate, you’ll want your own
PatchDriver
, exposed as a plugin.
Defining a PatchDriver
Simply create a class that inherits from
mass_driver.models.patchdriver.PatchDriver
, storing any kind of configuration as
slots, and exposing a single run
method:
from mass_driver.models.patchdriver import PatchDriver, PatchResult, PatchOutcome
from mass_driver.models.repository import ClonedRepo
class PerlPackageBumper(PatchDriver):
"""Bump version of Perl packages"""
# Each of these are parameters to tweak per migration
# Set them under [mass-driver.migration.driver_config]
package_target: str
package_version: str
def run(self, repo: ClonedRepo) -> PatchResult:
"""Run the package bumper"""
packages = get_packages()
if self.package_target not in packages:
# Do set PatchResult.details to explain not-applicable or errors
return PatchResult(outcome=PatchOutcome.PATCH_DOES_NOT_APPLY,
details="Package not present, no need to bump")
version = packages[self.package_target]
if version == self.package_version:
return PatchResult(outcome=PatchOutcome.ALREADY_PATCHED)
# Version different from now on
set_package_version(packages, self.package_target, self.package_version)
return PatchResult(outcome=PatchOutcome.PATCHED_OK)
This class is now a valid Driver, but we need to package it to make it visible to Mass Driver.
Packaging a driver for plugin discovery
Using the creating a plugin via package metadata Guide, tweaked for Poetry usage (note Poetry isn’t required, just convenient):
In your pyproject.toml
, add the following entry:
[tool.poetry.plugins.'massdriver.drivers']
perlbump = 'mypackage.mymodule:PerlPackageBumper'
The key here is we’re declaring a new plugin called perlbump
, part of
massdriver.drivers
.
If you’re more of a setup.py
person, follow the equivalent instructions for
setup.py
Using our new plugin
Now that the plugin is declared, install your package, then use the mass driver drivers --list
to see if the perlbump
driver is discovered.
Once it is, you can follow the using mass-driver docs to execute it.
Testing and validating a PatchDriver
Obviously, we don’t recommend testing a mass-PR tool in production by cloning hundreds of repos.
We’ve set up some unit-level tests for PatchDriver, to be able to validate offline that a driver is working.
These tests focus on individual file transformation, which is a very common usecase.
Here’s the layout of the tests/
folder we’ll be using:
├── test_counterdriver
│ ├── count_to_1
│ │ ├── input.txt
│ │ ├── migration.toml
│ │ └── outcome.txt
│ └── count_to_2
│ ├── input.txt
│ ├── migration.toml
│ └── output.txt
└── test_counterdriver.py
Note that the actual pytest
test is test_counterdriver.py
, with a similarly
named folder test_counterdriver/
containing 2 folders.
The pattern of testing we use here is to try transforming input.txt
via
mass-driver (configured by migration.toml
), checking patching is successful
(PatchOutcome.PATCHED_OK
), and comparing the resulting file to output.txt
,
flagging any differences as test failures.
Optionally, presence of an outcome.txt
will be used to check what outcome to
expect instead (from the PatchOutcome
Enum).
Similarly, the presence of a details.txt
will be used to check the
PatchResult.details
field, with the check: details_txt in result.details
.
As for the test code in pytest
to trigger all this:
from pathlib import Path
import pytest
from mass_driver.tests.fixtures import copy_folder, massdrive_check_file
# Go from this filename.py to folder:
# ./test_counterdriver.py -> ./test_counterdriver/
TESTS_FOLDER = Path(__file__).with_suffix("")
@pytest.mark.parametrize(
"test_folder", [f.name for f in TESTS_FOLDER.iterdir() if f.is_dir()]
)
def test_driver_one(test_folder: Path, tmp_path):
"""Check a single pattern"""
absolute_reference = TESTS_FOLDER / test_folder
workdir = tmp_path / "repo"
copy_folder(absolute_reference, workdir)
result, reference = massdrive_check_file(workdir)
assert result == reference, "Massdriver result should match reference"
The key here is use of the test fixture
mass_driver.tests.fixtures.massdrive_check_file()
, which takes a folder
with these text files, and runs mass-driver over it.
For more granular, custom testing, the fixture
mass_driver.tests.fixtures.massdrive()
, can run mass-driver CLI in a
prepackaged way, returning just
mass_driver.models.activity.MigrationOutcome
+
mass_driver.models.activity.ForgeResult
(if any forge defined) +
mass_driver.models.activity.ScanResult
(if any scans defined).