Skip to content

Commit

Permalink
fixes a path bug for the conda-debug command
Browse files Browse the repository at this point in the history
  • Loading branch information
travishathaway committed May 5, 2022
1 parent 3268fdd commit d2ca8de
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ jobs:
call conda update -q --all||exit 1
call conda install -q pip python-libarchive-c pytest git pytest-cov jinja2 m2-patch flake8 mock requests contextlib2 chardet glob2 perl pyflakes pycrypto posix m2-git anaconda-client numpy beautifulsoup4 pytest-xdist pytest-mock filelock pkginfo psutil pytz tqdm conda-package-handling||exit 1
call conda install pytest-replay pytest-rerunfailures -y||exit 1
call conda install -c conda-forge pyfakefs -y||exit 1
echo safety_checks: disabled >> %UserProfile%\.condarc
echo local_repodata_ttl: 1800 >> %UserProfile%\.condarc
call conda install -q py-lief||exit 1
Expand Down
2 changes: 1 addition & 1 deletion ci/github/install_conda_build_test_deps
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function install_conda_build_test_deps_fn()
_PKGS+=(${DEF_CHAN}::anaconda-client ${DEF_CHAN}::git ${DEF_CHAN}::requests ${DEF_CHAN}::filelock ${DEF_CHAN}::contextlib2 ${DEF_CHAN}::jinja2 ${DEF_CHAN}::flaky)
_PKGS+=(${DEF_CHAN}::ripgrep ${DEF_CHAN}::pyflakes ${DEF_CHAN}::beautifulsoup4 ${DEF_CHAN}::chardet ${DEF_CHAN}::pycrypto ${DEF_CHAN}::glob2 ${DEF_CHAN}::psutil ${DEF_CHAN}::pytz ${DEF_CHAN}::tqdm)
_PKGS+=(${DEF_CHAN}::conda-package-handling ${DEF_CHAN}::perl ${DEF_CHAN}::python-libarchive-c)
_PKGS+=(${DEF_CHAN}::pip ${DEF_CHAN}::numpy ${DEF_CHAN}::pkginfo)
_PKGS+=(${DEF_CHAN}::pip ${DEF_CHAN}::numpy ${DEF_CHAN}::pkginfo conda-forge::pyfakefs)
if [[ $(uname) =~ .*inux.* ]] && [[ ! ${MACOS_ARM64} == yes ]] ; then
_PKGS+=(${DEF_CHAN}::patchelf)
fi
Expand Down
48 changes: 25 additions & 23 deletions conda_build/cli/main_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@
#
# conda is distributed under the terms of the BSD 3-clause license.
# Consult LICENSE.txt or http://opensource.org/licenses/BSD-3-Clause.


import logging
import os
import sys
from argparse import ArgumentParser, Namespace

from conda_build import api
from conda_build.utils import CONDA_PACKAGE_EXTENSIONS, on_win
from conda_build.utils import on_win
# we extend the render parser because we basically need to render the recipe before
# we can say what env to create. This is not really true for debugging tests, but meh...
from conda_build.cli.main_render import get_render_parser
from conda_build.cli.main_render import execute as render_execute
from conda_build import validators as valid


logging.basicConfig(level=logging.INFO)


def parse_args(args):
def get_parser() -> ArgumentParser:
"""Returns a parser object for this command"""
p = get_render_parser()
p.description = """
Expand All @@ -43,39 +42,42 @@ def parse_args(args):
"The top-level recipe can be specified by passing 'TOPLEVEL' here"))
p.add_argument("-a", "--activate-string-only", action="store_true",
help="Output only the string to the used generated activation script. Use this for creating envs in scripted "
"environments.")
"environments.")

# cut out some args from render that don't make sense here
# https://stackoverflow.com/a/32809642/1170370
p._handle_conflict_resolve(None, [('--output', [_ for _ in p._actions if _.option_strings == ['--output']][0])])
p._handle_conflict_resolve(None, [('--bootstrap', [_ for _ in p._actions if _.option_strings == ['--bootstrap']][0])])
p._handle_conflict_resolve(None, [('--old-build-string', [_ for _ in p._actions if
_.option_strings == ['--old-build-string']][0])])
args = p.parse_args(args)
return p, args

return p


def execute(args):
p, _args = parse_args(args)
ARG_VALIDATORS = (
('recipe_or_package_file_path', valid.validate_is_conda_pkg_or_recipe_dir),
)


@valid.validate_args(ARG_VALIDATORS, get_parser())
def execute(args: Namespace):
test = True

try:
if not any(os.path.splitext(_args.recipe_or_package_file_path)[1] in ext for ext in CONDA_PACKAGE_EXTENSIONS):
# --output silences console output here
thing_to_debug = render_execute(args, print_results=False)
test = False
else:
thing_to_debug = _args.recipe_or_package_file_path
activation_string = api.debug(thing_to_debug, verbose=(not _args.activate_string_only), **_args.__dict__)

if not _args.activate_string_only:
activation_string = api.debug(
args.recipe_or_package_file_path,
verbose=(not args.activate_string_only),
**args.__dict__
)

if not args.activate_string_only:
print("#" * 80)
if test:
print("Test environment created for debugging. To enter a debugging environment:\n")
else:
print("Build and/or host environments created for debugging. To enter a debugging environment:\n")
print(activation_string)
if not _args.activate_string_only:
if not args.activate_string_only:
if test:
test_file = "conda_test_runner.bat" if on_win else "conda_test_runner.sh"
print(f"To run your tests, you might want to start with running the {test_file} file.")
Expand All @@ -85,9 +87,9 @@ def execute(args):
print("#" * 80)

except ValueError as e:
print(str(e))
sys.stderr.write(f'Error: conda-debug encountered the following error while attempting to execute:\n{e}\n')
sys.exit(1)


def main():
return execute(sys.argv[1:])
return execute()
14 changes: 14 additions & 0 deletions conda_build/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import tempfile
from threading import Thread
import time
from pathlib import Path

try:
from json.decoder import JSONDecodeError
Expand Down Expand Up @@ -2112,3 +2113,16 @@ def shutil_move_more_retrying(src, dest, debug_name):
elif attempts_left != -1:
log.error(
f"Failed to rename {debug_name} directory despite sleeping and retrying.")


def is_conda_pkg(pkg_path: str) -> bool:
"""
Determines whether string is pointing to a valid conda pkg
"""
path = Path(pkg_path)

return (
path.is_file() and (
any(path.name.endswith(ext) for ext in CONDA_PACKAGE_EXTENSIONS)
)
)
59 changes: 59 additions & 0 deletions conda_build/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import os
import sys
from argparse import ArgumentParser, Namespace
from functools import wraps
from typing import Sequence, Callable, Tuple

from conda_build.utils import CONDA_PACKAGE_EXTENSIONS
from conda_build import utils

ParserFunction = Callable[..., Tuple[ArgumentParser, Namespace]]
ValidatorFunction = Callable[[str, Namespace], str]


def validate_args(
validators: Sequence[Tuple[str, ValidatorFunction]],
parser: ArgumentParser,
):
"""
Runs a set of validation rules for a command. We assume that the first positional
argument is and
"""
def outer_wrap(func):
@wraps(func)
def wrapper(*args_, **kwargs):
args = sys.argv[1:]
cmd_args = parser.parse_args(args)

for arg, validate in validators:
arg_val = getattr(cmd_args, arg)
setattr(cmd_args, arg, validate(arg_val, cmd_args))

return func(cmd_args, *args_, **kwargs)
return wrapper
return outer_wrap


def get_is_conda_pkg_or_recipe_error_message() -> str:
"""Return the error displayed on the `validate_is_conda_pkg_or_recipe_dir` validator"""
valid_ext_str = ' or '.join(CONDA_PACKAGE_EXTENSIONS)
return (
'Error: Unable to parse provided recipe directory or package file.\n\n'
f'Please make sure this argument is either a valid package \n'
f'file ({valid_ext_str}) or points to a directory containing recipe.'
)


def validate_is_conda_pkg_or_recipe_dir(arg_val: str, _: Namespace) -> str:
"""
Makes sure the argument is either a conda pkg file or a recipe directory.
"""
if os.path.isdir(arg_val):
return arg_val
elif utils.is_conda_pkg(arg_val):
return arg_val
else:
sys.stderr.write(get_is_conda_pkg_or_recipe_error_message())
sys.exit(1)
Empty file added tests/cli/__init__.py
Empty file.
60 changes: 60 additions & 0 deletions tests/cli/test_main_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import io
import sys
from unittest import mock

import pytest
from pytest import CaptureFixture
from pyfakefs.fake_filesystem import FakeFilesystem

from conda_build.cli import main_debug as debug
from conda_build import validators as valid


@pytest.fixture(scope='session')
def main_debug_help() -> str:
"""Read what the current help message should be and return it as a fixture"""
parser = debug.get_parser()

with io.StringIO() as fp:
parser.print_usage(file=fp)
fp.seek(0)
return fp.read()


def test_main_debug_help_message(capsys: CaptureFixture, main_debug_help: str):
sys.argv = ['conda-debug']

with pytest.raises(SystemExit):
debug.main()

captured = capsys.readouterr()
assert main_debug_help in captured.err


def test_main_debug_file_does_not_exist(capsys: CaptureFixture):
sys.argv = ['conda-debug', 'file-does-not-exist']

with pytest.raises(SystemExit):
debug.main()

captured = capsys.readouterr()
assert captured.err == valid.get_is_conda_pkg_or_recipe_error_message()


def test_main_debug_happy_path(fs: FakeFilesystem, capsys: CaptureFixture):
"""
Happy path through the main_debug.main function.
"""
with mock.patch('conda_build.api.debug') as mock_debug:
fake_pkg_file = 'fake-conda-pkg.conda'
fs.create_file(fake_pkg_file)
sys.argv = ['conda-debug', fake_pkg_file]

debug.main()

caputured = capsys.readouterr()

assert caputured.err == ''

assert len(mock_debug.mock_calls) == 2

1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ glob2
jinja2
pkginfo
psutil
pyfakefs
pytest
pytest-cov
pytest-mock
Expand Down
42 changes: 42 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import os
import subprocess
import sys
from typing import NamedTuple

import pytest
from pyfakefs.fake_filesystem import FakeFilesystem

from conda_build.exceptions import BuildLockError
import conda_build.utils as utils
Expand Down Expand Up @@ -470,3 +472,43 @@ def test_find_recipe_multipe_bad():
# too many in base
with pytest.raises(IOError):
utils.find_recipe(tmp)


class IsCondaPkgTestData(NamedTuple):
value: str
expected: bool
is_dir: bool
create: bool


IS_CONDA_PKG_DATA = (
IsCondaPkgTestData(
value='aws-c-common-0.4.57-hb1e8313_1.tar.bz2',
expected=True,
is_dir=False,
create=True
),
IsCondaPkgTestData(
value='aws-c-common-0.4.57-hb1e8313_1.tar.bz2',
expected=False,
is_dir=False,
create=False
),
IsCondaPkgTestData(
value='somedir',
expected=False,
is_dir=True,
create=False
),
)


@pytest.mark.parametrize('value,expected,is_dir,create', IS_CONDA_PKG_DATA)
def test_is_conda_pkg(fs: FakeFilesystem, value: str, expected: bool, is_dir: bool, create: bool):
if create:
if is_dir:
fs.create_dir(value)
else:
fs.create_file(value)

assert utils.is_conda_pkg(value) == expected
25 changes: 25 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from argparse import Namespace

import pytest
from pyfakefs.fake_filesystem import FakeFilesystem

from conda_build import validators as valid

IS_CONDA_PACKAGE_OR_DIR_DATA = (
('aws-c-common-0.4.57-hb1e8313_1.tar.bz2', True, False, True),
('somedir', True, True, True),
)


@pytest.mark.parametrize('value,expected,is_dir,create', IS_CONDA_PACKAGE_OR_DIR_DATA)
def test_validate_is_conda_pkg_or_recipe_dir(
fs: FakeFilesystem, value: str, expected: bool, is_dir: bool, create: bool
):
if create:
if is_dir:
fs.create_dir(value)
else:
fs.create_file(value)
name_space = Namespace() # intentionally left empty because our validator doesn't need it

assert valid.validate_is_conda_pkg_or_recipe_dir(value, name_space)

0 comments on commit d2ca8de

Please sign in to comment.