Skip to content

Commit

Permalink
Improve the npm API's error handling and test coverage (#2227)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored Nov 27, 2024
1 parent 3612f81 commit f88c2bf
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 13 deletions.
23 changes: 15 additions & 8 deletions betty/_npm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,29 @@

import logging
import sys
from subprocess import CalledProcessError
from typing import Sequence, TYPE_CHECKING

from betty import subprocess
from betty.asyncio import wait_to_thread
from betty.error import UserFacingError
from betty.locale import Str, DEFAULT_LOCALIZER
from betty.requirement import Requirement
from betty.subprocess import run_process

if TYPE_CHECKING:
from pathlib import Path
from asyncio import subprocess as aiosubprocess


_NPM_UNAVAILABLE_MESSAGE = Str._(
_NPM_SUMMARY_AVAILABLE = Str._("`npm` is available")
_NPM_SUMMARY_UNAVAILABLE = Str._("`npm` is not available")
_NPM_DETAILS = Str._(
"npm (https://www.npmjs.com/) must be available for features that require Node.js packages to be installed. Ensure that the `npm` executable is available in your `PATH`."
)


class NpmUnavailable(UserFacingError, RuntimeError):
def __init__(self):
super().__init__(_NPM_UNAVAILABLE_MESSAGE)
super().__init__(_NPM_DETAILS)


async def npm(
Expand All @@ -39,7 +41,7 @@ async def npm(
Run an npm command.
"""
try:
return await run_process(
return await subprocess.run_process(
["npm", *arguments],
cwd=cwd,
# Use a shell on Windows so subprocess can find the executables it needs (see
Expand All @@ -55,7 +57,7 @@ def __init__(self):
super().__init__()
self._met: bool
self._summary: Str
self._details = _NPM_UNAVAILABLE_MESSAGE
self._details = _NPM_DETAILS

def _check(self) -> None:
if hasattr(self, "_met"):
Expand All @@ -64,10 +66,15 @@ def _check(self) -> None:
wait_to_thread(npm(["--version"]))
except NpmUnavailable:
self._met = False
self._summary = Str._("`npm` is not available")
self._summary = _NPM_SUMMARY_UNAVAILABLE
except CalledProcessError as error:
logging.getLogger(__name__).exception(error)
logging.getLogger(__name__).debug(_NPM_DETAILS.localize(DEFAULT_LOCALIZER))
self._met = False
self._summary = _NPM_SUMMARY_UNAVAILABLE
else:
self._met = True
self._summary = Str._("`npm` is available")
self._summary = _NPM_SUMMARY_AVAILABLE
finally:
logging.getLogger(__name__).debug(self._summary.localize(DEFAULT_LOCALIZER))

Expand Down
36 changes: 31 additions & 5 deletions betty/tests/test__npm.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
from pathlib import Path
from subprocess import CalledProcessError

import pytest
from pytest_mock import MockerFixture

from betty._npm import NpmRequirement, NpmUnavailable
from betty._npm import NpmRequirement, NpmUnavailable, npm


class TestNpm:
async def test(self) -> None:
await npm(["--version"])

async def test_command_not_found(self, mocker: MockerFixture) -> None:
mocker.patch("betty.subprocess.run_process", side_effect=FileNotFoundError)
with pytest.raises(NpmUnavailable):
await npm(["--version"])

async def test_command_error(self, mocker: MockerFixture, tmp_path: Path) -> None:
mocker.patch(
"betty.subprocess.run_process",
side_effect=CalledProcessError(1, "", "", ""),
)
with pytest.raises(CalledProcessError):
await npm(["--version"])


class TestNpmRequirement:
async def test_check_met(self) -> None:
def test_is_met(self) -> None:
sut = NpmRequirement()
assert sut.is_met()

async def test_check_unmet(self, mocker: MockerFixture) -> None:
m_npm = mocker.patch("betty._npm.npm")
m_npm.side_effect = NpmUnavailable()
def test_is_met_with_command_not_found(self, mocker: MockerFixture) -> None:
mocker.patch("betty._npm.npm", side_effect=NpmUnavailable)
sut = NpmRequirement()
assert not sut.is_met()

def test_is_met_with_command_error(self, mocker: MockerFixture) -> None:
mocker.patch("betty._npm.npm", side_effect=CalledProcessError(1, "", "", ""))
sut = NpmRequirement()
assert not sut.is_met()

0 comments on commit f88c2bf

Please sign in to comment.