Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: fixes a CLI argument bug for the conda-debug command #4448

Merged
merged 11 commits into from
May 12, 2022
62 changes: 28 additions & 34 deletions conda_build/cli/main_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@
#
# 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

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.cli 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 @@ -33,6 +31,7 @@ def parse_args(args):
help=("Path to recipe directory or package file to use for dependency and source information. "
"If you use a recipe, you get the build/host env and source work directory. If you use "
"a package file, you get the test environments and the test_tmp folder."),
type=valid.validate_is_conda_pkg_or_recipe_dir
)
p.add_argument("-p", "--path",
help=("root path in which to place envs, source and activation script. Defaults to a "
Expand All @@ -43,51 +42,46 @@ 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)
test = True
def execute():
parser = get_parser()
args = parser.parse_args()

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__)
activation_string = api.debug(
args.recipe_or_package_file_path,
verbose=(not args.activate_string_only),
**args.__dict__
)

if not _args.activate_string_only:
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(
"Test environment created for debugging. To enter a debugging environment:\n"
)

print(activation_string)
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.")
else:
build_file = "conda_build.bat" if on_win else "conda_build.sh"
print(f"To run your build, you might want to start with running the {build_file} file.")
if not args.activate_string_only:
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."
)
print("#" * 80)

except ValueError as e:
print(str(e))
print(f"Error: conda-debug encountered the following error:\n{e}", file=sys.stderr)
sys.exit(1)


def main():
return execute(sys.argv[1:])
return execute()
25 changes: 25 additions & 0 deletions conda_build/cli/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import os
from argparse import ArgumentError

from conda_build.utils import CONDA_PACKAGE_EXTENSIONS
from conda_build import utils

CONDA_PKG_OR_RECIPE_ERROR_MESSAGE = (
"\nUnable to parse provided recipe directory or package file.\n\n"
f"Please make sure this argument is either a valid package \n"
f'file ({" or ".join(CONDA_PACKAGE_EXTENSIONS)}) or points to a directory containing recipe.'
)


def validate_is_conda_pkg_or_recipe_dir(arg_val: str) -> 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:
raise ArgumentError(None, CONDA_PKG_OR_RECIPE_ERROR_MESSAGE)
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)
)
travishathaway marked this conversation as resolved.
Show resolved Hide resolved
)
Empty file added tests/cli/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions tests/cli/test_main_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import io
import os.path
import sys
from unittest import mock

import pytest
from pytest import CaptureFixture

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


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

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

sys.argv = []


def test_main_debug_help_message(capsys: CaptureFixture, main_debug_help: str):
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 valid.CONDA_PKG_OR_RECIPE_ERROR_MESSAGE in captured.err


def test_main_debug_happy_path(tmpdir, capsys: CaptureFixture):
"""
Happy path through the main_debug.main function.
"""
with mock.patch("conda_build.api.debug") as mock_debug:
fake_pkg_file = os.path.join(tmpdir, "fake-conda-pkg.conda")
fp = open(fake_pkg_file, "w")
fp.write("text")
fp.close()
sys.argv = ['conda-debug', fake_pkg_file]

debug.main()

captured = capsys.readouterr()

assert captured.err == ''

assert len(mock_debug.mock_calls) == 2
38 changes: 38 additions & 0 deletions tests/cli/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
from argparse import ArgumentError
from typing import Union

import pytest

from conda_build.cli import validators as valid


@pytest.mark.parametrize(
'file_or_folder,expected,is_dir,create',
[
# Happy path cases
('aws-c-common-0.4.57-hb1e8313_1.tar.bz2', 'aws-c-common-0.4.57-hb1e8313_1.tar.bz2', False, True),
('aws-c-common-0.4.57-hb1e8313_1.conda', 'aws-c-common-0.4.57-hb1e8313_1.conda', False, True),
('somedir', 'somedir', True, True),
# Error case (i.e. the file or directory does not exist
('aws-c-common-0.4.57-hb1e8313_1.conda', False, False, False),
],
)
def test_validate_is_conda_pkg_or_recipe_dir(
file_or_folder: str, expected: Union[str, bool], is_dir: bool, create: bool, tmpdir
):
if create:
file_or_folder = os.path.join(tmpdir, file_or_folder)
expected = os.path.join(tmpdir, expected)
if is_dir:
os.mkdir(file_or_folder)
else:
with open(file_or_folder, "w") as fp:
fp.write("test")

try:
received = valid.validate_is_conda_pkg_or_recipe_dir(file_or_folder)
except (ArgumentError, SystemExit): # if we get these errors, we know it's not valid
received = False

assert received == expected
43 changes: 43 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import subprocess
import sys
from typing import NamedTuple

import pytest

Expand Down Expand Up @@ -470,3 +471,45 @@ 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(tmpdir, value: str, expected: bool, is_dir: bool, create: bool):
if create:
value = os.path.join(tmpdir, value)
if is_dir:
os.mkdir(value)
else:
with open(value, "w") as fp:
fp.write("test")

assert utils.is_conda_pkg(value) == expected