diff --git a/.appveyor.yml b/.appveyor.yml index c2e3538..b236709 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,11 +18,12 @@ install: # Install python packages - "%PYTHON%/Scripts/pip.exe install twine" - "%PYTHON%/Scripts/pip.exe install codecov" + - "%PYTHON%/Scripts/pip.exe install pytest" test_script: # test/hello - "%PYTHON%/python setup.py develop" - - "%PYTHON%/Scripts/coverage run test/test.py" + - "%PYTHON%/Scripts/coverage run -m pytest test" - "%PYTHON%/Scripts/coverage combine" - "%PYTHON%/Scripts/codecov" #- "%PYTHON%/python setup.py develop" diff --git a/.travis.yml b/.travis.yml index feb6b53..aa44dc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ before_install: install: - pip3 install setuptools - pip3 install codecov + - pip3 install pytest - python3 setup.py develop script: diff --git a/clang_build/build_type.py b/clang_build/build_type.py index 6d01b03..ee711b8 100644 --- a/clang_build/build_type.py +++ b/clang_build/build_type.py @@ -1,18 +1,30 @@ from enum import Enum as _Enum + class BuildType(_Enum): - Default = 'default' - Release = 'release' - RelWithDebInfo = 'relwithdebinfo' - Debug = 'debug' - Coverage = 'coverage' + """Enumeration of all build types. + + Construction is case sensitive, i.e. + + >>> BuildType("default") == BuildType("Default") + True + + """ + + Default = "default" + Release = "release" + RelWithDebInfo = "relwithdebinfo" + Debug = "debug" + Coverage = "coverage" def __str__(self): + """Return the value of the BuildType.""" return self.value # Let's be case insensitive @classmethod def _missing_(cls, value): + """Check for identical value except for caseing.""" for item in cls: if item.value.lower() == value.lower(): return item diff --git a/clang_build/clang_build.py b/clang_build/clang_build.py index f81b303..ac61ee6 100644 --- a/clang_build/clang_build.py +++ b/clang_build/clang_build.py @@ -44,6 +44,12 @@ def _setup_logger(log_level=None): ch.setFormatter(formatter) _LOGGER.addHandler(ch) +def _check_positive(value): + value = int(value) + if value <= 0: + raise _argparse.ArgumentTypeError("%s is negative") + + return value def parse_args(args): _command_line_description = ( @@ -60,6 +66,7 @@ def parse_args(args): action='store_true') parser.add_argument('-d', '--directory', type=_Path, + default=_Path(), help='set the root source directory') parser.add_argument('-b', '--build-type', choices=list(_BuildType), @@ -78,7 +85,7 @@ def parse_args(args): help='also build sources which have already been built', action='store_true') parser.add_argument('-j', '--jobs', - type=int, + type=_check_positive, default=1, help='set the number of concurrent build jobs') parser.add_argument('--debug', @@ -103,14 +110,15 @@ def build(args): # Create container of environment variables environment = _Environment(vars(args)) - categories = ['Configure', 'Compile', 'Link'] - if environment.bundle: - categories.append('Generate bundle') - if environment.redistributable: - categories.append('Generate redistributable') + categories = ['Configure', 'Build'] + #if environment.bundle: + # categories.append('Generate bundle') + #if environment.redistributable: + # categories.append('Generate redistributable') with _CategoryProgress(categories, not args.progress) as progress_bar: project = _Project.from_directory(args.directory, environment) + progress_bar.update() project.build(args.all, args.targets, args.jobs) progress_bar.update() diff --git a/clang_build/compiler.py b/clang_build/compiler.py index c22fd79..ae4d432 100644 --- a/clang_build/compiler.py +++ b/clang_build/compiler.py @@ -161,7 +161,7 @@ def _get_driver(self, source_file): else: return [str(self.clangpp), self.max_cpp_dialect] - def compile(self, source_file, object_file, flags): + def compile(self, source_file, object_file, flags=None): """Compile a given source file into an object file. If the object file is placed into a non-existing folder, this @@ -191,7 +191,7 @@ def compile(self, source_file, object_file, flags): return self._run_clang_command( self._get_driver(source_file) + ["-c", str(source_file), "-o", str(object_file)] - + flags + + (flags if flags else []) ) def generate_dependency_file(self, source_file, dependency_file, flags): diff --git a/clang_build/directories.py b/clang_build/directories.py index 1e833ee..ca215de 100644 --- a/clang_build/directories.py +++ b/clang_build/directories.py @@ -1,4 +1,14 @@ +from copy import copy + class Directories: + + def include_public_total(self): + includes = copy(self.include_public) + for target in self.dependencies: + includes += target.directories.include_public_total() + + return includes + def __init__(self, files, dependencies): self.dependencies = dependencies @@ -6,13 +16,6 @@ def __init__(self, files, dependencies): self.include_private = files["include_directories"] self.include_public = files["include_directories_public"] - # Default include path - # if self.root_directory.joinpath('include').exists(): - # self._include_directories_public = [self.root_directory.joinpath('include')] + self._include_directories_public - - # Public include directories of dependencies are forwarded - for target in self.dependencies: - self.include_public += target.directories.include_public # Make unique and resolve self.include_private = list( @@ -23,9 +26,7 @@ def __init__(self, files, dependencies): ) def final_directories_list(self): - return list(dict.fromkeys( - self.include_private + self.include_public - )) + return list(dict.fromkeys(self.include_private + self.include_public_total())) def include_command(self): include_directories_command = [] @@ -35,5 +36,5 @@ def include_command(self): return include_directories_command def make_private_directories_public(self): - self.include_public = self.final_directories_list() - self.include_private = [] \ No newline at end of file + self.include_public = self.include_private + self.include_public + self.include_private = [] diff --git a/setup.cfg b/setup.cfg index 0446aca..0ef94bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,4 +12,7 @@ packages = clang_build [entry_points] console_scripts = - clang-build = clang_build.clang_build:_main \ No newline at end of file + clang-build = clang_build.clang_build:_main + +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py index c39069a..2c7d500 100644 --- a/setup.py +++ b/setup.py @@ -8,4 +8,5 @@ setuptools.setup( python_requires='>=3.7', setup_requires=['pbr<4'], - pbr=True) + pbr=True, + tests_require=['pytest']) diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/test_build_type.py b/test/test_build_type.py new file mode 100644 index 0000000..1ac4032 --- /dev/null +++ b/test/test_build_type.py @@ -0,0 +1,4 @@ +from clang_build.build_type import BuildType + +def test_case_insensitivity(): + assert BuildType("default") == BuildType("Default") diff --git a/test/test_circle.py b/test/test_circle.py new file mode 100644 index 0000000..02aebc1 --- /dev/null +++ b/test/test_circle.py @@ -0,0 +1,7 @@ +from clang_build.circle import Circle + +def test_string_representation(): + c = Circle(["A", "B", "C"]) + + assert str(c) == "A -> B -> C" + assert repr(c) == f'{repr("A")} -> {repr("B")} -> {repr("C")}' diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..ab988f6 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,59 @@ +import subprocess +from pathlib import Path +from shutil import rmtree + +import pytest + +from clang_build import clang_build as cli +from clang_build.build_type import BuildType + + +def cli_argument_check( + argument, argument_short, default_value, custom_parameter, expected_outcome +): + args_argument = argument.replace("-", "_") + # default check + args = cli.parse_args([]) + assert vars(args)[args_argument] == default_value + + # custom input + args = cli.parse_args([f"--{argument}"] + custom_parameter) + assert vars(args)[args_argument] == expected_outcome + + if argument_short: + args = cli.parse_args([f"-{argument_short}"] + custom_parameter) + assert vars(args)[args_argument] == expected_outcome + + +def test_cli_arguments(): + cli_argument_check("verbose", "V", False, [], True) + cli_argument_check("progress", "p", False, [], True) + cli_argument_check("directory", "d", Path(), ["my_folder"], Path("my_folder")) + cli_argument_check("build-type", "b", BuildType.Default, ["dEbUg"], BuildType.Debug) + cli_argument_check("all", "a", False, [], True) + cli_argument_check( + "targets", "t", None, ["target1", "target2"], ["target1", "target2"] + ) + with pytest.raises(SystemExit): + cli_argument_check("all", "a", False, ["--targets", "target1"], False) + cli_argument_check("force-build", "f", False, [], True) + cli_argument_check("jobs", "j", 1, ["12"], 12) + with pytest.raises(SystemExit): + cli_argument_check("jobs", "j", 1, ["0"], 0) + cli_argument_check("debug", None, False, [], True) + cli_argument_check("no-graph", None, False, [], True) + cli_argument_check("bundle", None, False, [], True) + cli_argument_check("redistributable", None, False, [], True) + + +def test_hello_world_mwe(): + try: + cli.build(cli.parse_args(["-d", "test/mwe"])) + output = ( + subprocess.check_output(["./build/default/bin/main"], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + assert output == "Hello!" + finally: + rmtree("build", ignore_errors=True) \ No newline at end of file diff --git a/test/test_compiler.py b/test/test_compiler.py new file mode 100644 index 0000000..bb3ccca --- /dev/null +++ b/test/test_compiler.py @@ -0,0 +1,87 @@ +from pathlib import Path +import subprocess + +from clang_build.compiler import Clang + + +def test_finds_clang(): + compiler = Clang() + + for exe in [compiler.clang, compiler.clangpp, compiler.clang_ar]: + subprocess.check_call([str(exe), "--version"]) + + +def create_file(content, path): + with open(path, 'w') as f: + f.write(content) + +def remove_file(path): + path.unlink(missing_ok=True) + +def remove_dir(path): + path.rmdir() + + +def test_compile_empty_source(): + compiler = Clang() + source_file = Path("empty.cpp") + object_file = Path("output.o") + try: + create_file("", source_file) + success, _ = compiler.compile(source_file, object_file) + assert object_file.exists() + assert success + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_faulty_source(): + compiler = Clang() + source_file = Path("faulty.cpp") + object_file = Path("should_not_be_here.o") + try: + create_file("{", source_file) + success, report = compiler.compile(source_file, object_file) + assert not success + assert str(source_file)+":1:1" in report + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_with_flags(): + compiler = Clang() + source_file = Path("needs_flags.cpp") + object_file = Path("should_not_be_here.o") + try: + create_file("int main(){\n#ifdef HIFLAG\n}\n#endif", source_file) + success, _ = compiler.compile(source_file, object_file) + assert not success + success, _ = compiler.compile(source_file, object_file, ["-DHIFLAG"]) + assert success + assert object_file.exists() + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_output_in_folder(): + compiler = Clang() + source_file = Path("empty.cpp") + object_file = Path("nested/folder/output.o") + try: + create_file("", source_file) + success, _ = compiler.compile(source_file, object_file) + assert success + assert object_file.exists() + finally: + remove_file(object_file) + remove_dir(object_file.parent) + remove_dir(object_file.parent.parent) + remove_file(source_file) + + +def test_link(): + assert False + + +def test_dependency_file(): + assert False \ No newline at end of file diff --git a/test/test_directories.py b/test/test_directories.py new file mode 100644 index 0000000..95e2344 --- /dev/null +++ b/test/test_directories.py @@ -0,0 +1,55 @@ +from pathlib import Path + +from clang_build.directories import Directories + +# correct order of directories +# + + +class MockDependency: + def __init__(self, directories): + self.directories = directories + + +def test_correct_order(): + d = Directories( + {"include_directories": [Path("a")], "include_directories_public": [Path("b")]}, + [ + MockDependency( + Directories( + { + "include_directories": [Path("m/a")], + "include_directories_public": [Path("m/b")], + }, + [], + ) + ) + ], + ) + + assert d.include_public == [Path("b")] + assert d.include_private == [Path("a")] + assert d.include_public_total() == [Path("b"), Path("m/b")] + assert d.include_command() == ["-I", "a", "-I", "b", "-I", str(Path("m/b"))] + assert d.final_directories_list() == [Path("a"), Path("b"), Path("m/b")] + + d.make_private_directories_public() + assert d.include_public == [Path("a"), Path("b")] + assert d.include_private == [] + + +def test_empty_dependency_list(): + d = Directories( + {"include_directories": [Path("a")], "include_directories_public": [Path("b")]}, + [], + ) + + assert d.include_public == [Path("b")] + assert d.include_private == [Path("a")] + assert d.include_public_total() == [Path("b")] + assert d.include_command() == ["-I", "a", "-I", "b"] + assert d.final_directories_list() == [Path("a"), Path("b")] + + d.make_private_directories_public() + assert d.include_public == [Path("a"), Path("b")] + assert d.include_private == [] diff --git a/test/test_environment.py b/test/test_environment.py new file mode 100644 index 0000000..a4b3c40 --- /dev/null +++ b/test/test_environment.py @@ -0,0 +1,63 @@ +from pathlib import Path + +from clang_build.environment import Environment +from clang_build.build_type import BuildType +from clang_build.compiler import Clang + + +def test_compiler(): + env = Environment({}) + assert isinstance(env.compiler, Clang) + + +def test_build_type(): + env = Environment({}) + assert env.build_type == BuildType.Default + + env = Environment({"build_type": BuildType.Release}) + assert env.build_type == BuildType.Release + + +def test_force_build(): + env = Environment({}) + assert env.force_build == False + + env = Environment({"force_build": True}) + assert env.force_build == True + + +def test_build_directory(): + env = Environment({}) + assert env.build_directory == Path("build") + + +def test_create_dependency_dotfile(): + env = Environment({}) + assert env.create_dependency_dotfile == True + + env = Environment({"no_graph": True}) + assert env.create_dependency_dotfile == False + + +def test_clone_recursive(): + env = Environment({}) + assert env.clone_recursive == True + + env = Environment({"no_recursive_clone": True}) + assert env.clone_recursive == False + + +def test_bundle(): + env = Environment({}) + assert env.bundle == False + + env = Environment({"bundle": True}) + assert env.bundle == True + + +def test_redistributable(): + env = Environment({}) + assert env.redistributable == False + + env = Environment({"redistributable": True}) + assert env.redistributable == True diff --git a/test/test.py b/test/test_projects.py similarity index 96% rename from test/test.py rename to test/test_projects.py index 6551fff..ea1c5aa 100644 --- a/test/test.py +++ b/test/test_projects.py @@ -37,16 +37,7 @@ def clang_build_try_except( args ): logger.error(printout) class TestClangBuild(unittest.TestCase): - def test_hello_world_mwe(self): - clang_build_try_except(['-d', 'test/mwe']) - - try: - output = subprocess.check_output(['./build/default/bin/main'], stderr=subprocess.STDOUT).decode('utf-8').strip() - except subprocess.CalledProcessError as e: - self.fail(f'Could not run compiled program. Message:\n{e.output}') - - self.assertEqual(output, 'Hello!') - + def test_build_types(self): for build_type in ['release', 'relwithdebinfo', 'debug', 'coverage']: clang_build_try_except(['-d', 'test/mwe', '-b', build_type])