Skip to content

Commit be5ca75

Browse files
implement health check for language:r
1 parent 7667ea1 commit be5ca75

File tree

2 files changed

+124
-2
lines changed

2 files changed

+124
-2
lines changed

pre_commit/languages/r.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import shutil
77
import tempfile
88
import textwrap
9+
import typing
910
from collections.abc import Generator
1011
from collections.abc import Sequence
1112

@@ -20,7 +21,56 @@
2021
ENVIRONMENT_DIR = 'renv'
2122
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
2223
get_default_version = lang_base.basic_get_default_version
23-
health_check = lang_base.basic_health_check
24+
25+
26+
def _execute_vanilla_r_code_as_script(code: str, **kwargs: typing.Any) -> str:
27+
with _r_code_in_tempfile(code) as f:
28+
cmd_out = cmd_output_b(_rscript_exec(), '--vanilla', f, **kwargs)
29+
return cmd_out[1].decode().rstrip('\n')
30+
31+
32+
def _read_installed_version(envdir: str) -> str:
33+
return _execute_vanilla_r_code_as_script(
34+
'cat(renv::settings$r.version())', cwd=envdir,
35+
)
36+
37+
38+
def _read_executable_version(envdir: str) -> str:
39+
return _execute_vanilla_r_code_as_script(
40+
'cat(as.character(getRversion()))',
41+
cwd=envdir,
42+
)
43+
44+
45+
def _write_current_r_version(envdir: str) -> None:
46+
_execute_vanilla_r_code_as_script(
47+
'renv::settings$r.version(as.character(getRversion()))',
48+
cwd=envdir,
49+
)
50+
51+
52+
def health_check(prefix: Prefix, version: str) -> str | None:
53+
# prefix: cloned hook repo .../{precommit}
54+
# env dir .../{precommit}/renv-default/
55+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
56+
57+
r_version_installation = _read_installed_version(envdir=envdir)
58+
# TODO what if none set? -> record current?
59+
# TODO how is this affecting old hooks environments
60+
r_version_current_executable = _read_executable_version(envdir=envdir)
61+
if r_version_installation in {'NULL', ''}:
62+
return (
63+
'Hooks were installed with an unknown R version. R version for '
64+
f'hook repo now set to {r_version_current_executable}'
65+
)
66+
elif r_version_installation != r_version_current_executable:
67+
return (
68+
f'Hooks were installed for R version {r_version_installation}, '
69+
'but current R executable has version '
70+
f'{r_version_current_executable}'
71+
)
72+
73+
return None
2474

2575

2676
@contextlib.contextmanager
@@ -130,7 +180,6 @@ def install_environment(
130180
'setwd(old); ',
131181
'renv::load("', getwd(), '");}})'
132182
)
133-
writeLines(activate_statement, 'activate.R')
134183
is_package <- tryCatch(
135184
{{
136185
path_desc <- file.path(prefix_dir, 'DESCRIPTION')
@@ -142,11 +191,13 @@ def install_environment(
142191
if (is_package) {{
143192
renv::install(prefix_dir)
144193
}}
194+
writeLines(activate_statement, 'activate.R')
145195
"""
146196

147197
with _r_code_in_tempfile(r_code_inst_environment) as f:
148198
cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)
149199

200+
_write_current_r_version(envdir=env_dir)
150201
if additional_dependencies:
151202
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
152203
with in_env(prefix, version):

tests/languages/r_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import os.path
44
import shutil
5+
from unittest import mock
56

67
import pytest
78

9+
import pre_commit.constants as C
810
from pre_commit import envcontext
11+
from pre_commit import lang_base
912
from pre_commit.languages import r
1013
from pre_commit.prefix import Prefix
1114
from pre_commit.store import _make_local_repo
@@ -247,3 +250,71 @@ def test_r_inline(tmp_path):
247250
args=('hi', 'hello'),
248251
)
249252
assert ret == (0, b'hi, hello, from R!\n')
253+
254+
255+
@pytest.fixture
256+
def prefix(tmpdir):
257+
with tmpdir.as_cwd():
258+
prefix = tmpdir.ensure_dir()
259+
# prefix: cloned hook repo .../{precommit}
260+
# env dir .../{precommit}/renv-default/
261+
prefix = Prefix(str(prefix))
262+
yield prefix
263+
264+
265+
@pytest.fixture() # TODO module level might be desirable?
266+
def installed_environment(
267+
renv_lock_file,
268+
hello_world_file,
269+
renv_folder,
270+
prefix,
271+
272+
):
273+
env_dir = lang_base.environment_dir(
274+
prefix, r.ENVIRONMENT_DIR, r.get_default_version(),
275+
)
276+
r.install_environment(prefix, C.DEFAULT, ())
277+
yield prefix, env_dir
278+
279+
280+
@pytest.fixture
281+
def older_r_version():
282+
with mock.patch.object(
283+
r, '_read_installed_version', lambda envdir: '1.0.0',
284+
):
285+
yield
286+
287+
288+
def test_health_check_healthy(installed_environment):
289+
# should be healthy right after creation
290+
prefix, _ = installed_environment
291+
assert r.health_check(prefix, C.DEFAULT) is None
292+
293+
294+
def test_health_check_after_downgrade(installed_environment, older_r_version):
295+
msg = 'Hooks were installed for R version'
296+
prefix, _ = installed_environment
297+
check_output = r.health_check(prefix, C.DEFAULT)
298+
assert check_output is not None and check_output.startswith(msg)
299+
300+
301+
@pytest.fixture(params=('NULL', 'NA', "''"))
302+
def installed_env_without_r_version_info(request, installed_environment):
303+
_, env_dir = installed_environment
304+
r._execute_vanilla_r_code_as_script(
305+
f'renv::settings$r.version({request.param})', cwd=env_dir,
306+
)
307+
yield
308+
309+
310+
def test_health_check_without_version(
311+
prefix,
312+
installed_env_without_r_version_info,
313+
):
314+
# TODO not sure that test and functionality is requried since it will
315+
# only run on new pre-commit and then, probably all envs are
316+
# re-installed anyways?
317+
# no R version specified fails as unhealty
318+
msg = 'Hooks were installed with an unknown R version'
319+
check_output = r.health_check(prefix, C.DEFAULT)
320+
assert check_output is not None and check_output.startswith(msg)

0 commit comments

Comments
 (0)