Skip to content

Commit 8133abd

Browse files
authored
Merge pull request #3265 from lorenzwalthert/issue-3206
Support health check for `language: r`
2 parents f641f6a + da0c1d0 commit 8133abd

File tree

2 files changed

+155
-22
lines changed

2 files changed

+155
-22
lines changed

pre_commit/languages/r.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,74 @@
1414
from pre_commit.envcontext import PatchesT
1515
from pre_commit.envcontext import UNSET
1616
from pre_commit.prefix import Prefix
17+
from pre_commit.util import cmd_output
1718
from pre_commit.util import cmd_output_b
1819
from pre_commit.util import win_exe
1920

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(
27+
code: str, *,
28+
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
29+
) -> str:
30+
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
31+
_, out, _ = cmd_output(
32+
_rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd,
33+
)
34+
return out.rstrip('\n')
35+
36+
37+
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
38+
return _execute_vanilla_r_code_as_script(
39+
'cat(renv::settings$r.version())',
40+
prefix=prefix, version=version,
41+
cwd=envdir,
42+
)
43+
44+
45+
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
46+
return _execute_vanilla_r_code_as_script(
47+
'cat(as.character(getRversion()))',
48+
prefix=prefix, version=version,
49+
cwd=envdir,
50+
)
51+
52+
53+
def _write_current_r_version(
54+
envdir: str, prefix: Prefix, version: str,
55+
) -> None:
56+
_execute_vanilla_r_code_as_script(
57+
'renv::settings$r.version(as.character(getRversion()))',
58+
prefix=prefix, version=version,
59+
cwd=envdir,
60+
)
61+
62+
63+
def health_check(prefix: Prefix, version: str) -> str | None:
64+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
65+
66+
r_version_installation = _read_installed_version(
67+
envdir=envdir, prefix=prefix, version=version,
68+
)
69+
r_version_current_executable = _read_executable_version(
70+
envdir=envdir, prefix=prefix, version=version,
71+
)
72+
if r_version_installation in {'NULL', ''}:
73+
return (
74+
f'Hooks were installed with an unknown R version. R version for '
75+
f'hook repo now set to {r_version_current_executable}'
76+
)
77+
elif r_version_installation != r_version_current_executable:
78+
return (
79+
f'Hooks were installed for R version {r_version_installation}, '
80+
f'but current R executable has version '
81+
f'{r_version_current_executable}'
82+
)
83+
84+
return None
2485

2586

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

211+
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
150212
if additional_dependencies:
151213
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
152-
with in_env(prefix, version):
153-
with _r_code_in_tempfile(r_code_inst_add) as f:
154-
cmd_output_b(
155-
_rscript_exec(), *RSCRIPT_OPTS,
156-
f,
157-
*additional_dependencies,
158-
cwd=env_dir,
159-
)
214+
_execute_vanilla_r_code_as_script(
215+
code=r_code_inst_add, prefix=prefix, version=version,
216+
args=additional_dependencies,
217+
cwd=env_dir,
218+
)
160219

161220

162221
def _inline_r_setup(code: str) -> str:

tests/languages/r_test.py

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import annotations
22

33
import os.path
4-
import shutil
4+
from unittest import mock
55

66
import pytest
77

8+
import pre_commit.constants as C
89
from pre_commit import envcontext
10+
from pre_commit import lang_base
911
from pre_commit.languages import r
1012
from pre_commit.prefix import Prefix
1113
from pre_commit.store import _make_local_repo
14+
from pre_commit.util import resource_text
1215
from pre_commit.util import win_exe
1316
from testing.language_helpers import run_language
1417

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

129132

130-
def test_r_hook(tmp_path):
133+
@pytest.fixture
134+
def renv_lock_file(tmp_path):
131135
renv_lock = '''\
132136
{
133137
"R": {
@@ -157,6 +161,12 @@ def test_r_hook(tmp_path):
157161
}
158162
}
159163
'''
164+
tmp_path.joinpath('renv.lock').write_text(renv_lock)
165+
yield
166+
167+
168+
@pytest.fixture
169+
def description_file(tmp_path):
160170
description = '''\
161171
Package: gli.clu
162172
Title: What the Package Does (One Line, Title Case)
@@ -178,27 +188,39 @@ def test_r_hook(tmp_path):
178188
Imports:
179189
rprojroot
180190
'''
181-
hello_world_r = '''\
191+
tmp_path.joinpath('DESCRIPTION').write_text(description)
192+
yield
193+
194+
195+
@pytest.fixture
196+
def hello_world_file(tmp_path):
197+
hello_world = '''\
182198
stopifnot(
183199
packageVersion('rprojroot') == '1.0',
184200
packageVersion('gli.clu') == '0.0.0.9000'
185201
)
186202
cat("Hello, World, from R!\n")
187203
'''
204+
tmp_path.joinpath('hello-world.R').write_text(hello_world)
205+
yield
188206

189-
tmp_path.joinpath('renv.lock').write_text(renv_lock)
190-
tmp_path.joinpath('DESCRIPTION').write_text(description)
191-
tmp_path.joinpath('hello-world.R').write_text(hello_world_r)
207+
208+
@pytest.fixture
209+
def renv_folder(tmp_path):
192210
renv_dir = tmp_path.joinpath('renv')
193211
renv_dir.mkdir()
194-
shutil.copy(
195-
os.path.join(
196-
os.path.dirname(__file__),
197-
'../../pre_commit/resources/empty_template_activate.R',
198-
),
199-
renv_dir.joinpath('activate.R'),
200-
)
212+
activate_r = resource_text('empty_template_activate.R')
213+
renv_dir.joinpath('activate.R').write_text(activate_r)
214+
yield
215+
201216

217+
def test_r_hook(
218+
tmp_path,
219+
renv_lock_file,
220+
description_file,
221+
hello_world_file,
222+
renv_folder,
223+
):
202224
expected = (0, b'Hello, World, from R!\n')
203225
assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected
204226

@@ -221,3 +243,55 @@ def test_r_inline(tmp_path):
221243
args=('hi', 'hello'),
222244
)
223245
assert ret == (0, b'hi, hello, from R!\n')
246+
247+
248+
@pytest.fixture
249+
def prefix(tmpdir):
250+
yield Prefix(str(tmpdir))
251+
252+
253+
@pytest.fixture
254+
def installed_environment(
255+
renv_lock_file,
256+
hello_world_file,
257+
renv_folder,
258+
prefix,
259+
):
260+
env_dir = lang_base.environment_dir(
261+
prefix, r.ENVIRONMENT_DIR, r.get_default_version(),
262+
)
263+
r.install_environment(prefix, C.DEFAULT, ())
264+
yield prefix, env_dir
265+
266+
267+
def test_health_check_healthy(installed_environment):
268+
# should be healthy right after creation
269+
prefix, _ = installed_environment
270+
assert r.health_check(prefix, C.DEFAULT) is None
271+
272+
273+
def test_health_check_after_downgrade(installed_environment):
274+
prefix, _ = installed_environment
275+
276+
# pretend the saved installed version is old
277+
with mock.patch.object(r, '_read_installed_version', return_value='1.0.0'):
278+
output = r.health_check(prefix, C.DEFAULT)
279+
280+
assert output is not None
281+
assert output.startswith('Hooks were installed for R version')
282+
283+
284+
@pytest.mark.parametrize('version', ('NULL', 'NA', "''"))
285+
def test_health_check_without_version(prefix, installed_environment, version):
286+
prefix, env_dir = installed_environment
287+
288+
# simulate old pre-commit install by unsetting the installed version
289+
r._execute_vanilla_r_code_as_script(
290+
f'renv::settings$r.version({version})',
291+
prefix=prefix, version=C.DEFAULT, cwd=env_dir,
292+
)
293+
294+
# no R version specified fails as unhealty
295+
msg = 'Hooks were installed with an unknown R version'
296+
check_output = r.health_check(prefix, C.DEFAULT)
297+
assert check_output is not None and check_output.startswith(msg)

0 commit comments

Comments
 (0)