Skip to content

Commit

Permalink
Merge pull request #3265 from lorenzwalthert/issue-3206
Browse files Browse the repository at this point in the history
Support health check for `language: r`
  • Loading branch information
asottile authored Jul 28, 2024
2 parents f641f6a + da0c1d0 commit 8133abd
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 22 deletions.
77 changes: 68 additions & 9 deletions pre_commit/languages/r.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,74 @@
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe

ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
get_default_version = lang_base.basic_get_default_version
health_check = lang_base.basic_health_check


def _execute_vanilla_r_code_as_script(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
) -> str:
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
_, out, _ = cmd_output(
_rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd,
)
return out.rstrip('\n')


def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_vanilla_r_code_as_script(
'cat(renv::settings$r.version())',
prefix=prefix, version=version,
cwd=envdir,
)


def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
return _execute_vanilla_r_code_as_script(
'cat(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)


def _write_current_r_version(
envdir: str, prefix: Prefix, version: str,
) -> None:
_execute_vanilla_r_code_as_script(
'renv::settings$r.version(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
)


def health_check(prefix: Prefix, version: str) -> str | None:
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)

r_version_installation = _read_installed_version(
envdir=envdir, prefix=prefix, version=version,
)
r_version_current_executable = _read_executable_version(
envdir=envdir, prefix=prefix, version=version,
)
if r_version_installation in {'NULL', ''}:
return (
f'Hooks were installed with an unknown R version. R version for '
f'hook repo now set to {r_version_current_executable}'
)
elif r_version_installation != r_version_current_executable:
return (
f'Hooks were installed for R version {r_version_installation}, '
f'but current R executable has version '
f'{r_version_current_executable}'
)

return None


@contextlib.contextmanager
Expand Down Expand Up @@ -147,16 +208,14 @@ def install_environment(
with _r_code_in_tempfile(r_code_inst_environment) as f:
cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)

_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
with in_env(prefix, version):
with _r_code_in_tempfile(r_code_inst_add) as f:
cmd_output_b(
_rscript_exec(), *RSCRIPT_OPTS,
f,
*additional_dependencies,
cwd=env_dir,
)
_execute_vanilla_r_code_as_script(
code=r_code_inst_add, prefix=prefix, version=version,
args=additional_dependencies,
cwd=env_dir,
)


def _inline_r_setup(code: str) -> str:
Expand Down
100 changes: 87 additions & 13 deletions tests/languages/r_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from __future__ import annotations

import os.path
import shutil
from unittest import mock

import pytest

import pre_commit.constants as C
from pre_commit import envcontext
from pre_commit import lang_base
from pre_commit.languages import r
from pre_commit.prefix import Prefix
from pre_commit.store import _make_local_repo
from pre_commit.util import resource_text
from pre_commit.util import win_exe
from testing.language_helpers import run_language

Expand Down Expand Up @@ -127,7 +130,8 @@ def test_path_rscript_exec_no_r_home_set():
assert r._rscript_exec() == 'Rscript'


def test_r_hook(tmp_path):
@pytest.fixture
def renv_lock_file(tmp_path):
renv_lock = '''\
{
"R": {
Expand Down Expand Up @@ -157,6 +161,12 @@ def test_r_hook(tmp_path):
}
}
'''
tmp_path.joinpath('renv.lock').write_text(renv_lock)
yield


@pytest.fixture
def description_file(tmp_path):
description = '''\
Package: gli.clu
Title: What the Package Does (One Line, Title Case)
Expand All @@ -178,27 +188,39 @@ def test_r_hook(tmp_path):
Imports:
rprojroot
'''
hello_world_r = '''\
tmp_path.joinpath('DESCRIPTION').write_text(description)
yield


@pytest.fixture
def hello_world_file(tmp_path):
hello_world = '''\
stopifnot(
packageVersion('rprojroot') == '1.0',
packageVersion('gli.clu') == '0.0.0.9000'
)
cat("Hello, World, from R!\n")
'''
tmp_path.joinpath('hello-world.R').write_text(hello_world)
yield

tmp_path.joinpath('renv.lock').write_text(renv_lock)
tmp_path.joinpath('DESCRIPTION').write_text(description)
tmp_path.joinpath('hello-world.R').write_text(hello_world_r)

@pytest.fixture
def renv_folder(tmp_path):
renv_dir = tmp_path.joinpath('renv')
renv_dir.mkdir()
shutil.copy(
os.path.join(
os.path.dirname(__file__),
'../../pre_commit/resources/empty_template_activate.R',
),
renv_dir.joinpath('activate.R'),
)
activate_r = resource_text('empty_template_activate.R')
renv_dir.joinpath('activate.R').write_text(activate_r)
yield


def test_r_hook(
tmp_path,
renv_lock_file,
description_file,
hello_world_file,
renv_folder,
):
expected = (0, b'Hello, World, from R!\n')
assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected

Expand All @@ -221,3 +243,55 @@ def test_r_inline(tmp_path):
args=('hi', 'hello'),
)
assert ret == (0, b'hi, hello, from R!\n')


@pytest.fixture
def prefix(tmpdir):
yield Prefix(str(tmpdir))


@pytest.fixture
def installed_environment(
renv_lock_file,
hello_world_file,
renv_folder,
prefix,
):
env_dir = lang_base.environment_dir(
prefix, r.ENVIRONMENT_DIR, r.get_default_version(),
)
r.install_environment(prefix, C.DEFAULT, ())
yield prefix, env_dir


def test_health_check_healthy(installed_environment):
# should be healthy right after creation
prefix, _ = installed_environment
assert r.health_check(prefix, C.DEFAULT) is None


def test_health_check_after_downgrade(installed_environment):
prefix, _ = installed_environment

# pretend the saved installed version is old
with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'):
output = r.health_check(prefix, C.DEFAULT)

assert output is not None
assert output.startswith('Hooks were installed for R version')


@pytest.mark.parametrize('version', ('NULL', 'NA', "''"))
def test_health_check_without_version(prefix, installed_environment, version):
prefix, env_dir = installed_environment

# simulate old pre-commit install by unsetting the installed version
r._execute_vanilla_r_code_as_script(
f'renv::settings$r.version({version})',
prefix=prefix, version=C.DEFAULT, cwd=env_dir,
)

# no R version specified fails as unhealty
msg = 'Hooks were installed with an unknown R version'
check_output = r.health_check(prefix, C.DEFAULT)
assert check_output is not None and check_output.startswith(msg)

0 comments on commit 8133abd

Please sign in to comment.